diff --git a/client/src/components/header.tsx b/client/src/components/header.tsx index 773a5bf..96536d9 100644 --- a/client/src/components/header.tsx +++ b/client/src/components/header.tsx @@ -7,6 +7,7 @@ import folxLogo from "@assets/folx_MT_poz_b_1772296729169.png"; const navItems = [ { label: "Start", href: "/" }, { label: "News", href: "/category/News" }, + { label: "Video", href: "/videos" }, ]; export default function Header() { diff --git a/client/src/pages/videos.tsx b/client/src/pages/videos.tsx new file mode 100644 index 0000000..a27c456 --- /dev/null +++ b/client/src/pages/videos.tsx @@ -0,0 +1,107 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "wouter"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; +import { Play } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import Header from "@/components/header"; +import Footer from "@/components/footer"; + +interface BunnyVideo { + guid: string; + title: string; + length: number; + dateUploaded: string; + thumbnail: string; + embedUrl: string; +} + +interface VideoResponse { + items: BunnyVideo[]; + totalItems: number; + currentPage: number; +} + +function formatDuration(seconds: number): string { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return `${m}:${s.toString().padStart(2, "0")}`; +} + +function VideoCard({ video }: { video: BunnyVideo }) { + return ( + +
+
+
+ {video.title} +
+
+
+ +
+
+
+ {formatDuration(video.length)} +
+
+
+

+ {video.title} +

+

+ {format(new Date(video.dateUploaded), "d. MMMM yyyy", { locale: de })} +

+
+
+ + ); +} + +function VideoCardSkeleton() { + return ( +
+ +
+ + +
+
+ ); +} + +export default function VideosPage() { + const { data, isLoading } = useQuery({ + queryKey: ["/api/videos"], + }); + + return ( +
+
+
+ {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+ ) : ( +
+ {(data?.items || []).map((video) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/server/routes.ts b/server/routes.ts index 1f7407f..194859f 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -6,6 +6,7 @@ import { seedDatabase } from "./seed"; import multer from "multer"; import path from "path"; import fs from "fs"; +import https from "https"; const uploadDir = path.join(process.cwd(), "client/public/uploads"); if (!fs.existsSync(uploadDir)) { @@ -106,5 +107,70 @@ export async function registerRoutes( res.json({ url }); }); + function bunnyFetch(path: string): Promise { + return new Promise((resolve, reject) => { + const options = { + hostname: "video.bunnycdn.com", + path, + method: "GET", + headers: { "AccessKey": process.env.BUNNY_API_KEY || "", "Accept": "application/json" }, + }; + const req = https.request(options, (res) => { + let body = ""; + res.on("data", (c: string) => (body += c)); + res.on("end", () => { + try { resolve(JSON.parse(body)); } catch { reject(new Error(body)); } + }); + }); + req.on("error", reject); + req.end(); + }); + } + + const LIBRARY_ID = process.env.BUNNY_LIBRARY_ID || "476412"; + const CDN_HOST = process.env.BUNNY_CDN_HOST || "vz-7982dfc4-cc8.b-cdn.net"; + + app.get("/api/videos", async (req, res) => { + try { + const page = parseInt(req.query.page as string) || 1; + const perPage = parseInt(req.query.perPage as string) || 20; + const search = req.query.search as string || ""; + let path = `/library/${LIBRARY_ID}/videos?page=${page}&itemsPerPage=${perPage}&orderBy=date`; + if (search) path += `&search=${encodeURIComponent(search)}`; + const data = await bunnyFetch(path); + const videos = (data.items || []).map((v: any) => ({ + guid: v.guid, + title: (v.title || "").replace(/\.mp4$/i, ""), + length: v.length, + dateUploaded: v.dateUploaded, + thumbnail: `https://${CDN_HOST}/${v.guid}/${v.thumbnailFileName || "thumbnail.jpg"}`, + embedUrl: `https://player.mediadelivery.net/embed/${LIBRARY_ID}/${v.guid}`, + hlsUrl: `https://${CDN_HOST}/${v.guid}/playlist.m3u8`, + status: v.status, + })); + res.json({ items: videos, totalItems: data.totalItems, currentPage: data.currentPage }); + } catch (err: any) { + res.status(500).json({ message: err.message }); + } + }); + + app.get("/api/videos/:guid", async (req, res) => { + try { + const data = await bunnyFetch(`/library/${LIBRARY_ID}/videos/${req.params.guid}`); + res.json({ + guid: data.guid, + title: (data.title || "").replace(/\.mp4$/i, ""), + length: data.length, + dateUploaded: data.dateUploaded, + thumbnail: `https://${CDN_HOST}/${data.guid}/${data.thumbnailFileName || "thumbnail.jpg"}`, + embedUrl: `https://player.mediadelivery.net/embed/${LIBRARY_ID}/${data.guid}`, + hlsUrl: `https://${CDN_HOST}/${data.guid}/playlist.m3u8`, + status: data.status, + }); + } catch (err: any) { + res.status(500).json({ message: err.message }); + } + }); + return httpServer; }