Witaj @name! Czas stworzyć Captain's Cinema - piracką platformę wideo jak YouTube. 🎬
Piraci nagrywają swoje przygody, bitwy morskie i odkrycia skarbów. Nauczysz się jak budować platformę do dzielenia się filmami.
Twitter/Instagram:
YouTube:
1interface Video {
2 id: string;
3 title: string;
4 description: string;
5 thumbnail: string;
6 duration: number; // w sekundach
7 views: number;
8 likes: number;
9 dislikes: number;
10 uploadDate: Date;
11 channel: {
12 id: string;
13 name: string;
14 avatar: string;
15 subscribers: number;
16 };
17 tags: string[];
18 category: string;
19}W prawdziwej aplikacji używasz
<video> lub embedów (YouTube, Vimeo). My symulujemy:1interface VideoPlayerProps {
2 video: Video;
3}
4
5export default function VideoPlayer({ video }: VideoPlayerProps) {
6 const [isPlaying, setIsPlaying] = useState(false);
7 const [currentTime, setCurrentTime] = useState(0);
8 const [volume, setVolume] = useState(100);
9
10 return (
11 <div className="bg-black rounded-lg overflow-hidden">
12 {/* Video Area - symulowane */}
13 <div className="relative aspect-video bg-gradient-to-br from-slate-800 to-slate-900 flex items-center justify-center">
14 {!isPlaying ? (
15 <button
16 onClick={() => setIsPlaying(true)}
17 className="w-20 h-20 bg-red-600 rounded-full flex items-center justify-center text-white text-4xl hover:bg-red-700 transition"
18 >
19 ▶
20 </button>
21 ) : (
22 <div className="text-white text-center">
23 <div className="text-6xl mb-4">🎬</div>
24 <p className="text-2xl font-bold">{video.title}</p>
25 <p className="text-gray-400">Odtwarzanie...</p>
26 </div>
27 )}
28
29 {/* Overlay Controls */}
30 <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
31 {/* Progress Bar */}
32 <div className="mb-3">
33 <input
34 type="range"
35 min="0"
36 max={video.duration}
37 value={currentTime}
38 onChange={(e) => setCurrentTime(parseInt(e.target.value))}
39 className="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer"
40 />
41 <div className="flex justify-between text-xs text-white mt-1">
42 <span>{formatTime(currentTime)}</span>
43 <span>{formatTime(video.duration)}</span>
44 </div>
45 </div>
46
47 {/* Controls */}
48 <div className="flex items-center gap-4">
49 <button
50 onClick={() => setIsPlaying(!isPlaying)}
51 className="text-white text-2xl hover:scale-110 transition"
52 >
53 {isPlaying ? '⏸' : '▶'}
54 </button>
55
56 <div className="flex items-center gap-2">
57 <button className="text-white text-xl">🔊</button>
58 <input
59 type="range"
60 min="0"
61 max="100"
62 value={volume}
63 onChange={(e) => setVolume(parseInt(e.target.value))}
64 className="w-20 h-1 bg-gray-600 rounded-lg"
65 />
66 </div>
67
68 <div className="flex-1" />
69
70 <button className="text-white text-xl hover:scale-110 transition">
71 ⚙️
72 </button>
73 <button className="text-white text-xl hover:scale-110 transition">
74 ⛶
75 </button>
76 </div>
77 </div>
78 </div>
79 </div>
80 );
81}
82
83function formatTime(seconds: number): string {
84 const mins = Math.floor(seconds / 60);
85 const secs = seconds % 60;
86 return `${mins}:${secs.toString().padStart(2, '0')}`;
87}1function VideoThumbnail({ video }: { video: Video }) {
2 return (
3 <div className="group cursor-pointer">
4 {/* Thumbnail */}
5 <div className="relative aspect-video bg-gray-200 rounded-lg overflow-hidden mb-2">
6 <img
7 src={video.thumbnail}
8 alt={video.title}
9 className="w-full h-full object-cover group-hover:scale-105 transition-transform"
10 />
11
12 {/* Duration Badge */}
13 <div className="absolute bottom-2 right-2 bg-black bg-opacity-80 text-white text-xs px-2 py-1 rounded">
14 {formatTime(video.duration)}
15 </div>
16
17 {/* Play Overlay on Hover */}
18 <div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-opacity flex items-center justify-center">
19 <div className="opacity-0 group-hover:opacity-100 transition-opacity text-white text-4xl">
20 ▶
21 </div>
22 </div>
23 </div>
24
25 {/* Video Info */}
26 <div className="flex gap-3">
27 {/* Channel Avatar */}
28 <img
29 src={video.channel.avatar}
30 alt={video.channel.name}
31 className="w-9 h-9 rounded-full"
32 />
33
34 <div className="flex-1">
35 {/* Title */}
36 <h3 className="font-semibold text-sm line-clamp-2 mb-1">
37 {video.title}
38 </h3>
39
40 {/* Channel Name */}
41 <p className="text-xs text-gray-600 mb-1">
42 {video.channel.name}
43 </p>
44
45 {/* Views & Date */}
46 <p className="text-xs text-gray-600">
47 {formatViews(video.views)} views • {formatDate(video.uploadDate)}
48 </p>
49 </div>
50 </div>
51 </div>
52 );
53}
54
55function formatViews(count: number): string {
56 if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
57 if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
58 return count.toString();
59}
60
61function formatDate(date: Date): string {
62 const days = Math.floor((new Date().getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
63 if (days === 0) return 'today';
64 if (days === 1) return 'yesterday';
65 if (days < 7) return `${days} days ago`;
66 if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
67 if (days < 365) return `${Math.floor(days / 30)} months ago`;
68 return `${Math.floor(days / 365)} years ago`;
69}1export default function VideoGrid({ videos }: { videos: Video[] }) {
2 return (
3 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
4 {videos.map(video => (
5 <VideoThumbnail key={video.id} video={video} />
6 ))}
7 </div>
8 );
9}1export default function WatchPage({ video }: { video: Video }) {
2 const [isSubscribed, setIsSubscribed] = useState(false);
3
4 return (
5 <div className="max-w-7xl mx-auto p-4">
6 <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
7 {/* Main Column - Video Player */}
8 <div className="lg:col-span-2">
9 <VideoPlayer video={video} />
10
11 {/* Video Info */}
12 <div className="mt-4">
13 <h1 className="text-xl font-bold mb-2">{video.title}</h1>
14
15 <div className="flex items-center justify-between">
16 <div className="flex items-center gap-3">
17 <img
18 src={video.channel.avatar}
19 alt={video.channel.name}
20 className="w-12 h-12 rounded-full"
21 />
22 <div>
23 <h3 className="font-semibold">{video.channel.name}</h3>
24 <p className="text-sm text-gray-600">
25 {formatViews(video.channel.subscribers)} subscribers
26 </p>
27 </div>
28 <button
29 onClick={() => setIsSubscribed(!isSubscribed)}
30 className={`px-4 py-2 rounded-full font-semibold transition ${
31 isSubscribed
32 ? 'bg-gray-200 text-gray-700'
33 : 'bg-red-600 text-white hover:bg-red-700'
34 }`}
35 >
36 {isSubscribed ? 'Subscribed' : 'Subscribe'}
37 </button>
38 </div>
39
40 {/* Like/Dislike */}
41 <div className="flex items-center gap-2 bg-gray-100 rounded-full px-3 py-2">
42 <button className="flex items-center gap-1 hover:text-blue-600">
43 👍 {formatViews(video.likes)}
44 </button>
45 <div className="w-px h-6 bg-gray-300" />
46 <button className="flex items-center gap-1 hover:text-red-600">
47 👎
48 </button>
49 </div>
50 </div>
51
52 {/* Description */}
53 <div className="mt-4 bg-gray-100 rounded-lg p-4">
54 <p className="text-sm font-semibold mb-2">
55 {formatViews(video.views)} views • {formatDate(video.uploadDate)}
56 </p>
57 <p className="text-sm whitespace-pre-wrap">{video.description}</p>
58 </div>
59 </div>
60 </div>
61
62 {/* Sidebar - Related Videos */}
63 <div className="space-y-2">
64 <h2 className="font-bold mb-3">Related Videos</h2>
65 {/* Lista related videos */}
66 </div>
67 </div>
68 </div>
69 );
70}1function YouTubeEmbed({ videoId }: { videoId: string }) {
2 return (
3 <div className="relative aspect-video">
4 <iframe
5 src={`https://www.youtube.com/embed/${videoId}`}
6 title="YouTube video player"
7 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
8 allowFullScreen
9 className="absolute inset-0 w-full h-full"
10 />
11 </div>
12 );
13}
14
15// Użycie:
16<YouTubeEmbed videoId="dQw4w9WgXcQ" />✅ Video structure - title, thumbnail, duration, views ✅ Video player - controls, progress bar, volume ✅ formatTime - seconds → MM:SS ✅ Video thumbnail - hover effects, duration badge ✅ Video grid - responsive layout ✅ Watch page - player + info + related videos ✅ Subscribe button - toggle subscription ✅ Like/Dislike - engagement buttons ✅ YouTube embed - real video integration
W następnym ćwiczeniu dodamy playlists i channel pages!
Do zobaczenia! 🚀