Merge pull request #5 from ReStruct-Corporate-Advantage/v1

V1
This commit is contained in:
Mohit Nagar
2024-02-27 18:27:40 +05:30
committed by GitHub
641 changed files with 55252 additions and 4704 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
plop-templates
plopfile.cjs
.github

0
.github/workflows.yml vendored Normal file
View File

14
.gitignore vendored
View File

@@ -26,7 +26,19 @@ coverage
# Production
build
dist
lib
public
# Miscellaneous
*.local
.DS_Store
.DS_Store
analyse.html
stats.html
*.pem
*.d.ts
analyticsrc.json
Date

20
.storybook/main.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { StorybookConfig } from "@storybook/react-vite"
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
core: {
disableTelemetry: true,
},
}
export default config

15
.storybook/preview.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { Preview } from "@storybook/react"
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
}
export default preview

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# pull the Node.js Docker image as dependencies
# ---- Dependencies Node ----
FROM node:16-alpine AS build
# copy the package.json files from local machine to the workdir in container
COPY package.json .
# install node packages
RUN npm set progress=false && npm config set depth 0
RUN npm install --legacy-peer-deps
# #
# # ---- Test ----
# # run linters, setup and tests
# FROM dependencies AS test
# COPY . .
# RUN npm run lint && npm run test
# copy app sources
COPY ./src ./src
COPY tsconfig.json vite.config.ts analyticsrc.json index.html /
RUN npm run build
FROM node:16-alpine
# copy the generated modules and all other files to the container
WORKDIR /usr/src/app
RUN npm install -g serve
COPY cert.pem /etc/ssl/certificates/cert.pem
COPY chain.pem /etc/ssl/certificates/chain.pem
COPY fullchain.pem /etc/ssl/certificates/fullchain.pem
COPY privkey.pem /etc/ssl/certificates/privkey.pem
COPY --from=build build .
# our app is running on port 3000 within the container, so need to expose it
EXPOSE 3000
# the command that starts our app
CMD ["serve", "-s", "."]

View File

@@ -0,0 +1,58 @@
import fs from "fs"
import { resolve } from "path"
import { PluginOption } from "vite"
import { LibCssOptions } from "./injectLibCss.d"
let viteConfig
const injectLibCss = function (options: LibCssOptions = {}): PluginOption {
return {
name: "ar-lib-css",
apply: "build",
enforce: "post",
configResolved(resolvedConfig) {
viteConfig = resolvedConfig
},
writeBundle(option, bundle) {
if (!viteConfig.build || !viteConfig.build.lib) {
// only for lib build
console.warn("vite-plugin-libcss only works in lib mode.")
return
}
if (option.format !== "es") {
// only for es built
return
}
const files = Object.keys(bundle)
const cssFile = files.find((v) => v.endsWith(".css"))
if (!cssFile) {
return
}
for (const file of files) {
if (!(bundle[file] as any).isEntry) {
// only for entry
continue
}
if (
options.exclude &&
(options.exclude as Array<string>).findIndex((ex) =>
file.startsWith(ex + "/"),
) > -1
) {
// check if the file matches the include pattern
continue
}
const outDir = viteConfig.build.outDir || "dist"
const filePath = resolve(viteConfig.root, outDir, file)
const data = fs.readFileSync(filePath, {
encoding: "utf8",
})
fs.writeFileSync(filePath, `import './${cssFile}';\n${data}`)
}
},
}
}
export default injectLibCss

View File

@@ -1,13 +0,0 @@
#!/bin/zsh
declare -a arr=("Alert", "Badge" "Breadcrumb" "BrowserIncompatibility" "Button" "Checkbox" "ColorPicker" "ContextMenu" "DateInput" "DatePicker" "DateRange" "Calendar" "PickerRange" "DetailsPanel" "Dialog" "FacetedFilter" "InlineMenu" "LabelValue" "LearnLink" "Link" "List" "Modal" "Notification" "NumericStepper" "Pagination" "Picklist" "Pill" "Pillbox" "Popover" "ProgressIndicator" "ProgressStepper" "Radio" "SearchField" "SecondaryNavigation" "SegmentedControl" "Select" "Slider" "Splitter" "Tab" "TabBar" "Table" "Tag" "TextArea" "TextInput" "Mask" "TimeEntry" "Toggle" "Toolbar" "Tooltip" "TypeAhead" "Uploader" "Widget")
for i in "${arr[@]}"
do
npm run component "$i"
done
declare -a arr=("AboutUs" "Application" "Banner" "Benefits" "Blog" "Brands" "Breadcrumbs" "CTA" "Card" "Careers" "Contact" "Content" "Cookies" "Dashboard" "Download" "Ecomm_Orders" "Ecomm_Products" "Empty" "FAQ" "Features" "Footer" "Form" "Gallery" "GraphTiles" "Graph" "HTTPCode" "Hero" "HowItWorks" "InstaPhotos" "Integrations" "LogoClouds" "Newsletter" "Notifications" "Portfolio" "Pricing" "ProductInfo" "Projects" "Reviews" "RichText" "Search" "Services" "SignInUp" "Snackbar" "Stats" "Steps" "Team" "Testimonials" "Toast" "Users")
for i in "${arr[@]}"
do
npm run molecule "$i"
done

View File

@@ -4,11 +4,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Redux App</title>
<title>Armory React Repository</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div class="theme-light" id="root"></div>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

29149
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,15 @@
{
"name": "@armco/armory-react-components",
"description": "React Component Library for Armco's stack of products and services",
"version": "0.0.8",
"version": "0.0.28",
"type": "module",
"author": "Armco (@restruct-corporate-advantage)",
"scripts": {
"dev": "vite",
"start": "vite",
"build": "tsc --project ./tsconfig-vite.json && vite build",
"build:publish": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc",
"build:cjs": "tsc --module commonjs --outDir dist/cjs",
"start": "NODE_ENV=production vite",
"build": "tsc && vite build",
"build:publish": "./scripts/build.sh",
"build:publish:compile": "tsc --p ./tsconfig-build.json && vite build --config vite-publish.config.ts",
"generate": "plop",
"atom": "plop atom",
"molecule": "plop molecule",
@@ -22,47 +21,93 @@
"lint": "eslint .",
"type-check": "tsc",
"publish:dry": "npm publish --dry-run",
"publish:public": "npm publish --access public",
"prepublishOnly": "npm run build:publish && npm --no-git-tag-version version patch"
"publish:local": "./scripts/publish-local.sh",
"publish:public": "./scripts/publish.sh",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@armco/armory-react-components": "^0.0.8",
"@armco/analytics": "^0.2.5",
"@armco/svg-canvas": "^0.1.3",
"@lottiefiles/react-lottie-player": "^3.5.3",
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^1.8.1",
"@storybook/cli": "^7.0.23",
"bootstrap": "^5.3.0",
"react": "^18.2.0",
"classnames": "^2.3.2",
"d3": "^7.8.5",
"highcharts": "^11.2.0",
"highlight.js": "^11.8.0",
"js-cookie": "^3.0.5",
"lottie-react": "^2.4.0",
"lottie-web": "^5.12.2",
"moment": "^2.29.4",
"react-app-polyfill": "^3.0.0",
"react-bootstrap": "^2.7.4",
"react-dev-utils": "^12.0.1",
"react-dom": "^18.2.0",
"react-redux": "^8.0.1",
"react-router-dom": "^6.13.0"
"react-router-dom": "^6.13.0",
"react-table": "^7.8.0",
"svgpath": "^2.6.0",
"uuid": "^9.0.0",
"vite-plugin-svgr": "^3.2.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.0.23",
"@storybook/addon-interactions": "^7.0.23",
"@storybook/addon-links": "^7.0.23",
"@storybook/blocks": "^7.0.23",
"@storybook/react": "^7.0.23",
"@storybook/react-vite": "^7.0.23",
"@storybook/testing-library": "^0.0.14-next.2",
"@testing-library/dom": "^9.2.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.2.5",
"@types/bootstrap": "^5.2.6",
"@types/d3": "^7.4.0",
"@types/js-cookie": "^3.0.3",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-dom": "^18.2.18",
"@types/react-table": "^7.7.19",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react": "^4.0.0",
"chalk": "^5.3.0",
"cherry-pick": "^0.5.0",
"cpy-cli": "^5.0.0",
"eslint": "^8.0.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-storybook": "^0.6.12",
"execa": "^8.0.1",
"fs-extra": "^11.2.0",
"glob": "^10.3.10",
"jsdom": "^21.1.0",
"plop": "^3.1.2",
"prettier": "^2.7.1",
"prettier-config-nick": "^1.0.2",
"prop-types": "^15.8.1",
"react": ">=16.8.0",
"react-dom": "^18.2.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.63.4",
"storybook": "^7.0.23",
"typescript": "^5.0.2",
"vite": "^4.0.0",
"vite-plugin-dts": "^3.7.1",
"vitest": "^0.30.1"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-redux": "^8.0.1",
"react-router-dom": "^6.13.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
"react-app/jest",
"plugin:storybook/recommended"
],
"plugins": [
"prettier"
@@ -73,7 +118,9 @@
}
},
"prettier": "prettier-config-nick",
"main": "index.tsx",
"types": "./build/index.d.ts",
"main": "./build/index.js",
"module": "./build/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/ReStruct-Corporate-Advantage/armory-react-components.git"
@@ -89,14 +136,11 @@
"foundation"
],
"files": [
"dist"
"build"
],
"license": "ISC",
"bugs": {
"url": "https://github.com/ReStruct-Corporate-Advantage/armory-react-components/issues"
},
"homepage": "https://github.com/ReStruct-Corporate-Advantage/armory-react-components#readme",
"peerDependencies": {
"react": ">=16"
}
"homepage": "https://github.com/ReStruct-Corporate-Advantage/armory-react-components#readme"
}

View File

