feat: lazy ReactPlayer, seed 11 vidéos YouTube (niveaux 0/1/2)
This commit is contained in:
@@ -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
166
backend/src/seeds/videos.ts
Normal 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); });
|
||||||
@@ -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">
|
||||||
|
<Suspense fallback={<div className="h-full w-full bg-od-surface-hi animate-pulse" />}>
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
src={playerUrl}
|
src={playerUrl}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
controls
|
controls
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user