diff --git a/backend/src/routes/video.routes.ts b/backend/src/routes/video.routes.ts index 5dad459..d9ddc05 100644 --- a/backend/src/routes/video.routes.ts +++ b/backend/src/routes/video.routes.ts @@ -35,19 +35,26 @@ router.get("/", async (req: Request, res: Response): Promise => { try { const user = (req as AuthenticatedRequest).user as AuthenticatedUser | undefined; const userLevel = user ? await getUserPlanLevel(user.id) : 0; + const q = typeof req.query.q === "string" ? req.query.q.trim() : ""; - const videos = await AppDataSource.getRepository(Video).find({ - where: { isPublished: true }, - order: { publishedAt: "DESC" }, - select: ["id", "title", "description", "thumbnailUrl", "duration", - "storageType", "storageKey", "requiredLevel", "publishedAt"], - }); + const qb = AppDataSource.getRepository(Video) + .createQueryBuilder("v") + .where("v.isPublished = :pub", { pub: true }) + .select([ + "v.id", "v.title", "v.description", "v.thumbnailUrl", "v.duration", + "v.storageType", "v.storageKey", "v.requiredLevel", "v.publishedAt", + ]) + .orderBy("v.publishedAt", "DESC"); + + if (q) { + qb.andWhere("(v.title LIKE :q OR v.description LIKE :q)", { q: `%${q}%` }); + } + + const videos = await qb.getMany(); - // Injequer un flag `locked` côté client pour les vidéos hors niveau const result = videos.map((v) => ({ ...v, locked: v.requiredLevel > userLevel, - // Ne pas exposer storageKey si la vidéo est verrouillée storageKey: v.requiredLevel > userLevel ? null : v.storageKey, })); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 5b9fc6b..677b149 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { apiFetch } from '../lib/api'; @@ -20,6 +20,7 @@ export default function HomePage() { const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const [query, setQuery] = useState(''); useEffect(() => { apiFetch('/videos') @@ -28,8 +29,16 @@ export default function HomePage() { .finally(() => setLoading(false)); }, []); - const free = videos.filter((v) => !v.locked); - const premium = videos.filter((v) => v.locked); + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return videos; + return videos.filter( + (v) => v.title.toLowerCase().includes(q) || v.description?.toLowerCase().includes(q), + ); + }, [videos, query]); + + const free = filtered.filter((v) => !v.locked); + const premium = filtered.filter((v) => v.locked); return (
@@ -40,6 +49,13 @@ export default function HomePage() {

Contenu libre et premium — connecte-toi pour accéder aux formations complètes.

+ setQuery(e.target.value)} + placeholder="Rechercher…" + className="mt-4 w-full max-w-sm rounded border border-od-border bg-od-surface px-3 py-2 text-sm text-od-text placeholder-od-muted outline-none focus:border-od-accent" + /> {loading && ( @@ -79,6 +95,9 @@ export default function HomePage() { {videos.length === 0 && (

Aucune vidéo disponible pour l'instant.

)} + {videos.length > 0 && filtered.length === 0 && ( +

Aucun résultat pour « {query} ».

+ )} )}