add dashboard, feedback, faq

This commit is contained in:
Billy Marcello
2025-11-22 19:21:04 +07:00
parent fcdbf7d664
commit 96ee371975
16 changed files with 1626 additions and 64 deletions
+1
View File
@@ -1 +1,2 @@
VITE_API_BASE_URL= VITE_API_BASE_URL=
VITE_RECAPTCHA_SITE_KEY
+371 -48
View File
@@ -13,12 +13,14 @@
"@tiptap/starter-kit": "^2.23.0", "@tiptap/starter-kit": "^2.23.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"froala-editor": "^4.5.2", "froala-editor": "^4.5.2",
"lucide-react": "^0.553.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-froala-wysiwyg": "^4.5.2", "react-froala-wysiwyg": "^4.5.2",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha": "^3.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"recharts": "^3.4.1",
"tailwindcss": "^4.1.6" "tailwindcss": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
@@ -871,13 +873,12 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.3.3", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.15.1", "@eslint/core": "^0.15.2",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@@ -885,11 +886,10 @@
} }
}, },
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.15.1", "version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15"
}, },
@@ -1071,6 +1071,31 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@reduxjs/toolkit": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz",
"integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.2.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@remirror/core-constants": { "node_modules/@remirror/core-constants": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
@@ -1344,6 +1369,16 @@
"win32" "win32"
] ]
}, },
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
@@ -2036,6 +2071,60 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2075,7 +2164,7 @@
"version": "19.1.8", "version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -2453,17 +2542,15 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.10.0", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -2535,7 +2622,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -2601,6 +2687,14 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2625,7 +2719,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@@ -2681,9 +2774,119 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2702,6 +2905,11 @@
} }
} }
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2713,7 +2921,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@@ -2731,7 +2938,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -2777,7 +2983,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -2786,7 +2991,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -2795,7 +2999,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
}, },
@@ -2807,7 +3010,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6", "get-intrinsic": "^1.2.6",
@@ -2818,6 +3020,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-toolkit": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz",
"integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.5", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
@@ -3058,6 +3269,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3203,10 +3419,9 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@@ -3242,7 +3457,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -3261,7 +3475,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1", "es-define-property": "^1.0.1",
@@ -3285,7 +3498,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0" "es-object-atoms": "^1.0.0"
@@ -3324,7 +3536,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -3359,7 +3570,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -3371,7 +3581,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
}, },
@@ -3386,7 +3595,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@@ -3413,6 +3621,15 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3440,6 +3657,14 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3496,11 +3721,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@@ -3861,6 +4085,14 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.553.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.553.0.tgz",
"integrity": "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@@ -3891,7 +4123,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -3929,7 +4160,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -3938,7 +4168,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
}, },
@@ -4519,6 +4748,28 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -4567,6 +4818,53 @@
"react-dom": ">=18" "react-dom": ">=18"
} }
}, },
"node_modules/recharts": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz",
"integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==",
"workspaces": [
"www"
],
"dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4781,6 +5079,11 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -4963,11 +5266,31 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
+2
View File
@@ -15,12 +15,14 @@
"@tiptap/starter-kit": "^2.23.0", "@tiptap/starter-kit": "^2.23.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"froala-editor": "^4.5.2", "froala-editor": "^4.5.2",
"lucide-react": "^0.553.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-froala-wysiwyg": "^4.5.2", "react-froala-wysiwyg": "^4.5.2",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha": "^3.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"recharts": "^3.4.1",
"tailwindcss": "^4.1.6" "tailwindcss": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
+10
View File
@@ -20,6 +20,11 @@ import ListLowongan from "./Pages/ListLowongan";
import ListProduct from "./Pages/ListProduct"; import ListProduct from "./Pages/ListProduct";
import AddProductHumic from "./Pages/AddProductHumic"; import AddProductHumic from "./Pages/AddProductHumic";
import EditProductHumic from "./Pages/EditProductHumic"; import EditProductHumic from "./Pages/EditProductHumic";
import ListFeedback from "./Pages/ListFeedback";
import AddFeedback from "./Pages/AddFeedback";
import ListFaq from "./Pages/ListFaq";
import AddFaq from "./Pages/AddFAQ";
import Dashboard from "./Pages/Dashboard";
function App() { function App() {
return ( return (
@@ -51,6 +56,11 @@ function App() {
<Route path="/partnership-admin" element={<PartnershipsList />} /> <Route path="/partnership-admin" element={<PartnershipsList />} />
<Route path="/add-partnership" element={<AddPartnershipAdmin />} /> <Route path="/add-partnership" element={<AddPartnershipAdmin />} />
<Route path="/edit-partnership/:id" element={<EditPartnerships />} /> <Route path="/edit-partnership/:id" element={<EditPartnerships />} />
<Route path="/feedback-list" element={<ListFeedback />} />
<Route path="/add-feedback" element={<AddFeedback />} />
<Route path="/faq-list" element={<ListFaq />} />
<Route path="/add-faq" element={<AddFaq />} />
<Route path="/dashboard" element={<Dashboard />} />
</Route> </Route>
</Routes> </Routes>
</Router> </Router>
+8 -2
View File
@@ -52,9 +52,15 @@ const Footer = () => {
<p>Tentang</p> <p>Tentang</p>
<p>Life at Humic</p> <p>Life at Humic</p>
</div> </div>
<div className="sec-2 flex flex-col gap-3"> <div className="sec-2 flex flex-col gap-3 hover:cursor-pointer">
<p className="font-bold">Lainnya</p> <p className="font-bold">Lainnya</p>
<p>Humic Website</p> <p
onClick={() =>
window.open("https://humic.telkomuniversity.ac.id", "_blank")
}
>
Humic Website
</p>
<p>Our Activity</p> <p>Our Activity</p>
<p>Life at Humic</p> <p>Life at Humic</p>
<p <p
+92 -1
View File
@@ -11,7 +11,37 @@ const SidebarAdmin = () => {
<div className="list-of-sidebar flex flex-col p-7 gap-4"> <div className="list-of-sidebar flex flex-col p-7 gap-4">
<div <div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${ className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/produc-lList" location.pathname === "/product-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA] "
}`}
onClick={() => navigate("/dashboard")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="size-8 text-[#2C5282]"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3 L5 8 A4 4 0 0 0 3 11 L3 15 A4 4 0 0 0 7 19 L17 19 A4 4 0 0 0 21 15 L21 11 A4 4 0 0 0 19 8 L12 3 Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 13 v4"
/>
</svg>
<h2 className="text-[#2C5282]">Dashboard</h2>
</div>
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/product-list"
? "bg-[#D3DFEA] font-bold" ? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA] " : "hover:bg-[#D3DFEA] "
}`} }`}
@@ -108,6 +138,67 @@ const SidebarAdmin = () => {
<h2 className="text-[#2C5282]">Pelamar</h2> <h2 className="text-[#2C5282]">Pelamar</h2>
</div> </div>
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/feedbacks-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA]"
}`}
onClick={() => navigate("/feedback-list")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-8 text-[#2C5282]"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 4h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-5l-3 3-3-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
/>
<circle cx="8" cy="10" r="1.2" fill="currentColor" />
<circle cx="12" cy="10" r="1.2" fill="currentColor" />
<circle cx="16" cy="10" r="1.2" fill="currentColor" />
</svg>
<h2 className="text-[#2C5282]">Feedback</h2>
</div>
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/faq-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA]"
}`}
onClick={() => navigate("/faq-list")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-8 text-[#2C5282]"
>
{/* Balon chat */}
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 4h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-5l-3 3-3-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
/>
{/* Tanda tanya */}
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 9.5a2.25 2.25 0 1 1 4.5 0c0 1.25-.75 1.75-1.5 1.85s-1 1-1 1.75"
/>
<circle cx="12" cy="16" r="0.6" fill="currentColor" />
</svg>
<h2 className="text-[#2C5282]">FAQ</h2>
</div>
</div> </div>
</div> </div>
+114
View File
@@ -0,0 +1,114 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import SidebarAdmin from "../Layout/SidebarAdmin";
import NavbarAdmin from "../Layout/NavbarAdmin";
import "froala-editor/js/plugins.pkgd.min.js";
import "froala-editor/css/froala_editor.pkgd.min.css";
import "froala-editor/css/froala_style.min.css";
import FroalaEditor from "react-froala-wysiwyg";
import axios from "axios";
const AddFaq = () => {
const [questions, setQuestions] = useState("");
const [answers, setAnswers] = useState("");
const navigate = useNavigate();
const handleSubmit = async () => {
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan.");
return;
}
if (!questions || !answers) {
alert("Semua field wajib diisi.");
return;
}
try {
const response = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/faq-api/add`,
{
pertanyaan: questions,
jawaban: answers,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (response.data.status) {
alert("Faq berhasil ditambahkan!");
navigate("/faq-list");
} else {
alert(response.data.message || "Gagal menambahkan faq.");
}
} catch (error: any) {
if (error.response) {
console.log("Error response data:", error.response.data);
}
console.error("Error adding faq:", error);
alert("Terjadi kesalahan saat menambahkan faq.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex justify-between items-center mb-8">
<h2 className="text-xl font-semibold">
Tambah Frequently Asked Questions
</h2>
<button
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
onClick={handleSubmit}
>
Submit
</button>
</div>
<form className="flex flex-col gap-6 max-w-full">
{/* Questions */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Questions</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={questions}
onChange={(e) => setQuestions(e.target.value)}
placeholder="Tulis pertanyaan kamu..."
/>
</div>
{/* Answers */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Answers</label>
<FroalaEditor
tag="textarea"
model={answers}
onModelChange={(model: any) => setAnswers(model)}
config={{
placeholderText: "Tulis jawaban kamu...",
charCounterCount: true,
toolbarSticky: true,
heightMin: 200,
}}
/>
</div>
</form>
</div>
</div>
</div>
);
};
export default AddFaq;
+178
View File
@@ -0,0 +1,178 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import SidebarAdmin from "../Layout/SidebarAdmin";
import NavbarAdmin from "../Layout/NavbarAdmin";
import "froala-editor/js/plugins.pkgd.min.js";
import "froala-editor/css/froala_editor.pkgd.min.css";
import "froala-editor/css/froala_style.min.css";
import FroalaEditor from "react-froala-wysiwyg";
import axios from "axios";
const AddFeedback = () => {
const [posisi, setPosisi] = useState("");
const [universitas, setUniversitas] = useState("");
const [nama, setNama] = useState("");
const [image, setImage] = useState<File | null>(null);
const [batch, setBatch] = useState("");
const [feedback, setFeedbacks] = useState("");
const [year, setYear] = useState("");
const navigate = useNavigate();
const handleSubmit = async () => {
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan.");
return;
}
if (!nama || !universitas || !posisi || !batch || !year || !feedback) {
alert("Semua field wajib diisi.");
return;
}
try {
const formData = new FormData();
formData.append("nama", nama);
formData.append("universitas", universitas);
formData.append("posisi", posisi);
formData.append("batch", batch);
formData.append("tahun", year);
formData.append("pesan", feedback);
if (image) formData.append("image", image);
const response = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/add`,
formData,
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
}
);
if (response.data.status) {
alert("Feedback berhasil ditambahkan!");
navigate("/feedback-list");
} else {
alert(response.data.message || "Gagal menambahkan feedback.");
}
} catch (error: any) {
console.error("Error adding feedback:", error);
alert("Terjadi kesalahan saat menambahkan feedback.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex justify-between items-center mb-8">
<h2 className="text-xl font-semibold">Tambah Feedback</h2>
<button
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
onClick={handleSubmit}
>
Submit
</button>
</div>
<form className="flex flex-col gap-6 max-w-full">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Nama</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={nama}
onChange={(e) => setNama(e.target.value)}
placeholder="John Doe"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Universitas</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={universitas}
onChange={(e) => setUniversitas(e.target.value)}
placeholder="Telkom University Bandung"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Posisi</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={posisi}
onChange={(e) => setPosisi(e.target.value)}
placeholder="UI/UX Designer"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Batch</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={batch}
onChange={(e) => setBatch(e.target.value)}
placeholder="5"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Tahun</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={year}
onChange={(e) => setYear(e.target.value)}
placeholder="2025"
/>
</div>
<div className="flex flex-col gap-2 col-span-2">
<label className="text-sm font-medium">Image anda</label>
<input
type="file"
accept="image/*"
onChange={(e) =>
setImage(e.target.files ? e.target.files[0] : null)
}
className="border border-gray-300 rounded-lg p-3"
/>
</div>
</div>
{/* Isi Konten */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Isi Konten</label>
<FroalaEditor
tag="textarea"
model={feedback}
onModelChange={(model: any) => setFeedbacks(model)}
config={{
placeholderText: "Tulis feedback kamu...",
charCounterCount: true,
toolbarSticky: true,
heightMin: 200,
}}
/>
</div>
</form>
</div>
</div>
</div>
);
};
export default AddFeedback;
+372
View File
@@ -0,0 +1,372 @@
import React, { useEffect, useState } from "react";
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import { Search, ChevronDown, Users, UserCheck, UserX, Clock } from "lucide-react";
import NavbarAdmin from "../Layout/NavbarAdmin";
import SidebarAdmin from "../Layout/SidebarAdmin";
type ChartDataItem = {
name: string;
value: number;
};
type StatCard = {
title: string;
value: number;
subtitle: string;
icon: React.ReactNode;
iconColor: string;
};
type DashboardApiResponse = {
totalPendaftar: number;
totalDiterima: number;
totalDitolak: number;
totalDiproses: number;
universitas: ChartDataItem[];
negara: ChartDataItem[];
};
const UNIVERSITAS_COLORS = ["#001a66", "#334d99", "#6699cc", "#99b3ff"];
const PENERIMAAN_COLORS = ["#0088ff", "#00cc00", "#ff3333"];
const NEGARA_COLORS = ["#cc3333", "#dd6666", "#ee9999", "#f0b3b3", "#f5cccc"];
const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
const [statCards, setStatCards] = useState<StatCard[]>([]);
const [universitasData, setUniversitasData] = useState<ChartDataItem[]>([]);
const [penerimaanData, setPenerimaanData] = useState<ChartDataItem[]>([]);
const [negaraData, setNegaraData] = useState<ChartDataItem[]>([]);
useEffect(() => {
const fetchDashboardData = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get`
);
const result = await response.json();
if (!response.ok) {
console.error("Gagal mengambil data dashboard:", result.message);
setErrorMessage(result.message || "Gagal mengambil data dashboard.");
setLoading(false);
return;
}
const data: DashboardApiResponse = result.data;
const statCardsFromApi: StatCard[] = [
{
title: "Total Pendaftar",
value: data.totalPendaftar,
subtitle: "+200 dari batch sebelumnya", // sesuaikan bila BE sudah support
icon: <Users size={24} />,
iconColor: "#0088ff",
},
{
title: "Total Diterima",
value: data.totalDiterima,
subtitle: `${(
(data.totalDiterima / (data.totalPendaftar || 1)) *
100
).toFixed(1)}% acceptance rate`,
icon: <UserCheck size={24} />,
iconColor: "#00cc00",
},
{
title: "Total Ditolak",
value: data.totalDitolak,
subtitle: `${(
(data.totalDitolak / (data.totalPendaftar || 1)) *
100
).toFixed(1)}% rejection rate`,
icon: <UserX size={24} />,
iconColor: "#ff3333",
},
{
title: "Sedang Diproses",
value: data.totalDiproses,
subtitle: `${(
(data.totalDiproses / (data.totalPendaftar || 1)) *
100
).toFixed(1)}% pending review`,
icon: <Clock size={24} />,
iconColor: "#ffaa00",
},
];
const penerimaanFromApi: ChartDataItem[] = [
{ name: "Total Pendaftar", value: data.totalPendaftar },
{ name: "Total Diterima", value: data.totalDiterima },
{ name: "Total Ditolak", value: data.totalDitolak },
];
setStatCards(statCardsFromApi);
setUniversitasData(data.universitas || []);
setPenerimaanData(penerimaanFromApi);
setNegaraData(data.negara || []);
setLoading(false);
} catch (error) {
console.error("Terjadi kesalahan saat mengambil data dashboard:", error);
setErrorMessage("Terjadi kesalahan saat mengambil data dashboard.");
setLoading(false);
}
};
fetchDashboardData();
}, []);
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="w-full flex-col flex-wrap bg-white p-10">
{/* Header Section */}
<div className="mb-[50px] flex flex-row items-center justify-between">
<h2 className="text-[24px] font-semibold">Dashboard</h2>
<div className="flex items-center gap-4">
{/* Batch Filter */}
<button className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px] text-gray-700 hover:bg-gray-50">
Batch 3 - 2025
<ChevronDown size={18} />
</button>
{/* Search Box */}
<div className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px]">
<Search size={18} className="text-gray-400" />
<input
type="text"
placeholder="Nama, Jurusan,"
className="w-52 text-sm text-gray-700 placeholder-gray-400 outline-none"
/>
</div>
</div>
</div>
{/* Loading / Error State */}
{loading && (
<p className="text-center text-gray-600">Memuat data dashboard...</p>
)}
{!loading && errorMessage && (
<p className="text-center text-red-500">{errorMessage}</p>
)}
{!loading && !errorMessage && (
<>
{/* Stat Cards */}
<div className="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{statCards.map((card, index) => (
<div
key={index}
className="rounded-lg border border-gray-300 bg-white p-6"
>
<div className="mb-4 flex items-center gap-3">
<div
className="rounded-lg bg-gray-100 p-2"
style={{ color: card.iconColor }}
>
{card.icon}
</div>
<h3 className="text-sm font-medium text-blue-600">
{card.title}
</h3>
</div>
<div className="mb-2">
<p className="text-2xl font-bold text-gray-900">
{card.value}
</p>
</div>
<p className="text-xs" style={{ color: card.iconColor }}>
{card.subtitle}
</p>
</div>
))}
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Universitas Chart */}
<div className="rounded-lg border border-gray-300 bg-white p-6">
<h2 className="mb-6 text-lg font-bold text-gray-900">
Universitas
</h2>
<div className="mb-6 flex justify-center">
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={universitasData}
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={100}
paddingAngle={2}
dataKey="value"
>
{universitasData.map((_, index) => (
<Cell
key={`universitas-cell-${index}`}
fill={
UNIVERSITAS_COLORS[
index % UNIVERSITAS_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-3 text-sm">
{universitasData.map((item, index) => (
<div
key={`universitas-legend-${index}`}
className="flex items-center justify-between"
>
<div className="flex items-center gap-2">
<div
className="h-3 w-3 rounded"
style={{
backgroundColor:
UNIVERSITAS_COLORS[
index % UNIVERSITAS_COLORS.length
],
}}
/>
<span className="text-gray-700">{item.name}</span>
</div>
<span className="text-sm font-semibold text-gray-900">
{item.value}%
</span>
</div>
))}
</div>
</div>
{/* Right Column - Penerimaan & Negara */}
<div className="space-y-6">
{/* Penerimaan Chart */}
<div className="rounded-lg border border-gray-300 bg-white p-6">
<h2 className="mb-6 text-lg font-bold text-gray-900">
Penerimaan
</h2>
<div className="mb-6 flex justify-center">
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={penerimaanData}
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={70}
paddingAngle={2}
dataKey="value"
>
{penerimaanData.map((_, index) => (
<Cell
key={`penerimaan-cell-${index}`}
fill={
PENERIMAAN_COLORS[
index % PENERIMAAN_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-2 text-sm">
{penerimaanData.map((item, index) => (
<div
key={`penerimaan-legend-${index}`}
className="flex items-center gap-2"
>
<div
className="h-3 w-3 rounded"
style={{
backgroundColor:
PENERIMAAN_COLORS[
index % PENERIMAAN_COLORS.length
],
}}
/>
<span className="text-gray-700">{item.name}</span>
</div>
))}
</div>
</div>
{/* Negara Chart */}
<div className="rounded-lg border border-gray-300 bg-white p-6">
<h2 className="mb-6 text-lg font-bold text-gray-900">
Negara
</h2>
<div className="mb-6 flex justify-center">
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={negaraData}
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={70}
paddingAngle={2}
dataKey="value"
>
{negaraData.map((_, index) => (
<Cell
key={`negara-cell-${index}`}
fill={
NEGARA_COLORS[
index % NEGARA_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-2 text-sm">
{negaraData.map((item, index) => (
<div
key={`negara-legend-${index}`}
className="flex items-center gap-2"
>
<div
className="h-3 w-3 rounded"
style={{
backgroundColor:
NEGARA_COLORS[index % NEGARA_COLORS.length],
}}
/>
<span className="text-gray-700">{item.name}</span>
</div>
))}
</div>
</div>
</div>
</div>
</>
)}
</div>
</div>
</div>
);
};
export default Dashboard;
+6 -2
View File
@@ -23,8 +23,10 @@ const DetailsProduct = () => {
fetch(`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/get/${id}`) fetch(`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/get/${id}`)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
if (data && data.data && data.data.length > 0) { if (data && data.data) {
setProduct(data.data[0]); setProduct(data.data);
} else {
setProduct(null);
} }
setLoading(false); setLoading(false);
}) })
@@ -43,6 +45,8 @@ const DetailsProduct = () => {
<div className="text-center mt-10 text-red-500">Data tidak ditemukan</div> <div className="text-center mt-10 text-red-500">Data tidak ditemukan</div>
); );
} }
console.log(`${import.meta.env.VITE_API_BASE_URL}${product.image_path}`);
return ( return (
<div className="body-details-of-product bg-[#F8F9FA] min-h-screen"> <div className="body-details-of-product bg-[#F8F9FA] min-h-screen">
+3 -3
View File
@@ -144,9 +144,9 @@ const EditPartnerships = () => {
<div className="flex flex-row"> <div className="flex flex-row">
<SidebarAdmin /> <SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full"> <div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="header-button flex justify-between items-center mb-[45px]"> <div className="flex justify-between items-center mb-[45px]">
<h2 className="text-[24px] font-semibold">Edit Partnership</h2> <h2 className="text-[24px] font-semibold">Edit Partnership</h2>
<div className="flex gap-4"> <div className="flex gap-4 pr-10">
<button <button
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
@@ -165,7 +165,7 @@ const EditPartnerships = () => {
</div> </div>
<form onSubmit={handleSubmit} className="flex flex-col gap-8 w-full"> <form onSubmit={handleSubmit} className="flex flex-col gap-8 w-full">
<div className="flex flex-row gap-16"> <div className="flex flex-row gap-10">
<div className="flex flex-col items-start gap-[15px]"> <div className="flex flex-col items-start gap-[15px]">
<h2 className="text-[16px] font-bold">Nama</h2> <h2 className="text-[16px] font-bold">Nama</h2>
<input <input
+4 -4
View File
@@ -57,7 +57,7 @@ const Internships = () => {
title: item.posisi, title: item.posisi,
location: item.lokasi, location: item.lokasi,
category: item.kelompok_peminatan, category: item.kelompok_peminatan,
status: item.status_lowongan === "dibuka" ? "Opened" : "Closed", status: item.status_lowongan === "dibuka" ? "Open" : "Closed",
statusColor: statusColor:
item.status_lowongan === "dibuka" ? "#4EAD00" : "#C3423F", item.status_lowongan === "dibuka" ? "#4EAD00" : "#C3423F",
isPaid: item.paid === "paid", isPaid: item.paid === "paid",
@@ -171,12 +171,12 @@ const Internships = () => {
<div <div
key={index} key={index}
className={`card-1 flex flex-col items-start p-5 lg:p-7 rounded-xl shadow-2xl ${ className={`card-1 flex flex-col items-start p-5 lg:p-7 rounded-xl shadow-2xl ${
intern.status === "Opened" intern.status === "Open"
? "bg-white cursor-pointer hover:shadow-xl transition" ? "bg-white cursor-pointer hover:shadow-xl transition"
: "bg-gray-200 cursor-not-allowed opacity-60" : "bg-gray-200 cursor-not-allowed opacity-60"
}`} }`}
onClick={() => { onClick={() => {
if (intern.status === "Opened") { if (intern.status === "Open") {
navigate(`/details/${intern.id}`); navigate(`/details/${intern.id}`);
} }
}} }}
@@ -189,7 +189,7 @@ const Internships = () => {
className="p-2 border-2 text-[12px] lg:text-[14px] font-bold rounded-4xl" className="p-2 border-2 text-[12px] lg:text-[14px] font-bold rounded-4xl"
style={{ style={{
backgroundColor: backgroundColor:
intern.status === "Opened" ? "#F6FFF4" : "#FAF0EF", intern.status === "Open" ? "#F6FFF4" : "#FAF0EF",
color: intern.statusColor, color: intern.statusColor,
borderColor: intern.statusColor, borderColor: intern.statusColor,
}} }}
+208
View File
@@ -0,0 +1,208 @@
import NavbarAdmin from "../Layout/NavbarAdmin";
import SidebarAdmin from "../Layout/SidebarAdmin";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
type Faq = {
id: number;
pertanyaan: string;
jawaban: string;
created_at: string;
};
const ListFaq = () => {
const navigate = useNavigate();
const [faq, setFaq] = useState<Faq[]>([]);
useEffect(() => {
const fetchFaq = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/faq-api/get`
);
const result = await response.json();
if (response.ok) {
setFaq(result.data);
} else {
console.error("Gagal mengambil data FAQ:", result.message);
}
} catch (error) {
console.error("Terjadi kesalahan:", error);
}
};
fetchFaq();
}, []);
const handleDelete = async (id: number) => {
const confirmDelete = confirm("Apakah Anda yakin ingin menghapus FAQ ini?");
if (!confirmDelete) return;
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan. Silakan login ulang.");
return;
}
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/faq-api/delete/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const result = await response.json();
if (response.ok) {
alert("Feedback berhasil dihapus!");
setFaq((prev) => prev.filter((faq) => faq.id !== id));
} else {
alert(result.message || "Gagal menghapus FAQ.");
}
} catch (error) {
console.error("Gagal menghapus:", error);
alert("Terjadi kesalahan saat menghapus FAQ.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex flex-row justify-between items-center mb-[50px]">
<h2 className="text-[24px] font-semibold">
Frequently Asked Questions
</h2>
<button
className="px-[20px] py-[10px] rounded-xl bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold"
onClick={() => navigate("/add-faq")}
>
Tambah
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="text-left">
<th className="px-6 py-3">Questions</th>
<th className="px-6 py-3">Answers</th>
<th className="px-6 py-3">Tanggal Unggah</th>
<th className="px-6 py-3">Aksi</th>
</tr>
</thead>
<tbody>
{faq.map((faq) => (
<tr key={faq.id} className="hover:bg-gray-50">
<td className="px-6 py-3">{faq.pertanyaan}</td>
<td className="px-6 py-3">
<div dangerouslySetInnerHTML={{ __html: faq.jawaban }} />
</td>
<td className="px-6 py-3">
{new Date(faq.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-3 flex gap-2">
{/* Tombol View */}
<button
className="bg-[#3BE21D] cursor-pointer text-white p-2 rounded-xl"
onClick={() =>
window.open(`/details-product/${faq.id}`, "_blank")
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
{/* Tombol Edit */}
<button
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
onClick={() => navigate(`/edit-faq/${faq.id}`)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 7.125 18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
/>
</svg>
</button>
{/* Tombol Hapus */}
<button
className="bg-[#E41E1E] text-white cursor-pointer p-2 rounded-xl"
onClick={() => handleDelete(faq.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
{faq.length === 0 && (
<p className="text-gray-500 mt-4 text-center">
Belum ada data FAQ.
</p>
)}
</div>
</div>
</div>
</div>
);
};
export default ListFaq;
+207
View File
@@ -0,0 +1,207 @@
import NavbarAdmin from "../Layout/NavbarAdmin";
import SidebarAdmin from "../Layout/SidebarAdmin";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
type Feedback = {
id: number;
nama: string;
image: string;
created_at: string;
};
const ListFeedback = () => {
const navigate = useNavigate();
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
useEffect(() => {
const fetchFeedbacks = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/get`
);
const result = await response.json();
if (response.ok) {
setFeedbacks(result.data);
} else {
console.error("Gagal mengambil data feedback:", result.message);
}
} catch (error) {
console.error("Terjadi kesalahan:", error);
}
};
fetchFeedbacks();
}, []);
const handleDelete = async (id: number) => {
const confirmDelete = confirm(
"Apakah Anda yakin ingin menghapus feedback ini?"
);
if (!confirmDelete) return;
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan. Silakan login ulang.");
return;
}
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/delete/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const result = await response.json();
if (response.ok) {
alert("Feedback berhasil dihapus!");
setFeedbacks((prev) => prev.filter((feedback) => feedback.id !== id));
} else {
alert(result.message || "Gagal menghapus feedback.");
}
} catch (error) {
console.error("Gagal menghapus:", error);
alert("Terjadi kesalahan saat menghapus feedback.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex flex-row justify-between items-center mb-[50px]">
<h2 className="text-[24px] font-semibold">Feedback</h2>
<button
className="px-[20px] py-[10px] rounded-xl bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold"
onClick={() => navigate("/add-feedback")}
>
Tambah
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="text-left">
<th className="px-6 py-3">Nama</th>
<th className="px-6 py-3">Foto</th>
<th className="px-6 py-3">Tanggal Unggah</th>
<th className="px-6 py-3">Aksi</th>
</tr>
</thead>
<tbody>
{feedbacks.map((feedback) => (
<tr key={feedback.id} className="hover:bg-gray-50">
<td className="px-6 py-3">{feedback.nama}</td>
<td className="px-6 py-3">
<img src={feedback.image} alt={feedback.nama} />
</td>
<td className="px-6 py-3">
{new Date(feedback.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-3 flex gap-2">
{/* Tombol View */}
<button
className="bg-[#3BE21D] cursor-pointer text-white p-2 rounded-xl"
onClick={() =>
window.open(`/details-product/${feedback.id}`, "_blank")
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
{/* Tombol Edit */}
<button
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
onClick={() =>
navigate(`/edit-product-humic/${feedback.id}`)
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 7.125 18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
/>
</svg>
</button>
{/* Tombol Hapus */}
<button
className="bg-[#E41E1E] text-white cursor-pointer p-2 rounded-xl"
onClick={() => handleDelete(feedback.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
{feedbacks.length === 0 && (
<p className="text-gray-500 mt-4 text-center">Belum ada data feedback.</p>
)}
</div>
</div>
</div>
</div>
);
};
export default ListFeedback;
+1 -1
View File
@@ -190,7 +190,7 @@ const ListProduct = () => {
</table> </table>
{projects.length === 0 && ( {projects.length === 0 && (
<p className="text-gray-500 mt-4">Belum ada data project.</p> <p className="text-gray-500 mt-4 text-center">Belum ada data project.</p>
)} )}
</div> </div>
</div> </div>
+49 -3
View File
@@ -17,7 +17,10 @@ const RegisterInternships = () => {
nama_belakang: "", nama_belakang: "",
email: "", email: "",
kontak: "", kontak: "",
universitas: "",
negara: "",
jurusan: "", jurusan: "",
batch: "",
motivasi: "", motivasi: "",
relevant_skills: "", relevant_skills: "",
cv: null as File | null, cv: null as File | null,
@@ -44,7 +47,10 @@ const RegisterInternships = () => {
} }
data.append("email", formData.email); data.append("email", formData.email);
data.append("kontak", formData.kontak); data.append("kontak", formData.kontak);
data.append("universitas", formData.universitas);
data.append("negara", formData.negara);
data.append("jurusan", formData.jurusan); data.append("jurusan", formData.jurusan);
data.append("batch", formData.batch);
data.append("motivasi", formData.motivasi); data.append("motivasi", formData.motivasi);
data.append("relevant_skills", formData.relevant_skills); data.append("relevant_skills", formData.relevant_skills);
data.append("g-recaptcha-response", recaptchaToken); data.append("g-recaptcha-response", recaptchaToken);
@@ -131,12 +137,13 @@ const RegisterInternships = () => {
<input <input
type="text" type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black" className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="John" placeholder="Muhammad Budi"
value={formData.nama_depan} value={formData.nama_depan}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, nama_depan: e.target.value }) setFormData({ ...formData, nama_depan: e.target.value })
} }
/> />
<p className="text-sm text-[#C3423F] mt-1">*required</p>
</div> </div>
<div> <div>
@@ -144,13 +151,13 @@ const RegisterInternships = () => {
<input <input
type="text" type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black" className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="Doe" placeholder="Pekerti"
value={formData.nama_belakang} value={formData.nama_belakang}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, nama_belakang: e.target.value }) setFormData({ ...formData, nama_belakang: e.target.value })
} }
/> />
<p className="text-sm text-[#C3423F] mt-1">*not required</p> <p className="text-sm text-[#C3423F] mt-1">*required</p>
</div> </div>
<div> <div>
@@ -179,6 +186,32 @@ const RegisterInternships = () => {
} }
/> />
</div> </div>
<div>
<label className="block mb-1 font-bold">Universitas</label>
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="Telkom University Bandung"
value={formData.universitas}
onChange={(e) =>
setFormData({ ...formData, universitas: e.target.value })
}
/>
</div>
<div>
<label className="block mb-1 font-bold">Negara</label>
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="Indonesia"
value={formData.negara}
onChange={(e) =>
setFormData({ ...formData, negara: e.target.value })
}
/>
</div>
<div> <div>
<label className="block mb-1 font-bold">Jurusan</label> <label className="block mb-1 font-bold">Jurusan</label>
@@ -207,6 +240,19 @@ const RegisterInternships = () => {
<p className="text-sm text-[#C3423F] mt-1">*separate with commas</p> <p className="text-sm text-[#C3423F] mt-1">*separate with commas</p>
</div> </div>
<div>
<label className="block mb-1 font-bold">Batch</label>
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="1"
value={formData.batch}
onChange={(e) =>
setFormData({ ...formData, batch: e.target.value })
}
/>
</div>
<div> <div>
<label className="block mb-1 font-bold"> <label className="block mb-1 font-bold">
CV (Curriculum Vitae) CV (Curriculum Vitae)