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