fix: update Tailwind CSS version to v3 in project documentation and ensure compliance with v3 standards

This commit is contained in:
tigeren 2025-08-25 16:54:17 +00:00
parent 555a71ffc6
commit 50deee7f2a
6 changed files with 320 additions and 18 deletions

View File

@ -1,5 +1,6 @@
Project Description:
This is a nextjs project, basically a youtube like video sites.
the tailwindcss is v3 version. must ensure all the css related tailwind sytling code should comply with the v3 standard
Feature requirement:
1. Has a youtube like UI

View File

@ -1,5 +1,6 @@
Project Description:
This is a nextjs project, basically a youtube like video sites.
the tailwindcss is v3 version. must ensure all the css related tailwind sytling code should comply with the v3 standard
Feature requirement:
1. Has a youtube like UI

2
PRD.md
View File

@ -1,6 +1,6 @@
Project Description:
This is a nextjs project, basically a youtube like video sites.
the tailwindcss is v4 version. must ensure all the css related tailwind sytling code should comply with the v4 standard
the tailwindcss is v3 version. must ensure all the css related tailwind sytling code should comply with the v3 standard
Feature requirement:
1. Has a youtube like UI

View File

@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from "next/server";
import db from "@/db";
import fs from "fs";
import path from "path";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const videoId = parseInt(id);
const video = db.prepare("SELECT * FROM media WHERE id = ? AND type = 'video'").get(videoId) as { path: string } | undefined;
if (!video) {
return NextResponse.json({ error: "Video not found" }, { status: 404 });
}
const videoPath = video.path;
if (!fs.existsSync(videoPath)) {
return NextResponse.json({ error: "Video file not found" }, { status: 404 });
}
const stat = fs.statSync(videoPath);
const fileSize = stat.size;
const range = request.headers.get("range");
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = end - start + 1;
const file = fs.createReadStream(videoPath, { start, end });
const headers = new Headers({
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": chunksize.toString(),
"Content-Type": "video/mp4",
});
return new Response(file as any, {
status: 206,
headers,
});
} else {
const headers = new Headers({
"Content-Length": fileSize.toString(),
"Content-Type": "video/mp4",
});
const file = fs.createReadStream(videoPath);
return new Response(file as any, {
status: 200,
headers,
});
}
} catch (error) {
console.error("Error streaming video:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

View File

@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
@ -7,6 +6,7 @@ import { Film, Play, Clock, HardDrive, Search, Filter } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import VideoPlayer from "@/components/video-player";
interface Video {
id: number;
@ -20,6 +20,8 @@ const VideosPage = () => {
const [videos, setVideos] = useState<Video[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
const [isPlayerOpen, setIsPlayerOpen] = useState(false);
useEffect(() => {
fetchVideos();
@ -50,6 +52,16 @@ const VideosPage = () => {
video.path.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleVideoClick = (video: Video) => {
setSelectedVideo(video);
setIsPlayerOpen(true);
};
const handleClosePlayer = () => {
setIsPlayerOpen(false);
setSelectedVideo(null);
};
if (loading) {
return (
<div className="min-h-screen p-6">
@ -68,8 +80,9 @@ const VideosPage = () => {
}
return (
<div className="min-h-screen p-6">
<div className="max-w-7xl mx-auto">
<>
<div className="min-h-screen p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-4 mb-4">
@ -108,7 +121,11 @@ const VideosPage = () => {
{filteredVideos.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-6">
{filteredVideos.map((video) => (
<Card key={video.id} className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer border-border overflow-hidden">
<Card
key={video.id}
className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer border-border overflow-hidden"
onClick={() => handleVideoClick(video)}
>
<div className="aspect-video relative overflow-hidden bg-muted">
<img
src={video.thumbnail || "/placeholder.svg"}
@ -158,12 +175,8 @@ const VideosPage = () => {
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
<Search className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">
No videos found
</h3>
<p className="text-muted-foreground mb-4">
Try adjusting your search terms
</p>
<h3 className="text-xl font-semibold text-foreground mb-2">No videos found</h3>
<p className="text-muted-foreground mb-4">Try adjusting your search terms</p>
<Button
variant="outline"
onClick={() => setSearchTerm("")}
@ -178,12 +191,8 @@ const VideosPage = () => {
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
<Film className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">
No Videos Found
</h3>
<p className="text-muted-foreground mb-6">
Add media libraries and scan for videos to get started
</p>
<h3 className="text-xl font-semibold text-foreground mb-2">No Videos Found</h3>
<p className="text-muted-foreground mb-6">Add media libraries and scan for videos to get started</p>
<Link href="/settings">
<Button>
<Film className="h-4 w-4 mr-2" />
@ -195,7 +204,17 @@ const VideosPage = () => {
)}
</div>
</div>
{/* Video Player Modal */}
{selectedVideo && (
<VideoPlayer
video={selectedVideo}
isOpen={isPlayerOpen}
onClose={handleClosePlayer}
/>
)}
</>
);
};
export default VideosPage;
export default VideosPage;

View File

@ -0,0 +1,218 @@
"use client";
import { useState, useRef, useEffect } from 'react';
import { X, Play, Pause, Maximize, Minimize, Volume2, VolumeX } from 'lucide-react';
interface VideoPlayerProps {
video: {
id: number;
title: string;
path: string;
size: number;
thumbnail: string;
};
isOpen: boolean;
onClose: () => void;
}
export default function VideoPlayer({ video, isOpen, onClose }: VideoPlayerProps) {
const [isPlaying, setIsPlaying] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(1);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (isOpen && videoRef.current) {
videoRef.current.src = `/api/stream/${video.id}`;
videoRef.current.load();
}
}, [isOpen, video.id]);
const handlePlayPause = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const handleFullscreen = () => {
if (videoRef.current) {
if (!isFullscreen) {
videoRef.current.requestFullscreen();
} else {
document.exitFullscreen();
}
}
};
const handleMute = () => {
if (videoRef.current) {
videoRef.current.muted = !isMuted;
setIsMuted(!isMuted);
}
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (videoRef.current) {
const newVolume = parseFloat(e.target.value);
videoRef.current.volume = newVolume;
setVolume(newVolume);
setIsMuted(newVolume === 0);
}
};
const handleTimeUpdate = () => {
if (videoRef.current) {
setCurrentTime(videoRef.current.currentTime);
}
};
const handleLoadedMetadata = () => {
if (videoRef.current) {
setDuration(videoRef.current.duration);
}
};
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (videoRef.current) {
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const newTime = (clickX / rect.width) * duration;
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
}
};
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
if (e.key === ' ') {
e.preventDefault();
handlePlayPause();
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
}
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center">
<div className="relative w-full h-full max-w-7xl max-h-[90vh] mx-auto my-8">
{/* Close button */}
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 transition-colors"
>
<X className="h-6 w-6" />
</button>
{/* Video container */}
<div className="relative w-full h-full bg-black rounded-lg overflow-hidden">
<video
ref={videoRef}
className="w-full h-full object-contain"
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source src={`/api/stream/${video.id}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Title overlay */}
<div className="absolute top-0 left-0 right-0 bg-gradient-to-b from-black/60 to-transparent p-4">
<h2 className="text-white text-lg font-semibold">{video.title}</h2>
</div>
{/* Controls overlay */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent">
<div className="p-4 space-y-2">
{/* Progress bar */}
<div
className="relative h-1 bg-white/20 rounded-full cursor-pointer"
onClick={handleProgressClick}
>
<div
className="absolute h-full bg-white rounded-full"
style={{ width: `${(currentTime / duration) * 100 || 0}%` }}
/>
</div>
{/* Controls */}
<div className="flex items-center justify-between text-white">
<div className="flex items-center space-x-4">
<button
onClick={handlePlayPause}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
</button>
<div className="flex items-center space-x-2">
<button
onClick={handleMute}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
{isMuted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={handleVolumeChange}
className="w-20 h-1 bg-white/20 rounded-full appearance-none cursor-pointer"
/>
</div>
<span className="text-sm">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
<button
onClick={handleFullscreen}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
{isFullscreen ? <Minimize className="h-5 w-5" /> : <Maximize className="h-5 w-5" />}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}