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 (
+
+
+
+
+

+
+
+
+ {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;
}