commit 6b7cd307546b89f10dc08dba9d822367e6c8c231 Author: Zahirul Iman Date: Wed Jun 4 23:43:21 2025 +0800 Enhanced API documentation with Scalar design, syntax highlighting, and comprehensive functionality diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..6f9f00f --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..969bbc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Nuxt dev/build outputs +.output +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example + +# Uploads directory +public/uploads/ diff --git a/.nuxtignore b/.nuxtignore new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c1c99bb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "vue3snippets.enable-compile-vue-file-on-did-save-code": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f452fe --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Nuxt 3 Minimal Starter + +Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more. + +## Setup + +Make sure to install the dependencies: + +```bash +# yarn +yarn install + +# npm +npm install + +# pnpm +pnpm install --shamefully-hoist +``` + +## Development Server + +Start the development server on http://localhost:3000 + +```bash +npm run dev +``` + +## Production + +Build the application for production: + +```bash +npm run build +``` + +Locally preview production build: + +```bash +npm run preview +``` + +Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information. +# corradAF + +This is the base project for corradAF. diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..6a71328 --- /dev/null +++ b/app.config.js @@ -0,0 +1,9 @@ +// app.config.ts +export default defineAppConfig({ + nuxtIcon: { + size: "24px", // default size applied + aliases: { + nuxt: "logos:nuxt-icon", + }, + }, +}); diff --git a/app.vue b/app.vue new file mode 100644 index 0000000..f27c589 --- /dev/null +++ b/app.vue @@ -0,0 +1,50 @@ + + + diff --git a/assets/css/menu-levels.css b/assets/css/menu-levels.css new file mode 100644 index 0000000..6b324be --- /dev/null +++ b/assets/css/menu-levels.css @@ -0,0 +1,119 @@ +/* Multi-level menu styling */ +.multi-level-menu { + border-left: 2px solid rgba(var(--color-primary), 0.3); +} + +/* Common styles for all menu levels */ +.navigation-item-wrapper a { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; +} + +/* Long menu text handling */ +.navigation-item-wrapper span { + text-overflow: ellipsis; + overflow: hidden; +} + +/* Show full text on hover */ +.navigation-item-wrapper a:hover span { + white-space: normal; + overflow: visible; + position: relative; + z-index: 10; +} + +/* Enhanced tooltip effect for very long menu items */ +.deepest-menu-item a:hover span { + background-color: rgba(var(--sidebar-menu), 0.95); + border-radius: 4px; + padding: 4px 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + max-width: 300px; +} + +/* Level-specific styles */ +.second-level-menu { + font-weight: 500; + padding-left: 0.25rem; +} + +.second-level-menu .mx-3, +.second-level-menu .mx-4 { + color: rgba(255, 255, 255, 0.95); + max-width: 200px; +} + +.third-level-menu { + font-style: italic; + padding-left: 0.5rem; +} + +.third-level-menu .mx-3, +.third-level-menu .mx-4 { + color: rgba(255, 255, 255, 0.9); + font-size: 0.95rem; + max-width: 180px; +} + +.deepest-menu-item { + padding-left: 0.75rem; + border-left: 2px solid rgba(var(--color-primary), 0.6); +} + +.deepest-menu-item .mx-3, +.deepest-menu-item .mx-4 { + color: rgba(255, 255, 255, 0.85); + font-size: 0.9rem; + max-width: 160px; +} + +/* Add visual indicators for each level */ +.second-level-menu a::before, +.third-level-menu a::before, +.deepest-menu-item a::before { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + margin-right: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.second-level-menu a::before { + background-color: rgba(var(--color-primary), 0.8); +} + +.third-level-menu a::before { + background-color: rgba(var(--color-accent), 0.8); + width: 5px; + height: 5px; +} + +.deepest-menu-item a::before { + background-color: rgba(var(--color-secondary), 0.8); + width: 4px; + height: 4px; +} + +/* Resonsive adjustments for different screen sizes */ +@media (max-width: 1200px) { + .second-level-menu .mx-3, + .second-level-menu .mx-4, + .third-level-menu .mx-3, + .third-level-menu .mx-4, + .deepest-menu-item .mx-3, + .deepest-menu-item .mx-4 { + max-width: 140px; + } +} + +@media (max-width: 992px) { + .navigation-item-wrapper span { + max-width: 120px; + } +} \ No newline at end of file diff --git a/assets/img/avatar/1.svg b/assets/img/avatar/1.svg new file mode 100644 index 0000000..8a1230a --- /dev/null +++ b/assets/img/avatar/1.svg @@ -0,0 +1 @@ +image/svg+xmlAdventurer NeutralLisa Wischofskyhttps://www.instagram.com/lischi_art/ \ No newline at end of file diff --git a/assets/img/avatar/2.svg b/assets/img/avatar/2.svg new file mode 100644 index 0000000..8036ad1 --- /dev/null +++ b/assets/img/avatar/2.svg @@ -0,0 +1 @@ +image/svg+xmlAdventurer NeutralLisa Wischofskyhttps://www.instagram.com/lischi_art/ \ No newline at end of file diff --git a/assets/img/avatar/3.svg b/assets/img/avatar/3.svg new file mode 100644 index 0000000..68b8c20 --- /dev/null +++ b/assets/img/avatar/3.svg @@ -0,0 +1 @@ +image/svg+xmlAdventurer NeutralLisa Wischofskyhttps://www.instagram.com/lischi_art/ \ No newline at end of file diff --git a/assets/img/avatar/4.svg b/assets/img/avatar/4.svg new file mode 100644 index 0000000..f948439 --- /dev/null +++ b/assets/img/avatar/4.svg @@ -0,0 +1 @@ +image/svg+xmlAdventurer NeutralLisa Wischofskyhttps://www.instagram.com/lischi_art/ \ No newline at end of file diff --git a/assets/img/avatar/user.webp b/assets/img/avatar/user.webp new file mode 100644 index 0000000..7c6f810 Binary files /dev/null and b/assets/img/avatar/user.webp differ diff --git a/assets/img/bg.jpg b/assets/img/bg.jpg new file mode 100644 index 0000000..a1d7ccd Binary files /dev/null and b/assets/img/bg.jpg differ diff --git a/assets/img/brand/google-logo.svg b/assets/img/brand/google-logo.svg new file mode 100644 index 0000000..b518c52 --- /dev/null +++ b/assets/img/brand/google-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/default-thumbnail.jpg b/assets/img/default-thumbnail.jpg new file mode 100644 index 0000000..832a898 Binary files /dev/null and b/assets/img/default-thumbnail.jpg differ diff --git a/assets/img/icon/check.svg b/assets/img/icon/check.svg new file mode 100644 index 0000000..1c20989 --- /dev/null +++ b/assets/img/icon/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/icon/chevron-right-dark.svg b/assets/img/icon/chevron-right-dark.svg new file mode 100644 index 0000000..52f6af8 --- /dev/null +++ b/assets/img/icon/chevron-right-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/icon/chevron-right.svg b/assets/img/icon/chevron-right.svg new file mode 100644 index 0000000..8abc75e --- /dev/null +++ b/assets/img/icon/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/illustration/404-2.svg b/assets/img/illustration/404-2.svg new file mode 100644 index 0000000..23defa0 --- /dev/null +++ b/assets/img/illustration/404-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/illustration/404.svg b/assets/img/illustration/404.svg new file mode 100644 index 0000000..e1c14a2 --- /dev/null +++ b/assets/img/illustration/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/illustration/500.svg b/assets/img/illustration/500.svg new file mode 100644 index 0000000..b544f74 --- /dev/null +++ b/assets/img/illustration/500.svg @@ -0,0 +1 @@ +monitor \ No newline at end of file diff --git a/assets/img/illustration/login.svg b/assets/img/illustration/login.svg new file mode 100644 index 0000000..4b04d69 --- /dev/null +++ b/assets/img/illustration/login.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/loader.gif b/assets/img/loader.gif new file mode 100644 index 0000000..870ab01 Binary files /dev/null and b/assets/img/loader.gif differ diff --git a/assets/img/logo/logo-imigresen.svg b/assets/img/logo/logo-imigresen.svg new file mode 100644 index 0000000..75750f9 --- /dev/null +++ b/assets/img/logo/logo-imigresen.svg @@ -0,0 +1,4140 @@ + + + + + + image/svg+xml + + Department of Immigration Malaysia Logo + + + + + Department of Immigration Malaysia Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/logo-nobg.svg b/assets/img/logo/logo-nobg.svg new file mode 100644 index 0000000..06d57b9 --- /dev/null +++ b/assets/img/logo/logo-nobg.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/logo-word-black-ai.svg b/assets/img/logo/logo-word-black-ai.svg new file mode 100644 index 0000000..6073931 --- /dev/null +++ b/assets/img/logo/logo-word-black-ai.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/logo-word-black.svg b/assets/img/logo/logo-word-black.svg new file mode 100644 index 0000000..eda6811 --- /dev/null +++ b/assets/img/logo/logo-word-black.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/logo-word-white-ai.svg b/assets/img/logo/logo-word-white-ai.svg new file mode 100644 index 0000000..2252264 --- /dev/null +++ b/assets/img/logo/logo-word-white-ai.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/logo-word-white.svg b/assets/img/logo/logo-word-white.svg new file mode 100644 index 0000000..9b622c0 --- /dev/null +++ b/assets/img/logo/logo-word-white.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/logo.svg b/assets/img/logo/logo.svg new file mode 100644 index 0000000..0bae9f2 --- /dev/null +++ b/assets/img/logo/logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/logo/lzs-logo.png b/assets/img/logo/lzs-logo.png new file mode 100644 index 0000000..904a2e3 Binary files /dev/null and b/assets/img/logo/lzs-logo.png differ diff --git a/assets/img/logo/niise-text.svg b/assets/img/logo/niise-text.svg new file mode 100644 index 0000000..02e8ca2 --- /dev/null +++ b/assets/img/logo/niise-text.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/img/logo/word-black-ai.svg b/assets/img/logo/word-black-ai.svg new file mode 100644 index 0000000..7d8700c --- /dev/null +++ b/assets/img/logo/word-black-ai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/img/logo/word-black.svg b/assets/img/logo/word-black.svg new file mode 100644 index 0000000..612f59d --- /dev/null +++ b/assets/img/logo/word-black.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/logo/word-white-ai.svg b/assets/img/logo/word-white-ai.svg new file mode 100644 index 0000000..c208246 --- /dev/null +++ b/assets/img/logo/word-white-ai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/img/logo/word-white.svg b/assets/img/logo/word-white.svg new file mode 100644 index 0000000..6785941 --- /dev/null +++ b/assets/img/logo/word-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/user/default.svg b/assets/img/user/default.svg new file mode 100644 index 0000000..f9f55f0 --- /dev/null +++ b/assets/img/user/default.svg @@ -0,0 +1 @@ +JD \ No newline at end of file diff --git a/assets/img/user/profile-1.jpg b/assets/img/user/profile-1.jpg new file mode 100644 index 0000000..68d5926 Binary files /dev/null and b/assets/img/user/profile-1.jpg differ diff --git a/assets/js/formkit-custom.js b/assets/js/formkit-custom.js new file mode 100644 index 0000000..ea02554 --- /dev/null +++ b/assets/js/formkit-custom.js @@ -0,0 +1,20 @@ +import { createInput } from "@formkit/vue"; +import OneTimePassword from "~/components/formkit/OneTimePassword.vue"; +import MaskText from "~/components/formkit/TextMask.vue"; +import FileDropzone from "~/components/formkit/FileDropzone.vue"; +import Toggle from "~/components/formkit/Toggle.vue"; + +export default { + otp: createInput(OneTimePassword, { + props: ["digits"], + }), + mask: createInput(MaskText, { + props: ["mask"], + }), + dropzone: createInput(FileDropzone, { + props: ["accept", "multiple", "maxSize", "minSize", "maxFiles", "disabled"], + }), + toggle: createInput(Toggle, { + props: ["onLabel", "offLabel"], + }), +}; diff --git a/assets/js/formkit-theme.js b/assets/js/formkit-theme.js new file mode 100644 index 0000000..dd57e22 --- /dev/null +++ b/assets/js/formkit-theme.js @@ -0,0 +1,94 @@ +// Create some re-useable definitions because +// many input types are identical in how +// we want to style them. +const textClassification = { + label: "formkit-outer-text", + inner: "formkit-inner-text", + input: "formkit-input-text", + prefix: "formkit-prefix-text", + message: "formkit-message-text", +}; +const boxClassification = { + inner: "formkit-inner-box", + fieldset: "formkit-fieldset-box", + legend: "formkit-legend-box", + wrapper: "formkit-wrapper-box", + help: "formkit-help-box", + input: "formkit-input-box", + label: "formkit-label-box", + message: "formkit-message-box", +}; +const buttonClassification = { + wrapper: "formkit-wrapper-button", + input: "formkit-input-button", +}; +const OtpClassification = { + label: "formkit-label-otp", + inner: "formkit-inner-otp", + digit: "formkit-digit-otp", + message: "formkit-message-otp", +}; + +const colorClassification = { + label: "formkit-label-color", + input: "formkit-input-color", +}; + +const fileClassification = { + label: "formkit-label-file", + inner: "formkit-inner-file", + input: "formkit-input-file", +}; + +const rangeClassification = { + input: "formkit-input-range", +}; + +// export our definitions using our above +// templates and declare one-offs and +// overrides as needed. +export default { + // the global key will apply to all inputs + global: { + label: "formkit-label-global", + outer: "formkit-outer-global", + help: "formkit-help-global", + messages: "formkit-messages-global", + message: "formkit-message-global", + wrapper: "formkit-wrapper-global", + }, + button: buttonClassification, + color: colorClassification, + date: textClassification, + "datetime-local": textClassification, + checkbox: boxClassification, + email: textClassification, + file: fileClassification, + month: textClassification, + number: textClassification, + password: textClassification, + radio: { + ...boxClassification, + input: "formkit-input-radio", + }, + range: rangeClassification, + search: textClassification, + select: { ...textClassification, option: "p-2" }, + submit: buttonClassification, + tel: textClassification, + text: textClassification, + textarea: { + ...textClassification, + input: "formkit-input-textarea", + }, + time: textClassification, + url: textClassification, + week: textClassification, + otp: OtpClassification, + mask: textClassification, + dropzone: { + ...textClassification, + inner: "formkit-inner-dropzone", + dropzone: "formkit-dropzone", + }, +}; diff --git a/assets/json/data.json b/assets/json/data.json new file mode 100644 index 0000000..a6b2942 --- /dev/null +++ b/assets/json/data.json @@ -0,0 +1,92 @@ +[ + { + "id": "#001", + "firstName": "John", + "lastName": "Doe", + "email": "johndoe@example.com", + "gender": "Male", + "status": "Active", + "age": 34 + }, + { + "id": "#002", + "firstName": "Jane", + "lastName": "Smith", + "email": "janesmith@example.com", + "gender": "Female", + "status": "Inactive", + "age": 28 + }, + { + "id": "#003", + "firstName": "Robert", + "lastName": "Brown", + "email": "robertbrown@example.com", + "gender": "Male", + "status": "Banned", + "age": 45 + }, + { + "id": "#004", + "firstName": "Emily", + "lastName": "White", + "email": "emilywhite@example.com", + "gender": "Female", + "status": "Active", + "age": 37 + }, + { + "id": "#005", + "firstName": "Michael", + "lastName": "Johnson", + "email": "michaeljohnson@example.com", + "gender": "Male", + "status": "Inactive", + "age": 50 + }, + { + "id": "#006", + "firstName": "Linda", + "lastName": "Williams", + "email": "lindawilliams@example.com", + "gender": "Female", + "status": "Active", + "age": 32 + }, + { + "id": "#007", + "firstName": "James", + "lastName": "Taylor", + "email": "jamestaylor@example.com", + "gender": "Male", + "status": "Banned", + "age": 40 + }, + { + "id": "#008", + "firstName": "Patricia", + "lastName": "Brown", + "email": "patriciabrown@example.com", + "gender": "Female", + "status": "Inactive", + "age": 29 + }, + { + "id": "#009", + "firstName": "David", + "lastName": "Wilson", + "email": "davidwilson@example.com", + "gender": "Male", + "status": "Active", + "age": 38 + }, + { + "id": "#010", + "firstName": "Elizabeth", + "lastName": "Garcia", + "email": "elizabethgarcia@example.com", + "gender": "Female", + "status": "Banned", + "age": 42 + } +] diff --git a/assets/json/iconamoon.json b/assets/json/iconamoon.json new file mode 100644 index 0000000..2fe9c49 --- /dev/null +++ b/assets/json/iconamoon.json @@ -0,0 +1,7126 @@ +[ + { + "name": "iconamoon:3d", + "svg": "" + }, + { + "name": "iconamoon:3d-bold", + "svg": "" + }, + { + "name": "iconamoon:3d-duotone", + "svg": "" + }, + { + "name": "iconamoon:3d-fill", + "svg": "" + }, + { + "name": "iconamoon:3d-light", + "svg": "" + }, + { + "name": "iconamoon:3d-thin", + "svg": "" + }, + { + "name": "iconamoon:apps", + "svg": "" + }, + { + "name": "iconamoon:apps-bold", + "svg": "" + }, + { + "name": "iconamoon:apps-fill", + "svg": "" + }, + { + "name": "iconamoon:apps-light", + "svg": "" + }, + { + "name": "iconamoon:apps-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-left-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-right-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-up-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-up-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-up-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-up-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-up-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-bottom-up-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-down-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-left-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-right-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-left-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-top-right-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-1", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-1-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-1-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-1-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-1-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-2", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-2-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-2-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-2-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-2-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-3-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-4-square", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-5-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-6-circle", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:arrow-up-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:attachment", + "svg": "" + }, + { + "name": "iconamoon:attachment-bold", + "svg": "" + }, + { + "name": "iconamoon:attachment-duotone", + "svg": "" + }, + { + "name": "iconamoon:attachment-fill", + "svg": "" + }, + { + "name": "iconamoon:attachment-light", + "svg": "" + }, + { + "name": "iconamoon:attachment-thin", + "svg": "" + }, + { + "name": "iconamoon:attention-circle", + "svg": "" + }, + { + "name": "iconamoon:attention-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:attention-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:attention-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:attention-circle-light", + "svg": "" + }, + { + "name": "iconamoon:attention-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:attention-square", + "svg": "" + }, + { + "name": "iconamoon:attention-square-bold", + "svg": "" + }, + { + "name": "iconamoon:attention-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:attention-square-fill", + "svg": "" + }, + { + "name": "iconamoon:attention-square-light", + "svg": "" + }, + { + "name": "iconamoon:attention-square-thin", + "svg": "" + }, + { + "name": "iconamoon:backspace", + "svg": "" + }, + { + "name": "iconamoon:backspace-bold", + "svg": "" + }, + { + "name": "iconamoon:backspace-duotone", + "svg": "" + }, + { + "name": "iconamoon:backspace-fill", + "svg": "" + }, + { + "name": "iconamoon:backspace-light", + "svg": "" + }, + { + "name": "iconamoon:backspace-thin", + "svg": "" + }, + { + "name": "iconamoon:badge", + "svg": "" + }, + { + "name": "iconamoon:badge-bold", + "svg": "" + }, + { + "name": "iconamoon:badge-duotone", + "svg": "" + }, + { + "name": "iconamoon:badge-fill", + "svg": "" + }, + { + "name": "iconamoon:badge-light", + "svg": "" + }, + { + "name": "iconamoon:badge-thin", + "svg": "" + }, + { + "name": "iconamoon:bluetooth", + "svg": "" + }, + { + "name": "iconamoon:bluetooth-bold", + "svg": "" + }, + { + "name": "iconamoon:bluetooth-duotone", + "svg": "" + }, + { + "name": "iconamoon:bluetooth-fill", + "svg": "" + }, + { + "name": "iconamoon:bluetooth-light", + "svg": "" + }, + { + "name": "iconamoon:bluetooth-thin", + "svg": "" + }, + { + "name": "iconamoon:bookmark", + "svg": "" + }, + { + "name": "iconamoon:bookmark-bold", + "svg": "" + }, + { + "name": "iconamoon:bookmark-duotone", + "svg": "" + }, + { + "name": "iconamoon:bookmark-fill", + "svg": "" + }, + { + "name": "iconamoon:bookmark-light", + "svg": "" + }, + { + "name": "iconamoon:bookmark-off", + "svg": "" + }, + { + "name": "iconamoon:bookmark-off-bold", + "svg": "" + }, + { + "name": "iconamoon:bookmark-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:bookmark-off-fill", + "svg": "" + }, + { + "name": "iconamoon:bookmark-off-light", + "svg": "" + }, + { + "name": "iconamoon:bookmark-off-thin", + "svg": "" + }, + { + "name": "iconamoon:bookmark-thin", + "svg": "" + }, + { + "name": "iconamoon:box", + "svg": "" + }, + { + "name": "iconamoon:box-bold", + "svg": "" + }, + { + "name": "iconamoon:box-duotone", + "svg": "" + }, + { + "name": "iconamoon:box-fill", + "svg": "" + }, + { + "name": "iconamoon:box-light", + "svg": "" + }, + { + "name": "iconamoon:box-thin", + "svg": "" + }, + { + "name": "iconamoon:briefcase", + "svg": "" + }, + { + "name": "iconamoon:briefcase-bold", + "svg": "" + }, + { + "name": "iconamoon:briefcase-duotone", + "svg": "" + }, + { + "name": "iconamoon:briefcase-fill", + "svg": "" + }, + { + "name": "iconamoon:briefcase-light", + "svg": "" + }, + { + "name": "iconamoon:briefcase-thin", + "svg": "" + }, + { + "name": "iconamoon:calculator", + "svg": "" + }, + { + "name": "iconamoon:calculator-bold", + "svg": "" + }, + { + "name": "iconamoon:calculator-duotone", + "svg": "" + }, + { + "name": "iconamoon:calculator-fill", + "svg": "" + }, + { + "name": "iconamoon:calculator-light", + "svg": "" + }, + { + "name": "iconamoon:calculator-thin", + "svg": "" + }, + { + "name": "iconamoon:calendar-1", + "svg": "" + }, + { + "name": "iconamoon:calendar-1-bold", + "svg": "" + }, + { + "name": "iconamoon:calendar-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:calendar-1-fill", + "svg": "" + }, + { + "name": "iconamoon:calendar-1-light", + "svg": "" + }, + { + "name": "iconamoon:calendar-1-thin", + "svg": "" + }, + { + "name": "iconamoon:calendar-2", + "svg": "" + }, + { + "name": "iconamoon:calendar-2-bold", + "svg": "" + }, + { + "name": "iconamoon:calendar-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:calendar-2-fill", + "svg": "" + }, + { + "name": "iconamoon:calendar-2-light", + "svg": "" + }, + { + "name": "iconamoon:calendar-2-thin", + "svg": "" + }, + { + "name": "iconamoon:calendar-add", + "svg": "" + }, + { + "name": "iconamoon:calendar-add-bold", + "svg": "" + }, + { + "name": "iconamoon:calendar-add-duotone", + "svg": "" + }, + { + "name": "iconamoon:calendar-add-fill", + "svg": "" + }, + { + "name": "iconamoon:calendar-add-light", + "svg": "" + }, + { + "name": "iconamoon:calendar-add-thin", + "svg": "" + }, + { + "name": "iconamoon:calendar-remove", + "svg": "" + }, + { + "name": "iconamoon:calendar-remove-bold", + "svg": "" + }, + { + "name": "iconamoon:calendar-remove-duotone", + "svg": "" + }, + { + "name": "iconamoon:calendar-remove-fill", + "svg": "" + }, + { + "name": "iconamoon:calendar-remove-light", + "svg": "" + }, + { + "name": "iconamoon:calendar-remove-thin", + "svg": "" + }, + { + "name": "iconamoon:camera-image", + "svg": "" + }, + { + "name": "iconamoon:camera-image-bold", + "svg": "" + }, + { + "name": "iconamoon:camera-image-duotone", + "svg": "" + }, + { + "name": "iconamoon:camera-image-fill", + "svg": "" + }, + { + "name": "iconamoon:camera-image-light", + "svg": "" + }, + { + "name": "iconamoon:camera-image-thin", + "svg": "" + }, + { + "name": "iconamoon:camera-video", + "svg": "" + }, + { + "name": "iconamoon:camera-video-bold", + "svg": "" + }, + { + "name": "iconamoon:camera-video-duotone", + "svg": "" + }, + { + "name": "iconamoon:camera-video-fill", + "svg": "" + }, + { + "name": "iconamoon:camera-video-light", + "svg": "" + }, + { + "name": "iconamoon:camera-video-thin", + "svg": "" + }, + { + "name": "iconamoon:category", + "svg": "" + }, + { + "name": "iconamoon:category-bold", + "svg": "" + }, + { + "name": "iconamoon:category-duotone", + "svg": "" + }, + { + "name": "iconamoon:category-fill", + "svg": "" + }, + { + "name": "iconamoon:category-light", + "svg": "" + }, + { + "name": "iconamoon:category-thin", + "svg": "" + }, + { + "name": "iconamoon:certificate-badge", + "svg": "" + }, + { + "name": "iconamoon:certificate-badge-bold", + "svg": "" + }, + { + "name": "iconamoon:certificate-badge-duotone", + "svg": "" + }, + { + "name": "iconamoon:certificate-badge-fill", + "svg": "" + }, + { + "name": "iconamoon:certificate-badge-light", + "svg": "" + }, + { + "name": "iconamoon:certificate-badge-thin", + "svg": "" + }, + { + "name": "iconamoon:check", + "svg": "" + }, + { + "name": "iconamoon:check-bold", + "svg": "" + }, + { + "name": "iconamoon:check-circle-1", + "svg": "" + }, + { + "name": "iconamoon:check-circle-1-bold", + "svg": "" + }, + { + "name": "iconamoon:check-circle-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:check-circle-1-fill", + "svg": "" + }, + { + "name": "iconamoon:check-circle-1-light", + "svg": "" + }, + { + "name": "iconamoon:check-circle-1-thin", + "svg": "" + }, + { + "name": "iconamoon:check-circle-2", + "svg": "" + }, + { + "name": "iconamoon:check-circle-2-bold", + "svg": "" + }, + { + "name": "iconamoon:check-circle-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:check-circle-2-fill", + "svg": "" + }, + { + "name": "iconamoon:check-circle-2-light", + "svg": "" + }, + { + "name": "iconamoon:check-circle-2-thin", + "svg": "" + }, + { + "name": "iconamoon:check-duotone", + "svg": "" + }, + { + "name": "iconamoon:check-fill", + "svg": "" + }, + { + "name": "iconamoon:check-light", + "svg": "" + }, + { + "name": "iconamoon:check-square", + "svg": "" + }, + { + "name": "iconamoon:check-square-bold", + "svg": "" + }, + { + "name": "iconamoon:check-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:check-square-fill", + "svg": "" + }, + { + "name": "iconamoon:check-square-light", + "svg": "" + }, + { + "name": "iconamoon:check-square-thin", + "svg": "" + }, + { + "name": "iconamoon:check-thin", + "svg": "" + }, + { + "name": "iconamoon:cheque", + "svg": "" + }, + { + "name": "iconamoon:cheque-bold", + "svg": "" + }, + { + "name": "iconamoon:cheque-duotone", + "svg": "" + }, + { + "name": "iconamoon:cheque-fill", + "svg": "" + }, + { + "name": "iconamoon:cheque-light", + "svg": "" + }, + { + "name": "iconamoon:cheque-thin", + "svg": "" + }, + { + "name": "iconamoon:clock", + "svg": "" + }, + { + "name": "iconamoon:clock-bold", + "svg": "" + }, + { + "name": "iconamoon:clock-duotone", + "svg": "" + }, + { + "name": "iconamoon:clock-fill", + "svg": "" + }, + { + "name": "iconamoon:clock-light", + "svg": "" + }, + { + "name": "iconamoon:clock-thin", + "svg": "" + }, + { + "name": "iconamoon:close", + "svg": "" + }, + { + "name": "iconamoon:close-bold", + "svg": "" + }, + { + "name": "iconamoon:close-circle-1", + "svg": "" + }, + { + "name": "iconamoon:close-circle-1-bold", + "svg": "" + }, + { + "name": "iconamoon:close-circle-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:close-circle-1-fill", + "svg": "" + }, + { + "name": "iconamoon:close-circle-1-light", + "svg": "" + }, + { + "name": "iconamoon:close-circle-1-thin", + "svg": "" + }, + { + "name": "iconamoon:close-circle-2", + "svg": "" + }, + { + "name": "iconamoon:close-circle-2-bold", + "svg": "" + }, + { + "name": "iconamoon:close-circle-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:close-circle-2-fill", + "svg": "" + }, + { + "name": "iconamoon:close-circle-2-light", + "svg": "" + }, + { + "name": "iconamoon:close-circle-2-thin", + "svg": "" + }, + { + "name": "iconamoon:close-duotone", + "svg": "" + }, + { + "name": "iconamoon:close-fill", + "svg": "" + }, + { + "name": "iconamoon:close-light", + "svg": "" + }, + { + "name": "iconamoon:close-square", + "svg": "" + }, + { + "name": "iconamoon:close-square-bold", + "svg": "" + }, + { + "name": "iconamoon:close-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:close-square-fill", + "svg": "" + }, + { + "name": "iconamoon:close-square-light", + "svg": "" + }, + { + "name": "iconamoon:close-square-thin", + "svg": "" + }, + { + "name": "iconamoon:close-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud", + "svg": "" + }, + { + "name": "iconamoon:cloud-add", + "svg": "" + }, + { + "name": "iconamoon:cloud-add-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-add-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-add-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-add-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-add-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-clock", + "svg": "" + }, + { + "name": "iconamoon:cloud-clock-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-clock-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-clock-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-clock-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-clock-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-download", + "svg": "" + }, + { + "name": "iconamoon:cloud-download-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-download-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-download-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-download-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-download-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-error", + "svg": "" + }, + { + "name": "iconamoon:cloud-error-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-error-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-error-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-error-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-error-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-no", + "svg": "" + }, + { + "name": "iconamoon:cloud-no-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-no-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-no-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-no-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-no-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-off", + "svg": "" + }, + { + "name": "iconamoon:cloud-off-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-off-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-off-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-off-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-remove", + "svg": "" + }, + { + "name": "iconamoon:cloud-remove-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-remove-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-remove-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-remove-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-remove-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-upload", + "svg": "" + }, + { + "name": "iconamoon:cloud-upload-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-upload-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-upload-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-upload-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-upload-thin", + "svg": "" + }, + { + "name": "iconamoon:cloud-yes", + "svg": "" + }, + { + "name": "iconamoon:cloud-yes-bold", + "svg": "" + }, + { + "name": "iconamoon:cloud-yes-duotone", + "svg": "" + }, + { + "name": "iconamoon:cloud-yes-fill", + "svg": "" + }, + { + "name": "iconamoon:cloud-yes-light", + "svg": "" + }, + { + "name": "iconamoon:cloud-yes-thin", + "svg": "" + }, + { + "name": "iconamoon:comment", + "svg": "" + }, + { + "name": "iconamoon:comment-add", + "svg": "" + }, + { + "name": "iconamoon:comment-add-bold", + "svg": "" + }, + { + "name": "iconamoon:comment-add-duotone", + "svg": "" + }, + { + "name": "iconamoon:comment-add-fill", + "svg": "" + }, + { + "name": "iconamoon:comment-add-light", + "svg": "" + }, + { + "name": "iconamoon:comment-add-thin", + "svg": "" + }, + { + "name": "iconamoon:comment-bold", + "svg": "" + }, + { + "name": "iconamoon:comment-check", + "svg": "" + }, + { + "name": "iconamoon:comment-check-bold", + "svg": "" + }, + { + "name": "iconamoon:comment-check-duotone", + "svg": "" + }, + { + "name": "iconamoon:comment-check-fill", + "svg": "" + }, + { + "name": "iconamoon:comment-check-light", + "svg": "" + }, + { + "name": "iconamoon:comment-check-thin", + "svg": "" + }, + { + "name": "iconamoon:comment-close", + "svg": "" + }, + { + "name": "iconamoon:comment-close-bold", + "svg": "" + }, + { + "name": "iconamoon:comment-close-duotone", + "svg": "" + }, + { + "name": "iconamoon:comment-close-fill", + "svg": "" + }, + { + "name": "iconamoon:comment-close-light", + "svg": "" + }, + { + "name": "iconamoon:comment-close-thin", + "svg": "" + }, + { + "name": "iconamoon:comment-dots", + "svg": "" + }, + { + "name": "iconamoon:comment-dots-bold", + "svg": "" + }, + { + "name": "iconamoon:comment-dots-duotone", + "svg": "" + }, + { + "name": "iconamoon:comment-dots-fill", + "svg": "" + }, + { + "name": "iconamoon:comment-dots-light", + "svg": "" + }, + { + "name": "iconamoon:comment-dots-thin", + "svg": "" + }, + { + "name": "iconamoon:comment-duotone", + "svg": "" + }, + { + "name": "iconamoon:comment-fill", + "svg": "" + }, + { + "name": "iconamoon:comment-light", + "svg": "" + }, + { + "name": "iconamoon:comment-remove", + "svg": "" + }, + { + "name": "iconamoon:comment-remove-bold", + "svg": "" + }, + { + "name": "iconamoon:comment-remove-duotone", + "svg": "" + }, + { + "name": "iconamoon:comment-remove-fill", + "svg": "" + }, + { + "name": "iconamoon:comment-remove-light", + "svg": "" + }, + { + "name": "iconamoon:comment-remove-thin", + "svg": "" + }, + { + "name": "iconamoon:comment-thin", + "svg": "" + }, + { + "name": "iconamoon:compare", + "svg": "" + }, + { + "name": "iconamoon:compare-bold", + "svg": "" + }, + { + "name": "iconamoon:compare-duotone", + "svg": "" + }, + { + "name": "iconamoon:compare-fill", + "svg": "" + }, + { + "name": "iconamoon:compare-light", + "svg": "" + }, + { + "name": "iconamoon:compare-thin", + "svg": "" + }, + { + "name": "iconamoon:component", + "svg": "" + }, + { + "name": "iconamoon:component-bold", + "svg": "" + }, + { + "name": "iconamoon:component-duotone", + "svg": "" + }, + { + "name": "iconamoon:component-fill", + "svg": "" + }, + { + "name": "iconamoon:component-light", + "svg": "" + }, + { + "name": "iconamoon:component-thin", + "svg": "" + }, + { + "name": "iconamoon:confused-face", + "svg": "" + }, + { + "name": "iconamoon:confused-face-bold", + "svg": "" + }, + { + "name": "iconamoon:confused-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:confused-face-fill", + "svg": "" + }, + { + "name": "iconamoon:confused-face-light", + "svg": "" + }, + { + "name": "iconamoon:confused-face-thin", + "svg": "" + }, + { + "name": "iconamoon:copy", + "svg": "" + }, + { + "name": "iconamoon:copy-bold", + "svg": "" + }, + { + "name": "iconamoon:copy-duotone", + "svg": "" + }, + { + "name": "iconamoon:copy-fill", + "svg": "" + }, + { + "name": "iconamoon:copy-light", + "svg": "" + }, + { + "name": "iconamoon:copy-thin", + "svg": "" + }, + { + "name": "iconamoon:credit-card", + "svg": "" + }, + { + "name": "iconamoon:credit-card-bold", + "svg": "" + }, + { + "name": "iconamoon:credit-card-duotone", + "svg": "" + }, + { + "name": "iconamoon:credit-card-fill", + "svg": "" + }, + { + "name": "iconamoon:credit-card-light", + "svg": "" + }, + { + "name": "iconamoon:credit-card-thin", + "svg": "" + }, + { + "name": "iconamoon:cursor", + "svg": "" + }, + { + "name": "iconamoon:cursor-bold", + "svg": "" + }, + { + "name": "iconamoon:cursor-duotone", + "svg": "" + }, + { + "name": "iconamoon:cursor-fill", + "svg": "" + }, + { + "name": "iconamoon:cursor-light", + "svg": "" + }, + { + "name": "iconamoon:cursor-thin", + "svg": "" + }, + { + "name": "iconamoon:delivery", + "svg": "" + }, + { + "name": "iconamoon:delivery-bold", + "svg": "" + }, + { + "name": "iconamoon:delivery-duotone", + "svg": "" + }, + { + "name": "iconamoon:delivery-fast", + "svg": "" + }, + { + "name": "iconamoon:delivery-fast-bold", + "svg": "" + }, + { + "name": "iconamoon:delivery-fast-duotone", + "svg": "" + }, + { + "name": "iconamoon:delivery-fast-fill", + "svg": "" + }, + { + "name": "iconamoon:delivery-fast-light", + "svg": "" + }, + { + "name": "iconamoon:delivery-fast-thin", + "svg": "" + }, + { + "name": "iconamoon:delivery-fill", + "svg": "" + }, + { + "name": "iconamoon:delivery-free", + "svg": "" + }, + { + "name": "iconamoon:delivery-free-bold", + "svg": "" + }, + { + "name": "iconamoon:delivery-free-duotone", + "svg": "" + }, + { + "name": "iconamoon:delivery-free-fill", + "svg": "" + }, + { + "name": "iconamoon:delivery-free-light", + "svg": "" + }, + { + "name": "iconamoon:delivery-free-thin", + "svg": "" + }, + { + "name": "iconamoon:delivery-light", + "svg": "" + }, + { + "name": "iconamoon:delivery-thin", + "svg": "" + }, + { + "name": "iconamoon:disappointed-face", + "svg": "" + }, + { + "name": "iconamoon:disappointed-face-bold", + "svg": "" + }, + { + "name": "iconamoon:disappointed-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:disappointed-face-fill", + "svg": "" + }, + { + "name": "iconamoon:disappointed-face-light", + "svg": "" + }, + { + "name": "iconamoon:disappointed-face-thin", + "svg": "" + }, + { + "name": "iconamoon:discount", + "svg": "" + }, + { + "name": "iconamoon:discount-bold", + "svg": "" + }, + { + "name": "iconamoon:discount-duotone", + "svg": "" + }, + { + "name": "iconamoon:discount-fill", + "svg": "" + }, + { + "name": "iconamoon:discount-light", + "svg": "" + }, + { + "name": "iconamoon:discount-thin", + "svg": "" + }, + { + "name": "iconamoon:discover", + "svg": "" + }, + { + "name": "iconamoon:discover-bold", + "svg": "" + }, + { + "name": "iconamoon:discover-duotone", + "svg": "" + }, + { + "name": "iconamoon:discover-fill", + "svg": "" + }, + { + "name": "iconamoon:discover-light", + "svg": "" + }, + { + "name": "iconamoon:discover-thin", + "svg": "" + }, + { + "name": "iconamoon:dislike", + "svg": "" + }, + { + "name": "iconamoon:dislike-bold", + "svg": "" + }, + { + "name": "iconamoon:dislike-duotone", + "svg": "" + }, + { + "name": "iconamoon:dislike-fill", + "svg": "" + }, + { + "name": "iconamoon:dislike-light", + "svg": "" + }, + { + "name": "iconamoon:dislike-thin", + "svg": "" + }, + { + "name": "iconamoon:do-redo", + "svg": "" + }, + { + "name": "iconamoon:do-redo-bold", + "svg": "" + }, + { + "name": "iconamoon:do-redo-duotone", + "svg": "" + }, + { + "name": "iconamoon:do-redo-fill", + "svg": "" + }, + { + "name": "iconamoon:do-redo-light", + "svg": "" + }, + { + "name": "iconamoon:do-redo-thin", + "svg": "" + }, + { + "name": "iconamoon:do-undo", + "svg": "" + }, + { + "name": "iconamoon:do-undo-bold", + "svg": "" + }, + { + "name": "iconamoon:do-undo-duotone", + "svg": "" + }, + { + "name": "iconamoon:do-undo-fill", + "svg": "" + }, + { + "name": "iconamoon:do-undo-light", + "svg": "" + }, + { + "name": "iconamoon:do-undo-thin", + "svg": "" + }, + { + "name": "iconamoon:download", + "svg": "" + }, + { + "name": "iconamoon:download-bold", + "svg": "" + }, + { + "name": "iconamoon:download-fill", + "svg": "" + }, + { + "name": "iconamoon:download-light", + "svg": "" + }, + { + "name": "iconamoon:download-thin", + "svg": "" + }, + { + "name": "iconamoon:edit", + "svg": "" + }, + { + "name": "iconamoon:edit-bold", + "svg": "" + }, + { + "name": "iconamoon:edit-duotone", + "svg": "" + }, + { + "name": "iconamoon:edit-fill", + "svg": "" + }, + { + "name": "iconamoon:edit-light", + "svg": "" + }, + { + "name": "iconamoon:edit-thin", + "svg": "" + }, + { + "name": "iconamoon:email", + "svg": "" + }, + { + "name": "iconamoon:email-bold", + "svg": "" + }, + { + "name": "iconamoon:email-duotone", + "svg": "" + }, + { + "name": "iconamoon:email-fill", + "svg": "" + }, + { + "name": "iconamoon:email-light", + "svg": "" + }, + { + "name": "iconamoon:email-thin", + "svg": "" + }, + { + "name": "iconamoon:enter", + "svg": "" + }, + { + "name": "iconamoon:enter-bold", + "svg": "" + }, + { + "name": "iconamoon:enter-duotone", + "svg": "" + }, + { + "name": "iconamoon:enter-fill", + "svg": "" + }, + { + "name": "iconamoon:enter-light", + "svg": "" + }, + { + "name": "iconamoon:enter-thin", + "svg": "" + }, + { + "name": "iconamoon:exit", + "svg": "" + }, + { + "name": "iconamoon:exit-bold", + "svg": "" + }, + { + "name": "iconamoon:exit-fill", + "svg": "" + }, + { + "name": "iconamoon:exit-light", + "svg": "" + }, + { + "name": "iconamoon:exit-thin", + "svg": "" + }, + { + "name": "iconamoon:expressionless-face", + "svg": "" + }, + { + "name": "iconamoon:expressionless-face-bold", + "svg": "" + }, + { + "name": "iconamoon:expressionless-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:expressionless-face-fill", + "svg": "" + }, + { + "name": "iconamoon:expressionless-face-light", + "svg": "" + }, + { + "name": "iconamoon:expressionless-face-thin", + "svg": "" + }, + { + "name": "iconamoon:eye", + "svg": "" + }, + { + "name": "iconamoon:eye-bold", + "svg": "" + }, + { + "name": "iconamoon:eye-duotone", + "svg": "" + }, + { + "name": "iconamoon:eye-fill", + "svg": "" + }, + { + "name": "iconamoon:eye-light", + "svg": "" + }, + { + "name": "iconamoon:eye-off", + "svg": "" + }, + { + "name": "iconamoon:eye-off-bold", + "svg": "" + }, + { + "name": "iconamoon:eye-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:eye-off-fill", + "svg": "" + }, + { + "name": "iconamoon:eye-off-light", + "svg": "" + }, + { + "name": "iconamoon:eye-off-thin", + "svg": "" + }, + { + "name": "iconamoon:eye-thin", + "svg": "" + }, + { + "name": "iconamoon:face-with-open-mouth", + "svg": "" + }, + { + "name": "iconamoon:face-with-open-mouth-bold", + "svg": "" + }, + { + "name": "iconamoon:face-with-open-mouth-duotone", + "svg": "" + }, + { + "name": "iconamoon:face-with-open-mouth-fill", + "svg": "" + }, + { + "name": "iconamoon:face-with-open-mouth-light", + "svg": "" + }, + { + "name": "iconamoon:face-with-open-mouth-thin", + "svg": "" + }, + { + "name": "iconamoon:face-without-mouth", + "svg": "" + }, + { + "name": "iconamoon:face-without-mouth-bold", + "svg": "" + }, + { + "name": "iconamoon:face-without-mouth-duotone", + "svg": "" + }, + { + "name": "iconamoon:face-without-mouth-fill", + "svg": "" + }, + { + "name": "iconamoon:face-without-mouth-light", + "svg": "" + }, + { + "name": "iconamoon:face-without-mouth-thin", + "svg": "" + }, + { + "name": "iconamoon:file", + "svg": "" + }, + { + "name": "iconamoon:file-add", + "svg": "" + }, + { + "name": "iconamoon:file-add-bold", + "svg": "" + }, + { + "name": "iconamoon:file-add-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-add-fill", + "svg": "" + }, + { + "name": "iconamoon:file-add-light", + "svg": "" + }, + { + "name": "iconamoon:file-add-thin", + "svg": "" + }, + { + "name": "iconamoon:file-audio", + "svg": "" + }, + { + "name": "iconamoon:file-audio-bold", + "svg": "" + }, + { + "name": "iconamoon:file-audio-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-audio-fill", + "svg": "" + }, + { + "name": "iconamoon:file-audio-light", + "svg": "" + }, + { + "name": "iconamoon:file-audio-thin", + "svg": "" + }, + { + "name": "iconamoon:file-bold", + "svg": "" + }, + { + "name": "iconamoon:file-check", + "svg": "" + }, + { + "name": "iconamoon:file-check-bold", + "svg": "" + }, + { + "name": "iconamoon:file-check-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-check-fill", + "svg": "" + }, + { + "name": "iconamoon:file-check-light", + "svg": "" + }, + { + "name": "iconamoon:file-check-thin", + "svg": "" + }, + { + "name": "iconamoon:file-close", + "svg": "" + }, + { + "name": "iconamoon:file-close-bold", + "svg": "" + }, + { + "name": "iconamoon:file-close-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-close-fill", + "svg": "" + }, + { + "name": "iconamoon:file-close-light", + "svg": "" + }, + { + "name": "iconamoon:file-close-thin", + "svg": "" + }, + { + "name": "iconamoon:file-document", + "svg": "" + }, + { + "name": "iconamoon:file-document-bold", + "svg": "" + }, + { + "name": "iconamoon:file-document-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-document-fill", + "svg": "" + }, + { + "name": "iconamoon:file-document-light", + "svg": "" + }, + { + "name": "iconamoon:file-document-thin", + "svg": "" + }, + { + "name": "iconamoon:file-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-fill", + "svg": "" + }, + { + "name": "iconamoon:file-image", + "svg": "" + }, + { + "name": "iconamoon:file-image-bold", + "svg": "" + }, + { + "name": "iconamoon:file-image-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-image-fill", + "svg": "" + }, + { + "name": "iconamoon:file-image-light", + "svg": "" + }, + { + "name": "iconamoon:file-image-thin", + "svg": "" + }, + { + "name": "iconamoon:file-light", + "svg": "" + }, + { + "name": "iconamoon:file-remove", + "svg": "" + }, + { + "name": "iconamoon:file-remove-bold", + "svg": "" + }, + { + "name": "iconamoon:file-remove-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-remove-fill", + "svg": "" + }, + { + "name": "iconamoon:file-remove-light", + "svg": "" + }, + { + "name": "iconamoon:file-remove-thin", + "svg": "" + }, + { + "name": "iconamoon:file-thin", + "svg": "" + }, + { + "name": "iconamoon:file-video", + "svg": "" + }, + { + "name": "iconamoon:file-video-bold", + "svg": "" + }, + { + "name": "iconamoon:file-video-duotone", + "svg": "" + }, + { + "name": "iconamoon:file-video-fill", + "svg": "" + }, + { + "name": "iconamoon:file-video-light", + "svg": "" + }, + { + "name": "iconamoon:file-video-thin", + "svg": "" + }, + { + "name": "iconamoon:flag", + "svg": "" + }, + { + "name": "iconamoon:flag-bold", + "svg": "" + }, + { + "name": "iconamoon:flag-duotone", + "svg": "" + }, + { + "name": "iconamoon:flag-fill", + "svg": "" + }, + { + "name": "iconamoon:flag-light", + "svg": "" + }, + { + "name": "iconamoon:flag-thin", + "svg": "" + }, + { + "name": "iconamoon:folder", + "svg": "" + }, + { + "name": "iconamoon:folder-add", + "svg": "" + }, + { + "name": "iconamoon:folder-add-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-add-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-add-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-add-light", + "svg": "" + }, + { + "name": "iconamoon:folder-add-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-check", + "svg": "" + }, + { + "name": "iconamoon:folder-check-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-check-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-check-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-check-light", + "svg": "" + }, + { + "name": "iconamoon:folder-check-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-close", + "svg": "" + }, + { + "name": "iconamoon:folder-close-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-close-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-close-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-close-light", + "svg": "" + }, + { + "name": "iconamoon:folder-close-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-document", + "svg": "" + }, + { + "name": "iconamoon:folder-document-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-document-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-document-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-document-light", + "svg": "" + }, + { + "name": "iconamoon:folder-document-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-image", + "svg": "" + }, + { + "name": "iconamoon:folder-image-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-image-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-image-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-image-light", + "svg": "" + }, + { + "name": "iconamoon:folder-image-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-light", + "svg": "" + }, + { + "name": "iconamoon:folder-music", + "svg": "" + }, + { + "name": "iconamoon:folder-music-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-music-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-music-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-music-light", + "svg": "" + }, + { + "name": "iconamoon:folder-music-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-remove", + "svg": "" + }, + { + "name": "iconamoon:folder-remove-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-remove-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-remove-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-remove-light", + "svg": "" + }, + { + "name": "iconamoon:folder-remove-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-thin", + "svg": "" + }, + { + "name": "iconamoon:folder-video", + "svg": "" + }, + { + "name": "iconamoon:folder-video-bold", + "svg": "" + }, + { + "name": "iconamoon:folder-video-duotone", + "svg": "" + }, + { + "name": "iconamoon:folder-video-fill", + "svg": "" + }, + { + "name": "iconamoon:folder-video-light", + "svg": "" + }, + { + "name": "iconamoon:folder-video-thin", + "svg": "" + }, + { + "name": "iconamoon:frame", + "svg": "" + }, + { + "name": "iconamoon:frame-bold", + "svg": "" + }, + { + "name": "iconamoon:frame-duotone", + "svg": "" + }, + { + "name": "iconamoon:frame-fill", + "svg": "" + }, + { + "name": "iconamoon:frame-light", + "svg": "" + }, + { + "name": "iconamoon:frame-thin", + "svg": "" + }, + { + "name": "iconamoon:frowning-face", + "svg": "" + }, + { + "name": "iconamoon:frowning-face-bold", + "svg": "" + }, + { + "name": "iconamoon:frowning-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:frowning-face-fill", + "svg": "" + }, + { + "name": "iconamoon:frowning-face-light", + "svg": "" + }, + { + "name": "iconamoon:frowning-face-thin", + "svg": "" + }, + { + "name": "iconamoon:funnel", + "svg": "" + }, + { + "name": "iconamoon:funnel-bold", + "svg": "" + }, + { + "name": "iconamoon:funnel-duotone", + "svg": "" + }, + { + "name": "iconamoon:funnel-fill", + "svg": "" + }, + { + "name": "iconamoon:funnel-light", + "svg": "" + }, + { + "name": "iconamoon:funnel-thin", + "svg": "" + }, + { + "name": "iconamoon:gift", + "svg": "" + }, + { + "name": "iconamoon:gift-bold", + "svg": "" + }, + { + "name": "iconamoon:gift-duotone", + "svg": "" + }, + { + "name": "iconamoon:gift-fill", + "svg": "" + }, + { + "name": "iconamoon:gift-light", + "svg": "" + }, + { + "name": "iconamoon:gift-thin", + "svg": "" + }, + { + "name": "iconamoon:headphone", + "svg": "" + }, + { + "name": "iconamoon:headphone-bold", + "svg": "" + }, + { + "name": "iconamoon:headphone-duotone", + "svg": "" + }, + { + "name": "iconamoon:headphone-fill", + "svg": "" + }, + { + "name": "iconamoon:headphone-light", + "svg": "" + }, + { + "name": "iconamoon:headphone-thin", + "svg": "" + }, + { + "name": "iconamoon:heart", + "svg": "" + }, + { + "name": "iconamoon:heart-bold", + "svg": "" + }, + { + "name": "iconamoon:heart-duotone", + "svg": "" + }, + { + "name": "iconamoon:heart-fill", + "svg": "" + }, + { + "name": "iconamoon:heart-light", + "svg": "" + }, + { + "name": "iconamoon:heart-off", + "svg": "" + }, + { + "name": "iconamoon:heart-off-bold", + "svg": "" + }, + { + "name": "iconamoon:heart-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:heart-off-fill", + "svg": "" + }, + { + "name": "iconamoon:heart-off-light", + "svg": "" + }, + { + "name": "iconamoon:heart-off-thin", + "svg": "" + }, + { + "name": "iconamoon:heart-thin", + "svg": "" + }, + { + "name": "iconamoon:history", + "svg": "" + }, + { + "name": "iconamoon:history-bold", + "svg": "" + }, + { + "name": "iconamoon:history-duotone", + "svg": "" + }, + { + "name": "iconamoon:history-fill", + "svg": "" + }, + { + "name": "iconamoon:history-light", + "svg": "" + }, + { + "name": "iconamoon:history-thin", + "svg": "" + }, + { + "name": "iconamoon:home", + "svg": "" + }, + { + "name": "iconamoon:home-bold", + "svg": "" + }, + { + "name": "iconamoon:home-duotone", + "svg": "" + }, + { + "name": "iconamoon:home-fill", + "svg": "" + }, + { + "name": "iconamoon:home-light", + "svg": "" + }, + { + "name": "iconamoon:home-thin", + "svg": "" + }, + { + "name": "iconamoon:information-circle", + "svg": "" + }, + { + "name": "iconamoon:information-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:information-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:information-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:information-circle-light", + "svg": "" + }, + { + "name": "iconamoon:information-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:information-square", + "svg": "" + }, + { + "name": "iconamoon:information-square-bold", + "svg": "" + }, + { + "name": "iconamoon:information-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:information-square-fill", + "svg": "" + }, + { + "name": "iconamoon:information-square-light", + "svg": "" + }, + { + "name": "iconamoon:information-square-thin", + "svg": "" + }, + { + "name": "iconamoon:invoice", + "svg": "" + }, + { + "name": "iconamoon:invoice-bold", + "svg": "" + }, + { + "name": "iconamoon:invoice-duotone", + "svg": "" + }, + { + "name": "iconamoon:invoice-fill", + "svg": "" + }, + { + "name": "iconamoon:invoice-light", + "svg": "" + }, + { + "name": "iconamoon:invoice-thin", + "svg": "" + }, + { + "name": "iconamoon:kissing-face", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-bold", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-fill", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-light", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-thin", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-with-smiling-eyes", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-with-smiling-eyes-bold", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-with-smiling-eyes-duotone", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-with-smiling-eyes-fill", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-with-smiling-eyes-light", + "svg": "" + }, + { + "name": "iconamoon:kissing-face-with-smiling-eyes-thin", + "svg": "" + }, + { + "name": "iconamoon:lightning-1", + "svg": "" + }, + { + "name": "iconamoon:lightning-1-bold", + "svg": "" + }, + { + "name": "iconamoon:lightning-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:lightning-1-fill", + "svg": "" + }, + { + "name": "iconamoon:lightning-1-light", + "svg": "" + }, + { + "name": "iconamoon:lightning-1-thin", + "svg": "" + }, + { + "name": "iconamoon:lightning-2", + "svg": "" + }, + { + "name": "iconamoon:lightning-2-bold", + "svg": "" + }, + { + "name": "iconamoon:lightning-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:lightning-2-fill", + "svg": "" + }, + { + "name": "iconamoon:lightning-2-light", + "svg": "" + }, + { + "name": "iconamoon:lightning-2-thin", + "svg": "" + }, + { + "name": "iconamoon:like", + "svg": "" + }, + { + "name": "iconamoon:like-bold", + "svg": "" + }, + { + "name": "iconamoon:like-duotone", + "svg": "" + }, + { + "name": "iconamoon:like-fill", + "svg": "" + }, + { + "name": "iconamoon:like-light", + "svg": "" + }, + { + "name": "iconamoon:like-thin", + "svg": "" + }, + { + "name": "iconamoon:link", + "svg": "" + }, + { + "name": "iconamoon:link-bold", + "svg": "" + }, + { + "name": "iconamoon:link-duotone", + "svg": "" + }, + { + "name": "iconamoon:link-external", + "svg": "" + }, + { + "name": "iconamoon:link-external-bold", + "svg": "" + }, + { + "name": "iconamoon:link-external-duotone", + "svg": "" + }, + { + "name": "iconamoon:link-external-fill", + "svg": "" + }, + { + "name": "iconamoon:link-external-light", + "svg": "" + }, + { + "name": "iconamoon:link-external-thin", + "svg": "" + }, + { + "name": "iconamoon:link-fill", + "svg": "" + }, + { + "name": "iconamoon:link-light", + "svg": "" + }, + { + "name": "iconamoon:link-thin", + "svg": "" + }, + { + "name": "iconamoon:location", + "svg": "" + }, + { + "name": "iconamoon:location-bold", + "svg": "" + }, + { + "name": "iconamoon:location-duotone", + "svg": "" + }, + { + "name": "iconamoon:location-fill", + "svg": "" + }, + { + "name": "iconamoon:location-light", + "svg": "" + }, + { + "name": "iconamoon:location-pin", + "svg": "" + }, + { + "name": "iconamoon:location-pin-bold", + "svg": "" + }, + { + "name": "iconamoon:location-pin-duotone", + "svg": "" + }, + { + "name": "iconamoon:location-pin-fill", + "svg": "" + }, + { + "name": "iconamoon:location-pin-light", + "svg": "" + }, + { + "name": "iconamoon:location-pin-off", + "svg": "" + }, + { + "name": "iconamoon:location-pin-off-bold", + "svg": "" + }, + { + "name": "iconamoon:location-pin-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:location-pin-off-fill", + "svg": "" + }, + { + "name": "iconamoon:location-pin-off-light", + "svg": "" + }, + { + "name": "iconamoon:location-pin-off-thin", + "svg": "" + }, + { + "name": "iconamoon:location-pin-thin", + "svg": "" + }, + { + "name": "iconamoon:location-thin", + "svg": "" + }, + { + "name": "iconamoon:lock", + "svg": "" + }, + { + "name": "iconamoon:lock-bold", + "svg": "" + }, + { + "name": "iconamoon:lock-duotone", + "svg": "" + }, + { + "name": "iconamoon:lock-fill", + "svg": "" + }, + { + "name": "iconamoon:lock-light", + "svg": "" + }, + { + "name": "iconamoon:lock-off", + "svg": "" + }, + { + "name": "iconamoon:lock-off-bold", + "svg": "" + }, + { + "name": "iconamoon:lock-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:lock-off-fill", + "svg": "" + }, + { + "name": "iconamoon:lock-off-light", + "svg": "" + }, + { + "name": "iconamoon:lock-off-thin", + "svg": "" + }, + { + "name": "iconamoon:lock-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-horizontal", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-horizontal-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-horizontal-duotone", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-horizontal-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-horizontal-light", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-horizontal-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-vertical", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-vertical-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-vertical-duotone", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-vertical-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-vertical-light", + "svg": "" + }, + { + "name": "iconamoon:menu-burger-vertical-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-circle", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-circle-light", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-light", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-square", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-square-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-square-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-square-light", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-square-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-horizontal-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-circle", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-circle-light", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-light", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-square", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-square-bold", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-square-fill", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-square-light", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-square-thin", + "svg": "" + }, + { + "name": "iconamoon:menu-kebab-vertical-thin", + "svg": "" + }, + { + "name": "iconamoon:microphone", + "svg": "" + }, + { + "name": "iconamoon:microphone-bold", + "svg": "" + }, + { + "name": "iconamoon:microphone-duotone", + "svg": "" + }, + { + "name": "iconamoon:microphone-fill", + "svg": "" + }, + { + "name": "iconamoon:microphone-light", + "svg": "" + }, + { + "name": "iconamoon:microphone-off", + "svg": "" + }, + { + "name": "iconamoon:microphone-off-bold", + "svg": "" + }, + { + "name": "iconamoon:microphone-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:microphone-off-fill", + "svg": "" + }, + { + "name": "iconamoon:microphone-off-light", + "svg": "" + }, + { + "name": "iconamoon:microphone-off-thin", + "svg": "" + }, + { + "name": "iconamoon:microphone-thin", + "svg": "" + }, + { + "name": "iconamoon:mode-dark", + "svg": "" + }, + { + "name": "iconamoon:mode-dark-bold", + "svg": "" + }, + { + "name": "iconamoon:mode-dark-duotone", + "svg": "" + }, + { + "name": "iconamoon:mode-dark-fill", + "svg": "" + }, + { + "name": "iconamoon:mode-dark-light", + "svg": "" + }, + { + "name": "iconamoon:mode-dark-thin", + "svg": "" + }, + { + "name": "iconamoon:mode-light", + "svg": "" + }, + { + "name": "iconamoon:mode-light-bold", + "svg": "" + }, + { + "name": "iconamoon:mode-light-duotone", + "svg": "" + }, + { + "name": "iconamoon:mode-light-fill", + "svg": "" + }, + { + "name": "iconamoon:mode-light-light", + "svg": "" + }, + { + "name": "iconamoon:mode-light-thin", + "svg": "" + }, + { + "name": "iconamoon:mouse", + "svg": "" + }, + { + "name": "iconamoon:mouse-bold", + "svg": "" + }, + { + "name": "iconamoon:mouse-duotone", + "svg": "" + }, + { + "name": "iconamoon:mouse-fill", + "svg": "" + }, + { + "name": "iconamoon:mouse-light", + "svg": "" + }, + { + "name": "iconamoon:mouse-thin", + "svg": "" + }, + { + "name": "iconamoon:move", + "svg": "" + }, + { + "name": "iconamoon:move-bold", + "svg": "" + }, + { + "name": "iconamoon:move-duotone", + "svg": "" + }, + { + "name": "iconamoon:move-fill", + "svg": "" + }, + { + "name": "iconamoon:move-light", + "svg": "" + }, + { + "name": "iconamoon:move-thin", + "svg": "" + }, + { + "name": "iconamoon:music-1", + "svg": "" + }, + { + "name": "iconamoon:music-1-bold", + "svg": "" + }, + { + "name": "iconamoon:music-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:music-1-fill", + "svg": "" + }, + { + "name": "iconamoon:music-1-light", + "svg": "" + }, + { + "name": "iconamoon:music-1-thin", + "svg": "" + }, + { + "name": "iconamoon:music-2", + "svg": "" + }, + { + "name": "iconamoon:music-2-bold", + "svg": "" + }, + { + "name": "iconamoon:music-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:music-2-fill", + "svg": "" + }, + { + "name": "iconamoon:music-2-light", + "svg": "" + }, + { + "name": "iconamoon:music-2-thin", + "svg": "" + }, + { + "name": "iconamoon:music-album", + "svg": "" + }, + { + "name": "iconamoon:music-album-bold", + "svg": "" + }, + { + "name": "iconamoon:music-album-duotone", + "svg": "" + }, + { + "name": "iconamoon:music-album-fill", + "svg": "" + }, + { + "name": "iconamoon:music-album-light", + "svg": "" + }, + { + "name": "iconamoon:music-album-thin", + "svg": "" + }, + { + "name": "iconamoon:music-artist", + "svg": "" + }, + { + "name": "iconamoon:music-artist-bold", + "svg": "" + }, + { + "name": "iconamoon:music-artist-duotone", + "svg": "" + }, + { + "name": "iconamoon:music-artist-fill", + "svg": "" + }, + { + "name": "iconamoon:music-artist-light", + "svg": "" + }, + { + "name": "iconamoon:music-artist-thin", + "svg": "" + }, + { + "name": "iconamoon:neutral-face", + "svg": "" + }, + { + "name": "iconamoon:neutral-face-bold", + "svg": "" + }, + { + "name": "iconamoon:neutral-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:neutral-face-fill", + "svg": "" + }, + { + "name": "iconamoon:neutral-face-light", + "svg": "" + }, + { + "name": "iconamoon:neutral-face-thin", + "svg": "" + }, + { + "name": "iconamoon:news", + "svg": "" + }, + { + "name": "iconamoon:news-bold", + "svg": "" + }, + { + "name": "iconamoon:news-duotone", + "svg": "" + }, + { + "name": "iconamoon:news-fill", + "svg": "" + }, + { + "name": "iconamoon:news-light", + "svg": "" + }, + { + "name": "iconamoon:news-thin", + "svg": "" + }, + { + "name": "iconamoon:notification", + "svg": "" + }, + { + "name": "iconamoon:notification-bold", + "svg": "" + }, + { + "name": "iconamoon:notification-duotone", + "svg": "" + }, + { + "name": "iconamoon:notification-fill", + "svg": "" + }, + { + "name": "iconamoon:notification-light", + "svg": "" + }, + { + "name": "iconamoon:notification-off", + "svg": "" + }, + { + "name": "iconamoon:notification-off-bold", + "svg": "" + }, + { + "name": "iconamoon:notification-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:notification-off-fill", + "svg": "" + }, + { + "name": "iconamoon:notification-off-light", + "svg": "" + }, + { + "name": "iconamoon:notification-off-thin", + "svg": "" + }, + { + "name": "iconamoon:notification-thin", + "svg": "" + }, + { + "name": "iconamoon:number-0", + "svg": "" + }, + { + "name": "iconamoon:number-0-bold", + "svg": "" + }, + { + "name": "iconamoon:number-0-circle", + "svg": "" + }, + { + "name": "iconamoon:number-0-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-0-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-0-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-0-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-0-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-0-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-0-fill", + "svg": "" + }, + { + "name": "iconamoon:number-0-light", + "svg": "" + }, + { + "name": "iconamoon:number-0-square", + "svg": "" + }, + { + "name": "iconamoon:number-0-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-0-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-0-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-0-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-0-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-0-thin", + "svg": "" + }, + { + "name": "iconamoon:number-1", + "svg": "" + }, + { + "name": "iconamoon:number-1-bold", + "svg": "" + }, + { + "name": "iconamoon:number-1-circle", + "svg": "" + }, + { + "name": "iconamoon:number-1-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-1-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-1-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-1-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-1-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-1-fill", + "svg": "" + }, + { + "name": "iconamoon:number-1-light", + "svg": "" + }, + { + "name": "iconamoon:number-1-square", + "svg": "" + }, + { + "name": "iconamoon:number-1-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-1-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-1-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-1-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-1-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-1-thin", + "svg": "" + }, + { + "name": "iconamoon:number-2", + "svg": "" + }, + { + "name": "iconamoon:number-2-bold", + "svg": "" + }, + { + "name": "iconamoon:number-2-circle", + "svg": "" + }, + { + "name": "iconamoon:number-2-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-2-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-2-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-2-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-2-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-2-fill", + "svg": "" + }, + { + "name": "iconamoon:number-2-light", + "svg": "" + }, + { + "name": "iconamoon:number-2-square", + "svg": "" + }, + { + "name": "iconamoon:number-2-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-2-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-2-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-2-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-2-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-2-thin", + "svg": "" + }, + { + "name": "iconamoon:number-3", + "svg": "" + }, + { + "name": "iconamoon:number-3-bold", + "svg": "" + }, + { + "name": "iconamoon:number-3-circle", + "svg": "" + }, + { + "name": "iconamoon:number-3-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-3-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-3-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-3-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-3-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-3-fill", + "svg": "" + }, + { + "name": "iconamoon:number-3-light", + "svg": "" + }, + { + "name": "iconamoon:number-3-square", + "svg": "" + }, + { + "name": "iconamoon:number-3-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-3-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-3-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-3-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-3-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-3-thin", + "svg": "" + }, + { + "name": "iconamoon:number-4", + "svg": "" + }, + { + "name": "iconamoon:number-4-bold", + "svg": "" + }, + { + "name": "iconamoon:number-4-circle", + "svg": "" + }, + { + "name": "iconamoon:number-4-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-4-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-4-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-4-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-4-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-4-fill", + "svg": "" + }, + { + "name": "iconamoon:number-4-light", + "svg": "" + }, + { + "name": "iconamoon:number-4-square", + "svg": "" + }, + { + "name": "iconamoon:number-4-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-4-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-4-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-4-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-4-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-4-thin", + "svg": "" + }, + { + "name": "iconamoon:number-5", + "svg": "" + }, + { + "name": "iconamoon:number-5-bold", + "svg": "" + }, + { + "name": "iconamoon:number-5-circle", + "svg": "" + }, + { + "name": "iconamoon:number-5-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-5-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-5-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-5-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-5-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-5-fill", + "svg": "" + }, + { + "name": "iconamoon:number-5-light", + "svg": "" + }, + { + "name": "iconamoon:number-5-square", + "svg": "" + }, + { + "name": "iconamoon:number-5-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-5-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-5-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-5-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-5-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-5-thin", + "svg": "" + }, + { + "name": "iconamoon:number-6", + "svg": "" + }, + { + "name": "iconamoon:number-6-bold", + "svg": "" + }, + { + "name": "iconamoon:number-6-circle", + "svg": "" + }, + { + "name": "iconamoon:number-6-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-6-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-6-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-6-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-6-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-6-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-6-fill", + "svg": "" + }, + { + "name": "iconamoon:number-6-light", + "svg": "" + }, + { + "name": "iconamoon:number-6-square", + "svg": "" + }, + { + "name": "iconamoon:number-6-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-6-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-6-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-6-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-6-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-6-thin", + "svg": "" + }, + { + "name": "iconamoon:number-7", + "svg": "" + }, + { + "name": "iconamoon:number-7-bold", + "svg": "" + }, + { + "name": "iconamoon:number-7-circle", + "svg": "" + }, + { + "name": "iconamoon:number-7-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-7-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-7-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-7-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-7-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-7-fill", + "svg": "" + }, + { + "name": "iconamoon:number-7-light", + "svg": "" + }, + { + "name": "iconamoon:number-7-square", + "svg": "" + }, + { + "name": "iconamoon:number-7-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-7-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-7-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-7-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-7-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-7-thin", + "svg": "" + }, + { + "name": "iconamoon:number-8", + "svg": "" + }, + { + "name": "iconamoon:number-8-bold", + "svg": "" + }, + { + "name": "iconamoon:number-8-circle", + "svg": "" + }, + { + "name": "iconamoon:number-8-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-8-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-8-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-8-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-8-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-8-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-8-fill", + "svg": "" + }, + { + "name": "iconamoon:number-8-light", + "svg": "" + }, + { + "name": "iconamoon:number-8-square", + "svg": "" + }, + { + "name": "iconamoon:number-8-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-8-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-8-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-8-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-8-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-8-thin", + "svg": "" + }, + { + "name": "iconamoon:number-9", + "svg": "" + }, + { + "name": "iconamoon:number-9-bold", + "svg": "" + }, + { + "name": "iconamoon:number-9-circle", + "svg": "" + }, + { + "name": "iconamoon:number-9-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:number-9-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-9-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:number-9-circle-light", + "svg": "" + }, + { + "name": "iconamoon:number-9-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:number-9-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-9-fill", + "svg": "" + }, + { + "name": "iconamoon:number-9-light", + "svg": "" + }, + { + "name": "iconamoon:number-9-square", + "svg": "" + }, + { + "name": "iconamoon:number-9-square-bold", + "svg": "" + }, + { + "name": "iconamoon:number-9-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:number-9-square-fill", + "svg": "" + }, + { + "name": "iconamoon:number-9-square-light", + "svg": "" + }, + { + "name": "iconamoon:number-9-square-thin", + "svg": "" + }, + { + "name": "iconamoon:number-9-thin", + "svg": "" + }, + { + "name": "iconamoon:options", + "svg": "" + }, + { + "name": "iconamoon:options-bold", + "svg": "" + }, + { + "name": "iconamoon:options-duotone", + "svg": "" + }, + { + "name": "iconamoon:options-fill", + "svg": "" + }, + { + "name": "iconamoon:options-light", + "svg": "" + }, + { + "name": "iconamoon:options-thin", + "svg": "" + }, + { + "name": "iconamoon:pen", + "svg": "" + }, + { + "name": "iconamoon:pen-bold", + "svg": "" + }, + { + "name": "iconamoon:pen-duotone", + "svg": "" + }, + { + "name": "iconamoon:pen-fill", + "svg": "" + }, + { + "name": "iconamoon:pen-light", + "svg": "" + }, + { + "name": "iconamoon:pen-thin", + "svg": "" + }, + { + "name": "iconamoon:pensive-face", + "svg": "" + }, + { + "name": "iconamoon:pensive-face-bold", + "svg": "" + }, + { + "name": "iconamoon:pensive-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:pensive-face-fill", + "svg": "" + }, + { + "name": "iconamoon:pensive-face-light", + "svg": "" + }, + { + "name": "iconamoon:pensive-face-thin", + "svg": "" + }, + { + "name": "iconamoon:phone", + "svg": "" + }, + { + "name": "iconamoon:phone-bold", + "svg": "" + }, + { + "name": "iconamoon:phone-duotone", + "svg": "" + }, + { + "name": "iconamoon:phone-fill", + "svg": "" + }, + { + "name": "iconamoon:phone-light", + "svg": "" + }, + { + "name": "iconamoon:phone-off", + "svg": "" + }, + { + "name": "iconamoon:phone-off-bold", + "svg": "" + }, + { + "name": "iconamoon:phone-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:phone-off-fill", + "svg": "" + }, + { + "name": "iconamoon:phone-off-light", + "svg": "" + }, + { + "name": "iconamoon:phone-off-thin", + "svg": "" + }, + { + "name": "iconamoon:phone-thin", + "svg": "" + }, + { + "name": "iconamoon:play-circle", + "svg": "" + }, + { + "name": "iconamoon:play-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:play-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:play-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:play-circle-light", + "svg": "" + }, + { + "name": "iconamoon:play-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:player-end", + "svg": "" + }, + { + "name": "iconamoon:player-end-bold", + "svg": "" + }, + { + "name": "iconamoon:player-end-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-end-fill", + "svg": "" + }, + { + "name": "iconamoon:player-end-light", + "svg": "" + }, + { + "name": "iconamoon:player-end-thin", + "svg": "" + }, + { + "name": "iconamoon:player-next", + "svg": "" + }, + { + "name": "iconamoon:player-next-bold", + "svg": "" + }, + { + "name": "iconamoon:player-next-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-next-fill", + "svg": "" + }, + { + "name": "iconamoon:player-next-light", + "svg": "" + }, + { + "name": "iconamoon:player-next-thin", + "svg": "" + }, + { + "name": "iconamoon:player-pause", + "svg": "" + }, + { + "name": "iconamoon:player-pause-bold", + "svg": "" + }, + { + "name": "iconamoon:player-pause-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-pause-fill", + "svg": "" + }, + { + "name": "iconamoon:player-pause-light", + "svg": "" + }, + { + "name": "iconamoon:player-pause-thin", + "svg": "" + }, + { + "name": "iconamoon:player-play", + "svg": "" + }, + { + "name": "iconamoon:player-play-bold", + "svg": "" + }, + { + "name": "iconamoon:player-play-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-play-fill", + "svg": "" + }, + { + "name": "iconamoon:player-play-light", + "svg": "" + }, + { + "name": "iconamoon:player-play-thin", + "svg": "" + }, + { + "name": "iconamoon:player-previous", + "svg": "" + }, + { + "name": "iconamoon:player-previous-bold", + "svg": "" + }, + { + "name": "iconamoon:player-previous-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-previous-fill", + "svg": "" + }, + { + "name": "iconamoon:player-previous-light", + "svg": "" + }, + { + "name": "iconamoon:player-previous-thin", + "svg": "" + }, + { + "name": "iconamoon:player-start", + "svg": "" + }, + { + "name": "iconamoon:player-start-bold", + "svg": "" + }, + { + "name": "iconamoon:player-start-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-start-fill", + "svg": "" + }, + { + "name": "iconamoon:player-start-light", + "svg": "" + }, + { + "name": "iconamoon:player-start-thin", + "svg": "" + }, + { + "name": "iconamoon:player-stop", + "svg": "" + }, + { + "name": "iconamoon:player-stop-bold", + "svg": "" + }, + { + "name": "iconamoon:player-stop-duotone", + "svg": "" + }, + { + "name": "iconamoon:player-stop-fill", + "svg": "" + }, + { + "name": "iconamoon:player-stop-light", + "svg": "" + }, + { + "name": "iconamoon:player-stop-thin", + "svg": "" + }, + { + "name": "iconamoon:playlist", + "svg": "" + }, + { + "name": "iconamoon:playlist-bold", + "svg": "" + }, + { + "name": "iconamoon:playlist-fill", + "svg": "" + }, + { + "name": "iconamoon:playlist-light", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-list", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-list-bold", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-list-fill", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-list-light", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-list-thin", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-song", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-song-bold", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-song-fill", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-song-light", + "svg": "" + }, + { + "name": "iconamoon:playlist-repeat-song-thin", + "svg": "" + }, + { + "name": "iconamoon:playlist-shuffle", + "svg": "" + }, + { + "name": "iconamoon:playlist-shuffle-bold", + "svg": "" + }, + { + "name": "iconamoon:playlist-shuffle-duotone", + "svg": "" + }, + { + "name": "iconamoon:playlist-shuffle-fill", + "svg": "" + }, + { + "name": "iconamoon:playlist-shuffle-light", + "svg": "" + }, + { + "name": "iconamoon:playlist-shuffle-thin", + "svg": "" + }, + { + "name": "iconamoon:playlist-thin", + "svg": "" + }, + { + "name": "iconamoon:printer", + "svg": "" + }, + { + "name": "iconamoon:printer-bold", + "svg": "" + }, + { + "name": "iconamoon:printer-duotone", + "svg": "" + }, + { + "name": "iconamoon:printer-fill", + "svg": "" + }, + { + "name": "iconamoon:printer-light", + "svg": "" + }, + { + "name": "iconamoon:printer-thin", + "svg": "" + }, + { + "name": "iconamoon:profile", + "svg": "" + }, + { + "name": "iconamoon:profile-bold", + "svg": "" + }, + { + "name": "iconamoon:profile-circle", + "svg": "" + }, + { + "name": "iconamoon:profile-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:profile-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:profile-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:profile-circle-light", + "svg": "" + }, + { + "name": "iconamoon:profile-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:profile-duotone", + "svg": "" + }, + { + "name": "iconamoon:profile-fill", + "svg": "" + }, + { + "name": "iconamoon:profile-light", + "svg": "" + }, + { + "name": "iconamoon:profile-thin", + "svg": "" + }, + { + "name": "iconamoon:question-mark-circle", + "svg": "" + }, + { + "name": "iconamoon:question-mark-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:question-mark-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:question-mark-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:question-mark-circle-light", + "svg": "" + }, + { + "name": "iconamoon:question-mark-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:question-mark-square", + "svg": "" + }, + { + "name": "iconamoon:question-mark-square-bold", + "svg": "" + }, + { + "name": "iconamoon:question-mark-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:question-mark-square-fill", + "svg": "" + }, + { + "name": "iconamoon:question-mark-square-light", + "svg": "" + }, + { + "name": "iconamoon:question-mark-square-thin", + "svg": "" + }, + { + "name": "iconamoon:relieved-face", + "svg": "" + }, + { + "name": "iconamoon:relieved-face-bold", + "svg": "" + }, + { + "name": "iconamoon:relieved-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:relieved-face-fill", + "svg": "" + }, + { + "name": "iconamoon:relieved-face-light", + "svg": "" + }, + { + "name": "iconamoon:relieved-face-thin", + "svg": "" + }, + { + "name": "iconamoon:restart", + "svg": "" + }, + { + "name": "iconamoon:restart-bold", + "svg": "" + }, + { + "name": "iconamoon:restart-duotone", + "svg": "" + }, + { + "name": "iconamoon:restart-fill", + "svg": "" + }, + { + "name": "iconamoon:restart-light", + "svg": "" + }, + { + "name": "iconamoon:restart-thin", + "svg": "" + }, + { + "name": "iconamoon:scanner", + "svg": "" + }, + { + "name": "iconamoon:scanner-bold", + "svg": "" + }, + { + "name": "iconamoon:scanner-fill", + "svg": "" + }, + { + "name": "iconamoon:scanner-light", + "svg": "" + }, + { + "name": "iconamoon:scanner-thin", + "svg": "" + }, + { + "name": "iconamoon:screen-full", + "svg": "" + }, + { + "name": "iconamoon:screen-full-bold", + "svg": "" + }, + { + "name": "iconamoon:screen-full-duotone", + "svg": "" + }, + { + "name": "iconamoon:screen-full-fill", + "svg": "" + }, + { + "name": "iconamoon:screen-full-light", + "svg": "" + }, + { + "name": "iconamoon:screen-full-thin", + "svg": "" + }, + { + "name": "iconamoon:screen-normal", + "svg": "" + }, + { + "name": "iconamoon:screen-normal-bold", + "svg": "" + }, + { + "name": "iconamoon:screen-normal-duotone", + "svg": "" + }, + { + "name": "iconamoon:screen-normal-fill", + "svg": "" + }, + { + "name": "iconamoon:screen-normal-light", + "svg": "" + }, + { + "name": "iconamoon:screen-normal-thin", + "svg": "" + }, + { + "name": "iconamoon:search", + "svg": "" + }, + { + "name": "iconamoon:search-bold", + "svg": "" + }, + { + "name": "iconamoon:search-duotone", + "svg": "" + }, + { + "name": "iconamoon:search-fill", + "svg": "" + }, + { + "name": "iconamoon:search-light", + "svg": "" + }, + { + "name": "iconamoon:search-thin", + "svg": "" + }, + { + "name": "iconamoon:send", + "svg": "" + }, + { + "name": "iconamoon:send-bold", + "svg": "" + }, + { + "name": "iconamoon:send-duotone", + "svg": "" + }, + { + "name": "iconamoon:send-fill", + "svg": "" + }, + { + "name": "iconamoon:send-light", + "svg": "" + }, + { + "name": "iconamoon:send-thin", + "svg": "" + }, + { + "name": "iconamoon:settings", + "svg": "" + }, + { + "name": "iconamoon:settings-bold", + "svg": "" + }, + { + "name": "iconamoon:settings-duotone", + "svg": "" + }, + { + "name": "iconamoon:settings-fill", + "svg": "" + }, + { + "name": "iconamoon:settings-light", + "svg": "" + }, + { + "name": "iconamoon:settings-thin", + "svg": "" + }, + { + "name": "iconamoon:share-1", + "svg": "" + }, + { + "name": "iconamoon:share-1-bold", + "svg": "" + }, + { + "name": "iconamoon:share-1-duotone", + "svg": "" + }, + { + "name": "iconamoon:share-1-fill", + "svg": "" + }, + { + "name": "iconamoon:share-1-light", + "svg": "" + }, + { + "name": "iconamoon:share-1-thin", + "svg": "" + }, + { + "name": "iconamoon:share-2", + "svg": "" + }, + { + "name": "iconamoon:share-2-bold", + "svg": "" + }, + { + "name": "iconamoon:share-2-duotone", + "svg": "" + }, + { + "name": "iconamoon:share-2-fill", + "svg": "" + }, + { + "name": "iconamoon:share-2-light", + "svg": "" + }, + { + "name": "iconamoon:share-2-thin", + "svg": "" + }, + { + "name": "iconamoon:shield", + "svg": "" + }, + { + "name": "iconamoon:shield-bold", + "svg": "" + }, + { + "name": "iconamoon:shield-duotone", + "svg": "" + }, + { + "name": "iconamoon:shield-fill", + "svg": "" + }, + { + "name": "iconamoon:shield-light", + "svg": "" + }, + { + "name": "iconamoon:shield-no", + "svg": "" + }, + { + "name": "iconamoon:shield-no-bold", + "svg": "" + }, + { + "name": "iconamoon:shield-no-duotone", + "svg": "" + }, + { + "name": "iconamoon:shield-no-fill", + "svg": "" + }, + { + "name": "iconamoon:shield-no-light", + "svg": "" + }, + { + "name": "iconamoon:shield-no-thin", + "svg": "" + }, + { + "name": "iconamoon:shield-off", + "svg": "" + }, + { + "name": "iconamoon:shield-off-bold", + "svg": "" + }, + { + "name": "iconamoon:shield-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:shield-off-fill", + "svg": "" + }, + { + "name": "iconamoon:shield-off-light", + "svg": "" + }, + { + "name": "iconamoon:shield-off-thin", + "svg": "" + }, + { + "name": "iconamoon:shield-thin", + "svg": "" + }, + { + "name": "iconamoon:shield-yes", + "svg": "" + }, + { + "name": "iconamoon:shield-yes-bold", + "svg": "" + }, + { + "name": "iconamoon:shield-yes-duotone", + "svg": "" + }, + { + "name": "iconamoon:shield-yes-fill", + "svg": "" + }, + { + "name": "iconamoon:shield-yes-light", + "svg": "" + }, + { + "name": "iconamoon:shield-yes-thin", + "svg": "" + }, + { + "name": "iconamoon:shopping-bag", + "svg": "" + }, + { + "name": "iconamoon:shopping-bag-bold", + "svg": "" + }, + { + "name": "iconamoon:shopping-bag-duotone", + "svg": "" + }, + { + "name": "iconamoon:shopping-bag-fill", + "svg": "" + }, + { + "name": "iconamoon:shopping-bag-light", + "svg": "" + }, + { + "name": "iconamoon:shopping-bag-thin", + "svg": "" + }, + { + "name": "iconamoon:shopping-card", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-add", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-add-bold", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-add-duotone", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-add-fill", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-add-light", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-add-thin", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-bold", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-duotone", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-fill", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-light", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-remove", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-remove-bold", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-remove-duotone", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-remove-fill", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-remove-light", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-remove-thin", + "svg": "" + }, + { + "name": "iconamoon:shopping-card-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-division", + "svg": "" + }, + { + "name": "iconamoon:sign-division-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-division-circle", + "svg": "" + }, + { + "name": "iconamoon:sign-division-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-division-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-division-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-division-circle-light", + "svg": "" + }, + { + "name": "iconamoon:sign-division-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-division-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-division-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-division-light", + "svg": "" + }, + { + "name": "iconamoon:sign-division-slash", + "svg": "" + }, + { + "name": "iconamoon:sign-division-slash-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-division-slash-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-division-slash-light", + "svg": "" + }, + { + "name": "iconamoon:sign-division-slash-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-division-square", + "svg": "" + }, + { + "name": "iconamoon:sign-division-square-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-division-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-division-square-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-division-square-light", + "svg": "" + }, + { + "name": "iconamoon:sign-division-square-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-division-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-equal", + "svg": "" + }, + { + "name": "iconamoon:sign-equal-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-equal-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-equal-light", + "svg": "" + }, + { + "name": "iconamoon:sign-equal-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-f", + "svg": "" + }, + { + "name": "iconamoon:sign-f-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-f-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-f-light", + "svg": "" + }, + { + "name": "iconamoon:sign-f-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-factorial", + "svg": "" + }, + { + "name": "iconamoon:sign-factorial-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-factorial-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-factorial-light", + "svg": "" + }, + { + "name": "iconamoon:sign-factorial-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-lemniscate", + "svg": "" + }, + { + "name": "iconamoon:sign-lemniscate-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-lemniscate-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-lemniscate-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-lemniscate-light", + "svg": "" + }, + { + "name": "iconamoon:sign-lemniscate-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-minus", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-circle", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-circle-light", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-light", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-square", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-square-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-square-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-square-light", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-square-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-minus-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-percent", + "svg": "" + }, + { + "name": "iconamoon:sign-percent-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-percent-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-percent-light", + "svg": "" + }, + { + "name": "iconamoon:sign-percent-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-pi", + "svg": "" + }, + { + "name": "iconamoon:sign-pi-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-pi-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-pi-light", + "svg": "" + }, + { + "name": "iconamoon:sign-pi-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-plus", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-circle", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-circle-light", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-light", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-minus", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-minus-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-minus-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-minus-light", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-minus-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-square", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-square-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-square-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-square-light", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-square-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-plus-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-radical", + "svg": "" + }, + { + "name": "iconamoon:sign-radical-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-radical-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-radical-light", + "svg": "" + }, + { + "name": "iconamoon:sign-radical-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-times", + "svg": "" + }, + { + "name": "iconamoon:sign-times-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-times-circle", + "svg": "" + }, + { + "name": "iconamoon:sign-times-circle-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-times-circle-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-times-circle-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-times-circle-light", + "svg": "" + }, + { + "name": "iconamoon:sign-times-circle-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-times-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-times-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-times-light", + "svg": "" + }, + { + "name": "iconamoon:sign-times-square", + "svg": "" + }, + { + "name": "iconamoon:sign-times-square-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-times-square-duotone", + "svg": "" + }, + { + "name": "iconamoon:sign-times-square-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-times-square-light", + "svg": "" + }, + { + "name": "iconamoon:sign-times-square-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-times-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-x", + "svg": "" + }, + { + "name": "iconamoon:sign-x-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-x-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-x-light", + "svg": "" + }, + { + "name": "iconamoon:sign-x-thin", + "svg": "" + }, + { + "name": "iconamoon:sign-y", + "svg": "" + }, + { + "name": "iconamoon:sign-y-bold", + "svg": "" + }, + { + "name": "iconamoon:sign-y-fill", + "svg": "" + }, + { + "name": "iconamoon:sign-y-light", + "svg": "" + }, + { + "name": "iconamoon:sign-y-thin", + "svg": "" + }, + { + "name": "iconamoon:slightly-smiling-face", + "svg": "" + }, + { + "name": "iconamoon:slightly-smiling-face-bold", + "svg": "" + }, + { + "name": "iconamoon:slightly-smiling-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:slightly-smiling-face-fill", + "svg": "" + }, + { + "name": "iconamoon:slightly-smiling-face-light", + "svg": "" + }, + { + "name": "iconamoon:slightly-smiling-face-thin", + "svg": "" + }, + { + "name": "iconamoon:smiling-face", + "svg": "" + }, + { + "name": "iconamoon:smiling-face-bold", + "svg": "" + }, + { + "name": "iconamoon:smiling-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:smiling-face-fill", + "svg": "" + }, + { + "name": "iconamoon:smiling-face-light", + "svg": "" + }, + { + "name": "iconamoon:smiling-face-thin", + "svg": "" + }, + { + "name": "iconamoon:sorting-center", + "svg": "" + }, + { + "name": "iconamoon:sorting-center-bold", + "svg": "" + }, + { + "name": "iconamoon:sorting-center-duotone", + "svg": "" + }, + { + "name": "iconamoon:sorting-center-fill", + "svg": "" + }, + { + "name": "iconamoon:sorting-center-light", + "svg": "" + }, + { + "name": "iconamoon:sorting-center-thin", + "svg": "" + }, + { + "name": "iconamoon:sorting-left", + "svg": "" + }, + { + "name": "iconamoon:sorting-left-bold", + "svg": "" + }, + { + "name": "iconamoon:sorting-left-duotone", + "svg": "" + }, + { + "name": "iconamoon:sorting-left-fill", + "svg": "" + }, + { + "name": "iconamoon:sorting-left-light", + "svg": "" + }, + { + "name": "iconamoon:sorting-left-thin", + "svg": "" + }, + { + "name": "iconamoon:sorting-right", + "svg": "" + }, + { + "name": "iconamoon:sorting-right-bold", + "svg": "" + }, + { + "name": "iconamoon:sorting-right-duotone", + "svg": "" + }, + { + "name": "iconamoon:sorting-right-fill", + "svg": "" + }, + { + "name": "iconamoon:sorting-right-light", + "svg": "" + }, + { + "name": "iconamoon:sorting-right-thin", + "svg": "" + }, + { + "name": "iconamoon:squinting-face", + "svg": "" + }, + { + "name": "iconamoon:squinting-face-bold", + "svg": "" + }, + { + "name": "iconamoon:squinting-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:squinting-face-fill", + "svg": "" + }, + { + "name": "iconamoon:squinting-face-light", + "svg": "" + }, + { + "name": "iconamoon:squinting-face-thin", + "svg": "" + }, + { + "name": "iconamoon:star", + "svg": "" + }, + { + "name": "iconamoon:star-bold", + "svg": "" + }, + { + "name": "iconamoon:star-duotone", + "svg": "" + }, + { + "name": "iconamoon:star-fill", + "svg": "" + }, + { + "name": "iconamoon:star-light", + "svg": "" + }, + { + "name": "iconamoon:star-off", + "svg": "" + }, + { + "name": "iconamoon:star-off-bold", + "svg": "" + }, + { + "name": "iconamoon:star-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:star-off-fill", + "svg": "" + }, + { + "name": "iconamoon:star-off-light", + "svg": "" + }, + { + "name": "iconamoon:star-off-thin", + "svg": "" + }, + { + "name": "iconamoon:star-thin", + "svg": "" + }, + { + "name": "iconamoon:store", + "svg": "" + }, + { + "name": "iconamoon:store-bold", + "svg": "" + }, + { + "name": "iconamoon:store-duotone", + "svg": "" + }, + { + "name": "iconamoon:store-fill", + "svg": "" + }, + { + "name": "iconamoon:store-light", + "svg": "" + }, + { + "name": "iconamoon:store-thin", + "svg": "" + }, + { + "name": "iconamoon:swap", + "svg": "" + }, + { + "name": "iconamoon:swap-bold", + "svg": "" + }, + { + "name": "iconamoon:swap-fill", + "svg": "" + }, + { + "name": "iconamoon:swap-light", + "svg": "" + }, + { + "name": "iconamoon:swap-thin", + "svg": "" + }, + { + "name": "iconamoon:synchronize", + "svg": "" + }, + { + "name": "iconamoon:synchronize-bold", + "svg": "" + }, + { + "name": "iconamoon:synchronize-duotone", + "svg": "" + }, + { + "name": "iconamoon:synchronize-fill", + "svg": "" + }, + { + "name": "iconamoon:synchronize-light", + "svg": "" + }, + { + "name": "iconamoon:synchronize-thin", + "svg": "" + }, + { + "name": "iconamoon:ticket", + "svg": "" + }, + { + "name": "iconamoon:ticket-bold", + "svg": "" + }, + { + "name": "iconamoon:ticket-duotone", + "svg": "" + }, + { + "name": "iconamoon:ticket-fill", + "svg": "" + }, + { + "name": "iconamoon:ticket-light", + "svg": "" + }, + { + "name": "iconamoon:ticket-thin", + "svg": "" + }, + { + "name": "iconamoon:trash", + "svg": "" + }, + { + "name": "iconamoon:trash-bold", + "svg": "" + }, + { + "name": "iconamoon:trash-duotone", + "svg": "" + }, + { + "name": "iconamoon:trash-fill", + "svg": "" + }, + { + "name": "iconamoon:trash-light", + "svg": "" + }, + { + "name": "iconamoon:trash-simple", + "svg": "" + }, + { + "name": "iconamoon:trash-simple-bold", + "svg": "" + }, + { + "name": "iconamoon:trash-simple-duotone", + "svg": "" + }, + { + "name": "iconamoon:trash-simple-fill", + "svg": "" + }, + { + "name": "iconamoon:trash-simple-light", + "svg": "" + }, + { + "name": "iconamoon:trash-simple-thin", + "svg": "" + }, + { + "name": "iconamoon:trash-thin", + "svg": "" + }, + { + "name": "iconamoon:trend-down", + "svg": "" + }, + { + "name": "iconamoon:trend-down-bold", + "svg": "" + }, + { + "name": "iconamoon:trend-down-fill", + "svg": "" + }, + { + "name": "iconamoon:trend-down-light", + "svg": "" + }, + { + "name": "iconamoon:trend-down-thin", + "svg": "" + }, + { + "name": "iconamoon:trend-up", + "svg": "" + }, + { + "name": "iconamoon:trend-up-bold", + "svg": "" + }, + { + "name": "iconamoon:trend-up-fill", + "svg": "" + }, + { + "name": "iconamoon:trend-up-light", + "svg": "" + }, + { + "name": "iconamoon:trend-up-thin", + "svg": "" + }, + { + "name": "iconamoon:type", + "svg": "" + }, + { + "name": "iconamoon:type-bold", + "svg": "" + }, + { + "name": "iconamoon:type-duotone", + "svg": "" + }, + { + "name": "iconamoon:type-fill", + "svg": "" + }, + { + "name": "iconamoon:type-light", + "svg": "" + }, + { + "name": "iconamoon:type-thin", + "svg": "" + }, + { + "name": "iconamoon:unavailable", + "svg": "" + }, + { + "name": "iconamoon:unavailable-bold", + "svg": "" + }, + { + "name": "iconamoon:unavailable-duotone", + "svg": "" + }, + { + "name": "iconamoon:unavailable-fill", + "svg": "" + }, + { + "name": "iconamoon:unavailable-light", + "svg": "" + }, + { + "name": "iconamoon:unavailable-thin", + "svg": "" + }, + { + "name": "iconamoon:upload", + "svg": "" + }, + { + "name": "iconamoon:upload-bold", + "svg": "" + }, + { + "name": "iconamoon:upload-fill", + "svg": "" + }, + { + "name": "iconamoon:upload-light", + "svg": "" + }, + { + "name": "iconamoon:upload-thin", + "svg": "" + }, + { + "name": "iconamoon:upside-down-face", + "svg": "" + }, + { + "name": "iconamoon:upside-down-face-bold", + "svg": "" + }, + { + "name": "iconamoon:upside-down-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:upside-down-face-fill", + "svg": "" + }, + { + "name": "iconamoon:upside-down-face-light", + "svg": "" + }, + { + "name": "iconamoon:upside-down-face-thin", + "svg": "" + }, + { + "name": "iconamoon:volume-down", + "svg": "" + }, + { + "name": "iconamoon:volume-down-bold", + "svg": "" + }, + { + "name": "iconamoon:volume-down-duotone", + "svg": "" + }, + { + "name": "iconamoon:volume-down-fill", + "svg": "" + }, + { + "name": "iconamoon:volume-down-light", + "svg": "" + }, + { + "name": "iconamoon:volume-down-thin", + "svg": "" + }, + { + "name": "iconamoon:volume-off", + "svg": "" + }, + { + "name": "iconamoon:volume-off-bold", + "svg": "" + }, + { + "name": "iconamoon:volume-off-duotone", + "svg": "" + }, + { + "name": "iconamoon:volume-off-fill", + "svg": "" + }, + { + "name": "iconamoon:volume-off-light", + "svg": "" + }, + { + "name": "iconamoon:volume-off-thin", + "svg": "" + }, + { + "name": "iconamoon:volume-up", + "svg": "" + }, + { + "name": "iconamoon:volume-up-bold", + "svg": "" + }, + { + "name": "iconamoon:volume-up-duotone", + "svg": "" + }, + { + "name": "iconamoon:volume-up-fill", + "svg": "" + }, + { + "name": "iconamoon:volume-up-light", + "svg": "" + }, + { + "name": "iconamoon:volume-up-thin", + "svg": "" + }, + { + "name": "iconamoon:winking-face", + "svg": "" + }, + { + "name": "iconamoon:winking-face-bold", + "svg": "" + }, + { + "name": "iconamoon:winking-face-duotone", + "svg": "" + }, + { + "name": "iconamoon:winking-face-fill", + "svg": "" + }, + { + "name": "iconamoon:winking-face-light", + "svg": "" + }, + { + "name": "iconamoon:winking-face-thin", + "svg": "" + }, + { + "name": "iconamoon:zoom-in", + "svg": "" + }, + { + "name": "iconamoon:zoom-in-bold", + "svg": "" + }, + { + "name": "iconamoon:zoom-in-duotone", + "svg": "" + }, + { + "name": "iconamoon:zoom-in-fill", + "svg": "" + }, + { + "name": "iconamoon:zoom-in-light", + "svg": "" + }, + { + "name": "iconamoon:zoom-in-thin", + "svg": "" + }, + { + "name": "iconamoon:zoom-out", + "svg": "" + }, + { + "name": "iconamoon:zoom-out-bold", + "svg": "" + }, + { + "name": "iconamoon:zoom-out-duotone", + "svg": "" + }, + { + "name": "iconamoon:zoom-out-fill", + "svg": "" + }, + { + "name": "iconamoon:zoom-out-light", + "svg": "" + }, + { + "name": "iconamoon:zoom-out-thin", + "svg": "" + } +] \ No newline at end of file diff --git a/assets/style/css/base/theme.css b/assets/style/css/base/theme.css new file mode 100644 index 0000000..a6c095c --- /dev/null +++ b/assets/style/css/base/theme.css @@ -0,0 +1,268 @@ +html[data-theme="biasa"] { + --color-primary: 0, 165, 154; + --color-secondary: 97, 176, 183; + --color-accent: 243, 244, 246; + --color-success: 79, 192, 103; + --color-info: 65, 133, 242; + --color-warning: 246, 141, 32; + --color-danger: 229, 83, 69; + --text-color: 9, 9, 11; + --border-color: 228, 228, 231; + --bg-1: 243, 244, 246; + --bg-2: 255, 255, 255; + --sidebar: 38, 50, 55; + --sidebar-menu: 26, 35, 38; + --sidebar-text: 255, 255, 255; + --header: 49, 65, 71; + --header-text: 255, 255, 255; + --scroll-color: 170, 170, 170; + --scroll-hover-color: 155, 155, 155; + --fk-border-color: 228, 228, 231; + --fk-placeholder-color: 146, 146, 153; + --box-shadow: rgba(36, 35, 71, 0.05) 0px 1px 1px, + rgba(36, 35, 71, 0.05) 0px 0px 1px 1px; + --cp-bg: 255, 255, 255; + --rounded-box: 0.2rem; + --rounded-btn: 0.2rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --tw-shadow: #e5eaf2; +} + +html[data-theme="gelap"] { + --color-primary: 97, 176, 183; + --color-secondary: 13, 27, 42; + --color-accent: 15, 23, 42; + --color-success: 79, 192, 103; + --color-info: 65, 133, 242; + --color-warning: 246, 141, 32; + --color-danger: 229, 83, 69; + --text-color: 209, 213, 219; + --border-color: 30, 41, 59; + --bg-1: 15, 23, 42; + --bg-2: 30, 41, 59; + --sidebar: 38, 50, 55; + --sidebar-menu: 26, 35, 38; + --sidebar-text: 255, 255, 255; + --header: 49, 65, 71; + --header-text: 255, 255, 255; + --scroll-color: 170, 170, 170; + --scroll-hover-color: 155, 155, 155; + --fk-border-color: 71, 85, 105; + --fk-placeholder-color: 71, 85, 105; + --box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --cp-bg: 255, 255, 255; + --rounded-box: 0.5rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.625rem 1.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --tw-shadow: #e5eaf2; +} + +html[data-theme="biru"] { + --color-primary: 0, 102, 204; + --color-secondary: 51, 153, 255; + --color-accent: 255, 204, 0; + --color-success: 46, 204, 113; + --color-info: 52, 152, 219; + --color-warning: 246, 141, 32; + --color-danger: 231, 76, 60; + --text-color: 0, 0, 0; + --border-color: 200, 200, 200; + --bg-1: 240, 248, 255; + --bg-2: 230, 240, 250; + --sidebar: 38, 50, 55; + --sidebar-menu: 26, 35, 38; + --sidebar-text: 255, 255, 255; + --header: 49, 65, 71; + --header-text: 255, 255, 255; + --scroll-color: 180, 180, 180; + --scroll-hover-color: 150, 150, 150; + --fk-border-color: 200, 200, 200; + --fk-placeholder-color: 150, 150, 150; + --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --cp-bg: 255, 255, 255; + --rounded-box: 0.5rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.625rem 1.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --tw-shadow: #e5eaf2; +} + +html[data-theme="merah"] { + --color-primary: 204, 0, 0; + --color-secondary: 255, 102, 102; + --color-accent: 255, 255, 153; + --color-success: 46, 204, 113; + --color-info: 52, 152, 219; + --color-warning: 246, 141, 32; + --color-danger: 231, 76, 60; + --text-color: 0, 0, 0; + --border-color: 200, 200, 200; + --bg-1: 255, 240, 240; + --bg-2: 255, 230, 230; + --sidebar: 38, 50, 55; + --sidebar-menu: 26, 35, 38; + --sidebar-text: 255, 255, 255; + --header: 49, 65, 71; + --header-text: 255, 255, 255; + --scroll-color: 180, 180, 180; + --scroll-hover-color: 150, 150, 150; + --fk-border-color: 200, 200, 200; + --fk-placeholder-color: 150, 150, 150; + --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --cp-bg: 255, 255, 255; + --rounded-box: 0.5rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.625rem 1.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --tw-shadow: #e5eaf2; +} + +html[data-theme="ungu"] { + --color-primary: 75, 0, 130; + --color-secondary: 138, 43, 226; + --color-accent: 255, 215, 0; + --color-success: 46, 204, 113; + --color-info: 52, 152, 219; + --color-warning: 246, 141, 32; + --color-danger: 231, 76, 60; + --text-color: 0, 0, 0; + --border-color: 200, 200, 200; + --bg-1: 240, 248, 255; + --bg-2: 230, 240, 250; + --sidebar: 38, 50, 55; + --sidebar-menu: 26, 35, 38; + --sidebar-text: 255, 255, 255; + --header: 49, 65, 71; + --header-text: 255, 255, 255; + --scroll-color: 180, 180, 180; + --scroll-hover-color: 150, 150, 150; + --fk-border-color: 200, 200, 200; + --fk-placeholder-color: 150, 150, 150; + --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --cp-bg: 255, 255, 255; + --rounded-box: 0.5rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.625rem 1.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --tw-shadow: #e5eaf2; +} + +html[data-theme="oren"] { + --color-primary: 255, 103, 0; + --color-secondary: 255, 159, 64; + --color-accent: 0, 128, 128; + --color-success: 46, 204, 113; + --color-info: 52, 152, 219; + --color-warning: 246, 141, 32; + --color-danger: 231, 76, 60; + --text-color: 0, 0, 0; + --border-color: 200, 200, 200; + --bg-1: 255, 250, 240; + --bg-2: 255, 245, 230; + --sidebar: 38, 50, 55; + --sidebar-menu: 26, 35, 38; + --sidebar-text: 255, 255, 255; + --header: 49, 65, 71; + --header-text: 255, 255, 255; + --scroll-color: 180, 180, 180; + --scroll-hover-color: 150, 150, 150; + --fk-border-color: 200, 200, 200; + --fk-placeholder-color: 150, 150, 150; + --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --cp-bg: 255, 255, 255; + --rounded-box: 0.5rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.625rem 1.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --tw-shadow: #e5eaf2; +} + +html[data-theme="LZS"] { + --color-primary: 0, 90, 173; /* #005AAD - Blue */ + --color-secondary: 141, 199, 61; /* #8DC73D - Green */ + --color-accent: 255, 242, 0; /* #FFF200 - Yellow */ + --color-success: 141, 199, 61; /* Using the green for success */ + --color-info: 0, 90, 173; /* Using the blue for info */ + --color-warning: 246, 141, 32; /* Using consistent orange for warning */ + --color-danger: 229, 83, 69; /* Keeping original red for danger */ + --text-color: 0, 0, 0; /* Black text */ + --border-color: 220, 220, 220; /* Light gray for borders */ + --bg-1: 245, 250, 255; /* Very light blue background */ + --bg-2: 255, 255, 255; /* White background */ + --sidebar: 0, 58, 112; /* Darker blue for sidebar - #003A70 */ + --sidebar-menu: 0, 40, 77; /* Even darker blue for sidebar menu - #00284D */ + --sidebar-text: 255, 255, 255; /* White text for sidebar */ + --header: 0, 90, 173; /* Blue header - #005AAD */ + --header-text: 255, 255, 255; /* White text for header */ + --scroll-color: 180, 180, 180; /* Gray scrollbar */ + --scroll-hover-color: 150, 150, 150; /* Darker gray on hover */ + --fk-border-color: 220, 220, 220; /* Light gray for form borders */ + --fk-placeholder-color: 150, 150, 150; /* Gray for placeholders */ + --box-shadow: rgba(0, 90, 173, 0.1) 0px 1px 2px, + rgba(0, 90, 173, 0.08) 0px 0px 2px; /* Blue-tinted shadow */ + --cp-bg: 255, 255, 255; /* White background */ + --rounded-box: 0.5rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: 0.2s; + --btn-text-case: uppercase; + --btn-focus-scale: 0.95; + --padding-btn: 0.625rem 1.5rem; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + /* Yellow accents in specific UI elements */ + --active-menu-bg: 255, 242, 0, 0.1; /* Subtle yellow background for active menu items */ + --active-menu-border: 255, 242, 0; /* Yellow border for active menu items */ + --focus-ring: 255, 242, 0, 0.5; /* Yellow focus ring */ + --tw-shadow: #e5eaf2; +} diff --git a/assets/style/css/component/alert.css b/assets/style/css/component/alert.css new file mode 100644 index 0000000..5be607f --- /dev/null +++ b/assets/style/css/component/alert.css @@ -0,0 +1,28 @@ +/* RS Alert Component */ +.alert { + @apply visible flex items-center justify-between py-3 px-3 rounded-lg; +} + +.alert.alert-primary { + @apply bg-primary/20 text-primary; +} + +.alert.alert-secondary { + @apply bg-secondary/20 text-secondary; +} + +.alert.alert-success { + @apply bg-success/20 text-success; +} + +.alert.alert-info { + @apply bg-info/20 text-info; +} + +.alert.alert-warning { + @apply bg-warning/20 text-warning; +} + +.alert.alert-danger { + @apply bg-danger/20 text-danger; +} diff --git a/assets/style/css/component/badge.css b/assets/style/css/component/badge.css new file mode 100644 index 0000000..85fe604 --- /dev/null +++ b/assets/style/css/component/badge.css @@ -0,0 +1,32 @@ +/* RS Badge Component */ +.badge { + @apply inline-flex items-center justify-center px-2 py-1 rounded-full text-xs font-semibold leading-none; +} + +.badge.badge-primary { + @apply bg-primary text-white; +} + +.badge.badge-secondary { + @apply bg-secondary text-white; +} + +.badge.badge-success { + @apply bg-success text-white; +} + +.badge.badge-info { + @apply bg-info text-white; +} + +.badge.badge-warning { + @apply bg-warning text-white; +} + +.badge.badge-danger { + @apply bg-danger text-white; +} + +.badge.badge-disabled { + @apply bg-gray-300 text-gray-600; +} diff --git a/assets/style/css/component/button.css b/assets/style/css/component/button.css new file mode 100644 index 0000000..b783216 --- /dev/null +++ b/assets/style/css/component/button.css @@ -0,0 +1,323 @@ +/* RS Button */ +.button { + @apply w-fit rounded-lg flex justify-center items-center h-fit; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +/* Enhanced hover effect with slight 3D transition */ +.button[class*="button-"]:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.button[class*="button-"]:active { + transform: translateY(0px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.button[class*="button-"]:disabled { + opacity: 0.3; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* Primary Button - Blue with yellow accent */ +.button.button-primary { + @apply bg-primary text-white; + box-shadow: 0 2px 4px rgba(var(--color-primary), 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border: 1px solid rgba(var(--color-primary), 0.8); +} + +.button.button-primary:hover::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background-color: rgb(var(--active-menu-border)); + animation: slide-in 0.3s ease forwards; +} + +@keyframes slide-in { + 0% { transform: scaleX(0); opacity: 0; } + 100% { transform: scaleX(1); opacity: 1; } +} + +.button.button-secondary { + @apply bg-secondary text-white; + box-shadow: 0 2px 4px rgba(var(--color-secondary), 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border: 1px solid rgba(var(--color-secondary), 0.8); +} + +.button.button-success { + @apply bg-success text-white; + box-shadow: 0 2px 4px rgba(var(--color-success), 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border: 1px solid rgba(var(--color-success), 0.8); +} + +.button.button-info { + @apply bg-info text-white; + box-shadow: 0 2px 4px rgba(var(--color-info), 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border: 1px solid rgba(var(--color-info), 0.8); +} + +.button.button-warning { + @apply bg-warning text-white; + box-shadow: 0 2px 4px rgba(var(--color-warning), 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border: 1px solid rgba(var(--color-warning), 0.8); +} + +.button.button-danger { + @apply bg-danger text-white; + box-shadow: 0 2px 4px rgba(var(--color-danger), 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border: 1px solid rgba(var(--color-danger), 0.8); +} + +/* Updated outline buttons */ +.button[class*="outline-"]:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.button.outline-primary { + @apply border border-primary text-primary; + box-shadow: 0 1px 3px rgba(var(--color-primary), 0.1); +} + +.button.outline-primary:hover { + @apply bg-primary/5; + box-shadow: 0 3px 6px rgba(var(--color-primary), 0.2); + transform: translateY(-1px); +} + +.button.outline-secondary { + @apply border border-secondary text-secondary; + box-shadow: 0 1px 3px rgba(var(--color-secondary), 0.1); +} + +.button.outline-secondary:hover { + @apply bg-secondary/5; + box-shadow: 0 3px 6px rgba(var(--color-secondary), 0.2); + transform: translateY(-1px); +} + +.button.outline-success { + @apply border border-success text-success; + box-shadow: 0 1px 3px rgba(var(--color-success), 0.1); +} + +.button.outline-success:hover { + @apply bg-success/5; + box-shadow: 0 3px 6px rgba(var(--color-success), 0.2); + transform: translateY(-1px); +} + +.button.outline-info { + @apply border border-info text-info; + box-shadow: 0 1px 3px rgba(var(--color-info), 0.1); +} + +.button.outline-info:hover { + @apply bg-info/5; + box-shadow: 0 3px 6px rgba(var(--color-info), 0.2); + transform: translateY(-1px); +} + +.button.outline-warning { + @apply border border-warning text-warning; + box-shadow: 0 1px 3px rgba(var(--color-warning), 0.1); +} + +.button.outline-warning:hover { + @apply bg-warning/5; + box-shadow: 0 3px 6px rgba(var(--color-warning), 0.2); + transform: translateY(-1px); +} + +.button.outline-danger { + @apply border border-danger text-danger; + box-shadow: 0 1px 3px rgba(var(--color-danger), 0.1); +} + +.button.outline-danger:hover { + @apply bg-danger/5; + box-shadow: 0 3px 6px rgba(var(--color-danger), 0.2); + transform: translateY(-1px); +} + +.button[class*="text-"]:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.button.texts-primary { + @apply text-primary; + position: relative; +} + +.button.texts-primary:hover { + @apply bg-primary/10; +} + +.button.texts-primary:after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: rgb(var(--color-primary)); + transition: width 0.3s ease; +} + +.button.texts-primary:hover:after { + width: 100%; +} + +.button.texts-secondary { + @apply text-secondary; + position: relative; +} + +.button.texts-secondary:hover { + @apply bg-secondary/10; +} + +.button.texts-secondary:after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: rgb(var(--color-secondary)); + transition: width 0.3s ease; +} + +.button.texts-secondary:hover:after { + width: 100%; +} + +.button.texts-success { + @apply text-success; + position: relative; +} + +.button.texts-success:hover { + @apply bg-success/10; +} + +.button.texts-success:after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: rgb(var(--color-success)); + transition: width 0.3s ease; +} + +.button.texts-success:hover:after { + width: 100%; +} + +.button.texts-info { + @apply text-info; + position: relative; +} + +.button.texts-info:hover { + @apply bg-info/10; +} + +.button.texts-info:after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: rgb(var(--color-info)); + transition: width 0.3s ease; +} + +.button.texts-info:hover:after { + width: 100%; +} + +.button.texts-warning { + @apply text-warning; + position: relative; +} + +.button.texts-warning:hover { + @apply bg-warning/10; +} + +.button.texts-warning:after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: rgb(var(--color-warning)); + transition: width 0.3s ease; +} + +.button.texts-warning:hover:after { + width: 100%; +} + +.button.texts-danger { + @apply text-danger; + position: relative; +} + +.button.texts-danger:hover { + @apply bg-danger/10; +} + +.button.texts-danger:after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: rgb(var(--color-danger)); + transition: width 0.3s ease; +} + +.button.texts-danger:hover:after { + width: 100%; +} + +.button-sm { + @apply text-xs; + padding: var(--padding-btn); + line-height: 1.4; +} + +.button-md { + @apply text-sm; + padding: var(--padding-btn); + line-height: 1.5; +} + +.button-lg { + @apply text-base; + padding: var(--padding-btn); + line-height: 1.6; +} diff --git a/assets/style/css/component/card.css b/assets/style/css/component/card.css new file mode 100644 index 0000000..398055e --- /dev/null +++ b/assets/style/css/component/card.css @@ -0,0 +1,24 @@ +/* RS Card Component */ +.card { + @apply mb-6; + background-color: rgb(var(--bg-2)); + border-radius: var(--rounded-box); + box-shadow: var(--box-shadow); +} + +.card .card-header { + @apply text-xl px-6 py-4 font-medium; + line-height: 1.5; + border-bottom: 1px solid rgb(var(--border-color)); +} + +.card .card-body { + @apply px-6 py-5; + line-height: 1.6; +} + +.card .card-footer { + @apply px-6 py-4; + line-height: 1.5; + border-top: 1px solid rgb(var(--border-color)); +} diff --git a/assets/style/css/component/collapse.css b/assets/style/css/component/collapse.css new file mode 100644 index 0000000..4f22bd9 --- /dev/null +++ b/assets/style/css/component/collapse.css @@ -0,0 +1,61 @@ +/* RS Collapse Component */ +.accordion { + @apply w-full mb-4 visible; + background-color: rgb(var(--bg-2)); +} + +.accordion.accordion-border { + @apply border-t border-x rounded-lg; + border-color: rgb(var(--border-color)); +} + +.accordion .accordion-group { + @apply overflow-hidden duration-300 dark:border-slate-700; + background-color: rgb(var(--bg-2)); +} + +.accordion .accordion-group.accordion-default { + @apply border-b; + border-color: rgb(var(--border-color)); +} + +.accordion .accordion-group.accordion-border { + @apply border-b last:rounded-b-lg; + border-color: rgb(var(--border-color)); +} + +.accordion .accordion-group.accordion-card { + @apply my-4; + box-shadow: var(--box-shadow); +} + +.accordion .accordion-group .accordion-header { + @apply text-lg font-medium px-5 pr-10 py-4 cursor-pointer; +} + +.accordion .accordion-group .accordion-body { + @apply px-5 pb-4 pt-2; +} + +.accordion-group .accordion-header { + position: relative; +} + +.accordion-group .accordion-header::after { + content: ""; + position: absolute; + width: 19px; + height: 19px; + top: 50%; + right: 0%; + transform: translate(-100%, -50%) rotate(0deg); + background: url("/assets/img/icon/chevron-right.svg"); + background-position: center center; + background-repeat: no-repeat; + background-size: cover; + transition: 0.25s ease-in-out; +} + +.accordion-group--open .accordion-header::after { + transform: translate(-100%, -50%) rotate(90deg); +} diff --git a/assets/style/css/component/common.css b/assets/style/css/component/common.css new file mode 100644 index 0000000..0ee9884 --- /dev/null +++ b/assets/style/css/component/common.css @@ -0,0 +1,40 @@ +body { + color: rgb(var(--text-color)); + background-color: rgb(var(--bg-1)); +} + +.w-header { + @apply z-20 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md; + background-color: rgb(var(--header)); + color: rgb(var(--header-text)); + box-shadow: var(--box-shadow); +} + +.w-header-search { + @apply px-4 z-40 duration-300 shadow-md -top-20 focus-within:top-0 right-0; + background-color: rgb(var(--bg-2)); +} + +.vertical-menu { + @apply text-base h-screen fixed w-64 top-0 z-50 duration-300 border-l-0 shadow-md; + background-color: rgb(var(--sidebar)); + color: rgb(var(--sidebar-text)); + box-shadow: var(--box-shadow); +} + +.icon-btn { + @apply flex + items-center + justify-center + transition-colors + duration-300; + color: rgb(var(--header-text)); +} + +.icon-btn.profile { + color: rgb(var(--header-text)); +} + +.icon-btn:hover { + background-color: rgb(var(--sidebar)); +} diff --git a/assets/style/css/component/dropdown.css b/assets/style/css/component/dropdown.css new file mode 100644 index 0000000..fde209d --- /dev/null +++ b/assets/style/css/component/dropdown.css @@ -0,0 +1,53 @@ +/* RS Dropdown Component */ +.dropdown { + @apply relative inline-flex; +} + +.dropdown .dropdown-section { + @apply absolute z-10 shadow-md rounded-lg py-1 whitespace-nowrap; + background-color: rgb(var(--bg-2)); +} + +.dropdown-section.list-bottom-sm { + @apply top-10; +} + +.dropdown-section.list-bottom-md { + @apply top-12; +} + +.dropdown-section.list-bottom-lg { + @apply top-16; +} + +.dropdown-section.list-top-sm { + @apply bottom-10; +} + +.dropdown-section.list-top-md { + @apply bottom-12; +} + +.dropdown-section.list-top-lg { + @apply bottom-16; +} + +.dropdown-section.list-left { + @apply top-0 -left-[10.5rem]; +} + +.dropdown-section.list-right { + @apply top-0 -right-[10.5rem]; +} + +.dropdown-section.list-align-right { + @apply right-0; +} + +.dropdown-section .dropdown-item { + @apply flex items-center cursor-pointer px-4 py-2; +} + +.dropdown-section .dropdown-item:hover { + background-color: rgb(var(--bg-1)); +} diff --git a/assets/style/css/component/modal.css b/assets/style/css/component/modal.css new file mode 100644 index 0000000..4d5f300 --- /dev/null +++ b/assets/style/css/component/modal.css @@ -0,0 +1,50 @@ +/* RS Modal Component */ +.modal { + @apply fixed top-0 left-0 w-full h-full overflow-hidden z-[1000] duration-300; + background: rgba(15, 23, 42, 0.5); +} + +.modal.modal-top { + @apply flex items-start; +} + +.modal.modal-center { + @apply flex items-center; +} + +.modal.modal-end { + @apply flex items-end; +} + +.modal.modal-hide-overlay { + @apply !bg-transparent; +} + +.modal .modal-dialog { + @apply w-full md:w-auto relative z-[9999]; + margin: 1.75rem auto; +} + +.modal .modal-dialog .modal-content { + @apply border-none shadow-lg relative flex flex-col w-full pointer-events-auto bg-clip-padding rounded-md outline-none text-current; + background-color: rgb(var(--bg-2)); +} + +.modal .modal-dialog .modal-content .modal-header { + @apply flex flex-shrink-0 items-center justify-between p-4 border-b rounded-t-md; + border-color: rgb(var(--border-color)); +} + +.modal .modal-dialog .modal-content .modal-body { + @apply relative p-4; +} + +.modal .modal-dialog .modal-content .modal-footer { + @apply flex flex-shrink-0 flex-wrap items-center justify-end px-4 pb-4 rounded-b-md gap-x-3; +} + +@media screen and (max-width: 768px) { + .modal-dialog { + margin: 1.75rem 1rem; + } +} diff --git a/assets/style/css/component/progress.css b/assets/style/css/component/progress.css new file mode 100644 index 0000000..9c1c9b6 --- /dev/null +++ b/assets/style/css/component/progress.css @@ -0,0 +1,80 @@ +/* RS Progress Component */ +.progress-wrapper { + @apply w-full mb-4; +} + +.progress-wrapper .progress-label { + @apply text-sm mb-1; +} + +.progress-wrapper .progress { + @apply flex rounded-full; +} + +.progress-wrapper .progress.progress-sm { + @apply h-3; +} + +.progress-wrapper .progress.progress-md { + @apply h-4; +} + +.progress-wrapper .progress.progress-lg { + @apply h-5; +} + +.progress-wrapper .progress.progress-sm { + @apply h-3; +} + +.progress-wrapper .progress.progress-primary { + @apply bg-primary/20; +} + +.progress-wrapper .progress.progress-secondary { + @apply bg-secondary/20; +} + +.progress-wrapper .progress.progress-success { + @apply bg-success/20; +} + +.progress-wrapper .progress.progress-info { + @apply bg-info/20; +} + +.progress-wrapper .progress.progress-warning { + @apply bg-warning/20; +} + +.progress-wrapper .progress.progress-danger { + @apply bg-danger/20; +} + +.progress-wrapper .progress .progress-bar { + @apply flex items-center justify-center rounded-full text-white; +} + +.progress-wrapper .progress .progress-bar.primary { + @apply bg-primary; +} + +.progress-wrapper .progress .progress-bar.secondary { + @apply bg-secondary; +} + +.progress-wrapper .progress .progress-bar.success { + @apply bg-success; +} + +.progress-wrapper .progress .progress-bar.info { + @apply bg-info; +} + +.progress-wrapper .progress .progress-bar.warning { + @apply bg-warning; +} + +.progress-wrapper .progress .progress-bar.danger { + @apply bg-danger; +} diff --git a/assets/style/css/component/tab.css b/assets/style/css/component/tab.css new file mode 100644 index 0000000..b026c61 --- /dev/null +++ b/assets/style/css/component/tab.css @@ -0,0 +1,322 @@ +/* RS Tab Component */ +.tab { + @apply rounded-md; +} + +.tab.vertical { + @apply flex flex-col md:flex-row; +} + +.tab.tab-card { + @apply shadow-md pt-4; +} + +.tab.card-vertical { + @apply shadow-md; +} + +.tab.card-primary { + @apply bg-primary; +} + +.tab.card-secondary { + @apply bg-secondary; +} + +.tab.card-success { + @apply bg-success; +} + +.tab.card-info { + @apply bg-info; +} + +.tab.card-warning { + @apply bg-warning; +} + +.tab.card-danger { + @apply bg-danger; +} + +.tab .tab-nav { + @apply flex flex-wrap list-none pl-0; +} + +.tab .tab-nav.tab-nav-card { + @apply mx-4 mb-0; +} + +.tab .tab-nav.card-vertical { + @apply py-0 pt-4 md:py-4; +} + +.tab .tab-nav.vertical { + @apply flex-row md:flex-col gap-2; +} + +.tab .tab-nav.vertical-fill { + @apply flex-1; +} + +.tab .tab-nav .tab-item { + @apply cursor-pointer; +} + +.tab .tab-nav .tab-item.fill { + @apply flex-1 w-full; +} + +.tab .tab-nav .tab-item.border-horizontal { + @apply border-0 hover:border hover:border-b-0 rounded-t-md px-6; +} + +.tab .tab-nav .tab-item.border-horizontal-active { + @apply border rounded-t-md border-b-0; +} + +.tab .tab-nav .tab-item.border-vertical { + @apply border-0 hover:border hover:border-r-0 rounded-l-md px-6; +} + +.tab .tab-nav .tab-item.border-vertical-active { + @apply border rounded-t-md rounded-bl-none md:rounded-r-none md:rounded-l-md border-r border-b-0 md:border-b md:border-r-0; +} + +.tab .tab-nav .tab-item.border-hover-primary { + @apply hover:border-primary; +} + +.tab .tab-nav .tab-item.border-hover-secondary { + @apply hover:border-secondary; +} + +.tab .tab-nav .tab-item.border-hover-success { + @apply hover:border-success; +} + +.tab .tab-nav .tab-item.border-hover-info { + @apply hover:border-info; +} + +.tab .tab-nav .tab-item.border-hover-warning { + @apply hover:border-warning; +} + +.tab .tab-nav .tab-item.border-hover-danger { + @apply hover:border-danger; +} + +.tab .tab-nav .tab-item.border-active-primary { + @apply border-primary text-primary; +} + +.tab .tab-nav .tab-item.border-active-secondary { + @apply border-secondary text-secondary; +} + +.tab .tab-nav .tab-item.border-active-success { + @apply border-success text-success; +} + +.tab .tab-nav .tab-item.border-active-info { + @apply border-info text-info; +} + +.tab .tab-nav .tab-item.border-active-warning { + @apply border-warning text-warning; +} + +.tab .tab-nav .tab-item.border-active-danger { + @apply border-danger text-danger; +} + +.tab .tab-nav .tab-item .tab-item-link { + @apply block font-medium text-base leading-tight capitalize border-x-0 border-t-0 py-3; +} + +.tab .tab-nav .tab-item .tab-item-link.default { + @apply hover:border-b-2 mx-2 px-4; +} + +.tab .tab-nav .tab-item .tab-item-link.default-vertical { + @apply hover:border-l-2 mx-2 px-4; +} + +.tab .tab-nav .tab-item .tab-item-link.default-active { + @apply border-b-2; +} + +.tab .tab-nav .tab-item .tab-item-link.default-vertical-active { + @apply border-l-2; +} + +.tab .tab-nav .tab-item .tab-item-link.default-hover-primary { + @apply hover:border-primary hover:text-primary; +} + +.tab .tab-nav .tab-item .tab-item-link.default-hover-secondary { + @apply hover:border-secondary hover:text-secondary; +} + +.tab .tab-nav .tab-item .tab-item-link.default-hover-success { + @apply hover:border-success hover:text-success; +} + +.tab .tab-nav .tab-item .tab-item-link.default-hover-info { + @apply hover:border-info hover:text-info; +} + +.tab .tab-nav .tab-item .tab-item-link.default-hover-warning { + @apply hover:border-warning hover:text-warning; +} + +.tab .tab-nav .tab-item .tab-item-link.default-hover-danger { + @apply hover:border-danger hover:text-danger; +} + +.tab .tab-nav .tab-item .tab-item-link.default-primary { + @apply text-primary border-primary; +} + +.tab .tab-nav .tab-item .tab-item-link.default-secondary { + @apply text-secondary border-secondary; +} + +.tab .tab-nav .tab-item .tab-item-link.default-success { + @apply text-success border-success; +} + +.tab .tab-nav .tab-item .tab-item-link.default-info { + @apply text-info border-info; +} + +.tab .tab-nav .tab-item .tab-item-link.default-warning { + @apply text-warning border-warning; +} + +.tab .tab-nav .tab-item .tab-item-link.default-danger { + @apply text-danger border-danger; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card { + @apply px-5 mx-1 text-white rounded-t-md; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-vertical { + @apply px-5 my-0 text-white rounded-bl-none rounded-t-md md:rounded-tr-none md:rounded-l-md; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-primary { + @apply bg-primary/90; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-secondary { + @apply bg-secondary/90; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-success { + @apply bg-success/90; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-info { + @apply bg-info/90; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-warning { + @apply bg-warning/90; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-danger { + @apply bg-danger/90; +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-primary-active { + @apply text-primary/90; + background-color: rgb(var(--bg-2)); +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-secondary-active { + @apply text-secondary/90; + background-color: rgb(var(--bg-2)); +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-success-active { + @apply text-success/90; + background-color: rgb(var(--bg-2)); +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-info-active { + @apply text-info/90; + background-color: rgb(var(--bg-2)); +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-warning-active { + @apply text-warning/90; + background-color: rgb(var(--bg-2)); +} + +.tab .tab-nav .tab-item .tab-item-link.link-card-danger-active { + @apply text-danger/90; + background-color: rgb(var(--bg-2)); +} + +.tab .tab-nav .tab-item .tab-item-link.link-justify-left { + @apply flex flex-wrap justify-start; +} + +.tab .tab-nav .tab-item .tab-item-link.link-justify-center { + @apply flex flex-wrap justify-center; +} + +.tab .tab-nav .tab-item .tab-item-link.link-justify-right { + @apply flex flex-wrap justify-end; +} + +.tab .tab-content { + background-color: rgb(var(--bg-2)); +} + +.tab .tab-content.content-vertical { + @apply flex-grow; +} + +.tab .tab-content.content-vertical-fill { + @apply flex-1; +} + +.tab .tab-content.content-border { + @apply border rounded-b-md; +} + +.tab .tab-content.content-border-vertical { + @apply border rounded-md rounded-l-none; +} + +.tab .tab-content.content-border-primary { + @apply border-primary; +} + +.tab .tab-content.content-border-secondary { + @apply border-secondary; +} + +.tab .tab-content.content-border-success { + @apply border-success; +} + +.tab .tab-content.content-border-info { + @apply border-info; +} + +.tab .tab-content.content-border-warning { + @apply border-warning; +} + +.tab .tab-content.content-border-danger { + @apply border-danger; +} + +.tab .tab-content .tab-pane { + @apply py-4 px-4; +} diff --git a/assets/style/css/component/table.css b/assets/style/css/component/table.css new file mode 100644 index 0000000..3582637 --- /dev/null +++ b/assets/style/css/component/table.css @@ -0,0 +1,32 @@ +/* Table Component */ +.table-wrapper { + @apply w-full border-0 rounded-md border-gray-200 dark:border-slate-700; +} + +.table-header { + @apply m-4 overflow-hidden; + transition: max-height 0.25s cubic-bezier(0, 1, 0, 1); + max-height: 42px; +} + +.table-header.open { + @apply overflow-visible; + transition: max-height 0.5s ease-in-out; + max-height: 9999px; +} + +.table-header-filter { + @apply flex flex-col md:flex-row justify-start md:justify-between items-start md:items-center gap-4; +} + +.table-content { + @apply w-full; +} + +.table-footer { + @apply flex justify-center md:justify-between items-center m-4; +} + +.table-footer-page { + @apply flex justify-center gap-x-2; +} diff --git a/assets/style/css/example-theme.css b/assets/style/css/example-theme.css new file mode 100644 index 0000000..5fb0932 --- /dev/null +++ b/assets/style/css/example-theme.css @@ -0,0 +1,65 @@ +/* Example Custom Theme for corradAF Base Project */ +/* This file demonstrates how custom themes should be structured */ +/* Define your custom theme variables here */ +/* For example: +:root { + --primary-color: #yourColor; +} +*/ + +:root { + /* Custom color variables */ + --color-primary: 46, 125, 50; /* Green theme */ + --color-secondary: 117, 117, 117; + --color-success: 76, 175, 80; + --color-info: 33, 150, 243; + --color-warning: 255, 152, 0; + --color-danger: 244, 67, 54; + + /* Custom background colors */ + --bg-primary: 245, 245, 245; + --bg-secondary: 255, 255, 255; +} + +/* Dark theme overrides */ +.dark { + --bg-primary: 18, 18, 18; + --bg-secondary: 30, 30, 30; +} + +/* Custom component styles */ +.btn-primary { + background-color: rgb(var(--color-primary)); + border-color: rgb(var(--color-primary)); +} + +.btn-primary:hover { + background-color: rgba(var(--color-primary), 0.8); + border-color: rgba(var(--color-primary), 0.8); +} + +/* Header customizations */ +.w-header { + background: linear-gradient(135deg, rgb(var(--color-primary)), rgba(var(--color-primary), 0.8)); + color: white; +} + +/* Sidebar customizations */ +.vertical-menu { + background-color: rgb(var(--bg-secondary)); + border-right: 1px solid rgba(var(--color-primary), 0.1); +} + +/* Card customizations */ +.card { + background-color: rgb(var(--bg-secondary)); + border: 1px solid rgba(var(--color-primary), 0.1); + box-shadow: 0 2px 4px rgba(var(--color-primary), 0.1); +} + +/* Example of responsive design */ +@media (max-width: 768px) { + .w-header { + background: rgb(var(--color-primary)); + } +} \ No newline at end of file diff --git a/assets/style/css/form/box.css b/assets/style/css/form/box.css new file mode 100644 index 0000000..e19092f --- /dev/null +++ b/assets/style/css/form/box.css @@ -0,0 +1,63 @@ +.formkit-inner-box { + @apply relative; +} + +.formkit-label { + @apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger; +} + +.formkit-fieldset-box { + @apply max-w-md border border-[rgb(var(--fk-border-color))] rounded-lg px-4 py-2; +} + +.formkit-legend-box { + @apply font-bold text-sm; +} + +.formkit-wrapper-box { + @apply flex items-center mb-3 cursor-pointer; +} + +.formkit-help-box { + @apply mb-3; +} + +.formkit-input-box { + @apply flex + items-center + hover:cursor-pointer + appearance-none + h-5 w-5 mr-2 + border-2 + border-[rgb(var(--fk-border-color))] + checked:bg-primary + checked:border-transparent + bg-[rgb(var(--bg-2))] + rounded-md + checked:shadow-sm checked:shadow-primary/40 + focus:outline-none focus:ring-0 transition duration-200; +} + +.formkit-input-radio { + @apply flex + items-center + hover:cursor-pointer + appearance-none + h-5 w-5 mr-2 + border-2 + border-[rgb(var(--fk-border-color))] + checked:bg-primary + checked:border-transparent + bg-[rgb(var(--bg-2))] + rounded-full + checked:shadow-sm checked:shadow-primary/40 + focus:outline-none focus:ring-0 transition duration-200; +} + +.formkit-label-box { + @apply text-sm formkit-disabled:text-gray-300; +} + +.formkit-message-box { + @apply formkit-invalid:text-red-500; +} diff --git a/assets/style/css/form/button.css b/assets/style/css/form/button.css new file mode 100644 index 0000000..441bc79 --- /dev/null +++ b/assets/style/css/form/button.css @@ -0,0 +1,7 @@ +.formkit-wrapper-button { + @apply mb-1; +} + +.formkit-input-button { + @apply bg-primary hover:bg-primary/90 text-white text-sm font-normal py-2 px-5 rounded-lg; +} diff --git a/assets/style/css/form/color.css b/assets/style/css/form/color.css new file mode 100644 index 0000000..7834abd --- /dev/null +++ b/assets/style/css/form/color.css @@ -0,0 +1,18 @@ +.formkit-label-color { + @apply block mb-1 font-bold text-sm; +} + +.formkit-input-color { + @apply w-16 h-10 cursor-pointer rounded-lg mb-2 border-none appearance-none bg-transparent; +} + +.formkit-input-color::-webkit-color-swatch { + border-radius: 5px; + border: none; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); +} +.formkit-input-color::-moz-color-swatch { + border-radius: 5px; + border: none; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); +} diff --git a/assets/style/css/form/dropzone.css b/assets/style/css/form/dropzone.css new file mode 100644 index 0000000..e64f29c --- /dev/null +++ b/assets/style/css/form/dropzone.css @@ -0,0 +1,7 @@ +.formkit-inner-dropzone { + @apply w-full; +} + +.formkit-dropzone { + @apply border-2 border-[rgb(var(--fk-border-color))] border-dashed p-6 active:bg-[rgb(var(--bg-1))]; +} diff --git a/assets/style/css/form/file.css b/assets/style/css/form/file.css new file mode 100644 index 0000000..523e8f7 --- /dev/null +++ b/assets/style/css/form/file.css @@ -0,0 +1,32 @@ +.formkit-label-file { + @apply block mb-1 font-bold text-sm; +} + +.formkit-inner-file { + @apply w-full cursor-pointer; +} + +.formkit-input-file { + @apply w-full cursor-pointer border rounded-lg text-gray-600 text-sm mb-1 file:cursor-pointer file:mr-4 file:py-2 file:px-4 file:rounded-l-lg file:border-0 file:text-sm file:bg-primary file:text-white hover:file:bg-primary/90; + border-color: rgb(var(--fk-border-color)); +} + +.formkit-file-list { + @apply flex flex-col; +} + +.formkit-file-item { + @apply flex items-center py-2 px-4 rounded-lg border border-gray-200 mb-1 mt-1; +} + +.formkit-file-name { + @apply text-[rgb(var(--text-color))] text-sm; +} + +.formkit-file-remove { + @apply ml-auto text-primary text-sm; +} + +.formkit-no-files { + @apply text-[rgb(var(--text-color))] text-sm; +} diff --git a/assets/style/css/form/global.css b/assets/style/css/form/global.css new file mode 100644 index 0000000..73416b1 --- /dev/null +++ b/assets/style/css/form/global.css @@ -0,0 +1,23 @@ +.formkit-label-global { + @apply text-[rgb(var(--text-color))]; +} + +.formkit-outer-global { + @apply mb-4 text-[rgb(var(--text-color))] formkit-disabled:opacity-50; +} + +.formkit-help-global { + @apply text-xs mt-1; +} + +.formkit-messages-global { + @apply list-none p-0 mt-1 mb-0; +} + +.formkit-message-global { + @apply text-red-500 mb-1 text-xs; +} + +.formkit-wrapper-global { + @apply relative; +} diff --git a/assets/style/css/form/otp.css b/assets/style/css/form/otp.css new file mode 100644 index 0000000..31656eb --- /dev/null +++ b/assets/style/css/form/otp.css @@ -0,0 +1,29 @@ +.formkit-label-otp { + @apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger; +} + +.formkit-inner-otp { + @apply flex + items-center + justify-start + align-middle + rounded-lg mb-1 + overflow-hidden; +} + +.formkit-digit-otp { + @apply w-10 h-10 mr-2 + text-center + rounded-lg + border + border-[rgb(var(--fk-border-color))] + text-sm + bg-[rgb(var(--bg-2))] + placeholder-secondary + focus-within:border-primary + focus:outline-none; +} + +.formkit-message-otp { + @apply formkit-invalid:text-red-500 dark:formkit-invalid:text-danger; +} diff --git a/assets/style/css/form/range.css b/assets/style/css/form/range.css new file mode 100644 index 0000000..6ea37d1 --- /dev/null +++ b/assets/style/css/form/range.css @@ -0,0 +1,3 @@ +.formkit-input-range { + @apply appearance-none w-full h-2 p-0 bg-[rgb(var(--bg-1))] rounded-full focus:outline-none focus:ring-0 focus:shadow-none; +} diff --git a/assets/style/css/form/text.css b/assets/style/css/form/text.css new file mode 100644 index 0000000..82ee8c7 --- /dev/null +++ b/assets/style/css/form/text.css @@ -0,0 +1,39 @@ +.formkit-inner-text { + @apply flex + items-center + justify-center + align-middle + w-full + border + border-[rgb(var(--fk-border-color))] + formkit-invalid:border-red-500 + rounded-lg + overflow-hidden + focus-within:border-primary + mb-0; +} + +.formkit-input-text { + @apply w-full + h-10 + px-3 + border-none + text-sm + bg-[rgb(var(--bg-2))] + placeholder-[rgb(var(--fk-placeholder-color))] + focus:outline-none + disabled:bg-[rgb(var(--bg-1))] + disabled:border-[rgb(var(--bg-1))] + disabled:placeholder-[rgb(var(--bg-1))] + read-only:bg-[rgb(var(--bg-1))] + read-only:border-[rgb(var(--bg-1))] + read-only:placeholder-[rgb(var(--bg-1))]; +} + +.formkit-prefix-text { + @apply ml-2; +} + +.formkit-message-text { + @apply formkit-invalid:text-red-500; +} diff --git a/assets/style/css/form/textarea.css b/assets/style/css/form/textarea.css new file mode 100644 index 0000000..edc4481 --- /dev/null +++ b/assets/style/css/form/textarea.css @@ -0,0 +1,8 @@ +.formkit-input-textarea { + @apply block + w-full + px-3 py-2 + placeholder-[rgb(var(--fk-placeholder-color))] + bg-[rgb(var(--bg-2))] + focus:outline-none; +} diff --git a/assets/style/css/tailwind.css b/assets/style/css/tailwind.css new file mode 100644 index 0000000..9005b22 --- /dev/null +++ b/assets/style/css/tailwind.css @@ -0,0 +1,33 @@ +/* Base Import Tailwind CSS */ +@import "tailwindcss/base"; +@import "./base/theme"; + +/* Components Import Tailwind CSS */ +@import "tailwindcss/components"; +@import "./component/common"; +@import "./component/alert"; +@import "./component/badge"; +@import "./component/button"; +@import "./component/card"; +@import "./component/collapse"; +@import "./component/dropdown"; +@import "./component/modal"; +@import "./component/progress"; +@import "./component/tab"; +@import "./component/table"; + +/* Form Import Tailwind CSS */ +@import "./form/global"; +@import "./form/text"; +@import "./form/textarea"; +@import "./form/box"; +@import "./form/button"; +@import "./form/otp"; +@import "./form/color"; +@import "./form/file"; +@import "./form/range"; +@import "./form/dropzone"; + +/* Utilities Import Tailwind CSS */ +@import "tailwindcss/utilities"; +@import "./utility/typography"; diff --git a/assets/style/css/utility/typography.css b/assets/style/css/utility/typography.css new file mode 100644 index 0000000..7c43e9d --- /dev/null +++ b/assets/style/css/utility/typography.css @@ -0,0 +1,29 @@ +h1 { + @apply text-4xl + font-bold; +} + +h2 { + @apply text-3xl + font-bold; +} + +h3 { + @apply text-2xl + font-bold; +} + +h4 { + @apply text-xl + font-bold; +} + +h5 { + @apply text-lg + font-bold; +} + +h6 { + @apply text-base + font-bold; +} diff --git a/assets/style/scss/custom/apps/products.scss b/assets/style/scss/custom/apps/products.scss new file mode 100644 index 0000000..6260088 --- /dev/null +++ b/assets/style/scss/custom/apps/products.scss @@ -0,0 +1,49 @@ +$small: 640px; +$medium: 768px; +$large: 1024px; +$xlarge: 1280px; + +.filter-wrapper { + &.filter-wrapper-show { + display: block; + } + + &.filter-wrapper-hide { + display: none; + } +} + +@media screen and (max-width: $medium) { + .filter-wrapper { + transition: right 0.25s ease; + &.filter-wrapper-show { + display: block; + right: 0px; + } + + &.filter-wrapper-hide { + display: block; + right: -260px; + } + } + + .filter-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: -50; + + &.show { + visibility: visible; + opacity: 1; + } + + &.hide { + visibility: hidden; + opacity: 0; + } + } +} diff --git a/assets/style/scss/custom/layout/horizontal.scss b/assets/style/scss/custom/layout/horizontal.scss new file mode 100644 index 0000000..1e44438 --- /dev/null +++ b/assets/style/scss/custom/layout/horizontal.scss @@ -0,0 +1,18 @@ +.h-layout { + .content-page { + padding: 10.5rem 2rem 0 2rem; + height: 100vh; + } + + .w-header, + .w-header-search { + width: 100%; + } + + .w-header-search { + position: fixed; + display: flex; + align-items: center; + height: 64px; + } +} diff --git a/assets/style/scss/custom/layout/vertical.scss b/assets/style/scss/custom/layout/vertical.scss new file mode 100644 index 0000000..cedeba6 --- /dev/null +++ b/assets/style/scss/custom/layout/vertical.scss @@ -0,0 +1,166 @@ +$small: 640px; +$medium: 768px; +$large: 1024px; +$xlarge: 1280px; + +@media screen and (max-width: $medium) { + body:has(.menu-overlay:not(.hide)) { + overflow: hidden; + } + + .menu-overlay { + position: fixed; + top: 0; + height: 120vh; + z-index: 45; + display: block; + visibility: visible; + opacity: 1; + transition: all 0.5s ease; + background-color: rgba(34, 41, 47, 0.5); + left: 0; + right: 0; + } + + .menu-overlay { + &.hide { + visibility: hidden; + opacity: 0; + } + } +} + +// Layout Vertical CSS +.v-layout { + .content-page { + height: 100%; + } + + .w-header-search { + position: fixed; + display: flex; + align-items: center; + height: 64px; + } + + .w-header, + .w-header-search { + width: calc(100% - 256px); + + @media screen and (max-width: $medium) { + width: 100%; + } + } + + .vertical-menu { + left: 0; + } + + .content-page { + margin-left: 256px; + padding: 6rem 2rem 10px 2rem; + + @media screen and (max-width: $medium) { + margin-left: 0px; + padding: 6rem 1.25rem 0 1.25rem; + } + } + + &.menu-hide { + .w-header { + width: calc(100% - 0px); + } + + .vertical-menu { + left: -260px; + } + + .content-page { + margin-left: 0; + } + } + + .menu-content, + .menu-content-child { + max-height: 1000px; + overflow: hidden; + + &.hide { + max-height: 0px; + } + } + + .menu-content-max, + .menu-content-child-max { + max-height: 100vh; + } + + svg.side-menu-arrow { + transition: 0.25s ease-in-out; + } + + .nav-open { + svg.side-menu-arrow { + transform: rotate(90deg); + } + } +} + +// Custom styles for LZS theme +html[data-theme="LZS"] { + .v-layout { + .active-menu { + position: relative; + transition: all 0.2s ease; + + // Yellow glow on active menu items + &:after { + content: ""; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 4px; + background-color: rgb(var(--active-menu-border)); + } + + // Icon color for active menu + svg { + color: rgb(var(--active-menu-border)); + } + } + + // Form focus states with yellow accent + input:focus, textarea:focus, select:focus { + outline: 2px solid rgba(var(--focus-ring)); + outline-offset: 2px; + } + + // Button hover with yellow accent + .btn-primary:hover, button.bg-primary:hover { + box-shadow: 0 0 0 2px rgba(var(--active-menu-border), 0.3); + } + + // Card headers with subtle yellow accent + .rs-card { + .card-header { + border-bottom: 1px solid rgba(var(--border-color)); + + h2, h3, h4 { + position: relative; + + &:before { + content: ""; + position: absolute; + left: -10px; + top: 50%; + transform: translateY(-50%); + height: 60%; + width: 3px; + background-color: rgb(var(--active-menu-border)); + } + } + } + } + } +} diff --git a/assets/style/scss/custom/library/_dropdown.scss b/assets/style/scss/custom/library/_dropdown.scss new file mode 100644 index 0000000..3458997 --- /dev/null +++ b/assets/style/scss/custom/library/_dropdown.scss @@ -0,0 +1,27 @@ +$small: 640px; +$medium: 768px; +$large: 1024px; +$xlarge: 1280px; + +.s-dropdown { + background-color: white; + padding: 10px 0; + border-radius: 5px; + top: 65px !important; + border: 1px solid rgb(229, 231, 235); + box-shadow: 0px 0px 10px rgba(226, 232, 240, 0.2); + + @media screen and (max-width: $medium) { + left: 0px !important; + width: 100% !important; + margin: 0px 2px !important; + } +} + +.dark { + .s-dropdown { + background-color: #1e293b; + box-shadow: 0px 0px 10px rgba(15, 23, 42, 0.2); + border: 1px solid #182130; + } +} diff --git a/assets/style/scss/custom/library/_floatingvue.scss b/assets/style/scss/custom/library/_floatingvue.scss new file mode 100644 index 0000000..74f6ed4 --- /dev/null +++ b/assets/style/scss/custom/library/_floatingvue.scss @@ -0,0 +1,19 @@ +.v-popper--theme-dropdown .v-popper__inner { + background: rgb(var(--bg-2)) !important; + border: 0px !important; + color: rgb(var(--text-color)) !important; +} + +// Disable arrow +.v-popper__arrow-container { + display: none; +} + +// .v-popper--theme-my-theme .v-popper__inner { +// background: #fff000; +// color: black; +// padding: 24px; +// border-radius: 6px; +// border: 1px solid #ddd; +// box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1); +// } diff --git a/assets/style/scss/custom/library/_formkit.scss b/assets/style/scss/custom/library/_formkit.scss new file mode 100644 index 0000000..3c71f40 --- /dev/null +++ b/assets/style/scss/custom/library/_formkit.scss @@ -0,0 +1,237 @@ +$small: 640px; +$medium: 768px; +$large: 1024px; +$xlarge: 1280px; + +.formkit-form { + .form-wizard { + border-radius: 0.5rem; + } + + &.top-form { + .formkit-actions { + button[type="submit"] { + display: block; + margin-right: 0; + margin-left: auto; + } + } + ul.top-steps { + display: flex; + justify-content: space-evenly; + margin-bottom: 1rem; + gap: 1rem; + + @media screen and (max-width: $medium) { + flex-direction: column; + justify-content: start; + } + + li { + flex-grow: 1; + display: inline-block; + + .progress { + position: relative; + bottom: -8px; + width: 0%; + height: 8px; + border-radius: 10px; + background-color: rgb(var(--color-primary)); + transition: width 0.25s ease-in; + } + + .step--errors { + display: inline-flex; + justify-content: center; + align-items: center; + float: right; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #f23131; + color: #fff; + font-size: 12px; + } + + .counter { + display: inline-block; + } + + &:after { + content: ""; + display: block; + width: 100%; + height: 8px; + border-radius: 10px; + background-color: rgb(var(--bg-1)); + } + + &[data-step-active="true"] { + color: rgb(var(--color-primary)); + font-weight: 600; + + .progress { + width: 100%; + } + } + + &[data-step-active="false"] { + color: rgb(var(--bg-1)); + .progress { + width: 0%; + } + } + + &[data-step-completed="true"] { + color: rgb(var(--color-primary)); + font-weight: 600; + .progress { + width: 100%; + } + } + } + } + } + + &.left-form { + display: grid; + gap: 1rem; + grid-template-columns: 230px auto; + + @media screen and (max-width: $medium) { + grid-template-columns: repeat(1, minmax(0, 1fr)); + gap: 0rem; + } + + .formkit-actions { + grid-column: span 2 / span 2; + + @media screen and (max-width: $medium) { + grid-column: span 1 / span 1; + } + + button[type="submit"] { + float: right; + } + } + + ul.left-steps { + display: block; + li { + margin-bottom: 1rem; + .progress { + position: relative; + bottom: -8px; + width: 0%; + height: 8px; + border-radius: 10px; + background-color: rgb(var(--color-primary)); + transition: width 0.25s ease-in; + } + + .step--errors { + display: inline-flex; + justify-content: center; + align-items: center; + float: right; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #f23131; + color: #fff; + font-size: 12px; + } + + .counter { + display: inline-block; + } + + &:after { + content: ""; + display: block; + width: 100%; + height: 8px; + border-radius: 10px; + background-color: rgb(var(--bg-1)); + } + + &[data-step-active="true"] { + color: rgb(var(--color-primary)); + font-weight: 600; + + .progress { + width: 100%; + } + } + + &[data-step-active="false"] { + color: rgb(var(--bg-1)); + .progress { + width: 0%; + } + } + + &[data-step-completed="true"] { + color: rgb(var(--color-primary)); + font-weight: 600; + .progress { + width: 100%; + } + } + } + } + } +} + +// Custom Formkit Input for VSelect +.formkit-vselect { + margin-bottom: 1rem; + + .vs__dropdown-menu { + background: rgb(var(--bg-2)); + } + + .vs__dropdown-menu li { + background: rgb(var(--bg-1)); + color: rgb(var(--text-color)); + } + + .vs__dropdown-toggle { + min-height: 2.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + color: rgb(var(--color-primary)); + + border-width: 1px; + border-radius: 0.5rem; + margin-bottom: 0px; + border-color: rgb(var(--fk-border-color)); + + .vs__selected { + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + + background-color: rgb(var(--color-primary)); + color: white; + + border: 0; + border-radius: 0.5rem; + + gap: 0.25rem; + margin-right: 0.35rem; + margin-left: 0; + } + } +} + +// Custom Decorator +input[type="checkbox"]:checked.icon-check + .formkit-decorator::before { + content: url('data:image/svg+xml;charset=UTF-8,'); + position: absolute; + top: -1px; +} diff --git a/assets/style/scss/custom/library/_fullcalendar.scss b/assets/style/scss/custom/library/_fullcalendar.scss new file mode 100644 index 0000000..ae7af69 --- /dev/null +++ b/assets/style/scss/custom/library/_fullcalendar.scss @@ -0,0 +1,114 @@ +$small: 640px; +$medium: 768px; +$large: 1024px; +$xlarge: 1280px; + +.fc-theme-standard td, +.fc-theme-standard th { + border-color: rgb(var(--bg-1)) !important; +} + +// Overwrite fullcalendar styles light +.fc { + .fc-scrollgrid { + border: 1px solid rgb(var(--bg-1)) !important; + } + + .fc-toolbar { + @media screen and (max-width: $medium) { + flex-direction: column; + align-items: flex-start !important; + gap: 10px; + } + + .fc-toolbar-chunk { + display: flex; + align-items: center; + } + } + + .fc-button { + padding: 0.75rem 1rem !important; + + &.fc-button-primary { + background-color: transparent !important; + color: rgb(var(--color-primary)); + border-color: rgb(var(--color-primary)) !important; + } + + &.fc-button-active { + background-color: rgb(var(--color-primary)) !important; + } + + &:hover { + background-color: rgb(var(--color-primary)) !important; + color: white !important; + } + + &:focus { + box-shadow: 0 0 0 0.05rem rgba(255, 113, 133, 0.5) !important; + background-color: rgb(var(--color-primary)) !important; + color: white !important; + } + } + + .fc-view-harness { + table { + &.fc-col-header { + td, + th { + border-color: rgb(var(--bg-1)) !important; + } + + .fc-col-header-cell { + padding: 0.5rem; + } + } + } + + .fc-daygrid-day { + &.fc-day-today { + background-color: rgb(var(--bg-1)); + background: rgb(var(--bg-1)); + } + } + + .fc-daygrid-day-top { + color: #6b727f !important; + } + + .fc-more-link { + color: rgb(var(--color-primary)); + } + } + + .fc-daygrid-event-harness { + .fc-daygrid-event { + padding: 0.5rem 1rem; + } + + .fc-h-event { + &.fc-event-start { + border-color: rgb(var(--color-primary)); + background-color: rgb(var(--color-primary)); + } + } + } + .fc-list { + .fc-list-event-dot { + background-color: rgb(var(--color-primary)); + border: 5px solid #f3f4f6; + border-color: rgb(var(--color-primary)) !important; + } + + .fc-list-day-cushion { + background-color: #f3f4f6; + padding: 0.5rem 1rem; + } + } + + .fc-popover-header { + padding: 0.5rem !important; + background: #f3f4f6; + } +} diff --git a/assets/style/scss/custom/library/_nprogress.scss b/assets/style/scss/custom/library/_nprogress.scss new file mode 100644 index 0000000..3a8cd29 --- /dev/null +++ b/assets/style/scss/custom/library/_nprogress.scss @@ -0,0 +1,81 @@ +/* Make clicks pass-through */ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #fb7185; + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 3px; +} + +/* Fancy blur effect */ +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #fb7185, 0 0 5px #fb7185; + opacity: 1; + + -webkit-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +/* Remove these to get rid of the spinner */ +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 15px; + right: 15px; +} + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: #fb7185; + border-left-color: #fb7185; + border-radius: 50%; + + -webkit-animation: nprogress-spinner 400ms linear infinite; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@-webkit-keyframes nprogress-spinner { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} +@keyframes nprogress-spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/assets/style/scss/custom/library/_swiper.scss b/assets/style/scss/custom/library/_swiper.scss new file mode 100644 index 0000000..2c94c55 --- /dev/null +++ b/assets/style/scss/custom/library/_swiper.scss @@ -0,0 +1,18 @@ +.swiper-button-next, +.swiper-button-prev { + color: #4B5563 !important; + padding: 1.5rem; +} + +.swiper-button-next:after, +.swiper-button-prev:after { + font-size: 1.5rem !important; +} + +.swiper-pagination-bullet { + background-color: #8b98a9 !important; +} + +.swiper-pagination-bullet-active { + background: #4B5563 !important; +} diff --git a/assets/style/scss/custom/library/_vuecountryflag.scss b/assets/style/scss/custom/library/_vuecountryflag.scss new file mode 100644 index 0000000..e396551 --- /dev/null +++ b/assets/style/scss/custom/library/_vuecountryflag.scss @@ -0,0 +1,7 @@ +.flag.normal-flag { + margin: 0em -1em -0em -1em !important; + transform: scale(0.38) !important; + -ms-transform: scale(0.38) !important; + -webkit-transform: scale(0.38) !important; + -moz-transform: scale(0.38) !important; +} diff --git a/assets/style/scss/custom/library/_vuetoastification.scss b/assets/style/scss/custom/library/_vuetoastification.scss new file mode 100644 index 0000000..8a40068 --- /dev/null +++ b/assets/style/scss/custom/library/_vuetoastification.scss @@ -0,0 +1,3 @@ +.Vue-Toastification__toast { + padding: 18px 21px; +} diff --git a/assets/style/scss/custom/scrollbar/scrollbar.scss b/assets/style/scss/custom/scrollbar/scrollbar.scss new file mode 100644 index 0000000..664be81 --- /dev/null +++ b/assets/style/scss/custom/scrollbar/scrollbar.scss @@ -0,0 +1,21 @@ +/* width */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +/* Track */ +::-webkit-scrollbar-track { + background-color: transparent; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background-color: rgb(var(--scroll-color)); + border-radius: 6px; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: rgb(var(--scroll-hover-color)); +} diff --git a/assets/style/scss/custom/transition/fade-up.scss b/assets/style/scss/custom/transition/fade-up.scss new file mode 100644 index 0000000..89342c2 --- /dev/null +++ b/assets/style/scss/custom/transition/fade-up.scss @@ -0,0 +1,12 @@ +// Animation for transition fade +.fade-up-enter-active, +.fade-up-leave-active { + transition: transform 0.4s cubic-bezier(0.17, 0.67, 0.84, 0.79), + opacity 0.3s linear; +} + +.fade-up-enter-from, +.fade-up-leave-to { + opacity: 0; + transform: translateY(-10%); +} diff --git a/assets/style/scss/custom/transition/fade.scss b/assets/style/scss/custom/transition/fade.scss new file mode 100644 index 0000000..0f3ba9b --- /dev/null +++ b/assets/style/scss/custom/transition/fade.scss @@ -0,0 +1,10 @@ +// Animation for transition fade +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} diff --git a/assets/style/scss/custom/transition/page.scss b/assets/style/scss/custom/transition/page.scss new file mode 100644 index 0000000..904190a --- /dev/null +++ b/assets/style/scss/custom/transition/page.scss @@ -0,0 +1,11 @@ +// Animation for transition fade +.page-enter-active, +.page-leave-active { + transition: transform 0.3s ease, opacity 0.4s ease; +} + +.page-enter-from, +.page-leave-to { + transform: scale(1.02); + opacity: 0; +} diff --git a/assets/style/scss/custom/transition/slide-fade.scss b/assets/style/scss/custom/transition/slide-fade.scss new file mode 100644 index 0000000..70b6742 --- /dev/null +++ b/assets/style/scss/custom/transition/slide-fade.scss @@ -0,0 +1,17 @@ +/* + Enter and leave animations can use different + durations and timing functions. +*/ +.slide-fade-enter-active { + transition: all 0.3s ease-out; +} + +.slide-fade-leave-active { + transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1); +} + +.slide-fade-enter-from, +.slide-fade-leave-to { + transform: translateX(20px); + opacity: 0; +} diff --git a/assets/style/scss/custom/transition/slide.scss b/assets/style/scss/custom/transition/slide.scss new file mode 100644 index 0000000..6b6bca5 --- /dev/null +++ b/assets/style/scss/custom/transition/slide.scss @@ -0,0 +1,10 @@ +.slide-enter-active, +.slide-leave-active { + transition: translateY, opacity 0.2s ease; +} + +.slide-enter-from, +.slide-leave-to { + transform: translateY(0px); + opacity: 0; +} diff --git a/assets/style/scss/main.scss b/assets/style/scss/main.scss new file mode 100644 index 0000000..39d59d8 --- /dev/null +++ b/assets/style/scss/main.scss @@ -0,0 +1,57 @@ +/*================================================================================ + Notes: This file is the main entry point for the SCSS stylesheet. + ================================================================================*/ + +/* Import DM Sans font from Google Fonts */ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"); + +html, +body { + height: 100%; +} + +#__nuxt { + font-family: "DM Sans", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + letter-spacing: -0.5px; /* Changed from -2px to -0.5px for better readability */ + font-size: 14px; +} + +// svg.icon{ +// width: 1.3rem; +// height: 1.3rem; +// vertical-align: middle; +// fill: currentColor; +// overflow: hidden; +// } + +code { + color: rgb(233, 74, 74); + background-color: rgba(146, 146, 146, 0.1); +} + +// Custom Layout SCSS +@import "./custom/layout/vertical"; +@import "./custom/layout/horizontal"; + +// Transition SCSS +@import "./custom/transition/page"; +@import "./custom/transition/fade"; +@import "./custom/transition/fade-up"; +@import "./custom/transition/slide"; +@import "./custom/transition/slide-fade"; + +// Scrollbar SCSS +@import "./custom/scrollbar/scrollbar"; + +// Custom SCSS library +@import "./custom/library/dropdown"; +@import "./custom/library/nprogress"; +@import "./custom/library/formkit"; +@import "./custom/library/vuetoastification"; +@import "./custom/library/swiper"; +@import "./custom/library/fullcalendar"; +@import "./custom/library/floatingvue"; +@import "./custom/library/vuecountryflag"; diff --git a/components/FontSizeStepper.vue b/components/FontSizeStepper.vue new file mode 100644 index 0000000..86146a0 --- /dev/null +++ b/components/FontSizeStepper.vue @@ -0,0 +1,78 @@ + + + \ No newline at end of file diff --git a/components/Loading.vue b/components/Loading.vue new file mode 100644 index 0000000..1843a70 --- /dev/null +++ b/components/Loading.vue @@ -0,0 +1,102 @@ + + + diff --git a/components/RSCalendar.vue b/components/RSCalendar.vue new file mode 100644 index 0000000..3bf6d40 --- /dev/null +++ b/components/RSCalendar.vue @@ -0,0 +1,243 @@ + + + diff --git a/components/RsAlert.vue b/components/RsAlert.vue new file mode 100644 index 0000000..4cc0dba --- /dev/null +++ b/components/RsAlert.vue @@ -0,0 +1,67 @@ + + + diff --git a/components/RsApiTester.vue b/components/RsApiTester.vue new file mode 100644 index 0000000..4ee9d3b --- /dev/null +++ b/components/RsApiTester.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/components/RsBadge.vue b/components/RsBadge.vue new file mode 100644 index 0000000..c814f94 --- /dev/null +++ b/components/RsBadge.vue @@ -0,0 +1,34 @@ + + + diff --git a/components/RsButton.vue b/components/RsButton.vue new file mode 100644 index 0000000..19beb81 --- /dev/null +++ b/components/RsButton.vue @@ -0,0 +1,58 @@ + + + diff --git a/components/RsCard.vue b/components/RsCard.vue new file mode 100644 index 0000000..d811bbe --- /dev/null +++ b/components/RsCard.vue @@ -0,0 +1,16 @@ + + + diff --git a/components/RsCodeMirror.vue b/components/RsCodeMirror.vue new file mode 100644 index 0000000..70b1516 --- /dev/null +++ b/components/RsCodeMirror.vue @@ -0,0 +1,223 @@ + + + + + diff --git a/components/RsCollapse.vue b/components/RsCollapse.vue new file mode 100644 index 0000000..dd60650 --- /dev/null +++ b/components/RsCollapse.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/RsCollapseItem.vue b/components/RsCollapseItem.vue new file mode 100644 index 0000000..54d24ca --- /dev/null +++ b/components/RsCollapseItem.vue @@ -0,0 +1,94 @@ + + + diff --git a/components/RsDropdown.vue b/components/RsDropdown.vue new file mode 100644 index 0000000..a6d223d --- /dev/null +++ b/components/RsDropdown.vue @@ -0,0 +1,222 @@ + + + diff --git a/components/RsDropdownItem.vue b/components/RsDropdownItem.vue new file mode 100644 index 0000000..6d715ee --- /dev/null +++ b/components/RsDropdownItem.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/RsFieldset.vue b/components/RsFieldset.vue new file mode 100644 index 0000000..dba5bd9 --- /dev/null +++ b/components/RsFieldset.vue @@ -0,0 +1,45 @@ + + + diff --git a/components/RsModal.vue b/components/RsModal.vue new file mode 100644 index 0000000..c3d8471 --- /dev/null +++ b/components/RsModal.vue @@ -0,0 +1,149 @@ + + + diff --git a/components/RsProgressBar.vue b/components/RsProgressBar.vue new file mode 100644 index 0000000..3d7053a --- /dev/null +++ b/components/RsProgressBar.vue @@ -0,0 +1,63 @@ + + + diff --git a/components/RsTab.vue b/components/RsTab.vue new file mode 100644 index 0000000..cf9fc3b --- /dev/null +++ b/components/RsTab.vue @@ -0,0 +1,230 @@ + + + diff --git a/components/RsTabItem.vue b/components/RsTabItem.vue new file mode 100644 index 0000000..a0eec26 --- /dev/null +++ b/components/RsTabItem.vue @@ -0,0 +1,20 @@ + + + diff --git a/components/RsTable.vue b/components/RsTable.vue new file mode 100644 index 0000000..b4116a1 --- /dev/null +++ b/components/RsTable.vue @@ -0,0 +1,805 @@ + + + diff --git a/components/RsWizard.vue b/components/RsWizard.vue new file mode 100644 index 0000000..86ffb1b --- /dev/null +++ b/components/RsWizard.vue @@ -0,0 +1,228 @@ + + + diff --git a/components/VoiceReader.vue b/components/VoiceReader.vue new file mode 100644 index 0000000..8fe3d04 --- /dev/null +++ b/components/VoiceReader.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/components/api-platform/CodeGenerationModal.vue b/components/api-platform/CodeGenerationModal.vue new file mode 100644 index 0000000..9956d70 --- /dev/null +++ b/components/api-platform/CodeGenerationModal.vue @@ -0,0 +1,634 @@ + + + \ No newline at end of file diff --git a/components/api-platform/CollectionsSidebar.vue b/components/api-platform/CollectionsSidebar.vue new file mode 100644 index 0000000..125aac2 --- /dev/null +++ b/components/api-platform/CollectionsSidebar.vue @@ -0,0 +1,318 @@ + + + \ No newline at end of file diff --git a/components/api-platform/CreateCollectionModal.vue b/components/api-platform/CreateCollectionModal.vue new file mode 100644 index 0000000..2f2c11a --- /dev/null +++ b/components/api-platform/CreateCollectionModal.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/components/api-platform/EnvironmentModal.vue b/components/api-platform/EnvironmentModal.vue new file mode 100644 index 0000000..35c5170 --- /dev/null +++ b/components/api-platform/EnvironmentModal.vue @@ -0,0 +1,226 @@ + + + \ No newline at end of file diff --git a/components/api-platform/EnvironmentSelector.vue b/components/api-platform/EnvironmentSelector.vue new file mode 100644 index 0000000..f1d742b --- /dev/null +++ b/components/api-platform/EnvironmentSelector.vue @@ -0,0 +1,61 @@ + + + \ No newline at end of file diff --git a/components/api-platform/ImportExportModal.vue b/components/api-platform/ImportExportModal.vue new file mode 100644 index 0000000..391e751 --- /dev/null +++ b/components/api-platform/ImportExportModal.vue @@ -0,0 +1,669 @@ + + + \ No newline at end of file diff --git a/components/api-platform/RequestBuilder.vue b/components/api-platform/RequestBuilder.vue new file mode 100644 index 0000000..5f5737a --- /dev/null +++ b/components/api-platform/RequestBuilder.vue @@ -0,0 +1,247 @@ + + + \ No newline at end of file diff --git a/components/api-platform/ResponseViewer.vue b/components/api-platform/ResponseViewer.vue new file mode 100644 index 0000000..cedecd6 --- /dev/null +++ b/components/api-platform/ResponseViewer.vue @@ -0,0 +1,937 @@ + + + \ No newline at end of file diff --git a/components/api-platform/SaveRequestModal.vue b/components/api-platform/SaveRequestModal.vue new file mode 100644 index 0000000..b607d61 --- /dev/null +++ b/components/api-platform/SaveRequestModal.vue @@ -0,0 +1,149 @@ + + + \ No newline at end of file diff --git a/components/api-platform/ScalarConfigModal.vue b/components/api-platform/ScalarConfigModal.vue new file mode 100644 index 0000000..26bddec --- /dev/null +++ b/components/api-platform/ScalarConfigModal.vue @@ -0,0 +1,351 @@ + + + \ No newline at end of file diff --git a/components/api-platform/TestScriptsModal.vue b/components/api-platform/TestScriptsModal.vue new file mode 100644 index 0000000..8616f38 --- /dev/null +++ b/components/api-platform/TestScriptsModal.vue @@ -0,0 +1,536 @@ + + + \ No newline at end of file diff --git a/components/api-platform/tabs/AuthTab.vue b/components/api-platform/tabs/AuthTab.vue new file mode 100644 index 0000000..31d7526 --- /dev/null +++ b/components/api-platform/tabs/AuthTab.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/components/api-platform/tabs/BodyTab.vue b/components/api-platform/tabs/BodyTab.vue new file mode 100644 index 0000000..1049ae8 --- /dev/null +++ b/components/api-platform/tabs/BodyTab.vue @@ -0,0 +1,436 @@ + + + \ No newline at end of file diff --git a/components/api-platform/tabs/HeadersTab.vue b/components/api-platform/tabs/HeadersTab.vue new file mode 100644 index 0000000..010ad0c --- /dev/null +++ b/components/api-platform/tabs/HeadersTab.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/components/api-platform/tabs/ParamsTab.vue b/components/api-platform/tabs/ParamsTab.vue new file mode 100644 index 0000000..43611dc --- /dev/null +++ b/components/api-platform/tabs/ParamsTab.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/components/draggable/nested.vue b/components/draggable/nested.vue new file mode 100644 index 0000000..1d0239e --- /dev/null +++ b/components/draggable/nested.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/components/draggable/sideMenuNested.vue b/components/draggable/sideMenuNested.vue new file mode 100644 index 0000000..0596c5b --- /dev/null +++ b/components/draggable/sideMenuNested.vue @@ -0,0 +1,533 @@ + + + diff --git a/components/formkit/DateTimePicker.vue b/components/formkit/DateTimePicker.vue new file mode 100644 index 0000000..6737d41 --- /dev/null +++ b/components/formkit/DateTimePicker.vue @@ -0,0 +1,13 @@ + + + diff --git a/components/formkit/FileDropzone.vue b/components/formkit/FileDropzone.vue new file mode 100644 index 0000000..57743b7 --- /dev/null +++ b/components/formkit/FileDropzone.vue @@ -0,0 +1,139 @@ + + + diff --git a/components/formkit/OneTimePassword.vue b/components/formkit/OneTimePassword.vue new file mode 100644 index 0000000..f141fde --- /dev/null +++ b/components/formkit/OneTimePassword.vue @@ -0,0 +1,83 @@ + + + diff --git a/components/formkit/TextMask.vue b/components/formkit/TextMask.vue new file mode 100644 index 0000000..55a1be8 --- /dev/null +++ b/components/formkit/TextMask.vue @@ -0,0 +1,23 @@ + + + diff --git a/components/formkit/Toggle.vue b/components/formkit/Toggle.vue new file mode 100644 index 0000000..3faee73 --- /dev/null +++ b/components/formkit/Toggle.vue @@ -0,0 +1,36 @@ + + + diff --git a/components/layouts/Breadcrumb.vue b/components/layouts/Breadcrumb.vue new file mode 100644 index 0000000..0d6f142 --- /dev/null +++ b/components/layouts/Breadcrumb.vue @@ -0,0 +1,92 @@ + + + diff --git a/components/layouts/BreadcrumbV2.vue b/components/layouts/BreadcrumbV2.vue new file mode 100644 index 0000000..268a040 --- /dev/null +++ b/components/layouts/BreadcrumbV2.vue @@ -0,0 +1,98 @@ + + + diff --git a/components/layouts/FormHeader.vue b/components/layouts/FormHeader.vue new file mode 100644 index 0000000..9f11cbf --- /dev/null +++ b/components/layouts/FormHeader.vue @@ -0,0 +1,20 @@ + + + diff --git a/components/layouts/Header.vue b/components/layouts/Header.vue new file mode 100644 index 0000000..7d4a379 --- /dev/null +++ b/components/layouts/Header.vue @@ -0,0 +1,328 @@ + + + + + diff --git a/components/layouts/configmenu/index.vue b/components/layouts/configmenu/index.vue new file mode 100644 index 0000000..acc60ec --- /dev/null +++ b/components/layouts/configmenu/index.vue @@ -0,0 +1,241 @@ + + + diff --git a/components/layouts/horizontal/index.vue b/components/layouts/horizontal/index.vue new file mode 100644 index 0000000..4322d41 --- /dev/null +++ b/components/layouts/horizontal/index.vue @@ -0,0 +1,7 @@ + + + + diff --git a/components/layouts/index.vue b/components/layouts/index.vue new file mode 100644 index 0000000..c6e9aa9 --- /dev/null +++ b/components/layouts/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/layouts/sidemenu/Item.vue b/components/layouts/sidemenu/Item.vue new file mode 100644 index 0000000..f155f84 --- /dev/null +++ b/components/layouts/sidemenu/Item.vue @@ -0,0 +1,166 @@ + + + diff --git a/components/layouts/sidemenu/ItemChild.vue b/components/layouts/sidemenu/ItemChild.vue new file mode 100644 index 0000000..a642f36 --- /dev/null +++ b/components/layouts/sidemenu/ItemChild.vue @@ -0,0 +1,145 @@ + + + diff --git a/components/layouts/sidemenu/index.vue b/components/layouts/sidemenu/index.vue new file mode 100644 index 0000000..9ca2f32 --- /dev/null +++ b/components/layouts/sidemenu/index.vue @@ -0,0 +1,87 @@ + + + diff --git a/components/layouts/vertical/index.vue b/components/layouts/vertical/index.vue new file mode 100644 index 0000000..8983836 --- /dev/null +++ b/components/layouts/vertical/index.vue @@ -0,0 +1,41 @@ + + + diff --git a/composables/codemirrorThemes.js b/composables/codemirrorThemes.js new file mode 100644 index 0000000..3cb5ef5 --- /dev/null +++ b/composables/codemirrorThemes.js @@ -0,0 +1,284 @@ +export default function () { + return [ + { + name: "3024-day", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/3024-day.png", + }, + { + name: "3024-night", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/3024-night.png", + }, + { + name: "abcdef", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/abcdef.png", + }, + { + name: "ambiance-mobile", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ambiance-mobile.png", + }, + { + name: "ambiance", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ambiance.png", + }, + { + name: "ayu-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ayu-dark.png", + }, + { + name: "ayu-mirage", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ayu-mirage.png", + }, + { + name: "base16-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/base16-dark.png", + }, + { + name: "base16-light", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/base16-light.png", + }, + { + name: "bespin", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/bespin.png", + }, + { + name: "blackboard", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/blackboard.png", + }, + { + name: "cobalt", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/cobalt.png", + }, + { + name: "colorforth", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/colorforth.png", + }, + { + name: "dracula", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/dracula.png", + }, + { + name: "duotone-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/duotone-dark.png", + }, + { + name: "duotone-light", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/duotone-light.png", + }, + { + name: "eclipse", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/eclipse.png", + }, + { + name: "elegant", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/elegant.png", + }, + { + name: "erlang-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/erlang-dark.png", + }, + { + name: "gruvbox-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/gruvbox-dark.png", + }, + { + name: "hopscotch", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/hopscotch.png", + }, + { + name: "icecoder", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/icecoder.png", + }, + { + name: "idea", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/idea.png", + }, + { + name: "isotope", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/isotope.png", + }, + { + name: "lesser-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/lesser-dark.png", + }, + { + name: "liquibyte", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/liquibyte.png", + }, + { + name: "lucario", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/lucario.png", + }, + { + name: "material", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/material.png", + }, + { + name: "mbo", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/mbo.png", + }, + { + name: "mdn-like", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/mdn-like.png", + }, + { + name: "midnight", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/midnight.png", + }, + { + name: "monokai", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/monokai.png", + }, + { + name: "neat", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/neat.png", + }, + { + name: "neo", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/neo.png", + }, + { + name: "night", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/night.png", + }, + { + name: "oceanic-next", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/oceanic-next.png", + }, + { + name: "panda-syntax", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/panda-syntax.png", + }, + { + name: "paraiso-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/paraiso-dark.png", + }, + { + name: "paraiso-light", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/paraiso-light.png", + }, + { + name: "pastel-on-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/pastel-on-dark.png", + }, + { + name: "railscasts", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/railscasts.png", + }, + { + name: "rubyblue", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/rubyblue.png", + }, + { + name: "seti", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/seti.png", + }, + { + name: "shadowfox", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/shadowfox.png", + }, + { + name: "solarized", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/solarized.png", + }, + { + name: "the-matrix", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/the-matrix.png", + }, + { + name: "tomorrow-night-bright", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/tomorrow-night-bright.png", + }, + { + name: "tomorrow-night-eighties", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/tomorrow-night-eighties.png", + }, + { + name: "ttcn", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ttcn.png", + }, + { + name: "twilight", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/twilight.png", + }, + { + name: "vibrant-ink", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/vibrant-ink.png", + }, + { + name: "xq-dark", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/xq-dark.png", + }, + { + name: "xq-light", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/xq-light.png", + }, + { + name: "yeti", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/yeti.png", + }, + { + name: "yonce", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/yonce.png", + }, + { + name: "zenburn", + image: + "https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/zenburn.png", + }, + ]; +} diff --git a/composables/languageList.js b/composables/languageList.js new file mode 100644 index 0000000..3c14fa7 --- /dev/null +++ b/composables/languageList.js @@ -0,0 +1,20 @@ +export default function () { + return [ + { + name: "English", + value: "en", + flagCode: "GB", + default: true, + }, + { + name: "Malay", + value: "ms", + flagCode: "MY", + }, + { + name: "Chinese", + value: "cn", + flagCode: "CN", + } + ]; +} diff --git a/composables/themeList.js b/composables/themeList.js new file mode 100644 index 0000000..3779de1 --- /dev/null +++ b/composables/themeList.js @@ -0,0 +1,55 @@ +export default function () { + return [ + { + theme: "biasa", + colors: [ + { + name: "primary", + value: "243, 88, 106", + }, + { + name: "secondary", + value: "240, 122, 37", + }, + { + name: "accent", + value: "243, 244, 246", + }, + ], + }, + { + theme: "gelap", + colors: [ + { + name: "primary", + value: "243, 88, 106", + }, + { + name: "secondary", + value: "240, 122, 37", + }, + { + name: "accent", + value: "15, 23, 42", + }, + ], + }, + { + theme: "LZS", + colors: [ + { + name: "primary", + value: "0, 90, 173", // #005AAD - Blue + }, + { + name: "secondary", + value: "141, 199, 61", // #8DC73D - Green + }, + { + name: "accent", + value: "255, 242, 0", // #FFF200 - Yellow + }, + ], + }, + ]; +} diff --git a/composables/themeList2.js b/composables/themeList2.js new file mode 100644 index 0000000..26230d5 --- /dev/null +++ b/composables/themeList2.js @@ -0,0 +1,129 @@ +export default function () { + return [ + { + theme: "biru", + colors: [ + { + name: "primary", + value: "0, 102, 204", // Strong blue + }, + { + name: "secondary", + value: "51, 153, 255", // Lighter blue + }, + { + name: "accent", + value: "255, 204, 0", // Gold + }, + { + name: "background", + value: "240, 248, 255", // Alice blue + }, + { + name: "text", + value: "0, 0, 0", // Black + }, + ], + }, + { + theme: "merah", + colors: [ + { + name: "primary", + value: "204, 0, 0", // Strong red + }, + { + name: "secondary", + value: "255, 102, 102", // Lighter red + }, + { + name: "accent", + value: "255, 255, 153", // Light yellow + }, + { + name: "background", + value: "255, 240, 240", // Very light pink + }, + { + name: "text", + value: "0, 0, 0", // Black + }, + ], + }, + { + theme: "ungu", + colors: [ + { + name: "primary", + value: "75, 0, 130", // Indigo + }, + { + name: "secondary", + value: "138, 43, 226", // Blue violet + }, + { + name: "accent", + value: "255, 215, 0", // Gold + }, + { + name: "background", + value: "240, 248, 255", // Alice blue + }, + { + name: "text", + value: "0, 0, 0", // Black + }, + ], + }, + { + theme: "oren", + colors: [ + { + name: "primary", + value: "255, 103, 0", // Dark orange + }, + { + name: "secondary", + value: "255, 159, 64", // Lighter orange + }, + { + name: "accent", + value: "0, 128, 128", // Teal + }, + { + name: "background", + value: "255, 250, 240", // Floral white + }, + { + name: "text", + value: "0, 0, 0", // Black + }, + ], + }, + { + theme: "LZS", + colors: [ + { + name: "primary", + value: "0, 90, 173", // #005AAD - Blue + }, + { + name: "secondary", + value: "141, 199, 61", // #8DC73D - Green + }, + { + name: "accent", + value: "255, 242, 0", // #FFF200 - Yellow + }, + { + name: "background", + value: "245, 250, 255", // Very light blue background + }, + { + name: "text", + value: "0, 0, 0", // Black + }, + ], + }, + ]; +} diff --git a/composables/useApiPlatform.js b/composables/useApiPlatform.js new file mode 100644 index 0000000..ab89e21 --- /dev/null +++ b/composables/useApiPlatform.js @@ -0,0 +1,136 @@ +// Global reactive state +const globalState = { + // Core reactive state + activeTab: ref('params'), + httpMethod: ref('GET'), + requestUrl: ref(''), + isLoading: ref(false), + requestName: ref(''), + + // UI State + showCollectionSidebar: ref(true), + showSaveRequestModal: ref(false), + selectedEnvironment: ref(''), + + // Notification system + notifications: ref([]), + + // Request data + requestParams: ref([ + { active: true, key: '', value: '', description: '' } + ]), + requestHeaders: ref([ + { active: true, key: '', value: '', description: '' } + ]), + requestAuth: ref({ + type: 'none', + bearer: '', + basic: { username: '', password: '' }, + apiKey: { key: '', value: '', addTo: 'header' }, + oauth2: { + grantType: 'authorization_code', + authUrl: '', + accessTokenUrl: '', + clientId: '', + clientSecret: '', + scope: '', + redirectUri: '', + state: '', + accessToken: '', + refreshToken: '', + tokenType: 'Bearer', + expiresIn: null, + expiresAt: null + } + }), + requestBody: ref({ + type: 'raw', + raw: '', + formData: [{ active: true, key: '', value: '', description: '', type: 'text', file: null }], + urlEncoded: [{ active: true, key: '', value: '', description: '' }] + }), + + // Response data + response: ref({ + status: null, + statusText: '', + headers: {}, + data: null, + time: 0, + size: 0 + }), + responseActiveTab: ref('body'), + + // Collections Management (simplified for development) + collections: ref([]), + + // Environment Management (simplified for development) + environments: ref([]), + + // History Management (start empty) + requestHistory: ref([]) +} + +const httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] + +// Global methods +const showNotification = (message, type = 'info', duration = 3000) => { + const id = Date.now() + const notification = { id, message, type } + globalState.notifications.value.push(notification) + + setTimeout(() => { + const index = globalState.notifications.value.findIndex(n => n.id === id) + if (index > -1) { + globalState.notifications.value.splice(index, 1) + } + }, duration) +} + +const dismissNotification = (id) => { + const index = globalState.notifications.value.findIndex(n => n.id === id) + if (index > -1) { + globalState.notifications.value.splice(index, 1) + } +} + +export const useApiPlatform = () => { + return { + // Core state + activeTab: globalState.activeTab, + httpMethod: globalState.httpMethod, + requestUrl: globalState.requestUrl, + isLoading: globalState.isLoading, + requestName: globalState.requestName, + + // UI State + showCollectionSidebar: globalState.showCollectionSidebar, + showSaveRequestModal: globalState.showSaveRequestModal, + selectedEnvironment: globalState.selectedEnvironment, + + // Request data + requestParams: globalState.requestParams, + requestHeaders: globalState.requestHeaders, + requestAuth: globalState.requestAuth, + requestBody: globalState.requestBody, + + // Response data + response: globalState.response, + responseActiveTab: globalState.responseActiveTab, + + // History and collections + requestHistory: globalState.requestHistory, + collections: globalState.collections, + environments: globalState.environments, + + // Notifications + notifications: globalState.notifications, + + // Static data + httpMethods, + + // Methods + showNotification, + dismissNotification + } +} \ No newline at end of file diff --git a/composables/useApiRequest.js b/composables/useApiRequest.js new file mode 100644 index 0000000..628bc6e --- /dev/null +++ b/composables/useApiRequest.js @@ -0,0 +1,136 @@ +export const useApiRequest = () => { + // MAIN SEND REQUEST FUNCTION - REAL BACKEND INTEGRATION + const sendRequest = async (requestData, callbacks = {}) => { + const { onStart, onSuccess, onError, onComplete } = callbacks + + if (!requestData.url) { + onError?.('Please enter a URL') + return + } + + onStart?.() + + try { + // Prepare request data for backend + const payload = { + url: requestData.url, + method: requestData.method, + headers: requestData.headers, + params: requestData.params, + auth: requestData.auth, + requestBody: requestData.body, + timeout: 30000 + } + + // Make request to our backend proxy + const result = await $fetch('/api/api-platform/send-request', { + method: 'POST', + body: payload + }) + + if (result.statusCode === 200) { + onSuccess?.(result.data) + return result.data + } else { + onError?.(result.message, result.data || null) + return result.data + } + + } catch (error) { + const errorResponse = { + status: 500, + statusText: 'Internal Server Error', + headers: {}, + data: { error: error.message || 'Something went wrong' }, + time: 0, + size: 0 + } + + onError?.('Failed to send request', errorResponse) + return errorResponse + } finally { + onComplete?.() + } + } + + // Utility methods + const addRow = (arrayRef) => { + arrayRef.value.push({ active: true, key: '', value: '', description: '' }) + } + + const removeRow = (arrayRef, index) => { + if (arrayRef.value.length > 1) { + arrayRef.value.splice(index, 1) + } + } + + const formatJson = (obj) => { + return JSON.stringify(obj, null, 2) + } + + const getStatusVariant = (status) => { + if (status >= 200 && status < 300) return 'success' + if (status >= 300 && status < 400) return 'warning' + if (status >= 400) return 'danger' + return 'secondary' + } + + const getMethodVariant = (method) => { + const variants = { + 'GET': 'info', + 'POST': 'success', + 'PUT': 'warning', + 'PATCH': 'secondary', + 'DELETE': 'danger' + } + return variants[method] || 'primary' + } + + // Enhanced JSON utilities with notification integration + const beautifyJson = (content, { showNotification } = {}) => { + try { + const parsed = JSON.parse(content) + const beautified = JSON.stringify(parsed, null, 2) + showNotification?.('JSON beautified successfully', 'success', 2000) + return beautified + } catch (error) { + showNotification?.('Invalid JSON format', 'error') + throw error + } + } + + const minifyJson = (content, { showNotification } = {}) => { + try { + const parsed = JSON.parse(content) + const minified = JSON.stringify(parsed) + showNotification?.('JSON minified successfully', 'success', 2000) + return minified + } catch (error) { + showNotification?.('Invalid JSON format', 'error') + throw error + } + } + + const copyToClipboard = async (content, message = 'Copied to clipboard', { showNotification } = {}) => { + try { + await navigator.clipboard.writeText(content) + showNotification?.(message, 'success', 2000) + return true + } catch (error) { + showNotification?.('Failed to copy to clipboard', 'error') + return false + } + } + + return { + sendRequest, + addRow, + removeRow, + formatJson, + getStatusVariant, + getMethodVariant, + beautifyJson, + minifyJson, + copyToClipboard + } +} \ No newline at end of file diff --git a/composables/useScalarConfig.js b/composables/useScalarConfig.js new file mode 100644 index 0000000..ad9a5bc --- /dev/null +++ b/composables/useScalarConfig.js @@ -0,0 +1,209 @@ +// API Documentation configuration management composable +const apiDocsConfig = ref({ + // Basic Information + title: 'Corrad AF 2024 API Platform', + description: 'Complete API reference for the Corrad AF 2024 API Platform project', + version: '2.0.0', + + // OpenAPI Source Configuration + sourceType: 'default', // 'default', 'collections', 'custom' + openApiUrl: '/openapi.json', // Default main OpenAPI JSON file + + // Theme Configuration + theme: 'light', // light, dark, auto + + // Features Configuration + showSidebar: true, + defaultExpandedTags: true, + showSearch: true, + + // Advanced Configuration + metaData: { + title: 'Corrad AF 2024 API Documentation', + description: 'Complete API reference for all endpoints in the Corrad AF 2024 platform', + favicon: '/favicon.ico' + }, + + // Display Options + groupByTags: true, + showParameters: true, + showRequestBody: true, + showResponses: true, + showExamples: true, + + // Contact Information + contact: { + name: 'API Support', + email: 'support@corradaf.com', + url: 'https://corradaf.com' + } +}); + +// Available source types +const availableSourceTypes = [ + { value: 'default', label: 'Default OpenAPI', description: 'Main OpenAPI specification', url: '/openapi.json' }, + { value: 'collections', label: 'Generated from Collections', description: 'Auto-generated from API collections', url: '/openapi-coll.json' }, + { value: 'custom', label: 'Custom JSON', description: 'Custom edited OpenAPI specification', url: '/openapi-custom.json' } +]; + +// Available theme options +const availableThemes = [ + { value: 'light', label: 'Light', description: 'Light theme' }, + { value: 'dark', label: 'Dark', description: 'Dark theme' }, + { value: 'auto', label: 'Auto', description: 'Follow system preference' } +]; + +// Configuration loaded flag +let configLoaded = false; + +// Load configuration from localStorage +const loadApiDocsConfig = () => { + if (configLoaded) return; // Avoid loading multiple times + + try { + const saved = localStorage.getItem('api-docs-config'); + if (saved) { + const parsed = JSON.parse(saved); + // Merge with defaults to ensure all properties exist + apiDocsConfig.value = { ...apiDocsConfig.value, ...parsed }; + } + configLoaded = true; + } catch (error) { + console.error('Failed to load API docs configuration:', error); + } +}; + +// Save configuration to localStorage +const saveApiDocsConfig = () => { + try { + localStorage.setItem('api-docs-config', JSON.stringify(apiDocsConfig.value)); + return true; + } catch (error) { + console.error('Failed to save API docs configuration:', error); + return false; + } +}; + +// Update specific configuration +const updateApiDocsConfig = (updates) => { + apiDocsConfig.value = { ...apiDocsConfig.value, ...updates }; + + // Update OpenAPI URL based on source type + if (updates.sourceType) { + const sourceConfig = availableSourceTypes.find(s => s.value === updates.sourceType); + if (sourceConfig) { + apiDocsConfig.value.openApiUrl = sourceConfig.url; + } + } + + return saveApiDocsConfig(); +}; + +// Update source type and corresponding URL +const updateSourceType = (sourceType) => { + const sourceConfig = availableSourceTypes.find(s => s.value === sourceType); + if (sourceConfig) { + apiDocsConfig.value.sourceType = sourceType; + apiDocsConfig.value.openApiUrl = sourceConfig.url; + return saveApiDocsConfig(); + } + return false; +}; + +// Reset to defaults +const resetApiDocsConfig = () => { + apiDocsConfig.value = { + title: 'Corrad AF 2024 API Platform', + description: 'Complete API reference for the Corrad AF 2024 API Platform project', + version: '2.0.0', + sourceType: 'default', + openApiUrl: '/openapi.json', + theme: 'light', + showSidebar: true, + defaultExpandedTags: true, + showSearch: true, + metaData: { + title: 'Corrad AF 2024 API Documentation', + description: 'Complete API reference for all endpoints in the Corrad AF 2024 platform', + favicon: '/favicon.ico' + }, + groupByTags: true, + showParameters: true, + showRequestBody: true, + showResponses: true, + showExamples: true, + contact: { + name: 'API Support', + email: 'support@corradaf.com', + url: 'https://corradaf.com' + } + }; + configLoaded = false; + return saveApiDocsConfig(); +}; + +// Generate API docs configuration object +const getApiDocsConfiguration = () => { + // Ensure config is loaded before generating + if (!configLoaded && process.client) { + loadApiDocsConfig(); + } + + const config = apiDocsConfig.value; + + // Ensure the URL is valid and use default if not + let openApiUrl = config.openApiUrl; + if (!openApiUrl || typeof openApiUrl !== 'string') { + console.warn('Invalid OpenAPI URL in config, using default'); + openApiUrl = '/openapi.json'; + } + + // Make sure URL starts with / or http(s) + if (!openApiUrl.startsWith('/') && !openApiUrl.startsWith('http')) { + openApiUrl = `/${openApiUrl}`; + } + + return { + url: openApiUrl, + title: config.title || 'API Documentation', + description: config.description || '', + version: config.version || '1.0.0', + theme: config.theme || 'light', + showSidebar: Boolean(config.showSidebar), + defaultExpandedTags: Boolean(config.defaultExpandedTags), + showSearch: Boolean(config.showSearch), + groupByTags: Boolean(config.groupByTags), + showParameters: Boolean(config.showParameters), + showRequestBody: Boolean(config.showRequestBody), + showResponses: Boolean(config.showResponses), + showExamples: Boolean(config.showExamples), + metaData: { + title: config.metaData?.title || 'API Documentation', + description: config.metaData?.description || 'Complete API reference', + favicon: config.metaData?.favicon || '/favicon.ico' + }, + contact: config.contact || {} + }; +}; + +// Auto-load configuration on client side +if (process.client) { + loadApiDocsConfig(); +} + +export const useScalarConfig = () => { + return { + // State (keeping the same export name for compatibility) + scalarConfig: readonly(apiDocsConfig), + availableThemes, + availableSourceTypes, + + // Methods (keeping the same names for compatibility) + loadScalarConfig: loadApiDocsConfig, + saveScalarConfig: saveApiDocsConfig, + updateScalarConfig: updateApiDocsConfig, + updateSourceType, + resetScalarConfig: resetApiDocsConfig, + getScalarConfiguration: getApiDocsConfiguration + }; +}; \ No newline at end of file diff --git a/composables/useSiteSettings.js b/composables/useSiteSettings.js new file mode 100644 index 0000000..f9fef1e --- /dev/null +++ b/composables/useSiteSettings.js @@ -0,0 +1,363 @@ +export const useSiteSettings = () => { + // Global site settings state + const siteSettings = useState('siteSettings', () => ({ + siteName: 'corradAF', + siteDescription: 'corradAF Base Project', + siteLogo: '', + siteLoginLogo: '', + siteLoadingLogo: '', + siteFavicon: '', + showSiteNameInHeader: true, + siteNameFontSize: 18, + customCSS: '', + selectedTheme: 'biasa', // Use existing theme system + customThemeFile: '', + currentFont: '', + fontSource: '', + // SEO fields + seoTitle: '', + seoDescription: '', + seoKeywords: '', + seoAuthor: '', + seoOgImage: '', + seoTwitterCard: 'summary_large_image', + seoCanonicalUrl: '', + seoRobots: 'index, follow', + seoGoogleAnalytics: '', + seoGoogleTagManager: '', + seoFacebookPixel: '' + })); + + // Loading state + const loading = useState('siteSettingsLoading', () => false); + + // Load site settings from API + const loadSiteSettings = async () => { + loading.value = true; + try { + const response = await $fetch("/api/devtool/config/site-settings", { + method: "GET", + }); + + if (response && response.data) { + siteSettings.value = { ...siteSettings.value, ...response.data }; + applyThemeSettings(); + updateGlobalMeta(); + } + } catch (error) { + console.error("Error loading site settings:", error); + } finally { + loading.value = false; + } + }; + + // Update global meta tags and SEO + const updateGlobalMeta = () => { + if (process.client) { + // Update page title - use SEO title if available + const title = siteSettings.value.seoTitle || siteSettings.value.siteName; + if (title) { + document.title = title; + + // Update meta description - use SEO description if available + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.name = 'description'; + document.head.appendChild(metaDescription); + } + metaDescription.content = siteSettings.value.seoDescription || siteSettings.value.siteDescription || title; + + // Update keywords meta tag + if (siteSettings.value.seoKeywords) { + let keywordsMeta = document.querySelector('meta[name="keywords"]'); + if (!keywordsMeta) { + keywordsMeta = document.createElement('meta'); + keywordsMeta.name = 'keywords'; + document.head.appendChild(keywordsMeta); + } + keywordsMeta.content = siteSettings.value.seoKeywords; + } + + // Update author meta tag + if (siteSettings.value.seoAuthor) { + let authorMeta = document.querySelector('meta[name="author"]'); + if (!authorMeta) { + authorMeta = document.createElement('meta'); + authorMeta.name = 'author'; + document.head.appendChild(authorMeta); + } + authorMeta.content = siteSettings.value.seoAuthor; + } + + // Update robots meta tag + let robotsMeta = document.querySelector('meta[name="robots"]'); + if (!robotsMeta) { + robotsMeta = document.createElement('meta'); + robotsMeta.name = 'robots'; + document.head.appendChild(robotsMeta); + } + robotsMeta.content = siteSettings.value.seoRobots; + + // Update Open Graph tags + let ogTitle = document.querySelector('meta[property="og:title"]'); + if (!ogTitle) { + ogTitle = document.createElement('meta'); + ogTitle.setAttribute('property', 'og:title'); + document.head.appendChild(ogTitle); + } + ogTitle.content = title; + + let ogDescription = document.querySelector('meta[property="og:description"]'); + if (!ogDescription) { + ogDescription = document.createElement('meta'); + ogDescription.setAttribute('property', 'og:description'); + document.head.appendChild(ogDescription); + } + ogDescription.content = siteSettings.value.seoDescription || siteSettings.value.siteDescription || title; + + // Update OG image + if (siteSettings.value.seoOgImage) { + let ogImage = document.querySelector('meta[property="og:image"]'); + if (!ogImage) { + ogImage = document.createElement('meta'); + ogImage.setAttribute('property', 'og:image'); + document.head.appendChild(ogImage); + } + ogImage.content = siteSettings.value.seoOgImage; + } + + // Update Twitter Card tags + let twitterCard = document.querySelector('meta[name="twitter:card"]'); + if (!twitterCard) { + twitterCard = document.createElement('meta'); + twitterCard.name = 'twitter:card'; + document.head.appendChild(twitterCard); + } + twitterCard.content = siteSettings.value.seoTwitterCard; + + let twitterTitle = document.querySelector('meta[name="twitter:title"]'); + if (!twitterTitle) { + twitterTitle = document.createElement('meta'); + twitterTitle.name = 'twitter:title'; + document.head.appendChild(twitterTitle); + } + twitterTitle.content = title; + + let twitterDescription = document.querySelector('meta[name="twitter:description"]'); + if (!twitterDescription) { + twitterDescription = document.createElement('meta'); + twitterDescription.name = 'twitter:description'; + document.head.appendChild(twitterDescription); + } + twitterDescription.content = siteSettings.value.seoDescription || siteSettings.value.siteDescription || title; + + // Update canonical URL + if (siteSettings.value.seoCanonicalUrl) { + let canonicalLink = document.querySelector('link[rel="canonical"]'); + if (!canonicalLink) { + canonicalLink = document.createElement('link'); + canonicalLink.rel = 'canonical'; + document.head.appendChild(canonicalLink); + } + canonicalLink.href = siteSettings.value.seoCanonicalUrl; + } + } + + // Update favicon + if (siteSettings.value.siteFavicon) { + let faviconLink = document.querySelector("link[rel*='icon']"); + if (!faviconLink) { + faviconLink = document.createElement('link'); + faviconLink.rel = 'icon'; + document.head.appendChild(faviconLink); + } + faviconLink.href = siteSettings.value.siteFavicon; + + // Update apple touch icon + let appleTouchIcon = document.querySelector("link[rel='apple-touch-icon']"); + if (!appleTouchIcon) { + appleTouchIcon = document.createElement('link'); + appleTouchIcon.rel = 'apple-touch-icon'; + document.head.appendChild(appleTouchIcon); + } + appleTouchIcon.href = siteSettings.value.siteFavicon; + } + + // Apply analytics scripts + if (siteSettings.value.seoGoogleAnalytics) { + // Add Google Analytics + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${siteSettings.value.seoGoogleAnalytics}`; + document.head.appendChild(script); + + const gtag = document.createElement('script'); + gtag.textContent = ` + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '${siteSettings.value.seoGoogleAnalytics}'); + `; + document.head.appendChild(gtag); + } + + if (siteSettings.value.seoGoogleTagManager) { + // Add Google Tag Manager + const gtmScript = document.createElement('script'); + gtmScript.textContent = ` + (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + })(window,document,'script','dataLayer','${siteSettings.value.seoGoogleTagManager}'); + `; + document.head.appendChild(gtmScript); + } + + if (siteSettings.value.seoFacebookPixel) { + // Add Facebook Pixel + const fbScript = document.createElement('script'); + fbScript.textContent = ` + !function(f,b,e,v,n,t,s) + {if(f.fbq)return;n=f.fbq=function(){n.callMethod? + n.callMethod.apply(n,arguments):n.queue.push(arguments)}; + if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; + n.queue=[];t=b.createElement(e);t.async=!0; + t.src=v;s=b.getElementsByTagName(e)[0]; + s.parentNode.insertBefore(t,s)}(window, document,'script', + 'https://connect.facebook.net/en_US/fbevents.js'); + fbq('init', '${siteSettings.value.seoFacebookPixel}'); + fbq('track', 'PageView'); + `; + document.head.appendChild(fbScript); + } + } + }; + + // Apply theme settings to the document + const applyThemeSettings = () => { + if (process.client) { + // Apply selected theme using existing theme system + if (siteSettings.value.selectedTheme) { + document.documentElement.setAttribute("data-theme", siteSettings.value.selectedTheme); + localStorage.setItem("theme", siteSettings.value.selectedTheme); + } + + // Apply custom theme file if exists (append to theme.css) + if (siteSettings.value.customThemeFile) { + let customThemeElement = document.getElementById('custom-theme-file'); + if (!customThemeElement) { + customThemeElement = document.createElement('link'); + customThemeElement.id = 'custom-theme-file'; + customThemeElement.rel = 'stylesheet'; + customThemeElement.type = 'text/css'; + document.head.appendChild(customThemeElement); + } + customThemeElement.href = siteSettings.value.customThemeFile; + } else { + // Remove custom theme file if it exists + const existingThemeElement = document.getElementById('custom-theme-file'); + if (existingThemeElement) { + existingThemeElement.remove(); + } + } + + // Apply custom CSS + let customStyleElement = document.getElementById('custom-site-styles'); + if (!customStyleElement) { + customStyleElement = document.createElement('style'); + customStyleElement.id = 'custom-site-styles'; + document.head.appendChild(customStyleElement); + } + customStyleElement.textContent = siteSettings.value.customCSS || ''; + } + }; + + // Set theme (integrate with existing theme system) + const setTheme = (theme) => { + siteSettings.value.selectedTheme = theme; + applyThemeSettings(); + // Optionally save to server + updateSiteSettings(siteSettings.value); + }; + + // Get current theme + const getCurrentTheme = () => { + return siteSettings.value.selectedTheme || 'biasa'; + }; + + // Update site settings + const updateSiteSettings = async (newSettings) => { + console.log("[useSiteSettings] updateSiteSettings called with:", JSON.parse(JSON.stringify(newSettings))); + try { + const response = await $fetch("/api/devtool/config/site-settings", { + method: "POST", + body: newSettings, + }); + console.log("[useSiteSettings] API response received:", JSON.parse(JSON.stringify(response))); + + if (response && response.data) { + siteSettings.value = { ...siteSettings.value, ...response.data }; + applyThemeSettings(); + updateGlobalMeta(); + console.log("[useSiteSettings] Returning success from updateSiteSettings."); + return { success: true, data: response.data }; + } + + let errorMessage = "Update operation failed: No data returned from server."; + if (response && typeof response === 'object' && response !== null && 'message' in response) { + errorMessage = response.message; + } else if (response) { + errorMessage = "Update operation failed: Unexpected server response format on success."; + } else if (response === undefined) { + errorMessage = "Update failed: Server returned no content (e.g. 204). Treating as failure as data is expected for settings."; + console.log("[useSiteSettings] Returning failure (204 or undefined response) from updateSiteSettings."); + return { success: false, error: { message: errorMessage, details: response } }; + } + + console.log("[useSiteSettings] Returning failure (general case) from updateSiteSettings:", errorMessage); + return { success: false, error: { message: errorMessage, details: response } }; + } catch (error) { + console.error("[useSiteSettings] Error in updateSiteSettings catch block:", error); + let detailedMessage = "An unexpected error occurred during update."; + if (error.data && error.data.message) { + detailedMessage = error.data.message; + } else if (error.message) { + detailedMessage = error.message; + } + console.log("[useSiteSettings] Returning failure (catch block) from updateSiteSettings:", detailedMessage); + return { success: false, error: { message: detailedMessage, details: error } }; + } + }; + + // Add custom theme to theme.css file + const addCustomThemeToFile = async (themeName, themeCSS) => { + try { + const response = await $fetch("/api/devtool/config/add-custom-theme", { + method: "POST", + body: { + themeName, + themeCSS + } + }); + + return response; + } catch (error) { + console.error("Error adding custom theme:", error); + return { success: false, error }; + } + }; + + return { + siteSettings, + loading: readonly(loading), + loadSiteSettings, + updateSiteSettings, + applyThemeSettings, + updateGlobalMeta, + getCurrentTheme, + setTheme, + addCustomThemeToFile + }; +}; \ No newline at end of file diff --git a/composables/useVariableSubstitution.js b/composables/useVariableSubstitution.js new file mode 100644 index 0000000..473cc60 --- /dev/null +++ b/composables/useVariableSubstitution.js @@ -0,0 +1,159 @@ +export const useVariableSubstitution = () => { + const { environments, selectedEnvironment } = useApiPlatform() + + // Get the currently selected environment + const currentEnvironment = computed(() => { + if (!selectedEnvironment.value) return null + return environments.value.find(env => env.id === selectedEnvironment.value) + }) + + // Create a variables map for quick lookup + const variablesMap = computed(() => { + if (!currentEnvironment.value) return {} + + const map = {} + currentEnvironment.value.variables.forEach(variable => { + if (variable.key && variable.value) { + map[variable.key] = variable.value + } + }) + return map + }) + + // Replace variables in a string ({{variableName}}) + const substituteVariables = (text) => { + if (!text || typeof text !== 'string') return text + if (!currentEnvironment.value) return text + + return text.replace(/\{\{([^}]+)\}\}/g, (match, variableName) => { + const trimmedName = variableName.trim() + return variablesMap.value[trimmedName] || match + }) + } + + // Process an entire request object and substitute all variables + const processRequest = (requestData) => { + if (!currentEnvironment.value) return requestData + + const processed = JSON.parse(JSON.stringify(requestData)) + + // Substitute URL + processed.url = substituteVariables(processed.url) + + // Substitute headers + if (processed.headers && Array.isArray(processed.headers)) { + processed.headers.forEach(header => { + header.key = substituteVariables(header.key) + header.value = substituteVariables(header.value) + }) + } + + // Substitute params + if (processed.params && Array.isArray(processed.params)) { + processed.params.forEach(param => { + param.key = substituteVariables(param.key) + param.value = substituteVariables(param.value) + }) + } + + // Substitute auth values + if (processed.auth) { + if (processed.auth.bearer) { + processed.auth.bearer = substituteVariables(processed.auth.bearer) + } + if (processed.auth.basic) { + processed.auth.basic.username = substituteVariables(processed.auth.basic.username) + processed.auth.basic.password = substituteVariables(processed.auth.basic.password) + } + if (processed.auth.apiKey) { + processed.auth.apiKey.key = substituteVariables(processed.auth.apiKey.key) + processed.auth.apiKey.value = substituteVariables(processed.auth.apiKey.value) + } + if (processed.auth.oauth2) { + processed.auth.oauth2.authUrl = substituteVariables(processed.auth.oauth2.authUrl) + processed.auth.oauth2.accessTokenUrl = substituteVariables(processed.auth.oauth2.accessTokenUrl) + processed.auth.oauth2.clientId = substituteVariables(processed.auth.oauth2.clientId) + processed.auth.oauth2.clientSecret = substituteVariables(processed.auth.oauth2.clientSecret) + processed.auth.oauth2.redirectUri = substituteVariables(processed.auth.oauth2.redirectUri) + } + } + + // Substitute body content + if (processed.requestBody) { + if (processed.requestBody.raw) { + processed.requestBody.raw = substituteVariables(processed.requestBody.raw) + } + if (processed.requestBody.formData && Array.isArray(processed.requestBody.formData)) { + processed.requestBody.formData.forEach(item => { + item.key = substituteVariables(item.key) + item.value = substituteVariables(item.value) + }) + } + if (processed.requestBody.urlEncoded && Array.isArray(processed.requestBody.urlEncoded)) { + processed.requestBody.urlEncoded.forEach(item => { + item.key = substituteVariables(item.key) + item.value = substituteVariables(item.value) + }) + } + } + + return processed + } + + // Find all variables used in a text string + const findVariables = (text) => { + if (!text || typeof text !== 'string') return [] + + const matches = text.match(/\{\{([^}]+)\}\}/g) + if (!matches) return [] + + return matches.map(match => { + const variableName = match.replace(/[{}]/g, '').trim() + return { + name: variableName, + found: !!variablesMap.value[variableName], + value: variablesMap.value[variableName] || null + } + }) + } + + // Get all variables used in the current request + const getRequestVariables = (requestData) => { + const variables = new Set() + + // Check URL + findVariables(requestData.url).forEach(v => variables.add(JSON.stringify(v))) + + // Check headers + if (requestData.headers) { + requestData.headers.forEach(header => { + findVariables(header.key).forEach(v => variables.add(JSON.stringify(v))) + findVariables(header.value).forEach(v => variables.add(JSON.stringify(v))) + }) + } + + // Check params + if (requestData.params) { + requestData.params.forEach(param => { + findVariables(param.key).forEach(v => variables.add(JSON.stringify(v))) + findVariables(param.value).forEach(v => variables.add(JSON.stringify(v))) + }) + } + + // Check body + if (requestData.body?.raw) { + findVariables(requestData.body.raw).forEach(v => variables.add(JSON.stringify(v))) + } + + return Array.from(variables).map(v => JSON.parse(v)) + } + + return { + currentEnvironment, + variablesMap, + substituteVariables, + processRequest, + findVariables, + getRequestVariables + } +} \ No newline at end of file diff --git a/composables/useVoiceReader.js b/composables/useVoiceReader.js new file mode 100644 index 0000000..4778dd1 --- /dev/null +++ b/composables/useVoiceReader.js @@ -0,0 +1,55 @@ +export function useVoiceReader() { + const isReading = ref(false); + const announceElement = ref(null); + let speechSynthesis; + let speechUtterance; + + onMounted(() => { + speechSynthesis = window.speechSynthesis; + speechUtterance = new SpeechSynthesisUtterance(); + + window.addEventListener("keydown", handleKeydown); + }); + + onUnmounted(() => { + if (speechSynthesis) { + speechSynthesis.cancel(); + } + window.removeEventListener("keydown", handleKeydown); + }); + + const toggleReading = () => { + if (!speechSynthesis) return; + + if (isReading.value) { + speechSynthesis.pause(); + isReading.value = false; + announce("Reading paused"); + } else { + const textToRead = document.body.innerText; + speechUtterance.text = textToRead; + speechSynthesis.speak(speechUtterance); + isReading.value = true; + announce("Reading started"); + } + }; + + const handleKeydown = (event) => { + if (event.ctrlKey && event.key === "r") { + event.preventDefault(); + toggleReading(); + } + }; + + const announce = (message) => { + if (announceElement.value) { + announceElement.value.textContent = message; + } + }; + + return { + isReading, + toggleReading, + announceElement, + }; +} diff --git a/docs/API_ACCESS_GUIDE.md b/docs/API_ACCESS_GUIDE.md new file mode 100644 index 0000000..fa53199 --- /dev/null +++ b/docs/API_ACCESS_GUIDE.md @@ -0,0 +1,178 @@ +# API Documentation Access Guide + +This guide explains how to access and use the comprehensive API documentation for the Corrad AF 2024 API Platform. + +## 🌐 Live API Documentation + +### Scalar API Reference (Interactive Documentation) +**URL:** `http://localhost:3000/api-docs` + +- **Interactive Interface**: Modern, user-friendly API explorer +- **Try It Out**: Test API endpoints directly from the documentation +- **Multiple Themes**: Choose from 10+ beautiful themes +- **Real-time Testing**: Send requests and see responses immediately +- **Authentication**: Built-in support for Bearer token authentication + +### OpenAPI Specification (Swagger JSON) +**URL:** `http://localhost:3000/api/openapi` + +- **Dynamic Server URLs**: Automatically detects your current host +- **Complete Specification**: All 79+ endpoints documented +- **Import Ready**: Use with Postman, Insomnia, or other API tools +- **Cache Optimized**: 5-minute cache for better performance + +### Static OpenAPI File +**URL:** `http://localhost:3000/openapi.json` + +- **Static Version**: Original OpenAPI specification file +- **Backup Access**: Alternative if dynamic endpoint is unavailable + +## 📱 Access Methods + +### 1. Browser Access +Open any web browser and navigate to: +``` +http://localhost:3000/api-docs +``` + +### 2. Postman Import +1. Open Postman +2. Click "Import" → "Link" +3. Enter: `http://localhost:3000/api/openapi` +4. Click "Continue" → "Import" + +### 3. Insomnia Import +1. Open Insomnia +2. Click "Create" → "Import From" → "URL" +3. Enter: `http://localhost:3000/api/openapi` +4. Click "Fetch and Import" + +### 4. Swagger UI +Use any Swagger UI instance with the URL: +``` +http://localhost:3000/api/openapi +``` + +### 5. curl Access +Fetch the OpenAPI specification: +```bash +curl http://localhost:3000/api/openapi +``` + +## 🔧 Features + +### Interactive Documentation +- **Modern UI**: Clean, responsive design +- **Code Examples**: Request/response examples for all endpoints +- **Authentication**: Integrated login and token management +- **Real-time Testing**: Send actual requests to the API +- **Multiple Formats**: JSON, form-data, and URL-encoded support + +### API Categories +1. **Authentication** (3 endpoints) + - Login, logout, token validation +2. **Business Logic** (1 endpoint) + - Asnaf profile analysis with AI +3. **API Platform** (3 endpoints) + - HTTP proxy, OAuth2 flows +4. **Metabase Integration** (1 endpoint) + - Analytics token management +5. **Development Tools** (70+ endpoints) + - User, role, menu management + - Database operations + - Content management + - Configuration tools + +### Authentication +Most endpoints require a Bearer token. To authenticate: + +1. Use the `/api/auth/login` endpoint +2. Copy the returned token +3. Add to Authorization header: `Bearer ` + +## 🎨 Customization + +### Theme Options +Access the settings panel in the documentation to choose from: +- Default +- Alternate +- Moon (Dark) +- Purple +- Solarized +- Blue Planet +- Saturn +- Kepler +- Mars +- Deep Space + +### Layout Options +- **Modern**: Three-column layout with sidebar +- **Classic**: Two-column traditional layout + +## 🚀 Quick Start + +1. **Start the Server** + ```bash + npm run dev + # or + yarn dev + ``` + +2. **Access Documentation** + Open: `http://localhost:3000/api-docs` + +3. **Authenticate** + - Use the login endpoint + - Enter your credentials + - Token will be automatically saved + +4. **Explore APIs** + - Browse by category + - Try the "Send Request" button + - View real responses + +## 🔗 URLs Summary + +| Service | URL | Description | +|---------|-----|-------------| +| **Interactive Docs** | `http://localhost:3000/api-docs` | Main API documentation interface | +| **OpenAPI Spec** | `http://localhost:3000/api/openapi` | Dynamic OpenAPI JSON | +| **Static Spec** | `http://localhost:3000/openapi.json` | Static OpenAPI file | +| **Postman Collection** | `./postman_collection.json` | Ready-to-import Postman collection | +| **Preview Mode** | `http://localhost:3000/api-docs?preview=true` | Preview with custom settings | + +## 📋 Postman Collection + +A complete Postman collection is available in the root directory: +- **File**: `postman_collection.json` +- **Endpoints**: All 79+ API endpoints +- **Environment**: Pre-configured variables +- **Authentication**: Automatic token management +- **Examples**: Sample requests and responses + +## 🔧 Troubleshooting + +### White Screen Issue (Fixed) +- Improved CSS styling for proper height management +- Better scrolling behavior +- Enhanced container management + +### Common Issues +1. **Documentation not loading**: Check if server is running on port 3000 +2. **Authentication errors**: Ensure valid token in Authorization header +3. **CORS issues**: Use the built-in proxy for external API calls +4. **Theme not applying**: Clear browser cache and reload + +### Development URLs +Replace `localhost:3000` with your actual server URL when deployed. + +## 📞 Support + +For API support and questions: +- **Email**: support@corradaf.com +- **Documentation**: This interactive documentation +- **Postman Collection**: Available in project root + +--- + +**Note**: This documentation is automatically updated with server information and includes all implemented endpoints from the codebase. \ No newline at end of file diff --git a/docs/API_ENDPOINTS_DOCUMENTATION.md b/docs/API_ENDPOINTS_DOCUMENTATION.md new file mode 100644 index 0000000..1b2b6c5 --- /dev/null +++ b/docs/API_ENDPOINTS_DOCUMENTATION.md @@ -0,0 +1,428 @@ +# Corrad AF 2024 API Platform - Complete API Endpoints Documentation + +This document provides a comprehensive list of all API endpoints available in the Corrad AF 2024 API Platform project. + +## Table of Contents + +1. [Authentication APIs](#authentication-apis) +2. [Business Logic APIs](#business-logic-apis) +3. [API Platform APIs](#api-platform-apis) +4. [Metabase Integration APIs](#metabase-integration-apis) +5. [Development Tools APIs](#development-tools-apis) + - [User Management](#user-management) + - [Role Management](#role-management) + - [Menu Management](#menu-management) + - [ORM & Database Management](#orm--database-management) + - [Configuration Management](#configuration-management) + - [API Management Tools](#api-management-tools) + - [Content Management](#content-management) + - [Lookup Data](#lookup-data) + +--- + +## Authentication APIs + +### POST `/api/auth/login` +**Description:** Authenticate user and receive access/refresh tokens +**Parameters:** +- `username` (string, required): User's username +- `password` (string, required): User's password + +**Response:** +```json +{ + "statusCode": 200, + "message": "Login success", + "data": { + "username": "user@example.com", + "roles": ["admin", "user"] + } +} +``` + +### GET `/api/auth/logout` +**Description:** Logout user and clear authentication cookies + +### GET `/api/auth/validate` +**Description:** Validate current authentication token + +--- + +## Business Logic APIs + +### POST `/api/analyze-asnaf` +**Description:** Analyze Asnaf profile using AI/OpenAI integration to determine eligibility and assistance recommendations +**Parameters:** +- `monthlyIncome` (string): Monthly income amount +- `otherIncome` (string): Other income sources +- `totalIncome` (string): Total income amount +- `occupation` (string): Applicant's occupation +- `maritalStatus` (string): Marital status +- `dependents` (array): List of dependents + +**Response:** +```json +{ + "hadKifayahPercentage": "75%", + "kategoriAsnaf": "Miskin", + "kategoriKeluarga": "Miskin (50-100% HK)", + "cadanganKategori": "Miskin", + "statusKelayakan": "Layak (Miskin)", + "cadanganBantuan": [ + {"nama": "Bantuan Kewangan Bulanan", "peratusan": "90%"}, + {"nama": "Bantuan Makanan Asas", "peratusan": "75%"} + ], + "ramalanJangkaMasaPulih": "6 bulan", + "rumusan": "Pemohon memerlukan perhatian segera." +} +``` + +--- + +## API Platform APIs + +### POST `/api/api-platform/send-request` +**Description:** Proxy HTTP requests through the platform with authentication and request/response handling +**Parameters:** +- `url` (string, required): Target URL to send request to +- `method` (string): HTTP method (GET, POST, PUT, DELETE, etc.) +- `headers` (array): Request headers +- `params` (array): Query parameters +- `auth` (object): Authentication configuration +- `requestBody` (object): Request body configuration +- `timeout` (number): Request timeout in milliseconds + +### POST `/api/api-platform/oauth2/client-credentials` +**Description:** Obtain OAuth2 access token using client credentials flow +**Parameters:** +- `client_id` (string, required): OAuth2 client ID +- `client_secret` (string, required): OAuth2 client secret +- `grant_type` (string, required): Grant type (client_credentials) +- `scope` (string): Requested scopes + +### POST `/api/api-platform/oauth2/exchange-code` +**Description:** Exchange authorization code for access token in OAuth2 flow +**Parameters:** +- `code` (string, required): Authorization code +- `client_id` (string, required): OAuth2 client ID +- `client_secret` (string, required): OAuth2 client secret +- `redirect_uri` (string, required): Redirect URI + +--- + +## Metabase Integration APIs + +### GET `/api/metabase/token` +**Description:** Get authentication token for Metabase integration + +--- + +## Development Tools APIs + +### User Management + +#### GET `/api/devtool/user/list` +**Description:** Get list of all users (excluding deleted) + +#### POST `/api/devtool/user/add` +**Description:** Create a new user +**Parameters:** +- `userUsername` (string, required): Username +- `userFullName` (string, required): Full name +- `userEmail` (string, required): Email address +- `userPhone` (string): Phone number +- `userPassword` (string, required): Password +- `roles` (array): Assigned roles + +#### PUT `/api/devtool/user/edit` +**Description:** Update existing user +**Parameters:** +- `userID` (string, required): User ID +- `userFullName` (string): Updated full name +- `userEmail` (string): Updated email +- `userPhone` (string): Updated phone +- `roles` (array): Updated roles + +#### DELETE `/api/devtool/user/delete` +**Description:** Soft delete user (mark as deleted) +**Parameters:** +- `userID` (string, required): User ID to delete + +### Role Management + +#### GET `/api/devtool/role/list` +**Description:** Get list of all roles + +#### POST `/api/devtool/role/add` +**Description:** Create a new role +**Parameters:** +- `roleName` (string, required): Role name +- `roleDescription` (string): Role description + +#### PUT `/api/devtool/role/edit` +**Description:** Update existing role +**Parameters:** +- `roleID` (string, required): Role ID +- `roleName` (string): Updated role name +- `roleDescription` (string): Updated description + +#### DELETE `/api/devtool/role/delete` +**Description:** Delete role +**Parameters:** +- `roleID` (string, required): Role ID to delete + +### Menu Management + +#### GET `/api/devtool/menu/user-list` +**Description:** Get menu items for users + +#### GET `/api/devtool/menu/role-list` +**Description:** Get menu items for roles + +#### POST `/api/devtool/menu/add` +**Description:** Add new menu item +**Parameters:** +- `title` (string, required): Menu item title +- `path` (string, required): Menu item path +- `icon` (string): Icon name +- `parent` (string): Parent menu ID +- `order` (number): Display order + +#### PUT `/api/devtool/menu/edit` +**Description:** Edit existing menu item +**Parameters:** +- `id` (string, required): Menu item ID +- `title` (string): Updated title +- `path` (string): Updated path +- `icon` (string): Updated icon + +#### DELETE `/api/devtool/menu/delete` +**Description:** Delete menu item +**Parameters:** +- `id` (string, required): Menu item ID + +#### POST `/api/devtool/menu/overwrite-navigation` +**Description:** Overwrite entire navigation structure +**Parameters:** +- `navigation` (array, required): New navigation structure + +#### POST `/api/devtool/menu/new-add` +**Description:** New add menu functionality + +### ORM & Database Management + +#### GET `/api/devtool/orm/schema` +**Description:** Get database schema information + +#### GET `/api/devtool/orm/studio` +**Description:** Access ORM studio interface + +#### GET `/api/devtool/orm/data/get` +**Description:** Get data from specific table +**Query Parameters:** +- `table` (string, required): Table name +- `limit` (number): Number of records to return + +#### GET `/api/devtool/orm/table/config` +**Description:** Get table configuration settings + +#### POST `/api/devtool/orm/table/create` +**Description:** Create new database table +**Parameters:** +- `tableName` (string, required): New table name +- `columns` (array, required): Column definitions + +#### GET `/api/devtool/orm/table/modify/get` +**Description:** Get table structure for modification +**Query Parameters:** +- `table` (string, required): Table name + +#### POST `/api/devtool/orm/table/modify` +**Description:** Modify existing table structure +**Parameters:** +- `tableName` (string, required): Table to modify +- `modifications` (object, required): Modification instructions + +#### DELETE `/api/devtool/orm/table/delete/{table}` +**Description:** Delete table by name (dynamic route) +**Path Parameters:** +- `table` (string, required): Table name to delete + +### Configuration Management + +#### GET `/api/devtool/config/site-settings` +**Description:** Get/manage site settings + +#### GET `/api/devtool/config/env` +**Description:** Get environment configuration + +#### POST `/api/devtool/config/upload-file` +**Description:** Upload file to server +**Body:** multipart/form-data +- `file` (file, required): File to upload +- `destination` (string): Upload destination path + +#### GET `/api/devtool/config/loading-logo` +**Description:** Get/set loading logo configuration + +#### POST `/api/devtool/config/add-custom-theme` +**Description:** Add custom theme configuration +**Parameters:** +- `themeName` (string, required): Theme name +- `colors` (object): Color configuration +- `fonts` (object): Font configuration + +### API Management Tools + +#### GET `/api/devtool/api/list` +**Description:** List all available APIs + +#### POST `/api/devtool/api/save` +**Description:** Save API configuration +**Parameters:** +- `apiName` (string, required): API name +- `endpoint` (string, required): API endpoint +- `method` (string, required): HTTP method +- `description` (string): API description +- `parameters` (array): API parameters +- `responses` (object): Response definitions + +#### POST `/api/devtool/api/linter` +**Description:** Lint API code for errors and best practices +**Parameters:** +- `code` (string, required): Code to lint +- `language` (string, required): Programming language + +#### POST `/api/devtool/api/prettier-format` +**Description:** Format code using Prettier +**Parameters:** +- `code` (string, required): Code to format +- `language` (string, required): Programming language + +#### GET `/api/devtool/api/file-code` +**Description:** Get source code of API file +**Query Parameters:** +- `file` (string, required): File path + +### Content Management + +#### Template Management + +##### GET `/api/devtool/content/template/get-list` +**Description:** Get list of available templates + +##### GET `/api/devtool/content/template/list` +**Description:** List all templates + +##### POST `/api/devtool/content/template/import` +**Description:** Import template +**Parameters:** +- `templateName` (string, required): Template name +- `templateContent` (string, required): Template content +- `templateType` (string, required): Template type + +##### GET `/api/devtool/content/template/tag` +**Description:** Get template tags + +#### Code Management + +##### GET `/api/devtool/content/code/file-code` +**Description:** Get source code of file +**Query Parameters:** +- `file` (string, required): File path + +##### POST `/api/devtool/content/code/save` +**Description:** Save code to file +**Parameters:** +- `file` (string, required): File path +- `content` (string, required): File content + +##### POST `/api/devtool/content/code/linter` +**Description:** Lint code for errors +**Parameters:** +- `code` (string, required): Code to lint +- `language` (string, required): Programming language + +##### POST `/api/devtool/content/code/prettier-format` +**Description:** Format code using Prettier +**Parameters:** +- `code` (string, required): Code to format +- `language` (string, required): Programming language + +#### Canvas Management + +##### GET `/api/devtool/content/canvas/file-code` +**Description:** Get canvas file code +**Query Parameters:** +- `canvas` (string, required): Canvas identifier + +### Lookup Data + +#### GET `/api/devtool/lookup/list` +**Description:** Get lookup data list + +--- + +## Base URL Configuration + +- **Development:** `http://localhost:3000` +- **API Base Path:** `/api` + +## Authentication + +Most endpoints require authentication via Bearer token obtained from the `/api/auth/login` endpoint. The token should be included in the Authorization header: + +``` +Authorization: Bearer +``` + +## Response Format + +All APIs follow a consistent response format: + +```json +{ + "statusCode": 200, + "message": "Success message", + "data": { + // Response data + } +} +``` + +## Error Handling + +Error responses follow the same format with appropriate HTTP status codes: + +```json +{ + "statusCode": 400, + "message": "Error message", + "errors": { + // Validation errors if applicable + } +} +``` + +## Import Instructions + +To import the Postman collection: + +1. Open Postman +2. Click "Import" button +3. Select "Upload Files" tab +4. Choose the `postman_collection.json` file +5. Click "Import" + +The collection includes: +- Pre-configured environment variables +- Automatic token management +- Request examples with sample data +- Organized folder structure for easy navigation + +## Notes + +- All development tool APIs are intended for development and administrative purposes +- The API Platform provides proxy functionality for external API calls +- Business logic APIs integrate with external services like OpenAI +- Database operations through ORM tools should be used with caution in production environments \ No newline at end of file diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..9e547c9 --- /dev/null +++ b/docs/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,198 @@ +# Implementation Summary: Enhanced OpenAPI Configuration + +This document summarizes the changes made to implement the requested OpenAPI configuration enhancements. + +## Requirements Fulfilled + +### 1. ✅ Change Dynamic and Static Routing to Markdown + +**Original Request**: Change the dynamic and static routing to see the openapi.json to docs\openapi.md (rename the file too) + +**Implementation**: +- **Moved**: `public/openapi.json` → `docs/openapi.md` +- **Updated**: `server/api/openapi.get.js` to serve markdown instead of JSON +- **Modified**: `composables/useScalarConfig.js` to point to markdown file +- **Enhanced**: Scalar viewer to parse JSON from markdown code blocks + +**Files Changed**: +- `docs/openapi.md` (new markdown file) +- `server/api/openapi.get.js` (updated to serve markdown) +- `composables/useScalarConfig.js` (updated default URL) +- `pages/api-docs/index.vue` (enhanced JSON extraction from markdown) + +### 2. ✅ Dynamic OpenAPI Generation from Collections + +**Original Request**: Make a dynamic and static swagger json based on saved collection api in api platform + +**Implementation**: +- **Created**: `server/api/openapi/generate-from-collections.post.js` - API endpoint for generation +- **Added**: Collection-to-OpenAPI conversion logic with schema inference +- **Implemented**: Automatic tagging, parameter extraction, and response mapping +- **Features**: Support for all HTTP methods, request bodies, query params, headers + +**Generation Features**: +- Collection names → API tags +- Request names → Operation summaries +- URL parsing → Path extraction +- JSON bodies → Schema inference +- Auth settings → Security configuration +- Query params → OpenAPI parameters +- Headers → OpenAPI parameters (excluding standard ones) + +### 3. ✅ Enhanced Configuration Interface + +**Original Request**: User can go to docs config, beside openapi json url, put button to pull and auto import the dynamic and static swagger json based on saved collection to code editor + +**Implementation**: +- **Enhanced**: `components/api-platform/ScalarConfigModal.vue` with three source options: + 1. **OpenAPI URL** - Traditional URL-based loading + 2. **Generate from Collections** - Auto-generation with one-click + 3. **Custom JSON** - Manual editing with validation + +**Interface Features**: +- Radio button selection for source type +- Collections display with request counts +- Generate button with collection validation +- Code editor with JSON validation +- Import/Export functionality +- Real-time validation feedback + +### 4. ✅ Code Editor with Validation + +**Original Request**: The user can edit the file and save. To be safe, add option for open api url, or custom meaning the user just copy paste the json to code editor + +**Implementation**: +- **Added**: Built-in JSON editor with syntax highlighting +- **Implemented**: Real-time validation with error reporting +- **Created**: File import functionality (JSON/YAML support) +- **Added**: Save functionality with server-side file management + +**Editor Features**: +- Live JSON validation +- Error highlighting and reporting +- File import/export +- Auto-formatting +- Save to server with filename validation + +## New API Endpoints + +### 1. Generate from Collections +``` +POST /api/openapi/generate-from-collections +``` +Converts saved API collections to OpenAPI 3.0.3 specification + +### 2. Save OpenAPI Files +``` +POST /api/docs/save-openapi +``` +Saves generated/edited specifications to docs directory + +### 3. Serve Documentation Files +``` +GET /docs/[...slug] +``` +Dynamic route for serving markdown and JSON files from docs directory + +## File Structure Changes + +``` +docs/ +├── openapi.md # Main OpenAPI specification (markdown) +├── openapi-collections.md # Generated from collections +├── openapi-custom.md # Custom edited specification +└── OPENAPI_CONFIGURATION_GUIDE.md # Usage documentation + +server/api/ +├── openapi.get.js # Serves markdown (updated) +├── openapi/ +│ └── generate-from-collections.post.js # Collection generation +├── docs/ +│ ├── save-openapi.post.js # File saving +│ └── [...slug].get.js # File serving + +components/api-platform/ +└── ScalarConfigModal.vue # Enhanced configuration UI + +pages/api-docs/ +└── index.vue # Enhanced Scalar initialization +``` + +## Technical Features + +### Schema Inference Engine +- Automatic type detection from JSON +- Nested object/array handling +- Required field detection +- Example value preservation + +### Validation System +- Real-time JSON validation +- OpenAPI structure validation +- Error reporting with line numbers +- Safe filename handling + +### File Management +- Server-side file operations +- Dynamic file serving +- Content-type detection +- Cache headers for performance + +### Security Measures +- Path traversal prevention +- Filename sanitization +- Input validation +- Error handling + +## Usage Workflow + +1. **Access Configuration**: Go to API Platform → Settings +2. **Choose Source**: Select from URL, Collections, or Custom +3. **Generate/Edit**: Use generation or manual editing +4. **Validate**: Real-time validation feedback +5. **Save**: Persist changes to server +6. **Preview**: Test in documentation viewer + +## Backward Compatibility + +- Existing URL-based configurations continue to work +- Default behavior preserved for existing users +- Graceful fallbacks for missing collections +- Error handling for invalid configurations + +## Benefits + +### For Developers +- Automated documentation generation +- Reduced manual maintenance +- Real-time validation feedback +- Multiple input methods + +### For API Consumers +- Always up-to-date documentation +- Consistent specification format +- Multiple access methods +- Rich interactive interface + +### For Organizations +- Centralized documentation management +- Automated workflow integration +- Version control friendly +- Standard compliance (OpenAPI 3.0.3) + +## Future Enhancements + +### Planned Features +- YAML support in editor +- Version history tracking +- Collaborative editing +- Custom schema templates +- Automated testing integration + +### Integration Opportunities +- CI/CD pipeline integration +- Git hooks for auto-generation +- API gateway synchronization +- Monitoring and analytics + +This implementation provides a comprehensive solution for OpenAPI specification management, balancing automation with customization while maintaining ease of use and standard compliance. \ No newline at end of file diff --git a/docs/OPENAPI_CONFIGURATION_GUIDE.md b/docs/OPENAPI_CONFIGURATION_GUIDE.md new file mode 100644 index 0000000..140e6c9 --- /dev/null +++ b/docs/OPENAPI_CONFIGURATION_GUIDE.md @@ -0,0 +1,250 @@ +# OpenAPI Configuration Guide + +This guide explains how to configure and use the enhanced OpenAPI documentation features in the Corrad AF 2024 API Platform. + +## Overview + +The platform now supports three methods for providing OpenAPI specifications: + +1. **OpenAPI URL** - Traditional URL-based specification loading +2. **Generate from Collections** - Auto-generate from saved API collections +3. **Custom JSON** - Manual specification editing with validation + +## Access Configuration + +Navigate to your API platform and open the **Scalar Configuration Modal** to access these features: + +1. Go to `/api-platform` +2. Look for the "API Documentation Settings" or configuration icon +3. Click on the **Basic Info** tab + +## Configuration Options + +### 1. OpenAPI URL (Default) + +Use this option when you have an existing OpenAPI specification available via URL. + +**Features:** +- Support for both JSON and Markdown files +- Dynamic server URL detection +- Proxy support for CORS issues + +**Usage:** +1. Select "OpenAPI URL" radio button +2. Enter your specification URL in the "OpenAPI Specification URL" field +3. Optionally configure a proxy URL if needed + +### 2. Generate from Collections + +Automatically generate OpenAPI specifications from your saved API collections. + +**Features:** +- Auto-detection of endpoints from collections +- Schema inference from request/response bodies +- Automatic tagging by collection names +- Parameter extraction from queries and headers + +**Usage:** +1. Select "Generate from Collections" radio button +2. Review available collections displayed +3. Click "Generate" button +4. The system will: + - Create an OpenAPI 3.0.3 specification + - Generate markdown documentation + - Save to `/docs/openapi-collections.md` + - Switch to custom editor for review + +**Generated Features:** +- Collection names become API tags +- Request names become operation summaries +- Query parameters and headers become OpenAPI parameters +- Request bodies are analyzed for schema generation +- Basic response schemas (success/error) are included + +### 3. Custom JSON + +Manually create or edit OpenAPI specifications with real-time validation. + +**Features:** +- Live JSON validation +- File import support (JSON/YAML) +- Syntax highlighting +- Error reporting +- Auto-save functionality + +**Usage:** +1. Select "Custom JSON" radio button +2. Either: + - Paste JSON directly into the editor + - Click "Import" to upload a file + - Use "Generate" from collections first, then edit +3. The editor provides: + - Real-time validation feedback + - Error highlighting + - Valid JSON confirmation +4. Click "Save Custom JSON" to save changes + +## File Management + +### Generated Files + +When using collection generation or custom JSON, files are saved to: + +- **Collections**: `/docs/openapi-collections.md` +- **Custom**: `/docs/openapi-custom.md` +- **Original**: `/docs/openapi.md` + +### File Access + +All generated files are accessible via: + +- **Web Interface**: `/api-docs` (interactive documentation) +- **Direct Access**: `/docs/[filename]` (raw file content) +- **API Endpoint**: `/api/openapi` (dynamic markdown) + +## API Endpoints + +### Generate from Collections +``` +POST /api/openapi/generate-from-collections +``` + +**Request Body:** +```json +{ + "collections": [/* array of collections */], + "config": { + "title": "API Title", + "description": "API Description", + "version": "1.0.0", + "contact": { + "name": "Support", + "email": "support@example.com" + } + } +} +``` + +### Save OpenAPI File +``` +POST /api/docs/save-openapi +``` + +**Request Body:** +```json +{ + "content": "file content", + "filename": "openapi.md" +} +``` + +### Serve Documentation Files +``` +GET /docs/[filename] +``` + +## Schema Generation + +When generating from collections, the system automatically: + +### 1. Infers Request Schemas +- Parses JSON request bodies +- Creates type-appropriate schemas +- Handles nested objects and arrays +- Provides example values + +### 2. Creates Parameters +- Query parameters from URL params +- Header parameters (excluding standard headers) +- Path parameters from URL structure + +### 3. Generates Responses +- Standard success/error response schemas +- Content-type detection +- Status code mapping + +### 4. Security Configuration +- Automatic bearer token detection +- Auth method mapping from collection settings + +## Best Practices + +### Collection Organization +1. **Naming**: Use descriptive collection names (become API tags) +2. **Grouping**: Group related endpoints in same collection +3. **Documentation**: Add descriptions to requests for better summaries + +### Request Configuration +1. **URLs**: Use full URLs with proper base paths +2. **Parameters**: Configure active parameters with descriptions +3. **Bodies**: Use valid JSON for better schema inference +4. **Headers**: Include necessary headers, exclude standard ones + +### Custom Editing +1. **Validation**: Always validate before saving +2. **Backup**: Export configurations before major changes +3. **Testing**: Use preview mode to test changes +4. **Structure**: Follow OpenAPI 3.0.3 specification + +## Troubleshooting + +### Common Issues + +**Generation Fails** +- Ensure collections contain valid requests +- Check request URLs are properly formatted +- Verify JSON bodies are valid + +**Validation Errors** +- Missing required OpenAPI fields (`openapi`, `info`) +- Invalid JSON syntax +- Incorrect schema structure + +**Display Issues** +- Check markdown JSON extraction +- Verify Scalar initialization +- Review browser console for errors + +### Debug Mode + +Enable debug logging by checking browser console when: +- Configuration loading fails +- Generation produces unexpected results +- Scalar fails to initialize + +## Migration Guide + +### From Static OpenAPI +1. Access configuration modal +2. Select "Custom JSON" +3. Import existing specification +4. Save as new file +5. Update configuration + +### From External URLs +1. Download current specification +2. Use "Custom JSON" import feature +3. Edit as needed +4. Save locally for better control + +## Integration Examples + +### Postman Integration +1. Generate or configure OpenAPI spec +2. Access via `/docs/openapi-collections.md` +3. Extract JSON from markdown +4. Import into Postman + +### Insomnia Integration +1. Use same process as Postman +2. Import OpenAPI JSON directly +3. All endpoints and schemas preserved + +### Development Workflow +1. Create API collections during development +2. Generate OpenAPI specification automatically +3. Review and edit as needed +4. Deploy documentation +5. Keep collections updated for regeneration + +This enhanced system provides flexibility while maintaining automation, allowing both rapid prototyping and detailed customization of API documentation. \ No newline at end of file diff --git a/docs/PHASE-3-IMPLEMENTATION-SUMMARY.md b/docs/PHASE-3-IMPLEMENTATION-SUMMARY.md new file mode 100644 index 0000000..35f92c5 --- /dev/null +++ b/docs/PHASE-3-IMPLEMENTATION-SUMMARY.md @@ -0,0 +1,191 @@ +# Phase 3: Collections & Environment Management - Implementation Summary + +## ✅ Completed Features + +### 1. Collections Sidebar with Tree View +- **File**: `components/api-platform/CollectionsSidebar.vue` +- **Features**: + - Collapsible collections with tree structure + - Request management within collections + - Quick actions: Edit, Delete, Add Request + - Recent requests section (last 5) + - Responsive design with mobile support + +### 2. Collection Management +- **File**: `components/api-platform/CreateCollectionModal.vue` +- **Features**: + - Create new collections with name and description + - Modal-based interface + - Form validation + +### 3. Save/Load Request Functionality +- **File**: `components/api-platform/SaveRequestModal.vue` +- **Features**: + - Save current request to any collection + - Auto-populate request name + - Create collections on-the-fly + - Complete request data preservation (params, headers, auth, body) + +### 4. Environment Management +- **File**: `components/api-platform/EnvironmentSelector.vue` +- **File**: `components/api-platform/EnvironmentModal.vue` +- **Features**: + - Environment dropdown selector + - Full environment CRUD operations + - Variable management per environment + - Visual indicators for variable count + +### 5. Variable Substitution System +- **File**: `composables/useVariableSubstitution.js` +- **Features**: + - Template syntax: `{{variableName}}` + - Complete request processing (URL, headers, params, auth, body) + - Real-time variable detection and validation + - Visual indicators in UI + +### 6. Persistence Layer +- **Storage**: localStorage +- **Features**: + - Automatic saving of collections and environments + - Persistent data across browser sessions + - JSON-based storage format + +## 🎯 User Interface Enhancements + +### Main Layout Updates +- **File**: `pages/api-platform/index.vue` +- Added collections sidebar toggle +- Environment selector in top bar +- Save request button +- Responsive layout adjustments + +### Request Builder Enhancements +- **File**: `components/api-platform/RequestBuilder.vue` +- Variable detection in URL field +- Variable preview panel +- Current environment indicator +- Integrated variable substitution + +### Global State Management +- **File**: `composables/useApiPlatform.js` +- Added UI state management +- Collections and environments state +- Modal management + +## 🔧 Technical Implementation + +### Variable Substitution Flow +1. User enters `{{variableName}}` in any field +2. System detects variables and shows indicators +3. On request send, variables are substituted with environment values +4. Visual feedback shows which variables are resolved + +### Data Structure + +#### Collections +```javascript +{ + id: timestamp, + name: "Collection Name", + description: "Optional description", + requests: [ + { + id: timestamp, + name: "Request Name", + method: "GET", + url: "{{baseUrl}}/api/endpoint", + params: [...], + headers: [...], + auth: {...}, + body: {...}, + createdAt: "ISO string" + } + ], + createdAt: "ISO string" +} +``` + +#### Environments +```javascript +{ + id: timestamp, + name: "Environment Name", + variables: [ + { + key: "baseUrl", + value: "https://api.example.com" + } + ] +} +``` + +## 🚀 Usage Examples + +### 1. Creating an Environment +1. Click environment settings icon +2. Create new environment (e.g., "Production") +3. Add variables: + - `baseUrl`: `https://api.production.com` + - `apiKey`: `prod-key-123` + +### 2. Using Variables in Requests +- URL: `{{baseUrl}}/users` +- Headers: `Authorization: Bearer {{apiKey}}` +- System automatically substitutes values when sending + +### 3. Saving Requests to Collections +1. Configure your request +2. Click "Save" button +3. Choose or create collection +4. Request is saved with all current settings + +### 4. Loading Saved Requests +1. Open collections sidebar +2. Browse collections +3. Click on any saved request +4. Request loads with all original settings + +## 📁 File Structure +``` +components/api-platform/ +├── CollectionsSidebar.vue # Main collections interface +├── CreateCollectionModal.vue # Collection creation +├── EnvironmentSelector.vue # Environment dropdown +├── EnvironmentModal.vue # Environment management +├── SaveRequestModal.vue # Request saving +└── RequestBuilder.vue # Enhanced with variables + +composables/ +├── useApiPlatform.js # Global state management +└── useVariableSubstitution.js # Variable processing + +pages/api-platform/ +└── index.vue # Main layout with sidebar +``` + +## ✨ Key Benefits + +1. **Organized Workflow**: Collections help organize related requests +2. **Environment Management**: Easy switching between dev/staging/prod +3. **Variable Substitution**: DRY principle for common values +4. **Persistent Storage**: Work survives browser restarts +5. **Team Collaboration**: Exportable collections (future feature) +6. **Professional UX**: Modern interface with visual feedback + +## 🔄 Integration with Existing Features + +- ✅ Fully compatible with existing request/response system +- ✅ Works with all authentication methods +- ✅ Integrates with notification system +- ✅ Maintains all existing functionality +- ✅ No breaking changes to existing components + +## 🎯 Phase 3 Goals - Status + +- ✅ Collections sidebar with tree view +- ✅ Save/Load requests to collections +- ✅ Environment selector dropdown +- ✅ Variable substitution ({{baseUrl}}) +- ✅ Persistence layer (localStorage) + +**Phase 3 is now complete and ready for use!** \ No newline at end of file diff --git a/docs/PHASE-4-IMPLEMENTATION-SUMMARY.md b/docs/PHASE-4-IMPLEMENTATION-SUMMARY.md new file mode 100644 index 0000000..edc1133 --- /dev/null +++ b/docs/PHASE-4-IMPLEMENTATION-SUMMARY.md @@ -0,0 +1,165 @@ +# Phase 4 Implementation Summary - Advanced Features + +## ✅ Completed Phase 4 Features + +### 1. OAuth2 Complete Flow Implementation + +**Enhanced Authentication Tab (`components/api-platform/tabs/AuthTab.vue`)** +- ✅ **Authorization Code Flow** - Full OAuth2 flow with popup window and callback handling +- ✅ **Implicit Flow** - Direct access token retrieval from authorization server +- ✅ **Client Credentials Flow** - Server-to-server authentication +- ✅ **Resource Owner Password Flow** - Username/password based token acquisition +- ✅ **Token Management** - Automatic token expiration tracking and display +- ✅ **State Parameter** - CSRF protection for OAuth2 flows +- ✅ **Scope Support** - Configurable OAuth2 scopes + +**Server-Side OAuth2 Endpoints** +- ✅ `/api/api-platform/oauth2/exchange-code.post.js` - Authorization code exchange +- ✅ `/api/api-platform/oauth2/client-credentials.post.js` - Client credentials flow +- ✅ `/api/api-platform/oauth2/password.post.js` - Resource owner password flow +- ✅ OAuth2 Callback Page (`pages/oauth/callback.vue`) - Handles authorization callbacks + +**OAuth2 Features:** +- **Multiple Grant Types**: Authorization Code, Implicit, Client Credentials, Password +- **Popup Window Flow**: Secure authorization without leaving the main application +- **Token Persistence**: Access tokens are stored in the auth state +- **Expiration Handling**: Automatic token expiry tracking +- **Error Handling**: Comprehensive error messages for failed flows +- **Security**: State parameter support for CSRF protection + +### 2. Advanced Test Scripts System + +**Comprehensive Test Scripts Modal (`components/api-platform/TestScriptsModal.vue`)** +- ✅ **Postman-compatible API** - Full `pm.*` API implementation +- ✅ **Advanced Script Editor** - Syntax highlighting and formatting +- ✅ **Test Templates** - Pre-built test patterns for common scenarios +- ✅ **Quick Snippets** - Insert common test code blocks +- ✅ **Real-time Execution** - Execute test scripts against response data +- ✅ **Detailed Results** - Pass/fail status with error messages +- ✅ **Console Output** - Capture and display console.log output +- ✅ **Script Persistence** - Auto-save test scripts to localStorage + +**Test Script Features:** +- **Status Code Validation**: Check response status codes +- **JSON Schema Validation**: Validate response structure and data types +- **Response Time Testing**: Performance assertions +- **Header Validation**: Check presence and values of response headers +- **Data Validation**: Complex assertions on response data +- **Custom Assertions**: Full expect/chai-style assertion library +- **Error Reporting**: Detailed error messages with context + +**Available Test APIs:** +```javascript +// Status checks +pm.response.to.have.status(200); + +// JSON data access +const jsonData = pm.response.json(); + +// Response time checks +pm.expect(pm.response.responseTime).to.be.below(200); + +// Header validation +pm.response.to.have.header("Content-Type"); + +// Data validation with chaining +pm.expect(jsonData).to.have.property('id').that.is.a('number'); +pm.expect(jsonData.email).that.matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); +``` + +### 3. Enhanced Response Export Options + +**Advanced Response Export Features (in `ResponseViewer.vue`)** +- ✅ **JSON/CSV Download** - Export response data in multiple formats +- ✅ **Response Body Export** - Download raw response data +- ✅ **Headers Export** - Export all response headers +- ✅ **Timeline Export** - Performance data export +- ✅ **Test Results Export** - Export test execution results + +### 4. Enhanced File Upload Support + +**Complete Form-Data Implementation (`components/api-platform/tabs/BodyTab.vue`)** +- ✅ **Drag & Drop File Upload** - Intuitive file upload interface +- ✅ **Multiple File Support** - Support for multiple file uploads +- ✅ **File Size Validation** - 10MB file size limit with validation +- ✅ **File Type Detection** - Automatic file type detection +- ✅ **Visual File Management** - File preview with remove functionality +- ✅ **Mixed Form Data** - Text fields and files in the same request + +### 5. Enhanced Import/Export System (Already Implemented) + +**Comprehensive Import/Export Modal (`components/api-platform/ImportExportModal.vue`)** +- ✅ **Postman Collection Import** - Full Postman v2.1+ support +- ✅ **OpenAPI Import** - Import from Swagger/OpenAPI 3.0+ specifications +- ✅ **Multiple Export Formats** - Export to Postman, Insomnia formats +- ✅ **Collection Management** - Organize requests in collections +- ✅ **OpenAPI Generation** - Generate OpenAPI specs from collections + +### 6. Advanced Code Generation (Already Implemented) + +**Comprehensive Code Generation Modal (`components/api-platform/CodeGenerationModal.vue`)** +- ✅ **Multiple Languages** - cURL, JavaScript (Fetch/Axios), PHP, Python, Node.js, Java, Go +- ✅ **Authentication Support** - All auth types in generated code +- ✅ **Request Body Support** - All body types in generated code +- ✅ **Headers & Parameters** - Complete request configuration +- ✅ **Copy to Clipboard** - Easy code copying functionality + +### 7. Enhanced Response Analysis + +**Advanced Response Viewer Features (`components/api-platform/ResponseViewer.vue`)** +- ✅ **Timeline Tab** - Performance waterfall with DNS, TCP, TLS breakdowns +- ✅ **Cookies Tab** - Complete cookie parsing and management +- ✅ **Test Results Tab** - Integrated test execution results +- ✅ **Response Export** - Multiple export formats (JSON, CSV, etc.) +- ✅ **Header Management** - Advanced header viewing and copying +- ✅ **JSON Beautify/Minify** - Response formatting tools + +## 🎯 Phase 4 Achievement Metrics + +### Core Features Status: +- ✅ **OAuth2 Complete Flow** - All 4 grant types implemented +- ✅ **Advanced Test Scripts** - Postman-compatible testing environment +- ✅ **File Upload Support** - Complete multipart form-data implementation +- ✅ **Code Generation** - 7+ programming languages supported +- ✅ **Import/Export** - Postman & OpenAPI support +- ✅ **Response Export** - Multiple format support +- ✅ **Enhanced Analysis** - Timeline, cookies, comprehensive response tools + +### Technical Implementation: +- **Client-Side**: Vue 3 Composition API with reactive state management +- **Server-Side**: Nuxt 3 API routes for OAuth2 token exchange +- **File Handling**: Complete multipart/form-data support with validation +- **Test Engine**: Custom JavaScript execution environment with pm.* API +- **Export System**: Multiple format support (JSON, CSV, Postman, OpenAPI) +- **Authentication**: Full OAuth2 specification compliance + +### User Experience: +- **Professional UI/UX**: Consistent design with dark mode support +- **Responsive Design**: Works across all device sizes +- **Real-time Feedback**: Live validation and error reporting +- **Accessibility**: Proper ARIA labels and keyboard navigation +- **Performance**: Optimized for large responses and complex test scripts + +## 🚀 Ready for Production + +Phase 4 represents a **complete, professional-grade API testing platform** with: + +1. **Enterprise Authentication**: Full OAuth2 support for modern APIs +2. **Advanced Testing**: Postman-compatible scripting environment +3. **File Handling**: Complete multipart form data support +4. **Code Generation**: Production-ready code in multiple languages +5. **Data Management**: Import/Export with industry-standard formats +6. **Performance Analysis**: Detailed timing and response analysis + +The API Platform now matches or exceeds the functionality of commercial API testing tools while maintaining a clean, intuitive interface built with modern web technologies. + +## 🔄 Integration Notes + +All Phase 4 features are: +- **Backward Compatible**: No breaking changes to existing functionality +- **Well Tested**: Comprehensive error handling and validation +- **Documented**: Clear user interfaces with helpful tooltips and descriptions +- **Performant**: Optimized for real-world usage scenarios +- **Secure**: Proper handling of sensitive data like OAuth2 tokens + +The implementation maintains the existing UI layout and design system while adding powerful new capabilities for advanced API testing and development workflows. \ No newline at end of file diff --git a/docs/PHASE-5-IMPLEMENTATION-SUMMARY.md b/docs/PHASE-5-IMPLEMENTATION-SUMMARY.md new file mode 100644 index 0000000..ce14848 --- /dev/null +++ b/docs/PHASE-5-IMPLEMENTATION-SUMMARY.md @@ -0,0 +1,189 @@ +# Phase 5 Implementation Summary - Enhanced Response Viewer + +## ✅ Completed Phase 5 Features + +### 1. Advanced Response Search & Navigation + +**Real-time Response Search (`components/api-platform/ResponseViewer.vue`)** +- ✅ **In-Response Search** - Live search within response body content +- ✅ **Search Highlighting** - Yellow highlighting of matched text with HTML escaping +- ✅ **Search Navigation** - Previous/Next navigation through search results +- ✅ **Match Counter** - Shows current match position (e.g., "2/5 matches") +- ✅ **Clear Search** - Easy search reset functionality +- ✅ **Keyboard Navigation** - Up/Down arrows to navigate between matches + +**Search Features:** +- **Real-time Highlighting**: Instant visual feedback as you type +- **Case-insensitive Search**: Finds matches regardless of case +- ✅ **Safe HTML Rendering**: Properly escaped to prevent XSS +- ✅ **Match Statistics**: Live count of total matches found +- ✅ **Responsive UI**: Search input adapts to available space + +### 2. Enhanced Headers & Cookies Filtering + +**Advanced Header Management** +- ✅ **Header Search/Filter** - Filter headers by name or value +- ✅ **Real-time Filtering** - Instant results as you type +- ✅ **No Match Messaging** - Clear feedback when no headers match +- ✅ **Preserved Functionality** - All existing copy/export features intact + +**Enhanced Cookie Management** +- ✅ **Cookie Search/Filter** - Filter by name, value, or domain +- ✅ **Multi-field Search** - Searches across cookie properties +- ✅ **Smart Filtering** - Matches partial strings in any cookie field +- ✅ **Enhanced UX** - Clear feedback for empty search results + +### 3. Advanced Performance Analysis + +**Comprehensive Performance Metrics Dashboard** +- ✅ **Performance Cards** - Grid display of key metrics (Time, Size, Status, Headers) +- ✅ **Performance Labels** - Color-coded performance ratings (Excellent/Good/Average/Slow) +- ✅ **Intelligent Insights** - Automated performance analysis with actionable recommendations +- ✅ **Status Color Coding** - Visual status indicators (Green/Blue/Yellow/Red) +- ✅ **Size Analysis** - File size evaluation with optimization suggestions + +**Enhanced Timeline Waterfall** +- ✅ **Visual Progress Bars** - Proportional bars showing time distribution +- ✅ **Percentage Breakdown** - Shows percentage of total time per stage +- ✅ **Stage Details** - DNS, TCP, TLS, Request, Processing, Response breakdown +- ✅ **Timeline Export** - Export performance data as JSON +- ✅ **Performance Comparison** - Compare with previous request performance + +**Performance Insights Engine** +```javascript +// Automated performance analysis +- Response time > 1000ms → "Consider optimization" +- Response time < 100ms → "Excellent response time!" +- File size > 1MB → "Consider pagination or compression" +- HTTP 4xx/5xx status → "Check request configuration" +``` + +### 4. Advanced Timeline Features + +**Enhanced Timeline Tab** +- ✅ **Performance Dashboard** - 4-card metric overview +- ✅ **Waterfall Visualization** - Visual timeline with progress bars +- ✅ **Export Functionality** - Download timeline data as JSON +- ✅ **Performance Comparison** - Compare with previous requests +- ✅ **Detailed Analysis** - Performance insights with recommendations +- ✅ **Overall Performance Summary** - Contextual performance messaging + +**Timeline Export Data Structure:** +```json +{ + "timestamp": "2024-01-15T10:30:00.000Z", + "url": "https://api.example.com/users", + "totalTime": 245, + "status": 200, + "size": 1024, + "stages": [...], + "insights": [...] +} +``` + +### 5. Enhanced Response Analysis Tools + +**Intelligent Response Processing** +- ✅ **Smart Format Detection** - Auto-detect JSON, XML, HTML, Text +- ✅ **Search Result Highlighting** - Safe HTML rendering with XSS protection +- ✅ **Performance Color Coding** - Visual performance indicators +- ✅ **Dynamic Insights** - Context-aware recommendations +- ✅ **Multi-format Support** - Handles all response types gracefully + +**Advanced Analysis Features:** +- **Response Size Analysis**: Identifies large responses that may need optimization +- **Performance Categorization**: Excellent (<100ms), Good (<500ms), Average (<1000ms), Slow (>1000ms) +- **HTTP Status Intelligence**: Color-coded status interpretation +- **Header Analysis**: Count and categorization of response headers + +### 6. Enhanced User Experience + +**Improved Interface Design** +- ✅ **Responsive Search Inputs** - Properly sized for different screen sizes +- ✅ **Visual Feedback** - Loading states, hover effects, disabled states +- ✅ **Smart Navigation** - Context-aware navigation controls +- ✅ **Consistent Styling** - Matches existing design system +- ✅ **Accessibility** - Proper ARIA labels and keyboard navigation + +**Enhanced Information Display** +- ✅ **Match Statistics** - Live search result counts +- ✅ **Performance Badges** - Visual performance indicators +- ✅ **Progressive Enhancement** - Works without JavaScript for basic functionality +- ✅ **Dark Mode Support** - Full dark theme compatibility + +## 🎯 Phase 5 Achievement Metrics + +### Core Enhancement Areas: +- ✅ **Response Search & Navigation** - Full-text search with highlighting and navigation +- ✅ **Advanced Filtering** - Headers and cookies filtering capabilities +- ✅ **Performance Analysis** - Comprehensive performance insights and recommendations +- ✅ **Timeline Enhancement** - Visual waterfall with export and comparison features +- ✅ **User Experience** - Responsive design with intelligent feedback + +### Technical Implementation Details: + +**Search Engine:** +```javascript +- Real-time highlighting with regex matching +- HTML entity escaping for security +- Dynamic match counting and navigation +- Cross-tab search state management +``` + +**Performance Analysis:** +```javascript +- Automated insight generation +- Color-coded performance indicators +- Timeline waterfall visualization +- Export and comparison functionality +``` + +**Filtering System:** +```javascript +- Multi-field search for headers and cookies +- Case-insensitive pattern matching +- Real-time result updates +- Empty state handling +``` + +### User Experience Improvements: +- **Instant Feedback**: Real-time search results and filtering +- **Visual Clarity**: Color-coded status and performance indicators +- **Data Export**: Timeline and performance data export capabilities +- **Comparison Tools**: Performance comparison with previous requests +- **Accessibility**: Full keyboard navigation and screen reader support + +## 🚀 Phase 5 Impact + +The Enhanced Response Viewer now provides: + +1. **Professional Analysis Tools**: Performance insights comparable to enterprise tools +2. **Advanced Search Capabilities**: Find specific data quickly in large responses +3. **Intelligent Filtering**: Efficiently navigate headers and cookies +4. **Performance Monitoring**: Track and analyze API performance over time +5. **Export Capabilities**: Share and analyze performance data externally + +### Maintained Compatibility: +- ✅ **Zero Breaking Changes**: All existing functionality preserved +- ✅ **Backward Compatibility**: Works with all existing API responses +- ✅ **Design Consistency**: Matches current UI/UX patterns +- ✅ **Performance**: No impact on response rendering speed + +## 🔄 Integration Status + +All Phase 5 enhancements are: +- **Production Ready**: Fully tested and error-handled +- **Responsive**: Works across all device sizes +- **Accessible**: WCAG compliant with proper ARIA labels +- **Performant**: Optimized for large responses and real-time search +- **Secure**: XSS protection and safe HTML rendering + +The API Platform Response Viewer now offers enterprise-grade analysis capabilities while maintaining the clean, intuitive interface that makes it accessible to developers of all skill levels. + +## 📈 Key Benefits + +1. **Developer Productivity**: Quickly find and analyze specific response data +2. **Performance Monitoring**: Track API performance trends over time +3. **Debugging Efficiency**: Advanced filtering and search for troubleshooting +4. **Data Export**: Share performance insights with team members +5. **Professional Analysis**: Enterprise-level response analysis tools \ No newline at end of file diff --git a/docs/SITE_SETTINGS.md b/docs/SITE_SETTINGS.md new file mode 100644 index 0000000..eed0bcc --- /dev/null +++ b/docs/SITE_SETTINGS.md @@ -0,0 +1,161 @@ +# Site Settings Feature + +## Overview +The Site Settings feature allows administrators to customize the appearance and branding of the application through a user-friendly interface. All settings are globally applied across the entire application including SEO, meta tags, and visual elements. + +## Features + +### 1. Basic Information +- **Site Name**: Customize the application name displayed globally in: + - Header and sidebar + - Browser title and meta tags + - SEO and Open Graph tags + - Loading screen + - All pages and components +- **Site Description**: Set a description used for: + - SEO meta descriptions + - Open Graph descriptions + - Twitter Card descriptions +- **Theme Selection**: Choose from available themes: + - Standard themes (from themeList.js) + - Accessibility themes (from themeList2.js) + - Custom themes added to theme.css + +### 2. Branding +- **Site Logo**: Upload a custom logo displayed in: + - Header (horizontal layout) + - Sidebar (vertical layout) + - Loading screen + - Login page + - Any component using site settings +- **Favicon**: Upload a custom favicon displayed in: + - Browser tabs + - Bookmarks + - Mobile home screen icons + +### 3. Advanced Settings +- **Custom CSS**: Add custom CSS injected into document head +- **Custom Theme File**: Upload CSS files saved to `/assets/style/css/` +- **Add Custom Theme to theme.css**: Directly add themes to the main theme.css file + +## How to Access + +1. Navigate to **Pentadbiran** → **Konfigurasi** → **Site Settings** +2. Use the tabbed interface: + - **Basic Info**: Site name, description, and theme selection + - **Branding**: Logo and favicon uploads + - **Advanced**: Custom CSS and theme management +3. Use the **Live Preview** panel to see changes in real-time +4. Click **Save Changes** to apply your settings + +## Technical Implementation + +### Database Schema +The settings are stored in the `site_settings` table with the following fields: +- `siteName`, `siteDescription` +- `siteLogo`, `siteFavicon` +- `selectedTheme` - Selected theme name +- `customCSS`, `customThemeFile` +- Legacy fields maintained for backward compatibility + +### API Endpoints +- `GET /api/devtool/config/site-settings` - Retrieve current settings +- `POST /api/devtool/config/site-settings` - Update settings +- `POST /api/devtool/config/upload-file` - Upload files (logos, themes) +- `POST /api/devtool/config/add-custom-theme` - Add custom theme to theme.css + +### File Upload Locations +- **Logo and Favicon files**: Saved to `public/uploads/site-settings/` +- **Theme CSS files**: Saved to `assets/style/css/` directory +- **Custom themes**: Added directly to `assets/style/css/base/theme.css` + +### Composable +The `useSiteSettings()` composable provides: +- `siteSettings` - Reactive settings object +- `loadSiteSettings()` - Load settings from API +- `updateSiteSettings()` - Update settings +- `setTheme()` - Set theme using existing theme system +- `getCurrentTheme()` - Get current theme +- `applyThemeSettings()` - Apply theme changes to DOM +- `updateGlobalMeta()` - Update global meta tags and SEO +- `addCustomThemeToFile()` - Add custom theme to theme.css + +### Global Integration +The site settings are globally integrated across: + +#### Header Component +- Uses site settings for logo and name display +- Theme selection dropdown uses same system as site settings +- Synced with site settings theme selection + +#### Loading Component +- Uses site logo if available, fallback to default +- Displays site name in loading screen + +#### App.vue +- Global meta tags updated from site settings +- Title, description, and favicon managed globally +- Theme initialization from site settings + +#### SEO and Meta Tags +- Document title updated globally +- Meta descriptions for SEO +- Open Graph tags for social sharing +- Twitter Card tags +- Favicon and apple-touch-icon + +### Theme System Integration +- Integrates with existing theme system (themeList.js, themeList2.js) +- Theme selection in header dropdown synced with site settings +- Custom themes can be added directly to theme.css +- Backward compatibility with existing theme structure + +### Custom Theme Structure +Custom themes added to theme.css should follow this structure: +```css +html[data-theme="your-theme-name"] { + --color-primary: 255, 0, 0; + --color-secondary: 0, 255, 0; + --color-success: 0, 255, 0; + --color-info: 0, 0, 255; + --color-warning: 255, 255, 0; + --color-danger: 255, 0, 0; + /* Add your theme variables here */ +} +``` + +## Default Values +If no settings are configured, the system uses these defaults: +- Site Name: "corradAF" +- Site Description: "corradAF Base Project" +- Selected Theme: "biasa" +- Logo: Default corradAF logo +- Favicon: Default favicon + +## Migration Notes +- Legacy color fields (primaryColor, secondaryColor, etc.) are maintained for backward compatibility +- `themeMode` field is mapped to `selectedTheme` for compatibility +- Existing installations will automatically use default values +- Theme selection integrates with existing theme dropdown in header + +## Notes +- Changes are applied immediately in the preview +- Theme changes affect the entire application +- Custom CSS is injected into the document head +- Theme files are saved to `/assets/style/css/` for proper integration +- File uploads are validated for type and size +- Settings persist across browser sessions +- Site name and description updates are reflected globally and immediately +- All meta tags and SEO elements are automatically updated +- Logo changes are reflected in all components that use site settings + +### Important Notes +- Changes are applied immediately in the preview +- Theme changes affect the entire application +- Custom CSS is injected into the document head +- Theme files are saved to `/assets/style/css/` for proper integration +- File uploads are validated for type and size +- Settings persist across browser sessions +- Site name and description updates are reflected globally and immediately +- All meta tags and SEO elements are automatically updated +- Logo changes are reflected in all components that use site settings \ No newline at end of file diff --git a/docs/openapi-custom.json b/docs/openapi-custom.json new file mode 100644 index 0000000..136e957 --- /dev/null +++ b/docs/openapi-custom.json @@ -0,0 +1,445 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Corrad AF 2024 API Platform", + "description": "Complete API reference for the Corrad AF 2024 API Platform project including authentication, business logic, development tools, and platform management endpoints.", + "version": "2.0.0", + "contact": { + "name": "API Support", + "email": "support@corradaf.com", + "url": "https://corradaf.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "{protocol}://{host}:{port}/api", + "description": "API Server", + "variables": { + "protocol": { + "enum": ["http", "https"], + "default": "http" + }, + "host": { + "default": "localhost" + }, + "port": { + "default": "3000" + } + } + } + ], + "tags": [ + { + "name": "authentication", + "description": "Authentication and authorization operations" + }, + { + "name": "business-logic", + "description": "Core business functionality" + }, + { + "name": "api-platform", + "description": "API platform and proxy operations" + }, + { + "name": "metabase", + "description": "Analytics and reporting integration" + }, + { + "name": "devtool-users", + "description": "User management for development" + }, + { + "name": "devtool-roles", + "description": "Role management for development" + }, + { + "name": "devtool-menu", + "description": "Menu management for development" + }, + { + "name": "devtool-orm", + "description": "Database and ORM management" + }, + { + "name": "devtool-config", + "description": "Configuration management" + }, + { + "name": "devtool-api", + "description": "API development tools" + }, + { + "name": "devtool-content", + "description": "Content management tools" + } + ], + "paths": { + "/auth/login": { + "post": { + "tags": ["authentication"], + "summary": "User Login", + "description": "Authenticate user and receive access/refresh tokens", + "operationId": "login", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "User's username" + }, + "password": { + "type": "string", + "format": "password", + "description": "User's password" + } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "Login successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponse" + } + } + } + }, + "400": { + "description": "Bad request - missing username or password", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/logout": { + "get": { + "tags": ["authentication"], + "summary": "User Logout", + "description": "Logout user and clear authentication cookies", + "operationId": "logout", + "responses": { + "200": { + "description": "Logout successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + } + } + } + }, + "/auth/validate": { + "get": { + "tags": ["authentication"], + "summary": "Validate Token", + "description": "Validate current authentication token", + "operationId": "validateToken", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Token is valid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "401": { + "description": "Invalid or expired token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/analyze-asnaf": { + "post": { + "tags": ["business-logic"], + "summary": "Analyze Asnaf Profile", + "description": "Analyze Asnaf profile using AI/OpenAI integration to determine eligibility and assistance recommendations", + "operationId": "analyzeAsnaf", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsnafAnalysisRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Analysis completed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsnafAnalysisResponse" + } + } + } + }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Analysis failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SuccessResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 200 + }, + "message": { + "type": "string", + "example": "Operation successful" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "Error message" + }, + "errors": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "LoginResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 200 + }, + "message": { + "type": "string", + "example": "Login success" + }, + "data": { + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "user@example.com" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["admin", "user"] + } + } + } + } + }, + "AsnafAnalysisRequest": { + "type": "object", + "properties": { + "monthlyIncome": { + "type": "string", + "description": "Monthly income amount", + "example": "2000" + }, + "otherIncome": { + "type": "string", + "description": "Other income sources", + "example": "500" + }, + "totalIncome": { + "type": "string", + "description": "Total income amount", + "example": "2500" + }, + "occupation": { + "type": "string", + "description": "Applicant's occupation", + "example": "Clerk" + }, + "maritalStatus": { + "type": "string", + "description": "Marital status", + "example": "Married" + }, + "dependents": { + "type": "array", + "description": "List of dependents", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + } + }, + "required": ["monthlyIncome", "totalIncome", "occupation", "maritalStatus"] + }, + "AsnafAnalysisResponse": { + "type": "object", + "properties": { + "hadKifayahPercentage": { + "type": "string", + "example": "75%" + }, + "kategoriAsnaf": { + "type": "string", + "example": "Miskin" + }, + "kategoriKeluarga": { + "type": "string", + "example": "Miskin (50-100% HK)" + }, + "cadanganKategori": { + "type": "string", + "example": "Miskin" + }, + "statusKelayakan": { + "type": "string", + "example": "Layak (Miskin)" + }, + "cadanganBantuan": { + "type": "array", + "items": { + "type": "object", + "properties": { + "nama": { + "type": "string", + "example": "Bantuan Kewangan Bulanan" + }, + "peratusan": { + "type": "string", + "example": "90%" + } + } + } + }, + "ramalanJangkaMasaPulih": { + "type": "string", + "example": "6 bulan" + }, + "rumusan": { + "type": "string", + "example": "Pemohon memerlukan perhatian segera." + } + } + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT Bearer token authentication" + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] +} \ No newline at end of file diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000..136e957 --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,445 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Corrad AF 2024 API Platform", + "description": "Complete API reference for the Corrad AF 2024 API Platform project including authentication, business logic, development tools, and platform management endpoints.", + "version": "2.0.0", + "contact": { + "name": "API Support", + "email": "support@corradaf.com", + "url": "https://corradaf.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "{protocol}://{host}:{port}/api", + "description": "API Server", + "variables": { + "protocol": { + "enum": ["http", "https"], + "default": "http" + }, + "host": { + "default": "localhost" + }, + "port": { + "default": "3000" + } + } + } + ], + "tags": [ + { + "name": "authentication", + "description": "Authentication and authorization operations" + }, + { + "name": "business-logic", + "description": "Core business functionality" + }, + { + "name": "api-platform", + "description": "API platform and proxy operations" + }, + { + "name": "metabase", + "description": "Analytics and reporting integration" + }, + { + "name": "devtool-users", + "description": "User management for development" + }, + { + "name": "devtool-roles", + "description": "Role management for development" + }, + { + "name": "devtool-menu", + "description": "Menu management for development" + }, + { + "name": "devtool-orm", + "description": "Database and ORM management" + }, + { + "name": "devtool-config", + "description": "Configuration management" + }, + { + "name": "devtool-api", + "description": "API development tools" + }, + { + "name": "devtool-content", + "description": "Content management tools" + } + ], + "paths": { + "/auth/login": { + "post": { + "tags": ["authentication"], + "summary": "User Login", + "description": "Authenticate user and receive access/refresh tokens", + "operationId": "login", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "User's username" + }, + "password": { + "type": "string", + "format": "password", + "description": "User's password" + } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "Login successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponse" + } + } + } + }, + "400": { + "description": "Bad request - missing username or password", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/logout": { + "get": { + "tags": ["authentication"], + "summary": "User Logout", + "description": "Logout user and clear authentication cookies", + "operationId": "logout", + "responses": { + "200": { + "description": "Logout successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + } + } + } + }, + "/auth/validate": { + "get": { + "tags": ["authentication"], + "summary": "Validate Token", + "description": "Validate current authentication token", + "operationId": "validateToken", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Token is valid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "401": { + "description": "Invalid or expired token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/analyze-asnaf": { + "post": { + "tags": ["business-logic"], + "summary": "Analyze Asnaf Profile", + "description": "Analyze Asnaf profile using AI/OpenAI integration to determine eligibility and assistance recommendations", + "operationId": "analyzeAsnaf", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsnafAnalysisRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Analysis completed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsnafAnalysisResponse" + } + } + } + }, + "400": { + "description": "Invalid request data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Analysis failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SuccessResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 200 + }, + "message": { + "type": "string", + "example": "Operation successful" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "Error message" + }, + "errors": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "LoginResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 200 + }, + "message": { + "type": "string", + "example": "Login success" + }, + "data": { + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "user@example.com" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["admin", "user"] + } + } + } + } + }, + "AsnafAnalysisRequest": { + "type": "object", + "properties": { + "monthlyIncome": { + "type": "string", + "description": "Monthly income amount", + "example": "2000" + }, + "otherIncome": { + "type": "string", + "description": "Other income sources", + "example": "500" + }, + "totalIncome": { + "type": "string", + "description": "Total income amount", + "example": "2500" + }, + "occupation": { + "type": "string", + "description": "Applicant's occupation", + "example": "Clerk" + }, + "maritalStatus": { + "type": "string", + "description": "Marital status", + "example": "Married" + }, + "dependents": { + "type": "array", + "description": "List of dependents", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + } + }, + "required": ["monthlyIncome", "totalIncome", "occupation", "maritalStatus"] + }, + "AsnafAnalysisResponse": { + "type": "object", + "properties": { + "hadKifayahPercentage": { + "type": "string", + "example": "75%" + }, + "kategoriAsnaf": { + "type": "string", + "example": "Miskin" + }, + "kategoriKeluarga": { + "type": "string", + "example": "Miskin (50-100% HK)" + }, + "cadanganKategori": { + "type": "string", + "example": "Miskin" + }, + "statusKelayakan": { + "type": "string", + "example": "Layak (Miskin)" + }, + "cadanganBantuan": { + "type": "array", + "items": { + "type": "object", + "properties": { + "nama": { + "type": "string", + "example": "Bantuan Kewangan Bulanan" + }, + "peratusan": { + "type": "string", + "example": "90%" + } + } + } + }, + "ramalanJangkaMasaPulih": { + "type": "string", + "example": "6 bulan" + }, + "rumusan": { + "type": "string", + "example": "Pemohon memerlukan perhatian segera." + } + } + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT Bearer token authentication" + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] +} \ No newline at end of file diff --git a/docs/postman_collection.json b/docs/postman_collection.json new file mode 100644 index 0000000..6ec2f96 --- /dev/null +++ b/docs/postman_collection.json @@ -0,0 +1,1151 @@ +{ + "info": { + "name": "Corrad AF 2024 API Platform", + "description": "Complete API collection for the Corrad AF 2024 API Platform project", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "version": "1.0.0" + }, + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:3000", + "description": "Base URL for the API" + }, + { + "key": "accessToken", + "value": "", + "description": "Access token for authenticated requests" + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{accessToken}}", + "type": "string" + } + ] + }, + "item": [ + { + "name": "Authentication", + "description": "Authentication and authorization endpoints", + "item": [ + { + "name": "Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin\",\n \"password\": \"password123\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/auth/login", + "host": ["{{baseUrl}}"], + "path": ["api", "auth", "login"] + }, + "description": "Authenticate user and receive access/refresh tokens" + }, + "response": [] + }, + { + "name": "Logout", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/auth/logout", + "host": ["{{baseUrl}}"], + "path": ["api", "auth", "logout"] + }, + "description": "Logout user and clear authentication cookies" + }, + "response": [] + }, + { + "name": "Validate Token", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/auth/validate", + "host": ["{{baseUrl}}"], + "path": ["api", "auth", "validate"] + }, + "description": "Validate current authentication token" + }, + "response": [] + } + ] + }, + { + "name": "Business Logic", + "description": "Core business functionality endpoints", + "item": [ + { + "name": "Analyze Asnaf Profile", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"monthlyIncome\": \"2000\",\n \"otherIncome\": \"500\",\n \"totalIncome\": \"2500\",\n \"occupation\": \"Clerk\",\n \"maritalStatus\": \"Married\",\n \"dependents\": [\n {\"name\": \"Child 1\", \"age\": 10},\n {\"name\": \"Child 2\", \"age\": 8}\n ]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/analyze-asnaf", + "host": ["{{baseUrl}}"], + "path": ["api", "analyze-asnaf"] + }, + "description": "Analyze Asnaf profile using AI/OpenAI integration to determine eligibility and assistance recommendations" + }, + "response": [] + } + ] + }, + { + "name": "API Platform", + "description": "API platform and proxy endpoints", + "item": [ + { + "name": "Send HTTP Request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"https://api.example.com/users\",\n \"method\": \"GET\",\n \"headers\": [\n {\"key\": \"Content-Type\", \"value\": \"application/json\", \"active\": true}\n ],\n \"params\": [\n {\"key\": \"page\", \"value\": \"1\", \"active\": true}\n ],\n \"auth\": {\n \"type\": \"bearer\",\n \"bearer\": \"your-token-here\"\n },\n \"requestBody\": {\n \"type\": \"none\"\n },\n \"timeout\": 30000\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/api-platform/send-request", + "host": ["{{baseUrl}}"], + "path": ["api", "api-platform", "send-request"] + }, + "description": "Proxy HTTP requests through the platform with authentication and request/response handling" + }, + "response": [] + }, + { + "name": "OAuth2 - Client Credentials", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"client_id\": \"your-client-id\",\n \"client_secret\": \"your-client-secret\",\n \"grant_type\": \"client_credentials\",\n \"scope\": \"read write\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/api-platform/oauth2/client-credentials", + "host": ["{{baseUrl}}"], + "path": ["api", "api-platform", "oauth2", "client-credentials"] + }, + "description": "Obtain OAuth2 access token using client credentials flow" + }, + "response": [] + }, + { + "name": "OAuth2 - Exchange Code", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"code\": \"authorization-code\",\n \"client_id\": \"your-client-id\",\n \"client_secret\": \"your-client-secret\",\n \"redirect_uri\": \"https://your-app.com/callback\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/api-platform/oauth2/exchange-code", + "host": ["{{baseUrl}}"], + "path": ["api", "api-platform", "oauth2", "exchange-code"] + }, + "description": "Exchange authorization code for access token in OAuth2 flow" + }, + "response": [] + } + ] + }, + { + "name": "Metabase Integration", + "description": "Analytics and reporting integration", + "item": [ + { + "name": "Get Metabase Token", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/metabase/token", + "host": ["{{baseUrl}}"], + "path": ["api", "metabase", "token"] + }, + "description": "Get authentication token for Metabase integration" + }, + "response": [] + } + ] + }, + { + "name": "Development Tools", + "description": "Development and administrative tools", + "item": [ + { + "name": "User Management", + "description": "User CRUD operations for development", + "item": [ + { + "name": "List Users", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/user/list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "user", "list"] + }, + "description": "Get list of all users (excluding deleted)" + }, + "response": [] + }, + { + "name": "Add User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userUsername\": \"newuser\",\n \"userFullName\": \"New User\",\n \"userEmail\": \"newuser@example.com\",\n \"userPhone\": \"+1234567890\",\n \"userPassword\": \"password123\",\n \"roles\": [\"user\"]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/user/add", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "user", "add"] + }, + "description": "Create a new user" + }, + "response": [] + }, + { + "name": "Edit User", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userID\": \"user-id-here\",\n \"userFullName\": \"Updated User Name\",\n \"userEmail\": \"updated@example.com\",\n \"userPhone\": \"+1234567891\",\n \"roles\": [\"user\", \"admin\"]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/user/edit", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "user", "edit"] + }, + "description": "Update existing user" + }, + "response": [] + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userID\": \"user-id-here\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/user/delete", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "user", "delete"] + }, + "description": "Soft delete user (mark as deleted)" + }, + "response": [] + } + ] + }, + { + "name": "Role Management", + "description": "Role CRUD operations for development", + "item": [ + { + "name": "List Roles", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/role/list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "role", "list"] + }, + "description": "Get list of all roles" + }, + "response": [] + }, + { + "name": "Add Role", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"roleName\": \"new-role\",\n \"roleDescription\": \"Description of the new role\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/role/add", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "role", "add"] + }, + "description": "Create a new role" + }, + "response": [] + }, + { + "name": "Edit Role", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"roleID\": \"role-id-here\",\n \"roleName\": \"updated-role\",\n \"roleDescription\": \"Updated description\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/role/edit", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "role", "edit"] + }, + "description": "Update existing role" + }, + "response": [] + }, + { + "name": "Delete Role", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"roleID\": \"role-id-here\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/role/delete", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "role", "delete"] + }, + "description": "Delete role" + }, + "response": [] + } + ] + }, + { + "name": "Menu Management", + "description": "Navigation menu management for development", + "item": [ + { + "name": "Get User Menu List", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/user-list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "user-list"] + }, + "description": "Get menu items for users" + }, + "response": [] + }, + { + "name": "Get Role Menu List", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/role-list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "role-list"] + }, + "description": "Get menu items for roles" + }, + "response": [] + }, + { + "name": "Add Menu Item", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"New Menu Item\",\n \"path\": \"/new-path\",\n \"icon\": \"icon-name\",\n \"parent\": null,\n \"order\": 1\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/add", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "add"] + }, + "description": "Add new menu item" + }, + "response": [] + }, + { + "name": "Edit Menu Item", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"menu-id\",\n \"title\": \"Updated Menu Item\",\n \"path\": \"/updated-path\",\n \"icon\": \"updated-icon\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/edit", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "edit"] + }, + "description": "Edit existing menu item" + }, + "response": [] + }, + { + "name": "Delete Menu Item", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"menu-id\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/delete", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "delete"] + }, + "description": "Delete menu item" + }, + "response": [] + }, + { + "name": "Overwrite Navigation", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"navigation\": [\n {\n \"title\": \"Dashboard\",\n \"path\": \"/dashboard\",\n \"icon\": \"dashboard\"\n }\n ]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/overwrite-navigation", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "overwrite-navigation"] + }, + "description": "Overwrite entire navigation structure" + }, + "response": [] + }, + { + "name": "New Add Menu", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/menu/new-add", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "menu", "new-add"] + }, + "description": "New add menu functionality" + }, + "response": [] + } + ] + }, + { + "name": "ORM & Database", + "description": "Database and ORM management tools", + "item": [ + { + "name": "Get Database Schema", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/schema", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "schema"] + }, + "description": "Get database schema information" + }, + "response": [] + }, + { + "name": "Access ORM Studio", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/studio", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "studio"] + }, + "description": "Access ORM studio interface" + }, + "response": [] + }, + { + "name": "Get Table Data", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/data/get?table=users&limit=10", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "data", "get"], + "query": [ + { + "key": "table", + "value": "users" + }, + { + "key": "limit", + "value": "10" + } + ] + }, + "description": "Get data from specific table" + }, + "response": [] + }, + { + "name": "Get Table Configuration", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/table/config", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "table", "config"] + }, + "description": "Get table configuration settings" + }, + "response": [] + }, + { + "name": "Create Table", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"tableName\": \"new_table\",\n \"columns\": [\n {\n \"name\": \"id\",\n \"type\": \"INTEGER\",\n \"primaryKey\": true,\n \"autoIncrement\": true\n },\n {\n \"name\": \"name\",\n \"type\": \"VARCHAR\",\n \"length\": 255,\n \"nullable\": false\n }\n ]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/table/create", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "table", "create"] + }, + "description": "Create new database table" + }, + "response": [] + }, + { + "name": "Get Table Modification Info", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/table/modify/get?table=users", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "table", "modify", "get"], + "query": [ + { + "key": "table", + "value": "users" + } + ] + }, + "description": "Get table structure for modification" + }, + "response": [] + }, + { + "name": "Modify Table", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"tableName\": \"users\",\n \"modifications\": {\n \"addColumns\": [\n {\n \"name\": \"new_field\",\n \"type\": \"VARCHAR\",\n \"length\": 100\n }\n ],\n \"dropColumns\": [\"old_field\"],\n \"modifyColumns\": [\n {\n \"name\": \"existing_field\",\n \"type\": \"TEXT\"\n }\n ]\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/table/modify", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "table", "modify"] + }, + "description": "Modify existing table structure" + }, + "response": [] + }, + { + "name": "Delete Table", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/orm/table/delete/table_name", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "orm", "table", "delete", "table_name"] + }, + "description": "Delete table by name (dynamic route)" + }, + "response": [] + } + ] + }, + { + "name": "Configuration Management", + "description": "Application configuration and settings", + "item": [ + { + "name": "Site Settings", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/config/site-settings", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "config", "site-settings"] + }, + "description": "Get/manage site settings" + }, + "response": [] + }, + { + "name": "Environment Variables", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/config/env", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "config", "env"] + }, + "description": "Get environment configuration" + }, + "response": [] + }, + { + "name": "Upload File", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": [] + }, + { + "key": "destination", + "value": "uploads/", + "type": "text" + } + ] + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/config/upload-file", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "config", "upload-file"] + }, + "description": "Upload file to server" + }, + "response": [] + }, + { + "name": "Loading Logo", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/config/loading-logo", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "config", "loading-logo"] + }, + "description": "Get/set loading logo configuration" + }, + "response": [] + }, + { + "name": "Add Custom Theme", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"themeName\": \"custom-theme\",\n \"colors\": {\n \"primary\": \"#007bff\",\n \"secondary\": \"#6c757d\",\n \"success\": \"#28a745\",\n \"danger\": \"#dc3545\"\n },\n \"fonts\": {\n \"primary\": \"Arial, sans-serif\",\n \"heading\": \"Georgia, serif\"\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/config/add-custom-theme", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "config", "add-custom-theme"] + }, + "description": "Add custom theme configuration" + }, + "response": [] + } + ] + }, + { + "name": "API Management Tools", + "description": "API development and testing tools", + "item": [ + { + "name": "List APIs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/api/list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "api", "list"] + }, + "description": "List all available APIs" + }, + "response": [] + }, + { + "name": "Save API", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"apiName\": \"new-api\",\n \"endpoint\": \"/api/new-endpoint\",\n \"method\": \"GET\",\n \"description\": \"New API endpoint\",\n \"parameters\": [],\n \"responses\": {}\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/api/save", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "api", "save"] + }, + "description": "Save API configuration" + }, + "response": [] + }, + { + "name": "API Linter", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"code\": \"const example = 'API code to lint';\",\n \"language\": \"javascript\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/api/linter", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "api", "linter"] + }, + "description": "Lint API code for errors and best practices" + }, + "response": [] + }, + { + "name": "Format Code (Prettier)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"code\": \"const unformatted={a:1,b:2};\",\n \"language\": \"javascript\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/api/prettier-format", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "api", "prettier-format"] + }, + "description": "Format code using Prettier" + }, + "response": [] + }, + { + "name": "Get File Code", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/api/file-code?file=server/api/example.js", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "api", "file-code"], + "query": [ + { + "key": "file", + "value": "server/api/example.js" + } + ] + }, + "description": "Get source code of API file" + }, + "response": [] + } + ] + }, + { + "name": "Content Management", + "description": "Content and template management tools", + "item": [ + { + "name": "Template Management", + "description": "Template operations", + "item": [ + { + "name": "Get Template List", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/content/template/get-list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "template", "get-list"] + }, + "description": "Get list of available templates" + }, + "response": [] + }, + { + "name": "List Templates", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/content/template/list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "template", "list"] + }, + "description": "List all templates" + }, + "response": [] + }, + { + "name": "Import Template", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"templateName\": \"imported-template\",\n \"templateContent\": \"\",\n \"templateType\": \"vue\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/content/template/import", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "template", "import"] + }, + "description": "Import template" + }, + "response": [] + }, + { + "name": "Template Tags", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/content/template/tag", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "template", "tag"] + }, + "description": "Get template tags" + }, + "response": [] + } + ] + }, + { + "name": "Code Management", + "description": "Code editing and management", + "item": [ + { + "name": "Get File Code", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/content/code/file-code?file=components/example.vue", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "code", "file-code"], + "query": [ + { + "key": "file", + "value": "components/example.vue" + } + ] + }, + "description": "Get source code of file" + }, + "response": [] + }, + { + "name": "Save Code", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"file\": \"components/example.vue\",\n \"content\": \"\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/content/code/save", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "code", "save"] + }, + "description": "Save code to file" + }, + "response": [] + }, + { + "name": "Code Linter", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"code\": \"\",\n \"language\": \"vue\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/content/code/linter", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "code", "linter"] + }, + "description": "Lint code for errors" + }, + "response": [] + }, + { + "name": "Format Code (Prettier)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"code\": \"\",\n \"language\": \"vue\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/devtool/content/code/prettier-format", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "code", "prettier-format"] + }, + "description": "Format code using Prettier" + }, + "response": [] + } + ] + }, + { + "name": "Canvas Management", + "description": "Visual canvas tools", + "item": [ + { + "name": "Canvas File Code", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/content/canvas/file-code?canvas=main-canvas", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "content", "canvas", "file-code"], + "query": [ + { + "key": "canvas", + "value": "main-canvas" + } + ] + }, + "description": "Get canvas file code" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Lookup Data", + "description": "Lookup and reference data", + "item": [ + { + "name": "List Lookup Data", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/devtool/lookup/list", + "host": ["{{baseUrl}}"], + "path": ["api", "devtool", "lookup", "list"] + }, + "description": "Get lookup data list" + }, + "response": [] + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Auto-set authorization header if token exists", + "if (pm.collectionVariables.get('accessToken')) {", + " pm.request.headers.add({", + " key: 'Authorization',", + " value: 'Bearer ' + pm.collectionVariables.get('accessToken')", + " });", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Auto-save access token from login response", + "if (pm.info.requestName === 'Login' && pm.response.code === 200) {", + " const response = pm.response.json();", + " if (response.data && response.data.accessToken) {", + " pm.collectionVariables.set('accessToken', response.data.accessToken);", + " }", + "}", + "", + "// Log response time", + "console.log('Response time: ' + pm.response.responseTime + 'ms');" + ] + } + } + ] +} \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..5ec8516 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,13 @@ +// For running pm2 | ecosystem.config.js + +module.exports = { + apps: [ + { + name: "corradAF", + port: "3000", + exec_mode: "cluster", + instances: "max", + script: "./.output/server/index.mjs", + }, + ], +}; diff --git a/error.vue b/error.vue new file mode 100644 index 0000000..887d095 --- /dev/null +++ b/error.vue @@ -0,0 +1,70 @@ + + + diff --git a/fixes-summary.md b/fixes-summary.md new file mode 100644 index 0000000..57882b9 --- /dev/null +++ b/fixes-summary.md @@ -0,0 +1,122 @@ +# Fixes Implemented ✅ + +## 1. Header Title Not Showing Issue 🔧 + +**Problem**: Site name not appearing in header even when toggle is enabled + +**Root Cause**: Header component only showed site name in horizontal layout (`v-else` condition), but most of the time the layout is vertical + +**Fixes Applied**: +- ✅ Added site name display to both vertical AND horizontal header layouts +- ✅ Enhanced site settings loading in Header component's `onMounted` hook +- ✅ Added immediate watchers to sync toggle changes with global site settings +- ✅ Added debug info panel to troubleshoot site settings state + +**Files Modified**: +- `components/layouts/Header.vue` - Added site name to vertical layout +- `pages/devtool/config/site-settings/index.vue` - Enhanced watchers and debugging + +## 2. Button Styling Standardization 🎨 + +**Problem**: Inconsistent button styling across tabs + +**Standardization Applied**: +- ✅ All upload buttons: `variant="outline" size="sm"` +- ✅ Save Changes button: `variant="primary" size="sm"` +- ✅ Reset button: `variant="outline" size="sm"` +- ✅ Apply Font button: `variant="outline" size="sm"` (changed from primary) + +**Consistent Button Pattern**: +```vue + + + Action Text + +``` + +**Files Modified**: +- `pages/devtool/config/site-settings/index.vue` - Standardized Apply Font button + +## 3. Site Settings Description Padding 📝 + +**Problem**: "Configure your platform's branding, appearance, SEO, and functionality" text had improper padding for two lines + +**Fix Applied**: +- ✅ Added `leading-relaxed` class for better line height +- ✅ Improved text readability and spacing + +**Before**: +```vue +

Configure your platform's branding, appearance, SEO, and functionality.

+``` + +**After**: +```vue +

Configure your platform's branding, appearance, SEO, and functionality.

+``` + +**Files Modified**: +- `pages/devtool/config/site-settings/index.vue` - Enhanced info card description + +## Additional Improvements 🚀 + +### Enhanced Toggle Functionality +- ✅ Real-time toggle updates without requiring save +- ✅ Immediate sync between settings page and global state +- ✅ Both header and sidemenu respect the toggle setting + +### Debug Information +- ✅ Added debug panel in live preview showing: + - Current site name + - Toggle state (Yes/No) + - Font size in pixels +- ✅ Helps troubleshoot configuration issues + +### Header Logic Improvements +- ✅ Site name now shows in both vertical and horizontal layouts +- ✅ Proper font size scaling in sidemenu (65% of header size) +- ✅ Automatic site settings loading on component mount + +## Testing Verification ✅ + +**Header Display Test**: +1. ✅ Site name appears in vertical layout (default) +2. ✅ Site name appears in horizontal layout +3. ✅ Toggle OFF hides name in both layouts +4. ✅ Toggle ON shows name in both layouts +5. ✅ Font size applies correctly +6. ✅ Changes are immediate + +**Button Consistency Test**: +1. ✅ All upload buttons use outline variant +2. ✅ Save button uses primary variant +3. ✅ Reset button uses outline variant +4. ✅ All buttons have consistent size (sm) +5. ✅ Icons are properly positioned with mr-1 + +**Description Styling Test**: +1. ✅ Text has proper line height for readability +2. ✅ Padding appears natural for two-line content +3. ✅ Dark mode compatibility maintained + +## Files Changed Summary 📁 + +1. **components/layouts/Header.vue** + - Added site name to vertical layout + - Enhanced site settings loading + - Improved responsive layout handling + +2. **pages/devtool/config/site-settings/index.vue** + - Standardized button variants and sizes + - Added debug information panel + - Enhanced toggle watching and real-time updates + - Improved description line height + - Fixed immediate change application + +## Next Steps 🎯 + +1. Test the site settings page at `/devtool/config/site-settings` +2. Verify header displays site name when toggle is enabled +3. Check that all buttons follow consistent styling +4. Confirm description text has proper spacing +5. Use debug panel to troubleshoot any remaining issues \ No newline at end of file diff --git a/formkit.config.js b/formkit.config.js new file mode 100644 index 0000000..9d4b5a5 --- /dev/null +++ b/formkit.config.js @@ -0,0 +1,49 @@ +import { generateClasses } from "@formkit/themes"; +import defaultTheme from "@/assets/js/formkit-theme.js"; +import customInput from "@/assets/js/formkit-custom.js"; + +// ----------------- Addons --------------------------- // + +// https://formkit.com/plugins/auto-animate +import { createAutoAnimatePlugin } from "@formkit/addons"; + +// https://formkit.com/plugins/floating-labels +import { createFloatingLabelsPlugin } from "@formkit/addons"; +import "@formkit/addons/css/floatingLabels"; + +// https://formkit.com/plugins/multi-step +import { createMultiStepPlugin } from "@formkit/addons"; +import "@formkit/addons/css/multistep"; + +// https://formkit.com/plugins/auto-height-textarea +import { createAutoHeightTextareaPlugin } from "@formkit/addons"; + +// https://formkit.com/plugins/local-storage +import { createLocalStoragePlugin } from "@formkit/addons"; + +export default { + plugins: [ + createFloatingLabelsPlugin({ + useAsDefault: false, // defaults to false + }), + createAutoAnimatePlugin({ + // optional config + }), + createMultiStepPlugin(), + createAutoHeightTextareaPlugin(), + createLocalStoragePlugin({ + // plugin defaults: + prefix: "formkit", + key: undefined, + control: undefined, + maxAge: 3600000, // 1 hour + debounce: 200, + beforeSave: undefined, + beforeLoad: undefined, + }), + ], + config: { + classes: generateClasses(defaultTheme), + }, + inputs: customInput, +}; diff --git a/layouts/default.vue b/layouts/default.vue new file mode 100644 index 0000000..0f04a1d --- /dev/null +++ b/layouts/default.vue @@ -0,0 +1,11 @@ + + + diff --git a/layouts/empty.vue b/layouts/empty.vue new file mode 100644 index 0000000..8bc6805 --- /dev/null +++ b/layouts/empty.vue @@ -0,0 +1,7 @@ + + + diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..4a124c6 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,26 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + const { $swal } = useNuxtApp(); + + if (process.client) { + // Validate every request to every page + const { data: validateUser } = await useFetch("/api/auth/validate", { + method: "GET", + }); + + // If user is not logged in, redirect to logout page + if (validateUser.value.statusCode === 401) { + $swal + .fire({ + title: "Session Expired", + text: "Your session has expired. Please login again.", + icon: "warning", + confirmButtonText: "OK", + }) + .then(() => { + return window.location.replace("/logout"); + }); + } + + return true; + } +}); diff --git a/middleware/dashboard.js b/middleware/dashboard.js new file mode 100644 index 0000000..08091f2 --- /dev/null +++ b/middleware/dashboard.js @@ -0,0 +1,11 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + // Validate every request to every page + const { data: validateUser } = await useFetch("/api/auth/validate", { + method: "GET", + }); + + // If user is not logged in, redirect to logout page + if (validateUser.value.statusCode === 401) return true; + + return navigateTo("/dashboard"); +}); diff --git a/middleware/forbidden.js b/middleware/forbidden.js new file mode 100644 index 0000000..da4fe40 --- /dev/null +++ b/middleware/forbidden.js @@ -0,0 +1,4 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + // Throw error to page 404 not found + throw new Error("Forbidden"); +}); diff --git a/middleware/main.js b/middleware/main.js new file mode 100644 index 0000000..b70e2fa --- /dev/null +++ b/middleware/main.js @@ -0,0 +1,3 @@ +export default defineNuxtRouteMiddleware((to, from) => { + return navigateTo("/login"); +}); \ No newline at end of file diff --git a/navigation/index.js b/navigation/index.js new file mode 100644 index 0000000..c542d5b --- /dev/null +++ b/navigation/index.js @@ -0,0 +1,94 @@ +export default [ + { + "header": "Utama", + "description": "", + "child": [ + { + "title": "Dashboard", + "path": "/dashboard", + "icon": "ic:outline-dashboard", + "child": [], + "meta": {} + } + ], + "meta": {} + }, + { + "header": "Pentadbiran", + "description": "Urus aplikasi anda", + "child": [ + { + "title": "API Platform", + "path": "/api-platform", + "icon": "", + "child": [] + }, + { + "title": "Konfigurasi", + "icon": "ic:outline-settings", + "child": [ + { + "title": "Persekitaran", + "path": "/devtool/config/environment" + }, + { + "title": "Site Settings", + "path": "/devtool/config/site-settings" + } + ] + }, + { + "title": "Penyunting Menu", + "icon": "ci:menu-alt-03", + "path": "/devtool/menu-editor", + "child": [] + }, + { + "title": "Urus Pengguna", + "path": "/devtool/user-management", + "icon": "ph:user-circle-gear", + "child": [ + { + "title": "Senarai Pengguna", + "path": "/devtool/user-management/user", + "icon": "", + "child": [] + }, + { + "title": "Senarai Peranan", + "path": "/devtool/user-management/role", + "icon": "", + "child": [] + } + ] + }, + { + "title": "Kandungan", + "icon": "mdi:pencil-ruler", + "child": [ + { + "title": "Penyunting", + "path": "/devtool/content-editor" + }, + { + "title": "Templat", + "path": "/devtool/content-editor/template" + } + ] + }, + { + "title": "Penyunting API", + "path": "/devtool/api-editor", + "icon": "material-symbols:api-rounded", + "child": [] + } + ], + "meta": { + "auth": { + "role": [ + "Developer" + ] + } + } + } +]; \ No newline at end of file diff --git a/nuxt.config.js b/nuxt.config.js new file mode 100644 index 0000000..6071dd0 --- /dev/null +++ b/nuxt.config.js @@ -0,0 +1,552 @@ +// https://v3.nuxtjs.org/api/configuration/nuxt.config +export default defineNuxtConfig({ + runtimeConfig: { + auth: { + secretAccess: process.env.NUXT_ACCESS_TOKEN_SECRET, + secretRefresh: process.env.NUXT_REFRESH_TOKEN_SECRET, + }, + metabase: { + secretKey: process.env.NUXT_METABASE_SECRET_KEY || "c98a5b005450e699b6d420f46e0062912ac75268716f1298c11d8bb11c291eb0", + siteUrl: process.env.NUXT_METABASE_SITE_URL || "http://mb.sena.my", + }, + }, + modules: [ + "@nuxtjs/tailwindcss", + "@formkit/nuxt", + "@vite-pwa/nuxt", + "@vueuse/nuxt", + "floating-vue/nuxt", + "@pinia/nuxt", + "@pinia-plugin-persistedstate/nuxt", + "nuxt-security", + "nuxt-typed-router", + "nuxt-icon", + "@davestewart/nuxt-scrollbar", + ], + app: { + pageTransition: { name: "page", mode: "out-in" }, + head: { + viewport: "width=device-width,initial-scale=1", + title: "corradAF", + titleTemplate: "%s - corradAF", + meta: [ + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { name: "description", content: "corradAF Admin Portal" }, + { name: "apple-mobile-web-app-title", content: "corradAF" }, + ], + }, + }, + css: ["~/assets/style/scss/main.scss"], + tailwindcss: { + cssPath: "~/assets/style/css/tailwind.css", + configPath: "tailwind.config", + exposeConfig: false, + config: {}, + injectPosition: 0, + viewer: false, + }, + formkit: { + defaultConfig: true, + }, + pwa: { + registerType: "autoUpdate", + workbox: { + navigateFallback: "/", + globPatterns: [ + "**/*.{js,json,css,html,txt,svg,png,ico,webp,woff,woff2,ttf,eot,otf,wasm}", + ], + }, + devOptions: { + enabled: false, + type: "module", + }, + meta: { + name: "corradAF", + author: "Corrad Software", + description: "corradAF Admin Portal", + theme_color: "#f3586a", + lang: "en", + }, + manifest: { + name: "corradAF", + short_name: "corradAF", + description: "corradAF Admin Portal", + start_url: "/", + display: "standalone", + theme_color: "#00A59A", + background_color: "#FAFAFA", + scope: "./", + icons: [ + { + src: "icons/windows11/SmallTile.scale-100.png", + sizes: "71x71", + }, + { + src: "icons/windows11/SmallTile.scale-125.png", + sizes: "89x89", + }, + { + src: "icons/windows11/SmallTile.scale-150.png", + sizes: "107x107", + }, + { + src: "icons/windows11/SmallTile.scale-200.png", + sizes: "142x142", + }, + { + src: "icons/windows11/SmallTile.scale-400.png", + sizes: "284x284", + }, + { + src: "icons/windows11/Square150x150Logo.scale-100.png", + sizes: "150x150", + }, + { + src: "icons/windows11/Square150x150Logo.scale-125.png", + sizes: "188x188", + }, + { + src: "icons/windows11/Square150x150Logo.scale-150.png", + sizes: "225x225", + }, + { + src: "icons/windows11/Square150x150Logo.scale-200.png", + sizes: "300x300", + }, + { + src: "icons/windows11/Square150x150Logo.scale-400.png", + sizes: "600x600", + }, + { + src: "icons/windows11/Wide310x150Logo.scale-100.png", + sizes: "310x150", + }, + { + src: "icons/windows11/Wide310x150Logo.scale-125.png", + sizes: "388x188", + }, + { + src: "icons/windows11/Wide310x150Logo.scale-150.png", + sizes: "465x225", + }, + { + src: "icons/windows11/Wide310x150Logo.scale-200.png", + sizes: "620x300", + }, + { + src: "icons/windows11/Wide310x150Logo.scale-400.png", + sizes: "1240x600", + }, + { + src: "icons/windows11/LargeTile.scale-100.png", + sizes: "310x310", + }, + { + src: "icons/windows11/LargeTile.scale-125.png", + sizes: "388x388", + }, + { + src: "icons/windows11/LargeTile.scale-150.png", + sizes: "465x465", + }, + { + src: "icons/windows11/LargeTile.scale-200.png", + sizes: "620x620", + }, + { + src: "icons/windows11/LargeTile.scale-400.png", + sizes: "1240x1240", + }, + { + src: "icons/windows11/Square44x44Logo.scale-100.png", + sizes: "44x44", + }, + { + src: "icons/windows11/Square44x44Logo.scale-125.png", + sizes: "55x55", + }, + { + src: "icons/windows11/Square44x44Logo.scale-150.png", + sizes: "66x66", + }, + { + src: "icons/windows11/Square44x44Logo.scale-200.png", + sizes: "88x88", + }, + { + src: "icons/windows11/Square44x44Logo.scale-400.png", + sizes: "176x176", + }, + { + src: "icons/windows11/StoreLogo.scale-100.png", + sizes: "50x50", + }, + { + src: "icons/windows11/StoreLogo.scale-125.png", + sizes: "63x63", + }, + { + src: "icons/windows11/StoreLogo.scale-150.png", + sizes: "75x75", + }, + { + src: "icons/windows11/StoreLogo.scale-200.png", + sizes: "100x100", + }, + { + src: "icons/windows11/StoreLogo.scale-400.png", + sizes: "200x200", + }, + { + src: "icons/windows11/SplashScreen.scale-100.png", + sizes: "620x300", + }, + { + src: "icons/windows11/SplashScreen.scale-125.png", + sizes: "775x375", + }, + { + src: "icons/windows11/SplashScreen.scale-150.png", + sizes: "930x450", + }, + { + src: "icons/windows11/SplashScreen.scale-200.png", + sizes: "1240x600", + }, + { + src: "icons/windows11/SplashScreen.scale-400.png", + sizes: "2480x1200", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-16.png", + sizes: "16x16", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-20.png", + sizes: "20x20", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-24.png", + sizes: "24x24", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-30.png", + sizes: "30x30", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-32.png", + sizes: "32x32", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-36.png", + sizes: "36x36", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-40.png", + sizes: "40x40", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-44.png", + sizes: "44x44", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-48.png", + sizes: "48x48", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-60.png", + sizes: "60x60", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-64.png", + sizes: "64x64", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-72.png", + sizes: "72x72", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-80.png", + sizes: "80x80", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-96.png", + sizes: "96x96", + }, + { + src: "icons/windows11/Square44x44Logo.targetsize-256.png", + sizes: "256x256", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png", + sizes: "16x16", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png", + sizes: "20x20", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png", + sizes: "24x24", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png", + sizes: "30x30", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png", + sizes: "32x32", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png", + sizes: "36x36", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png", + sizes: "40x40", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png", + sizes: "44x44", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png", + sizes: "48x48", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png", + sizes: "60x60", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png", + sizes: "64x64", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png", + sizes: "72x72", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png", + sizes: "80x80", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png", + sizes: "96x96", + }, + { + src: "icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png", + sizes: "256x256", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png", + sizes: "16x16", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png", + sizes: "20x20", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png", + sizes: "24x24", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png", + sizes: "30x30", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png", + sizes: "32x32", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png", + sizes: "36x36", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png", + sizes: "40x40", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png", + sizes: "44x44", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png", + sizes: "48x48", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png", + sizes: "60x60", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png", + sizes: "64x64", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png", + sizes: "72x72", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png", + sizes: "80x80", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png", + sizes: "96x96", + }, + { + src: "icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png", + sizes: "256x256", + }, + { + src: "icons/android/android-launchericon-512-512.png", + sizes: "512x512", + }, + { + src: "icons/android/android-launchericon-192-192.png", + sizes: "192x192", + }, + { + src: "icons/android/android-launchericon-144-144.png", + sizes: "144x144", + }, + { + src: "icons/android/android-launchericon-96-96.png", + sizes: "96x96", + }, + { + src: "icons/android/android-launchericon-72-72.png", + sizes: "72x72", + }, + { + src: "icons/android/android-launchericon-48-48.png", + sizes: "48x48", + }, + { + src: "icons/ios/16.png", + sizes: "16x16", + }, + { + src: "icons/ios/20.png", + sizes: "20x20", + }, + { + src: "icons/ios/29.png", + sizes: "29x29", + }, + { + src: "icons/ios/32.png", + sizes: "32x32", + }, + { + src: "icons/ios/40.png", + sizes: "40x40", + }, + { + src: "icons/ios/50.png", + sizes: "50x50", + }, + { + src: "icons/ios/57.png", + sizes: "57x57", + }, + { + src: "icons/ios/58.png", + sizes: "58x58", + }, + { + src: "icons/ios/60.png", + sizes: "60x60", + }, + { + src: "icons/ios/64.png", + sizes: "64x64", + }, + { + src: "icons/ios/72.png", + sizes: "72x72", + }, + { + src: "icons/ios/76.png", + sizes: "76x76", + }, + { + src: "icons/ios/80.png", + sizes: "80x80", + }, + { + src: "icons/ios/87.png", + sizes: "87x87", + }, + { + src: "icons/ios/100.png", + sizes: "100x100", + }, + { + src: "icons/ios/114.png", + sizes: "114x114", + }, + { + src: "icons/ios/120.png", + sizes: "120x120", + }, + { + src: "icons/ios/128.png", + sizes: "128x128", + }, + { + src: "icons/ios/144.png", + sizes: "144x144", + }, + { + src: "icons/ios/152.png", + sizes: "152x152", + }, + { + src: "icons/ios/167.png", + sizes: "167x167", + }, + { + src: "icons/ios/180.png", + sizes: "180x180", + }, + { + src: "icons/ios/192.png", + sizes: "192x192", + }, + { + src: "icons/ios/256.png", + sizes: "256x256", + }, + { + src: "icons/ios/512.png", + sizes: "512x512", + }, + { + src: "icons/ios/1024.png", + sizes: "1024x1024", + }, + ], + }, + }, + security: { + // GLOBAL SECURITY OPTIONS + + // Allow 200 requests per hour (the Twitter search limit). Also understands + // 'second', 'minute', 'day', or a number of milliseconds + rateLimiter: { + tokensPerInterval: 200, + interval: "minute", + fireImmediately: false, + throwError: true, // optional + }, + headers: false, + }, + routeRules: { + "/api/devtool/**": { + security: { + xssValidator: false, + requestSizeLimiter: false, + }, + }, + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..269d72b --- /dev/null +++ b/package.json @@ -0,0 +1,85 @@ +{ + "private": true, + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare", + "prisma": "npx prisma db pull && npx prisma generate && nuxt dev" + }, + "devDependencies": { + "@nuxtjs/tailwindcss": "^6.8.0", + "@pinia-plugin-persistedstate/nuxt": "^1.1.1", + "@vite-pwa/nuxt": "^0.1.0", + "eslint": "^8.39.0", + "eslint-plugin-vue": "^9.16.1", + "nuxt": "^3.6.5", + "nuxt-icon": "^0.1.7", + "nuxt-security": "^0.13.0", + "nuxt-typed-router": "^3.2.5", + "postcss-import": "^15.1.0" + }, + "dependencies": { + "@babel/eslint-parser": "^7.19.1", + "@codemirror/lang-html": "^6.4.3", + "@codemirror/lang-javascript": "^6.1.6", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/theme-one-dark": "^6.1.2", + "@davestewart/nuxt-scrollbar": "^1.0.0", + "@formkit/addons": "^1.0.0", + "@formkit/auto-animate": "^0.7.0", + "@formkit/nuxt": "^1.0.0", + "@formkit/pro": "^0.115.3", + "@formkit/themes": "^1.0.0", + "@fullcalendar/core": "^5.11.3", + "@fullcalendar/daygrid": "^5.11.3", + "@fullcalendar/interaction": "^5.11.3", + "@fullcalendar/list": "^5.11.3", + "@fullcalendar/luxon2": "^5.11.3", + "@fullcalendar/scrollgrid": "^5.11.3", + "@fullcalendar/timegrid": "^5.11.3", + "@fullcalendar/vue3": "^5.11.2", + "@kiwicom/eslint-config": "^12.7.3", + "@pinia/nuxt": "^0.4.11", + "@popperjs/core": "^2.11.8", + "@prisma/client": "6.8.2", + "@shimyshack/uid": "^0.1.7", + "@sweetalert2/theme-dark": "^5.0.14", + "@vueup/vue-quill": "^1.0.0", + "@vueuse/core": "^9.5.0", + "@vueuse/nuxt": "^9.5.0", + "apexcharts": "^3.36.0", + "chart.js": "^3.9.1", + "codemirror": "^6.0.1", + "cross-env": "^7.0.3", + "crypto-js": "^4.1.1", + "floating-vue": "^2.0.0-beta.24", + "jsonwebtoken": "^8.5.1", + "jspdf": "^2.5.1", + "luxon": "^3.1.0", + "maska": "^1.5.0", + "pinia": "^2.1.6", + "prettier": "^2.8.1", + "prisma": "6.8.2", + "sass": "^1.62.0", + "swiper": "^8.4.4", + "thememirror": "^2.0.1", + "uuid": "^10.0.0", + "v-calendar": "^3.0.3", + "vue-chart-3": "^3.1.8", + "vue-code-highlight": "^0.7.8", + "vue-codemirror": "^6.1.1", + "vue-country-flag-next": "^2.3.2", + "vue-fullscreen": "^3.1.1", + "vue-select": "^4.0.0-beta.5", + "vue-sweetalert2": "^5.0.5", + "vue-toastification": "^2.0.0-rc.5", + "vue-window-size": "^1.0.8", + "vue3-apexcharts": "^1.4.1", + "vue3-click-away": "^1.2.4", + "vue3-dropzone": "^2.0.1", + "vue3-recaptcha-v2": "^2.0.2", + "vuedraggable": "^4.1.0" + } +} diff --git a/pages/api-docs/index.vue b/pages/api-docs/index.vue new file mode 100644 index 0000000..7a37661 --- /dev/null +++ b/pages/api-docs/index.vue @@ -0,0 +1,1381 @@ + + + + + \ No newline at end of file diff --git a/pages/api-platform/index.vue b/pages/api-platform/index.vue new file mode 100644 index 0000000..b6e57a1 --- /dev/null +++ b/pages/api-platform/index.vue @@ -0,0 +1,255 @@ + + + + + \ No newline at end of file diff --git a/pages/dashboard/index.vue b/pages/dashboard/index.vue new file mode 100644 index 0000000..2484307 --- /dev/null +++ b/pages/dashboard/index.vue @@ -0,0 +1,315 @@ + + + diff --git a/pages/devtool/api-editor/code/index.vue b/pages/devtool/api-editor/code/index.vue new file mode 100644 index 0000000..d7a2ee0 --- /dev/null +++ b/pages/devtool/api-editor/code/index.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/pages/devtool/api-editor/index.vue b/pages/devtool/api-editor/index.vue new file mode 100644 index 0000000..de8d722 --- /dev/null +++ b/pages/devtool/api-editor/index.vue @@ -0,0 +1,327 @@ + + \ No newline at end of file diff --git a/pages/devtool/code-playground/index.js b/pages/devtool/code-playground/index.js new file mode 100644 index 0000000..fc98745 --- /dev/null +++ b/pages/devtool/code-playground/index.js @@ -0,0 +1,35 @@ +import RsAlert from "../../../components/RsAlert.vue"; +import RsBadge from "../../../components/RsBadge.vue"; +import RsButton from "../../../components/RsButton.vue"; +import RsCard from "../../../components/RsCard.vue"; +import RsCodeMirror from "../../../components/RsCodeMirror.vue"; +import RsCollapse from "../../../components/RsCollapse.vue"; +import RsCollapseItem from "../../../components/RsCollapseItem.vue"; +import RsDropdown from "../../../components/RsDropdown.vue"; +import RsDropdownItem from "../../../components/RsDropdownItem.vue"; +import RsFieldset from "../../../components/RsFieldset.vue"; +import RsModal from "../../../components/RsModal.vue"; +import RsProgressBar from "../../../components/RsProgressBar.vue"; +import RsTab from "../../../components/RsTab.vue"; +import RsTabItem from "../../../components/RsTabItem.vue"; +import RsTable from "../../../components/RsTable.vue"; +import RsWizard from "../../../components/RsWizard.vue"; + +export { + RsAlert, + RsBadge, + RsButton, + RsCard, + RsCodeMirror, + RsCollapse, + RsCollapseItem, + RsDropdown, + RsDropdownItem, + RsFieldset, + RsModal, + RsProgressBar, + RsTab, + RsTabItem, + RsTable, + RsWizard, +}; diff --git a/pages/devtool/code-playground/index.vue b/pages/devtool/code-playground/index.vue new file mode 100644 index 0000000..f31e8e2 --- /dev/null +++ b/pages/devtool/code-playground/index.vue @@ -0,0 +1,422 @@ + + + + diff --git a/pages/devtool/config/application-log/index.vue b/pages/devtool/config/application-log/index.vue new file mode 100644 index 0000000..677f24b --- /dev/null +++ b/pages/devtool/config/application-log/index.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/pages/devtool/config/environment/index.vue b/pages/devtool/config/environment/index.vue new file mode 100644 index 0000000..77bcd0a --- /dev/null +++ b/pages/devtool/config/environment/index.vue @@ -0,0 +1,27 @@ + + + diff --git a/pages/devtool/config/index.vue b/pages/devtool/config/index.vue new file mode 100644 index 0000000..677f24b --- /dev/null +++ b/pages/devtool/config/index.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/pages/devtool/config/site-settings/index.vue b/pages/devtool/config/site-settings/index.vue new file mode 100644 index 0000000..05f2139 --- /dev/null +++ b/pages/devtool/config/site-settings/index.vue @@ -0,0 +1,1108 @@ + + + + + \ No newline at end of file diff --git a/pages/devtool/content-editor/code/index.vue b/pages/devtool/content-editor/code/index.vue new file mode 100644 index 0000000..223aea6 --- /dev/null +++ b/pages/devtool/content-editor/code/index.vue @@ -0,0 +1,226 @@ + + + diff --git a/pages/devtool/content-editor/index.vue b/pages/devtool/content-editor/index.vue new file mode 100644 index 0000000..884e783 --- /dev/null +++ b/pages/devtool/content-editor/index.vue @@ -0,0 +1,247 @@ + + + + diff --git a/pages/devtool/content-editor/template/index.vue b/pages/devtool/content-editor/template/index.vue new file mode 100644 index 0000000..8b18c25 --- /dev/null +++ b/pages/devtool/content-editor/template/index.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/pages/devtool/content-editor/template/view/[id]/index.vue b/pages/devtool/content-editor/template/view/[id]/index.vue new file mode 100644 index 0000000..e332bda --- /dev/null +++ b/pages/devtool/content-editor/template/view/[id]/index.vue @@ -0,0 +1,33 @@ + + + diff --git a/pages/devtool/menu-editor/index.vue b/pages/devtool/menu-editor/index.vue new file mode 100644 index 0000000..b27d732 --- /dev/null +++ b/pages/devtool/menu-editor/index.vue @@ -0,0 +1,761 @@ + + + \ No newline at end of file diff --git a/pages/devtool/orm/index.vue b/pages/devtool/orm/index.vue new file mode 100644 index 0000000..7ab6ae1 --- /dev/null +++ b/pages/devtool/orm/index.vue @@ -0,0 +1,169 @@ + + + diff --git a/pages/devtool/orm/table/create/index.vue b/pages/devtool/orm/table/create/index.vue new file mode 100644 index 0000000..13bdac5 --- /dev/null +++ b/pages/devtool/orm/table/create/index.vue @@ -0,0 +1,315 @@ + + + diff --git a/pages/devtool/orm/table/modify/[table].vue b/pages/devtool/orm/table/modify/[table].vue new file mode 100644 index 0000000..b2e8855 --- /dev/null +++ b/pages/devtool/orm/table/modify/[table].vue @@ -0,0 +1,360 @@ + + + diff --git a/pages/devtool/orm/view/[table]/index.vue b/pages/devtool/orm/view/[table]/index.vue new file mode 100644 index 0000000..6d984cc --- /dev/null +++ b/pages/devtool/orm/view/[table]/index.vue @@ -0,0 +1,79 @@ + + + diff --git a/pages/devtool/user-management/role/index.vue b/pages/devtool/user-management/role/index.vue new file mode 100644 index 0000000..74212ef --- /dev/null +++ b/pages/devtool/user-management/role/index.vue @@ -0,0 +1,499 @@ + + diff --git a/pages/devtool/user-management/user/index.vue b/pages/devtool/user-management/user/index.vue new file mode 100644 index 0000000..b157201 --- /dev/null +++ b/pages/devtool/user-management/user/index.vue @@ -0,0 +1,497 @@ + + diff --git a/pages/forgot-password/index.vue b/pages/forgot-password/index.vue new file mode 100644 index 0000000..445bad3 --- /dev/null +++ b/pages/forgot-password/index.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..0cf6a06 --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,10 @@ + + + diff --git a/pages/login/index.vue b/pages/login/index.vue new file mode 100644 index 0000000..d7090d5 --- /dev/null +++ b/pages/login/index.vue @@ -0,0 +1,201 @@ + + + diff --git a/pages/logout/index.vue b/pages/logout/index.vue new file mode 100644 index 0000000..6bc920b --- /dev/null +++ b/pages/logout/index.vue @@ -0,0 +1,28 @@ + + + diff --git a/pages/oauth/callback.vue b/pages/oauth/callback.vue new file mode 100644 index 0000000..1ac8b93 --- /dev/null +++ b/pages/oauth/callback.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/pages/register/index.vue b/pages/register/index.vue new file mode 100644 index 0000000..030d813 --- /dev/null +++ b/pages/register/index.vue @@ -0,0 +1,241 @@ + + + diff --git a/plugins/apex-chart.client.js b/plugins/apex-chart.client.js new file mode 100644 index 0000000..343d647 --- /dev/null +++ b/plugins/apex-chart.client.js @@ -0,0 +1,5 @@ +import VueApexCharts from "vue3-apexcharts"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("VueApexCharts", VueApexCharts); +}); diff --git a/plugins/chart-js.client.js b/plugins/chart-js.client.js new file mode 100644 index 0000000..e9eda5b --- /dev/null +++ b/plugins/chart-js.client.js @@ -0,0 +1,7 @@ +// Workaround because chart.js doesn't provide an default export +import * as ChartJs from "chart.js"; +const { Chart, registerables } = ChartJs; + +export default defineNuxtPlugin(() => { + Chart.register(...registerables); +}); diff --git a/plugins/formkit-auto-animate.js b/plugins/formkit-auto-animate.js new file mode 100644 index 0000000..37dd775 --- /dev/null +++ b/plugins/formkit-auto-animate.js @@ -0,0 +1,5 @@ +import { autoAnimatePlugin } from "@formkit/auto-animate/vue"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(autoAnimatePlugin); +}); diff --git a/plugins/full-calendar.client.js b/plugins/full-calendar.client.js new file mode 100644 index 0000000..986bf69 --- /dev/null +++ b/plugins/full-calendar.client.js @@ -0,0 +1,27 @@ +import "@fullcalendar/core/vdom"; // solve problem with Vite +import FullCalendar from "@fullcalendar/vue3"; +import interactionPlugin from "@fullcalendar/interaction"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import listPlugin from "@fullcalendar/list"; +import scrollGrid from "@fullcalendar/scrollgrid"; +import luxon2Plugin from "@fullcalendar/luxon2"; + +FullCalendar.options = { + plugins: [ + interactionPlugin, + dayGridPlugin, + timeGridPlugin, + listPlugin, + scrollGrid, + luxon2Plugin, + ], +}; + +export default defineNuxtPlugin((/* nuxtApp */) => { + return { + provide: { + FullCalendar, + }, + }; +}); diff --git a/plugins/maska.js b/plugins/maska.js new file mode 100644 index 0000000..169859f --- /dev/null +++ b/plugins/maska.js @@ -0,0 +1,5 @@ +import Maska from "maska"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(Maska); +}); diff --git a/plugins/recaptcha.client.js b/plugins/recaptcha.client.js new file mode 100644 index 0000000..5e072bf --- /dev/null +++ b/plugins/recaptcha.client.js @@ -0,0 +1,8 @@ +import { install } from "vue3-recaptcha-v2"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(install, { + sitekey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI", + cnDomains: false, + }); +}); diff --git a/plugins/site-settings.client.js b/plugins/site-settings.client.js new file mode 100644 index 0000000..8f82207 --- /dev/null +++ b/plugins/site-settings.client.js @@ -0,0 +1,9 @@ +export default defineNuxtPlugin(async () => { + // Only run on client side + if (process.client) { + const { loadSiteSettings } = useSiteSettings(); + + // Load site settings on app initialization + await loadSiteSettings(); + } +}); \ No newline at end of file diff --git a/plugins/swiper.js b/plugins/swiper.js new file mode 100644 index 0000000..722898a --- /dev/null +++ b/plugins/swiper.js @@ -0,0 +1,7 @@ +import { Swiper, SwiperSlide } from "swiper/vue"; +import "swiper/css/bundle"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("Swiper", Swiper); + nuxtApp.vueApp.component("SwiperSlide", SwiperSlide); +}); diff --git a/plugins/uid-plugin.js b/plugins/uid-plugin.js new file mode 100644 index 0000000..24e036d --- /dev/null +++ b/plugins/uid-plugin.js @@ -0,0 +1,5 @@ +import { UidPlugin } from "@shimyshack/uid"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(UidPlugin); +}); diff --git a/plugins/v-select.js b/plugins/v-select.js new file mode 100644 index 0000000..ede656d --- /dev/null +++ b/plugins/v-select.js @@ -0,0 +1,6 @@ +import vSelect from "vue-select"; +import "vue-select/dist/vue-select.css"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("v-select", vSelect); +}); diff --git a/plugins/vue-calendar.js b/plugins/vue-calendar.js new file mode 100644 index 0000000..eb1c2f5 --- /dev/null +++ b/plugins/vue-calendar.js @@ -0,0 +1,5 @@ +import VCalendar from "v-calendar"; +import "v-calendar/style.css"; +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(VCalendar); +}); diff --git a/plugins/vue-code-highlight.js b/plugins/vue-code-highlight.js new file mode 100644 index 0000000..ef0ec1f --- /dev/null +++ b/plugins/vue-code-highlight.js @@ -0,0 +1,8 @@ +import VueCodeHighlight from "vue-code-highlight"; + +import "vue-code-highlight/themes/prism-okaidia.css"; +import "vue-code-highlight/themes/window.css"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(VueCodeHighlight); +}); diff --git a/plugins/vue-codemirror.js b/plugins/vue-codemirror.js new file mode 100644 index 0000000..82838fc --- /dev/null +++ b/plugins/vue-codemirror.js @@ -0,0 +1,6 @@ +import { Codemirror } from "vue-codemirror"; + +// Install plugin +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("CodeMirror", Codemirror); +}); diff --git a/plugins/vue-country-flag.js b/plugins/vue-country-flag.js new file mode 100644 index 0000000..c0e9a9b --- /dev/null +++ b/plugins/vue-country-flag.js @@ -0,0 +1,7 @@ +// Import vue 3 plugin here +import CountryFlag from "vue-country-flag-next"; + +// Install the plugin +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("CountryFlag", CountryFlag); +}); diff --git a/plugins/vue-draggable.client.js b/plugins/vue-draggable.client.js new file mode 100644 index 0000000..58d50a0 --- /dev/null +++ b/plugins/vue-draggable.client.js @@ -0,0 +1,5 @@ +import draggable from "vuedraggable"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("draggable", draggable); +}); diff --git a/plugins/vue-fullscreen.js b/plugins/vue-fullscreen.js new file mode 100644 index 0000000..7b50e15 --- /dev/null +++ b/plugins/vue-fullscreen.js @@ -0,0 +1,5 @@ +import VueFullscreen from "vue-fullscreen"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(VueFullscreen); +}); diff --git a/plugins/vue-quill.client.js b/plugins/vue-quill.client.js new file mode 100644 index 0000000..db49b28 --- /dev/null +++ b/plugins/vue-quill.client.js @@ -0,0 +1,7 @@ +import { QuillEditor } from "@vueup/vue-quill"; +import "@vueup/vue-quill/dist/vue-quill.snow.css"; // Add css for snow theme +// import '@vueup/vue-quill/dist/vue-quill.bubble.css'; // for bubble theme + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("QuillEditor", QuillEditor); +}); diff --git a/plugins/vue-sweetalert2.js b/plugins/vue-sweetalert2.js new file mode 100644 index 0000000..0297888 --- /dev/null +++ b/plugins/vue-sweetalert2.js @@ -0,0 +1,6 @@ +import Swal from "sweetalert2"; +import "sweetalert2/dist/sweetalert2.min.css"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.provide("swal", Swal); +}); diff --git a/plugins/vue-toastification.client.js b/plugins/vue-toastification.client.js new file mode 100644 index 0000000..ca46118 --- /dev/null +++ b/plugins/vue-toastification.client.js @@ -0,0 +1,12 @@ +import Toast from "vue-toastification"; +import "vue-toastification/dist/index.css"; + +export default defineNuxtPlugin((nuxtApp) => { + const options = { + transition: "Vue-Toastification__bounce", + maxToasts: 20, + newestOnTop: true, + }; + + nuxtApp.vueApp.use(Toast, options); +}); diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..a5a6ff8 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + "postcss-import": {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/prisma/migrations/20230808013656_initialize/migration.sql b/prisma/migrations/20230808013656_initialize/migration.sql new file mode 100644 index 0000000..c611a65 --- /dev/null +++ b/prisma/migrations/20230808013656_initialize/migration.sql @@ -0,0 +1,72 @@ +-- CreateTable +CREATE TABLE `audit` ( + `auditID` INTEGER NOT NULL AUTO_INCREMENT, + `auditIP` VARCHAR(255) NULL, + `auditURL` VARCHAR(255) NULL, + `auditURLMethod` VARCHAR(255) NULL, + `auditURLPayload` VARCHAR(255) NULL, + `auditCreatedDate` DATETIME(0) NULL, + + PRIMARY KEY (`auditID`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user` ( + `userID` INTEGER NOT NULL AUTO_INCREMENT, + `userSecretKey` VARCHAR(255) NULL, + `userUsername` VARCHAR(255) NULL, + `userPassword` VARCHAR(255) NULL, + `userFullName` VARCHAR(255) NULL, + `userEmail` VARCHAR(255) NULL, + `userPhone` VARCHAR(255) NULL, + `userStatus` VARCHAR(255) NULL, + `userCreatedDate` DATETIME(0) NULL, + `userModifiedDate` DATETIME(0) NULL, + + PRIMARY KEY (`userID`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `role` ( + `roleID` INTEGER NOT NULL AUTO_INCREMENT, + `roleName` VARCHAR(255) NULL, + `roleDescription` VARCHAR(255) NULL, + `roleStatus` VARCHAR(255) NULL, + `roleCreatedDate` DATETIME(0) NULL, + `roleModifiedDate` DATETIME(0) NULL, + + PRIMARY KEY (`roleID`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `lookup` ( + `lookupID` INTEGER NOT NULL AUTO_INCREMENT, + `lookupOrder` INTEGER NULL, + `lookupTitle` VARCHAR(255) NULL, + `lookupRefCode` VARCHAR(255) NULL, + `lookupValue` VARCHAR(255) NULL, + `lookupType` VARCHAR(255) NULL, + `lookupStatus` VARCHAR(255) NULL, + `lookupCreatedDate` DATETIME(0) NULL, + `lookupModifiedDate` DATETIME(0) NULL, + + PRIMARY KEY (`lookupID`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `userrole` ( + `userRoleID` INTEGER NOT NULL AUTO_INCREMENT, + `userRoleUserID` INTEGER NOT NULL DEFAULT 0, + `userRoleRoleID` INTEGER NOT NULL DEFAULT 0, + `userRoleCreatedDate` DATETIME(0) NOT NULL, + + INDEX `FK_userrole_role`(`userRoleRoleID`), + INDEX `FK_userrole_user`(`userRoleUserID`), + PRIMARY KEY (`userRoleID`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `userrole` ADD CONSTRAINT `FK_userrole_role` FOREIGN KEY (`userRoleRoleID`) REFERENCES `role`(`roleID`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `userrole` ADD CONSTRAINT `FK_userrole_user` FOREIGN KEY (`userRoleUserID`) REFERENCES `user`(`userID`) ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5a788a --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "mysql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..a3dadf1 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,333 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model audit { + auditID Int @id @default(autoincrement()) + auditIP String? @db.VarChar(255) + auditURL String? @db.VarChar(255) + auditURLMethod String? @db.VarChar(255) + auditURLPayload String? @db.VarChar(255) + auditCreatedDate DateTime? @db.DateTime(0) +} + +model lookup { + lookupID Int @id @default(autoincrement()) + lookupOrder Int? + lookupTitle String? @db.VarChar(255) + lookupRefCode String? @db.VarChar(255) + lookupValue String? @db.VarChar(255) + lookupType String? @db.VarChar(255) + lookupStatus String? @db.VarChar(255) + lookupCreatedDate DateTime? @db.DateTime(0) + lookupModifiedDate DateTime? @db.DateTime(0) +} + +model role { + roleID Int @id @default(autoincrement()) + roleName String? @db.VarChar(255) + roleDescription String? @db.VarChar(255) + roleStatus String? @db.VarChar(255) + roleCreatedDate DateTime? @db.DateTime(0) + roleModifiedDate DateTime? @db.DateTime(0) + userrole userrole[] +} + +model user { + userID Int @id @default(autoincrement()) + userSecretKey String? @db.VarChar(255) + userUsername String? @db.VarChar(255) + userPassword String? @db.VarChar(255) + userFullName String? @db.VarChar(255) + userEmail String? @db.VarChar(255) + userPhone String? @db.VarChar(255) + userStatus String? @db.VarChar(255) + userCreatedDate DateTime? @db.DateTime(0) + userModifiedDate DateTime? @db.DateTime(0) + userrole userrole[] +} + +model userrole { + userRoleID Int @id @default(autoincrement()) + userRoleUserID Int @default(0) + userRoleRoleID Int @default(0) + userRoleCreatedDate DateTime @db.DateTime(0) + role role @relation(fields: [userRoleRoleID], references: [roleID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_role") + user user @relation(fields: [userRoleUserID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_user") + + @@index([userRoleRoleID], map: "FK_userrole_role") + @@index([userRoleUserID], map: "FK_userrole_user") +} + +model site_settings { + settingID Int @id @default(autoincrement()) + siteName String? @db.VarChar(255) + siteNameFontSize Int? @default(18) + siteDescription String? @db.Text + siteLogo String? @db.VarChar(500) + siteLoadingLogo String? @db.VarChar(500) + siteFavicon String? @db.VarChar(500) + showSiteNameInHeader Boolean? @default(true) + primaryColor String? @db.VarChar(50) + secondaryColor String? @db.VarChar(50) + successColor String? @db.VarChar(50) + infoColor String? @db.VarChar(50) + warningColor String? @db.VarChar(50) + dangerColor String? @db.VarChar(50) + customCSS String? @db.Text + themeMode String? @db.VarChar(50) + customThemeFile String? @db.VarChar(500) + currentFont String? @db.VarChar(255) + fontSource String? @db.VarChar(500) + seoTitle String? @db.VarChar(255) + seoDescription String? @db.Text + seoKeywords String? @db.Text + seoAuthor String? @db.VarChar(255) + seoOgImage String? @db.VarChar(500) + seoTwitterCard String? @default("summary_large_image") @db.VarChar(50) + seoCanonicalUrl String? @db.VarChar(500) + seoRobots String? @default("index, follow") @db.VarChar(100) + seoGoogleAnalytics String? @db.VarChar(255) + seoGoogleTagManager String? @db.VarChar(255) + seoFacebookPixel String? @db.VarChar(255) + settingCreatedDate DateTime? @db.DateTime(0) + settingModifiedDate DateTime? @db.DateTime(0) + siteLoginLogo String? @db.VarChar(500) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model notification_analytics { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + notification_id String @db.VarChar(36) + channel_type String @db.VarChar(20) + metric_type String @db.VarChar(30) + metric_value Int? @default(0) + recorded_at DateTime? @default(now()) @db.Timestamp(0) + metadata Json? @default(dbgenerated("(_utf8mb4\\'{}\\')")) + notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_analytics_ibfk_1") + + @@index([notification_id], map: "idx_notification_analytics_notification_id") +} + +model notification_categories { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + name String @db.VarChar(100) + value String @unique(map: "value") @db.VarChar(50) + description String? @db.Text + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + notifications notifications[] +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model notification_channels { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + notification_id String @db.VarChar(36) + channel_type String @db.VarChar(20) + is_enabled Boolean? @default(true) + created_at DateTime? @default(now()) @db.Timestamp(0) + notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_channels_ibfk_1") + + @@index([notification_id], map: "notification_id") +} + +model notification_delivery { + id Int @id @default(autoincrement()) + notification_id Int + channel_type String + recipient String + is_success Boolean @default(false) + error_message String? @db.Text + attempts Int @default(0) + sent_at DateTime? + delivered_at DateTime? + created_at DateTime @default(now()) + updated_at DateTime +} + +model notification_delivery_config { + id Int @id @default(autoincrement()) + channel_type String @unique + is_enabled Boolean @default(false) + provider String + provider_config Json @default(dbgenerated("(_utf8mb4\\'{}\\')")) + status String @default("Not Configured") + success_rate Float @default(0) @db.Float + created_at DateTime @default(now()) + updated_at DateTime + created_by Int + updated_by Int +} + +model notification_delivery_settings { + id Int @id @default(1) + auto_retry Boolean @default(true) + enable_fallback Boolean @default(true) + max_retries Int @default(3) + retry_delay Int @default(30) + priority String @default("normal") + enable_reports Boolean @default(true) + created_at DateTime @default(now()) + updated_at DateTime + created_by Int + updated_by Int +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model notification_queue { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + notification_id String @db.VarChar(36) + recipient_id String @db.VarChar(36) + scheduled_for DateTime @db.Timestamp(0) + priority Int? @default(5) + attempts Int? @default(0) + max_attempts Int? @default(3) + status String? @default("queued") @db.VarChar(20) + last_attempt_at DateTime? @db.Timestamp(0) + error_message String? @db.Text + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_queue_ibfk_1") + notification_recipients notification_recipients @relation(fields: [recipient_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_queue_ibfk_2") + + @@index([scheduled_for], map: "idx_notification_queue_scheduled_for") + @@index([status], map: "idx_notification_queue_status") + @@index([notification_id], map: "notification_id") + @@index([recipient_id], map: "recipient_id") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model notification_recipients { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + notification_id String @db.VarChar(36) + user_id String @db.VarChar(36) + email String? @db.VarChar(255) + channel_type String @db.VarChar(20) + status String? @default("pending") @db.VarChar(20) + sent_at DateTime? @db.Timestamp(0) + delivered_at DateTime? @db.Timestamp(0) + opened_at DateTime? @db.Timestamp(0) + clicked_at DateTime? @db.Timestamp(0) + error_message String? @db.Text + ab_test_variant String? @db.VarChar(1) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + notification_queue notification_queue[] + notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_recipients_ibfk_1") + + @@index([status], map: "idx_notification_recipients_status") + @@index([user_id], map: "idx_notification_recipients_user_id") + @@index([notification_id], map: "notification_id") +} + +model notification_templates { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + name String @db.VarChar(100) + value String @unique(map: "value") @db.VarChar(50) + subject String? @db.VarChar(255) + email_content String? @db.Text + push_title String? @db.VarChar(100) + push_body String? @db.VarChar(300) + variables Json? @default(dbgenerated("(_utf8mb4\\'[]\\')")) + is_active Boolean? @default(true) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + notifications notifications[] +} + +model notification_user_segments { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + notification_id String @db.VarChar(36) + segment_id String @db.VarChar(36) + created_at DateTime? @default(now()) @db.Timestamp(0) + notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_user_segments_ibfk_1") + user_segments user_segments @relation(fields: [segment_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "notification_user_segments_ibfk_2") + + @@index([notification_id], map: "notification_id") + @@index([segment_id], map: "segment_id") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model notifications { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + title String @db.VarChar(255) + type String @db.VarChar(20) + priority String @db.VarChar(20) + category_id String? @db.VarChar(36) + status String? @default("draft") @db.VarChar(20) + delivery_type String @db.VarChar(20) + scheduled_at DateTime? @db.Timestamp(0) + timezone String? @default("UTC") @db.VarChar(50) + expires_at DateTime? @db.Timestamp(0) + enable_ab_testing Boolean? @default(false) + ab_test_split Int? @default(50) + ab_test_name String? @db.VarChar(100) + enable_tracking Boolean? @default(true) + audience_type String @db.VarChar(20) + specific_users String? @db.Text + user_status String? @db.VarChar(20) + registration_period String? @db.VarChar(50) + exclude_unsubscribed Boolean? @default(true) + respect_do_not_disturb Boolean? @default(true) + content_type String @db.VarChar(20) + template_id String? @db.VarChar(36) + email_subject String? @db.VarChar(255) + email_content String? @db.Text + call_to_action_text String? @db.VarChar(100) + call_to_action_url String? @db.Text + push_title String? @db.VarChar(100) + push_body String? @db.VarChar(300) + push_image_url String? @db.Text + estimated_reach Int? @default(0) + actual_sent Int? @default(0) + created_by String @db.VarChar(36) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + sent_at DateTime? @db.Timestamp(0) + notification_analytics notification_analytics[] + notification_channels notification_channels[] + notification_queue notification_queue[] + notification_recipients notification_recipients[] + notification_user_segments notification_user_segments[] + notification_categories notification_categories? @relation(fields: [category_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "notifications_ibfk_1") + notification_templates notification_templates? @relation(fields: [template_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "notifications_ibfk_2") + + @@index([category_id], map: "category_id") + @@index([created_by], map: "idx_notifications_created_by") + @@index([scheduled_at], map: "idx_notifications_scheduled_at") + @@index([status], map: "idx_notifications_status") + @@index([template_id], map: "template_id") +} + +model user_notification_preferences { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + user_id String @db.VarChar(36) + channel_type String @db.VarChar(20) + category_value String? @db.VarChar(50) + is_enabled Boolean? @default(true) + do_not_disturb_start DateTime? @db.Time(0) + do_not_disturb_end DateTime? @db.Time(0) + timezone String? @default("UTC") @db.VarChar(50) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + + @@unique([user_id, channel_type, category_value], map: "user_id") + @@index([user_id], map: "idx_user_notification_preferences_user_id") +} + +model user_segments { + id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36) + name String @db.VarChar(100) + value String @unique(map: "value") @db.VarChar(50) + description String? @db.Text + criteria Json + is_active Boolean? @default(true) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + notification_user_segments notification_user_segments[] +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..4c81029 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icons/android/android-launchericon-144-144.png b/public/icons/android/android-launchericon-144-144.png new file mode 100644 index 0000000..d1745df Binary files /dev/null and b/public/icons/android/android-launchericon-144-144.png differ diff --git a/public/icons/android/android-launchericon-192-192.png b/public/icons/android/android-launchericon-192-192.png new file mode 100644 index 0000000..c658e96 Binary files /dev/null and b/public/icons/android/android-launchericon-192-192.png differ diff --git a/public/icons/android/android-launchericon-48-48.png b/public/icons/android/android-launchericon-48-48.png new file mode 100644 index 0000000..94789a3 Binary files /dev/null and b/public/icons/android/android-launchericon-48-48.png differ diff --git a/public/icons/android/android-launchericon-512-512.png b/public/icons/android/android-launchericon-512-512.png new file mode 100644 index 0000000..0f32288 Binary files /dev/null and b/public/icons/android/android-launchericon-512-512.png differ diff --git a/public/icons/android/android-launchericon-72-72.png b/public/icons/android/android-launchericon-72-72.png new file mode 100644 index 0000000..874557d Binary files /dev/null and b/public/icons/android/android-launchericon-72-72.png differ diff --git a/public/icons/android/android-launchericon-96-96.png b/public/icons/android/android-launchericon-96-96.png new file mode 100644 index 0000000..33e573a Binary files /dev/null and b/public/icons/android/android-launchericon-96-96.png differ diff --git a/public/icons/ios/100.png b/public/icons/ios/100.png new file mode 100644 index 0000000..f72f31c Binary files /dev/null and b/public/icons/ios/100.png differ diff --git a/public/icons/ios/1024.png b/public/icons/ios/1024.png new file mode 100644 index 0000000..7316ff7 Binary files /dev/null and b/public/icons/ios/1024.png differ diff --git a/public/icons/ios/114.png b/public/icons/ios/114.png new file mode 100644 index 0000000..a58d8d8 Binary files /dev/null and b/public/icons/ios/114.png differ diff --git a/public/icons/ios/120.png b/public/icons/ios/120.png new file mode 100644 index 0000000..a624062 Binary files /dev/null and b/public/icons/ios/120.png differ diff --git a/public/icons/ios/128.png b/public/icons/ios/128.png new file mode 100644 index 0000000..8dc9761 Binary files /dev/null and b/public/icons/ios/128.png differ diff --git a/public/icons/ios/144.png b/public/icons/ios/144.png new file mode 100644 index 0000000..d1745df Binary files /dev/null and b/public/icons/ios/144.png differ diff --git a/public/icons/ios/152.png b/public/icons/ios/152.png new file mode 100644 index 0000000..8a8d828 Binary files /dev/null and b/public/icons/ios/152.png differ diff --git a/public/icons/ios/16.png b/public/icons/ios/16.png new file mode 100644 index 0000000..bf1e8dc Binary files /dev/null and b/public/icons/ios/16.png differ diff --git a/public/icons/ios/167.png b/public/icons/ios/167.png new file mode 100644 index 0000000..557eab1 Binary files /dev/null and b/public/icons/ios/167.png differ diff --git a/public/icons/ios/180.png b/public/icons/ios/180.png new file mode 100644 index 0000000..1221b67 Binary files /dev/null and b/public/icons/ios/180.png differ diff --git a/public/icons/ios/192.png b/public/icons/ios/192.png new file mode 100644 index 0000000..c658e96 Binary files /dev/null and b/public/icons/ios/192.png differ diff --git a/public/icons/ios/20.png b/public/icons/ios/20.png new file mode 100644 index 0000000..1e94680 Binary files /dev/null and b/public/icons/ios/20.png differ diff --git a/public/icons/ios/256.png b/public/icons/ios/256.png new file mode 100644 index 0000000..191ff36 Binary files /dev/null and b/public/icons/ios/256.png differ diff --git a/public/icons/ios/29.png b/public/icons/ios/29.png new file mode 100644 index 0000000..66f379c Binary files /dev/null and b/public/icons/ios/29.png differ diff --git a/public/icons/ios/32.png b/public/icons/ios/32.png new file mode 100644 index 0000000..eeeb3f8 Binary files /dev/null and b/public/icons/ios/32.png differ diff --git a/public/icons/ios/40.png b/public/icons/ios/40.png new file mode 100644 index 0000000..b26761b Binary files /dev/null and b/public/icons/ios/40.png differ diff --git a/public/icons/ios/50.png b/public/icons/ios/50.png new file mode 100644 index 0000000..555d6ed Binary files /dev/null and b/public/icons/ios/50.png differ diff --git a/public/icons/ios/512.png b/public/icons/ios/512.png new file mode 100644 index 0000000..0f32288 Binary files /dev/null and b/public/icons/ios/512.png differ diff --git a/public/icons/ios/57.png b/public/icons/ios/57.png new file mode 100644 index 0000000..a014f64 Binary files /dev/null and b/public/icons/ios/57.png differ diff --git a/public/icons/ios/58.png b/public/icons/ios/58.png new file mode 100644 index 0000000..abf29d3 Binary files /dev/null and b/public/icons/ios/58.png differ diff --git a/public/icons/ios/60.png b/public/icons/ios/60.png new file mode 100644 index 0000000..972dfb1 Binary files /dev/null and b/public/icons/ios/60.png differ diff --git a/public/icons/ios/64.png b/public/icons/ios/64.png new file mode 100644 index 0000000..5d3034c Binary files /dev/null and b/public/icons/ios/64.png differ diff --git a/public/icons/ios/72.png b/public/icons/ios/72.png new file mode 100644 index 0000000..874557d Binary files /dev/null and b/public/icons/ios/72.png differ diff --git a/public/icons/ios/76.png b/public/icons/ios/76.png new file mode 100644 index 0000000..9f0b0cf Binary files /dev/null and b/public/icons/ios/76.png differ diff --git a/public/icons/ios/80.png b/public/icons/ios/80.png new file mode 100644 index 0000000..c65629c Binary files /dev/null and b/public/icons/ios/80.png differ diff --git a/public/icons/ios/87.png b/public/icons/ios/87.png new file mode 100644 index 0000000..f6e49b3 Binary files /dev/null and b/public/icons/ios/87.png differ diff --git a/public/icons/windows11/LargeTile.scale-100.png b/public/icons/windows11/LargeTile.scale-100.png new file mode 100644 index 0000000..a8fa884 Binary files /dev/null and b/public/icons/windows11/LargeTile.scale-100.png differ diff --git a/public/icons/windows11/LargeTile.scale-125.png b/public/icons/windows11/LargeTile.scale-125.png new file mode 100644 index 0000000..bf7b6a7 Binary files /dev/null and b/public/icons/windows11/LargeTile.scale-125.png differ diff --git a/public/icons/windows11/LargeTile.scale-150.png b/public/icons/windows11/LargeTile.scale-150.png new file mode 100644 index 0000000..e539dec Binary files /dev/null and b/public/icons/windows11/LargeTile.scale-150.png differ diff --git a/public/icons/windows11/LargeTile.scale-200.png b/public/icons/windows11/LargeTile.scale-200.png new file mode 100644 index 0000000..9014e81 Binary files /dev/null and b/public/icons/windows11/LargeTile.scale-200.png differ diff --git a/public/icons/windows11/LargeTile.scale-400.png b/public/icons/windows11/LargeTile.scale-400.png new file mode 100644 index 0000000..4a8f5f3 Binary files /dev/null and b/public/icons/windows11/LargeTile.scale-400.png differ diff --git a/public/icons/windows11/SmallTile.scale-100.png b/public/icons/windows11/SmallTile.scale-100.png new file mode 100644 index 0000000..f7fafd3 Binary files /dev/null and b/public/icons/windows11/SmallTile.scale-100.png differ diff --git a/public/icons/windows11/SmallTile.scale-125.png b/public/icons/windows11/SmallTile.scale-125.png new file mode 100644 index 0000000..601b774 Binary files /dev/null and b/public/icons/windows11/SmallTile.scale-125.png differ diff --git a/public/icons/windows11/SmallTile.scale-150.png b/public/icons/windows11/SmallTile.scale-150.png new file mode 100644 index 0000000..9b3edad Binary files /dev/null and b/public/icons/windows11/SmallTile.scale-150.png differ diff --git a/public/icons/windows11/SmallTile.scale-200.png b/public/icons/windows11/SmallTile.scale-200.png new file mode 100644 index 0000000..f28a5e2 Binary files /dev/null and b/public/icons/windows11/SmallTile.scale-200.png differ diff --git a/public/icons/windows11/SmallTile.scale-400.png b/public/icons/windows11/SmallTile.scale-400.png new file mode 100644 index 0000000..b94a642 Binary files /dev/null and b/public/icons/windows11/SmallTile.scale-400.png differ diff --git a/public/icons/windows11/SplashScreen.scale-100.png b/public/icons/windows11/SplashScreen.scale-100.png new file mode 100644 index 0000000..5f4626d Binary files /dev/null and b/public/icons/windows11/SplashScreen.scale-100.png differ diff --git a/public/icons/windows11/SplashScreen.scale-125.png b/public/icons/windows11/SplashScreen.scale-125.png new file mode 100644 index 0000000..a1e00a6 Binary files /dev/null and b/public/icons/windows11/SplashScreen.scale-125.png differ diff --git a/public/icons/windows11/SplashScreen.scale-150.png b/public/icons/windows11/SplashScreen.scale-150.png new file mode 100644 index 0000000..1a8c6b6 Binary files /dev/null and b/public/icons/windows11/SplashScreen.scale-150.png differ diff --git a/public/icons/windows11/SplashScreen.scale-200.png b/public/icons/windows11/SplashScreen.scale-200.png new file mode 100644 index 0000000..741809a Binary files /dev/null and b/public/icons/windows11/SplashScreen.scale-200.png differ diff --git a/public/icons/windows11/SplashScreen.scale-400.png b/public/icons/windows11/SplashScreen.scale-400.png new file mode 100644 index 0000000..dd84d5b Binary files /dev/null and b/public/icons/windows11/SplashScreen.scale-400.png differ diff --git a/public/icons/windows11/Square150x150Logo.scale-100.png b/public/icons/windows11/Square150x150Logo.scale-100.png new file mode 100644 index 0000000..71ac343 Binary files /dev/null and b/public/icons/windows11/Square150x150Logo.scale-100.png differ diff --git a/public/icons/windows11/Square150x150Logo.scale-125.png b/public/icons/windows11/Square150x150Logo.scale-125.png new file mode 100644 index 0000000..4f6ee48 Binary files /dev/null and b/public/icons/windows11/Square150x150Logo.scale-125.png differ diff --git a/public/icons/windows11/Square150x150Logo.scale-150.png b/public/icons/windows11/Square150x150Logo.scale-150.png new file mode 100644 index 0000000..f4944fa Binary files /dev/null and b/public/icons/windows11/Square150x150Logo.scale-150.png differ diff --git a/public/icons/windows11/Square150x150Logo.scale-200.png b/public/icons/windows11/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..26d3bc0 Binary files /dev/null and b/public/icons/windows11/Square150x150Logo.scale-200.png differ diff --git a/public/icons/windows11/Square150x150Logo.scale-400.png b/public/icons/windows11/Square150x150Logo.scale-400.png new file mode 100644 index 0000000..1398d62 Binary files /dev/null and b/public/icons/windows11/Square150x150Logo.scale-400.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000..367fc8d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png new file mode 100644 index 0000000..56f7b65 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000..13a7348 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000..53b4463 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png new file mode 100644 index 0000000..c8081cd Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000..b4d7faa Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png new file mode 100644 index 0000000..7dc7f30 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png new file mode 100644 index 0000000..90bc051 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png new file mode 100644 index 0000000..340fd9e Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000..cf3a14d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png new file mode 100644 index 0000000..cc5e37d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png new file mode 100644 index 0000000..ae2282a Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png new file mode 100644 index 0000000..b97a6ae Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png new file mode 100644 index 0000000..3962cca Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png new file mode 100644 index 0000000..8ccbb27 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000..367fc8d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png new file mode 100644 index 0000000..56f7b65 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 0000000..13a7348 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000..53b4463 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png new file mode 100644 index 0000000..c8081cd Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000..b4d7faa Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png new file mode 100644 index 0000000..7dc7f30 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png new file mode 100644 index 0000000..90bc051 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png new file mode 100644 index 0000000..340fd9e Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000..cf3a14d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png new file mode 100644 index 0000000..cc5e37d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png new file mode 100644 index 0000000..ae2282a Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png new file mode 100644 index 0000000..b97a6ae Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png new file mode 100644 index 0000000..3962cca Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png differ diff --git a/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png new file mode 100644 index 0000000..8ccbb27 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png differ diff --git a/public/icons/windows11/Square44x44Logo.scale-100.png b/public/icons/windows11/Square44x44Logo.scale-100.png new file mode 100644 index 0000000..340fd9e Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.scale-100.png differ diff --git a/public/icons/windows11/Square44x44Logo.scale-125.png b/public/icons/windows11/Square44x44Logo.scale-125.png new file mode 100644 index 0000000..78fbfbd Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.scale-125.png differ diff --git a/public/icons/windows11/Square44x44Logo.scale-150.png b/public/icons/windows11/Square44x44Logo.scale-150.png new file mode 100644 index 0000000..e18f5e6 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.scale-150.png differ diff --git a/public/icons/windows11/Square44x44Logo.scale-200.png b/public/icons/windows11/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..5240c8e Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.scale-200.png differ diff --git a/public/icons/windows11/Square44x44Logo.scale-400.png b/public/icons/windows11/Square44x44Logo.scale-400.png new file mode 100644 index 0000000..42b2456 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.scale-400.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-16.png b/public/icons/windows11/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000..367fc8d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-16.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-20.png b/public/icons/windows11/Square44x44Logo.targetsize-20.png new file mode 100644 index 0000000..56f7b65 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-20.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-24.png b/public/icons/windows11/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000..13a7348 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-24.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-256.png b/public/icons/windows11/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000..53b4463 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-256.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-30.png b/public/icons/windows11/Square44x44Logo.targetsize-30.png new file mode 100644 index 0000000..c8081cd Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-30.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-32.png b/public/icons/windows11/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000..b4d7faa Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-32.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-36.png b/public/icons/windows11/Square44x44Logo.targetsize-36.png new file mode 100644 index 0000000..7dc7f30 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-36.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-40.png b/public/icons/windows11/Square44x44Logo.targetsize-40.png new file mode 100644 index 0000000..90bc051 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-40.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-44.png b/public/icons/windows11/Square44x44Logo.targetsize-44.png new file mode 100644 index 0000000..340fd9e Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-44.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-48.png b/public/icons/windows11/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000..cf3a14d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-48.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-60.png b/public/icons/windows11/Square44x44Logo.targetsize-60.png new file mode 100644 index 0000000..cc5e37d Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-60.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-64.png b/public/icons/windows11/Square44x44Logo.targetsize-64.png new file mode 100644 index 0000000..ae2282a Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-64.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-72.png b/public/icons/windows11/Square44x44Logo.targetsize-72.png new file mode 100644 index 0000000..b97a6ae Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-72.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-80.png b/public/icons/windows11/Square44x44Logo.targetsize-80.png new file mode 100644 index 0000000..3962cca Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-80.png differ diff --git a/public/icons/windows11/Square44x44Logo.targetsize-96.png b/public/icons/windows11/Square44x44Logo.targetsize-96.png new file mode 100644 index 0000000..8ccbb27 Binary files /dev/null and b/public/icons/windows11/Square44x44Logo.targetsize-96.png differ diff --git a/public/icons/windows11/StoreLogo.scale-100.png b/public/icons/windows11/StoreLogo.scale-100.png new file mode 100644 index 0000000..555d6ed Binary files /dev/null and b/public/icons/windows11/StoreLogo.scale-100.png differ diff --git a/public/icons/windows11/StoreLogo.scale-125.png b/public/icons/windows11/StoreLogo.scale-125.png new file mode 100644 index 0000000..10895f8 Binary files /dev/null and b/public/icons/windows11/StoreLogo.scale-125.png differ diff --git a/public/icons/windows11/StoreLogo.scale-150.png b/public/icons/windows11/StoreLogo.scale-150.png new file mode 100644 index 0000000..e02db47 Binary files /dev/null and b/public/icons/windows11/StoreLogo.scale-150.png differ diff --git a/public/icons/windows11/StoreLogo.scale-200.png b/public/icons/windows11/StoreLogo.scale-200.png new file mode 100644 index 0000000..f72f31c Binary files /dev/null and b/public/icons/windows11/StoreLogo.scale-200.png differ diff --git a/public/icons/windows11/StoreLogo.scale-400.png b/public/icons/windows11/StoreLogo.scale-400.png new file mode 100644 index 0000000..283f165 Binary files /dev/null and b/public/icons/windows11/StoreLogo.scale-400.png differ diff --git a/public/icons/windows11/Wide310x150Logo.scale-100.png b/public/icons/windows11/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000..24f2702 Binary files /dev/null and b/public/icons/windows11/Wide310x150Logo.scale-100.png differ diff --git a/public/icons/windows11/Wide310x150Logo.scale-125.png b/public/icons/windows11/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000..67f1006 Binary files /dev/null and b/public/icons/windows11/Wide310x150Logo.scale-125.png differ diff --git a/public/icons/windows11/Wide310x150Logo.scale-150.png b/public/icons/windows11/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000..50f6e73 Binary files /dev/null and b/public/icons/windows11/Wide310x150Logo.scale-150.png differ diff --git a/public/icons/windows11/Wide310x150Logo.scale-200.png b/public/icons/windows11/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..5f4626d Binary files /dev/null and b/public/icons/windows11/Wide310x150Logo.scale-200.png differ diff --git a/public/icons/windows11/Wide310x150Logo.scale-400.png b/public/icons/windows11/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000..741809a Binary files /dev/null and b/public/icons/windows11/Wide310x150Logo.scale-400.png differ diff --git a/public/img/background/bg1.jpg b/public/img/background/bg1.jpg new file mode 100644 index 0000000..863c587 Binary files /dev/null and b/public/img/background/bg1.jpg differ diff --git a/public/img/background/bg2.jpg b/public/img/background/bg2.jpg new file mode 100644 index 0000000..09ca735 Binary files /dev/null and b/public/img/background/bg2.jpg differ diff --git a/public/img/logo/corradAF-logo.svg b/public/img/logo/corradAF-logo.svg new file mode 100644 index 0000000..6242211 --- /dev/null +++ b/public/img/logo/corradAF-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/template/form1.jpg b/public/img/template/form1.jpg new file mode 100644 index 0000000..754419c Binary files /dev/null and b/public/img/template/form1.jpg differ diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/public/openapi.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/api/api-platform/oauth2/client-credentials.post.js b/server/api/api-platform/oauth2/client-credentials.post.js new file mode 100644 index 0000000..bd35c52 --- /dev/null +++ b/server/api/api-platform/oauth2/client-credentials.post.js @@ -0,0 +1,45 @@ +export default defineEventHandler(async (event) => { + try { + const { clientId, clientSecret, scope, tokenUrl } = await readBody(event) + + if (!clientId || !clientSecret || !tokenUrl) { + return { + success: false, + error: 'Missing required parameters' + } + } + + // Get access token using client credentials + const tokenData = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret + }) + + if (scope) { + tokenData.append('scope', scope) + } + + const tokenResponse = await $fetch(tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + body: tokenData.toString() + }) + + return { + success: true, + data: tokenResponse + } + + } catch (error) { + console.error('OAuth2 client credentials error:', error) + + return { + success: false, + error: error.data?.error || error.message || 'Client credentials flow failed' + } + } +}) \ No newline at end of file diff --git a/server/api/api-platform/oauth2/exchange-code.post.js b/server/api/api-platform/oauth2/exchange-code.post.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/server/api/api-platform/oauth2/exchange-code.post.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/api/api-platform/send-request.post.js b/server/api/api-platform/send-request.post.js new file mode 100644 index 0000000..aee8ec8 --- /dev/null +++ b/server/api/api-platform/send-request.post.js @@ -0,0 +1,267 @@ +export default defineEventHandler(async (event) => { + try { + const requestData = await readBody(event); + + if (!requestData.url) { + return { + statusCode: 400, + message: "URL is required", + }; + } + + let { + url, + method = 'GET', + headers = {}, + params = [], + auth = {}, + requestBody = {}, + timeout = 30000 + } = requestData; + + // Fix URL if it doesn't have protocol + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + const startTime = Date.now(); + + // Build final URL with query parameters + let urlObj; + try { + urlObj = new URL(url); + } catch (error) { + return { + statusCode: 400, + message: "Invalid URL format", + data: { + status: 400, + statusText: 'Bad Request', + headers: {}, + data: { error: 'Invalid URL format. Please check the URL and try again.' }, + time: 0, + size: 0 + } + }; + } + + // Add active query parameters + params.forEach(param => { + if (param.active && param.key && param.value) { + urlObj.searchParams.set(param.key, param.value); + } + }); + + // Add API key to query if specified + if (auth.type === 'apiKey' && auth.apiKey?.key && auth.apiKey?.value && auth.apiKey?.addTo === 'query') { + urlObj.searchParams.set(auth.apiKey.key, auth.apiKey.value); + } + + const finalUrl = urlObj.toString(); + + // Build headers + const requestHeaders = {}; + + // Add custom headers + headers.forEach(header => { + if (header.active && header.key && header.value) { + requestHeaders[header.key] = header.value; + } + }); + + // Add authentication headers + if (auth.type === 'bearer' && auth.bearer) { + requestHeaders['Authorization'] = `Bearer ${auth.bearer}`; + } else if (auth.type === 'basic' && auth.basic?.username && auth.basic?.password) { + const credentials = Buffer.from(`${auth.basic.username}:${auth.basic.password}`).toString('base64'); + requestHeaders['Authorization'] = `Basic ${credentials}`; + } else if (auth.type === 'apiKey' && auth.apiKey?.key && auth.apiKey?.value && auth.apiKey?.addTo === 'header') { + requestHeaders[auth.apiKey.key] = auth.apiKey.value; + } else if (auth.type === 'oauth2' && auth.oauth2?.accessToken) { + requestHeaders['Authorization'] = `${auth.oauth2.tokenType || 'Bearer'} ${auth.oauth2.accessToken}`; + } + + // Build request body + let requestBodyData = null; + let contentType = null; + + if (method !== 'GET' && method !== 'HEAD' && requestBody.type && requestBody.type !== 'none') { + if (requestBody.type === 'raw' && requestBody.raw) { + requestBodyData = requestBody.raw; + // Try to parse as JSON to set appropriate content type + try { + JSON.parse(requestBody.raw); + contentType = 'application/json'; + } catch (e) { + contentType = 'text/plain'; + } + } else if (requestBody.type === 'x-www-form-urlencoded' && requestBody.urlEncoded) { + const urlEncodedData = new URLSearchParams(); + requestBody.urlEncoded.forEach(item => { + if (item.active && item.key && item.value) { + urlEncodedData.append(item.key, item.value); + } + }); + requestBodyData = urlEncodedData.toString(); + contentType = 'application/x-www-form-urlencoded'; + } else if (requestBody.type === 'form-data' && requestBody.formData) { + // For form-data, we'll handle files as base64 for now + // In a real implementation, you'd want multipart/form-data support + const formData = new URLSearchParams(); + let hasFiles = false; + + requestBody.formData.forEach(item => { + if (item.active && item.key) { + if (item.type === 'text' && item.value) { + formData.append(item.key, item.value); + } else if (item.type === 'file' && item.file) { + // For now, we'll send file name as value + // Real file upload would need special handling + formData.append(item.key, `[FILE: ${item.file.name}]`); + hasFiles = true; + } + } + }); + + if (hasFiles) { + // Log a warning that file uploads aren't fully supported yet + console.warn('File uploads in form-data are not fully supported in the backend proxy yet'); + } + + requestBodyData = formData.toString(); + contentType = 'application/x-www-form-urlencoded'; // Fallback to URL-encoded for now + } + } + + // Set content type if not already set + if (contentType && !requestHeaders['Content-Type'] && !requestHeaders['content-type']) { + requestHeaders['Content-Type'] = contentType; + } + + // Create fetch options + const fetchOptions = { + method, + headers: requestHeaders, + signal: AbortSignal.timeout(timeout) + }; + + if (requestBodyData) { + fetchOptions.body = requestBodyData; + } + + // Make the actual HTTP request + const response = await fetch(finalUrl, fetchOptions); + + const endTime = Date.now(); + const responseTime = endTime - startTime; + + // Get response data + const responseHeaders = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + let responseData; + const contentTypeHeader = response.headers.get('content-type') || ''; + + if (contentTypeHeader.includes('application/json')) { + try { + responseData = await response.json(); + } catch (e) { + responseData = await response.text(); + } + } else if (contentTypeHeader.includes('text/') || contentTypeHeader.includes('application/xml')) { + responseData = await response.text(); + } else { + // For binary data, convert to text representation + try { + responseData = await response.text(); + } catch (e) { + responseData = '[Binary data]'; + } + } + + // Calculate response size (approximate) + const responseSize = typeof responseData === 'string' + ? new Blob([responseData]).size + : new Blob([JSON.stringify(responseData)]).size; + + return { + statusCode: 200, + message: "Request completed", + data: { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + data: responseData, + time: responseTime, + size: responseSize, + url: finalUrl + } + }; + + } catch (error) { + console.error('API Platform Request Error:', error); + + const endTime = Date.now(); + + // Handle different types of errors + if (error.name === 'AbortError') { + return { + statusCode: 408, + message: "Request timeout", + data: { + status: 408, + statusText: 'Request Timeout', + headers: {}, + data: { error: 'Request timed out' }, + time: 30000, + size: 0 + } + }; + } + + if (error.name === 'TypeError' && error.message.includes('fetch')) { + return { + statusCode: 400, + message: "Invalid URL or network error", + data: { + status: 400, + statusText: 'Bad Request', + headers: {}, + data: { error: 'Invalid URL format or network error' }, + time: endTime - Date.now() + 1000, + size: 0 + } + }; + } + + if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { + return { + statusCode: 502, + message: "Connection error", + data: { + status: 502, + statusText: 'Bad Gateway', + headers: {}, + data: { error: 'Failed to connect to the server' }, + time: endTime - Date.now() + 1000, + size: 0 + } + }; + } + + return { + statusCode: 500, + message: "Internal server error", + data: { + status: 500, + statusText: 'Internal Server Error', + headers: {}, + data: { error: error.message || 'Something went wrong' }, + time: endTime - Date.now() + 1000, + size: 0 + } + }; + } +}); \ No newline at end of file diff --git a/server/api/auth/login.post.js b/server/api/auth/login.post.js new file mode 100644 index 0000000..2a1d95f --- /dev/null +++ b/server/api/auth/login.post.js @@ -0,0 +1,93 @@ +import sha256 from "crypto-js/sha256.js"; +import jwt from "jsonwebtoken"; + +const ENV = useRuntimeConfig(); + +export default defineEventHandler(async (event) => { + try { + const { username, password } = await readBody(event); + + if (!username || !password) { + return { + statusCode: 400, + message: "Username and password are required", + }; + } + + const user = await prisma.user.findFirst({ + where: { + userUsername: username, + }, + }); + + if (!user) { + return { + statusCode: 404, + message: "User does not exist", + }; + } + + const hashedPassword = sha256(password).toString(); + if (user.userPassword !== hashedPassword) { + return { + statusCode: 401, + message: "Invalid password", + }; + } + + // Get user roles + const roles = await prisma.userrole.findMany({ + where: { + userRoleUserID: user.userID, + }, + select: { + role: { + select: { + roleName: true, + }, + }, + }, + }); + + const roleNames = roles.map((r) => r.role.roleName); + + const accessToken = generateAccessToken({ + username: user.userUsername, + roles: roleNames, + }); + + const refreshToken = generateRefreshToken({ + username: user.userUsername, + roles: roleNames, + }); + + // Set cookie httpOnly + event.res.setHeader("Set-Cookie", [ + `accessToken=${accessToken}; HttpOnly; Secure; SameSite=Lax; Path=/`, + `refreshToken=${refreshToken}; HttpOnly; Secure; SameSite=Lax; Path=/`, + ]); + + return { + statusCode: 200, + message: "Login success", + data: { + username: user.userUsername, + roles: roleNames, + }, + }; + } catch (error) { + console.log(error); + return { + statusCode: 500, + message: "Internal server error", + }; + } +}); + +function generateAccessToken(user) { + return jwt.sign(user, ENV.auth.secretAccess, { expiresIn: "1d" }); +} + +function generateRefreshToken(user) { + return jwt.sign(user, ENV.auth.secretRefresh, { expiresIn: "30d" }); +} diff --git a/server/api/auth/logout.get.js b/server/api/auth/logout.get.js new file mode 100644 index 0000000..c7e14d1 --- /dev/null +++ b/server/api/auth/logout.get.js @@ -0,0 +1,19 @@ +export default defineEventHandler(async (event) => { + try { + event.res.setHeader("Set-Cookie", [ + `accessToken=; HttpOnly; Secure; SameSite=Lax; Path=/`, + `refreshToken=; HttpOnly; Secure; SameSite=Lax; Path=/`, + ]); + + return { + statusCode: 200, + message: "Logout success", + }; + } catch (error) { + console.log(error); + return { + statusCode: 400, + message: "Server error", + }; + } +}); diff --git a/server/api/auth/validate.get.js b/server/api/auth/validate.get.js new file mode 100644 index 0000000..a5014e9 --- /dev/null +++ b/server/api/auth/validate.get.js @@ -0,0 +1,34 @@ +export default defineEventHandler(async (event) => { + try { + const { userID } = event.context.user; + + if (userID == null) { + return { + statusCode: 401, + message: "Unauthorized", + }; + } + + const validatedUser = await prisma.user.findFirst({ + where: { + userID: parseInt(userID), + }, + }); + if (!validatedUser) { + return { + statusCode: 401, + message: "Unauthorized", + }; + } + + return { + statusCode: 200, + message: "Authorized", + }; + } catch (error) { + return { + statusCode: 401, + message: "Unauthorized", + }; + } +}); diff --git a/server/api/devtool/api/file-code.js b/server/api/devtool/api/file-code.js new file mode 100644 index 0000000..d2fbfe7 --- /dev/null +++ b/server/api/devtool/api/file-code.js @@ -0,0 +1,24 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + const query = await getQuery(event); + + try { + // Get vue code from path in query + const filePath = path.join(process.cwd() + "/server/", query.path + ".js"); + const code = fs.readFileSync(filePath, "utf8"); + + return { + statusCode: 200, + message: "Code successfully loaded", + data: code, + }; + } catch (error) { + // console.log(error); + return { + statusCode: 500, + message: "File not found", + }; + } +}); diff --git a/server/api/devtool/api/linter.js b/server/api/devtool/api/linter.js new file mode 100644 index 0000000..32572eb --- /dev/null +++ b/server/api/devtool/api/linter.js @@ -0,0 +1,191 @@ +// import esline vue +import { ESLint } from "eslint"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + + try { + if (body.code === undefined) { + return { + statusCode: 400, + message: "Bad Request", + }; + } + + // run linter + const code = body.code; + const validateNitroCode = (code) => { + // Check if this is a server route file + const isServerRoute = code.includes("defineEventHandler"); + + if (isServerRoute) { + let lineNumber = 1; + + // 1. Validate event handler structure + if (!code.includes("export default defineEventHandler")) { + throw { + message: + "Nitro route handlers must use 'export default defineEventHandler'", + line: 1, + column: 0, + }; + } + + // 2. Check for proper request handling + const hasRequestBody = code.includes("await readBody(event)"); + const hasRequestQuery = code.includes("getQuery(event)"); + const usesEventWithoutImport = + code.includes("event.") && !hasRequestBody && !hasRequestQuery; + + if (usesEventWithoutImport) { + // Find the line where event is improperly used + const lines = code.split("\n"); + for (let i = 0; i < lines.length; i++) { + if ( + lines[i].includes("event.") && + !lines[i].includes("readBody") && + !lines[i].includes("getQuery") + ) { + throw { + message: + "Use 'readBody(event)' for POST data or 'getQuery(event)' for query parameters", + line: i + 1, + column: lines[i].indexOf("event."), + }; + } + } + } + + // 3. Validate response structure + const responseRegex = /return\s+{([^}]+)}/g; + let match; + let lastIndex = 0; + + while ((match = responseRegex.exec(code)) !== null) { + lineNumber += (code.slice(lastIndex, match.index).match(/\n/g) || []) + .length; + lastIndex = match.index; + + const responseContent = match[1]; + + // Check for required response properties + if (!responseContent.includes("statusCode")) { + throw { + message: "API responses must include a 'statusCode' property", + line: lineNumber, + column: match.index - code.lastIndexOf("\n", match.index), + }; + } + + // Validate status code usage + const statusMatch = responseContent.match(/statusCode:\s*(\d+)/); + if (statusMatch) { + const statusCode = parseInt(statusMatch[1]); + if (![200, 201, 400, 401, 403, 404, 500].includes(statusCode)) { + throw { + message: `Invalid status code: ${statusCode}. Use standard HTTP status codes.`, + line: lineNumber, + column: statusMatch.index, + }; + } + } + } + + // 4. Check error handling + if (code.includes("try") && !code.includes("catch")) { + throw { + message: + "Missing error handling. Add a catch block for try statements.", + line: + code.split("\n").findIndex((line) => line.includes("try")) + 1, + column: 0, + }; + } + + // 5. Validate async/await usage + const asyncLines = code.match(/async.*=>/g) || []; + const awaitLines = code.match(/await\s+/g) || []; + + if (awaitLines.length > 0 && asyncLines.length === 0) { + throw { + message: "Using 'await' requires an async function", + line: + code.split("\n").findIndex((line) => line.includes("await")) + 1, + column: 0, + }; + } + + // // 6. Check for proper imports + // const requiredImports = new Set(); + // if (hasRequestBody) requiredImports.add("readBody"); + // if (hasRequestQuery) requiredImports.add("getQuery"); + + // const importLines = code.match(/import.*from/g) || []; + // requiredImports.forEach((imp) => { + // if (!importLines.some((line) => line.includes(imp))) { + // throw { + // message: `Missing import for '${imp}' utility`, + // line: 1, + // column: 0, + // }; + // } + // }); + } + }; + + try { + validateNitroCode(code); + + const eslint = new ESLint({ + overrideConfig: { + parser: "@babel/eslint-parser", + extends: ["@kiwicom"], + parserOptions: { + requireConfigFile: false, + ecmaVersion: 2020, + sourceType: "module", + }, + }, + useEslintrc: false, + }); + + const results = await eslint.lintText(code); + + if (results[0].messages.length > 0) { + const messages = results[0].messages[0]; + + if (messages.fatal === true) { + return { + statusCode: 400, + message: "Bad Linter Test", + data: messages, + }; + } + + return { + statusCode: 200, + message: "Good Linter test", + data: messages, + }; + } + } catch (error) { + console.log(error); + return { + statusCode: 400, + message: "Bad Linter Test", + data: { + message: error.message, + line: error.line || 1, + column: error.column || 0, + }, + }; + } + } catch (error) { + console.log(error); + return { + statusCode: 500, + message: "Internal Server Error", + errror: error, + }; + } +}); diff --git a/server/api/devtool/api/list.js b/server/api/devtool/api/list.js new file mode 100644 index 0000000..903ff37 --- /dev/null +++ b/server/api/devtool/api/list.js @@ -0,0 +1,77 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + try { + // get api folder path from server root folder and its files and folders inside it + const apiFolderPath = path.join(process.cwd() + "/server/api"); + const apis = fs.readdirSync(apiFolderPath); + + const apiList = getFilesAndFolders(apiFolderPath); + + const apiUrls = getApiUrls(apiList); + const jsonObject = JSON.parse(JSON.stringify(apiUrls)); + + return { + statusCode: 200, + message: "API List successfully fetched", + data: jsonObject, + }; + } catch (error) { + return { + statusCode: 500, + message: error.message, + }; + } +}); + +function getFilesAndFolders(folderPath) { + const folderFiles = fs.readdirSync(folderPath); + const files = []; + const folders = []; + const apiURL = "/api"; + + folderFiles.forEach((file) => { + const filePath = path.join(folderPath + "/" + file); + if (file == "devtool") return; + if (fs.lstatSync(filePath).isDirectory()) { + folders.push(getFilesAndFolders(filePath)); + } else { + const processPath = path.join(process.cwd() + "/server/api"); + const apiUrl = filePath + .replace(processPath, apiURL) + .replace(/\\/g, "/") + .replace(".js", ""); + const fileName = file.replace(".js", ""); + const parentFolder = folderPath + .replace(processPath, "") + .replace(/\\/g, ""); + + files.push({ + name: fileName, + parentName: parentFolder, + url: apiUrl, + }); + } + }); + + return { files, folders }; +} + +function getApiUrls(folder) { + const apiUrls = []; + + folder.files.forEach((file) => { + apiUrls.push({ + name: file.name, + parentName: file.parentName, + url: file.url, + }); + }); + + folder.folders.forEach((nestedFolder) => { + apiUrls.push(...getApiUrls(nestedFolder)); + }); + + return apiUrls; +} diff --git a/server/api/devtool/api/prettier-format.js b/server/api/devtool/api/prettier-format.js new file mode 100644 index 0000000..f8f4bb2 --- /dev/null +++ b/server/api/devtool/api/prettier-format.js @@ -0,0 +1,27 @@ +import prettier from "prettier"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + + try { + if (body.code === undefined) { + return { + statusCode: 400, + message: "Bad Request", + }; + } + + const code = prettier.format(body.code, { semi: false, parser: "babel" }); + + return { + statusCode: 200, + message: "Code successfully formatted", + data: code, + }; + } catch (error) { + return { + statusCode: 500, + message: "Internal Server Error", + }; + } +}); diff --git a/server/api/devtool/api/save.js b/server/api/devtool/api/save.js new file mode 100644 index 0000000..567ff59 --- /dev/null +++ b/server/api/devtool/api/save.js @@ -0,0 +1,71 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + + try { + const codeDefault = ` + export default defineEventHandler(async (event) => { + + // const query = await getQuery(event); // Get Params from URL + // const body = await readBody(event); // Get Body Data + + return { + statusCode: 200, + message: "API Route Created", + }; + + });`; + + // Overwrite vue code from path in body with new code + const filePath = path.join(process.cwd() + "/server/", body.path + ".js"); + + if (body.type == "update") { + fs.writeFileSync(filePath, body.code, "utf8"); + } else if (body.type == "add") { + // if the folder doesn't exist, create it + if (!fs.existsSync(path.dirname(filePath))) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + } + + fs.writeFileSync(filePath, codeDefault, "utf8"); + } else if (body.type == "edit") { + // if the folder doesn't exist, create it + if (!fs.existsSync(path.dirname(filePath))) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + } + + // Copy the file from the default path to the new path + const oldPath = path.join( + process.cwd() + "/server/", + body.oldPath + ".js" + ); + + // Copy file + fs.copyFileSync(oldPath, filePath); + + // Delete old file + fs.unlinkSync(oldPath); + } else if (body.type == "delete") { + // Delete file from path + fs.unlinkSync(filePath); + + return { + statusCode: 200, + message: "Code successfully deleted", + }; + } + + return { + statusCode: 200, + message: "Code successfully saved", + }; + } catch (error) { + console.log(error); + return { + statusCode: 500, + message: "Internal Server Error", + }; + } +}); diff --git a/server/api/devtool/config/add-custom-theme.js b/server/api/devtool/config/add-custom-theme.js new file mode 100644 index 0000000..d6ae1f9 --- /dev/null +++ b/server/api/devtool/config/add-custom-theme.js @@ -0,0 +1,90 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + const method = getMethod(event); + + if (method !== "POST") { + return { + statusCode: 405, + message: "Method not allowed", + }; + } + + try { + const body = await readBody(event); + const { themeName, themeCSS } = body; + + if (!themeName || !themeCSS) { + return { + statusCode: 400, + message: "Theme name and CSS are required", + }; + } + + // Validate theme name (alphanumeric and hyphens only) + if (!/^[a-zA-Z0-9-_]+$/.test(themeName)) { + return { + statusCode: 400, + message: "Theme name can only contain letters, numbers, hyphens, and underscores", + }; + } + + // Path to theme.css file + const themeCSSPath = path.join(process.cwd(), 'assets', 'style', 'css', 'base', 'theme.css'); + + // Check if theme.css exists + if (!fs.existsSync(themeCSSPath)) { + return { + statusCode: 404, + message: "theme.css file not found", + }; + } + + // Read current theme.css content + let currentContent = fs.readFileSync(themeCSSPath, 'utf8'); + + // Check if theme already exists + const themePattern = new RegExp(`html\\[data-theme="${themeName}"\\]`, 'g'); + if (themePattern.test(currentContent)) { + return { + statusCode: 409, + message: `Theme "${themeName}" already exists`, + }; + } + + // Format the new theme CSS + const formattedThemeCSS = themeCSS.trim(); + + // Ensure the CSS starts with the correct selector if not provided + let finalThemeCSS; + if (!formattedThemeCSS.includes(`html[data-theme="${themeName}"]`)) { + finalThemeCSS = `html[data-theme="${themeName}"] {\n${formattedThemeCSS}\n}`; + } else { + finalThemeCSS = formattedThemeCSS; + } + + // Add the new theme to the end of the file + const newContent = currentContent + '\n\n' + finalThemeCSS + '\n'; + + // Write the updated content back to the file + fs.writeFileSync(themeCSSPath, newContent, 'utf8'); + + return { + statusCode: 200, + message: "Custom theme added successfully", + data: { + themeName, + success: true + }, + }; + + } catch (error) { + console.error("Add custom theme error:", error); + return { + statusCode: 500, + message: "Internal server error", + error: error.message, + }; + } +}); \ No newline at end of file diff --git a/server/api/devtool/config/env.js b/server/api/devtool/config/env.js new file mode 100644 index 0000000..557c0c9 --- /dev/null +++ b/server/api/devtool/config/env.js @@ -0,0 +1,22 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + // Get .env file, parse and return + const envFile = path.join(process.cwd(), ".env"); + + if (!fs.existsSync(envFile)) { + return { + statusCode: 404, + message: "File not found", + }; + } + + const env = fs.readFileSync(envFile, "utf-8"); + + return { + statusCode: 200, + message: "Success", + data: env, + }; +}); diff --git a/server/api/devtool/config/loading-logo.js b/server/api/devtool/config/loading-logo.js new file mode 100644 index 0000000..8573df2 --- /dev/null +++ b/server/api/devtool/config/loading-logo.js @@ -0,0 +1,44 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + const method = getMethod(event); + + try { + if (method === "GET") { + // Get only the loading logo and site name for faster loading + const settings = await prisma.site_settings.findFirst({ + select: { + siteLoadingLogo: true, + siteName: true, + }, + orderBy: { settingID: "desc" }, + }); + + return { + statusCode: 200, + message: "Success", + data: { + siteLoadingLogo: settings?.siteLoadingLogo || '', + siteName: settings?.siteName || 'corradAF', + }, + }; + } + + return { + statusCode: 405, + message: "Method not allowed", + }; + } catch (error) { + console.error("Loading logo API error:", error); + + return { + statusCode: 500, + message: "Internal server error", + error: error.message, + }; + } finally { + await prisma.$disconnect(); + } +}); \ No newline at end of file diff --git a/server/api/devtool/config/site-settings.js b/server/api/devtool/config/site-settings.js new file mode 100644 index 0000000..753bb6c --- /dev/null +++ b/server/api/devtool/config/site-settings.js @@ -0,0 +1,217 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + const method = getMethod(event); + + try { + if (method === "GET") { + // Get site settings + let settings = await prisma.site_settings.findFirst({ + orderBy: { settingID: "desc" }, + }); + + // If no settings exist, create default ones + if (!settings) { + settings = await prisma.site_settings.create({ + data: { + siteName: "corradAF", + siteDescription: "corradAF Base Project", + themeMode: "biasa", + showSiteNameInHeader: true, + seoRobots: "index, follow", + seoTwitterCard: "summary_large_image", + settingCreatedDate: new Date(), + settingModifiedDate: new Date(), + }, + }); + } + + // Transform data to match new structure + const transformedSettings = { + siteName: settings.siteName || "corradAF", + siteNameFontSize: settings.siteNameFontSize || 18, + siteDescription: settings.siteDescription || "corradAF Base Project", + siteLogo: settings.siteLogo || "", + siteLoadingLogo: settings.siteLoadingLogo || "", + siteFavicon: settings.siteFavicon || "", + siteLoginLogo: settings.siteLoginLogo || "", + showSiteNameInHeader: settings.showSiteNameInHeader !== false, + customCSS: settings.customCSS || "", + selectedTheme: settings.themeMode || "biasa", // Use themeMode as selectedTheme + customThemeFile: settings.customThemeFile || "", + currentFont: settings.currentFont || "", + fontSource: settings.fontSource || "", + // SEO fields + seoTitle: settings.seoTitle || "", + seoDescription: settings.seoDescription || "", + seoKeywords: settings.seoKeywords || "", + seoAuthor: settings.seoAuthor || "", + seoOgImage: settings.seoOgImage || "", + seoTwitterCard: settings.seoTwitterCard || "summary_large_image", + seoCanonicalUrl: settings.seoCanonicalUrl || "", + seoRobots: settings.seoRobots || "index, follow", + seoGoogleAnalytics: settings.seoGoogleAnalytics || "", + seoGoogleTagManager: settings.seoGoogleTagManager || "", + seoFacebookPixel: settings.seoFacebookPixel || "" + }; + + return { + statusCode: 200, + message: "Success", + data: transformedSettings, + }; + } + + if (method === "POST") { + let body; + try { + body = await readBody(event); + } catch (bodyError) { + console.error("Error reading request body:", bodyError); + return { + statusCode: 400, + message: "Invalid request body", + error: bodyError.message, + }; + } + + // Validate required fields + if (!body || typeof body !== 'object') { + return { + statusCode: 400, + message: "Request body must be a valid JSON object", + }; + } + + // Check if settings exist + const existingSettings = await prisma.site_settings.findFirst(); + + // Prepare data for database (use themeMode instead of selectedTheme) + // Filter out undefined values to avoid database errors + const dbData = {}; + + // Only add fields that are not undefined + if (body.siteName !== undefined) dbData.siteName = body.siteName; + if (body.siteNameFontSize !== undefined) dbData.siteNameFontSize = body.siteNameFontSize; + if (body.siteDescription !== undefined) dbData.siteDescription = body.siteDescription; + if (body.siteLogo !== undefined) dbData.siteLogo = body.siteLogo; + if (body.siteLoadingLogo !== undefined) dbData.siteLoadingLogo = body.siteLoadingLogo; + if (body.siteFavicon !== undefined) dbData.siteFavicon = body.siteFavicon; + if (body.siteLoginLogo !== undefined) dbData.siteLoginLogo = body.siteLoginLogo; + if (body.showSiteNameInHeader !== undefined) dbData.showSiteNameInHeader = body.showSiteNameInHeader; + if (body.customCSS !== undefined) dbData.customCSS = body.customCSS; + if (body.selectedTheme !== undefined) dbData.themeMode = body.selectedTheme; + if (body.customThemeFile !== undefined) dbData.customThemeFile = body.customThemeFile; + if (body.currentFont !== undefined) dbData.currentFont = body.currentFont; + if (body.fontSource !== undefined) dbData.fontSource = body.fontSource; + if (body.seoTitle !== undefined) dbData.seoTitle = body.seoTitle; + if (body.seoDescription !== undefined) dbData.seoDescription = body.seoDescription; + if (body.seoKeywords !== undefined) dbData.seoKeywords = body.seoKeywords; + if (body.seoAuthor !== undefined) dbData.seoAuthor = body.seoAuthor; + if (body.seoOgImage !== undefined) dbData.seoOgImage = body.seoOgImage; + if (body.seoTwitterCard !== undefined) dbData.seoTwitterCard = body.seoTwitterCard; + if (body.seoCanonicalUrl !== undefined) dbData.seoCanonicalUrl = body.seoCanonicalUrl; + if (body.seoRobots !== undefined) dbData.seoRobots = body.seoRobots; + if (body.seoGoogleAnalytics !== undefined) dbData.seoGoogleAnalytics = body.seoGoogleAnalytics; + if (body.seoGoogleTagManager !== undefined) dbData.seoGoogleTagManager = body.seoGoogleTagManager; + if (body.seoFacebookPixel !== undefined) dbData.seoFacebookPixel = body.seoFacebookPixel; + + dbData.settingModifiedDate = new Date(); + + let settings; + if (existingSettings) { + // Update existing settings + settings = await prisma.site_settings.update({ + where: { settingID: existingSettings.settingID }, + data: dbData, + }); + } else { + // Create new settings + settings = await prisma.site_settings.create({ + data: { + ...dbData, + settingCreatedDate: new Date(), + }, + }); + } + + // Transform response to match new structure + const transformedSettings = { + siteName: settings.siteName || "corradAF", + siteNameFontSize: settings.siteNameFontSize || 18, + siteDescription: settings.siteDescription || "corradAF Base Project", + siteLogo: settings.siteLogo || "", + siteLoadingLogo: settings.siteLoadingLogo || "", + siteFavicon: settings.siteFavicon || "", + siteLoginLogo: settings.siteLoginLogo || "", + showSiteNameInHeader: settings.showSiteNameInHeader !== false, + customCSS: settings.customCSS || "", + selectedTheme: settings.themeMode || "biasa", // Use themeMode as selectedTheme + customThemeFile: settings.customThemeFile || "", + currentFont: settings.currentFont || "", + fontSource: settings.fontSource || "", + // SEO fields + seoTitle: settings.seoTitle || "", + seoDescription: settings.seoDescription || "", + seoKeywords: settings.seoKeywords || "", + seoAuthor: settings.seoAuthor || "", + seoOgImage: settings.seoOgImage || "", + seoTwitterCard: settings.seoTwitterCard || "summary_large_image", + seoCanonicalUrl: settings.seoCanonicalUrl || "", + seoRobots: settings.seoRobots || "index, follow", + seoGoogleAnalytics: settings.seoGoogleAnalytics || "", + seoGoogleTagManager: settings.seoGoogleTagManager || "", + seoFacebookPixel: settings.seoFacebookPixel || "" + }; + + return { + statusCode: 200, + message: "Settings updated successfully", + data: transformedSettings, + }; + } + + return { + statusCode: 405, + message: "Method not allowed", + }; + } catch (error) { + console.error("Site settings API error:", error); + + // Provide more specific error messages + if (error.code === 'P2002') { + return { + statusCode: 400, + message: "Duplicate entry error", + error: error.message, + }; + } + + if (error.code === 'P2025') { + return { + statusCode: 404, + message: "Record not found", + error: error.message, + }; + } + + if (error.code && error.code.startsWith('P')) { + return { + statusCode: 400, + message: "Database error", + error: error.message, + code: error.code, + }; + } + + return { + statusCode: 500, + message: "Internal server error", + error: error.message, + }; + } finally { + await prisma.$disconnect(); + } +}); \ No newline at end of file diff --git a/server/api/devtool/config/upload-file.js b/server/api/devtool/config/upload-file.js new file mode 100644 index 0000000..b3bf422 --- /dev/null +++ b/server/api/devtool/config/upload-file.js @@ -0,0 +1,134 @@ +import fs from "fs"; +import path from "path"; +import { v4 as uuidv4 } from "uuid"; + +export default defineEventHandler(async (event) => { + const method = getMethod(event); + + if (method !== "POST") { + return { + statusCode: 405, + message: "Method not allowed", + }; + } + + try { + const form = await readMultipartFormData(event); + + if (!form || form.length === 0) { + return { + statusCode: 400, + message: "No file uploaded", + }; + } + + const file = form[0]; + const fileType = form.find(field => field.name === 'type')?.data?.toString() || 'logo'; + + // Validate file type + const allowedTypes = { + logo: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml'], + 'loading-logo': ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml'], + 'login-logo': ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml'], + favicon: ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png'], + 'og-image': ['image/jpeg', 'image/jpg', 'image/png'], + theme: ['text/css', 'application/octet-stream'] + }; + + if (!allowedTypes[fileType] || !allowedTypes[fileType].includes(file.type)) { + return { + statusCode: 400, + message: `Invalid file type for ${fileType}. Allowed types: ${allowedTypes[fileType].join(', ')}`, + }; + } + + let uploadDir, fileUrl; + + // Determine upload directory based on file type + if (fileType === 'theme') { + // Theme files go to assets/style/css + uploadDir = path.join(process.cwd(), 'assets', 'style', 'css'); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + // Generate unique filename for theme + const fileExtension = path.extname(file.filename || ''); + const uniqueFilename = `custom-theme-${uuidv4()}${fileExtension}`; + const filePath = path.join(uploadDir, uniqueFilename); + + // Save file + fs.writeFileSync(filePath, file.data); + + // Return relative path for theme files + fileUrl = `/assets/style/css/${uniqueFilename}`; + } else { + // Logo, loading-logo, favicon, and og-image files go to public/uploads + uploadDir = path.join(process.cwd(), 'public', 'uploads', 'site-settings'); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const fileExtension = path.extname(file.filename || ''); + let baseFilename; + + switch (fileType) { + case 'logo': + baseFilename = 'site-logo'; + break; + case 'loading-logo': + baseFilename = 'loading-logo'; + break; + case 'login-logo': + baseFilename = 'login-logo'; + break; + case 'favicon': + baseFilename = 'favicon'; + break; + case 'og-image': + baseFilename = 'og-image'; + break; + default: + // This case should ideally not be reached if fileType is validated earlier + // and is one of the image types. + // However, as a fallback, use the fileType itself or a generic name. + // For safety, and to avoid using uuidv4 for these specific types as requested, + // we should ensure this path isn't taken for the specified image types. + // If an unexpected fileType gets here, it might be better to error or use a UUID. + // For now, we'll stick to the primary requirement of fixed names for specified types. + // If we need UUID for other non-logo image types, that logic can be added. + // console.warn(`Unexpected fileType received: ${fileType} for non-theme upload.`); + // For simplicity, if it's an image type not explicitly handled, it will get a name like 'unknown-type.ext' + baseFilename = fileType; + } + + const filenameWithExt = `${baseFilename}${fileExtension}`; + const filePath = path.join(uploadDir, filenameWithExt); + + // Save file (overwrites if exists) + fs.writeFileSync(filePath, file.data); + + // Return file URL + fileUrl = `/uploads/site-settings/${filenameWithExt}`; + } + + return { + statusCode: 200, + message: "File uploaded successfully", + data: { + filename: path.basename(fileUrl), + url: fileUrl, + type: fileType, + size: file.data.length, + }, + }; + + } catch (error) { + console.error("Upload error:", error); + return { + statusCode: 500, + message: "Internal server error", + error: error.message, + }; + } +}); \ No newline at end of file diff --git a/server/api/devtool/content/canvas/file-code.js b/server/api/devtool/content/canvas/file-code.js new file mode 100644 index 0000000..7a01349 --- /dev/null +++ b/server/api/devtool/content/canvas/file-code.js @@ -0,0 +1,48 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + const query = await getQuery(event); + + let code = ""; + // console.log(query.path); + + try { + // Get vue code from path in query + const filePath = path.join(process.cwd() + "/pages/", query.path + ".vue"); + try { + code = fs.readFileSync(filePath, "utf8"); + return { + statusCode: 200, + message: "Code successfully loaded", + data: code, + }; + } catch (error) {} + + // Check if there is path with index.vue + const filePathIndex = path.join( + process.cwd() + "/pages/", + query.path + "/index.vue" + ); + + code = fs.readFileSync(filePathIndex, "utf8"); + + // Only get the template part of the code and make sure its from the first template tag to the last template tag + code = code.substring( + code.indexOf("") + ); + + return { + statusCode: 200, + message: "Code successfully loaded", + data: code, + mode: "index", + }; + } catch (error) { + return { + statusCode: 500, + message: "File not found", + }; + } +}); diff --git a/server/api/devtool/content/code/file-code.js b/server/api/devtool/content/code/file-code.js new file mode 100644 index 0000000..84b752c --- /dev/null +++ b/server/api/devtool/content/code/file-code.js @@ -0,0 +1,42 @@ +import fs from "fs"; +import path from "path"; + +export default defineEventHandler(async (event) => { + const query = await getQuery(event); + + let code = ""; + // console.log(query.path); + + try { + // Get vue code from path in query + const filePath = path.join(process.cwd() + "/pages/", query.path + ".vue"); + try { + code = fs.readFileSync(filePath, "utf8"); + return { + statusCode: 200, + message: "Code successfully loaded", + data: code, + }; + } catch (error) {} + + // Check if there is path with index.vue + const filePathIndex = path.join( + process.cwd() + "/pages/", + query.path + "/index.vue" + ); + + code = fs.readFileSync(filePathIndex, "utf8"); + + return { + statusCode: 200, + message: "Code successfully loaded", + data: code, + mode: "index", + }; + } catch (error) { + return { + statusCode: 500, + message: "File not found", + }; + } +}); diff --git a/server/api/devtool/content/code/linter.js b/server/api/devtool/content/code/linter.js new file mode 100644 index 0000000..85554c0 --- /dev/null +++ b/server/api/devtool/content/code/linter.js @@ -0,0 +1,450 @@ +import { ESLint } from "eslint"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + + try { + if (body.code === undefined) { + return { + statusCode: 400, + message: "Bad Request", + }; + } + + const code = body.code; + + // Extract script and template content once + const scriptContent = + code.match(/]*>([\s\S]*?)<\/script>/)?.[1] || ""; + const templateContent = code.match(/