changes
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
23
frontend/public/robots.txt
Normal file
23
frontend/public/robots.txt
Normal file
@@ -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
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import Layout from './components/Layout.jsx'
|
import Layout from './components/Layout.jsx'
|
||||||
|
import ProtectedRoute from './components/ProtectedRoute.jsx'
|
||||||
import Connexion from './pages/Connexion'
|
import Connexion from './pages/Connexion'
|
||||||
import Inscription from './pages/Inscription'
|
import Inscription from './pages/Inscription'
|
||||||
import Accueil from './pages/Accueil'
|
import Accueil from './pages/Accueil'
|
||||||
@@ -11,6 +12,8 @@ import Admin from './pages/Admin'
|
|||||||
import Profile from './pages/Profile'
|
import Profile from './pages/Profile'
|
||||||
import SaisirCodeUnique from './pages/SaisirCodeUnique.jsx'
|
import SaisirCodeUnique from './pages/SaisirCodeUnique.jsx'
|
||||||
import Perdu from './pages/Perdu'
|
import Perdu from './pages/Perdu'
|
||||||
|
import NotFound from './pages/NotFound'
|
||||||
|
import Sitemap from './pages/Sitemap'
|
||||||
import MentionsLegales from './pages/MentionsLegales'
|
import MentionsLegales from './pages/MentionsLegales'
|
||||||
import Confidentialite from './pages/Confidentialite'
|
import Confidentialite from './pages/Confidentialite'
|
||||||
import Contact from './pages/Contact.jsx'
|
import Contact from './pages/Contact.jsx'
|
||||||
@@ -22,18 +25,53 @@ export default function App() {
|
|||||||
<Route path="/" element={<Navigate to="/connexion" replace />} />
|
<Route path="/" element={<Navigate to="/connexion" replace />} />
|
||||||
<Route path="/connexion" element={<Connexion />} />
|
<Route path="/connexion" element={<Connexion />} />
|
||||||
<Route path="/inscription" element={<Inscription />} />
|
<Route path="/inscription" element={<Inscription />} />
|
||||||
|
|
||||||
|
{/* Pages publiques */}
|
||||||
<Route path="/accueil" element={<Layout><Accueil /></Layout>} />
|
<Route path="/accueil" element={<Layout><Accueil /></Layout>} />
|
||||||
<Route path="/regles-jeu" element={<Layout><ReglesJeu /></Layout>} />
|
<Route path="/regles-jeu" element={<Layout><ReglesJeu /></Layout>} />
|
||||||
<Route path="/participation" element={<Layout><Participation /></Layout>} />
|
<Route path="/participation" element={<Layout><Participation /></Layout>} />
|
||||||
<Route path="/mes-gains" element={<Layout><MesGains /></Layout>} />
|
|
||||||
<Route path="/employe" element={<Layout><Employe /></Layout>} />
|
|
||||||
<Route path="/admin" element={<Layout><Admin /></Layout>} />
|
|
||||||
<Route path="/profile" element={<Layout><Profile /></Layout>} />
|
|
||||||
<Route path="/saisir-code-unique" element={<Layout><SaisirCodeUnique /></Layout>} />
|
|
||||||
<Route path="/perdu" element={<Layout><Perdu /></Layout>} />
|
|
||||||
<Route path="/mentions-legales" element={<Layout><MentionsLegales /></Layout>} />
|
<Route path="/mentions-legales" element={<Layout><MentionsLegales /></Layout>} />
|
||||||
<Route path="/confidentialite" element={<Layout><Confidentialite /></Layout>} />
|
<Route path="/confidentialite" element={<Layout><Confidentialite /></Layout>} />
|
||||||
<Route path="/contact" element={<Layout><Contact /></Layout>} />
|
<Route path="/contact" element={<Layout><Contact /></Layout>} />
|
||||||
|
<Route path="/sitemap" element={<Layout><Sitemap /></Layout>} />
|
||||||
|
|
||||||
|
{/* Pages CLIENT */}
|
||||||
|
<Route path="/mes-gains" element={
|
||||||
|
<ProtectedRoute roles={['CLIENT']}>
|
||||||
|
<Layout><MesGains /></Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/saisir-code-unique" element={
|
||||||
|
<ProtectedRoute roles={['CLIENT']}>
|
||||||
|
<Layout><SaisirCodeUnique /></Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/profile" element={
|
||||||
|
<ProtectedRoute roles={['CLIENT']}>
|
||||||
|
<Layout><Profile /></Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/perdu" element={
|
||||||
|
<ProtectedRoute roles={['CLIENT']}>
|
||||||
|
<Layout><Perdu /></Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
{/* Page EMPLOYEE */}
|
||||||
|
<Route path="/employe" element={
|
||||||
|
<ProtectedRoute roles={['EMPLOYEE']}>
|
||||||
|
<Layout><Employe /></Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
{/* Page ADMIN */}
|
||||||
|
<Route path="/admin" element={
|
||||||
|
<ProtectedRoute roles={['ADMIN']}>
|
||||||
|
<Layout><Admin /></Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -123,6 +123,15 @@ export default function Footer() {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/sitemap"
|
||||||
|
className="hover:text-white transition"
|
||||||
|
>
|
||||||
|
Plan du site
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
import { useState } from 'react'
|
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'
|
import logo from '../assets/img/logo.png'
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
const { pathname } = useLocation()
|
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 = [
|
const navLinks = [
|
||||||
{ to: '/accueil', label: 'Accueil' },
|
{ to: '/accueil', label: 'Accueil' },
|
||||||
{ to: '/regles-jeu', label: 'Règles du jeu' },
|
{ to: '/regles-jeu', label: 'Règles du jeu' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const initials = user
|
||||||
|
? `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white shadow-sm relative z-50">
|
<header className="bg-white shadow-sm relative z-50">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
||||||
|
|
||||||
<Link to="/accueil" className="flex items-center flex-shrink-0">
|
<Link to={isLoggedIn ? '/mes-gains' : '/accueil'} className="flex items-center flex-shrink-0">
|
||||||
<img src={logo} alt="Thé Tip Top" className="h-10 sm:h-12 w-auto" />
|
<img src={logo} alt="Thé Tip Top" className="h-10 sm:h-12 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@@ -32,14 +47,32 @@ export default function Header() {
|
|||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link to="/mes-gains" className="flex items-center gap-2 text-sm text-gray-700 hover:text-green-700 font-medium transition-colors">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-xs font-bold">
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
<span>{user.firstname}</span>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="text-sm text-gray-400 hover:text-red-500 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link to="/connexion" className="bg-green-700 text-white px-4 py-2 rounded-lg hover:bg-green-800 transition-colors text-sm">
|
<Link to="/connexion" className="bg-green-700 text-white px-4 py-2 rounded-lg hover:bg-green-800 transition-colors text-sm">
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/inscription" className="bg-green-700 text-white px-4 py-2 rounded-lg hover:bg-green-800 transition-colors text-sm">
|
<Link to="/inscription" className="border border-green-700 text-green-700 px-4 py-2 rounded-lg hover:bg-green-50 transition-colors text-sm">
|
||||||
Inscription
|
Inscription
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Hamburger mobile */}
|
{/* Hamburger mobile */}
|
||||||
@@ -69,7 +102,26 @@ export default function Header() {
|
|||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 pt-3">
|
<div className="flex flex-col gap-2 pt-3">
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to="/mes-gains"
|
||||||
|
onClick={() => 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})
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full text-center border border-red-300 text-red-500 px-4 py-2.5 rounded-lg text-sm font-medium hover:bg-red-50 transition-colors"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Link
|
<Link
|
||||||
to="/connexion"
|
to="/connexion"
|
||||||
onClick={() => setMenuOpen(false)}
|
onClick={() => setMenuOpen(false)}
|
||||||
@@ -84,6 +136,8 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
Inscription
|
Inscription
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
17
frontend/src/components/ProtectedRoute.jsx
Normal file
17
frontend/src/components/ProtectedRoute.jsx
Normal file
@@ -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 <Navigate to="/connexion" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles && !roles.includes(user.role)) {
|
||||||
|
const redirects = { ADMIN: '/admin', EMPLOYEE: '/employe', CLIENT: '/mes-gains' }
|
||||||
|
return <Navigate to={redirects[user.role] || '/connexion'} replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
56
frontend/src/components/UserNav.jsx
Normal file
56
frontend/src/components/UserNav.jsx
Normal file
@@ -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: (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gains',
|
||||||
|
to: '/mes-gains',
|
||||||
|
label: 'Mes gains',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M20 12V22H4V12M22 7H2v5h20V7zM12 22V7M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'profile',
|
||||||
|
to: '/profile',
|
||||||
|
label: 'Profil',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex border-b border-gray-200 mb-6">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Link
|
||||||
|
key={tab.key}
|
||||||
|
to={tab.to}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-1.5 py-3 text-xs sm:text-sm font-medium transition-colors ${
|
||||||
|
active === tab.key
|
||||||
|
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
||||||
|
: 'text-gray-400 hover:text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
<span className="hidden sm:inline">{tab.label}</span>
|
||||||
|
<span className="sm:hidden">{tab.label.split(' ')[0]}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,72 +1,179 @@
|
|||||||
const MOCK_STATS = {
|
import { useState, useEffect } from 'react'
|
||||||
totalParticipants: 1248,
|
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi } from '../services/admin'
|
||||||
gainsRemis: 342,
|
|
||||||
gainsEnAttente: 906,
|
function StatCard({ label, value, sub }) {
|
||||||
tauxParticipation: '78%',
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow p-4 text-center">
|
||||||
|
<p className="text-2xl sm:text-3xl font-bold text-green-700">{value ?? '—'}</p>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-500 mt-1">{label}</p>
|
||||||
|
{sub && <p className="text-xs text-gray-400 mt-0.5">{sub}</p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
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 (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<p className="text-gray-400">Chargement...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
|
||||||
|
<p className="text-red-500 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-4 sm:p-6">
|
<div className="min-h-screen bg-gray-50 p-4 sm:p-6">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-5xl mx-auto space-y-6">
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-800 mb-4 sm:mb-6">Tableau de bord Admin</h1>
|
|
||||||
|
|
||||||
{/* Stats */}
|
<h1 className="text-xl sm:text-2xl font-bold text-gray-800">Tableau de bord Admin</h1>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-4 mb-6 sm:mb-8">
|
|
||||||
{[
|
{/* Stats principales */}
|
||||||
{ label: 'Participants', value: MOCK_STATS.totalParticipants },
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
|
||||||
{ label: 'Gains remis', value: MOCK_STATS.gainsRemis },
|
<StatCard label="Participants" value={stats?.users} />
|
||||||
{ label: 'En attente', value: MOCK_STATS.gainsEnAttente },
|
<StatCard label="Participations" value={stats?.participations} />
|
||||||
{ label: 'Taux participation', value: MOCK_STATS.tauxParticipation },
|
<StatCard label="Gains en attente" value={stats?.claimsPending} />
|
||||||
].map((stat) => (
|
<StatCard label="Gains remis" value={stats?.claimsDelivered} />
|
||||||
<div key={stat.label} className="bg-white rounded-xl shadow p-3 sm:p-4 text-center">
|
|
||||||
<p className="text-xl sm:text-2xl font-bold text-green-700">{stat.value}</p>
|
|
||||||
<p className="text-xs sm:text-sm text-gray-500 mt-1">{stat.label}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tirage au sort */}
|
{/* Stats tickets */}
|
||||||
<div className="bg-white rounded-xl shadow p-4 sm:p-6 mb-4 sm:mb-6">
|
<div className="grid grid-cols-3 gap-3 sm:gap-4">
|
||||||
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-3">Grand tirage au sort</h2>
|
<StatCard label="Tickets total" value={stats?.ticketsTotal} />
|
||||||
<button
|
<StatCard label="Tickets utilisés" value={stats?.ticketsUsed} />
|
||||||
onClick={() => alert('TODO: connecter au backend')}
|
<StatCard label="Tickets restants" value={stats?.ticketsRemaining} />
|
||||||
className="bg-green-700 hover:bg-green-800 text-white font-semibold px-5 sm:px-6 py-2 sm:py-2.5 rounded-lg transition-colors text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
Lancer le tirage
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Liste participants */}
|
{/* Répartition par lot */}
|
||||||
|
{stats?.lotsStats?.length > 0 && (
|
||||||
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
|
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
|
||||||
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Participants</h2>
|
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Répartition des lots</h2>
|
||||||
<div className="overflow-x-auto -mx-4 sm:mx-0">
|
<div className="overflow-x-auto -mx-4 sm:mx-0">
|
||||||
<table className="w-full text-sm min-w-[400px]">
|
<table className="w-full text-sm min-w-[480px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-gray-500 border-b">
|
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
|
||||||
<th className="pb-2 px-4 sm:px-0">Nom</th>
|
<th className="pb-2 px-4 sm:px-0">Lot</th>
|
||||||
<th className="pb-2 px-4 sm:px-0">Email</th>
|
<th className="pb-2 px-4 sm:px-0 text-center">%</th>
|
||||||
<th className="pb-2 px-4 sm:px-0 text-right">Gains</th>
|
<th className="pb-2 px-4 sm:px-0 text-center">Tickets</th>
|
||||||
|
<th className="pb-2 px-4 sm:px-0 text-center">Participations</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{MOCK_USERS.map((user) => (
|
{stats.lotsStats.map((lot) => (
|
||||||
<tr key={user.id} className="border-b last:border-0">
|
<tr key={lot.id} className="border-b last:border-0">
|
||||||
<td className="py-3 px-4 sm:px-0 font-medium text-gray-800 whitespace-nowrap">{user.name}</td>
|
<td className="py-2.5 px-4 sm:px-0 font-medium text-gray-800 text-xs sm:text-sm">{lot.name}</td>
|
||||||
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs sm:text-sm">{user.email}</td>
|
<td className="py-2.5 px-4 sm:px-0 text-center text-gray-500 text-xs sm:text-sm">{lot.percentage}%</td>
|
||||||
<td className="py-3 px-4 sm:px-0 text-right text-green-700 font-semibold">{user.gains}</td>
|
<td className="py-2.5 px-4 sm:px-0 text-center text-gray-600 text-xs sm:text-sm">{lot._count.tickets.toLocaleString('fr-FR')}</td>
|
||||||
|
<td className="py-2.5 px-4 sm:px-0 text-center text-green-700 font-semibold text-xs sm:text-sm">{lot._count.participations}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tirage au sort */}
|
||||||
|
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
|
||||||
|
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-3">Grand tirage au sort</h2>
|
||||||
|
|
||||||
|
{winner?.winner ? (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<p className="text-green-700 font-semibold mb-1">🏆 Gagnant tiré au sort</p>
|
||||||
|
<p className="text-gray-800 font-medium">
|
||||||
|
{winner.winner.user.firstname} {winner.winner.user.lastname}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-sm">{winner.winner.user.email}</p>
|
||||||
|
<p className="text-gray-400 text-xs mt-1">
|
||||||
|
Lot : {winner.winner.prize}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button
|
||||||
|
onClick={handleDraw}
|
||||||
|
disabled={drawing}
|
||||||
|
className="bg-green-700 hover:bg-green-800 disabled:opacity-60 text-white font-semibold px-5 py-2.5 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
{drawing ? 'Tirage en cours...' : 'Lancer le tirage'}
|
||||||
|
</button>
|
||||||
|
{drawError && <p className="text-red-500 text-xs">{drawError}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste participants */}
|
||||||
|
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
|
||||||
|
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">
|
||||||
|
Participants <span className="text-gray-400 font-normal text-sm">({users.length})</span>
|
||||||
|
</h2>
|
||||||
|
<div className="overflow-x-auto -mx-4 sm:mx-0">
|
||||||
|
<table className="w-full text-sm min-w-[400px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
|
||||||
|
<th className="pb-2 px-4 sm:px-0">Nom</th>
|
||||||
|
<th className="pb-2 px-4 sm:px-0">Email</th>
|
||||||
|
<th className="pb-2 px-4 sm:px-0 text-right">Participations</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="py-6 text-center text-gray-400 text-sm">
|
||||||
|
Aucun participant pour le moment
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id} className="border-b last:border-0">
|
||||||
|
<td className="py-3 px-4 sm:px-0 font-medium text-gray-800 whitespace-nowrap text-xs sm:text-sm">
|
||||||
|
{user.firstname} {user.lastname}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs">{user.email}</td>
|
||||||
|
<td className="py-3 px-4 sm:px-0 text-right text-green-700 font-semibold text-xs sm:text-sm">
|
||||||
|
{user._count.participations}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,39 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { myGainsApi } from '../services/game'
|
import { myGainsApi } from '../services/game'
|
||||||
|
import UserNav from '../components/UserNav'
|
||||||
function TabNav({ active }) {
|
|
||||||
return (
|
|
||||||
<div className="flex border-b border-gray-200 mb-6">
|
|
||||||
<Link
|
|
||||||
to="/mes-gains"
|
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
|
||||||
active === 'gains'
|
|
||||||
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
|
||||||
: 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 12V22H4V12M22 7H2v5h20V7zM12 22V7M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z" />
|
|
||||||
</svg>
|
|
||||||
Mes gains
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/profile"
|
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
|
||||||
active === 'profile'
|
|
||||||
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
|
||||||
: 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
Profile
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
||||||
@@ -57,21 +24,9 @@ export default function MesGains() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
||||||
<TabNav active="gains" />
|
<UserNav active="gains" />
|
||||||
|
|
||||||
<Link
|
{loading && <p className="text-center text-gray-400 text-sm mt-8">Chargement...</p>}
|
||||||
to="/saisir-code-unique"
|
|
||||||
className="flex items-center justify-center gap-2 w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors mb-6 text-sm"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
Saisir un nouveau code
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<p className="text-center text-gray-400 text-sm mt-8">Chargement...</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
|
<div className="px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
|
||||||
@@ -83,19 +38,13 @@ export default function MesGains() {
|
|||||||
<p className="text-center text-gray-400 text-sm mt-8">Aucun gain pour le moment.</p>
|
<p className="text-center text-gray-400 text-sm mt-8">Aucun gain pour le moment.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Timeline */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{gains.map((gain, index) => (
|
{gains.map((gain, index) => (
|
||||||
<div key={gain.participationId} className="flex gap-4">
|
<div key={gain.participationId} className="flex gap-4">
|
||||||
{/* Ligne verticale + cercle */}
|
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="w-3 h-3 mt-1 rounded-full border-2 border-gray-400 flex-shrink-0" />
|
<div className="w-3 h-3 mt-1 rounded-full border-2 border-gray-400 flex-shrink-0" />
|
||||||
{index < gains.length - 1 && (
|
{index < gains.length - 1 && <div className="w-px flex-1 bg-gray-200 my-1" />}
|
||||||
<div className="w-px flex-1 bg-gray-200 my-1" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contenu */}
|
|
||||||
<div className={`flex-1 pb-6 ${index === 0 ? 'bg-gray-50 rounded-lg px-3 pt-1 -ml-1' : ''}`}>
|
<div className={`flex-1 pb-6 ${index === 0 ? 'bg-gray-50 rounded-lg px-3 pt-1 -ml-1' : ''}`}>
|
||||||
<p className="font-semibold text-gray-800 text-sm">{gain.lot}</p>
|
<p className="font-semibold text-gray-800 text-sm">{gain.lot}</p>
|
||||||
<p className="text-gray-500 text-xs mt-1 leading-relaxed">{gain.description}</p>
|
<p className="text-gray-500 text-xs mt-1 leading-relaxed">{gain.description}</p>
|
||||||
|
|||||||
26
frontend/src/pages/NotFound.jsx
Normal file
26
frontend/src/pages/NotFound.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white flex flex-col items-center justify-center px-6 text-center">
|
||||||
|
|
||||||
|
<p className="text-8xl sm:text-9xl font-bold text-[#006B6B] leading-none">404</p>
|
||||||
|
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-bold italic font-serif text-gray-800 mt-4 mb-2">
|
||||||
|
Page introuvable
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-gray-400 text-sm sm:text-base max-w-xs mb-8">
|
||||||
|
La page que vous recherchez n'existe pas ou a été déplacée.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/accueil"
|
||||||
|
className="bg-[#006B6B] hover:bg-[#005959] text-white font-medium px-8 py-3 rounded-lg transition-colors text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
Retour à l'accueil
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,41 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import UserNav from '../components/UserNav'
|
||||||
|
|
||||||
function TabNav({ active }) {
|
|
||||||
return (
|
|
||||||
<div className="flex border-b border-gray-200 mb-6">
|
|
||||||
<Link
|
|
||||||
to="/mes-gains"
|
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
|
||||||
active === 'gains'
|
|
||||||
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
|
||||||
: 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 12V22H4V12M22 7H2v5h20V7zM12 22V7M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z" />
|
|
||||||
</svg>
|
|
||||||
Mes gains
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/profile"
|
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
|
||||||
active === 'profile'
|
|
||||||
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
|
||||||
: 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
Profile
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||||
|
|
||||||
const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
@@ -46,9 +12,8 @@ export default function Profile() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
||||||
<TabNav active="profile" />
|
<UserNav active="profile" />
|
||||||
|
|
||||||
{/* Avatar */}
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="w-14 h-14 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-lg font-bold">
|
<div className="w-14 h-14 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-lg font-bold">
|
||||||
{initials}
|
{initials}
|
||||||
@@ -62,21 +27,18 @@ export default function Profile() {
|
|||||||
{user.email || '—'}
|
{user.email || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-800 mb-1">Nom</label>
|
<label className="block text-sm font-medium text-gray-800 mb-1">Nom</label>
|
||||||
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
||||||
{user.lastname || '—'}
|
{user.lastname || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-800 mb-1">Prénom</label>
|
<label className="block text-sm font-medium text-gray-800 mb-1">Prénom</label>
|
||||||
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
||||||
{user.firstname || '—'}
|
{user.firstname || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors"
|
className="w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { participateApi } from '../services/game'
|
import { participateApi } from '../services/game'
|
||||||
|
import UserNav from '../components/UserNav'
|
||||||
|
|
||||||
export default function SaisirCodeUnique() {
|
export default function SaisirCodeUnique() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -24,6 +25,8 @@ export default function SaisirCodeUnique() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center px-8 py-12 max-w-sm mx-auto">
|
<div className="min-h-screen bg-gray-50 flex flex-col justify-center px-8 py-12 max-w-sm mx-auto">
|
||||||
|
|
||||||
|
<UserNav active="code" />
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold italic font-serif text-[#006B6B]">Saisir mon code gagnant</h1>
|
<h1 className="text-2xl font-bold italic font-serif text-[#006B6B]">Saisir mon code gagnant</h1>
|
||||||
<p className="text-gray-500 mt-2 text-sm">Saisissez le code présent sur votre ticket</p>
|
<p className="text-gray-500 mt-2 text-sm">Saisissez le code présent sur votre ticket</p>
|
||||||
|
|||||||
96
frontend/src/pages/Sitemap.jsx
Normal file
96
frontend/src/pages/Sitemap.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'Navigation principale',
|
||||||
|
links: [
|
||||||
|
{ label: 'Accueil', to: '/accueil', desc: "Page d'accueil du jeu-concours" },
|
||||||
|
{ label: 'Règles du jeu', to: '/regles-jeu', desc: 'Comment participer et que gagner' },
|
||||||
|
{ label: 'Contact', to: '/contact', desc: 'Nous contacter' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Compte utilisateur',
|
||||||
|
links: [
|
||||||
|
{ label: 'Connexion', to: '/connexion', desc: 'Se connecter à son espace' },
|
||||||
|
{ label: 'Inscription', to: '/inscription', desc: 'Créer un compte participant' },
|
||||||
|
{ label: 'Mon profil', to: '/profile', desc: 'Voir et modifier ses informations' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Jeu-concours',
|
||||||
|
links: [
|
||||||
|
{ label: 'Saisir mon code', to: '/saisir-code-unique', desc: 'Entrer son code ticket' },
|
||||||
|
{ label: 'Mes gains', to: '/mes-gains', desc: 'Historique de ses participations et lots' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Espaces professionnels',
|
||||||
|
links: [
|
||||||
|
{ label: 'Espace employé', to: '/employe', desc: 'Vérifier et remettre un lot' },
|
||||||
|
{ label: 'Tableau de bord admin', to: '/admin', desc: 'Statistiques et gestion du jeu' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Informations légales',
|
||||||
|
links: [
|
||||||
|
{ label: 'Mentions légales', to: '/mentions-legales', desc: 'Informations légales du site' },
|
||||||
|
{ label: 'Politique de confidentialité', to: '/confidentialite', desc: 'Gestion des données personnelles' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Sitemap() {
|
||||||
|
return (
|
||||||
|
<main className="bg-green-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
|
||||||
|
<div className="mb-8 sm:mb-10">
|
||||||
|
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-green-800 mb-2">
|
||||||
|
Plan du site
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 text-sm sm:text-base">
|
||||||
|
Retrouvez ici l'ensemble des pages disponibles sur le site Thé Tip Top.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid sm:grid-cols-2 gap-6">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<div key={section.title} className="bg-white rounded-2xl shadow p-5 sm:p-6">
|
||||||
|
<h2 className="text-base sm:text-lg font-bold text-green-700 mb-4 pb-2 border-b border-gray-100">
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{section.links.map((link) => (
|
||||||
|
<li key={link.to}>
|
||||||
|
<Link
|
||||||
|
to={link.to}
|
||||||
|
className="group flex items-start gap-3"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mt-0.5 text-[#006B6B] flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-800 group-hover:text-[#006B6B] transition-colors">
|
||||||
|
{link.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">{link.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
frontend/src/services/admin.js
Normal file
35
frontend/src/services/admin.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
function authHeaders() {
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStatsApi() {
|
||||||
|
const res = await fetch('/api/admin/stats', { headers: authHeaders() })
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.message || 'Erreur stats.')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsersApi() {
|
||||||
|
const res = await fetch('/api/admin/users', { headers: authHeaders() })
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.message || 'Erreur utilisateurs.')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function drawWinnerApi() {
|
||||||
|
const res = await fetch('/api/admin/draw', { method: 'POST', headers: authHeaders() })
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.message || 'Erreur tirage.')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWinnerApi() {
|
||||||
|
const res = await fetch('/api/admin/draw', { headers: authHeaders() })
|
||||||
|
if (res.status === 404) return null
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.message || 'Erreur gagnant.')
|
||||||
|
return data
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user