diff --git a/frontend/Design/image.png b/frontend/Design/image.png deleted file mode 100644 index 41aaeb4..0000000 Binary files a/frontend/Design/image.png and /dev/null differ diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..86a7682 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,23 @@ +User-agent: * + +# Pages publiques autorisées +Allow: / +Allow: /accueil +Allow: /regles-jeu +Allow: /contact +Allow: /mentions-legales +Allow: /confidentialite +Allow: /sitemap +Allow: /inscription +Allow: /connexion + +# Pages privées interdites aux robots +Disallow: /admin +Disallow: /employe +Disallow: /profile +Disallow: /mes-gains +Disallow: /saisir-code-unique +Disallow: /perdu + +# Sitemap +Sitemap: https://thetiptop.fr/sitemap diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3ef0c6d..7263b3c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import Layout from './components/Layout.jsx' +import ProtectedRoute from './components/ProtectedRoute.jsx' import Connexion from './pages/Connexion' import Inscription from './pages/Inscription' import Accueil from './pages/Accueil' @@ -11,6 +12,8 @@ import Admin from './pages/Admin' import Profile from './pages/Profile' import SaisirCodeUnique from './pages/SaisirCodeUnique.jsx' import Perdu from './pages/Perdu' +import NotFound from './pages/NotFound' +import Sitemap from './pages/Sitemap' import MentionsLegales from './pages/MentionsLegales' import Confidentialite from './pages/Confidentialite' import Contact from './pages/Contact.jsx' @@ -22,18 +25,53 @@ export default function App() { } /> } /> } /> + + {/* Pages publiques */} } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> } /> } /> } /> + } /> + + {/* Pages CLIENT */} + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + {/* Page EMPLOYEE */} + + + + } /> + + {/* Page ADMIN */} + + + + } /> + + } /> ) diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx index 2cba567..604ba0a 100644 --- a/frontend/src/components/Footer.jsx +++ b/frontend/src/components/Footer.jsx @@ -123,6 +123,15 @@ export default function Footer() { +
  • + + Plan du site + +
  • + diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index a17a658..a9b7291 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -1,21 +1,36 @@ import { useState } from 'react' -import { Link, useLocation } from 'react-router-dom' +import { Link, useLocation, useNavigate } from 'react-router-dom' import logo from '../assets/img/logo.png' export default function Header() { const [menuOpen, setMenuOpen] = useState(false) const { pathname } = useLocation() + const navigate = useNavigate() + + const user = JSON.parse(localStorage.getItem('user') || 'null') + const isLoggedIn = !!user + + const handleLogout = () => { + localStorage.removeItem('token') + localStorage.removeItem('user') + setMenuOpen(false) + navigate('/connexion') + } const navLinks = [ { to: '/accueil', label: 'Accueil' }, { to: '/regles-jeu', label: 'Règles du jeu' }, ] + const initials = user + ? `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase() + : null + return (
    - + Thé Tip Top @@ -32,14 +47,32 @@ export default function Header() { {link.label} ))} -
    - - Connexion - - - Inscription - -
    + + {isLoggedIn ? ( +
    + +
    + {initials} +
    + {user.firstname} + + +
    + ) : ( +
    + + Connexion + + + Inscription + +
    + )} {/* Hamburger mobile */} @@ -69,21 +102,42 @@ export default function Header() { {link.label} ))} +
    - setMenuOpen(false)} - className="w-full text-center bg-green-700 text-white px-4 py-2.5 rounded-lg hover:bg-green-800 transition-colors text-sm font-medium" - > - Connexion - - setMenuOpen(false)} - className="w-full text-center border border-green-700 text-green-700 px-4 py-2.5 rounded-lg hover:bg-green-50 transition-colors text-sm font-medium" - > - Inscription - + {isLoggedIn ? ( + <> + setMenuOpen(false)} + className="w-full text-center bg-[#006B6B] text-white px-4 py-2.5 rounded-lg text-sm font-medium" + > + Mon espace ({user.firstname}) + + + + ) : ( + <> + setMenuOpen(false)} + className="w-full text-center bg-green-700 text-white px-4 py-2.5 rounded-lg hover:bg-green-800 transition-colors text-sm font-medium" + > + Connexion + + setMenuOpen(false)} + className="w-full text-center border border-green-700 text-green-700 px-4 py-2.5 rounded-lg hover:bg-green-50 transition-colors text-sm font-medium" + > + Inscription + + + )}
    )} diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..968d36b --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,17 @@ +import { Navigate } from 'react-router-dom' + +export default function ProtectedRoute({ children, roles }) { + const token = localStorage.getItem('token') + const user = JSON.parse(localStorage.getItem('user') || 'null') + + if (!token || !user) { + return + } + + if (roles && !roles.includes(user.role)) { + const redirects = { ADMIN: '/admin', EMPLOYEE: '/employe', CLIENT: '/mes-gains' } + return + } + + return children +} diff --git a/frontend/src/components/UserNav.jsx b/frontend/src/components/UserNav.jsx new file mode 100644 index 0000000..1367a3b --- /dev/null +++ b/frontend/src/components/UserNav.jsx @@ -0,0 +1,56 @@ +import { Link } from 'react-router-dom' + +export default function UserNav({ active }) { + const tabs = [ + { + key: 'code', + to: '/saisir-code-unique', + label: 'Saisir un code', + icon: ( + + + + ), + }, + { + key: 'gains', + to: '/mes-gains', + label: 'Mes gains', + icon: ( + + + + ), + }, + { + key: 'profile', + to: '/profile', + label: 'Profil', + icon: ( + + + + ), + }, + ] + + return ( +
    + {tabs.map((tab) => ( + + {tab.icon} + {tab.label} + {tab.label.split(' ')[0]} + + ))} +
    + ) +} diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 94dc907..e55af6c 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -1,72 +1,179 @@ -const MOCK_STATS = { - totalParticipants: 1248, - gainsRemis: 342, - gainsEnAttente: 906, - tauxParticipation: '78%', +import { useState, useEffect } from 'react' +import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi } from '../services/admin' + +function StatCard({ label, value, sub }) { + return ( +
    +

    {value ?? '—'}

    +

    {label}

    + {sub &&

    {sub}

    } +
    + ) } -const MOCK_USERS = [ - { id: 1, name: 'Jean Dupont', email: 'jean.dupont@email.com', gains: 2 }, - { id: 2, name: 'Marie Martin', email: 'marie.martin@email.com', gains: 1 }, - { id: 3, name: 'Paul Bernard', email: 'paul.bernard@email.com', gains: 3 }, -] - export default function Admin() { + const [stats, setStats] = useState(null) + const [users, setUsers] = useState([]) + const [winner, setWinner] = useState(undefined) + const [drawing, setDrawing] = useState(false) + const [drawError, setDrawError] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + useEffect(() => { + Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()]) + .then(([s, u, w]) => { + setStats(s) + setUsers(u.users) + setWinner(w) + }) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)) + }, []) + + const handleDraw = async () => { + if (!window.confirm('Lancer le tirage au sort ? Cette action est irréversible.')) return + setDrawing(true) + setDrawError('') + try { + const data = await drawWinnerApi() + setWinner(data) + } catch (e) { + setDrawError(e.message) + } finally { + setDrawing(false) + } + } + + if (loading) return ( +
    +

    Chargement...

    +
    + ) + + if (error) return ( +
    +

    {error}

    +
    + ) + return (
    -
    -

    Tableau de bord Admin

    +
    - {/* Stats */} -
    - {[ - { label: 'Participants', value: MOCK_STATS.totalParticipants }, - { label: 'Gains remis', value: MOCK_STATS.gainsRemis }, - { label: 'En attente', value: MOCK_STATS.gainsEnAttente }, - { label: 'Taux participation', value: MOCK_STATS.tauxParticipation }, - ].map((stat) => ( -
    -

    {stat.value}

    -

    {stat.label}

    -
    - ))} +

    Tableau de bord Admin

    + + {/* Stats principales */} +
    + + + +
    + {/* Stats tickets */} +
    + + + +
    + + {/* Répartition par lot */} + {stats?.lotsStats?.length > 0 && ( +
    +

    Répartition des lots

    +
    + + + + + + + + + + + {stats.lotsStats.map((lot) => ( + + + + + + + ))} + +
    Lot%TicketsParticipations
    {lot.name}{lot.percentage}%{lot._count.tickets.toLocaleString('fr-FR')}{lot._count.participations}
    +
    +
    + )} + {/* Tirage au sort */} -
    +

    Grand tirage au sort

    - + + {winner?.winner ? ( +
    +

    🏆 Gagnant tiré au sort

    +

    + {winner.winner.user.firstname} {winner.winner.user.lastname} +

    +

    {winner.winner.user.email}

    +

    + Lot : {winner.winner.prize} +

    +
    + ) : ( +
    + + {drawError &&

    {drawError}

    } +
    + )}
    {/* Liste participants */}
    -

    Participants

    +

    + Participants ({users.length}) +

    - + - + - {MOCK_USERS.map((user) => ( + {users.length === 0 && ( + + + + )} + {users.map((user) => ( - - - + + + ))}
    Nom EmailGainsParticipations
    + Aucun participant pour le moment +
    {user.name}{user.email}{user.gains} + {user.firstname} {user.lastname} + {user.email} + {user._count.participations} +
    +
    ) diff --git a/frontend/src/pages/MesGains.jsx b/frontend/src/pages/MesGains.jsx index 53a25f9..5646002 100644 --- a/frontend/src/pages/MesGains.jsx +++ b/frontend/src/pages/MesGains.jsx @@ -1,39 +1,6 @@ import { useState, useEffect } from 'react' -import { Link } from 'react-router-dom' import { myGainsApi } from '../services/game' - -function TabNav({ active }) { - return ( -
    - - - - - Mes gains - - - - - - Profile - -
    - ) -} +import UserNav from '../components/UserNav' function formatDate(dateStr) { return new Date(dateStr).toLocaleDateString('fr-FR', { @@ -57,21 +24,9 @@ export default function MesGains() { return (
    - + - - - - - Saisir un nouveau code - - - {loading && ( -

    Chargement...

    - )} + {loading &&

    Chargement...

    } {error && (
    @@ -83,19 +38,13 @@ export default function MesGains() {

    Aucun gain pour le moment.

    )} - {/* Timeline */}
    {gains.map((gain, index) => (
    - {/* Ligne verticale + cercle */}
    - {index < gains.length - 1 && ( -
    - )} + {index < gains.length - 1 &&
    }
    - - {/* Contenu */}

    {gain.lot}

    {gain.description}

    diff --git a/frontend/src/pages/NotFound.jsx b/frontend/src/pages/NotFound.jsx new file mode 100644 index 0000000..1bca46e --- /dev/null +++ b/frontend/src/pages/NotFound.jsx @@ -0,0 +1,26 @@ +import { Link } from 'react-router-dom' + +export default function NotFound() { + return ( +
    + +

    404

    + +

    + Page introuvable +

    + +

    + La page que vous recherchez n'existe pas ou a été déplacée. +

    + + + Retour à l'accueil + + +
    + ) +} diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index e485c39..c61c208 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -1,41 +1,7 @@ -import { Link } from 'react-router-dom' - -function TabNav({ active }) { - return ( -
    - - - - - Mes gains - - - - - - Profile - -
    - ) -} +import UserNav from '../components/UserNav' export default function Profile() { const user = JSON.parse(localStorage.getItem('user') || '{}') - const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase() const handleSubmit = (e) => { @@ -46,9 +12,8 @@ export default function Profile() { return (
    - + - {/* Avatar */}
    {initials} @@ -62,21 +27,18 @@ export default function Profile() { {user.email || '—'}
    -
    {user.lastname || '—'}
    -
    {user.firstname || '—'}
    -