diff --git a/frontend/package.json b/frontend/package.json index 934329f..eabf39d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^7.5.0", "react-select": "^5.10.1", + "rollup-plugin-cleanup": "^3.2.1", "shadcn": "^2.10.0", "sweetalert2": "^11.23.0", "swiper": "^12.1.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b8c5751..9b1f503 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: react-select: specifier: ^5.10.1 version: 5.10.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + rollup-plugin-cleanup: + specifier: ^3.2.1 + version: 3.2.1(rollup@4.62.2) shadcn: specifier: ^2.10.0 version: 2.10.0(@types/node@22.20.0)(typescript@5.7.3) @@ -1973,6 +1976,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2444,6 +2450,10 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-cleanup@1.2.0: + resolution: {integrity: sha512-JeDD0yiiSt80fXzAVa/crrS0JDPQljyBG/RpOtaSbyDq03VHa9szJWMaWOYU/bcTn412uMN2MxApXq8v79cUiQ==} + engines: {node: ^10.14.2 || >=12.0.0} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2616,6 +2626,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2854,6 +2867,10 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + perf-regexes@1.0.1: + resolution: {integrity: sha512-L7MXxUDtqr4PUaLFCDCXBfGV/6KLIuSEccizDI7JxT+c9x1G1v04BQ4+4oag84SHaCdrBgQAIs/Cqn+flwFPng==} + engines: {node: '>=6.14'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3061,6 +3078,15 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup-plugin-cleanup@3.2.1: + resolution: {integrity: sha512-zuv8EhoO3TpnrU8MX8W7YxSbO4gmOR0ny06Lm3nkFfq0IVKdBUtHwhVzY1OAJyNCIAdLiyPnOrU0KnO0Fri1GQ==} + engines: {node: ^10.14.2 || >=12.0.0} + peerDependencies: + rollup: '>=2.0' + + rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3170,6 +3196,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + skip-regex@1.0.2: + resolution: {integrity: sha512-pEjMUbwJ5Pl/6Vn6FsamXHXItJXSRftcibixDmNCWbWhic0hzHrwkMZo0IZ7fMRH9KxcWDFSkzhccB4285PutA==} + engines: {node: '>=4.2'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3182,6 +3212,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -5487,6 +5521,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@0.6.1: {} + esutils@2.0.3: {} etag@1.8.1: {} @@ -5967,6 +6003,12 @@ snapshots: jose@6.2.3: {} + js-cleanup@1.2.0: + dependencies: + magic-string: 0.25.9 + perf-regexes: 1.0.1 + skip-regex: 1.0.2 + js-tokens@4.0.0: {} js-yaml@4.2.0: @@ -6101,6 +6143,10 @@ snapshots: dependencies: react: 19.2.7 + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6340,6 +6386,8 @@ snapshots: path-type@4.0.0: {} + perf-regexes@1.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6568,6 +6616,16 @@ snapshots: reusify@1.1.0: {} + rollup-plugin-cleanup@3.2.1(rollup@4.62.2): + dependencies: + js-cleanup: 1.2.0 + rollup: 4.62.2 + rollup-pluginutils: 2.8.2 + + rollup-pluginutils@2.8.2: + dependencies: + estree-walker: 0.6.1 + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 @@ -6768,12 +6826,16 @@ snapshots: sisteransi@1.0.5: {} + skip-regex@1.0.2: {} + source-map-js@1.2.1: {} source-map@0.5.7: {} source-map@0.6.1: {} + sourcemap-codec@1.4.8: {} + statuses@2.0.2: {} stdin-discarder@0.1.0: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9e0a619..0de1a2b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,46 +1,49 @@ -import React from 'react'; +import React, { lazy } from 'react'; import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import AdminRoute from './components/utils/adminroute'; import PrivateRoute from './components/utils/privateroute'; import ProtectedRoute from './components/utils/protectedroute'; -import { - AdminBanned, - AdminPageBus, - AdminPageChall, - AdminPageEmail, - AdminPageEvents, - AdminPageExport, - AdminPageFaction, - AdminPageGames, - AdminPageNews, - AdminPagePerm, - AdminPageRole, - AdminPageShotgun, - AdminPageTeam, - AdminPageTent, - AdminPageUser, -} from './pages/admin'; -import LoginPage from './pages/auth'; -import ChallPage from './pages/challenge'; -import DiscordPage from './pages/discord'; -import FoodPage from './pages/food'; -import GamesPage from './pages/games'; -import HomePage from './pages/home'; -import LegalsPage from './pages/legals'; -import NewsPage from './pages/news'; -import NotFoundPage from './pages/notFound'; -import ParrainagePage from './pages/parrainage'; -import { AvailablePermanencesPage, MyPermanencesPage, RespoCallPage } from './pages/perm'; -import PlanningsPage from './pages/plannings'; -import PrivacyPage from './pages/privacy'; -import ProfilPage from './pages/profil'; -import RegisterPage from './pages/register'; -import ResetPasswordPage from './pages/resetPassword'; -import Roadbook from './pages/roadbook'; -import SdiPage from './pages/sdi'; -import ShotgunPage from './pages/shotgun'; -import WeiPage from './pages/wei'; +import { OnboardingProvider } from './contexts/onboarding'; +import { UserProvider } from './contexts/user'; + +const AdminPageBanned = lazy(() => import('./pages/admin/adminBanned')); +const AdminPageBus = lazy(() => import('./pages/admin/adminBus')); +const AdminPageChallenges = lazy(() => import('./pages/admin/adminChallenges')); +const AdminPageEmail = lazy(() => import('./pages/admin/adminEmail')); +const AdminPageEvents = lazy(() => import('./pages/admin/adminEvents')); +const AdminPageExport = lazy(() => import('./pages/admin/adminExport')); +const AdminPageFaction = lazy(() => import('./pages/admin/adminFaction')); +const AdminPageGames = lazy(() => import('./pages/admin/adminGames')); +const AdminPageNews = lazy(() => import('./pages/admin/adminNews')); +const AdminPagePerm = lazy(() => import('./pages/admin/adminPerm')); +const AdminPageRole = lazy(() => import('./pages/admin/adminRole')); +const AdminPageShotgun = lazy(() => import('./pages/admin/adminShotgun')); +const AdminPageTeam = lazy(() => import('./pages/admin/adminTeam')); +const AdminPageTent = lazy(() => import('./pages/admin/adminTent')); +const AdminPageUser = lazy(() => import('./pages/admin/adminUser')); +const ChallPage = lazy(() => import('./pages/challenge')); +const DiscordPage = lazy(() => import('./pages/discord')); +const FoodPage = lazy(() => import('./pages/food')); +const GamesPage = lazy(() => import('./pages/games')); +const HomePage = lazy(() => import('./pages/home')); +const LegalsPage = lazy(() => import('./pages/legals')); +const LoginPage = lazy(() => import('./pages/auth')); +const NewsPage = lazy(() => import('./pages/news')); +const NotFoundPage = lazy(() => import('./pages/notFound')); +const ParrainagePage = lazy(() => import('./pages/parrainage')); +const PermanencesPageAvailable = lazy(() => import('./pages/perm/permAvailable')); +const PermanencesPageMy = lazy(() => import('./pages/perm/permMy')); +const PermanencesPageRespoCall = lazy(() => import('./pages/perm/permRespoCall')); +const PlanningsPage = lazy(() => import('./pages/plannings')); +const PrivacyPage = lazy(() => import('./pages/privacy')); +const ProfilPage = lazy(() => import('./pages/profil')); +const RegisterPage = lazy(() => import('./pages/register')); +const ResetPasswordPage = lazy(() => import('./pages/resetPassword')); +const RoadbookPage = lazy(() => import('./pages/roadbook')); +const SdiPage = lazy(() => import('./pages/sdi')); +const ShotgunPage = lazy(() => import('./pages/shotgun')); +const WeiPage = lazy(() => import('./pages/wei')); const App: React.FC = () => { const VITE_ANALYTICS_WEBSITE_ID = import.meta.env.VITE_ANALYTICS_WEBSITE_ID; @@ -60,269 +63,274 @@ const App: React.FC = () => { return ( - - {/* Public */} - } /> - } /> - } /> - } /> - } /> - } /> + + + + {/* Public */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Utilisateurs connectés */} - {/* Utilisateurs connectés */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* Étudiant et Admin */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + {/* Étudiant et Admin */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* ResposCE et Admin */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + {/* ResposCE et Admin */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* ResposCE et Admin */} - - - - } - /> + {/* ResposCE et Admin */} + + + + } + /> - {/* Arbitre et Admin*/} - - - - } - /> - {/* Admin uniquement */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + {/* Arbitre et Admin*/} + + + + } + /> + {/* Admin uniquement */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* Fallback */} - } /> - + {/* Fallback */} + } /> + + + ); }; diff --git a/frontend/src/components/WEI_SDI_Food/sdiSection.tsx b/frontend/src/components/WEI_SDI_Food/sdiSection.tsx index 6b7f3e3..7711c83 100644 --- a/frontend/src/components/WEI_SDI_Food/sdiSection.tsx +++ b/frontend/src/components/WEI_SDI_Food/sdiSection.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; +import { useOnboarding } from '../../contexts/onboarding'; import { decodeToken, getToken } from '../../services/requests/auth.service'; import { checkSDIStatus } from '../../services/requests/event.service'; -import { getCurrentUserOnboardingStatus } from '../../services/requests/user.service'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; export const SdiSection = () => { @@ -16,6 +16,8 @@ export const SdiSection = () => { : { userPermission: undefined, userRoles: [] }; const roles = [userPermission, ...userRoles.map((r) => r.roleName)].filter(Boolean) as string[]; + const { status: onboardingStatus, loading: onboardingLoading } = useOnboarding(); + useEffect(() => { const script = document.createElement('script'); script.src = 'https://www.billetweb.fr/js/export.js'; @@ -23,21 +25,16 @@ export const SdiSection = () => { document.body.appendChild(script); fetchStatus(); - if (roles.includes('Nouveau')) { - fetchOnboardingStatus(); - } + // onboarding handled by context }, [roles]); - const fetchOnboardingStatus = async () => { - try { - const onboardingStatus = await getCurrentUserOnboardingStatus(); - setHasContactInformation(onboardingStatus.hasemergencyContactInformation); - setHasVssForm(onboardingStatus.vss_form == 'validated'); - setNeedsVssForm(onboardingStatus.needsVssForm); - } catch (error) { - console.error("Erreur lors de la récupération du statut d'onboarding :", error); - } - }; + useEffect(() => { + if (!roles.includes('Nouveau')) return; + if (onboardingLoading) return; + setHasContactInformation(onboardingStatus?.hasemergencyContactInformation ?? true); + setHasVssForm(onboardingStatus ? onboardingStatus.vss_form == 'validated' : true); + setNeedsVssForm(onboardingStatus?.needsVssForm ?? false); + }, [roles, onboardingLoading, onboardingStatus]); const fetchStatus = async () => { try { diff --git a/frontend/src/components/WEI_SDI_Food/weiSection.tsx b/frontend/src/components/WEI_SDI_Food/weiSection.tsx index 05145e0..0ad9bbb 100644 --- a/frontend/src/components/WEI_SDI_Food/weiSection.tsx +++ b/frontend/src/components/WEI_SDI_Food/weiSection.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; +import { useOnboarding } from '../../contexts/onboarding'; import { decodeToken, getToken } from '../../services/requests/auth.service'; import { checkWEIStatus } from '../../services/requests/event.service'; -import { getCurrentUserOnboardingStatus } from '../../services/requests/user.service'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; export const WeiSection = () => { @@ -16,6 +16,8 @@ export const WeiSection = () => { : { userPermission: undefined, userRoles: [] }; const roles = [userPermission, ...userRoles.map((r) => r.roleName)].filter(Boolean) as string[]; + const { status: onboardingStatus, loading: onboardingLoading } = useOnboarding(); + useEffect(() => { const script = document.createElement('script'); script.src = 'https://www.billetweb.fr/js/export.js'; @@ -23,21 +25,15 @@ export const WeiSection = () => { document.body.appendChild(script); fetchStatus(); - if (roles.includes('Nouveau')) { - fetchOnboardingStatus(); - } }, [roles]); - const fetchOnboardingStatus = async () => { - try { - const onboardingStatus = await getCurrentUserOnboardingStatus(); - setHasContactInformation(onboardingStatus.hasemergencyContactInformation); - setHasVssForm(onboardingStatus.vss_form == 'validated'); - setNeedsVssForm(onboardingStatus.needsVssForm); - } catch (error) { - console.error("Erreur lors de la récupération du statut d'onboarding :", error); - } - }; + useEffect(() => { + if (!roles.includes('Nouveau')) return; + if (onboardingLoading) return; + setHasContactInformation(onboardingStatus?.hasemergencyContactInformation ?? true); + setHasVssForm(onboardingStatus ? onboardingStatus.vss_form == 'validated' : true); + setNeedsVssForm(onboardingStatus?.needsVssForm ?? false); + }, [roles, onboardingLoading, onboardingStatus]); const fetchStatus = async () => { try { diff --git a/frontend/src/components/home/emergencyModal.tsx b/frontend/src/components/home/emergencyModal.tsx index 5413d05..fbfc161 100644 --- a/frontend/src/components/home/emergencyModal.tsx +++ b/frontend/src/components/home/emergencyModal.tsx @@ -1,8 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { useOnboarding } from '../../contexts/onboarding'; import { decodeToken, getToken } from '../../services/requests/auth.service'; -import { createUserContactInformation, getCurrentUserOnboardingStatus } from '../../services/requests/user.service'; +import { createUserContactInformation } from '../../services/requests/user.service'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import Modal from '../ui/modal'; @@ -72,6 +73,8 @@ function EmergencyModal() { const isLogin = searchParams.get('login') === 'true'; const isNew = roles.includes('Nouveau'); + const { status: onboardingStatus, loading: onboardingLoading } = useOnboarding(); + useEffect(() => { if (!isLogin) { setFlowStep('idle'); @@ -80,48 +83,27 @@ function EmergencyModal() { setLanguage('fr'); return; } - - let cancelled = false; - - const loadOnboardingStatus = async () => { + if (!isNew) return; + if (onboardingLoading) { setFlowStep('loading'); - setError(null); - - try { - const status = await getCurrentUserOnboardingStatus(); - - if (cancelled) { - return; - } - - if (!status.hasemergencyContactInformation) { - setFlowStep('emergency'); - return; - } - - if (status.needsVssForm) { - setFlowStep('vss'); - return; - } - - setFlowStep('idle'); - setSearchParams({}); - } catch { - if (!cancelled) { - setFlowStep('emergency'); - setError(copy[language].statusError); - } - } - }; - - if (isNew) { - loadOnboardingStatus(); + return; } - - return () => { - cancelled = true; - }; - }, [isLogin, isNew, setSearchParams]); + if (!onboardingStatus) { + setFlowStep('emergency'); + setError(copy[language].statusError); + return; + } + if (!onboardingStatus.hasemergencyContactInformation) { + setFlowStep('emergency'); + return; + } + if (onboardingStatus.needsVssForm) { + setFlowStep('vss'); + return; + } + setFlowStep('idle'); + setSearchParams({}); + }, [isLogin, isNew, onboardingLoading, onboardingStatus, language, setSearchParams]); const closeFlow = () => { setSearchParams({}); diff --git a/frontend/src/components/home/teamSection.tsx b/frontend/src/components/home/teamSection.tsx index 031e9db..d825d35 100644 --- a/frontend/src/components/home/teamSection.tsx +++ b/frontend/src/components/home/teamSection.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; +import { useUser } from '../../contexts/user'; import { type TeamDisplayInfos } from '../../interfaces/team.interface'; import { getUserTeam } from '../../services/requests/team.service'; import { Button } from '../ui/button'; @@ -9,16 +10,38 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; export const Team = () => { const [teamInfos, setTeamInfos] = useState(null); const [loading, setLoading] = useState(true); + const { user, loading: userLoading } = useUser(); useEffect(() => { - fetchTeam(); - }, []); + const fetchTeam = async () => { + const team = await getUserTeam(); + setTeamInfos(team); + setLoading(false); + }; - const fetchTeam = async () => { - const team = await getUserTeam(); - setTeamInfos(team); - setLoading(false); - }; + if (user?.permission === 'Nouveau') { + void fetchTeam(); + } else { + // if not a new user, we don't need to load team + setLoading(false); + } + }, [user]); + + if (userLoading) { + return ( +
+ + +

Chargement de ton équipe...

+
+
+
+ ); + } + + if (user?.permission !== 'Nouveau') { + return null; + } if (loading) { return ( diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index b109bd6..9d5e8cf 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -4,8 +4,8 @@ import { AnimatePresence, motion } from 'framer-motion'; import { Fragment, useEffect, useState } from 'react'; import { NavLink, useLocation, useSearchParams } from 'react-router-dom'; +import { useOnboarding } from '../contexts/onboarding'; import { decodeToken, getToken } from '../services/requests/auth.service'; -import { getCurrentUserOnboardingStatus } from '../services/requests/user.service'; import { Button } from './ui/button'; interface NavItem { @@ -31,6 +31,7 @@ export const Navbar = () => { const roles = [userPermission, ...userRoles.map((r) => r.roleName)].filter(Boolean) as string[]; const [hasContactInformation, setHasContactInformation] = useState(false); const [needsVssForm, setNeedsVssForm] = useState(false); + const { status: onboardingStatus, loading: onboardingLoading } = useOnboarding(); const [, setSearchParams] = useSearchParams(); const handleLogout = () => { @@ -41,31 +42,16 @@ export const Navbar = () => { useEffect(() => { setMenuOpen(false); - const fetchOnboardingStatus = async () => { - try { - const onboardingStatus = await getCurrentUserOnboardingStatus(); - setHasContactInformation(onboardingStatus.hasemergencyContactInformation); - setNeedsVssForm(onboardingStatus.needsVssForm); - } catch (error) { - console.error("Erreur lors de la récupération du statut d'onboarding :", error); - } - }; + if (!isAuthenticated) return; - if (isAuthenticated) { - fetchOnboardingStatus(); + if (!onboardingLoading && onboardingStatus) { + setHasContactInformation(onboardingStatus.hasemergencyContactInformation); + setNeedsVssForm(onboardingStatus.needsVssForm); } - const handleOnboardingUpdate = () => { - if (isAuthenticated) { - fetchOnboardingStatus(); - } - }; - - window.addEventListener('user-onboarding-updated', handleOnboardingUpdate); - - return () => { - window.removeEventListener('user-onboarding-updated', handleOnboardingUpdate); - }; + // OnboardingProvider refreshes on 'user-onboarding-updated' event globally. + // No local event listeners required here. + return undefined; }, [isAuthenticated, pathname]); const navItems: NavItem[] = [ diff --git a/frontend/src/components/profil/profilForm.tsx b/frontend/src/components/profil/profilForm.tsx index 6eba1b0..1209b90 100644 --- a/frontend/src/components/profil/profilForm.tsx +++ b/frontend/src/components/profil/profilForm.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'; // import { FaDiscord } from 'react-icons/fa'; import Select from 'react-select'; -import { type User } from '../../interfaces/user.interface'; -import { getCurrentUser, updateCurrentUser } from '../../services/requests/user.service'; +import { useUser } from '../../contexts/user'; +import { updateCurrentUser } from '../../services/requests/user.service'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; import { Input } from '../ui/input'; @@ -26,32 +26,31 @@ const branchOptions = [ ]; export const ProfilForm = () => { - const [user, setUser] = useState(null); + const { user, loading: userLoading, refreshUser } = useUser(); const [branch, setBranch] = useState(''); const [contact, setContact] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { - const fetchUser = async () => { - try { - const me = await getCurrentUser(); - setUser(me); - setBranch(me.branch || ''); - setContact(me.contact || ''); - } catch (err) { - console.error("Erreur lors de la récupération de l'utilisateur", err); - } - }; - fetchUser(); - }, []); + if (user) { + setBranch(user.branch || ''); + setContact(user.contact || ''); + } + }, [user]); const handleSubmit = async () => { setLoading(true); const response = await updateCurrentUser({ branch: branch, contact: contact }); - alert(response.message); + // refresh cached user data after update + try { + await refreshUser(); + } catch { + alert(response.message); + } setLoading(false); }; + if (userLoading || loading) return null; if (!user) return null; return ( diff --git a/frontend/src/components/tent/tentSection.tsx b/frontend/src/components/tent/tentSection.tsx index ba9e27f..1e1d576 100644 --- a/frontend/src/components/tent/tentSection.tsx +++ b/frontend/src/components/tent/tentSection.tsx @@ -2,12 +2,13 @@ import { useEffect, useState } from 'react'; import Select from 'react-select'; import Swal from 'sweetalert2'; +import { useOnboarding } from '../../contexts/onboarding'; import { type Tent } from '../../interfaces/tent.interface'; import { type User } from '../../interfaces/user.interface'; import { decodeToken, getToken } from '../../services/requests/auth.service'; import { checkWEIStatus } from '../../services/requests/event.service'; import { cancelTent, createTent, getUserTent } from '../../services/requests/tent.service'; -import { getCurrentUserOnboardingStatus, getUsers } from '../../services/requests/user.service'; +import { getUsers } from '../../services/requests/user.service'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; @@ -23,6 +24,8 @@ export const TentPublic = () => { : { userPermission: undefined, userRoles: [] }; const roles = [userPermission, ...userRoles.map((r) => r.roleName)].filter(Boolean) as string[]; + const { status: onboardingStatus, loading: onboardingLoading } = useOnboarding(); + useEffect(() => { const fetchUsers = async () => { try { @@ -56,19 +59,13 @@ export const TentPublic = () => { fetchUsers(); fetchTent(); fetchWEIStatus(); - if (roles.includes('Nouveau')) { - fetchOnboardingStatus(); - } }, [roles]); - const fetchOnboardingStatus = async () => { - try { - const onboardingStatus = await getCurrentUserOnboardingStatus(); - setCanBook(onboardingStatus.vss_form == 'validated'); - } catch (error) { - console.error("Erreur lors de la récupération du statut d'onboarding :", error); - } - }; + useEffect(() => { + if (!roles.includes('Nouveau')) return; + if (onboardingLoading) return; + setCanBook(onboardingStatus ? onboardingStatus.vss_form == 'validated' : false); + }, [roles, onboardingLoading, onboardingStatus]); if (!token) return null; const { userId } = decodeToken(token); diff --git a/frontend/src/components/utils/privateroute.tsx b/frontend/src/components/utils/privateroute.tsx index f09a752..a4945bb 100644 --- a/frontend/src/components/utils/privateroute.tsx +++ b/frontend/src/components/utils/privateroute.tsx @@ -10,11 +10,7 @@ interface PrivateRouteProps { children: React.ReactNode; } -const PrivateRoute: React.FC = ({ - permissionRequired, - roleRequired, - children, -}) => { +const PrivateRoute: React.FC = ({ permissionRequired, roleRequired, children }) => { const token = getToken(); if (!token) { @@ -28,20 +24,20 @@ const PrivateRoute: React.FC = ({ return ; } - const isAdmin = decoded.userPermission === "Admin"; + const isAdmin = decoded.userPermission === 'Admin'; - const hasPermission = - !permissionRequired || decoded.userPermission === permissionRequired; + const hasPermission = !permissionRequired || decoded.userPermission === permissionRequired; - const hasRole = - !roleRequired || - decoded.userRoles?.some((role) => role.roleName === roleRequired); + const hasRole = !roleRequired || decoded.userRoles?.some((role) => role.roleName === roleRequired); if (!isAdmin && !(hasPermission || hasRole)) { return ; } + // PrivateRoute only handles authz checks; user data is provided by `UserProvider` at app root. return <>{children}; }; export default PrivateRoute; + +// UserProvider and useUser moved to src/context/user.tsx diff --git a/frontend/src/contexts/onboarding.tsx b/frontend/src/contexts/onboarding.tsx new file mode 100644 index 0000000..5552f5a --- /dev/null +++ b/frontend/src/contexts/onboarding.tsx @@ -0,0 +1,76 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; + +import type { UserOnboardingStatus } from '../interfaces/user.interface'; +import { getToken } from '../services/requests/auth.service'; +import { getCurrentUserOnboardingStatus } from '../services/requests/user.service'; + +type OnboardingContextType = { + status: UserOnboardingStatus | null; + loading: boolean; + refreshOnboarding: () => Promise; +}; + +const OnboardingContext = createContext(undefined); + +let cachedOnboarding: UserOnboardingStatus | null = null; + +export const useOnboarding = (): OnboardingContextType => { + const ctx = useContext(OnboardingContext); + if (!ctx) throw new Error('useOnboarding must be used within an OnboardingProvider'); + return ctx; +}; + +export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const tokenPresent = Boolean(getToken()); + const [status, setStatus] = useState(cachedOnboarding); + const [loading, setLoading] = useState(() => cachedOnboarding === null && tokenPresent); + + const fetchStatus = async () => { + const token = getToken(); + if (!token) { + setStatus(null); + setLoading(false); + return; + } + + setLoading(true); + try { + const s = await getCurrentUserOnboardingStatus(); + setStatus(s ?? null); + cachedOnboarding = s ?? null; + } catch { + setStatus(null); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!tokenPresent && !cachedOnboarding) { + // nothing to do when not authenticated + return; + } + + if (!cachedOnboarding) void fetchStatus(); + + const handler = () => { + void fetchStatus(); + }; + + window.addEventListener('user-onboarding-updated', handler); + return () => window.removeEventListener('user-onboarding-updated', handler); + }, []); + + const refreshOnboarding = async () => { + cachedOnboarding = null; + await fetchStatus(); + }; + + return ( + + {children} + + ); +}; + +export default OnboardingProvider; diff --git a/frontend/src/contexts/user.tsx b/frontend/src/contexts/user.tsx new file mode 100644 index 0000000..d9d7453 --- /dev/null +++ b/frontend/src/contexts/user.tsx @@ -0,0 +1,66 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; + +import type { User } from '../interfaces/user.interface'; +import { getToken } from '../services/requests/auth.service'; +import { getCurrentUser } from '../services/requests/user.service'; + +type UserContextType = { + user: User | null; + loading: boolean; + refreshUser: () => Promise; +}; + +const UserContext = createContext(undefined); + +let cachedUser: User | null = null; + +export const useUser = (): UserContextType => { + const ctx = useContext(UserContext); + if (!ctx) { + throw new Error('useUser must be used within a UserProvider'); + } + return ctx; +}; + +export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const tokenPresent = Boolean(getToken()); + const [user, setUser] = useState(cachedUser); + const [loading, setLoading] = useState(() => cachedUser === null && tokenPresent); + + const fetchCurrentUser = async () => { + const token = getToken(); + if (!token) { + setUser(null); + setLoading(false); + return; + } + + setLoading(true); + try { + const current = await getCurrentUser(); + setUser(current ?? null); + cachedUser = current ?? null; + } catch { + setUser(null); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!tokenPresent && !cachedUser) return; + if (!cachedUser) { + void fetchCurrentUser(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const refreshUser = async () => { + cachedUser = null; + await fetchCurrentUser(); + }; + + return {children}; +}; + +export default UserProvider; diff --git a/frontend/src/pages/admin.tsx b/frontend/src/pages/admin.tsx deleted file mode 100644 index 433ab0f..0000000 --- a/frontend/src/pages/admin.tsx +++ /dev/null @@ -1,367 +0,0 @@ -//--------------Challenge Import--------------// -import { useEffect, useRef, useState } from 'react'; - -import { AdminBannedAddEmail, AdminBannedList } from '../components/Admin/adminBanned'; -import { AdminBusTools } from '../components/Admin/adminBus'; -import AdminChallengeList from '../components/Admin/AdminChallenge/adminChalengeList'; -import { AdminChallengeAddPointsForm } from '../components/Admin/AdminChallenge/adminChallengeAddPointsForm'; -import ChallengeEditor from '../components/Admin/AdminChallenge/adminChallengeEditor'; -import { AdminValidatedChallengesList } from '../components/Admin/AdminChallenge/adminChallengeValidatedList'; -import { AdminEmail } from '../components/Admin/adminEmail'; -import { AdminEvents } from '../components/Admin/adminEvent'; -import { - AdminExportConnect, - AdminImportFoodMenu, - AdminImportNotebooks, - AdminImportPlannings, -} from '../components/Admin/adminExportImport'; -import { AdminFactionManagement } from '../components/Admin/adminFaction'; -import { AdminRolePointsManager } from '../components/Admin/adminGames'; -import { AdminLayout } from '../components/Admin/adminLayout'; -import { AdminNews } from '../components/Admin/adminNews'; -//--------------Perm Import--------------// -import PermanenceActions from '../components/Admin/AdminPerm/adminPermAction'; -import PermanenceForm from '../components/Admin/AdminPerm/adminPermForm'; -import { ImportPermCSV } from '../components/Admin/AdminPerm/adminPermImport'; -import PermanenceList from '../components/Admin/AdminPerm/adminPermList'; -import { AdminRoleManagement, AdminRolePreferences } from '../components/Admin/adminRole'; -import { AdminShotgunRanking } from '../components/Admin/adminShotgun'; -import { AdminTeamManagement, DistributeTeam } from '../components/Admin/adminTeam'; -import { TentAdmin } from '../components/Admin/adminTent'; -import { AdminRegisterNewStudent, AdminSyncNewStudent, AdminUser } from '../components/Admin/adminUser'; -import { RevealSection } from '../components/ui/revealSection'; -import { type Banned } from '../interfaces/banned.interface'; -import { type Challenge, type ValidatedChallenge } from '../interfaces/challenge.interface'; -import { type Faction } from '../interfaces/faction.interface'; -import { type Permanence } from '../interfaces/permanence.interface'; -import { type Team } from '../interfaces/team.interface'; -import { type User } from '../interfaces/user.interface'; -import { getAllChallenges, getAllChallengesValidates } from '../services/requests/challenge.service'; -import { getAllFactionsUser } from '../services/requests/faction.service'; -import { getAllPermanences } from '../services/requests/permanence.service'; -import { getAllTeams } from '../services/requests/team.service'; -import { getUsers, getUsersAdmin } from '../services/requests/user.service'; - -export const AdminPageTeam: React.FC = () => ( - -
- - - - - - - -
-
-); - -export const AdminPageFaction: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageRole: React.FC = () => ( - -
- - - - - - - -
-
-); - -export const AdminPageEvents: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageShotgun: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageExport: React.FC = () => ( - -
- - - - - - - - - - - - - - - -
-
-); - -export const AdminPagePerm: React.FC = () => { - const [permanences, setPermanences] = useState([]); - const [users, setUsers] = useState([]); - const [editMode, setEditMode] = useState(false); - const [editPermanence, setEditPermanence] = useState(null); - - const editorRef = useRef(null); - - useEffect(() => { - void fetchPermanences(); - void fetchUsers(); - }, []); - - const fetchPermanences = async () => { - const res = await getAllPermanences(); - setPermanences(res.data as Permanence[]); - }; - - const fetchUsers = async () => { - const res = await getUsersAdmin(); - setUsers(res as User[]); - }; - - return ( - -
- {/* Formulaire (créer/éditer) */} - - { - setEditMode(false); - setEditPermanence(null); - }} - /> - - - {/* Liste des permanences */} - - { - setEditMode(true); - setEditPermanence(perm); - setTimeout(() => { - editorRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, 100); - }} - /> - - - {/* Actions globales */} - - - - - {/* Import CSV (si dispo) */} - - - -
-
- ); -}; - -export const AdminPageChall: React.FC = () => { - const [challenges, setChallenges] = useState([]); - const [validatedChallenges, setValidatedChallenges] = useState([]); - const [users, setUsers] = useState([]); - const [teams, setTeams] = useState([]); - const [factions, setFactions] = useState([]); - const [editingChallenge, setEditingChallenge] = useState(null); - const editorRef = useRef(null); - - const fetchChallengesUsersTeamsFactions = async () => { - try { - const challsRes = await getAllChallenges(); - const usersRes = await getUsers(); - const teamsRes = await getAllTeams(); - const factionsRes = await getAllFactionsUser(); - - const challsResFiltered = challsRes.filter((c: Challenge) => c.category != 'Free'); - setChallenges(challsResFiltered); - setUsers(usersRes); - setTeams(teamsRes); - setFactions(factionsRes); - } catch (err) { - console.error('Erreur chargement challenges', err); - } - }; - - const fetchValidatedChallenges = async () => { - try { - const res = await getAllChallengesValidates(); - setValidatedChallenges(res); - } catch (err) { - console.error('Erreur chargement challenges validés', err); - } - }; - - useEffect(() => { - fetchChallengesUsersTeamsFactions(); - fetchValidatedChallenges(); - }, []); - - const handleEdit = (challenge: Challenge) => { - setEditingChallenge(challenge); - editorRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; - - return ( - -
- - {/* Formulaire création / édition */} - - - - - {/* Liste des challenges */} - { - fetchChallengesUsersTeamsFactions(); - fetchValidatedChallenges(); - }} - onEdit={handleEdit} - users={users} - teams={teams} - factions={factions} - /> - - - - {/* Ajout de points */} - - - - - {/* Liste des challenges validés */} - - -
-
- ); -}; - -export const AdminPageEmail: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageUser: React.FC = () => ( - -
- - - - - - - - - - - -
-
-); - -export const AdminPageNews: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageTent: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageBus: React.FC = () => ( - -
- - - -
-
-); - -export const AdminPageGames: React.FC = () => ( - -
- - - -
-
-); - -export const AdminBanned: React.FC = () => { - const [bannedList, setBannedList] = useState([]); - - return ( - -
- - - - - - - -
-
- ); -}; diff --git a/frontend/src/pages/admin/adminBanned.tsx b/frontend/src/pages/admin/adminBanned.tsx new file mode 100644 index 0000000..7385a62 --- /dev/null +++ b/frontend/src/pages/admin/adminBanned.tsx @@ -0,0 +1,26 @@ +import { useState } from 'react'; + +import { AdminBannedAddEmail, AdminBannedList } from '../../components/Admin/adminBanned'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; +import { type Banned } from '../../interfaces/banned.interface'; + +const AdminPageBanned: React.FC = () => { + const [bannedList, setBannedList] = useState([]); + + return ( + +
+ + + + + + + +
+
+ ); +}; + +export default AdminPageBanned; diff --git a/frontend/src/pages/admin/adminBus.tsx b/frontend/src/pages/admin/adminBus.tsx new file mode 100644 index 0000000..1981724 --- /dev/null +++ b/frontend/src/pages/admin/adminBus.tsx @@ -0,0 +1,15 @@ +import { AdminBusTools } from '../../components/Admin/adminBus'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageBus: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageBus; diff --git a/frontend/src/pages/admin/adminChallenges.tsx b/frontend/src/pages/admin/adminChallenges.tsx new file mode 100644 index 0000000..53dbfc7 --- /dev/null +++ b/frontend/src/pages/admin/adminChallenges.tsx @@ -0,0 +1,107 @@ +import { useEffect, useRef, useState } from 'react'; + +import AdminChallengeList from '../../components/Admin/AdminChallenge/adminChalengeList'; +import { AdminChallengeAddPointsForm } from '../../components/Admin/AdminChallenge/adminChallengeAddPointsForm'; +import ChallengeEditor from '../../components/Admin/AdminChallenge/adminChallengeEditor'; +import { AdminValidatedChallengesList } from '../../components/Admin/AdminChallenge/adminChallengeValidatedList'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; +import { type Challenge, type ValidatedChallenge } from '../../interfaces/challenge.interface'; +import { type Faction } from '../../interfaces/faction.interface'; +import { type Team } from '../../interfaces/team.interface'; +import { type User } from '../../interfaces/user.interface'; +import { getAllChallenges, getAllChallengesValidates } from '../../services/requests/challenge.service'; +import { getAllFactionsUser } from '../../services/requests/faction.service'; +import { getAllTeams } from '../../services/requests/team.service'; +import { getUsers } from '../../services/requests/user.service'; + +const AdminPageChallenges: React.FC = () => { + const [challenges, setChallenges] = useState([]); + const [validatedChallenges, setValidatedChallenges] = useState([]); + const [users, setUsers] = useState([]); + const [teams, setTeams] = useState([]); + const [factions, setFactions] = useState([]); + const [editingChallenge, setEditingChallenge] = useState(null); + const editorRef = useRef(null); + + const fetchChallengesUsersTeamsFactions = async () => { + try { + const challsRes = await getAllChallenges(); + const usersRes = await getUsers(); + const teamsRes = await getAllTeams(); + const factionsRes = await getAllFactionsUser(); + + const challsResFiltered = challsRes.filter((c: Challenge) => c.category != 'Free'); + setChallenges(challsResFiltered); + setUsers(usersRes); + setTeams(teamsRes); + setFactions(factionsRes); + } catch (err) { + console.error('Erreur chargement challenges', err); + } + }; + + const fetchValidatedChallenges = async () => { + try { + const res = await getAllChallengesValidates(); + setValidatedChallenges(res); + } catch (err) { + console.error('Erreur chargement challenges validés', err); + } + }; + + useEffect(() => { + fetchChallengesUsersTeamsFactions(); + fetchValidatedChallenges(); + }, []); + + const handleEdit = (challenge: Challenge) => { + setEditingChallenge(challenge); + editorRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + return ( + +
+ + {/* Formulaire création / édition */} + + + + + {/* Liste des challenges */} + { + fetchChallengesUsersTeamsFactions(); + fetchValidatedChallenges(); + }} + onEdit={handleEdit} + users={users} + teams={teams} + factions={factions} + /> + + + + {/* Ajout de points */} + + + + + {/* Liste des challenges validés */} + + +
+
+ ); +}; + +export default AdminPageChallenges; diff --git a/frontend/src/pages/admin/adminEmail.tsx b/frontend/src/pages/admin/adminEmail.tsx new file mode 100644 index 0000000..e30db64 --- /dev/null +++ b/frontend/src/pages/admin/adminEmail.tsx @@ -0,0 +1,15 @@ +import { AdminEmail } from '../../components/Admin/adminEmail'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageEmail: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageEmail; diff --git a/frontend/src/pages/admin/adminEvents.tsx b/frontend/src/pages/admin/adminEvents.tsx new file mode 100644 index 0000000..27b200f --- /dev/null +++ b/frontend/src/pages/admin/adminEvents.tsx @@ -0,0 +1,15 @@ +import { AdminEvents } from '../../components/Admin/adminEvent'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageEvents: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageEvents; diff --git a/frontend/src/pages/admin/adminExport.tsx b/frontend/src/pages/admin/adminExport.tsx new file mode 100644 index 0000000..a44d62c --- /dev/null +++ b/frontend/src/pages/admin/adminExport.tsx @@ -0,0 +1,32 @@ +import { + AdminExportConnect, + AdminImportFoodMenu, + AdminImportNotebooks, + AdminImportPlannings, +} from '../../components/Admin/adminExportImport'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageExport: React.FC = () => ( + +
+ + + + + + + + + + + + + + + +
+
+); + +export default AdminPageExport; diff --git a/frontend/src/pages/admin/adminFaction.tsx b/frontend/src/pages/admin/adminFaction.tsx new file mode 100644 index 0000000..4c707b6 --- /dev/null +++ b/frontend/src/pages/admin/adminFaction.tsx @@ -0,0 +1,15 @@ +import { AdminFactionManagement } from '../../components/Admin/adminFaction'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageFaction: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageFaction; diff --git a/frontend/src/pages/admin/adminGames.tsx b/frontend/src/pages/admin/adminGames.tsx new file mode 100644 index 0000000..bb5a67e --- /dev/null +++ b/frontend/src/pages/admin/adminGames.tsx @@ -0,0 +1,15 @@ +import { AdminRolePointsManager } from '../../components/Admin/adminGames'; +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageGames: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageGames; diff --git a/frontend/src/pages/admin/adminNews.tsx b/frontend/src/pages/admin/adminNews.tsx new file mode 100644 index 0000000..2d38311 --- /dev/null +++ b/frontend/src/pages/admin/adminNews.tsx @@ -0,0 +1,15 @@ +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { AdminNews } from '../../components/Admin/adminNews'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageNews: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageNews; diff --git a/frontend/src/pages/admin/adminPerm.tsx b/frontend/src/pages/admin/adminPerm.tsx new file mode 100644 index 0000000..d36c929 --- /dev/null +++ b/frontend/src/pages/admin/adminPerm.tsx @@ -0,0 +1,83 @@ +import { useEffect, useRef, useState } from 'react'; + +import { AdminLayout } from '../../components/Admin/adminLayout'; +import PermanenceActions from '../../components/Admin/AdminPerm/adminPermAction'; +import PermanenceForm from '../../components/Admin/AdminPerm/adminPermForm'; +import { ImportPermCSV } from '../../components/Admin/AdminPerm/adminPermImport'; +import PermanenceList from '../../components/Admin/AdminPerm/adminPermList'; +import { RevealSection } from '../../components/ui/revealSection'; +import { type Permanence } from '../../interfaces/permanence.interface'; +import { type User } from '../../interfaces/user.interface'; +import { getAllPermanences } from '../../services/requests/permanence.service'; +import { getUsersAdmin } from '../../services/requests/user.service'; + +const AdminPagePerm: React.FC = () => { + const [permanences, setPermanences] = useState([]); + const [users, setUsers] = useState([]); + const [editMode, setEditMode] = useState(false); + const [editPermanence, setEditPermanence] = useState(null); + + const editorRef = useRef(null); + + useEffect(() => { + void fetchPermanences(); + void fetchUsers(); + }, []); + + const fetchPermanences = async () => { + const res = await getAllPermanences(); + setPermanences(res.data as Permanence[]); + }; + + const fetchUsers = async () => { + const res = await getUsersAdmin(); + setUsers(res as User[]); + }; + + return ( + +
+ {/* Formulaire (créer/éditer) */} + + { + setEditMode(false); + setEditPermanence(null); + }} + /> + + + {/* Liste des permanences */} + + { + setEditMode(true); + setEditPermanence(perm); + setTimeout(() => { + editorRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 100); + }} + /> + + + {/* Actions globales */} + + + + + {/* Import CSV (si dispo) */} + + + +
+
+ ); +}; + +export default AdminPagePerm; diff --git a/frontend/src/pages/admin/adminRole.tsx b/frontend/src/pages/admin/adminRole.tsx new file mode 100644 index 0000000..d5ee50b --- /dev/null +++ b/frontend/src/pages/admin/adminRole.tsx @@ -0,0 +1,19 @@ +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { AdminRoleManagement, AdminRolePreferences } from '../../components/Admin/adminRole'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageRole: React.FC = () => ( + +
+ + + + + + + +
+
+); + +export default AdminPageRole; diff --git a/frontend/src/pages/admin/adminShotgun.tsx b/frontend/src/pages/admin/adminShotgun.tsx new file mode 100644 index 0000000..b7ada63 --- /dev/null +++ b/frontend/src/pages/admin/adminShotgun.tsx @@ -0,0 +1,14 @@ +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { AdminShotgunRanking } from '../../components/Admin/adminShotgun'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageShotgun: React.FC = () => ( + +
+ + + +
+
+); +export default AdminPageShotgun; diff --git a/frontend/src/pages/admin/adminTeam.tsx b/frontend/src/pages/admin/adminTeam.tsx new file mode 100644 index 0000000..cf3b6de --- /dev/null +++ b/frontend/src/pages/admin/adminTeam.tsx @@ -0,0 +1,19 @@ +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { AdminTeamManagement, DistributeTeam } from '../../components/Admin/adminTeam'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageTeam: React.FC = () => ( + +
+ + + + + + + +
+
+); + +export default AdminPageTeam; diff --git a/frontend/src/pages/admin/adminTent.tsx b/frontend/src/pages/admin/adminTent.tsx new file mode 100644 index 0000000..090461b --- /dev/null +++ b/frontend/src/pages/admin/adminTent.tsx @@ -0,0 +1,15 @@ +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { TentAdmin } from '../../components/Admin/adminTent'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageTent: React.FC = () => ( + +
+ + + +
+
+); + +export default AdminPageTent; diff --git a/frontend/src/pages/admin/adminUser.tsx b/frontend/src/pages/admin/adminUser.tsx new file mode 100644 index 0000000..3057e21 --- /dev/null +++ b/frontend/src/pages/admin/adminUser.tsx @@ -0,0 +1,23 @@ +import { AdminLayout } from '../../components/Admin/adminLayout'; +import { AdminRegisterNewStudent, AdminSyncNewStudent, AdminUser } from '../../components/Admin/adminUser'; +import { RevealSection } from '../../components/ui/revealSection'; + +const AdminPageUser: React.FC = () => ( + +
+ + + + + + + + + + + +
+
+); + +export default AdminPageUser; diff --git a/frontend/src/pages/perm.tsx b/frontend/src/pages/perm.tsx deleted file mode 100644 index c93f633..0000000 --- a/frontend/src/pages/perm.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Navigate } from 'react-router-dom'; -import Swal from 'sweetalert2'; - -import { Footer } from '../components/footer'; -import EmergencyModal from '../components/home/emergencyModal'; -import { Navbar } from '../components/navbar'; -import { RespoPresenceManagement } from '../components/permanence/appealPerm'; -import { AvailablePermanencesList } from '../components/permanence/permList'; -import { MyPermanencesList } from '../components/permanence/permUser'; -import { type Permanence } from '../interfaces/permanence.interface'; -import { type DecodedToken } from '../interfaces/token.interfaces'; -import { decodeToken, getToken } from '../services/requests/auth.service'; -import { - applyToPermanence, - cancelPermanence, - getMyPermanences, - getOpenPermanences, - isUserRespo, -} from '../services/requests/permanence.service'; - -export const AvailablePermanencesPage: React.FC = () => { - const [permanences, setPermanences] = useState([]); - const [myPermanences, setMyPermanences] = useState([]); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { - try { - const [openPerms, myPerms] = await Promise.all([getOpenPermanences(), getMyPermanences()]); - setPermanences(openPerms); - setMyPermanences(myPerms); - } catch (err) { - console.error('Erreur lors du chargement des permanences', err); - Swal.fire('Erreur', 'Impossible de charger les permanences.', 'error'); - } - }; - - const handleApplyToPermanence = async (permId: number) => { - if (isSubmitting) return; - setIsSubmitting(true); - - try { - const response = await applyToPermanence(permId); - await Swal.fire('Succès ✅', response.message, 'success'); - fetchData(); - } catch (err) { - console.error("Erreur lors de l'inscription", err); - Swal.fire('Erreur', "Impossible de s'inscrire à la permanence.", 'error'); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
- - -
-
- -
-
-
- ); -}; - -export const MyPermanencesPage: React.FC = () => { - const [myPermanences, setMyPermanences] = useState([]); - - useEffect(() => { - fetchMyPermanences(); - }, []); - - const fetchMyPermanences = async () => { - try { - const perms = await getMyPermanences(); - setMyPermanences(perms); - } catch (err) { - console.error('Erreur lors du chargement', err); - Swal.fire('Erreur', 'Impossible de charger vos permanences.', 'error'); - } - }; - - const handleCancelPermanence = async (permId: number) => { - const selectedPermanence = myPermanences.find((perm) => perm.id === permId); - if (!selectedPermanence) { - Swal.fire('Erreur', 'Permanence non trouvée.', 'error'); - return; - } - - const now = new Date(); - const deadline = new Date(selectedPermanence.start_at); - deadline.setDate(deadline.getDate() - 1); - - if (now > deadline) { - Swal.fire('Info', "La désinscription n'est plus possible.", 'info'); - return; - } - - const result = await Swal.fire({ - title: 'Êtes-vous sûr ?', - text: 'Vous ne pourrez plus revenir en arrière.', - icon: 'warning', - showCancelButton: true, - confirmButtonText: 'Oui, me désinscrire', - cancelButtonText: 'Annuler', - confirmButtonColor: '#d33', - cancelButtonColor: '#3085d6', - }); - - if (!result.isConfirmed) return; - - try { - const response = await cancelPermanence(permId); - Swal.fire('Succès ✅', response.message, 'success'); - fetchMyPermanences(); - } catch (err) { - console.error("Erreur lors de l'annulation", err); - Swal.fire('Erreur', 'Impossible de vous désinscrire.', 'error'); - } - }; - - return ( -
- - -
-
- -
-
-
- ); -}; - -export const RespoCallPage = () => { - const [isRespo, setIsRespo] = useState(null); - - useEffect(() => { - const checkRespoStatus = async () => { - try { - const token = getToken(); - - if (!token) { - return ; - } - - let decoded: DecodedToken; - try { - decoded = decodeToken(token); - } catch { - return ; - } - - const result = await isUserRespo(Number(decoded.userId)); - setIsRespo(result.data === true); - } catch (error) { - console.error('Erreur lors de la vérification du rôle respo', error); - setIsRespo(false); - } - }; - - checkRespoStatus(); - }, []); - - return ( -
- - -
-
- {isRespo === null ? ( -

Chargement...

- ) : isRespo === false ? ( -
- ❌ Accès refusé : vous n'êtes pas responsable d'une permanence. -
- ) : ( - - )} -
-
-
-
- ); -}; diff --git a/frontend/src/pages/perm/permAvailable.tsx b/frontend/src/pages/perm/permAvailable.tsx new file mode 100644 index 0000000..9e3d3bd --- /dev/null +++ b/frontend/src/pages/perm/permAvailable.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import Swal from 'sweetalert2'; + +import EmergencyModal from '../../components/home/emergencyModal'; +import { Navbar } from '../../components/navbar'; +import { AvailablePermanencesList } from '../../components/permanence/permList'; +import { type Permanence } from '../../interfaces/permanence.interface'; +import { applyToPermanence, getMyPermanences, getOpenPermanences } from '../../services/requests/permanence.service'; + +const PermanencesPageAvailable: React.FC = () => { + const [permanences, setPermanences] = useState([]); + const [myPermanences, setMyPermanences] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const [openPerms, myPerms] = await Promise.all([getOpenPermanences(), getMyPermanences()]); + setPermanences(openPerms); + setMyPermanences(myPerms); + } catch (err) { + console.error('Erreur lors du chargement des permanences', err); + Swal.fire('Erreur', 'Impossible de charger les permanences.', 'error'); + } + }; + + const handleApplyToPermanence = async (permId: number) => { + if (isSubmitting) return; + setIsSubmitting(true); + + try { + const response = await applyToPermanence(permId); + await Swal.fire('Succès ✅', response.message, 'success'); + fetchData(); + } catch (err) { + console.error("Erreur lors de l'inscription", err); + Swal.fire('Erreur', "Impossible de s'inscrire à la permanence.", 'error'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ + +
+
+ +
+
+
+ ); +}; + +export default PermanencesPageAvailable; diff --git a/frontend/src/pages/perm/permMy.tsx b/frontend/src/pages/perm/permMy.tsx new file mode 100644 index 0000000..4665ce3 --- /dev/null +++ b/frontend/src/pages/perm/permMy.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react'; +import Swal from 'sweetalert2'; + +import EmergencyModal from '../../components/home/emergencyModal'; +import { Navbar } from '../../components/navbar'; +import { MyPermanencesList } from '../../components/permanence/permUser'; +import { type Permanence } from '../../interfaces/permanence.interface'; +import { cancelPermanence, getMyPermanences } from '../../services/requests/permanence.service'; + +const PermanencesPageMy: React.FC = () => { + const [myPermanences, setMyPermanences] = useState([]); + + useEffect(() => { + fetchMyPermanences(); + }, []); + + const fetchMyPermanences = async () => { + try { + const perms = await getMyPermanences(); + setMyPermanences(perms); + } catch (err) { + console.error('Erreur lors du chargement', err); + Swal.fire('Erreur', 'Impossible de charger vos permanences.', 'error'); + } + }; + + const handleCancelPermanence = async (permId: number) => { + const selectedPermanence = myPermanences.find((perm) => perm.id === permId); + if (!selectedPermanence) { + Swal.fire('Erreur', 'Permanence non trouvée.', 'error'); + return; + } + + const now = new Date(); + const deadline = new Date(selectedPermanence.start_at); + deadline.setDate(deadline.getDate() - 1); + + if (now > deadline) { + Swal.fire('Info', "La désinscription n'est plus possible.", 'info'); + return; + } + + const result = await Swal.fire({ + title: 'Êtes-vous sûr ?', + text: 'Vous ne pourrez plus revenir en arrière.', + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Oui, me désinscrire', + cancelButtonText: 'Annuler', + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + }); + + if (!result.isConfirmed) return; + + try { + const response = await cancelPermanence(permId); + Swal.fire('Succès ✅', response.message, 'success'); + fetchMyPermanences(); + } catch (err) { + console.error("Erreur lors de l'annulation", err); + Swal.fire('Erreur', 'Impossible de vous désinscrire.', 'error'); + } + }; + + return ( +
+ + +
+
+ +
+
+
+ ); +}; + +export default PermanencesPageMy; diff --git a/frontend/src/pages/perm/permRespoCall.tsx b/frontend/src/pages/perm/permRespoCall.tsx new file mode 100644 index 0000000..7b7d225 --- /dev/null +++ b/frontend/src/pages/perm/permRespoCall.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; + +import { Footer } from '../../components/footer'; +import EmergencyModal from '../../components/home/emergencyModal'; +import { Navbar } from '../../components/navbar'; +import { RespoPresenceManagement } from '../../components/permanence/appealPerm'; +import { type DecodedToken } from '../../interfaces/token.interfaces'; +import { decodeToken, getToken } from '../../services/requests/auth.service'; +import { isUserRespo } from '../../services/requests/permanence.service'; + +const PermanencesPageRespoCall = () => { + const [isRespo, setIsRespo] = useState(null); + + useEffect(() => { + const checkRespoStatus = async () => { + try { + const token = getToken(); + + if (!token) { + return ; + } + + let decoded: DecodedToken; + try { + decoded = decodeToken(token); + } catch { + return ; + } + + const result = await isUserRespo(Number(decoded.userId)); + setIsRespo(result.data === true); + } catch (error) { + console.error('Erreur lors de la vérification du rôle respo', error); + setIsRespo(false); + } + }; + + checkRespoStatus(); + }, []); + + return ( +
+ + +
+
+ {isRespo === null ? ( +

Chargement...

+ ) : isRespo === false ? ( +
+ ❌ Accès refusé : vous n'êtes pas responsable d'une permanence. +
+ ) : ( + + )} +
+
+
+
+ ); +}; + +export default PermanencesPageRespoCall; diff --git a/frontend/src/pages/roadbook.tsx b/frontend/src/pages/roadbook.tsx index de80e67..0ea7a98 100644 --- a/frontend/src/pages/roadbook.tsx +++ b/frontend/src/pages/roadbook.tsx @@ -5,7 +5,7 @@ import EmergencyModal from '../components/home/emergencyModal'; import { Navbar } from '../components/navbar'; import { RoadBookCard } from '../components/roadbook/roadbookCard'; -const Roadbook: React.FC = () => ( +const RoadbookPage: React.FC = () => (
@@ -18,4 +18,4 @@ const Roadbook: React.FC = () => (
); -export default Roadbook; +export default RoadbookPage; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 693559d..6b7f453 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -24,7 +24,12 @@ api.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { - console.warn('🔑 Token expiré ou invalide, redirection...'); + console.warn('🔑 Token expiré ou invalide — suppression du token et redirection...'); + try { + localStorage.removeItem('authToken'); + } catch { + console.error('❌ Impossible de supprimer le token du localStorage.'); + } window.location.replace('/'); } return Promise.reject(error); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 8829ce4..ab1fc16 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,32 +1,30 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"], - "baseUrl": ".", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"], + "baseUrl": ".", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 20ff94b..d82983e 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,19 +1,17 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.node.json" + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } } - ], - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": [ - "./src/*" - ] - } - } -} \ No newline at end of file +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 87cfa44..1bca458 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -1,26 +1,22 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": [ - "ES2023" - ], - "module": "ESNext", - "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": [ - "vite.config.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7929520..e2137e4 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,21 +1,29 @@ -import tailwindcss from '@tailwindcss/vite' -import react from '@vitejs/plugin-react' -import path from "path" -import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import cleanup from 'rollup-plugin-cleanup'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + '@': path.resolve(__dirname, './src'), }, }, server: { port: 4000, - allowedHosts: true + allowedHosts: true, }, build: { outDir: 'dist', - } -}) + rollupOptions: { + plugins: [ + cleanup({ + comments: 'none', + }), + ], + }, + }, +});