@@ -1,3 +1,3 @@
.c-{{pascalCase name}} {
.ar-{{pascalCase name}} {
}

View File

@@ -1,9 +1,8 @@
import { {{pascalCase name}}Props } from ".."
import "./{{pascalCase name}}.component.scss"
interface {{pascalCase name}}Props {}
const {{pascalCase name}} = (props: {{pascalCase name}}Props): JSX.Element => {
return <div className="c-{{pascalCase name}}">In Component {{pascalCase name}}</div>
return <div className="ar-{{pascalCase name}}">In Component {{pascalCase name}}</div>
}
export default {{pascalCase name}}

View File

@@ -1,3 +1,3 @@
.c-{{pascalCase name}} {
.ar-{{pascalCase name}} {
}

View File

@@ -4,8 +4,8 @@ export interface {{pascalCase name}}State {}
const initialState: {{pascalCase name}}State = {}
export const {{snakeCase name}}Slice = createSlice({
name: "{{snakeCase name}}",
export const {{camelCase name}}Slice = createSlice({
name: "{{camelCase name}}",
initialState,
reducers: {
increment: (state) => {},
@@ -13,6 +13,6 @@ export const {{snakeCase name}}Slice = createSlice({
extraReducers: (builder) => {},
})
export const { increment } = {{snakeCase name}}Slice.actions
export const { increment } = {{camelCase name}}Slice.actions
export default {{snakeCase name}}Slice.reducer
export default {{camelCase name}}Slice.reducer

View File

@@ -3,7 +3,7 @@ import "./{{pascalCase name}}.page.scss"
interface {{pascalCase name}}Props {}
const {{pascalCase name}} = (props: {{pascalCase name}}Props): JSX.Element => {
return <div className="c-{{pascalCase name}}">In Page {{pascalCase name}}</div>
return <div className="ar-{{pascalCase name}}">In Page {{pascalCase name}}</div>
}
export default {{pascalCase name}}

View File

@@ -0,0 +1,2 @@
/* PLOP_INJECT_INTERFACE */

View File

@@ -47,6 +47,12 @@ module.exports = (plop) => {
pattern: `/* PLOP_INJECT_EXPORT */`,
template: `\t{{pascalCase name}},`,
},
{
type: "append",
path: "src/app/types/components.interface.ts",
pattern: `/* PLOP_INJECT_INTERFACE */`,
template: `export interface {{pascalCase name}}Props extends BaseProps {\n}\n`,
},
],
})
plop.setGenerator("atom", {
@@ -97,6 +103,12 @@ module.exports = (plop) => {
pattern: `/* PLOP_INJECT_EXPORT */`,
template: `\t{{pascalCase name}},`,
},
{
type: "append",
path: "src/app/types/components.interface.ts",
pattern: `/* PLOP_INJECT_INTERFACE */`,
template: `export interface {{pascalCase name}}Props extends BaseProps {\n}\n`,
},
],
})
plop.setGenerator("molecule", {
@@ -147,6 +159,12 @@ module.exports = (plop) => {
pattern: `/* PLOP_INJECT_EXPORT */`,
template: `\t{{pascalCase name}},`,
},
{
type: "append",
path: "src/app/types/components.interface.ts",
pattern: `/* PLOP_INJECT_INTERFACE */`,
template: `export interface {{pascalCase name}}Props extends BaseProps {\n}\n`,
},
],
})
plop.setGenerator("page", {

131
scripts/build.sh Executable file
View File

@@ -0,0 +1,131 @@
#!/bin/sh
copy_files() {
local source_dir="$1"
local destination_dir="$2"
local files_to_copy=".tsx|.scss"
echo "Copying files from $source_dir to $destination_dir"
find "$source_dir" -type f \( -name "*.tsx" -o -name "*.scss" \) | while read -r file; do
# Check if the file has one of the specified extensions
for ext in $(echo "$files_to_copy" | tr '|' '\n'); do
if echo "$file" | grep -q "$ext$"; then
destination_file="$destination_dir/$(basename "$file")"
# Create destination directory if it doesn't exist
mkdir -p "$(dirname "$destination_file")"
# echo "Copying $file to $destination_file"
cp "$file" "$destination_file"
echo "Copied $file to $destination_file"
break # Exit the loop after finding a match
fi
done
done
}
search_replace_in_files() {
local directory_or_file="$1"
local search_string="$2"
local replace_string="$3"
local -a dir_names=("${@:4}") # Accept array of directory names as arguments
echo "Initiate search and replace in files $search_string $replace_string"
# Step 1: Check if the argument is a file
if [ -f "$directory_or_file" ]; then
# Process the single file
perl -pi -e "s|$search_string|$replace_string|g" "$directory_or_file"
else
# Step 2: Get all files recursively matching the condition (only .tsx and .scss)
files=$(find "$directory_or_file" -type f \( -name "*.tsx" -o -name "*.scss" -o -name "*.ts" \))
# Step 3: Iterate over each file and perform search and replace
for file in $files; do
# Step 4: Iterate over each directory name in the array
for dir_name in "${dir_names[@]}"; do
# Step 5: Check if the file name starts with any of the specified directories
if [[ "$file" == *"/$dir_name/"* ]]; then
echo "Skipping file $file as it starts with '$dir_name/'"
continue 2 # Continue to the next iteration of the outer loop
fi
done
# Step 6: Perform search and replace in each file using perl
perl -pi -e "s|$search_string|$replace_string|g" "$file"
done
fi
echo "Search and replace in TypeScript and SCSS files completed."
}
# Function to split the string into an array based on a delimiter
split_string() {
local string="$1"
local delimiter="$2"
IFS="$delimiter" read -r -a array <<< "$string"
echo "${array[@]}"
}
rm -rf ./lib
mkdir -p "./lib"
file_types=("types" "utils" "config" "static" "store" "hooks")
ignore_dirs=("utils config static" "types" "utils" "utils" "" "")
copy_files "src/app/components/atoms" "lib"
copy_files "src/app/components/molecules" "lib"
cp src/app/components/atoms/index.tsx lib/atoms.ts
cp src/app/components/molecules/index.tsx lib/molecules.ts
cp src/app/components/components.ts lib/index.ts
cp -r src/app/types lib
cp -r src/app/utils lib
cp -r src/app/config lib
cp -r src/app/static lib
rm -rf lib/static/styles/*
cp src/app/static/styles/_mixins.scss lib/static/styles/mixins.scss
cp src/app/static/styles/_variables.scss lib/static/styles/variables.scss
cp src/app/static/styles/index.scss lib/static/styles/index.scss
cp src/app/hooks.ts "lib"
cp -r src/app/hooks "lib"
cp src/react-app-env.d.ts lib
cp src/vite-env.d.ts lib
cp package.json lib
# cp src/app/components/atoms/Calendar/JustCalendar.tsx "lib"
# cp src/app/components/atoms/Calendar/MonthSelector.tsx "lib"
# cp src/app/components/atoms/Calendar/MonthSelector.component.scss "lib"
# cp src/app/components/atoms/Calendar/JustCalendar.component.scss "lib"
# cp src/app/components/atoms/Calendar/EventForm.tsx "lib"
# cp src/app/components/atoms/Calendar/MonthNavigator.tsx "lib"
# cp src/app/components/atoms/Calendar/MonthNavigator.component.scss "lib"
cp src/app/components/atoms/Calendar/helper.ts "lib"
# cp src/app/components/molecules/Carousel/Thumbs.tsx "lib"
cp src/app/components/molecules/Carousel/cssClasses.ts "lib"
cp src/app/components/molecules/Carousel/animations.ts "lib"
cp src/app/store.ts "lib"
for i in $(seq 0 $((${#file_types[@]} - 1))); do
echo ${file_types[i]} ${ignore_dirs[i]}
ignore_dirs_array=($(split_string "${ignore_dirs[i]}" " "))
search_replace_in_files "lib" "\.\./\.\./\.\./${file_types[i]}" "./${file_types[i]}" "${ignore_dirs_array[@]}"
search_replace_in_files "lib" "\.\./\.\./${file_types[i]}" "./${file_types[i]}" "${ignore_dirs_array[@]}"
search_replace_in_files "lib" "\.\./${file_types[i]}" "./${file_types[i]}" "${ignore_dirs_array[@]}"
done
search_replace_in_files "lib" "\.\./components/molecules" ".."
search_replace_in_files "lib" "\.\./components/atoms" ".."
search_replace_in_files "lib/types/types.ts" "Carousel/Carousel" "Carousel"
search_replace_in_files "lib/DatePicker.tsx" "\.\./Calendar/helper" "./helper" "types"
search_replace_in_files "lib/DateRangePicker.tsx" "\.\./Calendar/helper" "./helper" "types"
search_replace_in_files "lib/package.json" "\./build/index" "index"
search_replace_in_files "lib/types/components.interface.ts" "\.\./Calendar/helper" "../helper"
search_replace_in_files "lib" "\"\.\./\.\.\"" "\"..\""
search_replace_in_files "lib/index.ts" "\.\./static/styles" "./static/styles"
search_replace_in_files "lib/atoms.ts" "\./SegmentedControl/Segment" "./Segment"
sed '/componentsViewerPage/d' lib/store.ts > temp_file && mv temp_file lib/store.ts
sed '/iconsPage/d' lib/store.ts > temp_file && mv temp_file lib/store.ts
sed '/iconPage/d' lib/store.ts > temp_file && mv temp_file lib/store.ts
sed '/tasksPage/d' lib/store.ts > temp_file && mv temp_file lib/store.ts
search_replace_in_files "lib" "\"\.\.\"" "\"\.\""
rm -rf build
npm run build:publish:compile

13
scripts/bulkgen.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/zsh
declare -a arr=("Alert", "Badge" "Breadcrumb" "BrowserIncompatibility" "Button" "Checkbox" "ColorPicker" "ContextMenu" "DateInput" "DatePicker" "Date" "Calendar" "PickerRange" "DetailsPanel" "Dialog" "FacetedFilter" "Icon" "InlineMenu" "LabelValue" "LearnLink" "Link" "List" "Modal" "Notification" "NumericStepper" "Pagination" "Picklist" "Pill" "Pillbox" "Popover" "ProgressIndicator" "ProgressStepper" "Radio" "SearchField" "SecondaryNavigation" "SegmentedControl" "Select" "Slider" "Splitter" "Tab" "TabBar" "Table" "Tag" "TextArea" "TextInput" "Mask" "TimeEntry" "Toggle" "Toolbar" "Tooltip" "TypeAhead" "Uploader" "Widget")
for i in "${arr[@]}"
do
npm run atom "$i"
done
declare -a arr=("AboutUs" "Application" "Carousel" "Banner" "Benefits" "Blog" "Brands" "Breadcrumbs" "CTA" "Card" "Careers" "Contact" "Content" "Cookies" "Dashboard" "Download" "Ecomm_Orders" "Ecomm_Products" "Empty" "FAQ" "Features" "Footer" "Form" "Gallery" "GraphTiles" "Graph" "HTTPCode" "Hero" "HowItWorks" "InstaPhotos" "Integrations" "LogoClouds" "Newsletter" "Notifications" "Portfolio" "Pricing" "ProductInfo" "Projects" "Reviews" "RichText" "Search" "Services" "SignInUp" "Snackbar" "Stats" "Steps" "Swiper" "Team" "Testimonials" "Toast" "Users")
for i in "${arr[@]}"
do
npm run molecule "$i"
done

5
scripts/publish-local.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
source ./scripts/build.sh
npm pack --pack-destination ~/__Projects__/Common

8
scripts/publish.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
semver=${1:-patch}
set -e
source ./scripts/build.sh
npm --no-git-tag-version version ${semver}
npm publish --access public

View File

@@ -14,7 +14,6 @@ class ErrorBoundary extends Component<Props, State> {
}
public static getDerivedStateFromError(_: Error): State {
// Update state so the next render will show the fallback UI.
return { hasError: true }
}

View File

@@ -1,12 +1,82 @@
import { Suspense, useEffect } from "react"
import { useRoutes } from "react-router-dom"
import * as pages from "./pages"
import { v4 as uuid } from "uuid"
import { AlertProps } from "./types/components.interface"
import Header from "./components/Header"
import { useAppDispatch, useAppSelector } from "./hooks"
import {
notification,
notify,
setLoggedIn,
setRightPanelContent,
setUser,
} from "./store"
import { Alert } from "./components"
import { ArAlertType } from "./types/enums"
import Helper from "./utils/helper"
import { Network } from "./utils"
import ROUTES from "./routes"
import { ENDPOINTS } from "./config/constants"
import API_CONFIG from "./config/api-config"
Helper.populateComponentsInRoutes(ROUTES, pages)
Helper.populatePagesInRoutes(ROUTES)
interface RouterProps {}
const Router = (props: RouterProps) => useRoutes(ROUTES)
const Router = (props: RouterProps) => {
const alertInfo = useAppSelector<AlertProps | undefined>(notification)
const dispatch = useAppDispatch()
useEffect(() => {
document
.getElementsByTagName("html")[0]
.setAttribute("ar-theme", "th-light-1")
Network.get(
API_CONFIG.IAM[process.env.NODE_ENV] +
ENDPOINTS.USERS.ROOT +
ENDPOINTS.USERS.CHECK,
)
.then((response) => {
if (response && response.status === 200) {
dispatch(setLoggedIn(true))
if (response.body) {
dispatch(setUser(response.body))
}
} else {
dispatch(setLoggedIn(false))
}
})
.catch((error) => {
if (error.status === 403) {
dispatch(setLoggedIn(false))
}
})
window.onmessage = (e) => {
if (e.data && e.data.event === "LOGGED_IN") {
dispatch(setLoggedIn(true))
dispatch(
notify({
show: true,
message: "Logged in successfully!",
type: ArAlertType.SUCCESS,
uid: uuid(),
}),
)
dispatch(setRightPanelContent({ name: "" }))
dispatch(setUser(e.data.data.body.user))
}
}
}, [])
const matchedRouteElement = useRoutes(ROUTES)
return (
<Suspense fallback={<div>Loading...</div>}>
<Header routeDetails={matchedRouteElement?.props.match} />
{matchedRouteElement}
{alertInfo && <Alert {...alertInfo} />}
</Suspense>
)
}
export default Router

View File

@@ -0,0 +1,3 @@
.ar-AssetSelector {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import AssetSelector from "./AssetSelector"
describe("AssetSelector", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,13 @@
import { AssetSelectorProps } from "../../types/components.interface"
import IconsList from "../IconsList"
import "./AssetSelector.component.scss"
const AssetSelector = (props: AssetSelectorProps): JSX.Element => {
return (
<div className="ar-AssetSelector">
<IconsList variant="compact" />
</div>
)
}
export default AssetSelector

View File

@@ -0,0 +1,3 @@
import AssetSelector from "./AssetSelector"
export default AssetSelector

View File

@@ -1,3 +1,3 @@
.c-ComponentList {
.ar-ComponentList {
}

View File

@@ -1,18 +1,35 @@
import { useNavigate } from "react-router-dom"
import { useAppSelector } from "../../hooks"
import { getCurrentTheme } from "../../store"
import { TreeList } from ".."
import { TreeListData } from "../../types/entity.interface"
import Adapter from "../../utils/adapters"
import Network from "../../utils/network"
import COMPONENTS from "../../config/components"
import "./ComponentList.component.scss"
interface ComponentListProps {}
const formattedTreeData = Adapter.adaptToTree(COMPONENTS)
const ComponentList = (props: ComponentListProps): JSX.Element => {
const formattedTreeData = Adapter.adaptToTree(COMPONENTS)
const navigate = useNavigate()
const theme = useAppSelector<string>(getCurrentTheme)
const handleComponentSelect = (treeNode: TreeListData) => {
const params = treeNode.data?.props
treeNode.data?.component &&
navigate(
Network.stringifyUrl(`/components/${treeNode.data?.component}`, params),
)
}
return (
<div className="c-ComponentList h-100 w-100 overflow-auto">
<div className="ar-ComponentList h-100 w-100 mx-2">
<TreeList
onItemSelect={handleComponentSelect}
data={formattedTreeData}
title="Component List"
firstExpanded={true}
theme={theme}
/>
</div>
)

View File

@@ -0,0 +1,3 @@
.ar-ConfigRowItem {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigRowItem from "./ConfigRowItem"
describe("ConfigRowItem", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,98 @@
import { ChangeEvent, useState } from "react"
import { ConfigRowItemProps } from "../../types/components.interface"
import { Button, LoadableIcon, TextInput } from ".."
import "./ConfigRowItem.component.scss"
import { ArButtonVariants } from "../../types/enums"
const ConfigRowItem = (props: ConfigRowItemProps): JSX.Element => {
const { config, disabled, isNew, onAdd, onUpdate, onDelete } = props
const [key, setKey] = useState<string>(config?.key || "")
const [value, setValue] = useState<string>(config?.value || "")
const [edited, setEdited] = useState<boolean>()
const configIsSubmittable = (isNew || edited) && key && value
return (
<div className="ar-ConfigRowItem row">
<div className="col-3">
<TextInput
value={key}
disabled={disabled && !edited}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setKey(e.target.value)
}
placeholder="Config key"
/>
</div>
<div className="col-4">
<TextInput
placeholder="Enter a value, text/json etc."
disabled={disabled && !edited}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
value={value}
/>
</div>
{isNew ? (
<div className="col-1 flex-v-center">
<Button
content="Add"
variant={ArButtonVariants.SUCCESS}
preIcon="io/IoMdAdd"
onClick={() => {
configIsSubmittable && onAdd && onAdd(key, value)
setKey("")
setValue("")
}}
/>
</div>
) : (
<div className="col-2 flex-v-center">
<span
className={`me-3${
configIsSubmittable ? " cursor-pointer" : " pe-none"
}`}
>
<LoadableIcon
icon="fa/FaCheck"
color={configIsSubmittable ? "green" : "rgba(0, 128, 0, 0.3)"}
onClick={() => {
config?._id &&
configIsSubmittable &&
onUpdate &&
onUpdate(config?._id, key, value)
setKey("")
setValue("")
}}
hoverShadow
/>
</span>
<span className={`me-3${edited ? " pe-none" : " cursor-pointer"}`}>
<LoadableIcon
icon={edited ? "rx/RxCross2" : "md/MdModeEditOutline"}
color={isNew || edited ? "rgba(165, 42, 42, 0.3)" : "brown"}
onClick={() => (edited ? setEdited(false) : setEdited(true))}
/>
</span>
<span className={`me-3 ${edited ? "pe-none" : "cursor-pointer"}`}>
<LoadableIcon
icon="md/MdAdd"
width="1.3rem"
color={!configIsSubmittable ? "green" : "rgba(0, 128, 0, 0.3)"}
onClick={() => onDelete && onDelete(config?._id)}
/>
</span>
<span className={edited ? "pe-none" : "cursor-pointer"}>
<LoadableIcon
icon="ri/RiDeleteBin6Line"
color={isNew || edited ? "rgba(128, 0, 0, 0.3)" : "red"}
onClick={() => onDelete && onDelete(config?._id)}
/>
</span>
</div>
)}
</div>
)
}
export default ConfigRowItem

View File

@@ -0,0 +1,3 @@
import ConfigRowItem from "./ConfigRowItem"
export default ConfigRowItem

View File

@@ -0,0 +1,3 @@
.ar-ConfigurationList {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationList from "./ConfigurationList"
describe("ConfigurationList", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,30 @@
import { useNavigate } from "react-router-dom"
import { TreeList } from ".."
import { ConfigurationListProps } from "../../types/components.interface"
import { TreeListData } from "../../types/entity.interface"
import { Network } from "../../utils"
import "./ConfigurationList.component.scss"
const ConfigurationList = (props: ConfigurationListProps): JSX.Element => {
const { list } = props
const navigate = useNavigate()
const handleComponentSelect = (treeNode: TreeListData) => {
const params = treeNode.data?.props
treeNode.data?.component &&
navigate(
Network.stringifyUrl(`/components/${treeNode.data?.component}`, params),
)
}
return (
<div className="ar-ConfigurationList w-100 p-2">
<TreeList
onItemSelect={handleComponentSelect}
data={list || []}
title="Configurations"
firstExpanded={true}
/>
</div>
)
}
export default ConfigurationList

View File

@@ -0,0 +1,3 @@
import ConfigurationList from "./ConfigurationList"
export default ConfigurationList

View File

@@ -0,0 +1,10 @@
.ar-ConfigurationLoginPrompt {
width: 50vw;
background-color: var(--ar-bg);
border-radius: 0.25rem;
small {
color: var(--ar-color-secondary);
line-height: 1rem;
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationLoginPrompt from "./ConfigurationLoginPrompt"
describe("ConfigurationLoginPrompt", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,51 @@
import { setRightPanelContent } from "../../store"
import { useAppDispatch } from "../../hooks"
import { Button, LoadableIcon } from ".."
import { ConfigurationLoginPromptProps } from "../../types/components.interface"
import { ArButtonVariants } from "../../types/enums"
import "./ConfigurationLoginPrompt.component.scss"
const ConfigurationLoginPrompt = (
props: ConfigurationLoginPromptProps,
): JSX.Element => {
const dispatch = useAppDispatch()
return (
<div className="ar-ConfigurationLoginPrompt p-3 border">
<div className="flex-h-center flex-column">
<h6 className="flex-v-center" style={{ color: "#ffbf00" }}>
<LoadableIcon
classes="me-2"
icon="fa/FaExclamationTriangle"
color="#ffbf00"
/>
Please login to continue
</h6>
<small>
This is the configurations feature where you can create and save
configurations as key-value pairs.
</small>
<small>
These key-value pairs can then be accessed using an endpoint inside
your application.
</small>
<small className="mb-3">
<strong>
In order to be able to save and secure these configurations, you
need to create an account, if one doesn't exist already and login
using the same. Please use the button below to continue.
</strong>
</small>
</div>
<Button
content="Login"
variant={ArButtonVariants.SUCCESS}
postIcon="io5/IoArrowForwardCircle"
onClick={() => {
dispatch(setRightPanelContent({ name: "LoginProvider" }))
}}
/>
</div>
)
}
export default ConfigurationLoginPrompt

View File

@@ -0,0 +1,3 @@
import ConfigurationLoginPrompt from "./ConfigurationLoginPrompt"
export default ConfigurationLoginPrompt

View File

@@ -0,0 +1,3 @@
.ar-ConfigurationNoConfigPrompt {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationNoConfigPrompt from "./ConfigurationNoConfigPrompt"
describe("ConfigurationNoConfigPrompt", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,95 @@
import { ReactNode, useState } from "react"
import { setModalState } from "../../store"
import { useAppDispatch } from "../../hooks"
import { Button, LoadableIcon, NamespaceOrgForm, Popover } from ".."
import { ConfigurationNoConfigPromptProps } from "../../types/components.interface"
import {
ArButtonVariants,
ArPopoverSlots,
ArPopoverTriggers,
} from "../../types/enums"
import "./ConfigurationNoConfigPrompt.component.scss"
const ConfigurationNoConfigPrompt = (
props: ConfigurationNoConfigPromptProps,
): ReactNode => {
const dispatch = useAppDispatch()
return (
<div className="ar-ConfigurationNoConfigPrompt flex-center flex-column border px-3 py-5 w-50">
<LoadableIcon
classes="mb-4"
icon="gr/GrConfigure"
size="7rem"
color="lightgrey"
/>
<p className="ar-ConfigurationNoConfigPrompt__message mx-5">
You don't have any configurations created yet, you can start by
selecting one of the options below
</p>
<div className="ar-ConfigurationNoConfigPrompt__buttons d-flex">
<Popover trigger={ArPopoverTriggers.HOVER}>
<Button
classes="me-3"
content="+ Space"
variant={ArButtonVariants.SUCCESS}
slot={ArPopoverSlots.ANCHOR}
onClick={() =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceOrgForm context="namespace" />,
}),
)
}
/>
<div
slot={ArPopoverSlots.POPOVER}
className="text-wrap"
style={{ maxWidth: "10rem" }}
>
<p>This is where you keep your configurations.</p>
<p className="mb-3">
Use namespaces in your project/s to avoid fetching all the
configurations for multiple projects.
</p>
<b>Namespace is required to create a configuration</b>
</div>
</Popover>
<Popover trigger={ArPopoverTriggers.HOVER}>
<Button
variant={ArButtonVariants.SUCCESS}
content="+ Organization"
slot={ArPopoverSlots.ANCHOR}
onClick={() =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceOrgForm context="org" />,
}),
)
}
/>
<div
slot={ArPopoverSlots.POPOVER}
className="text-wrap"
style={{ maxWidth: "10rem" }}
>
<p>
You may skip creating an organization as one will be created for
you if you don't.
</p>
<p>
Or you may choose to create one. This will allow you to control
severl aspects of an organization at creation time itself
</p>
</div>
</Popover>
</div>
</div>
)
}
export default ConfigurationNoConfigPrompt

View File

@@ -0,0 +1,3 @@
import ConfigurationNoConfigPrompt from "./ConfigurationNoConfigPrompt"
export default ConfigurationNoConfigPrompt

View File

@@ -0,0 +1,13 @@
.ar-ConfigurationViewer {
background-color: var(--ar-bg);
.ar-ConfigurationViewer__header {
background-color: var(--ar-bg-base);
border-bottom: 1px solid var(--ar-color-layout-border);
}
input:disabled {
border: none;
background: transparent;
font-weight: bold;
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationViewer from "./ConfigurationViewer"
describe("ConfigurationViewer", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,205 @@
import { v4 as uuid } from "uuid"
import { useAppDispatch } from "../../hooks"
import { notify, setModalState } from "../../store"
import {
Breadcrumb,
Button,
ConfigRowItem,
InlineMenu,
LoadableIcon,
NamespaceInfoBox,
NamespaceOrgForm,
Popover,
} from ".."
import {
ArAlertType,
ArButtonVariants,
ArPopoverPositions,
ArPopoverSlots,
ArSizes,
} from "../../types/enums"
import { ObjectType } from "../../types/types"
import { ConfigurationViewerProps } from "../../types/components.interface"
import { Network } from "../../utils"
import API_CONFIG from "../../config/api-config"
import { ENDPOINTS } from "../../config/constants"
import "./ConfigurationViewer.component.scss"
const ConfigurationViewer = (props: ConfigurationViewerProps): JSX.Element => {
const { namespace, fetchNamespaces } = props
const configurations = namespace?.configs
const dispatch = useAppDispatch()
const addConfig = (key: string, value: string, _id?: string) => {
const payload: ObjectType = {
key,
value,
namespace: namespace._id,
version: "v1",
}
_id && (payload._id = _id)
Network.post(
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.CONFIG.ROOT +
ENDPOINTS.CONFIG.SAVE +
namespace._id,
payload,
)
.then((res) => {
if (res.status === 200) {
fetchNamespaces()
dispatch(
notify({
show: true,
message: "Added a new config successfully",
type: ArAlertType.SUCCESS,
uid: uuid(),
}),
)
}
})
.catch((e) => {
dispatch(
notify({
show: true,
message: "Failed to add new config",
type: ArAlertType.ERROR,
uid: uuid(),
}),
)
})
}
const deleteConfig = (_id?: string) => {
Network.get(
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.CONFIG.ROOT +
ENDPOINTS.CONFIG.DELETE +
"/" +
namespace._id +
"/" +
_id,
)
.then((res) => {
if (res.status === 200) {
fetchNamespaces()
dispatch(
notify({
show: true,
message: "Removed config successfully",
type: ArAlertType.SUCCESS,
uid: uuid(),
}),
)
}
})
.catch((e) => {
dispatch(
notify({
show: true,
message: "Failed to delete config",
type: ArAlertType.ERROR,
uid: uuid(),
}),
)
})
}
return (
<div className="ar-ConfigurationViewer d-flex flex-column w-100 h-100">
<div className="ar-ConfigurationViewer__header px-3 py-2 w-100">
<Breadcrumb
classes="d-inline-block"
data={[{ label: namespace.name, route: `/config/${namespace.name}` }]}
/>
<Button
classes="float-end"
content="Edit"
color="brown"
preIcon="fa/FaRegEdit"
variant={ArButtonVariants.LINK}
size={ArSizes.SMALL}
/>
<Popover
classes="float-end"
// trigger={ArPopoverTriggers.HOVER}
position={ArPopoverPositions.BOTTOMLEFT}
>
<Button
content="Add"
color="green"
preIcon="md/MdAddCircleOutline"
variant={ArButtonVariants.LINK}
size={ArSizes.SMALL}
slot={ArPopoverSlots.ANCHOR}
/>
<InlineMenu
slot={ArPopoverSlots.POPOVER}
data={{
Org: {},
Namespace: {
onClick: () =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceOrgForm context="namespace" />,
}),
),
},
}}
/>
</Popover>
<LoadableIcon
classes="cursor-pointer"
icon="io/IoMdInformationCircle"
color="#0d6efd"
onClick={() =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceInfoBox namespace={namespace} />,
}),
)
}
/>
</div>
<div className="ar-ConfigurationViewer__content flex-1 d-flex px-3 py-2 w-100">
<div className="d-flex flex-1 flex-column">
<ConfigRowItem onAdd={addConfig} isNew />
<div className="my-3 border" />
{configurations && configurations.length > 0 ? (
configurations.map((configuration) => {
return (
<ConfigRowItem
onUpdate={(_id: string, key: string, value: string) =>
addConfig(key, value, _id)
}
onDelete={deleteConfig}
config={configuration}
disabled
/>
)
})
) : (
<div className="row flex-1">
<div className="col flex-center fw-bold">
Nothing here, start by adding configurations above
</div>
</div>
)}
</div>
</div>
<div className="ar-ConfigurationViewer__footer px-3 py-2 w-100">
<Button
classes="float-end"
content="Submit"
variant={ArButtonVariants.SUCCESS}
size={ArSizes.SMALL}
/>
</div>
</div>
)
}
export default ConfigurationViewer

View File

@@ -0,0 +1,3 @@
import ConfigurationViewer from "./ConfigurationViewer"
export default ConfigurationViewer

View File

@@ -1,5 +1,3 @@
@import "../../static/styles/variables";
.c-Content {
background-color: var(--ar-bg-base);
.ar-Content {
background-color: var(--ar-bg-secondary);
}

View File

@@ -1,15 +1,17 @@
import React from "react"
import { ReactNode } from "react"
import "./Content.component.scss"
interface ContentProps {
children?: JSX.Element | Array<JSX.Element>
children?: ReactNode
classes?: string
}
const Content = (props: ContentProps): JSX.Element => {
const { children, classes } = props
return (
<div className={`c-Content${classes ? " " + classes : ""}`}>{children}</div>
<div className={`ar-Content flex-grow-1${classes ? " " + classes : ""}`}>
{children}
</div>
)
}

View File

@@ -1,3 +1,38 @@
.c-Drawer {
border-right: 1px solid #eee;
.ar-Drawer {
border-right: 1px solid var(--ar-color-layout-border);
&.collapsed {
.ar-Drawer__expander {
right: 1rem;
}
}
.ar-Drawer__expander {
top: 4px;
right: 6px;
}
/* width */
&::-webkit-scrollbar {
width: 4px;
}
/* Track */
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: #bbb;
border-radius: 2px;
&:hover {
background: #888;
}
}
& + .ar-Content {
width: 85%;
}
&:focus-visible {
outline: none;
}
}

View File

@@ -1,17 +1,65 @@
import ComponentList from "../ComponentList/ComponentList"
import { useEffect, useRef } from "react"
import { useAppDispatch, useAppSelector } from "../../hooks"
import { getCurrentTheme, getDrawerState, setDrawerState } from "../../store"
import LoadableIcon from "../atoms/LoadableIcon"
import { DrawerProps } from "../../types/components.interface"
import { ArThemes } from "../../types/enums"
import { Helper } from "../../utils"
import "./Drawer.component.scss"
interface DrawerProps {
children?: JSX.Element | Array<JSX.Element>
classes?: string
}
const isMobile = Helper.isMobile()
let clickedSelf: boolean
const Drawer = (props: DrawerProps): JSX.Element => {
const { children, classes } = props
const { children, classes, isCollapsible } = props
const drawerRef = useRef<HTMLDivElement>(null)
const dispatch = useAppDispatch()
const drawerState = useAppSelector<DrawerProps | undefined>(getDrawerState)
const theme = useAppSelector<string>(getCurrentTheme)
useEffect(() => {
if (isMobile) {
if (!drawerState?.collapsed) {
drawerRef.current?.focus()
}
}
}, [drawerState])
return (
<aside className={`c-Drawer${classes ? " " + classes : ""}`}>
{children}
<aside
className={`ar-Drawer overflow-auto${classes ? " " + classes : ""}${
isCollapsible ? " position-relative" : ""
}${drawerState?.collapsed ? " collapsed" : ""}`}
tabIndex={-1}
ref={drawerRef}
onMouseDown={() => {
if (isMobile) {
clickedSelf = true
setTimeout(() => (clickedSelf = false), 0)
}
}}
onBlur={() => {
if (!clickedSelf && isMobile) {
dispatch(setDrawerState({ collapsed: true }))
}
}}
>
{isCollapsible && (
<LoadableIcon
classes="ar-Drawer__expander position-absolute cursor-pointer d-none d-sm-inline"
color={theme === ArThemes.DARK1 ? "white" : "black"}
icon={
drawerState?.collapsed
? "tb/TbLayoutSidebarLeftExpand"
: "tb/TbLayoutSidebarLeftCollapse"
}
size="1.5rem"
onClick={() =>
dispatch(setDrawerState({ collapsed: !drawerState?.collapsed }))
}
/>
)}
{!drawerState?.collapsed && children}
</aside>
)
}

View File

@@ -1,3 +1,15 @@
.c-Editor {
.ar-Editor {
border: 1px solid var(--ar-color-layout-border);
.ar-Editor__tools {
background-color: var(--ar-bg-base);
border-bottom: 1px solid var(--ar-color-layout-border);
}
}
.ar-Editor__frame__main {
background-color: var(--ar-bg-tertiary);
&.background {
background-repeat: no-repeat;
background-size: 100%;
}
}

View File

@@ -1,9 +1,300 @@
import { ReactNode, useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import { createPortal } from "react-dom"
import {
Alert,
Button,
FrameContentDefinition,
FrameContentProps,
Toggle,
Helper,
ArSizes,
ObjectType,
Network,
Dropdown,
PRIMITIVES,
ArDropdownVariants,
} from ".."
import { ComponentDescription } from "../../types/componentlist.interface"
import StyleHelper from "./StyleHelper"
import COMPONENTS from "../../config/components"
import * as images from "../../static/images"
import "./Editor.component.scss"
interface EditorProps {}
const children = {
bg: (
<div
className="ar-Editor__frame__dummy-child"
style={{ height: "100vh", width: "100vw" }}
/>
),
content: (
<span slot="content" style={{ height: "10rem", width: "10rem" }}>
Popover Content
</span>
),
popover: (
<div slot="popover" style={{ height: "10rem", width: "10rem" }}>
Popover Content
</div>
),
anchor: <Button content="Click Me!" slot="anchor" />,
}
const FrameContent = (
props: FrameContentProps,
): JSX.Element | string | null => {
const { contentDefinition, notificationProps } = props
const { component, props: componentProps, data } = contentDefinition || {}
const background: string = data?.test?.background
const SelectedComponent = component
let childRenders: Array<JSX.Element> = []
if (data?.type === "HOC") {
childRenders = data.children.map(
(name: string) => children[name as keyof object],
)
}
return SelectedComponent ? (
<div
className={`ar-Editor__frame__main h-100 w-100 flex-center p-4${
background ? " background" : ""
}`}
style={{
// @ts-ignore
backgroundImage: background ? `url(${images[background]})` : "",
}}
>
{data && data.type === "HOC" ? (
<SelectedComponent {...componentProps}>
{childRenders}
</SelectedComponent>
) : (
<SelectedComponent {...componentProps} />
)}
{notificationProps && <Alert {...notificationProps} />}
</div>
) : (
"Select an item from the side panel to view here"
)
}
const Editor = (props: EditorProps): JSX.Element => {
return <div className="c-Editor">In Component Editor</div>
const Editor = (): JSX.Element | string => {
const [contentRef, setContentRef] = useState<HTMLIFrameElement | null>(null)
const [notificationProps, setNotificationProps] = useState<any>(null)
const [selectedComponentDefinition, setSelectedComponentDefinition] =
useState<FrameContentDefinition | null>(null)
const [theme, setTheme] = useState<string>("th-light-1")
const [portal, setPortal] = useState<ReactNode>()
const [selectedProps, updateSelectedProps] = useState<ObjectType | null>()
const [component, setComponent] = useState<string>()
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
window.onmessage = (e) => {
"ar" in e.data && setNotificationProps(e.data?.ar)
}
}, [])
useEffect(() => {
if (theme) {
const iframeHtml =
contentRef?.contentWindow?.document?.getElementsByTagName("html")[0]
iframeHtml && iframeHtml.setAttribute("ar-theme", theme)
const iframeBody = iframeHtml?.getElementsByTagName("body")[0]
iframeBody && (iframeBody.style.backgroundColor = "var(--ar-bg)")
}
}, [contentRef, theme])
useEffect(() => {
const locationPathArr = location.pathname
.split("/")
.filter((p: string) => p)
const componentName =
locationPathArr.length > 1 && locationPathArr[locationPathArr.length - 1]
if (componentName) {
setComponent(componentName)
const { selectedItem, hierarchy } = Helper.findComponentDescription(
componentName,
COMPONENTS,
)
if (selectedItem && hierarchy) {
let props: { [key: string]: string | number | boolean | undefined } = {
demo: true,
}
if (location.search) {
const params = location.search.substring(1)?.split("&")
if (params) {
params.forEach((prop) => {
const keyValue = prop.split("=")
const name = keyValue && keyValue[0]
let value: string | boolean = keyValue && keyValue[1]
value = value === "true" ? true : decodeURIComponent(value)
value =
value === "false" ? false : decodeURIComponent(value as string)
if (prop && value && selectedItem) {
props[name] = value
}
})
}
}
if (
!selectedComponentDefinition ||
selectedComponentDefinition.componentName !== componentName ||
JSON.stringify(selectedComponentDefinition.props) !==
JSON.stringify(props)
) {
const SelectedComponent = Helper.importComponent(
componentName,
hierarchy?.toLowerCase(),
)
setSelectedComponentDefinition({
componentName,
component: SelectedComponent,
props,
data: selectedItem,
hierarchy,
})
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location])
useEffect(() => {
selectedProps && navigate(Network.stringifyUrl("/", selectedProps).slice(1))
}, [selectedProps])
useEffect(() => {
updateSelectedProps(null)
}, [component])
useEffect(() => {
if (
contentRef &&
selectedComponentDefinition &&
process.env.NODE_ENV !== "production"
) {
StyleHelper.injectComponentStyleInFrame(
selectedComponentDefinition.data as ComponentDescription,
selectedComponentDefinition.hierarchy,
contentRef,
)
}
}, [contentRef, selectedComponentDefinition])
useEffect(() => {
if (contentRef) {
const bsLink =
contentRef?.contentWindow?.document.getElementsByTagName("link")
if (!bsLink || bsLink.length === 0) {
if (process.env.NODE_ENV === "production") {
StyleHelper.injectLinkInFrame(
["index", "ComponentsViewerPage"],
contentRef,
)
} else {
StyleHelper.injectBootstrapInFrame(contentRef)
StyleHelper.injectVariablesInFrame(contentRef)
StyleHelper.injectGlobalInFrame(contentRef)
StyleHelper.injectEditorStyleInFrame(contentRef)
StyleHelper.injectComponentStyleInFrame(
{ name: "Alert" },
"ATOMS",
contentRef,
)
}
}
}
}, [contentRef])
const mountNode = contentRef?.contentWindow?.document?.body
useEffect(() => {
if (mountNode) {
if (selectedComponentDefinition && selectedComponentDefinition.props) {
selectedComponentDefinition.props.theme = theme
}
setPortal(
createPortal(
<FrameContent
contentDefinition={selectedComponentDefinition}
notificationProps={notificationProps}
/>,
mountNode,
),
)
}
}, [selectedComponentDefinition, mountNode, notificationProps, theme])
const currentItemVariants = selectedComponentDefinition?.data.variants || []
return (
<div className="ar-Editor w-100 h-100 d-flex flex-column">
<div className="ar-Editor__tools px-3 py-2">
<div className="row justify-content-end">
<div className="col flex-v-center justify-content-end">
<div className="ar-Editor__prop-selector flex-v-center border-right me-3">
{Object.keys(currentItemVariants)
.filter((prop: string) => prop !== "demo")
.map((prop: any, index: number, arr) => {
const propsValues = currentItemVariants[prop]
const isBoolean =
propsValues.length === 2 &&
propsValues.indexOf(true) > -1 &&
propsValues.indexOf(false) > -1
return isBoolean ? (
<Toggle
key={"prop-toggle-" + index + "-" + prop}
classes="me-3"
label={Helper.toReadable(prop)}
onChange={(isChecked: boolean) =>
updateSelectedProps({
...(selectedProps || {}),
[prop]: isChecked,
})
}
size={ArSizes.SMALL}
hideStatus
/>
) : (
<Dropdown
classes="me-2"
options={propsValues.map((propValue: PRIMITIVES) => ({
name: propValue,
}))}
onSelectionChanged={(obj) => {
updateSelectedProps({
...(selectedProps || {}),
[prop]: obj.value,
})
}}
variant={ArDropdownVariants.SELECTIONSASPILLS}
placeholder={Helper.toReadable(prop)}
/>
)
})}
</div>
<span className="float-end">
<Toggle
toggleOnName="Dark"
toggleOffName="Light"
onChange={(checked: boolean) =>
setTheme(checked ? "th-dark-1" : "th-light-1")
}
/>
</span>
</div>
</div>
</div>
<iframe
className="ar-Editor__frame w-100 h-100"
ref={setContentRef}
title="Armco Component Viewer"
>
{portal}
</iframe>
{/* <div className="ar-Editor__props-container d-inline-block h-100"></div> */}
</div>
)
}
export default Editor

View File

@@ -0,0 +1,133 @@
import { ComponentDescription } from "../../types/componentlist.interface"
//// Editor Frame Style related helpers
class StyleHelper {
static injectGlobalInFrame(frame: HTMLIFrameElement | null) {
const clipCallback = (text: string) => {
return {
clipStart: text && text.indexOf("html {\\n font-size: 16px"),
}
}
StyleHelper.injectScssInFrame(
"/src/app/static/styles/_global.scss",
frame,
undefined,
clipCallback,
)
}
static injectBootstrapInFrame(frame: HTMLIFrameElement | null) {
StyleHelper.injectLinkInFrame(
"/src/app/static/styles/bootstrap.min.css",
frame,
)
}
static injectVariablesInFrame(frame: HTMLIFrameElement | null) {
StyleHelper.injectScssInFrame(
"/src/app/static/styles/_variables.scss",
frame,
)
}
static injectEditorStyleInFrame(frame: HTMLIFrameElement | null) {
StyleHelper.injectScssInFrame(
"/src/app/components/Editor/Editor.component.scss",
frame,
)
}
static injectComponentStyleInFrame(
selectedComponent: ComponentDescription,
hierarchy: string,
frame: HTMLIFrameElement | null,
) {
if (hierarchy && selectedComponent && selectedComponent.name) {
const componentStyleElement =
frame?.contentWindow?.document.querySelector("style.component-style")
componentStyleElement && componentStyleElement.remove()
const dependencyComponentsToInject = selectedComponent.uses
? [
{ name: selectedComponent.name, hierarchy },
...selectedComponent.uses,
]
: [{ name: selectedComponent.name, hierarchy }]
dependencyComponentsToInject.forEach(
(component: { [key: string]: string }) => {
const scssPath = `/src/app/components/${component.hierarchy.toLowerCase()}/${
component.name
}/${component.name}.component.scss`
StyleHelper.injectScssInFrame(scssPath, frame, component)
},
)
}
}
static injectScssInFrame(
scssPath: string,
frame: HTMLIFrameElement | null,
component?: { [key: string]: string },
clipCallback?: any,
) {
import("sass").then((module) => {
const { compileString } = module
fetch(scssPath)
.then((r) => r.text())
.then((text) => {
const { clipStart, clipEnd } = clipCallback
? clipCallback(text)
: () => ({})
text = text
.substring(
clipStart ||
text.indexOf("__vite__css = ") + '__vite__css = "'.length,
clipEnd || text.indexOf("__vite__updateStyle(") - 2,
)
.replace(/\\n/g, "")
.replace(/\\"/g, '"')
const css = compileString(text).css
StyleHelper.injectStyleInFrame(css, frame, component)
})
})
}
static injectStyleInFrame(
css: string,
frame: HTMLIFrameElement | null,
component?: { [key: string]: string },
) {
const style = document.createElement("style")
if (component && component.name) {
style.setAttribute("id", component.name)
style.setAttribute("class", "component-style")
}
style.appendChild(document.createTextNode(css))
frame?.contentWindow?.document?.head.appendChild(style)
}
static injectLinkInFrame(
link: string | Array<string>,
frame: HTMLIFrameElement | null,
) {
if (Array.isArray(link)) {
const links = document.querySelectorAll("link[rel='stylesheet']")
link.forEach((item: string) => {
Array.from(links).forEach((selectedLink) => {
const cssUrl = selectedLink.getAttribute("href")
if (cssUrl && cssUrl.startsWith(`/assets/${item}`)) {
const clone = selectedLink.cloneNode(true)
frame?.contentWindow?.document?.head.appendChild(clone)
}
})
})
} else {
const cssLink = document.createElement("link")
cssLink.href = link
cssLink.rel = "stylesheet"
cssLink.type = "text/css"
frame?.contentWindow?.document?.head.appendChild(cssLink)
}
}
}
export default StyleHelper

View File

@@ -0,0 +1,28 @@
.ar-FavoritesList {
max-width: 0;
transition: max-width 0.3s;
&.show {
max-width: 10rem;
}
.ar-FavoritesList__header {
background-color: var(--ar-color-prominent);
&.loggedIn {
background-color: var(--ar-color-success);
}
}
&.shrink {
max-width: 4rem;
}
.ar-FavoriteItem {
width: 0;
transition: width 0.3s linear;
&.show {
width: 5rem;
}
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import FavoritesList from "./FavoritesList"
describe("FavoritesList", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,119 @@
import { useEffect, useState } from "react"
import { useAppDispatch, useAppSelector } from "../../hooks"
import { getFavorites } from "../../pages/IconsPage/IconsPage.slice"
import { getLoggedIn, setRightPanelContent } from "../../store"
import { Button, IconTile, LoadableIcon, Popover } from ".."
import {
FavoritesItemProps,
FavoritesListProps,
IconTileProps,
} from "../../types/components.interface"
import {
ArButtonVariants,
ArPopoverPositions,
ArPopoverTriggers,
ArSizes,
} from "../../types/enums"
import "./FavoritesList.component.scss"
const FavoriteItem = (props: FavoritesItemProps): JSX.Element | null => {
const { index, favorite } = props
const [shown, show] = useState<boolean>()
useEffect(() => {
show(true)
return () => {
show(false)
setTimeout(() => {}, 300)
}
}, [])
return (
<span
className={`ar-FavoriteItem d-flex flex-column${shown ? " show" : ""}`}
>
<IconTile key={index} icon={favorite.icon} hideFooter hideBorder />
</span>
)
}
const FavoritesList = (props: FavoritesListProps): JSX.Element => {
const favorites = useAppSelector(getFavorites)
const isLoggedIn = useAppSelector<boolean | undefined>(getLoggedIn)
const dispatch = useAppDispatch()
const icons =
favorites &&
favorites.map((favorite: IconTileProps, index: number) => (
<FavoriteItem index={index} favorite={favorite} />
))
return (
<div
className={`ar-FavoritesList h-100${icons?.length > 0 ? " show" : ""}${
isLoggedIn ? " shrink" : ""
}`}
>
<LoadableIcon icon="tb/TbLayoutSidebarRightCollapse" />
{!isLoggedIn ? (
<Popover
trigger={ArPopoverTriggers.HOVER}
position={ArPopoverPositions.LEFTBOTTOM}
>
<span
className="ar-FavoritesList__header p-2 border-bottom d-flex justify-content-center w-100"
slot="anchor"
>
<LoadableIcon icon="io/IoIosWarning" color="white" />
</span>
<div slot="popover" className="p-2">
<strong className="mb-2 d-inline-block">
You're not logged in, all of your favorites will be lost when the
session is over.
</strong>
<p className="mb-0">
Please login to ensure your changes are saved!
</p>
</div>
</Popover>
) : (
<Popover
trigger={ArPopoverTriggers.HOVER}
position={ArPopoverPositions.LEFTBOTTOM}
>
<span
className="ar-FavoritesList__header loggedIn p-2 border-bottom d-flex justify-content-center w-100"
slot="anchor"
>
<LoadableIcon icon="io/IoIosCheckmarkCircleOutline" color="white" />
</span>
<div slot="popover" className="p-2">
<strong className="mb-2 d-inline-block">
Your favorites are being synchronized with your account!
</strong>
</div>
</Popover>
)}
<div
className={`ar-FavoritesList__favorites flex-v-center flex-column mt-3${
icons?.length > 0 ? " px-2" : ""
}`}
>
{icons}
</div>
{!isLoggedIn && (
<Button
classes="mx-2"
variant={ArButtonVariants.WARNING}
size={ArSizes.SMALL}
content="Login"
preIcon="ci/CiWarning"
onClick={() => {
dispatch(setRightPanelContent({ name: "LoginProvider" }))
}}
/>
)}
</div>
)
}
export default FavoritesList

View File

@@ -0,0 +1,3 @@
import FavoritesList from "./FavoritesList"
export default FavoritesList

View File

@@ -0,0 +1,26 @@
.ar-FlexTools {
.col {
border-right: var(--ar-border);
}
.ar-FlexTools__separator {
border-right: var(--ar-border);
}
.ar-FlexTools__brand {
border-radius: 50%;
}
.ar-FlexTools__app-name {
font-family: "Allerta Stencil";
font-weight: 700;
font-size: 1rem;
.ar-FlexTools__app-name-i {
color: red;
}
.ar-FlexTools__app-name-o {
color: green;
}
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import FlexTools from "./FlexTools"
describe("FlexTools", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,129 @@
import { useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { useAppSelector } from "../../hooks"
import { getCurrentTheme } from "../../store"
import {
AppAndToolsSelector,
LoadableIcon,
Popover,
TabBar,
UserOptions,
} from ".."
import {
ArAnimations,
ArPopoverPositions,
ArPopoverSlots,
ArTabType,
ArThemes,
} from "../../types/enums"
import {
FlexToolsProps,
TabBarProps,
TabProps,
} from "../../types/components.interface"
import { Helper } from "../../utils"
import navigator from "../../config/navigator"
import "./FlexTools.component.scss"
const userOptions = [
[{ name: "Profile" }, { name: "Settings" }, { name: "My Stuff" }],
[{ name: "+ Organization" }, { name: "+ Upload Icon" }],
]
const isMobile = Helper.isMobile()
const FlexTools = (props: FlexToolsProps): JSX.Element => {
const { isLanding, route } = props
const theme = useAppSelector<string>(getCurrentTheme)
const navigate = useNavigate()
useEffect(() => {
navigator.forEach((item) => {
navigator &&
item.items?.forEach(
(subItem) =>
(subItem.onClick = () => subItem.url && navigate(subItem.url)),
)
})
}, [navigate])
const onTabSelected = (id: string, tab: TabProps) => {
tab?.data?.url && navigate(tab.data.url as string)
}
const separator = <span className="ar-FlexTools__separator h-100 me-2" />
const tabBarProps: TabBarProps = {
data: navigator,
variant: ArTabType.MODERN,
onTabSelected,
}
if (route === "landing") {
tabBarProps.activeId = ""
}
const appSelector = isLanding ? (
<TabBar {...tabBarProps} />
) : (
<Popover
classes={isMobile ? "" : "me-3"}
position={
isMobile ? ArPopoverPositions.TOPCENTER : ArPopoverPositions.BOTTOMLEFT
}
animation={ArAnimations.FADEINOUT}
transition
>
<LoadableIcon
classes="cursor-pointer"
slot={ArPopoverSlots.ANCHOR}
icon="io5.IoApps"
size="1.5rem"
color={theme === ArThemes.DARK1 ? "lightgrey" : "grey"}
hoverColor="orange"
/>
<AppAndToolsSelector slot={ArPopoverSlots.POPOVER} data={navigator} />
</Popover>
)
const userOps = (
<UserOptions
classes={`h-100 flex-center px-2${isLanding ? " ms-auto" : ""}`}
options={userOptions}
isLanding={isLanding}
theme={theme}
/>
)
return (
<div className="ar-FlexTools h-100 flex-center w-100">
{!isMobile ? (
<>
{appSelector}
{!isLanding && separator}
{userOps}
</>
) : (
<div className="row w-100">
<div className="col flex-center" onClick={() => navigate("/")}>
{/* <img
src={AppLogo}
alt="App Logo"
className="ar-FlexTools__brand mx-2"
width="32"
height="32"
onClick={() => navigate("/")}
/> */}
<span className="ar-FlexTools__app-name h-100 flex-center">
Stuffle.
<span className="ar-FlexTools__app-name-i">i</span>
<span className="ar-FlexTools__app-name-o">o</span>
</span>
</div>
<div className="ar-FlexTools__app-selector col flex-center">
{appSelector}
</div>
<div className="col flex-center">{userOps}</div>
</div>
)}
</div>
)
}
export default FlexTools

View File

@@ -0,0 +1,3 @@
import FlexTools from "./FlexTools"
export default FlexTools

View File

@@ -0,0 +1,12 @@
.ar-FontsList {
.ar-FontsList__header-search-upload {
background-color: var(--ar-bg);
color: var(--ar-colora);
background-color: var(--ar-bg-base);
}
.ar-FontsList__font-tile-container {
background-color: var(--ar-bg);
color: var(--ar-color);
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import FontsList from "./FontsList"
describe("FontsList", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,140 @@
import { ChangeEvent, useEffect, useState } from "react"
import { FontsListProps } from "../../types/components.interface"
import { ArrayType, FunctionType, ObjectType } from "../../types/types"
import { ArButtonVariants, ArLoaderTypes, ArSizes } from "../../types/enums"
import { Button, FontTile, Loader, Pagination, Search, Toggle } from ".."
import { Helper, Network } from "../../utils"
import API_CONFIG from "../../config/api-config"
import { ENDPOINTS } from "../../config/constants"
import "./FontsList.component.scss"
const fetchFontsPage = (
limit: number,
from: number,
filters: ObjectType,
dataSetter: FunctionType,
pageSetter: FunctionType,
setLoading: FunctionType,
) => {
const pageApi =
API_CONFIG.STATIC_HOST[process.env.NODE_ENV] +
ENDPOINTS.STATIC.FONT.ROOT +
ENDPOINTS.STATIC.FONT.PAGE
const queryParams: ObjectType = { pageSize: limit, from, ...filters }
Network.get(pageApi, queryParams)
.then((response) => {
if (response && response.status === 200) {
if (response.body && dataSetter) {
const content = JSON.parse(response.body)
const styleElement = document.createElement("style")
styleElement.type = "text/css"
styleElement.appendChild(
document.createTextNode(content.returnFontGroups),
)
document.head.appendChild(styleElement)
dataSetter(content.fontNames)
pageSetter(content.fontNames.slice(0, 30))
}
}
setLoading(false)
})
.catch((error) => {
console.error(error)
setLoading(false)
})
}
const FontsList = (props: FontsListProps): JSX.Element => {
const [searchText, setSearchText] = useState<string | undefined>()
const [fonts, setFonts] = useState<ArrayType>()
const [page, setPage] = useState<ArrayType>()
const [loading, setLoading] = useState<boolean>()
// TODO: Fetch Fonts
useEffect(() => {
const filters: ObjectType = {}
setLoading(true)
fetchFontsPage(30, 0, filters, setFonts, setPage, setLoading)
}, [])
useEffect(() => {
const filters: ObjectType = {}
if (searchText) {
filters.search = searchText
}
setLoading(true)
fetchFontsPage(200, 0, filters, setFonts, setPage, setLoading)
}, [searchText])
return (
<div className="ar-FontsList h-100 w-100">
{!fonts && (
<Loader label="Loading Fonts..." type={ArLoaderTypes.SHAPES} />
)}
<div className="ar-FontsList__header-search-upload py-2 px-3 mb-2 border">
<div className="row">
<span className="offset-4 col-4 d-none d-md-flex flex-v-center justify-content-end">
<Button
classes="h-100 float-end me-3"
content="Upload"
size={ArSizes.SMALL}
variant={ArButtonVariants.SUCCESS}
/>
<Toggle
onChange={() => {}}
toggleOnName="Compact"
toggleOffName="Compact"
/>
</span>
<div className="col-4 offset-4 offset-md-0 flex-v-center">
<Search
classes="bg-white"
placeholder="Search by name, style, description"
onChange={Helper.debounce(
(event: ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value),
1000,
)}
data={
[]
// fonts
// ? fonts.map(
// (font): SearchItem => ({
// label: font.name,
// data: font,
// }),
// )
// : []
}
/>
</div>
</div>
</div>
{page && (
<div className="ar-FontsList__font-tile-container py-2 px-3 border d-flex justify-content-between flex-wrap flex-grow-1">
{loading && (
<Loader label="Applying filters..." type={ArLoaderTypes.CIRCLE} />
)}
{(page as Array<string>).map((font) => (
<FontTile font={font} />
))}
</div>
)}
<Pagination
classes="my-3 flex-center"
data={fonts}
maxPillsToShow={5}
pageSetter={setPage}
// trigger={ArPageTriggers.SCROLL}
count={1}
load={100}
// dataFetcher={(load, count) =>
// fetchIconsPage(load, count, {}, setIcons, setPage, setLoading)
// }
/>
</div>
)
}
export default FontsList

View File

@@ -0,0 +1,3 @@
import FontsList from "./FontsList"
export default FontsList

View File

@@ -1,6 +1,4 @@
@import "../../static/styles/variables";
.c-Footer {
.ar-Footer {
background-color: var(--ar-bg-base);
border-top: 1px solid #eee;
}

View File

@@ -1,9 +1,36 @@
import { useAppDispatch, useAppSelector } from "../../hooks"
import { getCurrentTheme, setTheme } from "../../store"
import { FlexTools, Toggle } from ".."
import { Helper } from "../../utils"
import "./Footer.component.scss"
interface FooterProps {}
const isMobile = Helper.isMobile()
const Footer = (props: FooterProps): JSX.Element => {
return <footer className="c-Footer w-100 mt-auto"></footer>
const theme = useAppSelector<string>(getCurrentTheme)
const dispatch = useAppDispatch()
return (
<footer className="ar-Footer w-100 d-flex py-1">
{!isMobile ? (
<Toggle
classes="ms-auto"
toggleOffName="Go Dark"
toggleOnName="Go Dark"
onChange={(isChecked: boolean) => {
const nextTheme = isChecked ? "th-dark-1" : "th-light-1"
dispatch(setTheme(nextTheme))
document
.getElementsByTagName("html")[0]
.setAttribute("ar-theme", nextTheme)
}}
/>
) : (
<FlexTools />
)}
</footer>
)
}
export default Footer

View File

@@ -1,6 +1,39 @@
@import "../../static/styles/variables";
.c-Header {
.ar-Header {
background-color: var(--ar-bg-base);
border-bottom: 1px solid #eee;
border-bottom: 1px solid var(--ar-color-layout-border);
.ar-Header__app-logo {
cursor: pointer;
.ar-Header__brand {
border-radius: 50%;
}
.ar-Header__app-name {
font-family: "Allerta Stencil";
font-weight: 700;
font-size: 1.5rem;
.ar-Header__app-name-i {
color: red;
}
.ar-Header__app-name-o {
color: green;
}
}
}
.ar-Header__app-search {
transition: width 0.3s;
@media screen and (max-width: 576px) {
flex: 1;
.ar-SearchField .ar-TextInput {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.ar-SearchField .ar-Button {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
}
}
}

View File

@@ -1,9 +1,104 @@
import { useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import { useAppDispatch, useAppSelector } from "../../hooks"
import {
getCurrentTheme,
getDrawerState,
setDrawerState,
setTheme,
} from "../../store"
import { FlexTools, LoadableIcon, SearchField } from ".."
import { DrawerProps, HeaderProps } from "../../types/components.interface"
import { ObjectType } from "../../types/types"
import { ArThemes } from "../../types/enums"
import "./Header.component.scss"
interface HeaderProps {}
const Header = (props: HeaderProps): JSX.Element => {
return <header className="c-Header w-100"></header>
const { routeDetails } = props
const navigate = useNavigate()
const location = useLocation()
const [searchFocussed, setSearchFocussed] = useState<boolean>()
const drawerState = useAppSelector<DrawerProps | undefined>(getDrawerState)
const dispatch = useAppDispatch()
const theme = useAppSelector<string>(getCurrentTheme)
const route = (routeDetails?.route as ObjectType)?.class
const isLanding =
!routeDetails ||
["landing", "assets", "playground", "tools-and-services"].indexOf(
route as string,
) > -1
const showExpander =
location.pathname.startsWith("/components") ||
location.pathname.startsWith("/icons")
return (
<header className="ar-Header w-100 flex-v-center px-2">
<div
className="ar-Header__app-logo h-100 flex-v-center me-3 d-none d-sm-flex"
onClick={() => navigate("/")}
>
<span className="ar-Header__app-name h-100 flex-center">
Stuffle.
<span className="ar-Header__app-name-i">i</span>
<span className="ar-Header__app-name-o">o</span>
</span>
</div>
{showExpander && (
<LoadableIcon
classes="ar-Drawer__expander d-inline d-sm-none me-3"
icon={
drawerState?.collapsed
? "tb/TbLayoutSidebarLeftExpand"
: "tb/TbLayoutSidebarLeftCollapse"
}
size="2rem"
onClick={() =>
dispatch(setDrawerState({ collapsed: !drawerState?.collapsed }))
}
color={theme === "th-dark-1" ? "lightgrey" : "black"}
/>
)}
{!isLanding && (
<div
tabIndex={-1}
onFocus={() => setSearchFocussed(true)}
onBlur={() => setSearchFocussed(false)}
className={`ar-Header__app-search h-100 flex-v-center me-3 ${
searchFocussed ? "w-50" : "w-25"
}`}
>
<SearchField
data={[]}
onChange={() => {}}
placeholder="Search icons, components, fonts..."
showPopUp
/>
</div>
)}
<LoadableIcon
classes="d-inline d-sm-none ms-auto"
icon="cg.CgDarkMode"
size="2rem"
onClick={() => {
const nextTheme =
theme === ArThemes.LIGHT1 ? ArThemes.DARK1 : ArThemes.LIGHT1
dispatch(setTheme(nextTheme))
document
.getElementsByTagName("html")[0]
.setAttribute("ar-theme", nextTheme)
}}
color={theme === ArThemes.DARK1 ? "lightgrey" : "black"}
/>
<div
className={`h-100 d-none d-sm-inline${
isLanding ? " w-100" : " ms-auto"
}`}
>
<FlexTools isLanding={isLanding} route={route as string} />
</div>
</header>
)
}
export default Header

View File

@@ -0,0 +1,48 @@
.ar-IconController {
.ar-IconController__main, .ar-IconController__header {
background-color: var(--ar-bg-base);
}
.ar-IconController__color-palette {
border-radius: 50%;
}
.ar-IconController__download-button, .ar-IconController__animate-button {
background-color: orange;
border-color: orange;
}
.ar-IconController__main {
border-radius: 0.3rem;
}
.ar-IconController__controls-form {
background-color: var(--ar-bg);
}
.hover-show {
cursor: pointer;
.ar-IconController__download-icon {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
&:hover {
.ar-IconController__download-icon {
opacity: 1;
pointer-events: auto;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0 4px 8px var(--ar-shadow);
border-radius: 3px
}
}
}
}
.ar-IconController__meta-item {
color: var(--ar-color-secondary);
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import IconController from "./IconController"
describe("IconController", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,373 @@
import { useNavigate } from "react-router-dom"
import { useEffect, useState } from "react"
import { Player, Controls } from "@lottiefiles/react-lottie-player"
import { setRightPanelContent } from "../../store"
import { useAppDispatch, useAppSelector } from "../../hooks"
import { getIconStyles } from "../../pages/IconPage/IconPage.slice"
import {
Breadcrumb,
Button,
Select,
LoadableIcon,
Suggestions,
Tags,
TextInput,
} from ".."
import { IconControllerProps } from "../../types/components.interface"
import { ArButtonVariants, ArPopoverSlots } from "../../types/enums"
import { ObjectType } from "../../types/types"
import { DomHelper, Network } from "../../utils"
import API_CONFIG from "../../config/api-config"
import "./IconController.component.scss"
const complement = (hex?: string) => {
if (hex) {
const hexParts = hex.substring(1).toLowerCase().split("")
const hexMap = { a: 10, b: 11, c: 12, d: 13, e: 14, f: 15 }
const reverseMap = { 10: "a", 11: "b", 12: "c", 13: "d", 14: "e", 15: "f" }
let complementValue = "#"
hexParts.forEach((part: string) => {
let num = +part
if (isNaN(num)) {
num = hexMap[part as keyof object]
}
const complementNumeric = 15 - num
complementValue +=
complementNumeric > 9
? reverseMap[complementNumeric as keyof object]
: complementNumeric
})
return complementValue
}
return "#fff"
}
const STATIC_ROOT = API_CONFIG.STATIC_HOST[process.env.NODE_ENV]
const IconController = (props: IconControllerProps): JSX.Element => {
const { group, icon, name } = props
const [size, setSize] = useState<string>()
const [unit, setUnit] = useState<string>("rem")
const [similarIcons, setSimilarIcons] = useState<Array<string>>()
const dispatch = useAppDispatch()
const iconStyles = useAppSelector(getIconStyles)
const fontColor = complement(iconStyles?.bgColor || "white")
const navigate = useNavigate()
// const suggestions = getSuggestions(group, name)
useEffect(() => {
const filename = `${group}_${name}-black.png`
Network.getStatic(`/static/${filename}`).then((res) => {
const imgBlob = res.body
const formData = new FormData()
const file = new File([imgBlob], filename, { type: imgBlob.type })
formData.append("file", file)
formData.append("count", "20")
Network.post(
"http://localhost:5002/api/similar-icon-paths",
formData,
undefined,
{ headers: { "Content-Type": "multipart/form-data" } },
true,
true,
).then((res) => {
if (res.status === 200) {
const iconPaths = res.body.map((str: string) =>
str.replace(
"../images",
`${API_CONFIG.STATIC_HOST[process.env.NODE_ENV]}/static`,
),
)
// setSimilarIcons(iconPaths)
Network.post(STATIC_ROOT + "/icon/png-to-svg", {
urls: iconPaths,
}).then((res) => {
setSimilarIcons(res.body)
})
}
})
})
}, [name, group])
return (
<div className="ar-IconController container h-100 overflow-auto">
{group && name && (
<Breadcrumb
data={[
{ label: group, route: `/icon/${group}` },
{ label: name, route: `/icon/${group}/${name}` },
]}
/>
)}
<div className="ar-IconController__header mb-3 p-3 border d-flex">
<h6 className="mb-0 fw-bold">
{group}.{name}
</h6>
<Button
classes="ar-IconController__animate-button ms-auto me-3"
variant={ArButtonVariants.PRIMARY}
postIcon="md/MdAnimation"
content="Animate"
onClick={() =>
navigate("/icon/animate", {
state: { icon, iconStyles, name: `${group}_${name}` },
})
}
/>
<Button
classes="ar-IconController__download-button me-3"
variant={ArButtonVariants.PRIMARY}
postIcon="md/MdDownload"
content="Download"
onClick={() =>
DomHelper.downloadSvg(
(icon as ObjectType).icon as string,
name || "download.svg",
{
size: "3rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
)
}
/>
<LoadableIcon
classes="ar-IconController__color-palette cursor-pointer hover-shadow"
icon="io/IoIosColorPalette"
color="orange"
slot={ArPopoverSlots.ANCHOR}
size="2rem"
onClick={() => dispatch(setRightPanelContent({ name: "IconEditor" }))}
/>
</div>
<div className="ar-IconController__main mb-3 p-3 border">
<div className="row">
<div
className="ar-IconController__icon-sizes col"
style={{ backgroundColor: iconStyles?.bgColor }}
>
{group && name && (
<div className="row h-100 border-right">
<div className="col flex-center flex-column border-right">
<div className="h-50 flex-center flex-column border-bottom w-100 position-relative hover-show">
<LoadableIcon
classes="ar-IconController__download-icon position-absolute top-1 end-1"
icon="md/MdDownload"
color="orange"
size="2.5rem"
onClick={() =>
DomHelper.downloadSvg(
(icon as ObjectType).icon as string,
name,
{
size: "3rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
)
}
/>
<LoadableIcon
key={`${group}/${name}`}
icon={`${group}/${name}`}
size="3rem"
color={iconStyles?.strokeColor || "royalblue"}
fillColor={iconStyles?.fillColor}
strokeColor={iconStyles?.strokeColor}
strokeWidth={iconStyles?.strokeWidth}
/>
<span
className="fw-bold"
style={{ color: fontColor || "black" }}
>
3rem
</span>
</div>
<div className="h-50 flex-center flex-column w-100 position-relative hover-show">
<LoadableIcon
classes="ar-IconController__download-icon position-absolute top-1 end-1"
icon="md/MdDownload"
color="orange"
size="2.5rem"
onClick={() =>
DomHelper.downloadSvg(
(icon as ObjectType).icon as string,
name,
{
size: "7rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
)
}
/>
<LoadableIcon
key={`${group}/${name}`}
icon={`${group}/${name}`}
size="7rem"
color={iconStyles?.strokeColor || "royalblue"}
fillColor={iconStyles?.fillColor}
strokeColor={iconStyles?.strokeColor}
strokeWidth={iconStyles?.strokeWidth}
/>
<span
className="fw-bold"
style={{ color: fontColor || "black" }}
>
7rem
</span>
</div>
</div>
<div className="col flex-center flex-column position-relative hover-show">
<LoadableIcon
classes="ar-IconController__download-icon position-absolute top-1 end-1"
icon="md/MdDownload"
color="orange"
size="2.5rem"
onClick={() =>
DomHelper.downloadSvg(
(icon as ObjectType).icon as string,
name,
{
size: size && unit ? size + unit : "20rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
)
}
/>
<LoadableIcon
key={`${group}/${name}`}
icon={`${group}/${name}`}
size={`${size}${unit}` || "20rem"}
color={iconStyles?.strokeColor || "royalblue"}
fillColor={iconStyles?.fillColor}
strokeColor={iconStyles?.strokeColor}
strokeWidth={iconStyles?.strokeWidth}
/>
<span
className="fw-bold"
style={{ color: fontColor || "black" }}
>
{size && unit ? size + unit : "20rem"}
</span>
</div>
</div>
)}
</div>
<div className="ar-IconController__controls col d-flex">
<div className="row w-100">
{icon && (
<div className="ar-IconController__meta col-4">
<h6>Icon Details</h6>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Created By: </strong>
{icon.createdby as string}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Description: </strong>
{icon.description as string}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Downloaded: </strong>
{((icon.meta as ObjectType)?.downloadedTimes as number) ||
0}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Liked: </strong>
{((icon.meta as ObjectType)?.downloadedTimes as number) ||
0}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Size: </strong>
{(icon.meta as ObjectType)?.size as number}
</div>
</div>
</div>
)}
<div
className={`ar-IconController__controls-form ${
icon ? "col-8" : "col-12"
} border`}
>
<div className="row h-100 py-3">
<div className="ar-IconController__controls-form-container col-12 h-25">
<div className="row">
<div className="col">
<TextInput
type="slider"
label="Icon Size"
min={1}
max={30}
onChange={(e) =>
setSize((e.target as HTMLInputElement).value)
}
/>
</div>
<div className="col">
<Select
label="Unit"
onSelectionChanged={(e: { value: string }) =>
setUnit(e.value)
}
options={[
// @ts-ignore
{ label: "rem", value: "rem" },
// @ts-ignore
{ label: "px", value: "px" },
// @ts-ignore
{ label: "vh", value: "vh" },
]}
/>
</div>
</div>
</div>
{/* <Tags classes="col-12 h-100 px-0" label={name} /> */}
{icon && (
<Tags
// clickHandler={clickHandler}
classes="col-12 h-75 px-0"
label={name}
tags={Object.fromEntries(
(
icon.tags as Array<{ name: string; verified: string }>
).map((key) => [key.name, 1]),
)}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
<Suggestions
classes="border mb-3"
suggestions={similarIcons}
title={`Icons Similar to ${name}`}
inSvgString
/>
<Suggestions
classes="border mb-3"
suggestions={similarIcons}
title="Trending Icons"
inSvgString
// suggestions={recentlyVisited}
/>
</div>
)
}
export default IconController

View File

@@ -0,0 +1,3 @@
import IconController from "./IconController"
export default IconController

View File

@@ -0,0 +1,2 @@
.ar-IconEditor {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import IconEditor from "./IconEditor"
describe("IconEditor", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from "react"
import { v4 as uuid } from "uuid"
import { useAppDispatch } from "../../hooks"
import { setIconStyles } from "../../pages/IconPage/IconPage.slice"
import { TabBar, IconStyleSelector } from ".."
import { IconEditorProps, TabProps } from "../../types/components.interface"
import "./IconEditor.component.scss"
const iconEditorTabs = [
{
label: "Icon",
id: uuid(),
},
{
label: "Background",
id: uuid(),
},
]
const IconEditor = (props: IconEditorProps): JSX.Element => {
const { layout } = props
const [iconStyles, setIconStylesState] = useState<{
fillColor?: string
strokeColor?: string
bgColor?: string
strokeWidth?: string
}>()
const dispatch = useAppDispatch()
useEffect(() => {
iconStyles && dispatch(setIconStyles(iconStyles))
}, [iconStyles, dispatch])
return (
<div className="ar-IconEditor h-100">
<div className="ar-IconEditor__config-form h-100 overflow-auto">
<IconStyleSelector
iconStyles={iconStyles}
setIconStyles={setIconStylesState}
layout={layout}
/>
</div>
</div>
)
}
export default IconEditor

View File

@@ -0,0 +1,3 @@
import IconEditor from "./IconEditor"
export default IconEditor

View File

@@ -0,0 +1,12 @@
.ar-IconMergeContainer {
svg {
width: 5rem;
height: 5rem;
margin-left: 2rem;
margin-right: 2rem;
path:hover {
outline: 1px dotted grey;
}
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import IconMergeContainer from "./IconMergeContainer"
describe("IconMergeContainer", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,154 @@
import { useEffect, useRef } from "react"
import { IconMergeContainerProps } from "../../types/components.interface"
import { DomHelper } from "../../utils"
import "./IconMergeContainer.component.scss"
import { ArrayType, ObjectType } from "../../types/types"
const iconTest = [
{
name: "CiAlignBottom",
icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><g id="Align_Bottom"><g><path d="M3.548,20.922h16.9a.5.5,0,0,0,0-1H3.548a.5.5,0,0,0,0,1Z"></path><path d="M9,18.919H6.565a2.5,2.5,0,0,1-2.5-2.5V5.578a2.5,2.5,0,0,1,2.5-2.5H9a2.5,2.5,0,0,1,2.5,2.5V16.419A2.5,2.5,0,0,1,9,18.919ZM6.565,4.078a1.5,1.5,0,0,0-1.5,1.5V16.419a1.5,1.5,0,0,0,1.5,1.5H9a1.5,1.5,0,0,0,1.5-1.5V5.578A1.5,1.5,0,0,0,9,4.078Z"></path><path d="M17.437,18.919H15a2.5,2.5,0,0,1-2.5-2.5V10.55A2.5,2.5,0,0,1,15,8.05h2.434a2.5,2.5,0,0,1,2.5,2.5v5.869A2.5,2.5,0,0,1,17.437,18.919ZM15,9.05a1.5,1.5,0,0,0-1.5,1.5v5.869a1.5,1.5,0,0,0,1.5,1.5h2.434a1.5,1.5,0,0,0,1.5-1.5V10.55a1.5,1.5,0,0,0-1.5-1.5Z"></path></g></g></svg>',
createdby: "Armco",
createdAt: "2023-10-14T20:46:51.541Z",
updatedAt: "2023-10-14T20:46:51.541Z",
meta: {
size: "772 bytes",
downloadTimes: 0,
favoriteTimes: 0,
},
group: "ci",
description: "",
tags: [
{
name: "Ci",
state: "verified",
},
{
name: "Align",
state: "verified",
},
{
name: "Bottom",
state: "verified",
},
{
name: "icon",
state: "verified",
},
{
name: "armco",
state: "verified",
},
],
},
{
name: "CiAlignCenterH",
icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><g id="Align_Center-H"><path d="M17.42,4.062H12.5v-.51a.5.5,0,0,0-1,0v.51H6.58a2.507,2.507,0,0,0-2.5,2.5V9a2.5,2.5,0,0,0,2.5,2.5H11.5v1H9.06A2.507,2.507,0,0,0,6.56,15v2.44a2.507,2.507,0,0,0,2.5,2.5H11.5v.51a.5.5,0,0,0,1,0v-.51h2.43a2.5,2.5,0,0,0,2.5-2.5V15a2.5,2.5,0,0,0-2.5-2.5H12.5v-1h4.92A2.5,2.5,0,0,0,19.92,9V6.562A2.507,2.507,0,0,0,17.42,4.062ZM11.5,18.942H9.06a1.511,1.511,0,0,1-1.5-1.5V15a1.5,1.5,0,0,1,1.5-1.5H11.5Zm0-8.44H6.58A1.5,1.5,0,0,1,5.08,9V6.562a1.5,1.5,0,0,1,1.5-1.5H11.5Zm3.43,3a1.5,1.5,0,0,1,1.5,1.5v2.44a1.5,1.5,0,0,1-1.5,1.5H12.5V13.5ZM18.92,9a1.5,1.5,0,0,1-1.5,1.5H12.5V5.062h4.92a1.5,1.5,0,0,1,1.5,1.5Z"></path></g></svg>',
createdby: "Armco",
createdAt: "2023-10-14T20:46:51.541Z",
updatedAt: "2023-10-14T20:46:51.541Z",
meta: {
size: "790 bytes",
downloadTimes: 0,
favoriteTimes: 0,
},
group: "ci",
description: "",
tags: [
{
name: "Ci",
state: "verified",
},
{
name: "Align",
state: "verified",
},
{
name: "Center",
state: "verified",
},
{
name: "H",
state: "verified",
},
{
name: "icon",
state: "verified",
},
{
name: "armco",
state: "verified",
},
],
},
{
name: "CiAlignCenterV",
icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><g id="Align_Center-V"><path d="M20.446,11.5h-.51V9.07a2.5,2.5,0,0,0-2.5-2.5h-2.43a2.5,2.5,0,0,0-2.5,2.5V11.5H11.5V6.58A2.5,2.5,0,0,0,9,4.08H6.566a2.5,2.5,0,0,0-2.5,2.5V11.5h-.52a.5.5,0,0,0,0,1h.52v4.92a2.5,2.5,0,0,0,2.5,2.5H9a2.5,2.5,0,0,0,2.5-2.5V12.5h1.01v2.43a2.5,2.5,0,0,0,2.5,2.5h2.43a2.5,2.5,0,0,0,2.5-2.5V12.5h.51A.5.5,0,0,0,20.446,11.5ZM10.5,17.42A1.5,1.5,0,0,1,9,18.92H6.566a1.5,1.5,0,0,1-1.5-1.5V12.5H10.5Zm0-5.92H5.066V6.58a1.5,1.5,0,0,1,1.5-1.5H9a1.5,1.5,0,0,1,1.5,1.5Zm8.44,3.43a1.5,1.5,0,0,1-1.5,1.5h-2.43a1.5,1.5,0,0,1-1.5-1.5V12.5h5.43Zm0-3.43h-5.43V9.07a1.5,1.5,0,0,1,1.5-1.5h2.43a1.5,1.5,0,0,1,1.5,1.5Z"></path></g></svg>',
createdby: "Armco",
createdAt: "2023-10-14T20:46:51.541Z",
updatedAt: "2023-10-14T20:46:51.541Z",
meta: {
size: "784 bytes",
downloadTimes: 0,
favoriteTimes: 0,
},
group: "ci",
description: "",
tags: [
{
name: "Ci",
state: "verified",
},
{
name: "Align",
state: "verified",
},
{
name: "Center",
state: "verified",
},
{
name: "V",
state: "verified",
},
{
name: "icon",
state: "verified",
},
{
name: "armco",
state: "verified",
},
],
},
]
const IconMergeContainer = (props: IconMergeContainerProps): JSX.Element => {
const { icons } = props
const svgRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const childList: Array<SVGSVGElement> = []
iconTest?.forEach((icon) => {
const svgElement = DomHelper.svgStringToElement(icon.icon)
if (svgElement) {
childList.push(svgElement)
}
})
svgRef.current?.replaceChildren(...childList)
const paths = childList.reduce((acc: ArrayType, svgElement) => {
const pathList = svgElement.querySelectorAll("path")
console.log(pathList)
pathList.forEach((pathItem) => {
pathItem.setAttribute("draggable", "true")
pathItem.addEventListener("dragstart", (e) => console.log(e))
})
return acc.concat(Array.from(pathList) as any)
}, [])
;(paths[0] as any).parentElement?.replaceChildren(...paths)
}, [svgRef])
return <div className="ar-IconMergeContainer" ref={svgRef} />
}
export default IconMergeContainer

View File

@@ -0,0 +1,3 @@
import IconMergeContainer from "./IconMergeContainer"
export default IconMergeContainer

View File

@@ -0,0 +1,3 @@
.ar-IconStyleSelector {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import IconStyleSelector from "./IconStyleSelector"
describe("IconStyleSelector", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,87 @@
import { v4 as uuid } from "uuid"
import { IconStyleSelectorProps } from "../../types/components.interface"
import { IconStyles } from "../../types/entity.interface"
import AdvancedColorPicker from "../atoms/AdvancedColorPicker"
import { Slider } from ".."
import "./IconStyleSelector.component.scss"
const fillColorId = uuid()
const strokeColorId = uuid()
const bgColorId = uuid()
const IconStyleSelector = (props: IconStyleSelectorProps): JSX.Element => {
const { layout, setIconStyles } = props
const setIconStylesLocal = (propName: string, propValue: string) => {
console.log(propName)
setIconStyles((currentIconStyles: IconStyles) => ({
...currentIconStyles,
[propName]: propValue,
}))
}
const fill = (
<AdvancedColorPicker
key="fill-color-selector"
id={fillColorId}
displaySample
onColorSelect={(color) => setIconStylesLocal("fillColor", color)}
title="Fill Color"
/>
)
const strokeWidth = (
<Slider
containerClasses="py-2 border-top border-bottom mb-4"
label="Stroke Width"
min={1}
max={10}
onChange={(e) => setIconStylesLocal("strokeWidth", e.target.value)}
withManual
/>
)
const stroke = (
<AdvancedColorPicker
key="stoke-color-selector"
id={strokeColorId}
displaySample
onColorSelect={(color) => setIconStylesLocal("strokeColor", color)}
title="Stroke Color"
/>
)
const bg = (
<AdvancedColorPicker
key="bg-color-selector"
id={bgColorId}
displaySample
onColorSelect={(color) => setIconStylesLocal("bgColor", color)}
title="Background Color"
/>
)
return (
<div className="ar-IconStyleSelector p-3">
{layout === "horizontal" ? (
<>
<div className="d-flex">
<div className="px-3">{fill}</div>
<div className="px-3">{stroke}</div>
</div>
<div className="row">
<div className="col">{strokeWidth}</div>
</div>
</>
) : (
<>
{fill}
{strokeWidth}
{stroke}
{bg}
</>
)}
</div>
)
}
export default IconStyleSelector

View File

@@ -0,0 +1,3 @@
import IconStyleSelector from "./IconStyleSelector"
export default IconStyleSelector

View File

@@ -1,32 +1,67 @@
.c-IconsList {
.c-IconsList__header-pagination-search {
.ar-IconsList {
.ar-IconsList__header-pagination-search-upload {
background-color: var(--ar-bg);
color: var(--ar-colora);
background-color: var(--ar-bg-secondary);
}
.c-IconsList__icon-tile-container {
.ar-IconList__selection-manager {
background-color: var(--ar-bg);
color: var(--ar-colora);
transition: all 0.3s;
&.hide {
height: 0;
overflow: hidden;
padding: 0 !important;
border: none !important;
}
}
.ar-IconsList__icon-tile-container {
background-color: var(--ar-bg);
color: var(--ar-color);
}
.c-IconTile {
.ar-IconTile {
cursor: pointer;
width: 7rem;
height: 8.5rem;
height: 7rem;
transition: width 0.3s, padding 0.3s;
&.compact {
width: 4rem;
height: 4rem;
}
&.list {
width: 8rem;
height: 2rem;
}
&:hover {
background-color: var(--ar-bg-hover);
}
.c-IconTile__image {
height: 6.5rem;
}
footer {
height: 1.9rem;
border-top: 1px solid var(--bs-border-color);
background-color: var(--ar-bg-base);
}
}
.ar-IconsList__upload-button {
background-color: orange;
border-color: orange;
}
.ar-IconsList__color-palette {
border-radius: 50%;
}
@media screen and (max-width: 576px) {
.ar-IconTile {
width: 3rem;
height: 3rem;
}
}
}

View File

@@ -1,8 +1,5 @@
import React from "react"
import IconsList from "./IconsList"
describe("IconsList", () => {
it("renders without error", () => {
})
it("renders without error", () => {})
})

View File

@@ -1,67 +1,246 @@
import { ChangeEvent, useState } from "react"
import { Pagination, Search } from ".."
import Helper from "../../utils/helper"
import { ChangeEvent, useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { v4 as uuid } from "uuid"
import { useAppDispatch, useAppSelector } from "../../hooks"
import { notify, setRightPanelContent } from "../../store"
import {
getFavorites,
getSelectedTag,
removeFavorite,
setFavorites,
} from "../../pages/IconsPage/IconsPage.slice"
import {
getIconStyles,
setIconStyles,
} from "../../pages/IconPage/IconPage.slice"
import {
Button,
IconStyleSelector,
IconTile,
LoadableIcon,
Loader,
Pagination,
Pillbox,
Popover,
SearchField,
SegmentedControl,
} from ".."
import {
ArButtonVariants,
ArIconTileTypes,
ArLoaderTypes,
ArPopoverSlots,
ArPopoverTriggers,
ArSizes,
} from "../../types/enums"
import {
IconStyles,
SearchItem,
SegmentData,
} from "../../types/entity.interface"
import { IconTileProps, IconsListProps } from "../../types/components.interface"
import { FunctionType, ObjectType } from "../../types/types"
import { IconResponse } from "../../types/iconresponse.interface"
import { Helper, Network } from "../../utils"
import API_CONFIG from "../../config/api-config"
import { ENDPOINTS } from "../../config/constants"
import "./IconsList.component.scss"
interface IconsListProps {
icons?: Array<IconResponse>
onSearchChanged: Function
}
interface IconTileProps {
icon: IconResponse
}
const IconTile = (props: IconTileProps): JSX.Element => {
const { icon } = props
return (
<span
className="c-IconTile d-inline-block mx-2 mb-3 border"
onClick={() => {
const iconLink = `http://localhost:8080/icon/${icon.group}/${icon.name}`
navigator.clipboard.writeText(iconLink)
// TODO: replace below line with a dispatcher to show notification
// alert("Copied link to clipboard: " + iconLink)
}}
>
<div className="c-IconTile__image text-center">
<img
className="h-100"
src={`data:image/svg+xml;utf8,${icon.svg}`}
alt="icon"
/>
</div>
<footer className="fw-bold text-nowrap overflow-hidden flex-center">
{icon.name}
</footer>
</span>
)
const fetchIconsPage = (
limit: number,
from: number,
filters: ObjectType,
dataSetter: FunctionType,
pageSetter: FunctionType,
setLoading: FunctionType,
) => {
const pageApi =
API_CONFIG.STATIC_HOST[process.env.NODE_ENV] +
ENDPOINTS.STATIC.ICON.ROOT +
ENDPOINTS.STATIC.ICON.PAGE
const queryParams: ObjectType = { pageSize: limit, from, ...filters }
// if (
// filters &&
// typeof filters === "object" &&
// Object.keys(filters).length > 0
// ) {
// queryParams.filters = filters
// }
Network.get(pageApi, queryParams)
.then((response) => {
if (response && response.status === 200) {
if (response.body && dataSetter) {
dataSetter(response.body)
pageSetter(response.body.slice(0, 100))
}
}
setLoading(false)
})
.catch((error) => {
console.error(error)
setLoading(false)
})
}
const IconsList = (props: IconsListProps): JSX.Element => {
const { icons, onSearchChanged } = props
const slices =
icons &&
icons.length > 0 &&
Helper.generateSlices(icons.length, 100, "index")
const { variant } = props
const [searchText, setSearchText] = useState<string | undefined>()
const [icons, setIcons] = useState<Array<IconResponse>>()
const [page, setPage] = useState<Array<IconResponse>>()
const [view, setView] = useState<SegmentData>()
const [loading, setLoading] = useState<boolean>()
const [isSelectMode, toggleSelectMode] = useState<boolean>()
const [selectedIcons, setSelectedIcons] = useState<Array<IconResponse>>()
const dispatch = useAppDispatch()
const favorites = useAppSelector(getFavorites)
const selectedTag = useAppSelector<string | undefined>(getSelectedTag)
const iconStyles = useAppSelector<IconStyles | undefined>(getIconStyles)
const navigate = useNavigate()
useEffect(() => {
setView({ name: ArIconTileTypes.COMFY })
}, [])
useEffect(() => {
if (favorites) {
dispatch(
setRightPanelContent(
favorites.length > 0 ? { name: "FavoritesList" } : { name: "" },
),
)
}
}, [favorites, dispatch])
useEffect(() => {
const filters: ObjectType = {}
if (selectedTag) {
filters.tags = selectedTag
}
if (searchText) {
filters.search = searchText
}
setLoading(true)
fetchIconsPage(2000, 0, filters, setIcons, setPage, setLoading)
}, [selectedTag, searchText])
const onIconTileClick = (iconProps: IconResponse) => {
isSelectMode
? setSelectedIcons((currentIcons) => {
currentIcons = [...(currentIcons || [])]
const existingIconIndex = currentIcons?.findIndex(
(currentIcon) => currentIcon.name === iconProps.name,
)
if (existingIconIndex > -1) {
currentIcons.splice(existingIconIndex, 1)
} else {
currentIcons.push(iconProps)
}
return currentIcons
})
: view?.name === ArIconTileTypes.COMPACT &&
dispatch(
notify({
show: true,
message: `Icon link for ${iconProps.name} copied`,
uid: uuid(),
}),
)
}
return (
<div className="c-IconsList h-100 w-100 overflow-auto p-3">
<div className="c-IconsList__header-pagination-search py-2 px-3 mb-3 border">
<div className="ar-IconsList h-100 w-100 overflow-auto position-relative d-flex flex-column">
{!icons && (
<Loader label="Loading Icons..." type={ArLoaderTypes.SHAPES} />
)}
<div className="ar-IconsList__header-pagination-search-upload py-2 px-3 mb-2 border">
<div className="row">
<div className="col-2 d-flex align-items-center">
<h6 className="mb-0">Icon Repository</h6>
<div className="col d-none d-md-flex align-items-center">
<h6 className="mb-0 h-100 flex-center px-3 border-right">
<Button
variant={ArButtonVariants.LINK}
content="Categories"
size={ArSizes.SMALL}
splitOptions={[
{
label: "All",
},
{
label: "Business",
},
{
label: "Medical",
},
]}
/>
</h6>
</div>
<div className="col-7 flex-center">
{slices && <Pagination data={slices} maxPillsToShow={5} />}
</div>
<div className="col-3">
<Search
classes="bg-white h-100"
<span className="col flex-v-center justify-content-end">
<Popover>
<LoadableIcon
classes="ar-IconsList__color-palette cursor-pointer ms-auto hover-shadow me-3"
icon="io/IoIosColorPalette"
color="orange"
slot={ArPopoverSlots.ANCHOR}
size="2rem"
onClick={() => {}}
/>
<IconStyleSelector
slot={ArPopoverSlots.POPOVER}
setIconStyles={(iconStylesUpdater) => {
const updatedStyles = iconStylesUpdater(iconStyles)
dispatch(setIconStyles(updatedStyles))
}}
layout="horizontal"
/>
</Popover>
<Button
classes="h-100 float-end me-3"
content="Create"
size={ArSizes.SMALL}
variant={ArButtonVariants.LINKHOVEREFFECT}
preIcon="io5/IoCreateOutline"
onClick={() => toggleSelectMode(true)}
/>
<Button
classes="ar-IconsList__upload-button h-100 float-end"
content="Upload"
size={ArSizes.SMALL}
variant={ArButtonVariants.PRIMARY}
/>
<SegmentedControl
classes="d-none d-sm-inline ms-3"
hasUniformSegments
segments={[
{
name: "comfy",
icon: "md/MdViewComfy",
tooltip: "View Comfortable",
},
{
name: "compact",
icon: "md/MdViewCompact",
tooltip: "View Compact",
},
{
name: "list",
icon: "io5/IoList",
tooltip: "Quick Peak",
},
]}
onChange={(view) => setView(view as SegmentData)}
/>
</span>
<div className="col d-none d-md-flex flex-v-center">
<SearchField
classes="bg-white"
placeholder="Search by name, tags, description"
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onSearchChanged(event.target.value)
}
onChange={Helper.debounce(
(event: ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value),
1000,
)}
data={
icons
? icons.map(
@@ -76,12 +255,169 @@ const IconsList = (props: IconsListProps): JSX.Element => {
</div>
</div>
</div>
<div className="c-IconsList__icon-tile-container py-2 px-3 border d-flex justify-content-between flex-wrap">
{icons &&
icons
.slice(0, 100)
.map((icon, index) => <IconTile key={index} icon={icon} />)}
<div
className={`ar-IconList__selection-manager d-flex${
isSelectMode ? " mb-2 py-2 px-3 border" : " hide"
}`}
>
{selectedIcons && selectedIcons.length > 0 && (
<Pillbox
data={(selectedIcons || []).map((icon) => ({
label: icon.name,
deletable: true,
}))}
onChange={() => {}}
/>
)}
<div className="ms-auto d-flex">
<Button
classes="me-3"
content="Cancel"
variant={ArButtonVariants.SECONDARY}
size={ArSizes.SMALL}
onClick={() => {
toggleSelectMode(false)
setSelectedIcons(undefined)
}}
/>
<Button
content="Done"
variant={ArButtonVariants.PRIMARY}
size={ArSizes.SMALL}
onClick={() =>
navigate("/icons/merge-icons", { state: selectedIcons })
}
disabled={!selectedIcons || selectedIcons.length === 0}
/>
</div>
</div>
{page && (
<div className="ar-IconsList__icon-tile-container-wrapper d-flex flex-grow-1 justify-content-between">
<div className="ar-IconsList__icon-tile-container py-2 px-3 border d-flex flex-wrap w-100">
{loading && (
<Loader label="Applying filters..." type={ArLoaderTypes.CIRCLE} />
)}
{page.map((icon, index) => (
<Popover
trigger={ArPopoverTriggers.HOVER}
key={"icon-tile-popover-" + index}
>
{!view || view.name !== ArIconTileTypes.LIST ? (
<span slot={ArPopoverSlots.POPOVER}>
{icon.name
?.match(/[A-Z][a-z]+/g)
?.slice(1)
.join(" ")}
</span>
) : null}
<IconTile
type={
view && view.name
? (view.name as ArIconTileTypes)
: ArIconTileTypes.COMFY
}
hideBorder={view?.name !== ArIconTileTypes.LIST}
classes={view?.name}
slot={ArPopoverSlots.ANCHOR}
key={index}
icon={icon}
iconSize={
view?.name === ArIconTileTypes.LIST ? "1rem" : "2rem"
}
onClick={onIconTileClick}
tools={[
{
iconProps: {
icon: "sl.SlOptions",
color: "white",
hoverColor: "lightblue",
},
children: [
{
iconProps: {
icon: "io.IoIosShareAlt",
hoverColor: "lightblue",
},
name: "share",
onClick: () => {},
},
],
name: "more-options",
onClick: () => {},
},
{
iconProps: {
icon: "ai/AiFillLike",
color: "white",
hoverColor: "lightblue",
},
name: "like",
onClick: () => {},
},
{
iconProps: {
icon: "md/MdFavorite",
hoverColor: "lightblue",
toggleColor: "red",
color: "white",
toggled: false,
},
name: "favorite",
onClick: () => {
const favorite = favorites.find(
(searchedIcon: IconTileProps) =>
icon.name === searchedIcon.icon.name,
)
if (!favorites || !favorite) {
dispatch(setFavorites({ icon }))
dispatch(
notify({
message: "Icon Added to your favorites",
show: true,
uid: uuid(),
}),
)
} else {
dispatch(removeFavorite({ icon }))
dispatch(
notify({
message: "Icon removed from your favorites",
show: true,
uid: uuid(),
}),
)
}
},
},
]}
hideFooter={
(view && (view.name as ArIconTileTypes)) !==
ArIconTileTypes.LIST
}
selectable={isSelectMode}
fillColor={iconStyles?.fillColor}
strokeColor={iconStyles?.strokeColor}
strokeWidth={iconStyles?.strokeWidth}
/>
</Popover>
))}
</div>
</div>
)}
{/* {slices && ( */}
<Pagination
classes="my-3 flex-center"
data={icons}
maxPillsToShow={5}
pageSetter={setPage}
// trigger={ArPageTriggers.SCROLL}
count={1}
load={100}
dataFetcher={(load, count) =>
fetchIconsPage(load, count, {}, setIcons, setPage, setLoading)
}
/>
{/* )} */}
</div>
)
}

View File

@@ -0,0 +1,3 @@
.ar-LandingContent {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import LandingContent from "./LandingContent"
describe("LandingContent", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,82 @@
import { Hero, ProductDescriptionTile } from ".."
import { LandingContentProps } from "../../types/components.interface"
import { ArPlacement } from "../../types/enums"
import "./LandingContent.component.scss"
const toolsAndProducts = [
{
name: "Icons & Fonts",
description: [
"Dive into a world of creativity with our diverse icon assets and fonts.",
],
image: "",
imagePlacement: ArPlacement.RIGHT,
},
{
name: "Task Manager and Config Manager",
description: [
"Streamline project management and configuration tasks effortlessly.",
"Master project management and configurations with our intuitive tools.",
],
image: "",
imagePlacement: ArPlacement.RIGHT,
},
{
name: "Components Library",
description: [
"Indulge in Unmatched Creativity with Our Opinionated and highly customizable Component Library",
"Discover a component library like no other, meticulously crafted and opinionated for optimal performance. Our library isn't just about components; it's a playground for creators. Immerse yourself in the richness of our carefully curated elements, each designed with a distinct perspective on aesthetics and functionality.",
"Explore a library that doesn't just follow trends but sets them. Elevate your projects with components that speak your language, and let your creativity soar in a space that celebrates individuality. Welcome to a component library where opinion meets innovation, and your designs become a masterpiece.",
],
image: "",
imagePlacement: ArPlacement.RIGHT,
},
{
name: "Analytics Collector and Viewer",
description: [
"Showcase the analytics capabilities for data-driven decisions.",
"Turn data into insights with our integrated analytics tools.",
],
image: "",
imagePlacement: ArPlacement.RIGHT,
},
{
name: "IAM",
description: [
"Explain the identity and access management features.",
"Manage identities and access with ease using our robust IAM features.",
],
image: "",
imagePlacement: ArPlacement.RIGHT,
},
{
name: "APIs",
description: [
"Highlight the flexibility of APIs for integration into various workflows.",
"Harness the flexibility of APIs for seamless integration into your development workflow.",
],
image: "",
imagePlacement: ArPlacement.RIGHT,
},
]
const LandingContent = (props: LandingContentProps): JSX.Element => {
return (
<div className="ar-LandingContent">
<Hero classes="mb-3" />
<div className="ar-LandingContent__tiles-container container">
{toolsAndProducts.map((item) => {
return (
<div className="row">
<div className="col">
<ProductDescriptionTile classes="mb-3" {...item} />
</div>
</div>
)
})}
</div>
</div>
)
}
export default LandingContent

View File

@@ -0,0 +1,3 @@
import LandingContent from "./LandingContent"
export default LandingContent

View File

@@ -0,0 +1,3 @@
.ar-LoginProvider {
width: 30rem;
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import LoginProvider from "./LoginProvider"
describe("LoginProvider", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,30 @@
import { setRightPanelContent } from "../../store"
import { useAppDispatch } from "../../hooks"
import { Button } from ".."
import { LoginProviderProps } from "../../types/components.interface"
import { ArButtonVariants, ArSizes } from "../../types/enums"
import WEB_CONFIG from "../../config/web-config"
import "./LoginProvider.component.scss"
const LoginProvider = (props: LoginProviderProps): JSX.Element => {
const { url } = props
const dispatch = useAppDispatch()
return (
<div className="ar-LoginProvider position-relative h-100">
<iframe
src={url || WEB_CONFIG.IAM[process.env.NODE_ENV]}
title="IAM"
className="ar-LoginProvider__frame h-100 w-100"
/>
<Button
variant={ArButtonVariants.LINKHOVEREFFECT}
size={ArSizes.SMALL}
classes="position-absolute top-0 end-0"
content="Close"
onClick={() => dispatch(setRightPanelContent({ name: "" }))}
/>
</div>
)
}
export default LoginProvider

View File

@@ -0,0 +1,3 @@
import LoginProvider from "./LoginProvider"
export default LoginProvider

Some files were not shown because too many files have changed in this diff Show More