diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 0720efc..bbb44a8 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -39,7 +39,7 @@ export default function Header({ theme, onToggleTheme, user, onLogout }: HeaderP Playlists )} - {user && ( + {user?.roles?.includes('admin') && ( admin diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index e8146e3..7d0aac6 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -6,6 +6,7 @@ export interface User { email: string | null; nickname: string; subscriptionLevel?: number; + roles: string[]; } interface AuthContextValue { diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index 0421e2d..eeddb74 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -1,5 +1,9 @@ import { useState, useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; import { apiFetch } from '../lib/api'; +import { useAuthContext } from '../context/AuthContext'; + +const API_BASE = import.meta.env.VITE_API_URL || '/api'; // ── Types ──────────────────────────────────────────────────────────────────── @@ -42,8 +46,12 @@ interface AdminUser { type Tab = 'videos' | 'users' | 'plans'; export default function AdminPage() { + const { user, loading: authLoading } = useAuthContext(); const [tab, setTab] = useState('videos'); + if (authLoading) return null; + if (!user?.roles?.includes('admin')) return ; + return (
@@ -74,20 +82,45 @@ export default function AdminPage() { function VideosTab() { const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); + const [fetchError, setFetchError] = useState(null); + const [actionError, setActionError] = useState(null); const [form, setForm] = useState({ title: '', storageType: 'youtube', storageKey: '', requiredLevel: 0, isPublished: false, }); const [saving, setSaving] = useState(false); + const [uploading, setUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); const [error, setError] = useState(null); useEffect(() => { apiFetch<{ success: boolean; data: { videos: Video[] } }>('/admin/videos') .then((r) => setVideos(r.data.videos)) - .catch(() => {}) + .catch(() => setFetchError('Impossible de charger les vidéos.')) .finally(() => setLoading(false)); }, []); + async function handleFileUpload(file: File) { + setUploading(true); + setUploadError(null); + setForm((f) => ({ ...f, storageKey: '' })); + try { + const fd = new FormData(); + fd.append('file', file); + const res = await fetch(`${API_BASE}/admin/videos/upload`, { + method: 'POST', + credentials: 'include', + body: fd, + }); + if (!res.ok) throw new Error(`${res.status}`); + const r = await res.json() as { success: boolean; data: { storageKey: string; storageType: string } }; + setForm((f) => ({ ...f, storageKey: r.data.storageKey, storageType: r.data.storageType })); + } catch { + setUploadError('Échec de l\'upload — vérifier format (mp4/webm, 4 Go max).'); + } + setUploading(false); + } + async function handleCreate(e: React.FormEvent) { e.preventDefault(); if (!form.title || !form.storageKey || saving) return; @@ -107,21 +140,27 @@ function VideosTab() { } async function togglePublish(video: Video) { + setActionError(null); try { const r = await apiFetch<{ success: boolean; data: { video: Video } }>( `/admin/videos/${video.id}`, { method: 'PATCH', body: JSON.stringify({ isPublished: !video.isPublished }) } ); setVideos((v) => v.map((x) => x.id === video.id ? r.data.video : x)); - } catch {} + } catch { + setActionError('Impossible de modifier la vidéo.'); + } } async function handleDelete(id: string) { if (!confirm('Supprimer cette vidéo ?')) return; + setActionError(null); try { await apiFetch(`/admin/videos/${id}`, { method: 'DELETE' }); setVideos((v) => v.filter((x) => x.id !== id)); - } catch {} + } catch { + setActionError('Impossible de supprimer la vidéo.'); + } } return ( @@ -142,7 +181,7 @@ function VideosTab() {
- setForm((f) => ({ ...f, storageKey: e.target.value }))} - placeholder={form.storageType === 'youtube' ? 'ID YouTube (ex: dQw4w9WgXcQ)' : 'Chemin / URL'} - required - className="flex-1 rounded border border-od-border bg-od-bg px-3 py-2 text-sm text-od-text placeholder-od-muted outline-none focus:border-od-accent" - /> + {form.storageType === 'local' ? ( +
+ { const f = e.target.files?.[0]; if (f) handleFileUpload(f); }} + className="flex-1 rounded border border-od-border bg-od-bg px-3 py-2 text-sm text-od-text outline-none focus:border-od-accent file:mr-2 file:rounded file:border-0 file:bg-od-surface file:px-2 file:py-0.5 file:font-mono file:text-xs file:text-od-muted" + /> + {uploading &&

Envoi en cours…

} + {uploadError &&

{uploadError}

} + {!uploading && !uploadError && form.storageKey && ( +

✓ Upload réussi

+ )} +
+ ) : ( + setForm((f) => ({ ...f, storageKey: e.target.value }))} + placeholder={form.storageType === 'youtube' ? 'ID YouTube (ex: dQw4w9WgXcQ)' : 'Chemin / URL'} + required + className="flex-1 rounded border border-od-border bg-od-bg px-3 py-2 text-sm text-od-text placeholder-od-muted outline-none focus:border-od-accent" + /> + )}
@@ -183,7 +239,7 @@ function VideosTab() { {error &&

{error}

} + +
@@ -320,6 +430,8 @@ function UsersTab() { function PlansTab() { const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); + const [fetchError, setFetchError] = useState(null); + const [actionError, setActionError] = useState(null); const [form, setForm] = useState({ slug: '', name: '', level: 1, priceInCents: 0 }); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -327,7 +439,7 @@ function PlansTab() { useEffect(() => { apiFetch<{ success: boolean; data: { plans: Plan[] } }>('/admin/plans') .then((r) => setPlans(r.data.plans)) - .catch(() => {}) + .catch(() => setFetchError('Impossible de charger les plans.')) .finally(() => setLoading(false)); }, []); @@ -350,16 +462,25 @@ function PlansTab() { } async function toggleActive(plan: Plan) { + setActionError(null); try { const r = await apiFetch<{ success: boolean; data: { plan: Plan } }>( `/admin/plans/${plan.id}`, { method: 'PATCH', body: JSON.stringify({ isActive: !plan.isActive }) } ); setPlans((p) => p.map((x) => x.id === plan.id ? r.data.plan : x)); - } catch {} + } catch { + setActionError('Impossible de modifier le plan.'); + } } - if (loading) return
; + if (loading) return ( +
+ {[...Array(2)].map((_, i) =>
)} +
+ ); + + if (fetchError) return

{fetchError}

; return (
@@ -412,6 +533,7 @@ function PlansTab() {
+ {actionError &&

{actionError}

} {plans.map((p) => (