From 78b5327e59dde9e4a06e62306a0ff124b126b3b4 Mon Sep 17 00:00:00 2001
From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com>
Date: Fri, 8 Aug 2025 21:10:03 +0000
Subject: [PATCH] Add administration interface for managing videos and viewing
platform statistics
Implements a new /bunny-admin route with a dedicated page for video management and statistics, integrates a BunnyVideoModal for video playback and sharing, and adds backend API endpoints for Bunny.net statistics and video deletion.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/plFksdF
---
client/src/App.tsx | 2 +
client/src/components/bunny-video-modal.tsx | 287 ++++++++++++++++
client/src/components/search-header.tsx | 3 +
client/src/components/video-grid.tsx | 4 +-
client/src/pages/BunnyAdminPage.tsx | 346 ++++++++++++++++++++
server/bunny.ts | 19 +-
server/routes.ts | 33 ++
7 files changed, 688 insertions(+), 6 deletions(-)
create mode 100644 client/src/components/bunny-video-modal.tsx
create mode 100644 client/src/pages/BunnyAdminPage.tsx
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 3caf64e..77d7084 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import Home from "@/pages/home";
import Admin from "@/pages/admin";
+import BunnyAdminPage from "@/pages/BunnyAdminPage";
import NotFound from "@/pages/not-found";
function Router() {
@@ -12,6 +13,7 @@ function Router() {
+
);
diff --git a/client/src/components/bunny-video-modal.tsx b/client/src/components/bunny-video-modal.tsx
new file mode 100644
index 0000000..992d8ff
--- /dev/null
+++ b/client/src/components/bunny-video-modal.tsx
@@ -0,0 +1,287 @@
+import { useEffect, useState } from "react";
+import { X, Share2, Edit3 } from "lucide-react";
+import { type Video } from "@shared/schema";
+import { Button } from "@/components/ui/button";
+import { apiRequest } from "@/lib/queryClient";
+import {
+ FacebookIcon,
+ TwitterIcon,
+ WhatsappIcon
+} from "react-share";
+
+interface BunnyVideoModalProps {
+ video: Video | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onEdit?: () => void;
+}
+
+function formatDuration(seconds: number): string {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
+}
+
+function formatViews(views: number): string {
+ if (views >= 1000000) {
+ return `${(views / 1000000).toFixed(1)}M ogledov`;
+ } else if (views >= 1000) {
+ return `${(views / 1000).toFixed(1)}K ogledov`;
+ }
+ return `${views} ogledov`;
+}
+
+function formatDate(date: Date | string): string {
+ const now = new Date();
+ const createdDate = typeof date === 'string' ? new Date(date) : date;
+
+ if (!createdDate || isNaN(createdDate.getTime())) {
+ return "Neznano";
+ }
+
+ const diffTime = Math.abs(now.getTime() - createdDate.getTime());
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return "Danes";
+ if (diffDays === 1) return "Pred 1 dnem";
+ if (diffDays < 7) return `Pred ${diffDays} dnevi`;
+ if (diffDays < 30) return `Pred ${Math.floor(diffDays / 7)} tednom${Math.floor(diffDays / 7) > 1 ? 'a' : ''}`;
+ return `Pred ${Math.floor(diffDays / 30)} mesec${Math.floor(diffDays / 30) > 1 ? 'i' : 'em'}`;
+}
+
+export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: BunnyVideoModalProps) {
+ const [showShareMenu, setShowShareMenu] = useState(false);
+
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && isOpen) {
+ onClose();
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener("keydown", handleEscape);
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+
+ return () => {
+ document.removeEventListener("keydown", handleEscape);
+ document.body.style.overflow = "";
+ };
+ }, [isOpen, onClose]);
+
+ const handleVideoPlay = async () => {
+ if (video) {
+ try {
+ await apiRequest("POST", `/api/videos/${video.id}/view`);
+ } catch (error) {
+ console.error("Failed to track video view:", error);
+ }
+ }
+ };
+
+ const getShareUrl = () => {
+ if (!video?.id) return window.location.origin;
+ return `${window.location.origin}?video=${video.id}`;
+ };
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(getShareUrl());
+ const notification = document.createElement('div');
+ notification.textContent = 'Povezava kopirana!';
+ notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg z-50 transition-opacity duration-300';
+ document.body.appendChild(notification);
+ setTimeout(() => {
+ notification.style.opacity = '0';
+ setTimeout(() => document.body.removeChild(notification), 300);
+ }, 2000);
+ setShowShareMenu(false);
+ } catch (error) {
+ console.error('Failed to copy link:', error);
+ const textArea = document.createElement('textarea');
+ textArea.value = getShareUrl();
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ setShowShareMenu(false);
+ }
+ };
+
+ const handleBackdropClick = (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) {
+ onClose();
+ }
+ };
+
+ const shareOnFacebook = () => {
+ const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(getShareUrl())}&t=${encodeURIComponent(video?.title || '')}`;
+ window.open(url, 'facebook-share', 'width=600,height=400');
+ setShowShareMenu(false);
+ };
+
+ const shareOnTwitter = () => {
+ const text = `Poglej si "${video?.title}" na go4.video`;
+ const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(getShareUrl())}`;
+ window.open(url, 'twitter-share', 'width=600,height=400');
+ setShowShareMenu(false);
+ };
+
+ const shareOnWhatsApp = () => {
+ const text = `Poglej si "${video?.title}" na go4.video: ${getShareUrl()}`;
+ const url = `https://wa.me/?text=${encodeURIComponent(text)}`;
+ window.open(url, 'whatsapp-share', 'width=600,height=400');
+ setShowShareMenu(false);
+ };
+
+ if (!isOpen || !video) return null;
+
+ return (
+
+
+ {/* Header with close button */}
+
+
+ {video.title}
+
+
+ {onEdit && (
+
+ )}
+
+
+
+ {showShareMenu && (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Video player area */}
+
+ {/* Main video player */}
+
+
+ {video.videoUrlIframe ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Video info sidebar */}
+
+
+
+ {video.title}
+
+
+
+ {formatViews(video.views)}
+ {formatDuration(video.duration)}
+ {formatDate(video.createdAt)}
+
+
+ {video.category && (
+
+
+ {video.category}
+
+
+ )}
+
+ {video.description && (
+
+
+ {video.description}
+
+
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/search-header.tsx b/client/src/components/search-header.tsx
index cf7df83..53c4978 100644
--- a/client/src/components/search-header.tsx
+++ b/client/src/components/search-header.tsx
@@ -45,6 +45,9 @@ export default function SearchHeader({
Admin
+
+ Bunny Admin
+