From ce6c1bc103cc7e24bd7924310a18a4fb6a1ba72f Mon Sep 17 00:00:00 2001 From: Alexandre78R Date: Wed, 25 Jun 2025 15:07:29 +0200 Subject: [PATCH 01/14] add data test globalTest page dahboard amin and crete request data --- backend/tests/singleton.ts | 3 +- frontend/Dockerfile | 6 +- .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 24 +++++++- .../src/requetes/queries/admin.queries.ts | 20 +++++++ frontend/src/types/graphql.ts | 55 +++++++++++++++++++ 5 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 frontend/src/requetes/queries/admin.queries.ts diff --git a/backend/tests/singleton.ts b/backend/tests/singleton.ts index f1296202..1e128ff4 100644 --- a/backend/tests/singleton.ts +++ b/backend/tests/singleton.ts @@ -1,5 +1,4 @@ import { PrismaClient } from "@prisma/client"; -import { mockDeep } from "jest-mock-extended"; // <-- Utilise jest-mock-extended -// import { vi } from "vitest"; // <-- Supprime ou commente cette ligne si tu n'utilises pas vi +import { mockDeep } from "jest-mock-extended"; export const prismaMock = mockDeep(); \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 56179eab..66e4a195 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -23,10 +23,10 @@ ENV NEXT_PUBLIC_JWT_SECRET=${NEXT_PUBLIC_JWT_SECRET} ARG JWT_SECRET ENV JWT_SECRET=${JWT_SECRET} -# CMD npm run dev +CMD npm run dev -RUN npm run build -CMD npm run start +# RUN npm run build +# CMD npm run start # RUN if [ "$NODE_ENV" = "production" ]; then npm run build; fi # CMD if [ "$NODE_ENV" = "production" ]; then npm run start; else npm run dev; fi \ No newline at end of file diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index f2daf5b8..79bf48fe 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -1,6 +1,28 @@ +import { useEffect, useState } from 'react'; +import { + useGetGlobalStatsQuery, + GetGlobalStatsQuery, +} from '@/types/graphql'; + + const Dashboard = (): React.ReactElement => { + const { data , loading } = useGetGlobalStatsQuery(); + const [stats, setStats] = useState(null); + + useEffect(() => { + // console.log("data", data); + if (data) { + setStats(data); + } else { + setStats(null); + } + }, [data]) + return ( -

Page Dashboard

+ <> +

Page Dashboard

+

{stats?.getGlobalStats.stats?.totalExperiences}

