feat: lazy ReactPlayer, seed 11 vidéos YouTube (niveaux 0/1/2)
Some checks failed
CI/CD — Build & Deploy / Build (push) Failing after 41s
CI/CD — Build & Deploy / Deploy to VPS (push) Has been skipped

This commit is contained in:
2026-03-14 08:25:41 +01:00
parent 11d9432218
commit 5eb0a43d7f
3 changed files with 180 additions and 9 deletions

View File

@@ -9,7 +9,8 @@
"typeorm": "ts-node -e \"require('typeorm/cli')\"", "typeorm": "ts-node -e \"require('typeorm/cli')\"",
"migration:generate": "npm run typeorm -- migration:generate", "migration:generate": "npm run typeorm -- migration:generate",
"migration:run": "npm run typeorm -- migration:run", "migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert" "migration:revert": "npm run typeorm -- migration:revert",
"seed:videos": "ts-node --transpile-only src/seeds/videos.ts"
}, },
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",

166
backend/src/seeds/videos.ts Normal file
View File

@@ -0,0 +1,166 @@
/**
* Seed vidéos YouTube — OriginsDigital
* Usage : npm run seed:videos
*
* Requiert DB_HOST, DB_USER, DB_PASSWORD, DB_NAME dans l'env.
*/
import "reflect-metadata";
import * as dotenv from "dotenv";
dotenv.config();
import { AppDataSource } from "../config/data-source";
import { Video } from "../entities/Video";
const yt = (id: string) => `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;
const VIDEOS: Partial<Video>[] = [
// ── Niveau 0 — libre ──────────────────────────────────────────────────────
{
title: "JavaScript in 100 Seconds",
description: "Tour rapide du JavaScript — syntaxe, event loop, histoire.",
storageType: "youtube",
storageKey: "DHjqpvDnNGE",
thumbnailUrl: yt("DHjqpvDnNGE"),
duration: 100,
requiredLevel: 0,
isPublished: true,
publishedAt: new Date(),
},
{
title: "TypeScript in 100 Seconds",
description: "Pourquoi typer son JavaScript — les bases en 100 secondes.",
storageType: "youtube",
storageKey: "zQnBQ4tB3ZA",
thumbnailUrl: yt("zQnBQ4tB3ZA"),
duration: 100,
requiredLevel: 0,
isPublished: true,
publishedAt: new Date(),
},
{
title: "React in 100 Seconds",
description: "Components, JSX, hooks — React expliqué en 100 secondes.",
storageType: "youtube",
storageKey: "Tn6-PIqc4UM",
thumbnailUrl: yt("Tn6-PIqc4UM"),
duration: 100,
requiredLevel: 0,
isPublished: true,
publishedAt: new Date(),
},
{
title: "CSS in 100 Seconds",
description: "Cascading Style Sheets — sélecteurs, box model, flexbox.",
storageType: "youtube",
storageKey: "OEV8gMkCHXQ",
thumbnailUrl: yt("OEV8gMkCHXQ"),
duration: 100,
requiredLevel: 0,
isPublished: true,
publishedAt: new Date(),
},
{
title: "Git in 100 Seconds",
description: "Versionner son code — commits, branches, merges.",
storageType: "youtube",
storageKey: "hwP7WQkmECE",
thumbnailUrl: yt("hwP7WQkmECE"),
duration: 100,
requiredLevel: 0,
isPublished: true,
publishedAt: new Date(),
},
{
title: "Node.js in 100 Seconds",
description: "JavaScript côté serveur — event loop, npm, streams.",
storageType: "youtube",
storageKey: "ENrzD9HAZK4",
thumbnailUrl: yt("ENrzD9HAZK4"),
duration: 100,
requiredLevel: 0,
isPublished: true,
publishedAt: new Date(),
},
// ── Niveau 1 — basic ──────────────────────────────────────────────────────
{
title: "Docker in 100 Seconds",
description: "Conteneuriser ses applications — images, volumes, compose.",
storageType: "youtube",
storageKey: "gAkwW2tuIqE",
thumbnailUrl: yt("gAkwW2tuIqE"),
duration: 100,
requiredLevel: 1,
isPublished: true,
publishedAt: new Date(),
},
{
title: "Tailwind CSS in 100 Seconds",
description: "Utility-first CSS — pourquoi ça change tout.",
storageType: "youtube",
storageKey: "mr15Xzb1Ook",
thumbnailUrl: yt("mr15Xzb1Ook"),
duration: 100,
requiredLevel: 1,
isPublished: true,
publishedAt: new Date(),
},
{
title: "SQL Explained in 100 Seconds",
description: "Bases de données relationnelles — SELECT, JOIN, index.",
storageType: "youtube",
storageKey: "zsjvFFKnte0",
thumbnailUrl: yt("zsjvFFKnte0"),
duration: 100,
requiredLevel: 1,
isPublished: true,
publishedAt: new Date(),
},
// ── Niveau 2 — pro ────────────────────────────────────────────────────────
{
title: "Linux in 100 Seconds",
description: "Le système d'exploitation qui fait tourner Internet.",
storageType: "youtube",
storageKey: "rrB13utjYV4",
thumbnailUrl: yt("rrB13utjYV4"),
duration: 100,
requiredLevel: 2,
isPublished: true,
publishedAt: new Date(),
},
{
title: "Redis in 100 Seconds",
description: "Cache in-memory — sessions, queues, pub/sub.",
storageType: "youtube",
storageKey: "G1rOthIU-uo",
thumbnailUrl: yt("G1rOthIU-uo"),
duration: 100,
requiredLevel: 2,
isPublished: true,
publishedAt: new Date(),
},
];
async function seed() {
await AppDataSource.initialize();
const repo = AppDataSource.getRepository(Video);
let inserted = 0;
let skipped = 0;
for (const data of VIDEOS) {
const exists = await repo.findOne({
where: { storageType: data.storageType, storageKey: data.storageKey },
});
if (exists) { skipped++; continue; }
await repo.save(repo.create(data));
inserted++;
}
await AppDataSource.destroy();
console.log(`✅ Seed terminé — ${inserted} insérées, ${skipped} ignorées (déjà présentes).`);
}
seed().catch((e) => { console.error("❌ Seed échoué :", e.message); process.exit(1); });

View File

@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, lazy, Suspense } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import ReactPlayer from 'react-player';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
// Lazy — HLS/DASH chunks ne chargent que sur /video/:id, pas sur la homepage
const ReactPlayer = lazy(() => import('react-player'));
interface Video { interface Video {
id: number; id: number;
title: string; title: string;
@@ -89,12 +91,14 @@ export default function VideoPage() {
{/* Player */} {/* Player */}
<div className="overflow-hidden rounded border border-od-border bg-od-surface"> <div className="overflow-hidden rounded border border-od-border bg-od-surface">
<div className="aspect-video w-full"> <div className="aspect-video w-full">
<ReactPlayer <Suspense fallback={<div className="h-full w-full bg-od-surface-hi animate-pulse" />}>
src={playerUrl} <ReactPlayer
width="100%" src={playerUrl}
height="100%" width="100%"
controls height="100%"
/> controls
/>
</Suspense>
</div> </div>
</div> </div>