+ ) } diff --git a/frontend/src/requetes/queries/admin.queries.ts b/frontend/src/requetes/queries/admin.queries.ts new file mode 100644 index 00000000..f74d6859 --- /dev/null +++ b/frontend/src/requetes/queries/admin.queries.ts @@ -0,0 +1,20 @@ +import { gql } from "@apollo/client"; + +export const GET_GLOBAL_STATS = gql` + query GetGlobalStats { + getGlobalStats { + code + message + stats { + totalUsers + totalProjects + totalSkills + totalEducations + totalExperiences + usersByRoleAdmin + usersByRoleEditor + usersByRoleView + } + } + } +`; \ No newline at end of file diff --git a/frontend/src/types/graphql.ts b/frontend/src/types/graphql.ts index 8d903b89..aa056f41 100644 --- a/frontend/src/types/graphql.ts +++ b/frontend/src/types/graphql.ts @@ -589,6 +589,11 @@ export type MutationMutationVariables = Exact<{ export type MutationMutation = { __typename?: 'Mutation', login: { __typename?: 'LoginResponse', token?: string | null, message: string, code: number } }; +export type GetGlobalStatsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetGlobalStatsQuery = { __typename?: 'Query', getGlobalStats: { __typename?: 'GlobalStatsResponse', code: number, message: string, stats?: { __typename?: 'GlobalStats', totalUsers: number, totalProjects: number, totalSkills: number, totalEducations: number, totalExperiences: number, usersByRoleAdmin: number, usersByRoleEditor: number, usersByRoleView: number } | null } }; + export type GenerateCaptchaQueryVariables = Exact<{ [key: string]: never; }>; @@ -760,6 +765,56 @@ export function useMutationMutation(baseOptions?: Apollo.MutationHookOptions; export type MutationMutationResult = Apollo.MutationResult; export type MutationMutationOptions = Apollo.BaseMutationOptions; +export const GetGlobalStatsDocument = gql` + query GetGlobalStats { + getGlobalStats { + code + message + stats { + totalUsers + totalProjects + totalSkills + totalEducations + totalExperiences + usersByRoleAdmin + usersByRoleEditor + usersByRoleView + } + } +} + `; + +/** + * __useGetGlobalStatsQuery__ + * + * To run a query within a React component, call `useGetGlobalStatsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetGlobalStatsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetGlobalStatsQuery({ + * variables: { + * }, + * }); + */ +export function useGetGlobalStatsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetGlobalStatsDocument, options); + } +export function useGetGlobalStatsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetGlobalStatsDocument, options); + } +export function useGetGlobalStatsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetGlobalStatsDocument, options); + } +export type GetGlobalStatsQueryHookResult = ReturnType; +export type GetGlobalStatsLazyQueryHookResult = ReturnType; +export type GetGlobalStatsSuspenseQueryHookResult = ReturnType; +export type GetGlobalStatsQueryResult = Apollo.QueryResult; export const GenerateCaptchaDocument = gql` query generateCaptcha { generateCaptcha { From 72dcc1691ff50692e959ea1a9b629634abda18ab Mon Sep 17 00:00:00 2001 From: Alexandre78R Date: Wed, 25 Jun 2025 16:24:15 +0200 Subject: [PATCH 02/14] add DasboardCars view stats and style page dashboard --- .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 33 ++++++++++++--- .../components/Dashboard/DashBordCard.tsx | 41 +++++++++++++++++++ frontend/src/styles/output.css | 2 +- 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/AdminLayout/components/Dashboard/DashBordCard.tsx diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index 79bf48fe..f268adf1 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -3,25 +3,48 @@ import { useGetGlobalStatsQuery, GetGlobalStatsQuery, } from '@/types/graphql'; - +import DashboardCard from '../../components/Dashboard/DashBordCard'; +import { + Users, + FolderKanban, + Brain, + GraduationCap, + Briefcase +} from "lucide-react"; +import LoadingCustom from '@/components/Loading/LoadingCustom'; const Dashboard = (): React.ReactElement => { - const { data , loading } = useGetGlobalStatsQuery(); + const { data , loading, error } = useGetGlobalStatsQuery(); const [stats, setStats] = useState(null); useEffect(() => { - // console.log("data", data); if (data) { setStats(data); } else { setStats(null); } }, [data]) + + if (loading) return ; + if (error || !data?.getGlobalStats) return

Erreur lors du chargement des statistiques.

; + + + const mainStats = [ + { title: "Utilisateurs", value: stats?.getGlobalStats.stats?.totalUsers ?? 0, icon: , color: "bg-blue-500 bg-cyan-400" }, + { title: "Projets", value: stats?.getGlobalStats.stats?.totalProjects ?? 0, icon: , color: "from-cyan-500 to-teal-400" }, + { title: "Compétences", value: stats?.getGlobalStats.stats?.totalSkills ?? 0, icon: , color: "from-violet-500 to-fuchsia-400" }, + { title: "Formations", value: stats?.getGlobalStats.stats?.totalEducations ?? 0, icon: , color: "from-yellow-400 to-orange-300" }, + { title: "Expériences", value: stats?.getGlobalStats.stats?.totalExperiences ?? 0, icon: , color: "from-pink-500 to-red-400" }, + ]; return ( <> -

Page Dashboard

-

{stats?.getGlobalStats.stats?.totalExperiences}

+

Tableau de bord

+
+ {mainStats.map((stat) => ( + + ))} +
) } diff --git a/frontend/src/components/AdminLayout/components/Dashboard/DashBordCard.tsx b/frontend/src/components/AdminLayout/components/Dashboard/DashBordCard.tsx new file mode 100644 index 00000000..caa5194e --- /dev/null +++ b/frontend/src/components/AdminLayout/components/Dashboard/DashBordCard.tsx @@ -0,0 +1,41 @@ +type DashboardCardProps = { + key ?: number | string; + title: string; + value: number; + icon?: React.ReactNode; + color?: string; +}; + +export default function DashboardCard({ + title, + value, + icon, + color = "from-blue-500 to-blue-400", +}: DashboardCardProps) { + return ( +
+
+ {icon} +
+
+

{title}

+ + {value} + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/styles/output.css b/frontend/src/styles/output.css index 9629369c..6c9a945c 100644 --- a/frontend/src/styles/output.css +++ b/frontend/src/styles/output.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-none{border-style:none}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.leading-normal{line-height:1.5}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-textButton{color:var(--textButton-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-none{border-style:none}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-textButton{color:var(--textButton-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}} \ No newline at end of file From 1ca896e25b28b4919a1e292c4b7360a82789cb82 Mon Sep 17 00:00:00 2001 From: Alexandre78R Date: Wed, 25 Jun 2025 17:35:27 +0200 Subject: [PATCH 03/14] add new resolver stats global for dashboard --- backend/src/resolvers/admin.resolver.ts | 76 ++++++++++++++++++- backend/src/resolvers/education.resolver.ts | 2 +- backend/src/resolvers/experience.resolver.ts | 2 +- backend/src/resolvers/project.resolver.ts | 2 +- backend/src/resolvers/skill.resolver.ts | 2 +- backend/src/resolvers/user.resolver.ts | 4 +- backend/src/types/graphql.ts | 26 +++++++ .../src/{entities => types}/response.types.ts | 44 +++++++++-- .../education/createEducation.test.ts | 2 +- .../education/deleteEducation.test.ts | 2 +- .../resolvers/education/educationById.test.ts | 2 +- .../resolvers/education/educationList.test.ts | 2 +- .../education/updateEducation.test.ts | 2 +- .../experience/createExperience.test.ts | 2 +- .../experience/deleteExperience.test.ts | 2 +- .../experience/experienceById.test.ts | 2 +- .../experience/experienceList.test.ts | 2 +- .../experience/updateExperience.test.ts | 2 +- .../resolvers/project/createProject.test.ts | 2 +- .../resolvers/project/deleteProject.test.ts | 2 +- .../resolvers/project/projectById.test.ts | 2 +- .../resolvers/project/projectList.test.ts | 2 +- .../resolvers/project/updateProject.test.ts | 2 +- .../resolvers/skill/createCategory.test.ts | 2 +- .../tests/resolvers/skill/createSkill.test.ts | 2 +- .../tests/resolvers/skill/deleteSkill.test.ts | 2 +- .../tests/resolvers/skill/skillList.test.ts | 2 +- .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 1 + 28 files changed, 162 insertions(+), 35 deletions(-) rename backend/src/{entities => types}/response.types.ts (75%) diff --git a/backend/src/resolvers/admin.resolver.ts b/backend/src/resolvers/admin.resolver.ts index fa3dc622..1229db4d 100644 --- a/backend/src/resolvers/admin.resolver.ts +++ b/backend/src/resolvers/admin.resolver.ts @@ -1,10 +1,19 @@ -import { Resolver, Mutation, Authorized, Query, Arg } from "type-graphql"; +import { Resolver, Mutation, Authorized, Query, Arg, Float } from "type-graphql"; import { UserRole } from "../entities/user.entity"; import { exec } from "child_process"; import * as path from "path"; import * as fs from "fs"; import * as util from "util"; -import { BackupFileInfo, BackupFilesResponse, BackupResponse, GlobalStats, GlobalStatsResponse, Response } from "../entities/response.types"; +import { + BackupFilesResponse, + BackupResponse, + GlobalStats, + GlobalStatsResponse, + Response, + UserRolePercent, + TopSkillsResponse, + TopSkillUsage, +} from "../types/response.types"; import { PrismaClient } from "@prisma/client"; import { promises as fsPromises } from 'fs'; @@ -99,7 +108,6 @@ export class AdminResolver { * Récupère des statistiques globales sur le contenu de la base de données. * Seuls les administrateurs peuvent y accéder. */ - @Authorized([UserRole.admin]) @Query(() => GlobalStatsResponse) async getGlobalStats(): Promise { try { @@ -243,4 +251,66 @@ export class AdminResolver { }; } } + + @Query(() => Float) + async getAverageSkillsPerProject(): Promise { + const totalProjects = await this.db.project.count(); + const totalProjectSkills = await this.db.projectSkill.count(); + + return totalProjects > 0 ? totalProjectSkills / totalProjects : 0; + } + + @Query(() => UserRolePercent) + async getUsersRoleDistribution(): Promise { + const totalUsers = await this.db.user.count(); + + const usersByRole = await this.db.user.groupBy({ + by: ['role'], + _count: { id: true }, + }); + + const map = usersByRole.reduce((acc, item) => { + acc[item.role] = item._count.id; + return acc; + }, {} as Record); + + return { + code: 200, + message: "User role distribution fetched successfully.", + admin: totalUsers > 0 ? (100 * (map[UserRole.admin] || 0)) / totalUsers : 0, + editor: totalUsers > 0 ? (100 * (map[UserRole.editor] || 0)) / totalUsers : 0, + view: totalUsers > 0 ? (100 * (map[UserRole.view] || 0)) / totalUsers : 0, + }; + } + + @Query(() => TopSkillsResponse) + async getTopUsedSkills(): Promise { + const topSkillCounts = await this.db.projectSkill.groupBy({ + by: ['skillId'], + _count: { skillId: true }, + orderBy: { _count: { skillId: 'desc' } }, + take: 5, + }); + + const skillIds = topSkillCounts.map(s => s.skillId); + + const skills = await this.db.skill.findMany({ + where: { id: { in: skillIds } }, + }); + + const result: TopSkillUsage[] = topSkillCounts.map(item => { + const skill = skills.find(s => s.id === item.skillId); + return { + id: item.skillId, + name: skill?.name || "Unknown", + usageCount: item._count.skillId, + }; + }); + + return { + code: 200, + message: "Top used skills fetched successfully.", + skills: result, + }; + } } \ No newline at end of file diff --git a/backend/src/resolvers/education.resolver.ts b/backend/src/resolvers/education.resolver.ts index bbf81dc6..2ff1a1d7 100644 --- a/backend/src/resolvers/education.resolver.ts +++ b/backend/src/resolvers/education.resolver.ts @@ -1,7 +1,7 @@ import { Resolver, Query, Int, Arg, Mutation, Authorized, Ctx } from "type-graphql"; import { Education } from "../entities/education.entity"; import { PrismaClient } from "@prisma/client"; -import { EducationResponse, EducationsResponse } from "../entities/response.types"; +import { EducationResponse, EducationsResponse } from "../types/response.types"; import { CreateEducationInput, UpdateEducationInput } from "../entities/inputs/education.input"; import { UserRole } from "../entities/user.entity"; import { MyContext } from ".."; diff --git a/backend/src/resolvers/experience.resolver.ts b/backend/src/resolvers/experience.resolver.ts index 85ad491d..181f52ec 100644 --- a/backend/src/resolvers/experience.resolver.ts +++ b/backend/src/resolvers/experience.resolver.ts @@ -1,6 +1,6 @@ import { Resolver, Query, Arg, Int, Mutation, Ctx, Authorized } from "type-graphql"; import { Experience } from "../entities/experience.entity"; -import { ExperienceResponse, ExperiencesResponse } from "../entities/response.types"; +import { ExperienceResponse, ExperiencesResponse } from "../types/response.types"; import { CreateExperienceInput, UpdateExperienceInput } from "../entities/inputs/experience.input"; import { UserRole } from "../entities/user.entity"; import { MyContext } from ".."; diff --git a/backend/src/resolvers/project.resolver.ts b/backend/src/resolvers/project.resolver.ts index 16f20df6..7adafc91 100644 --- a/backend/src/resolvers/project.resolver.ts +++ b/backend/src/resolvers/project.resolver.ts @@ -1,7 +1,7 @@ import { Resolver, Query, Arg, Int, Mutation, Authorized, Ctx } from "type-graphql"; import { Project } from "../entities/project.entity"; import { CreateProjectInput, UpdateProjectInput } from "../entities/inputs/project.input"; -import { Response, ProjectResponse, ProjectsResponse } from "../entities/response.types"; +import { Response, ProjectResponse, ProjectsResponse } from "../types/response.types"; import { UserRole } from "../entities/user.entity"; import { MyContext } from ".."; import { PrismaClient } from "@prisma/client"; diff --git a/backend/src/resolvers/skill.resolver.ts b/backend/src/resolvers/skill.resolver.ts index d2d16a00..2cc76de4 100644 --- a/backend/src/resolvers/skill.resolver.ts +++ b/backend/src/resolvers/skill.resolver.ts @@ -2,7 +2,7 @@ import { Resolver, Query, Mutation, Arg, Int, Authorized, Ctx } from "type-graph import { Skill } from "../entities/skill.entity"; import { CreateCategoryInput, CreateSkillInput, UpdateCategoryInput, UpdateSkillInput } from "../entities/inputs/skill.input"; import { SkillSubItem } from "../entities/skillSubItem.entity"; -import { CategoryResponse, SubItemResponse } from "../entities/response.types"; +import { CategoryResponse, SubItemResponse } from "../types/response.types"; import { UserRole } from "../entities/user.entity"; import { MyContext } from ".."; import { PrismaClient } from "@prisma/client"; diff --git a/backend/src/resolvers/user.resolver.ts b/backend/src/resolvers/user.resolver.ts index 34e5a754..5267b0a3 100644 --- a/backend/src/resolvers/user.resolver.ts +++ b/backend/src/resolvers/user.resolver.ts @@ -1,14 +1,14 @@ import { Resolver, Query, Arg, Mutation, Ctx, Authorized } from "type-graphql"; import { PrismaClient } from "@prisma/client"; import { User } from "../entities/user.entity"; -import { UsersResponse, UserResponse, LoginResponse } from "../entities/response.types"; +import { UsersResponse, UserResponse, LoginResponse } from "../types/response.types"; import { UserRole } from "../entities/user.entity"; import { generateSecurePassword } from "../lib/generateSecurePassword"; import { sendEmail } from "../mail/mail.service"; import { CreateUserInput, LoginInput } from "../entities/inputs/user.input"; import argon2 from "argon2"; import { structureMessageCreatedAccountHTML, structureMessageCreatedAccountTEXT } from "../mail/structureMail.service"; -import { Response } from "../entities/response.types"; +import { Response } from "../types/response.types"; import { emailRegex, passwordRegex, checkRegex } from "../regex"; import { jwtVerify, SignJWT } from "jose"; import { TextEncoder } from "util"; diff --git a/backend/src/types/graphql.ts b/backend/src/types/graphql.ts index b3611283..72c5090d 100644 --- a/backend/src/types/graphql.ts +++ b/backend/src/types/graphql.ts @@ -411,7 +411,10 @@ export type Query = { experienceById: ExperienceResponse; experienceList: ExperiencesResponse; generateCaptcha: CaptchaResponse; + getAverageSkillsPerProject: Scalars['Float']['output']; getGlobalStats: GlobalStatsResponse; + getTopUsedSkills: TopSkillsResponse; + getUsersRoleDistribution: UserRolePercent; listBackupFiles: BackupFilesResponse; me?: Maybe; projectById: ProjectResponse; @@ -471,6 +474,20 @@ export type SubItemResponse = { subItems?: Maybe>; }; +export type TopSkillUsage = { + __typename?: 'TopSkillUsage'; + id: Scalars['Int']['output']; + name: Scalars['String']['output']; + usageCount: Scalars['Int']['output']; +}; + +export type TopSkillsResponse = { + __typename?: 'TopSkillsResponse'; + code: Scalars['Int']['output']; + message: Scalars['String']['output']; + skills: Array; +}; + export type UpdateCategoryInput = { categoryEN?: InputMaybe; categoryFR?: InputMaybe; @@ -544,6 +561,15 @@ export type UserResponse = { user?: Maybe; }; +export type UserRolePercent = { + __typename?: 'UserRolePercent'; + admin: Scalars['Float']['output']; + code: Scalars['Int']['output']; + editor: Scalars['Float']['output']; + message: Scalars['String']['output']; + view: Scalars['Float']['output']; +}; + export type UsersResponse = { __typename?: 'UsersResponse'; code: Scalars['Int']['output']; diff --git a/backend/src/entities/response.types.ts b/backend/src/types/response.types.ts similarity index 75% rename from backend/src/entities/response.types.ts rename to backend/src/types/response.types.ts index 53a7fc5a..7296e934 100644 --- a/backend/src/entities/response.types.ts +++ b/backend/src/types/response.types.ts @@ -1,10 +1,10 @@ -import { ObjectType, Field, Int } from "type-graphql"; -import { Project } from "./project.entity"; -import { Skill } from "./skill.entity"; -import { SkillSubItem } from "./skillSubItem.entity"; -import { Education } from "./education.entity"; -import { Experience } from "./experience.entity"; -import { User } from "./user.entity"; +import { ObjectType, Field, Int, Float } from "type-graphql"; +import { Project } from "../entities/project.entity"; +import { Skill } from "../entities/skill.entity"; +import { SkillSubItem } from "../entities/skillSubItem.entity"; +import { Education } from "../entities/education.entity"; +import { Experience } from "../entities/experience.entity"; +import { User } from "../entities/user.entity"; @ObjectType() export class Response { @@ -150,4 +150,34 @@ export class BackupFilesResponse extends Response { export class BackupResponse extends Response { @Field() path: string; +} + +@ObjectType() +export class UserRolePercent extends Response { + @Field(() => Float) + admin: number; + + @Field(() => Float) + editor: number; + + @Field(() => Float) + view: number; +} + +@ObjectType() +export class TopSkillUsage { + @Field(() => Int) + id: number; + + @Field() + name: string; + + @Field(() => Int) + usageCount: number; +} + +@ObjectType() +export class TopSkillsResponse extends Response { + @Field(() => [TopSkillUsage]) + skills: TopSkillUsage[]; } \ No newline at end of file diff --git a/backend/tests/resolvers/education/createEducation.test.ts b/backend/tests/resolvers/education/createEducation.test.ts index 6d0dd996..7fe90824 100644 --- a/backend/tests/resolvers/education/createEducation.test.ts +++ b/backend/tests/resolvers/education/createEducation.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { CreateEducationInput } from "../../../src/entities/inputs/education.input"; -import { EducationResponse } from "../../../src/entities/response.types"; +import { EducationResponse } from "../../../src/types/response.types"; import { Education as PrismaEducation } from "@prisma/client"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/education/deleteEducation.test.ts b/backend/tests/resolvers/education/deleteEducation.test.ts index a4fde497..aeb99939 100644 --- a/backend/tests/resolvers/education/deleteEducation.test.ts +++ b/backend/tests/resolvers/education/deleteEducation.test.ts @@ -3,7 +3,7 @@ import { EducationResolver } from "../../../src/resolvers/education.resolver"; import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; -import { EducationResponse } from "../../../src/entities/response.types"; +import { EducationResponse } from "../../../src/types/response.types"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/education/educationById.test.ts b/backend/tests/resolvers/education/educationById.test.ts index 88d848cc..1651a82c 100644 --- a/backend/tests/resolvers/education/educationById.test.ts +++ b/backend/tests/resolvers/education/educationById.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { EducationResolver } from "../../../src/resolvers/education.resolver"; import { prismaMock } from "../../singleton"; -import { EducationResponse } from "../../../src/entities/response.types"; +import { EducationResponse } from "../../../src/types/response.types"; import { Education as PrismaEducation } from "@prisma/client"; describe("EducationResolver - educationById", () => { diff --git a/backend/tests/resolvers/education/educationList.test.ts b/backend/tests/resolvers/education/educationList.test.ts index 96f4f00d..85490376 100644 --- a/backend/tests/resolvers/education/educationList.test.ts +++ b/backend/tests/resolvers/education/educationList.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { EducationResolver } from "../../../src/resolvers/education.resolver"; import { prismaMock } from "../../singleton"; -import { EducationsResponse } from "../../../src/entities/response.types"; +import { EducationsResponse } from "../../../src/types/response.types"; import { Education as PrismaEducation } from "@prisma/client"; describe("EducationResolver - educationList", () => { diff --git a/backend/tests/resolvers/education/updateEducation.test.ts b/backend/tests/resolvers/education/updateEducation.test.ts index 7835888a..42a1e864 100644 --- a/backend/tests/resolvers/education/updateEducation.test.ts +++ b/backend/tests/resolvers/education/updateEducation.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { UpdateEducationInput } from "../../../src/entities/inputs/education.input"; -import { EducationResponse } from "../../../src/entities/response.types"; +import { EducationResponse } from "../../../src/types/response.types"; import { Education as PrismaEducation } from "@prisma/client"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/experience/createExperience.test.ts b/backend/tests/resolvers/experience/createExperience.test.ts index bfb17c04..f65ac348 100644 --- a/backend/tests/resolvers/experience/createExperience.test.ts +++ b/backend/tests/resolvers/experience/createExperience.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { CreateExperienceInput } from "../../../src/entities/inputs/experience.input"; -import { ExperienceResponse } from "../../../src/entities/response.types"; +import { ExperienceResponse } from "../../../src/types/response.types"; import { Experience as PrismaExperience } from "@prisma/client"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/experience/deleteExperience.test.ts b/backend/tests/resolvers/experience/deleteExperience.test.ts index a0a14e3b..7933bdb7 100644 --- a/backend/tests/resolvers/experience/deleteExperience.test.ts +++ b/backend/tests/resolvers/experience/deleteExperience.test.ts @@ -3,7 +3,7 @@ import { ExperienceResolver } from "../../../src/resolvers/experience.resolver"; import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; -import { ExperienceResponse } from "../../../src/entities/response.types"; +import { ExperienceResponse } from "../../../src/types/response.types"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/experience/experienceById.test.ts b/backend/tests/resolvers/experience/experienceById.test.ts index 52351be9..0a07e269 100644 --- a/backend/tests/resolvers/experience/experienceById.test.ts +++ b/backend/tests/resolvers/experience/experienceById.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { ExperienceResolver } from "../../../src/resolvers/experience.resolver"; import { prismaMock } from "../../singleton"; -import { ExperienceResponse } from "../../../src/entities/response.types"; +import { ExperienceResponse } from "../../../src/types/response.types"; import { Experience as PrismaExperience } from "@prisma/client"; describe("ExperienceResolver - experienceById", () => { diff --git a/backend/tests/resolvers/experience/experienceList.test.ts b/backend/tests/resolvers/experience/experienceList.test.ts index 29000351..65d963b4 100644 --- a/backend/tests/resolvers/experience/experienceList.test.ts +++ b/backend/tests/resolvers/experience/experienceList.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { ExperienceResolver } from "../../../src/resolvers/experience.resolver"; import { prismaMock } from "../../singleton"; -import { ExperiencesResponse } from "../../../src/entities/response.types"; +import { ExperiencesResponse } from "../../../src/types/response.types"; import { Experience as PrismaExperience } from "@prisma/client"; describe("ExperienceResolver - experienceList", () => { diff --git a/backend/tests/resolvers/experience/updateExperience.test.ts b/backend/tests/resolvers/experience/updateExperience.test.ts index dabc7566..2e8e7eeb 100644 --- a/backend/tests/resolvers/experience/updateExperience.test.ts +++ b/backend/tests/resolvers/experience/updateExperience.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { UpdateExperienceInput } from "../../../src/entities/inputs/experience.input"; -import { ExperienceResponse } from "../../../src/entities/response.types"; +import { ExperienceResponse } from "../../../src/types/response.types"; import { Experience as PrismaExperience } from "@prisma/client"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/project/createProject.test.ts b/backend/tests/resolvers/project/createProject.test.ts index 929bf0c7..706ff307 100644 --- a/backend/tests/resolvers/project/createProject.test.ts +++ b/backend/tests/resolvers/project/createProject.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { CreateProjectInput } from "../../../src/entities/inputs/project.input"; -import { ProjectResponse } from "../../../src/entities/response.types"; +import { ProjectResponse } from "../../../src/types/response.types"; import Cookies from 'cookies'; import { Project as PrismaProject, Skill as PrismaSkill, ProjectSkill as PrismaProjectSkill } from "@prisma/client"; diff --git a/backend/tests/resolvers/project/deleteProject.test.ts b/backend/tests/resolvers/project/deleteProject.test.ts index 66ac4d79..f0c948d4 100644 --- a/backend/tests/resolvers/project/deleteProject.test.ts +++ b/backend/tests/resolvers/project/deleteProject.test.ts @@ -3,7 +3,7 @@ import { ProjectResolver } from "../../../src/resolvers/project.resolver"; import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; -import { Response } from "../../../src/entities/response.types"; +import { Response } from "../../../src/types/response.types"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/project/projectById.test.ts b/backend/tests/resolvers/project/projectById.test.ts index c0761224..fe377739 100644 --- a/backend/tests/resolvers/project/projectById.test.ts +++ b/backend/tests/resolvers/project/projectById.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { ProjectResolver } from "../../../src/resolvers/project.resolver"; import { prismaMock } from "../../singleton"; -import { ProjectResponse } from "../../../src/entities/response.types"; +import { ProjectResponse } from "../../../src/types/response.types"; import { Project as PrismaProject, Skill as PrismaSkill, ProjectSkill as PrismaProjectSkill } from "@prisma/client"; describe("ProjectResolver - projectById", () => { diff --git a/backend/tests/resolvers/project/projectList.test.ts b/backend/tests/resolvers/project/projectList.test.ts index 30cde717..b4e3bf58 100644 --- a/backend/tests/resolvers/project/projectList.test.ts +++ b/backend/tests/resolvers/project/projectList.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { ProjectResolver } from "../../../src/resolvers/project.resolver"; import { prismaMock } from "../../singleton"; -import { ProjectsResponse } from "../../../src/entities/response.types"; +import { ProjectsResponse } from "../../../src/types/response.types"; import { Project as PrismaProject, Skill as PrismaSkill, ProjectSkill as PrismaProjectSkill } from "@prisma/client"; describe("ProjectResolver - projectList", () => { diff --git a/backend/tests/resolvers/project/updateProject.test.ts b/backend/tests/resolvers/project/updateProject.test.ts index b4be6a6b..96c3196e 100644 --- a/backend/tests/resolvers/project/updateProject.test.ts +++ b/backend/tests/resolvers/project/updateProject.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { UpdateProjectInput } from "../../../src/entities/inputs/project.input"; // Adjust path if needed -import { ProjectResponse } from "../../../src/entities/response.types"; +import { ProjectResponse } from "../../../src/types/response.types"; import Cookies from 'cookies'; import { Project as PrismaProject, Skill as PrismaSkill, ProjectSkill as PrismaProjectSkill } from "@prisma/client"; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/skill/createCategory.test.ts b/backend/tests/resolvers/skill/createCategory.test.ts index a315ce42..b7bd506c 100644 --- a/backend/tests/resolvers/skill/createCategory.test.ts +++ b/backend/tests/resolvers/skill/createCategory.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { CreateCategoryInput } from "../../../src/entities/inputs/skill.input"; // Ajuste le chemin vers ton input type -import { CategoryResponse } from "../../../src/entities/response.types"; // Ajuste le chemin vers ton response type +import { CategoryResponse } from "../../../src/types/response.types"; // Ajuste le chemin vers ton response type import { Skill } from "../../../src/entities/skill.entity"; // Assumant que c'est ton DTO Skill pour les catégories import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/skill/createSkill.test.ts b/backend/tests/resolvers/skill/createSkill.test.ts index 1853f948..6a3d2b91 100644 --- a/backend/tests/resolvers/skill/createSkill.test.ts +++ b/backend/tests/resolvers/skill/createSkill.test.ts @@ -4,7 +4,7 @@ import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; import { CreateSkillInput } from "../../../src/entities/inputs/skill.input"; -import { SubItemResponse } from "../../../src/entities/response.types"; +import { SubItemResponse } from "../../../src/types/response.types"; import { SkillSubItem } from "../../../src/entities/skillSubItem.entity"; // Vérifie ce chemin d'entité DTO import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/skill/deleteSkill.test.ts b/backend/tests/resolvers/skill/deleteSkill.test.ts index 3de1aff1..c1e69dea 100644 --- a/backend/tests/resolvers/skill/deleteSkill.test.ts +++ b/backend/tests/resolvers/skill/deleteSkill.test.ts @@ -3,7 +3,7 @@ import { SkillResolver } from "../../../src/resolvers/skill.resolver"; import { prismaMock } from "../../singleton"; import { MyContext } from "../../../src"; import { User, UserRole } from "../../../src/entities/user.entity"; -import { SubItemResponse } from "../../../src/entities/response.types"; +import { SubItemResponse } from "../../../src/types/response.types"; import Cookies from 'cookies'; import { mockDeep } from 'jest-mock-extended'; diff --git a/backend/tests/resolvers/skill/skillList.test.ts b/backend/tests/resolvers/skill/skillList.test.ts index 32123d98..2f7aa2f4 100644 --- a/backend/tests/resolvers/skill/skillList.test.ts +++ b/backend/tests/resolvers/skill/skillList.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { SkillResolver } from "../../../src/resolvers/skill.resolver"; import { prismaMock } from "../../singleton"; -import { CategoryResponse } from "../../../src/entities/response.types"; +import { CategoryResponse } from "../../../src/types/response.types"; import { SkillCategory as PrismaSkillCategory, Skill as PrismaSkill } from "@prisma/client"; describe("SkillResolver - skillList", () => { diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index f268adf1..ad941d4f 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -18,6 +18,7 @@ const Dashboard = (): React.ReactElement => { const [stats, setStats] = useState(null); useEffect(() => { + console.log("Dashboard data:", data); if (data) { setStats(data); } else { From 9a4a7fb2d90b64174c5452fccdb1242e7dfa8e84 Mon Sep 17 00:00:00 2001 From: Alexandre78R Date: Wed, 25 Jun 2025 21:14:47 +0200 Subject: [PATCH 04/14] add stats global ++ and graph et style , commit for save code (bug css) --- backend/src/resolvers/admin.resolver.ts | 1 - frontend/package-lock.json | 361 ++++++++---------- frontend/package.json | 2 + .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 122 ++++-- .../src/components/AdminLayout/SideBar.tsx | 6 +- .../src/components/Charts/DoughnutChart.tsx | 45 +++ .../components/Charts/HorizontalBarChart.tsx | 79 ++++ .../src/requetes/queries/admin.queries.ts | 21 +- frontend/src/styles/output.css | 2 +- frontend/src/types/graphql.ts | 45 ++- 10 files changed, 453 insertions(+), 231 deletions(-) create mode 100644 frontend/src/components/Charts/DoughnutChart.tsx create mode 100644 frontend/src/components/Charts/HorizontalBarChart.tsx diff --git a/backend/src/resolvers/admin.resolver.ts b/backend/src/resolvers/admin.resolver.ts index 1229db4d..2bd388ef 100644 --- a/backend/src/resolvers/admin.resolver.ts +++ b/backend/src/resolvers/admin.resolver.ts @@ -289,7 +289,6 @@ export class AdminResolver { by: ['skillId'], _count: { skillId: true }, orderBy: { _count: { skillId: 'desc' } }, - take: 5, }); const skillIds = topSkillCounts.map(s => s.skillId); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d24c1fca..ae0b2002 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.4.0", "@types/react-toastify": "^4.1.0", + "chart.js": "^4.5.0", "clsx": "^2.1.1", "framer-motion": "^11.2.5", "graphql": "^16.9.0", @@ -34,6 +35,7 @@ "lucide-react": "^0.516.0", "next": "^14.2.14", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-player": "^2.16.0", "react-redux": "^9.1.2", @@ -311,12 +313,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -568,17 +572,19 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -592,101 +598,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", - "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1101,24 +1031,23 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1150,13 +1079,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", - "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2537,6 +2466,12 @@ "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==" }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -2827,9 +2762,10 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/@next/env": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.14.tgz", - "integrity": "sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg==" + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz", + "integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug==", + "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.3", @@ -2841,12 +2777,13 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.14.tgz", - "integrity": "sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz", + "integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2856,12 +2793,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.14.tgz", - "integrity": "sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz", + "integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2871,12 +2809,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.14.tgz", - "integrity": "sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz", + "integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2886,12 +2825,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.14.tgz", - "integrity": "sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz", + "integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2901,12 +2841,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.14.tgz", - "integrity": "sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz", + "integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2916,12 +2857,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.14.tgz", - "integrity": "sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz", + "integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2931,12 +2873,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.14.tgz", - "integrity": "sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz", + "integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2946,12 +2889,13 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.14.tgz", - "integrity": "sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz", + "integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2961,12 +2905,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.14.tgz", - "integrity": "sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz", + "integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -4066,10 +4011,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4677,9 +4623,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4829,9 +4776,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001723", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", - "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "funding": [ { "type": "opencollective", @@ -4914,6 +4861,18 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5498,9 +5457,10 @@ } }, "node_modules/dset": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", - "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -6552,10 +6512,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6687,9 +6648,10 @@ } }, "node_modules/graphql-config/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -8080,9 +8042,10 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8150,15 +8113,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8173,11 +8137,12 @@ "dev": true }, "node_modules/next": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.14.tgz", - "integrity": "sha512-Q1coZG17MW0Ly5x76shJ4dkC23woLAhhnDnw+DfTc7EpZSGuWrlsZ3bZaO8t6u1Yu8FVfhkqJE+U8GC7E0GLPQ==", + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz", + "integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==", + "license": "MIT", "dependencies": { - "@next/env": "14.2.14", + "@next/env": "14.2.30", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -8192,15 +8157,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.14", - "@next/swc-darwin-x64": "14.2.14", - "@next/swc-linux-arm64-gnu": "14.2.14", - "@next/swc-linux-arm64-musl": "14.2.14", - "@next/swc-linux-x64-gnu": "14.2.14", - "@next/swc-linux-x64-musl": "14.2.14", - "@next/swc-win32-arm64-msvc": "14.2.14", - "@next/swc-win32-ia32-msvc": "14.2.14", - "@next/swc-win32-x64-msvc": "14.2.14" + "@next/swc-darwin-arm64": "14.2.30", + "@next/swc-darwin-x64": "14.2.30", + "@next/swc-linux-arm64-gnu": "14.2.30", + "@next/swc-linux-arm64-musl": "14.2.30", + "@next/swc-linux-x64-gnu": "14.2.30", + "@next/swc-linux-x64-musl": "14.2.30", + "@next/swc-win32-arm64-msvc": "14.2.30", + "@next/swc-win32-ia32-msvc": "14.2.30", + "@next/swc-win32-x64-msvc": "14.2.30" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -8722,9 +8687,10 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -8995,6 +8961,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -9149,11 +9125,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -10035,14 +10006,6 @@ "node": ">=0.6.0" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0031be69..0e378646 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.4.0", "@types/react-toastify": "^4.1.0", + "chart.js": "^4.5.0", "clsx": "^2.1.1", "framer-motion": "^11.2.5", "graphql": "^16.9.0", @@ -38,6 +39,7 @@ "lucide-react": "^0.516.0", "next": "^14.2.14", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-player": "^2.16.0", "react-redux": "^9.1.2", diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index ad941d4f..6606438c 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -1,7 +1,5 @@ -import { useEffect, useState } from 'react'; import { useGetGlobalStatsQuery, - GetGlobalStatsQuery, } from '@/types/graphql'; import DashboardCard from '../../components/Dashboard/DashBordCard'; import { @@ -12,41 +10,117 @@ import { Briefcase } from "lucide-react"; import LoadingCustom from '@/components/Loading/LoadingCustom'; +import HorizontalBarChart from '@/components/Charts/HorizontalBarChart'; const Dashboard = (): React.ReactElement => { const { data , loading, error } = useGetGlobalStatsQuery(); - const [stats, setStats] = useState(null); - - useEffect(() => { - console.log("Dashboard data:", data); - if (data) { - setStats(data); - } else { - setStats(null); - } - }, [data]) - + if (loading) return ; if (error || !data?.getGlobalStats) return

Erreur lors du chargement des statistiques.

; - + + const stats = data.getGlobalStats.stats; + const averageSkills = data.getAverageSkillsPerProject; + const roleDistribution = data.getUsersRoleDistribution; + const topSkills = data.getTopUsedSkills.skills; const mainStats = [ - { title: "Utilisateurs", value: stats?.getGlobalStats.stats?.totalUsers ?? 0, icon: , color: "bg-blue-500 bg-cyan-400" }, - { title: "Projets", value: stats?.getGlobalStats.stats?.totalProjects ?? 0, icon: , color: "from-cyan-500 to-teal-400" }, - { title: "Compétences", value: stats?.getGlobalStats.stats?.totalSkills ?? 0, icon: , color: "from-violet-500 to-fuchsia-400" }, - { title: "Formations", value: stats?.getGlobalStats.stats?.totalEducations ?? 0, icon: , color: "from-yellow-400 to-orange-300" }, - { title: "Expériences", value: stats?.getGlobalStats.stats?.totalExperiences ?? 0, icon: , color: "from-pink-500 to-red-400" }, + { title: "Projets", value: stats?.totalProjects ?? 0, icon: , color: "from-cyan-500 to-teal-400" }, + { title: "Compétences", value: stats?.totalSkills ?? 0, icon: , color: "from-violet-500 to-fuchsia-400" }, + { title: "Formations", value: stats?.totalEducations ?? 0, icon: , color: "from-yellow-400 to-orange-300" }, + { title: "Expériences", value: stats?.totalExperiences ?? 0, icon: , color: "from-pink-500 to-red-400" }, + { title: "Utilisateurs", value: stats?.totalUsers ?? 0, icon: , color: "bg-blue-500 bg-cyan-400" }, ]; return ( - <> -

Tableau de bord

-
+
+

Tableau de bord

+ + {/*
{mainStats.map((stat) => ( - + ))}
- + +
+

+ Moyenne de compétences par projet +

+

+ Chaque projet utilise en moyenne{' '} + + {averageSkills.toFixed(2)} + {' '} + compétences. +

+
*/} + + + {/*
+

+ Les compétences les plus utilisées +

+
    + {topSkills.map((skill) => ( +
  • + {skill.name} + + {skill.usageCount} projets + +
  • + ))} +
+
*/} + + {/*
+

+ Compétences les plus utilisées (Top 5) +

+ + skill.name)} + data={topSkills.map(skill => skill.usageCount)} + /> +
*/} + + {/*
+

+ Compétences les plus utilisées (Top 5) +

+ + skill.name)} + data={topSkills.map(skill => skill.usageCount)} + /> +
+ +
+

+ Répartition des rôles utilisateurs +

+
+ {[ + { label: 'Administrateurs', value: roleDistribution?.admin ?? 0 }, + { label: 'Éditeurs', value: roleDistribution?.editor ?? 0 }, + { label: 'Lecteurs', value: roleDistribution?.view ?? 0 }, + ].map((role) => ( +
+ {role.label} + + {role.value.toFixed(0)}% + +
+ ))} +
+
*/} + +
) } diff --git a/frontend/src/components/AdminLayout/SideBar.tsx b/frontend/src/components/AdminLayout/SideBar.tsx index 904d1997..35a67913 100644 --- a/frontend/src/components/AdminLayout/SideBar.tsx +++ b/frontend/src/components/AdminLayout/SideBar.tsx @@ -74,7 +74,7 @@ const SideBar = ({ }} className={clsx( 'flex items-center justify-between w-full px-4 py-2 rounded transition-all', - activeTab === item.key ? 'text-primary font-semibold' : 'text-primary', + activeTab === item.key ? 'text-primary font-semibold' : 'text-text', item.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary hover:text-secondary' @@ -96,7 +96,7 @@ const SideBar = ({ )} {hasChildren && isOpen && ( -
+
{item.children?.map((child) => ( ); }; -export default ButtonCustom; +export default ButtonCustom; \ No newline at end of file diff --git a/frontend/src/components/Charts/DoughnutChart.tsx b/frontend/src/components/Charts/DoughnutChart.tsx index cca0b7b4..0a17ae32 100644 --- a/frontend/src/components/Charts/DoughnutChart.tsx +++ b/frontend/src/components/Charts/DoughnutChart.tsx @@ -1,45 +1,77 @@ -import { - Chart as ChartJS, - ArcElement, - Tooltip, - Legend, - Colors, -} from 'chart.js'; -import { Doughnut } from 'react-chartjs-2'; +// import { useEffect, useState } from 'react'; +// import { +// Chart as ChartJS, +// ArcElement, +// Tooltip, +// Legend, +// } from 'chart.js'; +// import { Doughnut } from 'react-chartjs-2'; -ChartJS.register(ArcElement, Tooltip, Legend, Colors); +// ChartJS.register(ArcElement, Tooltip, Legend); -interface DoughnutChartProps { - labels: string[]; - data: number[]; - title?: string; -} +// interface DoughnutChartProps { +// labels: string[]; +// data: number[]; +// title?: string; +// } -const DoughnutChart: React.FC = ({ labels, data, title }) => { - return ( -
- {title &&

{title}

} - -
- ); -}; +// const DoughnutChart: React.FC = ({ labels, data, title }) => { +// const [colors, setColors] = useState([]); +// const [labelColor, setLabelColor] = useState('#334155'); -export default DoughnutChart; \ No newline at end of file +// /* ↪️ Récupère dynamiquement les variables CSS au montage (et au changement de thème) */ +// useEffect(() => { +// const style = getComputedStyle(document.documentElement); +// setColors( +// style +// .getPropertyValue('--chart-colors') +// .split(',') +// .map((c) => c.trim()), +// ); +// setLabelColor(style.getPropertyValue('--chart-label-color').trim()); +// }, []); + +// return ( +//
+// {title && ( +//

+// {title} +//

+// )} + +// +//
+// ); +// }; + +// export default DoughnutChart; diff --git a/frontend/src/components/Charts/HorizontalBarChart.tsx b/frontend/src/components/Charts/HorizontalBarChart.tsx index 5917546f..feb6a095 100644 --- a/frontend/src/components/Charts/HorizontalBarChart.tsx +++ b/frontend/src/components/Charts/HorizontalBarChart.tsx @@ -1,79 +1,95 @@ -import { useState, useEffect } from 'react'; -import { - Chart as ChartJS, - BarElement, - CategoryScale, - LinearScale, - Tooltip, - Legend, -} from 'chart.js'; -import { Bar } from 'react-chartjs-2'; -import { useTheme } from "@/context/Theme/ThemeContext"; +// import { useState, useEffect } from 'react'; +// import { +// Chart as ChartJS, +// BarElement, +// CategoryScale, +// LinearScale, +// Tooltip, +// Legend, +// } from 'chart.js'; +// import { Bar } from 'react-chartjs-2'; +// import { useTheme } from "@/context/Theme/ThemeContext"; -ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); +// // ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); -interface HorizontalBarChartProps { - labels: string[]; - data: number[]; -} +// interface HorizontalBarChartProps { +// labels: string[]; +// data: number[]; +// } -const HorizontalBarChart: React.FC = ({ labels, data }) => { - const [primaryColor, setPrimaryColor] = useState('#6366f1'); - const [labelColor, setLabelColor] = useState('#1f2937'); - - const { theme } = useTheme(); +// const HorizontalBarChart: React.FC = ({ labels, data }) => { +// const [primaryColor, setPrimaryColor] = useState('#6366f1'); +// const [labelColor, setLabelColor] = useState('#1f2937'); - useEffect(() => { - const cssPrimary = getComputedStyle(document.documentElement) - .getPropertyValue('--primary-color') - .trim(); - const cssLabelColor = getComputedStyle(document.documentElement) - .getPropertyValue('--text-color') - .trim(); +// const { theme } = useTheme(); - if (cssPrimary) setPrimaryColor(cssPrimary); - if (cssLabelColor) setLabelColor(cssLabelColor); - }, [theme]); +// useEffect(() => { +// const style = getComputedStyle(document.documentElement); - return ( -
- -
- ); -}; +// const cssPrimary = style.getPropertyValue('--primary-color').trim(); +// const cssTextColor = style.getPropertyValue('--text-color').trim(); -export default HorizontalBarChart; \ No newline at end of file +// if (cssPrimary) setPrimaryColor(cssPrimary); +// if (cssTextColor) setLabelColor(cssTextColor); +// }, [theme]); + +// return ( +//
+// +//
+// ); +// }; + +// export default HorizontalBarChart; \ No newline at end of file diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index be5272d2..7eedc9d6 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -177,4 +177,4 @@ body { html { scroll-behavior: smooth; -} \ No newline at end of file +} diff --git a/frontend/src/styles/output.css b/frontend/src/styles/output.css index 9629369c..9f97393c 100644 --- a/frontend/src/styles/output.css +++ b/frontend/src/styles/output.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-none{border-style:none}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.leading-normal{line-height:1.5}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-textButton{color:var(--textButton-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..cf94ba0a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "portfolio", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} From af4173b8580a46ade0e6d31c3cce3a607a5fce33 Mon Sep 17 00:00:00 2001 From: Alexandre78R Date: Wed, 25 Jun 2025 22:40:51 +0200 Subject: [PATCH 06/14] fixed text h1 css normal --- frontend/src/components/Title/Title.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Title/Title.tsx b/frontend/src/components/Title/Title.tsx index 332b1c70..25f37915 100644 --- a/frontend/src/components/Title/Title.tsx +++ b/frontend/src/components/Title/Title.tsx @@ -1,4 +1,4 @@ -import { Typography } from "@mui/material"; +import { Typography, Box } from "@mui/material"; type Props = { title: string; @@ -6,16 +6,20 @@ type Props = { const Title: React.FC = ({ title }): React.ReactElement => { return ( -
+ {title} -
+ ); }; -export default Title; +export default Title; \ No newline at end of file From f74b61ab3959d8b73da4565c809d61f69ddbf833 Mon Sep 17 00:00:00 2001 From: Alexandre78R Date: Wed, 25 Jun 2025 22:52:21 +0200 Subject: [PATCH 07/14] fixed text h2 h3 css normal --- frontend/src/components/AboutMe/AboutMe.tsx | 5 ++-- frontend/src/components/Contact/Contact.tsx | 10 ++------ .../Title/{Title.tsx => TitleH2.tsx} | 4 +-- frontend/src/components/Title/TitleH3.tsx | 25 +++++++++++++++++++ frontend/src/pages/index.tsx | 14 +++++------ 5 files changed, 38 insertions(+), 20 deletions(-) rename frontend/src/components/Title/{Title.tsx => TitleH2.tsx} (79%) create mode 100644 frontend/src/components/Title/TitleH3.tsx diff --git a/frontend/src/components/AboutMe/AboutMe.tsx b/frontend/src/components/AboutMe/AboutMe.tsx index 5d7b4e10..3856e3a9 100644 --- a/frontend/src/components/AboutMe/AboutMe.tsx +++ b/frontend/src/components/AboutMe/AboutMe.tsx @@ -1,6 +1,7 @@ import { useLang } from "@/context/Lang/LangContext"; import { Typography } from "@mui/material"; import ButtonCustom from "../Button/Button"; +import TitleH3 from "../Title/TitleH3"; const AboutMe: React.FC = (): React.ReactElement => { const { translations } = useLang(); @@ -12,9 +13,7 @@ const AboutMe: React.FC = (): React.ReactElement => { return (
- - {translations.titleAboutMe} - +

{translations.descriptionAboutMe1}

{translations.descriptionAboutMe2}

{translations.descriptionAboutMe3}

diff --git a/frontend/src/components/Contact/Contact.tsx b/frontend/src/components/Contact/Contact.tsx index cebc83fc..687c25b6 100644 --- a/frontend/src/components/Contact/Contact.tsx +++ b/frontend/src/components/Contact/Contact.tsx @@ -7,6 +7,7 @@ import InputField from "@/components/InputField/InputField"; import { useSendContactMutation } from "@/types/graphql"; import Captcha from "../Captcha/Captcha"; import { checkRegex, emailRegex } from "@/regex"; +import TitleH3 from "../Title/TitleH3"; const Contact: React.FC = (): React.ReactElement => { const { translations } = useLang(); @@ -95,14 +96,7 @@ const Contact: React.FC = (): React.ReactElement => { return (
- - {translations.nameFormulaireContact} - - +
= ({ title }): React.ReactElement => { +const TitleH2: React.FC = ({ title }): React.ReactElement => { return ( = ({ title }): React.ReactElement => { ); }; -export default Title; \ No newline at end of file +export default TitleH2; \ No newline at end of file diff --git a/frontend/src/components/Title/TitleH3.tsx b/frontend/src/components/Title/TitleH3.tsx new file mode 100644 index 00000000..95e9804f --- /dev/null +++ b/frontend/src/components/Title/TitleH3.tsx @@ -0,0 +1,25 @@ +import { Typography, Box } from "@mui/material"; + +type Props = { + title: string; +}; + +const TitleH3: React.FC = ({ title }): React.ReactElement => { + return ( + + + {title} + + + ); +}; + +export default TitleH3; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 4180ef8a..d6b31d79 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import { useLang } from "@/context/Lang/LangContext"; import { useSectionRefs } from "@/context/SectionRefs/SectionRefsContext"; import HorizontalScroll from "@/components/horizontalScroll/horizontalScroll"; -import Title from "@/components/Title/Title"; +import TitleH2 from "@/components/Title/TitleH2"; import Header from "@/components/Header/Header"; import AboutMe from "@/components/AboutMe/AboutMe"; import Footer from "@/components/Footer/Footer"; @@ -148,7 +148,7 @@ const Home: React.FC = (): React.ReactElement => {
{selectedView === "terminal" ? (
- + <TitleH2 title="Terminal" /> <div className="flex flex-col items-center"> <Terminal /> </div> @@ -156,12 +156,12 @@ const Home: React.FC = (): React.ReactElement => { ) : ( <> <section ref={aboutMeRef} id="aboutme"> - <Title title={translations.nameCategoryAboutMe} /> + <TitleH2 title={translations.nameCategoryAboutMe} /> <AboutMe /> </section> <section ref={skillRef} id="skill"> <div> - <Title title={translations.nameCategorySkills} /> + <TitleH2 title={translations.nameCategorySkills} /> </div> <div> <HorizontalScroll data={dataSkills} category="skills" /> @@ -169,19 +169,19 @@ const Home: React.FC = (): React.ReactElement => { </section> <section ref={projectRef} id="project"> <div> - <Title title={translations.nameCategoryProjects} /> + <TitleH2 title={translations.nameCategoryProjects} /> </div> <HorizontalScroll data={dataProjects} category="projects" /> </section> <section ref={educationRef} id="project"> <div> - <Title title={translations.nameCategoryCareer} /> + <TitleH2 title={translations.nameCategoryCareer} /> </div> <Educations /> </section> <section ref={contactRef} id="contact"> <div> - <Title title={translations.nameCategoryContact} /> + <TitleH2 title={translations.nameCategoryContact} /> </div> <Contact /> </section> From 2aefdc414a86eeb8a03045ec7dda3bce672ea0d5 Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Wed, 25 Jun 2025 23:47:14 +0200 Subject: [PATCH 08/14] finish page dahboard --- frontend/package-lock.json | 11 ++ frontend/package.json | 1 + .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 72 +++---- .../AdminLayout/components/Text/TextAdmin.tsx | 57 ++++++ frontend/src/components/Button/Button.tsx | 2 +- .../src/components/Charts/DoughnutChart.tsx | 140 +++++++------- .../components/Charts/HorizontalBarChart.tsx | 181 +++++++++--------- frontend/src/lang/en.tsx | 15 ++ frontend/src/lang/fr.tsx | 15 ++ frontend/src/lang/typeLang.tsx | 15 ++ 10 files changed, 319 insertions(+), 190 deletions(-) create mode 100644 frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7db0d4f..ae0b2002 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,6 +35,7 @@ "lucide-react": "^0.516.0", "next": "^14.2.14", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-player": "^2.16.0", "react-redux": "^9.1.2", @@ -8960,6 +8961,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 355cbf1e..0e378646 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,7 @@ "lucide-react": "^0.516.0", "next": "^14.2.14", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-player": "^2.16.0", "react-redux": "^9.1.2", diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index 35c66a96..14989f9a 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -10,11 +10,14 @@ import { Briefcase } from "lucide-react"; import LoadingCustom from '@/components/Loading/LoadingCustom'; -// import HorizontalBarChart from '@/components/Charts/HorizontalBarChart'; +import HorizontalBarChart from '@/components/Charts/HorizontalBarChart'; +import { useLang } from '@/context/Lang/LangContext'; +import TextAdmin from '../../components/Text/TextAdmin'; const Dashboard = (): React.ReactElement => { + const { data , loading, error } = useGetGlobalStatsQuery(); - + const { translations } = useLang(); if (loading) return <LoadingCustom />; if (error || !data?.getGlobalStats) return <p className="p-4 text-primary">Erreur lors du chargement des statistiques.</p>; @@ -24,17 +27,18 @@ const Dashboard = (): React.ReactElement => { const topSkills = data.getTopUsedSkills.skills; const mainStats = [ - { title: "Projets", value: stats?.totalProjects ?? 0, icon: <FolderKanban size={24} />, color: "from-cyan-500 to-teal-400" }, - { title: "Compétences", value: stats?.totalSkills ?? 0, icon: <Brain size={24} />, color: "from-violet-500 to-fuchsia-400" }, - { title: "Formations", value: stats?.totalEducations ?? 0, icon: <GraduationCap size={24} />, color: "from-yellow-400 to-orange-300" }, - { title: "Expériences", value: stats?.totalExperiences ?? 0, icon: <Briefcase size={24} />, color: "from-pink-500 to-red-400" }, - { title: "Utilisateurs", value: stats?.totalUsers ?? 0, icon: <Users size={24} />, color: "bg-blue-500 bg-cyan-400" }, + { title: translations.messagePageDashBoardCardStatsProject, value: stats?.totalProjects ?? 0, icon: <FolderKanban size={24} />, color: "from-cyan-500 to-teal-400" }, + { title: translations.messagePageDashBoardCardStatsSkill, value: stats?.totalSkills ?? 0, icon: <Brain size={24} />, color: "from-violet-500 to-fuchsia-400" }, + { title: translations.messagePageDashBoardCardStatsEducation, value: stats?.totalEducations ?? 0, icon: <GraduationCap size={24} />, color: "from-yellow-400 to-orange-300" }, + { title: translations.messagePageDashBoardCardStatsExperience, value: stats?.totalExperiences ?? 0, icon: <Briefcase size={24} />, color: "from-pink-500 to-red-400" }, + { title: translations.messagePageDashBoardCardStatsUser, value: stats?.totalUsers ?? 0, icon: <Users size={24} />, color: "bg-blue-500 bg-cyan-400" }, ]; return ( <div className="space-y-10"> - {/* <h1 className="text-3xl font-bold text-primary">Tableau de bord</h1> - + <TextAdmin type='h1'> + {translations.messagePageDashBoardTitle} + </TextAdmin> <section className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6"> {mainStats.map((stat) => ( <DashboardCard key={stat.title} {...stat} /> @@ -42,17 +46,17 @@ const Dashboard = (): React.ReactElement => { </section> <section className="bg-muted p-6 rounded-xl shadow-sm"> - <h2 className="text-xl font-semibold text-primary mb-2"> - Moyenne de compétences par projet - </h2> - <p className="text-muted-foreground text-l text-text"> - Chaque projet utilise en moyenne{' '} + <TextAdmin type='h2'> + {translations.messagePageDashBoardTittleSection1} + </TextAdmin> + <TextAdmin type='p'> + {translations.messagePageDashBoardMessageAverageLeft}{' '} <span className="text-primary font-bold"> {averageSkills.toFixed(2)} </span>{' '} - compétences. - </p> - </section> */} + {translations.messagePageDashBoardMessageAverageRight} + </TextAdmin> + </section> {/* <section className="bg-muted p-6 rounded-xl shadow-sm mb-20"> @@ -86,39 +90,41 @@ const Dashboard = (): React.ReactElement => { /> </section> */} - {/* <section className="bg-muted p-6 rounded-xl shadow-sm"> - <h2 className="text-xl font-semibold text-primary mb-4"> - Compétences les plus utilisées (Top 5) - </h2> + <section className="bg-muted p-6 rounded-xl shadow-sm"> + <TextAdmin type='h2'> + {translations.messagePageDashBoardTittleSection2} + </TextAdmin> <HorizontalBarChart labels={topSkills.map(skill => skill.name)} data={topSkills.map(skill => skill.usageCount)} /> - </section> */} + </section> - {/* <section className="bg-muted p-6 rounded-xl shadow-sm"> - <h2 className="text-xl font-semibold text-primary mb-4"> - Répartition des rôles utilisateurs - </h2> + <section className="bg-muted p-6 rounded-xl shadow-sm"> + <TextAdmin type='h2'> + {translations.messagePageDashBoardTittleSection3} + </TextAdmin> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-text"> {[ - { label: 'Administrateurs', value: roleDistribution?.admin ?? 0 }, - { label: 'Éditeurs', value: roleDistribution?.editor ?? 0 }, - { label: 'Lecteurs', value: roleDistribution?.view ?? 0 }, + { label: translations.messagePageDashBoardRoleAdmin, value: roleDistribution?.admin ?? 0 }, + { label: translations.messagePageDashBoardRoleEditor, value: roleDistribution?.editor ?? 0 }, + { label: translations.messagePageDashBoardRoleViewer, value: roleDistribution?.view ?? 0 }, ].map((role) => ( <div key={role.label} className="flex flex-col items-center justify-center bg-background rounded-lg shadow-md py-4" > - <span className="text-muted-foreground">{role.label}</span> - <span className="text-2xl font-bold text-primary"> + <TextAdmin type='span'> + {role.label} + </TextAdmin> + <TextAdmin type='span'> {role.value.toFixed(0)}% - </span> + </TextAdmin> </div> ))} </div> - </section> */} + </section> </div> ) diff --git a/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx b/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx new file mode 100644 index 00000000..a16d963d --- /dev/null +++ b/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx @@ -0,0 +1,57 @@ +import { Typography, Box } from "@mui/material"; +import React from "react"; + +type Props = { + type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span"; + children: React.ReactNode; +}; + +const variantMap: Record<Props["type"], + "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "body1" | "body2"> = { + h1: "h1", + h2: "h2", + h3: "h3", + h4: "h4", + h5: "h5", + h6: "h6", + p: "body1", + span: "body2", +}; + +const fontSizeMap: Record<Props["type"], string> = { + h1: "2.5rem", // ~ text-4xl + h2: "2rem", // ~ text-3xl + h3: "1.75rem", // ~ text-2xl + h4: "1.5rem", // ~ text-xl + h5: "1.25rem", // ~ text-lg + h6: "1.125rem", // ~ text-base + p: "1rem", // ~ text-base + span: "0.875rem", // ~ text-sm +}; + +const getColorForType = (type: Props["type"]): string => { + if (type === "p" || type === "span") { + return "var(--text-color)"; + } + return "var(--primary-color)"; +}; + +const TextAdmin: React.FC<Props> = ({type, children }): React.ReactElement => { + return ( + <Box> + <Typography + variant={variantMap[type]} + component={type} + sx={{ + color: getColorForType(type), + fontSize: fontSizeMap[type], + fontWeight: "bold", + }} + > + {children} + </Typography> + </Box> + ); +}; + +export default TextAdmin; \ No newline at end of file diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx index 09e08eaa..bd33589d 100644 --- a/frontend/src/components/Button/Button.tsx +++ b/frontend/src/components/Button/Button.tsx @@ -19,7 +19,7 @@ const ButtonCustom: React.FC<Props> = ({ variant="contained" disabled={disable} sx={{ - fontSize: "1rem", + fontSize: "15px", px: 5, py: 1, borderRadius: "999px", diff --git a/frontend/src/components/Charts/DoughnutChart.tsx b/frontend/src/components/Charts/DoughnutChart.tsx index 0a17ae32..82f418c1 100644 --- a/frontend/src/components/Charts/DoughnutChart.tsx +++ b/frontend/src/components/Charts/DoughnutChart.tsx @@ -1,77 +1,77 @@ -// import { useEffect, useState } from 'react'; -// import { -// Chart as ChartJS, -// ArcElement, -// Tooltip, -// Legend, -// } from 'chart.js'; -// import { Doughnut } from 'react-chartjs-2'; +import { useEffect, useState } from 'react'; +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, +} from 'chart.js'; +import { Doughnut } from 'react-chartjs-2'; -// ChartJS.register(ArcElement, Tooltip, Legend); +ChartJS.register(ArcElement, Tooltip, Legend); -// interface DoughnutChartProps { -// labels: string[]; -// data: number[]; -// title?: string; -// } +interface DoughnutChartProps { + labels: string[]; + data: number[]; + title?: string; +} -// const DoughnutChart: React.FC<DoughnutChartProps> = ({ labels, data, title }) => { -// const [colors, setColors] = useState<string[]>([]); -// const [labelColor, setLabelColor] = useState<string>('#334155'); +const DoughnutChart: React.FC<DoughnutChartProps> = ({ labels, data, title }) => { + const [colors, setColors] = useState<string[]>([]); + const [labelColor, setLabelColor] = useState<string>('#334155'); -// /* ↪️ Récupère dynamiquement les variables CSS au montage (et au changement de thème) */ -// useEffect(() => { -// const style = getComputedStyle(document.documentElement); -// setColors( -// style -// .getPropertyValue('--chart-colors') -// .split(',') -// .map((c) => c.trim()), -// ); -// setLabelColor(style.getPropertyValue('--chart-label-color').trim()); -// }, []); + /* ↪️ Récupère dynamiquement les variables CSS au montage (et au changement de thème) */ + useEffect(() => { + const style = getComputedStyle(document.documentElement); + setColors( + style + .getPropertyValue('--chart-colors') + .split(',') + .map((c) => c.trim()), + ); + setLabelColor(style.getPropertyValue('--chart-label-color').trim()); + }, []); -// return ( -// <div className="w-full max-w-md mx-auto"> -// {title && ( -// <h3 className="text-lg font-semibold text-center mb-4 text-primary"> -// {title} -// </h3> -// )} + return ( + <div className="w-full max-w-md mx-auto"> + {title && ( + <h3 className="text-lg font-semibold text-center mb-4 text-primary"> + {title} + </h3> + )} -// <Doughnut -// data={{ -// labels, -// datasets: [ -// { -// data, -// backgroundColor: colors, -// borderWidth: 1, -// }, -// ], -// }} -// options={{ -// responsive: true, -// plugins: { -// tooltip: { -// backgroundColor: '#000000cc', -// titleColor: '#ffffff', -// bodyColor: '#ffffff', -// }, -// legend: { -// position: 'bottom', -// labels: { -// color: labelColor, -// boxWidth: 16, -// padding: 12, -// font: { size: 14 }, -// }, -// }, -// }, -// }} -// /> -// </div> -// ); -// }; + <Doughnut + data={{ + labels, + datasets: [ + { + data, + backgroundColor: colors, + borderWidth: 1, + }, + ], + }} + options={{ + responsive: true, + plugins: { + tooltip: { + backgroundColor: '#000000cc', + titleColor: '#ffffff', + bodyColor: '#ffffff', + }, + legend: { + position: 'bottom', + labels: { + color: labelColor, + boxWidth: 16, + padding: 12, + font: { size: 14 }, + }, + }, + }, + }} + /> + </div> + ); +}; -// export default DoughnutChart; +export default DoughnutChart; diff --git a/frontend/src/components/Charts/HorizontalBarChart.tsx b/frontend/src/components/Charts/HorizontalBarChart.tsx index feb6a095..5917af54 100644 --- a/frontend/src/components/Charts/HorizontalBarChart.tsx +++ b/frontend/src/components/Charts/HorizontalBarChart.tsx @@ -1,95 +1,104 @@ -// import { useState, useEffect } from 'react'; -// import { -// Chart as ChartJS, -// BarElement, -// CategoryScale, -// LinearScale, -// Tooltip, -// Legend, -// } from 'chart.js'; -// import { Bar } from 'react-chartjs-2'; -// import { useTheme } from "@/context/Theme/ThemeContext"; +import { useState, useEffect } from 'react'; +import { + Chart as ChartJS, + BarElement, + CategoryScale, + LinearScale, + Tooltip, + Legend, +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; +import { useTheme } from "@/context/Theme/ThemeContext"; +import { useLang } from '@/context/Lang/LangContext'; -// // ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); +ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); -// interface HorizontalBarChartProps { -// labels: string[]; -// data: number[]; -// } +interface HorizontalBarChartProps { + labels: string[]; + data: number[]; +} -// const HorizontalBarChart: React.FC<HorizontalBarChartProps> = ({ labels, data }) => { -// const [primaryColor, setPrimaryColor] = useState<string>('#6366f1'); -// const [labelColor, setLabelColor] = useState<string>('#1f2937'); +const HorizontalBarChart: React.FC<HorizontalBarChartProps> = ({ labels, data }) => { + const [primaryColor, setPrimaryColor] = useState<string>('#6366f1'); + const [labelColor, setLabelColor] = useState<string>('#1f2937'); -// const { theme } = useTheme(); + const { theme } = useTheme(); + const { translations } = useLang(); -// useEffect(() => { -// const style = getComputedStyle(document.documentElement); + useEffect(() => { + const style = getComputedStyle(document.documentElement); -// const cssPrimary = style.getPropertyValue('--primary-color').trim(); -// const cssTextColor = style.getPropertyValue('--text-color').trim(); + const cssPrimary = style.getPropertyValue('--primary-color').trim(); + const cssTextColor = style.getPropertyValue('--text-color').trim(); -// if (cssPrimary) setPrimaryColor(cssPrimary); -// if (cssTextColor) setLabelColor(cssTextColor); -// }, [theme]); + if (cssPrimary) setPrimaryColor(cssPrimary); + if (cssTextColor) setLabelColor(cssTextColor); + }, [theme, translations]); -// return ( -// <div className="w-full max-w-2xl mx-auto"> -// <Bar -// data={{ -// labels, -// datasets: [ -// { -// label: 'Nombre d’utilisations', -// data, -// backgroundColor: primaryColor, -// borderRadius: 4, -// barThickness: 12, -// borderWidth: 1, -// }, -// ], -// }} -// options={{ -// indexAxis: 'y', // horizontal -// responsive: true, -// plugins: { -// legend: { display: false }, -// tooltip: { -// backgroundColor: '#000000cc', -// titleColor: '#ffffff', -// bodyColor: '#ffffff', -// }, -// }, -// scales: { -// x: { -// beginAtZero: true, -// ticks: { -// precision: 0, -// color: labelColor, -// font: { -// size: 12, -// }, -// }, -// grid: { -// color: '#e5e7eb33', // optional: très léger -// }, -// }, -// y: { -// ticks: { -// color: labelColor, -// font: { -// size: 14, -// }, -// }, -// grid: { -// display: false, -// }, -// }, -// }, -// }} -// /> -// </div> -// ); -// }; + return ( + <div className="w-full max-w-4xl mx-auto" style={{ height: Math.max(300, labels.length * 40) }}> + <Bar + data={{ + labels, + datasets: [ + { + label: translations.messagePageDashBoardMessageStatsChart || "Nombre d'utilisations", + data, + backgroundColor: primaryColor, + borderRadius: 4, + barThickness: 12, + borderWidth: 1, + }, + ], + }} + options={{ + indexAxis: 'y', + responsive: true, + maintainAspectRatio: false, + animation: { + duration: 1000, + delay: (ctx) => { + const index = ctx.dataIndex; + const datasetIndex = ctx.datasetIndex; + return index * 150 + datasetIndex * 50; + }, + }, + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: '#000000cc', + titleColor: '#ffffff', + bodyColor: '#ffffff', + }, + }, + scales: { + x: { + beginAtZero: true, + ticks: { + precision: 0, + color: labelColor, + font: { size: 12 }, + }, + grid: { + color: '#e5e7eb33', + }, + }, + y: { + ticks: { + color: labelColor, + font: { size: 14 }, + callback: function(value) { + const label = this.getLabelForValue(Number(value)); + return label.length > 20 ? label.slice(0, 20) + '…' : label; + }, + }, + grid: { display: false }, + }, + }, + }} + /> + </div> + ); +}; -// export default HorizontalBarChart; \ No newline at end of file +export default HorizontalBarChart; \ No newline at end of file diff --git a/frontend/src/lang/en.tsx b/frontend/src/lang/en.tsx index 3dccddab..7eade6fb 100644 --- a/frontend/src/lang/en.tsx +++ b/frontend/src/lang/en.tsx @@ -141,6 +141,21 @@ const en: Lang = { "sideBarAdmin-cv": "Resume", "sideBarAdmin-cv/view": "View CV", "sideBarAdmin-cv/update": "Edit CV", + messagePageDashBoardTitle : "Dashboard", + messagePageDashBoardCardStatsProject : "Projects", + messagePageDashBoardCardStatsSkill : "Skills", + messagePageDashBoardCardStatsEducation : "Educations", + messagePageDashBoardCardStatsExperience : "experiences", + messagePageDashBoardCardStatsUser : "Users", + messagePageDashBoardTittleSection1 : "Average skills per project", + messagePageDashBoardTittleSection2 : "Most used skills", + messagePageDashBoardTittleSection3 : "Distribution of user roles", + messagePageDashBoardMessageAverageLeft : "Each project uses an average of", + messagePageDashBoardMessageAverageRight : "skills.", + messagePageDashBoardMessageStatsChart : "Number of uses", + messagePageDashBoardRoleAdmin : "Administrators", + messagePageDashBoardRoleEditor : "Editors", + messagePageDashBoardRoleViewer : "Readers", }; export default en; \ No newline at end of file diff --git a/frontend/src/lang/fr.tsx b/frontend/src/lang/fr.tsx index 4fd488f8..0a59d379 100644 --- a/frontend/src/lang/fr.tsx +++ b/frontend/src/lang/fr.tsx @@ -141,6 +141,21 @@ const fr: Lang = { "sideBarAdmin-cv": "CV", "sideBarAdmin-cv/view": "Voir le CV", "sideBarAdmin-cv/update": "Modifier le CV", + messagePageDashBoardTitle : "Tableau de bord", + messagePageDashBoardCardStatsProject : "Projets", + messagePageDashBoardCardStatsSkill : "Compétences", + messagePageDashBoardCardStatsEducation : "Formations", + messagePageDashBoardCardStatsExperience : "Expériences", + messagePageDashBoardCardStatsUser : "Utilisateurs", + messagePageDashBoardTittleSection1 : "Moyenne de compétences par projet", + messagePageDashBoardTittleSection2 : "Compétences les plus utilisées", + messagePageDashBoardTittleSection3 : "Répartition des rôles utilisateurs", + messagePageDashBoardMessageAverageLeft : "Chaque projet utilise en moyenne", + messagePageDashBoardMessageAverageRight : "compétences.", + messagePageDashBoardMessageStatsChart : "Nombre d'utilisations", + messagePageDashBoardRoleAdmin : "Administrateurs", + messagePageDashBoardRoleEditor : "Éditeurs", + messagePageDashBoardRoleViewer : "Lecteurs", }; export default fr; \ No newline at end of file diff --git a/frontend/src/lang/typeLang.tsx b/frontend/src/lang/typeLang.tsx index 51f12304..e1654194 100644 --- a/frontend/src/lang/typeLang.tsx +++ b/frontend/src/lang/typeLang.tsx @@ -124,6 +124,21 @@ type Lang = { "sideBarAdmin-cv": string; "sideBarAdmin-cv/view": string; "sideBarAdmin-cv/update": string; + messagePageDashBoardTitle : string; + messagePageDashBoardCardStatsProject : string; + messagePageDashBoardCardStatsSkill : string; + messagePageDashBoardCardStatsEducation : string; + messagePageDashBoardCardStatsExperience : string; + messagePageDashBoardCardStatsUser : string; + messagePageDashBoardTittleSection1 : string; + messagePageDashBoardTittleSection2 : string; + messagePageDashBoardTittleSection3 : string; + messagePageDashBoardMessageAverageLeft : string; + messagePageDashBoardMessageAverageRight : string; + messagePageDashBoardMessageStatsChart : string; + messagePageDashBoardRoleAdmin : string; + messagePageDashBoardRoleEditor : string; + messagePageDashBoardRoleViewer : string; }; export default Lang; \ No newline at end of file From fd7a12f31ac5f8c791b5a1a56632dd5d96801668 Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Thu, 26 Jun 2025 16:28:24 +0200 Subject: [PATCH 09/14] add route backend admin backup (list, view data and dl file) --- backend/src/index copy.ts | 83 +++++++++++----- backend/src/index.ts | 96 +++++++++++++++++++ backend/src/middlewares/authenticate.ts | 47 +++++++++ backend/src/middlewares/requireAdmin.ts | 9 ++ .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 2 +- .../AdminLayout/components/Text/TextAdmin.tsx | 16 ++-- 6 files changed, 222 insertions(+), 31 deletions(-) create mode 100644 backend/src/middlewares/authenticate.ts create mode 100644 backend/src/middlewares/requireAdmin.ts diff --git a/backend/src/index copy.ts b/backend/src/index copy.ts index 50697be9..b5585b88 100644 --- a/backend/src/index copy.ts +++ b/backend/src/index copy.ts @@ -7,7 +7,6 @@ import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHt import cors from "cors"; import { buildSchema } from "type-graphql"; import { ContactResolver } from "./resolvers/contact.resolver"; -import { GenerateImageResolver } from "./resolvers/generateImage.resolver"; import path from 'path'; import { CaptchaResolver } from './resolvers/captcha.resolver'; import { captchaImageMap, cleanUpExpiredCaptchas } from './CaptchaMap'; @@ -25,11 +24,12 @@ import "dotenv/config"; import { customAuthChecker } from "./lib/authChecker"; import { AdminResolver } from './resolvers/admin.resolver'; import { generateBadgeSvg } from './lib/badgeGenerator'; +import { loadedLogos, loadLogos } from './lib/logoLoader'; const prisma = new PrismaClient(); export interface JwtPayload { - userId: number; + id: number; } export interface MyContext { @@ -37,9 +37,18 @@ export interface MyContext { res: express.Response; apiKey: string | undefined; cookies: Cookies; + token : string | undefined | null; user: User | null; } +// export interface JwtPayload { +// userId: number; +// email?: string; +// role?: string; +// iat?: number; +// exp?: number; +// } + const app = express(); const httpServer = http.createServer(app); @@ -47,7 +56,6 @@ async function main() { const schema = await buildSchema({ resolvers: [ ContactResolver, - GenerateImageResolver, CaptchaResolver, SkillResolver, ProjectResolver, @@ -71,16 +79,16 @@ async function main() { const imageId = req.params.id; const filename = captchaImageMap[imageId]; if (filename) { - const imagePath = path.join(__dirname, 'images', filename); + const imagePath = path.join(__dirname, 'images/captcha', filename); res.sendFile(imagePath); } else { res.status(404).send('Image not found'); } }); - app.get('/badge/:label/:message/:messageColor/:labelColor', (req, res) => { - const { label, message, messageColor, labelColor } = req.params; - const { logo, logoColor, logoPosition } = req.query; + app.get('/badge/:label/:message/:messageColor/:labelColor/:logo', (req, res) => { + const { label, message, messageColor, labelColor, logo } = req.params; + const { logoColor, logoPosition } = req.query; try { const decodedLabel = decodeURIComponent(label); @@ -91,34 +99,45 @@ async function main() { const finalLogoPosition: 'left' | 'right' = logoPosition === 'right' ? 'right' : 'left'; + let logoDataForBadge: { base64: string; mimeType: string } | undefined; + if (logo) { + logoDataForBadge = loadedLogos.get(String(logo).toLowerCase()); + if (!logoDataForBadge) { + console.warn(`Logo personnalisé '${logo}' non trouvé dans les logos chargés.`); + } + } + const svg = generateBadgeSvg( decodedLabel, decodedMessage, decodedMessageColor, decodedLabelColor, - logo ? String(logo) : undefined, + logoDataForBadge, logoColor ? String(logoColor) : undefined, finalLogoPosition ); - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.send(svg); } catch (error) { console.error("Erreur lors de la génération du badge SVG:", error); - res.status(500).send('<svg width="120" height="20" xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)"><rect width="120" height="20" fill="#E05D44"/><text x="5" y="14" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11px" fill="white">Error</text></svg>'); + res.status(500).send('<svg width="120" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="120" height="20" fill="#E05D44"/><text x="5" y="14" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11px" fill="white">Error</text></svg>'); } }); app.get('/badge/stats/projects-count', async (req, res) => { try { const projectCount = await prisma.project.count(); + const logoData = loadedLogos.get('github'); + if (!logoData) console.warn("Logo 'github' non trouvé pour le badge projets."); + const svg = generateBadgeSvg( 'Projets', String(projectCount), '4CAF50', - '2F4F4F', - 'JavaScript', + '2F4F4F', + logoData, 'white', 'right' ); @@ -127,8 +146,27 @@ async function main() { res.send(svg); } catch (error) { console.error("Erreur lors de la génération du badge des projets:", error); - res.status(500).send('<svg width="120" height="20" xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)"><rect width="120" height="20" fill="#E05D44"/><text x="5" y="14" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11px" fill="white">Error</text></svg>'); + res.status(500).send('<svg width="120" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="120" height="20" fill="#E05D44"/><text x="5" y="14" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11px" fill="white">Error</text></svg>'); + } + }); + + app.get('/upload/:type/:filename', (req, res) => { + const { type, filename } = req.params; + + if (!['image', 'video'].includes(type)) { + return res.status(400).send('Invalid type. Use "image" or "video".'); } + + const filePath = path.join(__dirname, '.', 'uploads', `${type}s`, filename); + + res.sendFile(filePath, (err) => { + if (err) { + if (!res.headersSent) { + console.error(`Fichier non trouvé : ${filePath}`); + return res.status(404).send('Fichier non trouvé'); + } + } + }); }); app.use( @@ -141,23 +179,23 @@ async function main() { expressMiddleware(server, { context: async ({ req, res }) => { const cookies = new Cookies(req, res); - console.log("cookies:", cookies.get("jwt")); + let user: User | null = null; - const token = cookies.get("jwt"); - console.log("Token du cookie:", token ? "Présent" : "Absent"); + const token = cookies.get("token"); if (token && process.env.JWT_SECRET) { + // console.log("token ---->", token) try { const { payload } = await jwtVerify<JwtPayload>( token, new TextEncoder().encode(process.env.JWT_SECRET) ); - console.log("Payload du token décodé:", payload); + // console.log("Payload du token décodé:", payload); const prismaUser = await prisma.user.findUnique({ - where: { id: payload.userId } + where: { id: payload.id } }); if (prismaUser) { @@ -173,7 +211,7 @@ async function main() { } catch (err) { console.error("Erreur de vérification JWT:", err); // Log l'erreur complète - cookies.set("jwt", "", { expires: new Date(0), httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const }); + cookies.set("token", "", { expires: new Date(0), httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const }); } } @@ -190,7 +228,7 @@ async function main() { await checkApiKey(apiKey); } - return { req, res, apiKey, cookies, user }; + return { req, res, apiKey, cookies, token, user }; }, }) ); @@ -203,4 +241,5 @@ async function main() { console.log(`🚀 Server lancé sur http://localhost:4000/`); } -main(); \ No newline at end of file +main(); +loadLogos(); \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index b5585b88..261c5381 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -25,6 +25,10 @@ import { customAuthChecker } from "./lib/authChecker"; import { AdminResolver } from './resolvers/admin.resolver'; import { generateBadgeSvg } from './lib/badgeGenerator'; import { loadedLogos, loadLogos } from './lib/logoLoader'; +import fs from "fs/promises"; +import { authenticate } from "./middlewares/authenticate"; +import { requireAdmin } from "./middlewares/requireAdmin"; +import { createReadStream } from 'fs'; const prisma = new PrismaClient(); @@ -150,6 +154,98 @@ async function main() { } }); + app.get( + "/admin/backups", + authenticate, + requireAdmin, + async (req, res): Promise<void> => { + try { + const backupDir = path.resolve(__dirname, ".", "data"); + const files = await fs.readdir(backupDir); + + const backups = files.filter((f) => + /^bdd_\d{8}_\d{6}\.sql$/i.test(f) + ); + + res.json(backups); + } catch (err) { + console.error("Erreur lecture des sauvegardes :", err); + res.status(500).send("Impossible de lire les sauvegardes"); + } + } + ); + + app.get( + "/admin/backups/:filename", + authenticate, + requireAdmin, + async (req, res): Promise<void> => { + try { + const backupDir = path.resolve(__dirname, ".", "data"); + const filename = req.params.filename; + + if (!filename) { + res.status(400).send("Nom de fichier manquant"); + return; + } + if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { + res.status(400).send("Nom de fichier invalide"); + return; + } + + const fullPath = path.join(backupDir, filename); + + try { + await fs.access(fullPath); + } catch { + res.status(404).send("Fichier introuvable"); + return; + } + + const content = await fs.readFile(fullPath, "utf-8"); + res.type("text/plain").send(content); + } catch (err) { + console.error("Erreur lecture fichier backup :", err); + res.status(500).send("Impossible de lire le fichier de sauvegarde"); + } + } + ); + + app.get( + "/admin/backups/:filename/download", + authenticate, + requireAdmin, + async (req, res): Promise<void> => { + try { + const backupDir = path.resolve(__dirname, ".", "data"); + const filename = req.params.filename; + + if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { + res.status(400).send("Nom de fichier invalide"); + return; + } + + const fullPath = path.join(backupDir, filename); + + try { + await fs.access(fullPath); + } catch { + res.status(404).send("Fichier introuvable"); + return; + } + + res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); + res.setHeader("Content-Type", "application/sql"); + + const fileStream = createReadStream(fullPath); + fileStream.pipe(res); + } catch (err) { + console.error("Erreur téléchargement fichier backup :", err); + res.status(500).send("Erreur lors du téléchargement"); + } + } + ); + app.get('/upload/:type/:filename', (req, res) => { const { type, filename } = req.params; diff --git a/backend/src/middlewares/authenticate.ts b/backend/src/middlewares/authenticate.ts new file mode 100644 index 00000000..9269cda3 --- /dev/null +++ b/backend/src/middlewares/authenticate.ts @@ -0,0 +1,47 @@ +import { RequestHandler } from "express"; +import Cookies from "cookies"; +import { jwtVerify } from "jose"; +import { PrismaClient } from "@prisma/client"; +import { JwtPayload } from "../index"; +import { User } from "../entities/user.entity"; + +declare global { + namespace Express { + interface Request { + user?: User; + } + } +} + +const prisma = new PrismaClient(); + +/** Vérifie le cookie JWT et remplit req.user */ +export const authenticate: RequestHandler = async (req, res, next) => { + const cookies = new Cookies(req, res); + const token = cookies.get("token"); + + if (!token || !process.env.JWT_SECRET) { + return res.status(401).send("Non authentifié"); + } + + try { + const { payload } = await jwtVerify<JwtPayload>( + token, + new TextEncoder().encode(process.env.JWT_SECRET) + ); + + const prismaUser = await prisma.user.findUnique({ + where: { id: payload.id }, + }); + if (!prismaUser) { + return res.status(401).send("Utilisateur introuvable"); + } + + // cast léger : prismaUser.role === "ADMIN" | "USER" … -> enum UserRole + req.user = prismaUser as unknown as User; + next(); + } catch (err) { + console.error("JWT invalide :", err); + return res.status(401).send("Token invalide ou expiré"); + } +}; \ No newline at end of file diff --git a/backend/src/middlewares/requireAdmin.ts b/backend/src/middlewares/requireAdmin.ts new file mode 100644 index 00000000..48d47f0e --- /dev/null +++ b/backend/src/middlewares/requireAdmin.ts @@ -0,0 +1,9 @@ +import { RequestHandler } from "express"; +import { UserRole } from "../entities/user.entity"; + +export const requireAdmin: RequestHandler = (req, res, next) => { + if (req.user?.role !== UserRole.admin) { + return res.status(403).send("Accès refusé (ADMIN requis)"); + } + next(); +}; \ No newline at end of file diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index 14989f9a..38203ef5 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -39,7 +39,7 @@ const Dashboard = (): React.ReactElement => { <TextAdmin type='h1'> {translations.messagePageDashBoardTitle} </TextAdmin> - <section className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6"> + <section className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6 p-6"> {mainStats.map((stat) => ( <DashboardCard key={stat.title} {...stat} /> ))} diff --git a/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx b/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx index a16d963d..e57eb8d5 100644 --- a/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx +++ b/frontend/src/components/AdminLayout/components/Text/TextAdmin.tsx @@ -19,14 +19,14 @@ const variantMap: Record<Props["type"], }; const fontSizeMap: Record<Props["type"], string> = { - h1: "2.5rem", // ~ text-4xl - h2: "2rem", // ~ text-3xl - h3: "1.75rem", // ~ text-2xl - h4: "1.5rem", // ~ text-xl - h5: "1.25rem", // ~ text-lg - h6: "1.125rem", // ~ text-base - p: "1rem", // ~ text-base - span: "0.875rem", // ~ text-sm + h1: "2rem", // ~ text-4xl // réduit depuis 2.5rem + h2: "1.75rem", // ~ text-3xl // réduit depuis 2rem + h3: "1.5rem", // ~ text-2xl // réduit depuis 1.75rem + h4: "1.25rem", // ~ text-xl // réduit depuis 1.5rem + h5: "1.125rem", // ~ text-lg // réduit depuis 1.25rem + h6: "1rem", // ~ text-base // réduit depuis 1.125rem + p: "0.875rem", // ~ text-base // réduit depuis 1rem + span: "0.75rem", // ~ text-sm // réduit depuis 0.875rem }; const getColorForType = (type: Props["type"]): string => { From 5d66603f479901114d8fabecee47d88f615f876a Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Thu, 26 Jun 2025 18:47:21 +0200 Subject: [PATCH 10/14] add split all routes backend files , (API rest : backups badges, captcha, upload) and api graphql --- backend/package-lock.json | 649 ++++++++++++++-------- backend/package.json | 1 + backend/src/index copy.ts | 98 +++- backend/src/index.ts | 350 ++---------- backend/src/middlewares/authenticate.ts | 8 +- backend/src/middlewares/requireAdmin.ts | 2 +- backend/src/resolvers/captcha.resolver.ts | 2 +- backend/src/routes/backups.routes.ts | 60 ++ backend/src/routes/badge.routes.ts | 57 ++ backend/src/routes/captcha.routes.ts | 20 + backend/src/routes/graphql.routes.ts | 122 ++++ backend/src/routes/upload.routes.ts | 23 + 12 files changed, 840 insertions(+), 552 deletions(-) create mode 100644 backend/src/routes/backups.routes.ts create mode 100644 backend/src/routes/badge.routes.ts create mode 100644 backend/src/routes/captcha.routes.ts create mode 100644 backend/src/routes/graphql.routes.ts create mode 100644 backend/src/routes/upload.routes.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index eb67ed98..7ec84e1d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,6 +23,7 @@ "graphql-scalars": "^1.22.4", "jose": "^5.2.3", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", "pg": "^8.11.3", @@ -1344,6 +1345,7 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3058,6 +3060,102 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3882,6 +3980,16 @@ "node": ">=10" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@prisma/client": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.9.0.tgz", @@ -4605,11 +4713,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } }, "node_modules/anymatch": { "version": "3.1.3", @@ -4677,7 +4788,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/array-flatten": { "version": "1.1.1", @@ -4935,6 +5047,24 @@ } ] }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4982,9 +5112,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5222,6 +5353,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5388,82 +5520,6 @@ "node": ">=8" } }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cli-highlight/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-highlight/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/cli-progress": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", @@ -5849,7 +5905,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5866,21 +5921,11 @@ "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", "dev": true }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" }, "node_modules/debounce": { "version": "1.2.1", @@ -5921,7 +5966,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -6153,6 +6197,12 @@ "xtend": "^4.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -6585,6 +6635,34 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -6982,6 +7060,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -7045,15 +7124,6 @@ "tslib": "^2.0.3" } }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7516,8 +7586,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isomorphic-ws": { "version": "5.0.0", @@ -7656,6 +7725,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", @@ -8296,6 +8380,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -8901,6 +8986,34 @@ "node": ">=10" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -8941,17 +9054,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nan": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", @@ -9116,6 +9218,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9246,6 +9357,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -9300,27 +9417,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9371,7 +9467,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -9402,6 +9497,28 @@ "node": ">=0.10.0" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -10010,12 +10127,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, "node_modules/scuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", @@ -10144,7 +10255,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -10156,7 +10266,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -10324,6 +10433,22 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -10402,6 +10527,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10413,6 +10553,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -10515,6 +10668,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10572,27 +10726,6 @@ "node": ">=8" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -10952,28 +11085,25 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typeorm": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.11.tgz", - "integrity": "sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", + "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", "license": "MIT", "dependencies": { - "@sqltools/formatter": "^1.2.2", - "app-root-path": "^3.0.0", + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", "buffer": "^6.0.3", - "chalk": "^4.1.0", - "cli-highlight": "^2.1.11", - "date-fns": "^2.28.0", - "debug": "^4.3.3", - "dotenv": "^16.0.0", - "glob": "^7.2.0", - "js-yaml": "^4.1.0", - "mkdirp": "^1.0.4", - "reflect-metadata": "^0.1.13", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", "sha.js": "^2.4.11", - "tslib": "^2.3.1", - "uuid": "^8.3.2", - "xml2js": "^0.4.23", - "yargs": "^17.3.1" + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" }, "bin": { "typeorm": "cli.js", @@ -10981,29 +11111,30 @@ "typeorm-ts-node-esm": "cli-ts-node-esm.js" }, "engines": { - "node": ">= 12.9.0" + "node": ">=16.13.0" }, "funding": { "url": "https://opencollective.com/typeorm" }, "peerDependencies": { - "@google-cloud/spanner": "^5.18.0", + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", "@sap/hana-client": "^2.12.25", - "better-sqlite3": "^7.1.2 || ^8.0.0", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "hdb-pool": "^0.1.6", "ioredis": "^5.0.4", - "mongodb": "^3.6.0", - "mssql": "^7.3.0", - "mysql2": "^2.2.5", - "oracledb": "^5.1.0", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0", + "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0" + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "peerDependenciesMeta": { "@google-cloud/spanner": { @@ -11130,6 +11261,15 @@ "node": ">=10" } }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/typeorm/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -11170,25 +11310,73 @@ } } }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/typeorm/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/typeorm/node_modules/reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "license": "Apache-2.0" + "node_modules/typeorm/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/typeorm/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/typescript": { @@ -11477,7 +11665,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -11516,6 +11703,24 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11555,28 +11760,6 @@ } } }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index eb9c345c..b0c78581 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,6 +32,7 @@ "graphql-scalars": "^1.22.4", "jose": "^5.2.3", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", "pg": "^8.11.3", diff --git a/backend/src/index copy.ts b/backend/src/index copy.ts index b5585b88..fd0c78a0 100644 --- a/backend/src/index copy.ts +++ b/backend/src/index copy.ts @@ -25,6 +25,10 @@ import { customAuthChecker } from "./lib/authChecker"; import { AdminResolver } from './resolvers/admin.resolver'; import { generateBadgeSvg } from './lib/badgeGenerator'; import { loadedLogos, loadLogos } from './lib/logoLoader'; +import fs from "fs/promises"; +import { authenticate } from "./middlewares/authenticate"; +import { requireAdmin } from "./middlewares/requireAdmin"; +import { createReadStream } from 'fs'; const prisma = new PrismaClient(); @@ -150,6 +154,98 @@ async function main() { } }); + app.get( + "/admin/backups", + authenticate, + requireAdmin, + async (req, res): Promise<void> => { + try { + const backupDir = path.resolve(__dirname, ".", "data"); + const files = await fs.readdir(backupDir); + + const backups = files.filter((f) => + /^bdd_\d{8}_\d{6}\.sql$/i.test(f) + ); + + res.json(backups); + } catch (err) { + console.error("Erreur lecture des sauvegardes :", err); + res.status(500).send("Unable to read backups"); + } + } + ); + + app.get( + "/admin/backups/:filename", + authenticate, + requireAdmin, + async (req, res): Promise<void> => { + try { + const backupDir = path.resolve(__dirname, ".", "data"); + const filename = req.params.filename; + + if (!filename) { + res.status(400).send("Missing file name"); + return; + } + if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { + res.status(400).send("Invalid file name"); + return; + } + + const fullPath = path.join(backupDir, filename); + + try { + await fs.access(fullPath); + } catch { + res.status(404).send("File not found"); + return; + } + + const content = await fs.readFile(fullPath, "utf-8"); + res.type("text/plain").send(content); + } catch (err) { + console.error("Erreur lecture fichier backup :", err); + res.status(500).send("Unable to read save file"); + } + } + ); + + app.get( + "/admin/backups/:filename/download", + authenticate, + requireAdmin, + async (req, res): Promise<void> => { + try { + const backupDir = path.resolve(__dirname, ".", "data"); + const filename = req.params.filename; + + if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { + res.status(400).send("Invalid file name"); + return; + } + + const fullPath = path.join(backupDir, filename); + + try { + await fs.access(fullPath); + } catch { + res.status(404).send("File not found"); + return; + } + + res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); + res.setHeader("Content-Type", "application/sql"); + + const fileStream = createReadStream(fullPath); + fileStream.pipe(res); + } catch (err) { + console.error("Erreur téléchargement fichier backup :", err); + res.status(500).send("Error while downloading backup file"); + } + } + ); + app.get('/upload/:type/:filename', (req, res) => { const { type, filename } = req.params; @@ -210,7 +306,7 @@ async function main() { } } catch (err) { - console.error("Erreur de vérification JWT:", err); // Log l'erreur complète + console.error("Erreur de vérification JWT:", err); cookies.set("token", "", { expires: new Date(0), httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const }); } } diff --git a/backend/src/index.ts b/backend/src/index.ts index 261c5381..239bb0c9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,36 +1,18 @@ -import 'reflect-metadata'; +import "reflect-metadata"; import express from "express"; import http from "http"; -import { ApolloServer } from "@apollo/server"; -import { expressMiddleware } from "@apollo/server/express4"; -import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer"; import cors from "cors"; -import { buildSchema } from "type-graphql"; -import { ContactResolver } from "./resolvers/contact.resolver"; -import path from 'path'; -import { CaptchaResolver } from './resolvers/captcha.resolver'; -import { captchaImageMap, cleanUpExpiredCaptchas } from './CaptchaMap'; -import { SkillResolver } from './resolvers/skill.resolver'; -import { checkApiKey } from './lib/checkApiKey'; -import { ExperienceResolver } from './resolvers/experience.resolver'; -import { EducationResolver } from './resolvers/education.resolver'; -import { ProjectResolver } from './resolvers/project.resolver'; -import { UserResolver } from './resolvers/user.resolver'; +import path from "path"; +import dotenv from "dotenv"; +import badgeRoutes from "./routes/badge.routes"; +import backupsRoutes from "./routes/backups.routes"; +import captchaRoutes from "./routes/captcha.routes"; +import uploadRoutes from "./routes/upload.routes"; +import { mountGraphQL } from "./routes/graphql.routes"; +import { cleanUpExpiredCaptchas } from "./CaptchaMap"; +import { loadLogos } from "./lib/logoLoader"; import Cookies from "cookies"; -import { PrismaClient } from "@prisma/client"; -import { User, UserRole } from "./entities/user.entity"; -import { jwtVerify } from "jose"; -import "dotenv/config"; -import { customAuthChecker } from "./lib/authChecker"; -import { AdminResolver } from './resolvers/admin.resolver'; -import { generateBadgeSvg } from './lib/badgeGenerator'; -import { loadedLogos, loadLogos } from './lib/logoLoader'; -import fs from "fs/promises"; -import { authenticate } from "./middlewares/authenticate"; -import { requireAdmin } from "./middlewares/requireAdmin"; -import { createReadStream } from 'fs'; - -const prisma = new PrismaClient(); +import { User } from "./entities/user.entity"; export interface JwtPayload { id: number; @@ -45,297 +27,43 @@ export interface MyContext { user: User | null; } -// export interface JwtPayload { -// userId: number; -// email?: string; -// role?: string; -// iat?: number; -// exp?: number; -// } +dotenv.config(); const app = express(); const httpServer = http.createServer(app); +const PORT = process.env.PORT || 4000; -async function main() { - const schema = await buildSchema({ - resolvers: [ - ContactResolver, - CaptchaResolver, - SkillResolver, - ProjectResolver, - ExperienceResolver, - EducationResolver, - UserResolver, - AdminResolver, - ], - validate: false, - authChecker: customAuthChecker, - }); - - const server = new ApolloServer<MyContext>({ - schema, - plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], - }); - - await server.start(); - - app.get('/dynamic-images/:id', (req, res) => { - const imageId = req.params.id; - const filename = captchaImageMap[imageId]; - if (filename) { - const imagePath = path.join(__dirname, 'images/captcha', filename); - res.sendFile(imagePath); - } else { - res.status(404).send('Image not found'); - } - }); - - app.get('/badge/:label/:message/:messageColor/:labelColor/:logo', (req, res) => { - const { label, message, messageColor, labelColor, logo } = req.params; - const { logoColor, logoPosition } = req.query; - - try { - const decodedLabel = decodeURIComponent(label); - const decodedMessage = decodeURIComponent(message); - const decodedMessageColor = decodeURIComponent(messageColor); - const decodedLabelColor = decodeURIComponent(labelColor); - - const finalLogoPosition: 'left' | 'right' = - logoPosition === 'right' ? 'right' : 'left'; - - let logoDataForBadge: { base64: string; mimeType: string } | undefined; - if (logo) { - logoDataForBadge = loadedLogos.get(String(logo).toLowerCase()); - if (!logoDataForBadge) { - console.warn(`Logo personnalisé '${logo}' non trouvé dans les logos chargés.`); - } - } - - const svg = generateBadgeSvg( - decodedLabel, - decodedMessage, - decodedMessageColor, - decodedLabelColor, - logoDataForBadge, - logoColor ? String(logoColor) : undefined, - finalLogoPosition - ); - - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.send(svg); - } catch (error) { - console.error("Erreur lors de la génération du badge SVG:", error); - res.status(500).send('<svg width="120" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="120" height="20" fill="#E05D44"/><text x="5" y="14" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11px" fill="white">Error</text></svg>'); - } - }); - - app.get('/badge/stats/projects-count', async (req, res) => { - try { - const projectCount = await prisma.project.count(); - const logoData = loadedLogos.get('github'); - if (!logoData) console.warn("Logo 'github' non trouvé pour le badge projets."); - - const svg = generateBadgeSvg( - 'Projets', - String(projectCount), - '4CAF50', - '2F4F4F', - logoData, - 'white', - 'right' - ); - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.send(svg); - } catch (error) { - console.error("Erreur lors de la génération du badge des projets:", error); - res.status(500).send('<svg width="120" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="120" height="20" fill="#E05D44"/><text x="5" y="14" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11px" fill="white">Error</text></svg>'); - } - }); - - app.get( - "/admin/backups", - authenticate, - requireAdmin, - async (req, res): Promise<void> => { - try { - const backupDir = path.resolve(__dirname, ".", "data"); - const files = await fs.readdir(backupDir); - - const backups = files.filter((f) => - /^bdd_\d{8}_\d{6}\.sql$/i.test(f) - ); - - res.json(backups); - } catch (err) { - console.error("Erreur lecture des sauvegardes :", err); - res.status(500).send("Impossible de lire les sauvegardes"); - } - } - ); - - app.get( - "/admin/backups/:filename", - authenticate, - requireAdmin, - async (req, res): Promise<void> => { - try { - const backupDir = path.resolve(__dirname, ".", "data"); - const filename = req.params.filename; - - if (!filename) { - res.status(400).send("Nom de fichier manquant"); - return; - } - if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { - res.status(400).send("Nom de fichier invalide"); - return; - } - - const fullPath = path.join(backupDir, filename); - - try { - await fs.access(fullPath); - } catch { - res.status(404).send("Fichier introuvable"); - return; - } +app.use( + cors({ + origin: process.env.CLIENT_URL?.split(",") ?? ["http://localhost:3000"], + credentials: true, + }) +); +app.use(express.json()); - const content = await fs.readFile(fullPath, "utf-8"); - res.type("text/plain").send(content); - } catch (err) { - console.error("Erreur lecture fichier backup :", err); - res.status(500).send("Impossible de lire le fichier de sauvegarde"); - } - } - ); +app.use("/api/badges", badgeRoutes); // → /api/badges/… +app.use("/api/backups", backupsRoutes); // → /api/backups/… +app.use("/api/dynamic-images", captchaRoutes); // → /api/dynamic-images/:id +app.use("/api/upload", uploadRoutes); // → /api/upload/:type/:filename - app.get( - "/admin/backups/:filename/download", - authenticate, - requireAdmin, - async (req, res): Promise<void> => { - try { - const backupDir = path.resolve(__dirname, ".", "data"); - const filename = req.params.filename; +app.use( + "/uploads", + express.static(path.join(__dirname, "../uploads"), { + maxAge: "7d", + immutable: true, + }) +); - if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { - res.status(400).send("Nom de fichier invalide"); - return; - } +(async () => { - const fullPath = path.join(backupDir, filename); - - try { - await fs.access(fullPath); - } catch { - res.status(404).send("Fichier introuvable"); - return; - } - - res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); - res.setHeader("Content-Type", "application/sql"); - - const fileStream = createReadStream(fullPath); - fileStream.pipe(res); - } catch (err) { - console.error("Erreur téléchargement fichier backup :", err); - res.status(500).send("Erreur lors du téléchargement"); - } - } - ); - - app.get('/upload/:type/:filename', (req, res) => { - const { type, filename } = req.params; - - if (!['image', 'video'].includes(type)) { - return res.status(400).send('Invalid type. Use "image" or "video".'); - } - - const filePath = path.join(__dirname, '.', 'uploads', `${type}s`, filename); - - res.sendFile(filePath, (err) => { - if (err) { - if (!res.headersSent) { - console.error(`Fichier non trouvé : ${filePath}`); - return res.status(404).send('Fichier non trouvé'); - } - } - }); - }); - - app.use( - "/graphql", - cors<cors.CorsRequest>({ - origin: ["http://localhost:3000"], - credentials: true, - }), - express.json(), - expressMiddleware(server, { - context: async ({ req, res }) => { - const cookies = new Cookies(req, res); - - let user: User | null = null; - - const token = cookies.get("token"); - - if (token && process.env.JWT_SECRET) { - // console.log("token ---->", token) - try { - const { payload } = await jwtVerify<JwtPayload>( - token, - new TextEncoder().encode(process.env.JWT_SECRET) - ); - - // console.log("Payload du token décodé:", payload); - - const prismaUser = await prisma.user.findUnique({ - where: { id: payload.id } - }); - - if (prismaUser) { - user = { - id: prismaUser.id, - email: prismaUser.email, - firstname: prismaUser.firstname, - lastname: prismaUser.lastname, - role: prismaUser.role as UserRole, - isPasswordChange: prismaUser.isPasswordChange, - }; - } - - } catch (err) { - console.error("Erreur de vérification JWT:", err); // Log l'erreur complète - cookies.set("token", "", { expires: new Date(0), httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const }); - } - } - - const apiKeyHeader = req.headers['x-api-key']; - const apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader; - - // const operationName = req.body.operationName || (req.body.query && req.body.query.match(/(mutation|query)\s+(\w+)/)?.[2]); - - if (!apiKey) { - throw new Error('Unauthorized: x-api-key header is missing.'); - } - - if (apiKey) { - await checkApiKey(apiKey); - } - - return { req, res, apiKey, cookies, token, user }; - }, - }) - ); + await mountGraphQL(app); setInterval(cleanUpExpiredCaptchas, 15 * 60 * 1000); - await new Promise<void>((resolve) => - httpServer.listen({ port: 4000 }, resolve) - ); - console.log(`🚀 Server lancé sur http://localhost:4000/`); -} + loadLogos(); -main(); -loadLogos(); \ No newline at end of file + httpServer.listen(PORT, () => { + console.log(`✅ REST ready → http://localhost:${PORT}`); + console.log(`✅ GraphQL ready→ http://localhost:${PORT}/graphql`); + }); +})(); \ No newline at end of file diff --git a/backend/src/middlewares/authenticate.ts b/backend/src/middlewares/authenticate.ts index 9269cda3..005eff71 100644 --- a/backend/src/middlewares/authenticate.ts +++ b/backend/src/middlewares/authenticate.ts @@ -15,13 +15,12 @@ declare global { const prisma = new PrismaClient(); -/** Vérifie le cookie JWT et remplit req.user */ export const authenticate: RequestHandler = async (req, res, next) => { const cookies = new Cookies(req, res); const token = cookies.get("token"); if (!token || !process.env.JWT_SECRET) { - return res.status(401).send("Non authentifié"); + return res.status(401).send("Unauthenticated"); } try { @@ -34,14 +33,13 @@ export const authenticate: RequestHandler = async (req, res, next) => { where: { id: payload.id }, }); if (!prismaUser) { - return res.status(401).send("Utilisateur introuvable"); + return res.status(401).send("User not found"); } - // cast léger : prismaUser.role === "ADMIN" | "USER" … -> enum UserRole req.user = prismaUser as unknown as User; next(); } catch (err) { console.error("JWT invalide :", err); - return res.status(401).send("Token invalide ou expiré"); + return res.status(401).send("Invalid or expired token"); } }; \ No newline at end of file diff --git a/backend/src/middlewares/requireAdmin.ts b/backend/src/middlewares/requireAdmin.ts index 48d47f0e..bd069d9d 100644 --- a/backend/src/middlewares/requireAdmin.ts +++ b/backend/src/middlewares/requireAdmin.ts @@ -3,7 +3,7 @@ import { UserRole } from "../entities/user.entity"; export const requireAdmin: RequestHandler = (req, res, next) => { if (req.user?.role !== UserRole.admin) { - return res.status(403).send("Accès refusé (ADMIN requis)"); + return res.status(403).send("Access denied (ADMIN required)"); } next(); }; \ No newline at end of file diff --git a/backend/src/resolvers/captcha.resolver.ts b/backend/src/resolvers/captcha.resolver.ts index 63b99bcf..370765af 100644 --- a/backend/src/resolvers/captcha.resolver.ts +++ b/backend/src/resolvers/captcha.resolver.ts @@ -59,7 +59,7 @@ export class CaptchaResolver { const captchaImages = selectedImages.map(image => { const imageId = uuidv4(); - const imageUrl = `${BASE_URL}/dynamic-images/${imageId}`; + const imageUrl = `${BASE_URL}/api/dynamic-images/${imageId}`; captchaImageMap[imageId] = image.src; diff --git a/backend/src/routes/backups.routes.ts b/backend/src/routes/backups.routes.ts new file mode 100644 index 00000000..01e77f16 --- /dev/null +++ b/backend/src/routes/backups.routes.ts @@ -0,0 +1,60 @@ +import { Router } from "express"; +import fs from "fs/promises"; +import path from "path"; +import { authenticate } from "../middlewares/authenticate"; +import { requireAdmin } from "../middlewares/requireAdmin"; +import { createReadStream } from "fs"; + +const router = Router(); + +const BACKUP_DIR = path.resolve(__dirname, "..", "data"); + +router.get("/", authenticate, requireAdmin, async (req, res) => { + try { + const files = await fs.readdir(BACKUP_DIR); + const backups = files.filter(f => /^bdd_\d{8}_\d{6}\.sql$/i.test(f)); + res.json(backups); + } catch (err) { + console.error("Erreur lecture sauvegardes:", err); + res.status(500).send("Erreur lecture des sauvegardes"); + } +}); + +router.get("/:filename", authenticate, requireAdmin, async (req, res) => { + const filename = req.params.filename; + const fullPath = path.join(BACKUP_DIR, filename); + + if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { + return res.status(400).send("Nom de fichier invalide"); + } + + try { + await fs.access(fullPath); + const content = await fs.readFile(fullPath, "utf-8"); + res.type("text/plain").send(content); + } catch (err) { + console.error("Erreur lecture fichier:", err); + res.status(404).send("Fichier non trouvé"); + } +}); + +router.get("/:filename/download", authenticate, requireAdmin, async (req, res) => { + const filename = req.params.filename; + const fullPath = path.join(BACKUP_DIR, filename); + + if (!/^bdd_\d{8}_\d{6}\.sql$/i.test(filename)) { + return res.status(400).send("Nom de fichier invalide"); + } + + try { + await fs.access(fullPath); + res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); + res.setHeader("Content-Type", "application/sql"); + createReadStream(fullPath).pipe(res); + } catch (err) { + console.error("Erreur téléchargement:", err); + res.status(404).send("Fichier non trouvé"); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/badge.routes.ts b/backend/src/routes/badge.routes.ts new file mode 100644 index 00000000..28a1bf3a --- /dev/null +++ b/backend/src/routes/badge.routes.ts @@ -0,0 +1,57 @@ +import { Router } from "express"; +import { generateBadgeSvg } from "../lib/badgeGenerator"; +import { loadedLogos } from "../lib/logoLoader"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); +const router = Router(); + +router.get('/:label/:message/:messageColor/:labelColor/:logo', (req, res) => { + const { label, message, messageColor, labelColor, logo } = req.params; + const { logoColor, logoPosition } = req.query; + + try { + const svg = generateBadgeSvg( + decodeURIComponent(label), + decodeURIComponent(message), + decodeURIComponent(messageColor), + decodeURIComponent(labelColor), + logo ? loadedLogos.get(String(logo).toLowerCase()) : undefined, + logoColor ? String(logoColor) : undefined, + logoPosition === 'right' ? 'right' : 'left' + ); + + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.send(svg); + } catch (error) { + console.error("Erreur badge SVG:", error); + res.status(500).send("Erreur génération badge"); + } +}); + +router.get('/stats/projects-count', async (req, res) => { + try { + const count = await prisma.project.count(); + const logo = loadedLogos.get('github'); + + const svg = generateBadgeSvg( + 'Projets', + String(count), + '4CAF50', + '2F4F4F', + logo, + 'white', + 'right' + ); + + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.send(svg); + } catch (err) { + console.error("Erreur badge projets:", err); + res.status(500).send("Erreur génération badge"); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/captcha.routes.ts b/backend/src/routes/captcha.routes.ts new file mode 100644 index 00000000..1cb76f99 --- /dev/null +++ b/backend/src/routes/captcha.routes.ts @@ -0,0 +1,20 @@ +import { Router } from "express"; +import path from "path"; +import fs from "fs"; +import { captchaImageMap } from "../CaptchaMap"; +const router = Router(); + +router.get("/:id", (req, res) => { + const imageId = req.params.id; + const filename = captchaImageMap[imageId]; + + if (filename) { + const imagePath = path.join(__dirname, "../images/captcha", filename); + res.sendFile(imagePath); + } else { + res.status(404).send("Image not found"); + } +}); + + +export default router; \ No newline at end of file diff --git a/backend/src/routes/graphql.routes.ts b/backend/src/routes/graphql.routes.ts new file mode 100644 index 00000000..f912671c --- /dev/null +++ b/backend/src/routes/graphql.routes.ts @@ -0,0 +1,122 @@ +import { Express } from "express"; +import { ApolloServer } from "@apollo/server"; +import { expressMiddleware } from "@apollo/server/express4"; +import { buildSchema } from "type-graphql"; +import cors from "cors"; +import Cookies from "cookies"; +import { jwtVerify } from "jose"; +import { PrismaClient } from "@prisma/client"; +import express from "express"; + +import { customAuthChecker } from "../lib/authChecker"; +import { checkApiKey } from "../lib/checkApiKey"; +import { User, UserRole } from "../entities/user.entity"; + +import type { Request, Response } from "express"; +import { ContactResolver } from "../resolvers/contact.resolver"; +import { CaptchaResolver } from "../resolvers/captcha.resolver"; +import { SkillResolver } from "../resolvers/skill.resolver"; +import { ProjectResolver } from "../resolvers/project.resolver"; +import { ExperienceResolver } from "../resolvers/experience.resolver"; +import { EducationResolver } from "../resolvers/education.resolver"; +import { UserResolver } from "../resolvers/user.resolver"; +import { AdminResolver } from "../resolvers/admin.resolver"; + +/* Types context GraphQL */ +export interface JwtPayload { + id: number; +} + +export interface GraphQLContext { + req: Request; + res: Response; + apiKey?: string; + cookies: Cookies; + token?: string; + user: User | null; +} + +const prisma = new PrismaClient(); + +/* Fonction appelée depuis app.ts */ +export async function mountGraphQL(app: Express) { + /* 1. Build schema avec TypeGraphQL */ + const schema = await buildSchema({ + resolvers: [ + ContactResolver, + CaptchaResolver, + SkillResolver, + ProjectResolver, + ExperienceResolver, + EducationResolver, + UserResolver, + AdminResolver, + ], + validate: false, + authChecker: customAuthChecker, + }); + + /* 2. Crée ApolloServer v4 */ + const server = new ApolloServer<GraphQLContext>({ schema }); + await server.start(); + + /* 3. Monte le middleware sur /graphql */ + app.use( + "/graphql", + cors<cors.CorsRequest>({ + origin: process.env.CLIENT_URL?.split(",") ?? ["http://localhost:3000"], + credentials: true, + }), + express.json(), + expressMiddleware(server, { + context: async ({ req, res }): Promise<GraphQLContext> => { + /* ▸ Cookies */ + const cookies = new Cookies(req, res); + + /* ▸ Auth utilisateur via JWT */ + let user: User | null = null; + const token = cookies.get("token"); + if (token && process.env.JWT_SECRET) { + try { + const { payload } = await jwtVerify<JwtPayload>( + token, + new TextEncoder().encode(process.env.JWT_SECRET) + ); + + const prismaUser = await prisma.user.findUnique({ + where: { id: payload.id }, + }); + + if (prismaUser) { + user = { + id: prismaUser.id, + email: prismaUser.email, + firstname: prismaUser.firstname, + lastname: prismaUser.lastname, + role: prismaUser.role as UserRole, + isPasswordChange: prismaUser.isPasswordChange, + }; + } + } catch (err) { + console.error("JWT invalide :", err); + cookies.set("token", "", { + expires: new Date(0), + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax" as const, + }); + } + } + + /* ▸ Vérification de la clé API */ + const apiKeyHeader = req.headers["x-api-key"]; + const apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader; + if (!apiKey) throw new Error("Unauthorized: x-api-key header is missing."); + await checkApiKey(apiKey); + + /* Contexte retourné à chaque resolver */ + return { req, res, cookies, token, user, apiKey }; + }, + }) + ); +} \ No newline at end of file diff --git a/backend/src/routes/upload.routes.ts b/backend/src/routes/upload.routes.ts new file mode 100644 index 00000000..936fa15c --- /dev/null +++ b/backend/src/routes/upload.routes.ts @@ -0,0 +1,23 @@ +import { Router } from "express"; +import path from "path"; + +const router = Router(); + +router.get("/:type/:filename", (req, res) => { + const { type, filename } = req.params; + + if (!['image', 'video'].includes(type)) { + return res.status(400).send('Type invalide (image ou video attendu)'); + } + + const filePath = path.join(__dirname, "..", "uploads", `${type}s`, filename); + + res.sendFile(filePath, err => { + if (err && !res.headersSent) { + console.error("Fichier non trouvé:", filePath); + res.status(404).send("Fichier non trouvé"); + } + }); +}); + +export default router; \ No newline at end of file From 1e58ef800572627496dc8453c6d55a9b68aab8a4 Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Thu, 26 Jun 2025 21:41:34 +0200 Subject: [PATCH 11/14] created list backup frontend and composent table, and action view download, fixed bug css tailwind css captcha and buttom orginal --- .../AdminLayout/Pages/BackUp/BackUpList.tsx | 101 +++++++++++++++++- .../AdminLayout/Pages/Dashboard/Dashboard.tsx | 2 +- .../AdminLayout/components/Table/Table.tsx | 67 ++++++++++++ frontend/src/components/Button/Button.tsx | 5 +- frontend/src/components/Captcha/Captcha.tsx | 29 ++++- frontend/src/components/Projects/Projects.tsx | 4 +- .../components/Commands/ProjectsCommand.tsx | 4 +- frontend/src/lang/en.tsx | 7 ++ frontend/src/lang/fr.tsx | 7 ++ frontend/src/lang/typeLang.tsx | 7 ++ .../src/requetes/queries/admin.queries.ts | 15 +++ frontend/src/styles/output.css | 2 +- frontend/src/types/graphql.ts | 51 +++++++++ 13 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/AdminLayout/components/Table/Table.tsx diff --git a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx index 97b99b4d..86474aa3 100644 --- a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx +++ b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx @@ -1,7 +1,100 @@ -const BackUpList = (): React.ReactElement => { - return ( - <p className="text-primary">Page BackUpList</p> - ) +import React from "react"; +import { useGetBackupsListQuery } from "@/types/graphql"; +import LoadingCustom from "@/components/Loading/LoadingCustom"; +import TextAdmin from "../../components/Text/TextAdmin"; +import { useLang } from "@/context/Lang/LangContext"; +import Table, { ColumnDef } from "../../components/Table/Table"; +import { Download, Eye } from "lucide-react"; + +const formatBytes = (bytes: number): string => { + if (!bytes) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +}; + +const formatDate = (iso: string): string => + new Date(iso).toLocaleString("fr-FR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + +interface BackupFileInfo { + fileName: string; + sizeBytes: number; + createdAt: string; + modifiedAt: string; } +const BackUpList = (): React.ReactElement => { + const { data, loading, error } = useGetBackupsListQuery(); + const { translations } = useLang(); + + if (loading) return <LoadingCustom />; + if (error || !data?.listBackupFiles?.files) + return ( + <p className="p-4 text-primary"> + {"Erreur lors du chargement des fichiers de sauvegarde."} + </p> + ); + + const backups: BackupFileInfo[] = data.listBackupFiles.files; + + const columns: ColumnDef<BackupFileInfo>[] = [ + { + header: translations.messagePageBackUpListFileName, + accessor: "fileName", + className: "font-mono text-xs", + headerClassName: "rounded-tl-2xl", + }, + { + header: translations.messagePageBackUpListSize, + accessor: (row) => formatBytes(row.sizeBytes), + }, + { + header: translations.messagePageBackUpListDateCreated, + accessor: (row) => formatDate(row.createdAt), + }, + { + header: translations.messagePageBackUpListDateModified, + accessor: (row) => formatDate(row.modifiedAt), + }, + { + header: translations.messagePageBackUpListAction, + accessor: (row) => ( + <> + <a + href={`${process.env.NEXT_PUBLIC_API_URL}/api/backups/${row.fileName}`} + target="_blank" + rel="noopener noreferrer" + className="inline-flex items-center gap-2 rounded-lg bg-primary/90 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary hover:text-secondary transition-colors" + > + <Eye className="h-4 w-4" /> + </a> + <a + href={`${process.env.NEXT_PUBLIC_API_URL}/api/backups/${row.fileName}/download`} + target="_blank" + rel="noopener noreferrer" + className="inline-flex items-center gap-2 rounded-lg bg-primary/90 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary hover:text-secondary transition-colors" + > + <Download className="h-4 w-4" /> + </a> + </> + ), + headerClassName: "rounded-tr-2xl", + }, + ]; + + return ( + <div className="space-y-10"> + <TextAdmin type="h1">{translations.messagePageBackUpListTitle}</TextAdmin> + <Table columns={columns} data={backups} /> + </div> + ); +}; + export default BackUpList; \ No newline at end of file diff --git a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx index 38203ef5..db710f7a 100644 --- a/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx +++ b/frontend/src/components/AdminLayout/Pages/Dashboard/Dashboard.tsx @@ -19,7 +19,7 @@ const Dashboard = (): React.ReactElement => { const { data , loading, error } = useGetGlobalStatsQuery(); const { translations } = useLang(); if (loading) return <LoadingCustom />; - if (error || !data?.getGlobalStats) return <p className="p-4 text-primary">Erreur lors du chargement des statistiques.</p>; + if (error || !data?.getGlobalStats) return <p className="p-4 text-primary">{translations.messagePageDashBoardErreurData}</p>; const stats = data.getGlobalStats.stats; const averageSkills = data.getAverageSkillsPerProject; diff --git a/frontend/src/components/AdminLayout/components/Table/Table.tsx b/frontend/src/components/AdminLayout/components/Table/Table.tsx new file mode 100644 index 00000000..f1fe2975 --- /dev/null +++ b/frontend/src/components/AdminLayout/components/Table/Table.tsx @@ -0,0 +1,67 @@ +import React from "react"; + +export interface ColumnDef<T> { + header: string; + accessor: keyof T | ((row: T) => React.ReactNode); + className?: string; + headerClassName?: string; +} + +interface TableProps<T> { + columns: ColumnDef<T>[]; + data: T[]; +} + +const Table = <T extends Record<string, any>>({ + columns, + data, +}: TableProps<T>) => { + return ( + <div className="overflow-x-auto rounded-xl border border-border bg-muted shadow-sm"> + <table className="min-w-full text-sm text-left text-text"> + <thead className="bg-background sticky top-0 z-10"> + <tr> + {columns.map((col, index) => ( + <th + key={index} + className={`px-6 py-4 font-bold uppercase text-xs tracking-wide border-b border-border ${ + col.headerClassName || "" + }`} + > + {col.header} + </th> + ))} + </tr> + </thead> + <tbody> + {data.map((row, rowIndex) => ( + <tr + key={rowIndex} + className={`transition-colors ${ + rowIndex % 2 === 0 ? "bg-muted/40" : "bg-muted/70" + } hover:bg-accent/30`} + > + {columns.map((col, colIndex) => { + const value = + typeof col.accessor === "function" + ? col.accessor(row) + : row[col.accessor]; + + return ( + <td + key={colIndex} + className={`px-6 py-4 ${col.className || ""}`} + > + {value} + </td> + ); + })} + </tr> + ))} + </tbody> + </table> + </div> + ); +}; + +export default Table; \ No newline at end of file diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx index bd33589d..ddd9faa5 100644 --- a/frontend/src/components/Button/Button.tsx +++ b/frontend/src/components/Button/Button.tsx @@ -26,14 +26,15 @@ const ButtonCustom: React.FC<Props> = ({ mt: 2, ml: 1, border: "none", - bgcolor: disable ? "black" : "var(--primary-color)", + bgcolor: disable ? "red" : "var(--primary-color)", "&:hover": disableHover ? {} : { bgcolor: "var(--secondary-color)", }, - pointerEvents: disable ? "none" : "auto", + // pointerEvents: disable ? "none" : "auto", outline: "none", + // color: disable ? "white" : "", }} > {text} diff --git a/frontend/src/components/Captcha/Captcha.tsx b/frontend/src/components/Captcha/Captcha.tsx index 10386a4f..776a9236 100644 --- a/frontend/src/components/Captcha/Captcha.tsx +++ b/frontend/src/components/Captcha/Captcha.tsx @@ -260,9 +260,18 @@ const CaptchaModal: React.FC<Props> = ({ height: 24, color: "green", backgroundColor: "white", + position: "absolute", + top: 8, + right: 8, + boxShadow: 2, + zIndex: 2, + p: 0, + "&:hover": { + backgroundColor: "white", + }, }} > - <CheckCircleIcon /> + <CheckCircleIcon sx={{ width: 24, height: 24 }} /> </IconButton> )} </div> @@ -271,6 +280,22 @@ const CaptchaModal: React.FC<Props> = ({ <div className="flex justify-center m-2"> <ButtonCustom onClick={handleSubmit} text="vérification" /> <IconButton + onClick={regenerateCaptcha} + disabled={refreshing} + sx={{ + color: 'var(--primary-color)', + cursor: refreshing ? 'not-allowed' : 'pointer', + m: 2, + opacity: refreshing ? 0.5 : 1, + '&:hover': { + color: 'var(--secondary-color)', + }, + pointerEvents: refreshing ? 'none' : 'auto', + }} + > + <RefreshIcon /> + </IconButton> + {/* <IconButton onClick={regenerateCaptcha} className={`text-text cursor-pointer m-2 hover:text-secondary ${ refreshing ? "opacity-50 cursor-not-allowed" : "" @@ -278,7 +303,7 @@ const CaptchaModal: React.FC<Props> = ({ sx={{ pointerEvents: refreshing ? "none" : "auto" }} > <RefreshIcon /> - </IconButton> + </IconButton> */} </div> </> )} diff --git a/frontend/src/components/Projects/Projects.tsx b/frontend/src/components/Projects/Projects.tsx index 8c6e21b5..8332ea26 100644 --- a/frontend/src/components/Projects/Projects.tsx +++ b/frontend/src/components/Projects/Projects.tsx @@ -63,7 +63,7 @@ const Projects: React.FC<ProjectComponent> = ({ <div className="video-container"> {isClient && ( <ReactPlayer - url={`${process.env.NEXT_PUBLIC_API_URL}/upload/${project.typeDisplay}/${project.contentDisplay}`} + url={`${process.env.NEXT_PUBLIC_API_URL}/api/upload/${project.typeDisplay}/${project.contentDisplay}`} width="310px" height="170px" controls @@ -119,7 +119,7 @@ const Projects: React.FC<ProjectComponent> = ({ ) : ( <div className="w-320px h-170 overflow-hidden bg-body text-text"> <img - src={`${process.env.NEXT_PUBLIC_API_URL}/upload/${project.typeDisplay}/${project.contentDisplay}`} + src={`${process.env.NEXT_PUBLIC_API_URL}/api/upload/${project.typeDisplay}/${project.contentDisplay}`} alt={project?.title} className="max-w-[310px] pb-2 overflow-hidden" /> diff --git a/frontend/src/components/Terminal/components/Commands/ProjectsCommand.tsx b/frontend/src/components/Terminal/components/Commands/ProjectsCommand.tsx index 5ee17937..5469eacd 100644 --- a/frontend/src/components/Terminal/components/Commands/ProjectsCommand.tsx +++ b/frontend/src/components/Terminal/components/Commands/ProjectsCommand.tsx @@ -122,7 +122,7 @@ const ProjectsCommand: React.FC = () => { <div className="video-container"> {isClient && ( <ReactPlayer - url={`${process.env.NEXT_PUBLIC_API_URL}/upload/${project.typeDisplay}/${project.contentDisplay}`} + url={`${process.env.NEXT_PUBLIC_API_URL}/api/upload/${project.typeDisplay}/${project.contentDisplay}`} width="310px" height="170px" controls @@ -131,7 +131,7 @@ const ProjectsCommand: React.FC = () => { </div> ) : ( <img - src={`${process.env.NEXT_PUBLIC_API_URL}/upload/${project.typeDisplay}/${project.contentDisplay}`} + src={`${process.env.NEXT_PUBLIC_API_URL}/api/upload/${project.typeDisplay}/${project.contentDisplay}`} alt="Card Image" className="w-[350px] h-[170px] pb-2 overflow-hidden" /> diff --git a/frontend/src/lang/en.tsx b/frontend/src/lang/en.tsx index 7eade6fb..15530876 100644 --- a/frontend/src/lang/en.tsx +++ b/frontend/src/lang/en.tsx @@ -142,6 +142,7 @@ const en: Lang = { "sideBarAdmin-cv/view": "View CV", "sideBarAdmin-cv/update": "Edit CV", messagePageDashBoardTitle : "Dashboard", + messagePageDashBoardErreurData : "Error loading statistics data.", messagePageDashBoardCardStatsProject : "Projects", messagePageDashBoardCardStatsSkill : "Skills", messagePageDashBoardCardStatsEducation : "Educations", @@ -156,6 +157,12 @@ const en: Lang = { messagePageDashBoardRoleAdmin : "Administrators", messagePageDashBoardRoleEditor : "Editors", messagePageDashBoardRoleViewer : "Readers", + messagePageBackUpListTitle: "Backup List", + messagePageBackUpListFileName: "File Name", + messagePageBackUpListSize: "Size", + messagePageBackUpListDateCreated: "Created on", + messagePageBackUpListDateModified: "Modified on", + messagePageBackUpListAction: "Action", }; export default en; \ No newline at end of file diff --git a/frontend/src/lang/fr.tsx b/frontend/src/lang/fr.tsx index 0a59d379..69cff25a 100644 --- a/frontend/src/lang/fr.tsx +++ b/frontend/src/lang/fr.tsx @@ -142,6 +142,7 @@ const fr: Lang = { "sideBarAdmin-cv/view": "Voir le CV", "sideBarAdmin-cv/update": "Modifier le CV", messagePageDashBoardTitle : "Tableau de bord", + messagePageDashBoardErreurData : "Erreur lors du chargement des données statistiques.", messagePageDashBoardCardStatsProject : "Projets", messagePageDashBoardCardStatsSkill : "Compétences", messagePageDashBoardCardStatsEducation : "Formations", @@ -156,6 +157,12 @@ const fr: Lang = { messagePageDashBoardRoleAdmin : "Administrateurs", messagePageDashBoardRoleEditor : "Éditeurs", messagePageDashBoardRoleViewer : "Lecteurs", + messagePageBackUpListTitle : "Liste des sauvegardes", + messagePageBackUpListFileName : "Nom du fichier", + messagePageBackUpListSize : "Taille", + messagePageBackUpListDateCreated : "Créé le", + messagePageBackUpListDateModified : "Modifié le", + messagePageBackUpListAction : "Action", }; export default fr; \ No newline at end of file diff --git a/frontend/src/lang/typeLang.tsx b/frontend/src/lang/typeLang.tsx index e1654194..011ee573 100644 --- a/frontend/src/lang/typeLang.tsx +++ b/frontend/src/lang/typeLang.tsx @@ -125,6 +125,7 @@ type Lang = { "sideBarAdmin-cv/view": string; "sideBarAdmin-cv/update": string; messagePageDashBoardTitle : string; + messagePageDashBoardErreurData : string; messagePageDashBoardCardStatsProject : string; messagePageDashBoardCardStatsSkill : string; messagePageDashBoardCardStatsEducation : string; @@ -139,6 +140,12 @@ type Lang = { messagePageDashBoardRoleAdmin : string; messagePageDashBoardRoleEditor : string; messagePageDashBoardRoleViewer : string; + messagePageBackUpListTitle : string; + messagePageBackUpListFileName : string; + messagePageBackUpListSize : string; + messagePageBackUpListDateCreated : string; + messagePageBackUpListDateModified : string; + messagePageBackUpListAction : string; }; export default Lang; \ No newline at end of file diff --git a/frontend/src/requetes/queries/admin.queries.ts b/frontend/src/requetes/queries/admin.queries.ts index a6ce8e54..dd529bff 100644 --- a/frontend/src/requetes/queries/admin.queries.ts +++ b/frontend/src/requetes/queries/admin.queries.ts @@ -35,3 +35,18 @@ export const GET_GLOBAL_STATS = gql` } } `; + +export const GET_BACKUPS_LIST = gql` + query GetBackupsList { + listBackupFiles { + files { + sizeBytes + modifiedAt + fileName + createdAt + } + message + code + } + } +`; \ No newline at end of file diff --git a/frontend/src/styles/output.css b/frontend/src/styles/output.css index 9f97393c..2227d66a 100644 --- a/frontend/src/styles/output.css +++ b/frontend/src/styles/output.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-tl-2xl{border-top-left-radius:1rem}.rounded-tr-2xl{border-top-right-radius:1rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/frontend/src/types/graphql.ts b/frontend/src/types/graphql.ts index bd036313..2c7349ee 100644 --- a/frontend/src/types/graphql.ts +++ b/frontend/src/types/graphql.ts @@ -620,6 +620,11 @@ export type GetGlobalStatsQueryVariables = Exact<{ [key: string]: never; }>; export type GetGlobalStatsQuery = { __typename?: 'Query', getAverageSkillsPerProject: number, getGlobalStats: { __typename?: 'GlobalStatsResponse', code: number, message: string, stats?: { __typename?: 'GlobalStats', totalUsers: number, totalProjects: number, totalSkills: number, totalEducations: number, totalExperiences: number, usersByRoleAdmin: number, usersByRoleEditor: number, usersByRoleView: number } | null }, getUsersRoleDistribution: { __typename?: 'UserRolePercent', admin: number, editor: number, view: number, message: string, code: number }, getTopUsedSkills: { __typename?: 'TopSkillsResponse', code: number, message: string, skills: Array<{ __typename?: 'TopSkillUsage', id: number, name: string, usageCount: number }> } }; +export type GetBackupsListQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetBackupsListQuery = { __typename?: 'Query', listBackupFiles: { __typename?: 'BackupFilesResponse', message: string, code: number, files?: Array<{ __typename?: 'BackupFileInfo', sizeBytes: number, modifiedAt: any, fileName: string, createdAt: any }> | null } }; + export type GenerateCaptchaQueryVariables = Exact<{ [key: string]: never; }>; @@ -858,6 +863,52 @@ export type GetGlobalStatsQueryHookResult = ReturnType<typeof useGetGlobalStatsQ export type GetGlobalStatsLazyQueryHookResult = ReturnType<typeof useGetGlobalStatsLazyQuery>; export type GetGlobalStatsSuspenseQueryHookResult = ReturnType<typeof useGetGlobalStatsSuspenseQuery>; export type GetGlobalStatsQueryResult = Apollo.QueryResult<GetGlobalStatsQuery, GetGlobalStatsQueryVariables>; +export const GetBackupsListDocument = gql` + query GetBackupsList { + listBackupFiles { + files { + sizeBytes + modifiedAt + fileName + createdAt + } + message + code + } +} + `; + +/** + * __useGetBackupsListQuery__ + * + * To run a query within a React component, call `useGetBackupsListQuery` and pass it any options that fit your needs. + * When your component renders, `useGetBackupsListQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetBackupsListQuery({ + * variables: { + * }, + * }); + */ +export function useGetBackupsListQuery(baseOptions?: Apollo.QueryHookOptions<GetBackupsListQuery, GetBackupsListQueryVariables>) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery<GetBackupsListQuery, GetBackupsListQueryVariables>(GetBackupsListDocument, options); + } +export function useGetBackupsListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBackupsListQuery, GetBackupsListQueryVariables>) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery<GetBackupsListQuery, GetBackupsListQueryVariables>(GetBackupsListDocument, options); + } +export function useGetBackupsListSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<GetBackupsListQuery, GetBackupsListQueryVariables>) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery<GetBackupsListQuery, GetBackupsListQueryVariables>(GetBackupsListDocument, options); + } +export type GetBackupsListQueryHookResult = ReturnType<typeof useGetBackupsListQuery>; +export type GetBackupsListLazyQueryHookResult = ReturnType<typeof useGetBackupsListLazyQuery>; +export type GetBackupsListSuspenseQueryHookResult = ReturnType<typeof useGetBackupsListSuspenseQuery>; +export type GetBackupsListQueryResult = Apollo.QueryResult<GetBackupsListQuery, GetBackupsListQueryVariables>; export const GenerateCaptchaDocument = gql` query generateCaptcha { generateCaptcha { From 6292cec0ef809d2ca6b54afd9c67b2946b3437c8 Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Fri, 27 Jun 2025 18:21:14 +0200 Subject: [PATCH 12/14] add created new backup bdd --- .../AdminLayout/Pages/BackUp/BackUpList.tsx | 54 +++++++++++++++++-- .../ConfirmDialog/ConfirmDialog.tsx | 47 ++++++++++++++++ frontend/src/components/Button/Button.tsx | 2 +- frontend/src/lang/en.tsx | 9 ++++ frontend/src/lang/fr.tsx | 9 ++++ frontend/src/lang/typeLang.tsx | 9 ++++ .../src/requetes/mutations/admin.mutations.ts | 11 ++++ frontend/src/styles/output.css | 2 +- frontend/src/types/graphql.ts | 39 ++++++++++++++ 9 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/AdminLayout/components/ConfirmDialog/ConfirmDialog.tsx create mode 100644 frontend/src/requetes/mutations/admin.mutations.ts diff --git a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx index 86474aa3..2c08601c 100644 --- a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx +++ b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx @@ -1,10 +1,16 @@ -import React from "react"; -import { useGetBackupsListQuery } from "@/types/graphql"; +import React, { useState } from "react"; +import { + useGetBackupsListQuery, + useGenerateDatabaseBackupMutation, +} from "@/types/graphql"; import LoadingCustom from "@/components/Loading/LoadingCustom"; import TextAdmin from "../../components/Text/TextAdmin"; import { useLang } from "@/context/Lang/LangContext"; import Table, { ColumnDef } from "../../components/Table/Table"; import { Download, Eye } from "lucide-react"; +import CustomToast from "@/components/ToastCustom/CustomToast"; +import ConfirmDialog from "../../components/ConfirmDialog/ConfirmDialog"; +import ButtonCustom from "@/components/Button/Button"; const formatBytes = (bytes: number): string => { if (!bytes) return "0 B"; @@ -31,14 +37,39 @@ interface BackupFileInfo { } const BackUpList = (): React.ReactElement => { - const { data, loading, error } = useGetBackupsListQuery(); + + const { data, loading, error, refetch } = useGetBackupsListQuery(); + const [generateBackup] = useGenerateDatabaseBackupMutation(); const { translations } = useLang(); + const [openDialog, setOpenDialog] = useState<boolean>(false); + + const handleOpenDialog: () => void = () => setOpenDialog(true); + const handleCloseDialog: () => void = () => setOpenDialog(false); + + const { showAlert } = CustomToast(); + + const handleGenerateBackup: () => Promise<void> = async () => { + try { + const { data } = await generateBackup(); + if (data?.generateDatabaseBackup.code === 200) { + showAlert("success", translations.messagePageBackUpCreatedSuccess); + await refetch(); + } else { + showAlert("error", translations.messagePageBackUpCreatedError1); + } + } catch (error) { + showAlert("error", translations.messagePageBackUpCreatedError2); + } finally { + setOpenDialog(false); + } + }; + if (loading) return <LoadingCustom />; if (error || !data?.listBackupFiles?.files) return ( <p className="p-4 text-primary"> - {"Erreur lors du chargement des fichiers de sauvegarde."} + {translations.messagePageBackUpListNotFound} </p> ); @@ -92,7 +123,22 @@ const BackUpList = (): React.ReactElement => { return ( <div className="space-y-10"> <TextAdmin type="h1">{translations.messagePageBackUpListTitle}</TextAdmin> + <ButtonCustom + text={translations.messagePageBackUpButtomCreated} + onClick={handleOpenDialog} + disable={false} + disableHover={false} + /> <Table columns={columns} data={backups} /> + <ConfirmDialog + open={openDialog} + title={translations.messagePageBackUpTitleConfirmCreated} + description={translations.messagePageBackUpDescConfirmCreated} + confirmLabel= {translations.messagePageBackUpMessageButtonValideCreated} + cancelLabel= {translations.messagePageBackUpMessageButtonCancelCreated} + onConfirm={handleGenerateBackup} + onCancel={handleCloseDialog} + /> </div> ); }; diff --git a/frontend/src/components/AdminLayout/components/ConfirmDialog/ConfirmDialog.tsx b/frontend/src/components/AdminLayout/components/ConfirmDialog/ConfirmDialog.tsx new file mode 100644 index 00000000..407dd23d --- /dev/null +++ b/frontend/src/components/AdminLayout/components/ConfirmDialog/ConfirmDialog.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import ModalCustom from "@/components/ModalCustom/ModalCustom"; +import ButtonCustom from "@/components/Button/Button"; + +interface ConfirmDialogProps { + open: boolean; + title?: string; + description: string; + confirmLabel?: string; + cancelLabel?: string; + onConfirm: () => void; + onCancel: () => void; + confirmDisabled?: boolean; + cancelDisabled?: boolean; +} + +const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ + open, + title = "Confirmation", + description, + confirmLabel = "Confirmer", + cancelLabel = "Annuler", + onConfirm, + onCancel, + confirmDisabled = false, + cancelDisabled = false, +}) => { + return ( + <ModalCustom open={open} onClose={onCancel} width={420} className="flex-col items-start"> + <h2 className="text-lg font-semibold text-text mb-4">{title}</h2> + <p className="text-sm text-text ">{description}</p> + <div className="w-full flex justify-end gap-3"> + <ButtonCustom + text={cancelLabel} + onClick={onCancel} + disable={cancelDisabled} + /> + <ButtonCustom + text={confirmLabel} + onClick={onConfirm} + /> + </div> + </ModalCustom> + ); +}; + +export default ConfirmDialog; \ No newline at end of file diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx index ddd9faa5..9f47ffb2 100644 --- a/frontend/src/components/Button/Button.tsx +++ b/frontend/src/components/Button/Button.tsx @@ -19,7 +19,7 @@ const ButtonCustom: React.FC<Props> = ({ variant="contained" disabled={disable} sx={{ - fontSize: "15px", + fontSize: "12px", px: 5, py: 1, borderRadius: "999px", diff --git a/frontend/src/lang/en.tsx b/frontend/src/lang/en.tsx index 15530876..5fb998b8 100644 --- a/frontend/src/lang/en.tsx +++ b/frontend/src/lang/en.tsx @@ -163,6 +163,15 @@ const en: Lang = { messagePageBackUpListDateCreated: "Created on", messagePageBackUpListDateModified: "Modified on", messagePageBackUpListAction: "Action", + messagePageBackUpCreatedSuccess : "Backup successfully generated!", + messagePageBackUpCreatedError1 : "An error occurred.", + messagePageBackUpCreatedError2 : "An error occurred while generating the backup.", + messagePageBackUpListNotFound : "Error loading backup files.", + messagePageBackUpButtomCreated : "New backup", + messagePageBackUpTitleConfirmCreated : "Creation confirmation", + messagePageBackUpDescConfirmCreated : "Are you sure you want to create a new database backup?", + messagePageBackUpMessageButtonValideCreated : "Yes, create", + messagePageBackUpMessageButtonCancelCreated : "Cancel", }; export default en; \ No newline at end of file diff --git a/frontend/src/lang/fr.tsx b/frontend/src/lang/fr.tsx index 69cff25a..d8cd30dc 100644 --- a/frontend/src/lang/fr.tsx +++ b/frontend/src/lang/fr.tsx @@ -163,6 +163,15 @@ const fr: Lang = { messagePageBackUpListDateCreated : "Créé le", messagePageBackUpListDateModified : "Modifié le", messagePageBackUpListAction : "Action", + messagePageBackUpCreatedSuccess : "Sauvegarde générée avec succès !", + messagePageBackUpCreatedError1 : "Une erreur est survenue.", + messagePageBackUpCreatedError2 : "Une erreur est survenue lors de la génération de la sauvegarde.", + messagePageBackUpListNotFound : "Erreur lors du chargement des fichiers de sauvegarde.", + messagePageBackUpButtomCreated : "Nouvelle sauvegarde", + messagePageBackUpTitleConfirmCreated : "Confirmation de la création", + messagePageBackUpDescConfirmCreated : "Voulez-vous vraiment créer une nouvelle sauvegarde de la base de données ?", + messagePageBackUpMessageButtonValideCreated : "Oui, créer", + messagePageBackUpMessageButtonCancelCreated : "Annuler", }; export default fr; \ No newline at end of file diff --git a/frontend/src/lang/typeLang.tsx b/frontend/src/lang/typeLang.tsx index 011ee573..802c7115 100644 --- a/frontend/src/lang/typeLang.tsx +++ b/frontend/src/lang/typeLang.tsx @@ -146,6 +146,15 @@ type Lang = { messagePageBackUpListDateCreated : string; messagePageBackUpListDateModified : string; messagePageBackUpListAction : string; + messagePageBackUpCreatedSuccess : string; + messagePageBackUpCreatedError1 : string; + messagePageBackUpCreatedError2 : string; + messagePageBackUpListNotFound : string; + messagePageBackUpButtomCreated : string; + messagePageBackUpTitleConfirmCreated : string; + messagePageBackUpDescConfirmCreated : string; + messagePageBackUpMessageButtonValideCreated : string; + messagePageBackUpMessageButtonCancelCreated : string; }; export default Lang; \ No newline at end of file diff --git a/frontend/src/requetes/mutations/admin.mutations.ts b/frontend/src/requetes/mutations/admin.mutations.ts new file mode 100644 index 00000000..47b84dfa --- /dev/null +++ b/frontend/src/requetes/mutations/admin.mutations.ts @@ -0,0 +1,11 @@ +import { gql } from "@apollo/client"; + +export const GENERATE_BACKUP = gql` + mutation generateDatabaseBackup { + generateDatabaseBackup { + code + message + path + } +} +`; \ No newline at end of file diff --git a/frontend/src/styles/output.css b/frontend/src/styles/output.css index 2227d66a..408cc075 100644 --- a/frontend/src/styles/output.css +++ b/frontend/src/styles/output.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-tl-2xl{border-top-left-radius:1rem}.rounded-tr-2xl{border-top-right-radius:1rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-tl-2xl{border-top-left-radius:1rem}.rounded-tr-2xl{border-top-right-radius:1rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pr-6{padding-right:1.5rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/frontend/src/types/graphql.ts b/frontend/src/types/graphql.ts index 2c7349ee..9aa5f4af 100644 --- a/frontend/src/types/graphql.ts +++ b/frontend/src/types/graphql.ts @@ -585,6 +585,11 @@ export type ValidationResponse = { isValid: Scalars['Boolean']['output']; }; +export type GenerateDatabaseBackupMutationVariables = Exact<{ [key: string]: never; }>; + + +export type GenerateDatabaseBackupMutation = { __typename?: 'Mutation', generateDatabaseBackup: { __typename?: 'BackupResponse', code: number, message: string, path: string } }; + export type ValidateCaptchaMutationVariables = Exact<{ challengeType: Scalars['String']['input']; selectedIndices: Array<Scalars['Float']['input']> | Scalars['Float']['input']; @@ -656,6 +661,40 @@ export type GetMeQueryVariables = Exact<{ [key: string]: never; }>; export type GetMeQuery = { __typename?: 'Query', me?: { __typename?: 'User', role: Role, lastname: string, isPasswordChange: boolean, id: string, firstname: string, email: string } | null }; +export const GenerateDatabaseBackupDocument = gql` + mutation generateDatabaseBackup { + generateDatabaseBackup { + code + message + path + } +} + `; +export type GenerateDatabaseBackupMutationFn = Apollo.MutationFunction<GenerateDatabaseBackupMutation, GenerateDatabaseBackupMutationVariables>; + +/** + * __useGenerateDatabaseBackupMutation__ + * + * To run a mutation, you first call `useGenerateDatabaseBackupMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGenerateDatabaseBackupMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [generateDatabaseBackupMutation, { data, loading, error }] = useGenerateDatabaseBackupMutation({ + * variables: { + * }, + * }); + */ +export function useGenerateDatabaseBackupMutation(baseOptions?: Apollo.MutationHookOptions<GenerateDatabaseBackupMutation, GenerateDatabaseBackupMutationVariables>) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation<GenerateDatabaseBackupMutation, GenerateDatabaseBackupMutationVariables>(GenerateDatabaseBackupDocument, options); + } +export type GenerateDatabaseBackupMutationHookResult = ReturnType<typeof useGenerateDatabaseBackupMutation>; +export type GenerateDatabaseBackupMutationResult = Apollo.MutationResult<GenerateDatabaseBackupMutation>; +export type GenerateDatabaseBackupMutationOptions = Apollo.BaseMutationOptions<GenerateDatabaseBackupMutation, GenerateDatabaseBackupMutationVariables>; export const ValidateCaptchaDocument = gql` mutation ValidateCaptcha($challengeType: String!, $selectedIndices: [Float!]!, $idCaptcha: String!) { validateCaptcha( From bbfbd92d034b1a1316d8f8e6440890ecce3ceb07 Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Fri, 27 Jun 2025 19:29:12 +0200 Subject: [PATCH 13/14] add delete backup bdd --- .../AdminLayout/Pages/BackUp/BackUpList.tsx | 51 ++++++++++++++++++- frontend/src/lang/en.tsx | 7 +++ frontend/src/lang/fr.tsx | 7 +++ frontend/src/lang/typeLang.tsx | 7 +++ .../src/requetes/mutations/admin.mutations.ts | 9 ++++ frontend/src/styles/output.css | 2 +- frontend/src/types/graphql.ts | 41 +++++++++++++++ 7 files changed, 121 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx index 2c08601c..99227c6c 100644 --- a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx +++ b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpList.tsx @@ -2,12 +2,13 @@ import React, { useState } from "react"; import { useGetBackupsListQuery, useGenerateDatabaseBackupMutation, + useDeleteBackupFileMutation, } from "@/types/graphql"; import LoadingCustom from "@/components/Loading/LoadingCustom"; import TextAdmin from "../../components/Text/TextAdmin"; import { useLang } from "@/context/Lang/LangContext"; import Table, { ColumnDef } from "../../components/Table/Table"; -import { Download, Eye } from "lucide-react"; +import { Download, Eye, Trash } from "lucide-react"; import CustomToast from "@/components/ToastCustom/CustomToast"; import ConfirmDialog from "../../components/ConfirmDialog/ConfirmDialog"; import ButtonCustom from "@/components/Button/Button"; @@ -40,9 +41,12 @@ const BackUpList = (): React.ReactElement => { const { data, loading, error, refetch } = useGetBackupsListQuery(); const [generateBackup] = useGenerateDatabaseBackupMutation(); + const [deleteBackupFile] = useDeleteBackupFileMutation(); const { translations } = useLang(); const [openDialog, setOpenDialog] = useState<boolean>(false); + const [selectedFileName, setSelectedFileName] = useState<string | null>(null); + const [openDeleteDialog, setOpenDeleteDialog] = useState<boolean>(false); const handleOpenDialog: () => void = () => setOpenDialog(true); const handleCloseDialog: () => void = () => setOpenDialog(false); @@ -65,6 +69,26 @@ const BackUpList = (): React.ReactElement => { } }; + const handleDeleteBackup = async (): Promise<void> => { + if (!selectedFileName) return; + + try { + const { data } = await deleteBackupFile({ variables: { fileName: selectedFileName } }); + + if (data?.deleteBackupFile.code === 200) { + showAlert("success", translations.messagePageBackUpDeletedSuccess); + await refetch(); + } else { + showAlert("error", data?.deleteBackupFile.message || translations.messagePageBackUpDeletedError1); + } + } catch (error) { + showAlert("error", translations.messagePageBackUpDeletedError2); + } finally { + setOpenDeleteDialog(false); + setSelectedFileName(null); + } + }; + if (loading) return <LoadingCustom />; if (error || !data?.listBackupFiles?.files) return ( @@ -73,7 +97,9 @@ const BackUpList = (): React.ReactElement => { </p> ); - const backups: BackupFileInfo[] = data.listBackupFiles.files; + const backups: BackupFileInfo[] = [...data.listBackupFiles.files].sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); const columns: ColumnDef<BackupFileInfo>[] = [ { @@ -114,6 +140,15 @@ const BackUpList = (): React.ReactElement => { > <Download className="h-4 w-4" /> </a> + <button + onClick={() => { + setSelectedFileName(row.fileName); + setOpenDeleteDialog(true); + }} + className="inline-flex items-center gap-2 rounded-lg bg-primary/90 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary hover:text-secondary transition-colors" + > + <Trash className="h-4 w-4" /> + </button> </> ), headerClassName: "rounded-tr-2xl", @@ -139,6 +174,18 @@ const BackUpList = (): React.ReactElement => { onConfirm={handleGenerateBackup} onCancel={handleCloseDialog} /> + <ConfirmDialog + open={openDeleteDialog} + title={translations.messagePageBackUpTitleConfirmDeleted} + description={translations.messagePageBackUpDescConfirmDeleted} + confirmLabel={translations.messagePageBackUpMessageButtonValideDeleted} + cancelLabel={translations.messagePageBackUpMessageButtonCancelDeleted} + onConfirm={handleDeleteBackup} + onCancel={() => { + setOpenDeleteDialog(false); + setSelectedFileName(null); + }} + /> </div> ); }; diff --git a/frontend/src/lang/en.tsx b/frontend/src/lang/en.tsx index 5fb998b8..74dfa830 100644 --- a/frontend/src/lang/en.tsx +++ b/frontend/src/lang/en.tsx @@ -172,6 +172,13 @@ const en: Lang = { messagePageBackUpDescConfirmCreated : "Are you sure you want to create a new database backup?", messagePageBackUpMessageButtonValideCreated : "Yes, create", messagePageBackUpMessageButtonCancelCreated : "Cancel", + messagePageBackUpTitleConfirmDeleted: "Confirm deletion", + messagePageBackUpDescConfirmDeleted: "Are you sure you want to delete this backup?", + messagePageBackUpMessageButtonValideDeleted: "Delete", + messagePageBackUpMessageButtonCancelDeleted: "Cancel", + messagePageBackUpDeletedSuccess: "Backup deleted successfully.", + messagePageBackUpDeletedError1: "An error occurred while deleting.", + messagePageBackUpDeletedError2: "Failed to delete backup.", }; export default en; \ No newline at end of file diff --git a/frontend/src/lang/fr.tsx b/frontend/src/lang/fr.tsx index d8cd30dc..77dcc280 100644 --- a/frontend/src/lang/fr.tsx +++ b/frontend/src/lang/fr.tsx @@ -172,6 +172,13 @@ const fr: Lang = { messagePageBackUpDescConfirmCreated : "Voulez-vous vraiment créer une nouvelle sauvegarde de la base de données ?", messagePageBackUpMessageButtonValideCreated : "Oui, créer", messagePageBackUpMessageButtonCancelCreated : "Annuler", + messagePageBackUpTitleConfirmDeleted: "Confirmer la suppression", + messagePageBackUpDescConfirmDeleted: "Voulez-vous vraiment supprimer cette sauvegarde ?", + messagePageBackUpMessageButtonValideDeleted: "Supprimer", + messagePageBackUpMessageButtonCancelDeleted: "Annuler", + messagePageBackUpDeletedSuccess: "Sauvegarde supprimée avec succès.", + messagePageBackUpDeletedError1: "Une erreur est survenue lors de la suppression.", + messagePageBackUpDeletedError2: "Échec de la suppression de la sauvegarde.", }; export default fr; \ No newline at end of file diff --git a/frontend/src/lang/typeLang.tsx b/frontend/src/lang/typeLang.tsx index 802c7115..f4373f77 100644 --- a/frontend/src/lang/typeLang.tsx +++ b/frontend/src/lang/typeLang.tsx @@ -155,6 +155,13 @@ type Lang = { messagePageBackUpDescConfirmCreated : string; messagePageBackUpMessageButtonValideCreated : string; messagePageBackUpMessageButtonCancelCreated : string; + messagePageBackUpTitleConfirmDeleted: string; + messagePageBackUpDescConfirmDeleted: string; + messagePageBackUpMessageButtonValideDeleted: string; + messagePageBackUpMessageButtonCancelDeleted: string; + messagePageBackUpDeletedSuccess: string; + messagePageBackUpDeletedError1: string; + messagePageBackUpDeletedError2: string; }; export default Lang; \ No newline at end of file diff --git a/frontend/src/requetes/mutations/admin.mutations.ts b/frontend/src/requetes/mutations/admin.mutations.ts index 47b84dfa..be9861ad 100644 --- a/frontend/src/requetes/mutations/admin.mutations.ts +++ b/frontend/src/requetes/mutations/admin.mutations.ts @@ -8,4 +8,13 @@ export const GENERATE_BACKUP = gql` path } } +`; + +export const DELETE_BACKUP = gql` + mutation DeleteBackupFile($fileName: String!) { + deleteBackupFile(fileName: $fileName) { + code + message + } +} `; \ No newline at end of file diff --git a/frontend/src/styles/output.css b/frontend/src/styles/output.css index 408cc075..b555e267 100644 --- a/frontend/src/styles/output.css +++ b/frontend/src/styles/output.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-tl-2xl{border-top-left-radius:1rem}.rounded-tr-2xl{border-top-right-radius:1rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pr-6{padding-right:1.5rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[80px\]{top:80px}.top-\[88px\]{top:88px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.m-\[3\%\]{margin:3%}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-\[0\.25rem\]{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-3\.5{margin-right:.875rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-\[-2\%\]{margin-top:-2%}.mt-\[1\%\]{margin-top:1%}.mt-\[64px\]{margin-top:64px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\/2{height:50%}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-\[100\%\]{height:100%}.h-\[170px\]{height:170px}.h-\[460px\]{height:460px}.h-\[calc\(100vh-80px\)\]{height:calc(100vh - 80px)}.h-\[calc\(100vh-xpx\)\]{height:calc(100vh - xpx)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.min-h-\[calc\(100vh-64px\)\]{min-height:calc(100vh - 64px)}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-\[100\%\]{width:100%}.w-\[100vh\]{width:100vh}.w-\[25px\]{width:25px}.w-\[350px\]{width:350px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[310px\]{max-width:310px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.rotate-180,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-tl-2xl{border-top-left-radius:1rem}.rounded-tr-2xl{border-top-right-radius:1rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-primary{border-color:var(--primary-color)}.border-secondary{border-color:var(--secondary-color)}.bg-admin{background-color:var(--admin-color)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-body{background-color:var(--body-color)}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-footer{background-color:var(--footer-color)}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary{background-color:var(--primary-color)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-40{--tw-bg-opacity:0.4}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:#3b82f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500{--tw-gradient-from:#06b6d4 var(--tw-gradient-from-position);--tw-gradient-to:#06b6d400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500{--tw-gradient-from:#ec4899 var(--tw-gradient-from-position);--tw-gradient-to:#ec489900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:#8b5cf600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:#facc1500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.to-fuchsia-400{--tw-gradient-to:#e879f9 var(--tw-gradient-to-position)}.to-orange-300{--tw-gradient-to:#fdba74 var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-teal-400{--tw-gradient-to:#2dd4bf var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pb-\[0\.25rem\]{padding-bottom:.25rem}.pl-8{padding-left:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-\[80px\]{padding-top:80px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.tracking-wide{letter-spacing:.025em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-primary{color:var(--primary-color)}.text-secondary{color:var(--secondary-color)}.text-text{color:var(--text-color)}.text-text200{color:var(--text200-color)}.text-text300{color:var(--text300-color)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-\[50\%\]{opacity:50%}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--primary-color:#b45852;--secondary-color:#dfbb5f;--scrollHandle-color:#19252e;--scrollHandleHover-color:#162028;--body-color:#01031b;--grey-color:#7f7f7f;--placeholder-color:#a0aec0;--text-color:#fff;--text100-color:#030303;--text200-color:#b2bdcc;--text300-color:#64748b;--textButton-color:#fff;--success-color:#1c8036;--error-color:#aa2020;--warn-color:#ebcc2a;--info-color:#3b89ff;--footer-color:#050f1a;--admin-color:#162739}body{background-color:var(--body-color)}.custom-scrollbar{overflow-y:hidden}.custom-scrollbar:hover{overflow-y:auto}.custom-scrollbar::-webkit-scrollbar{width:0;height:0}.custom-scrollbar:hover::-webkit-scrollbar{width:2px;height:2px}@keyframes expandOpen{0%{opacity:0;max-height:0}to{opacity:1;max-height:500px}}@keyframes expandClose{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}.expanded-animation-open{animation:expandOpen 1s ease-in-out forwards}.expanded-animation-close{animation:expandClose 1s ease-in-out forwards}.MuiCardContent-root{padding:0}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--body-color)}::-webkit-scrollbar-thumb{background:var(--scrollHandle-color)}::-webkit-scrollbar-thumb:hover{background:var(--scrollHandleHover-color)}.emulator{align-items:center;display:flex;justify-content:center;width:100vh;overflow:auto}.inputTerminal{background-color:var(--body-color);color:var(--text-color);caret-color:var(--primary-color);margin-left:5px;width:15vh}.inputTerminal:focus-visible{outline:none}.video-container{display:flex;justify-content:center;align-items:center;overflow:hidden}.custom-video{width:310px;height:170px;-o-object-fit:cover;object-fit:cover}.Toastify__toast-container{font-family:Arial,sans-serif}.Toastify__toast{border-radius:4px}.Toastify__toast--success .Toastify__icon{color:var(--success-color)}.Toastify__toast--error .Toastify__icon{color:var(--error-color)}.Toastify__toast--info .Toastify__icon{color:var(--info-color)}.Toastify__toast--warn .Toastify__icon{color:var(--warn-color)}.Toastify__toast--error .Toastify__close-button,.Toastify__toast--info .Toastify__close-button,.Toastify__toast--success .Toastify__close-button,.Toastify__toast--warn .Toastify__close-button{color:var(--text-button-color)}html{scroll-behavior:smooth}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-primary:hover{background-color:var(--primary-color)}.hover\:bg-secondary:hover{background-color:var(--secondary-color)}.hover\:text-secondary:hover{color:var(--secondary-color)}.hover\:text-text100:hover{color:var(--text100-color)}.hover\:opacity-\[75\%\]:hover{opacity:75%}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}@media (min-width:640px){.sm\:ml-3{margin-left:.75rem}.sm\:max-w-\[80\%\]{max-width:80%}.sm\:max-w-\[90\%\]{max-width:90%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.md\:left-auto{left:auto}.md\:top-0{top:0}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:ml-4{margin-left:1rem}.md\:ml-80{margin-left:20rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-\[70\%\]{width:70%}.md\:max-w-\[60\%\]{max-width:60%}.md\:max-w-\[75\%\]{max-width:75%}.md\:-translate-y-1\/2{--tw-translate-y:-50%}.md\:-translate-y-1\/2,.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:border-l-0{border-left-width:0}.md\:border-t{border-top-width:1px}.md\:pl-0{padding-left:0}.md\:pt-8{padding-top:2rem}}@media (min-width:1024px){.lg\:ml-6{margin-left:1.5rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:hidden{display:none}.lg\:w-\[70\%\]{width:70%}.lg\:max-w-\[50\%\]{max-width:50%}.lg\:max-w-\[60\%\]{max-width:60%}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}.xl\:max-w-\[35\%\]{max-width:35%}.xl\:max-w-\[40\%\]{max-width:40%}.xl\:max-w-\[50\%\]{max-width:50%}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/frontend/src/types/graphql.ts b/frontend/src/types/graphql.ts index 9aa5f4af..60c6dcc3 100644 --- a/frontend/src/types/graphql.ts +++ b/frontend/src/types/graphql.ts @@ -590,6 +590,13 @@ export type GenerateDatabaseBackupMutationVariables = Exact<{ [key: string]: nev export type GenerateDatabaseBackupMutation = { __typename?: 'Mutation', generateDatabaseBackup: { __typename?: 'BackupResponse', code: number, message: string, path: string } }; +export type DeleteBackupFileMutationVariables = Exact<{ + fileName: Scalars['String']['input']; +}>; + + +export type DeleteBackupFileMutation = { __typename?: 'Mutation', deleteBackupFile: { __typename?: 'Response', code: number, message: string } }; + export type ValidateCaptchaMutationVariables = Exact<{ challengeType: Scalars['String']['input']; selectedIndices: Array<Scalars['Float']['input']> | Scalars['Float']['input']; @@ -695,6 +702,40 @@ export function useGenerateDatabaseBackupMutation(baseOptions?: Apollo.MutationH export type GenerateDatabaseBackupMutationHookResult = ReturnType<typeof useGenerateDatabaseBackupMutation>; export type GenerateDatabaseBackupMutationResult = Apollo.MutationResult<GenerateDatabaseBackupMutation>; export type GenerateDatabaseBackupMutationOptions = Apollo.BaseMutationOptions<GenerateDatabaseBackupMutation, GenerateDatabaseBackupMutationVariables>; +export const DeleteBackupFileDocument = gql` + mutation DeleteBackupFile($fileName: String!) { + deleteBackupFile(fileName: $fileName) { + code + message + } +} + `; +export type DeleteBackupFileMutationFn = Apollo.MutationFunction<DeleteBackupFileMutation, DeleteBackupFileMutationVariables>; + +/** + * __useDeleteBackupFileMutation__ + * + * To run a mutation, you first call `useDeleteBackupFileMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteBackupFileMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteBackupFileMutation, { data, loading, error }] = useDeleteBackupFileMutation({ + * variables: { + * fileName: // value for 'fileName' + * }, + * }); + */ +export function useDeleteBackupFileMutation(baseOptions?: Apollo.MutationHookOptions<DeleteBackupFileMutation, DeleteBackupFileMutationVariables>) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation<DeleteBackupFileMutation, DeleteBackupFileMutationVariables>(DeleteBackupFileDocument, options); + } +export type DeleteBackupFileMutationHookResult = ReturnType<typeof useDeleteBackupFileMutation>; +export type DeleteBackupFileMutationResult = Apollo.MutationResult<DeleteBackupFileMutation>; +export type DeleteBackupFileMutationOptions = Apollo.BaseMutationOptions<DeleteBackupFileMutation, DeleteBackupFileMutationVariables>; export const ValidateCaptchaDocument = gql` mutation ValidateCaptcha($challengeType: String!, $selectedIndices: [Float!]!, $idCaptcha: String!) { validateCaptcha( From 1b5d915f8d17e17b959e5a33cdaf6b6133f81c34 Mon Sep 17 00:00:00 2001 From: Alexandre78R <alexandre.renard98@gmail.com> Date: Fri, 27 Jun 2025 19:36:41 +0200 Subject: [PATCH 14/14] fixed sidebar backup --- .../src/components/AdminLayout/Navigation.tsx | 16 +++++++++++----- .../AdminLayout/Pages/BackUp/BackUpCreate.tsx | 7 ------- frontend/src/lang/en.tsx | 1 - frontend/src/lang/fr.tsx | 1 - frontend/src/lang/typeLang.tsx | 1 - frontend/src/pages/admin/[...slug].tsx | 1 - 6 files changed, 11 insertions(+), 16 deletions(-) delete mode 100644 frontend/src/components/AdminLayout/Pages/BackUp/BackUpCreate.tsx diff --git a/frontend/src/components/AdminLayout/Navigation.tsx b/frontend/src/components/AdminLayout/Navigation.tsx index 1f268fba..71eeed0b 100644 --- a/frontend/src/components/AdminLayout/Navigation.tsx +++ b/frontend/src/components/AdminLayout/Navigation.tsx @@ -89,14 +89,20 @@ const navigation: NavItem[] = [ }, { name: 'Sauvegarde', - key: 'backup', + key: 'backup/list', icon: DatabaseBackup, roles: ['admin', 'editor', 'view'], - children: [ - { name: 'Voir les backup', key: 'backup/list', icon: Eye, roles: ['admin', 'editor', 'view'], parentKey: 'backup' }, - { name: 'Nouvelle backup', key: 'backup/create', icon: FolderPlus, roles: ['admin'], parentKey: 'backup' }, - ], }, + // { + // name: 'Sauvegarde', + // key: 'backup', + // icon: DatabaseBackup, + // roles: ['admin', 'editor', 'view'], + // children: [ + // { name: 'Voir les backup', key: 'backup/list', icon: Eye, roles: ['admin', 'editor', 'view'], parentKey: 'backup' }, + // { name: 'Nouvelle backup', key: 'backup/create', icon: FolderPlus, roles: ['admin'], parentKey: 'backup' }, + // ], + // }, { name: 'CV', key: 'cv', diff --git a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpCreate.tsx b/frontend/src/components/AdminLayout/Pages/BackUp/BackUpCreate.tsx deleted file mode 100644 index 98a528d3..00000000 --- a/frontend/src/components/AdminLayout/Pages/BackUp/BackUpCreate.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const BackUpCreate = (): React.ReactElement => { - return ( - <p className="text-primary">Page BackUpCreate</p> - ) -} - -export default BackUpCreate; \ No newline at end of file diff --git a/frontend/src/lang/en.tsx b/frontend/src/lang/en.tsx index 74dfa830..258d1a7e 100644 --- a/frontend/src/lang/en.tsx +++ b/frontend/src/lang/en.tsx @@ -137,7 +137,6 @@ const en: Lang = { "sideBarAdmin-settings": "Settings", "sideBarAdmin-backup": "Backup", "sideBarAdmin-backup/list": "View backups", - "sideBarAdmin-backup/create": "New backup", "sideBarAdmin-cv": "Resume", "sideBarAdmin-cv/view": "View CV", "sideBarAdmin-cv/update": "Edit CV", diff --git a/frontend/src/lang/fr.tsx b/frontend/src/lang/fr.tsx index 77dcc280..6380ab63 100644 --- a/frontend/src/lang/fr.tsx +++ b/frontend/src/lang/fr.tsx @@ -137,7 +137,6 @@ const fr: Lang = { "sideBarAdmin-settings": "Paramètres", "sideBarAdmin-backup": "Sauvegarde", "sideBarAdmin-backup/list": "Voir les sauvegardes", - "sideBarAdmin-backup/create": "Nouvelle sauvegarde", "sideBarAdmin-cv": "CV", "sideBarAdmin-cv/view": "Voir le CV", "sideBarAdmin-cv/update": "Modifier le CV", diff --git a/frontend/src/lang/typeLang.tsx b/frontend/src/lang/typeLang.tsx index f4373f77..a98d2de8 100644 --- a/frontend/src/lang/typeLang.tsx +++ b/frontend/src/lang/typeLang.tsx @@ -120,7 +120,6 @@ type Lang = { "sideBarAdmin-settings": string; "sideBarAdmin-backup": string; "sideBarAdmin-backup/list": string; - "sideBarAdmin-backup/create": string; "sideBarAdmin-cv": string; "sideBarAdmin-cv/view": string; "sideBarAdmin-cv/update": string; diff --git a/frontend/src/pages/admin/[...slug].tsx b/frontend/src/pages/admin/[...slug].tsx index bad3c380..5723718b 100644 --- a/frontend/src/pages/admin/[...slug].tsx +++ b/frontend/src/pages/admin/[...slug].tsx @@ -21,7 +21,6 @@ const pagesMap: Record<string, () => Promise<any>> = { 'theme-colors/list': () => import('@/components/AdminLayout/Pages/Themes/ThemesList'), 'theme-colors/create': () => import('@/components/AdminLayout/Pages/Themes/ThemeCreate'), 'backup/list': () => import('@/components/AdminLayout/Pages/BackUp/BackUpList'), - 'backup/create': () => import('@/components/AdminLayout/Pages/BackUp/BackUpCreate'), 'cv/view': () => import('@/components/AdminLayout/Pages/CV/CVView'), 'cv/update': () => import('@/components/AdminLayout/Pages/CV/CVUpdate'), }