feat: optimize bundle size and improve image loading

- Unify video page components into single VideoPageContent component
  - Reduces ~2300 lines of duplicated code between dashboard and watch pages
  - Uses mode prop ('dashboard' | 'watch') to handle differences

- Move shadcn to devDependencies (CLI tool, not needed at runtime)

- Remove radix-ui meta-package (components already imported individually)

- Replace <img> tags with next/image for automatic optimization
  - Add remotePatterns config for YouTube, Vimeo, Unsplash domains
  - Update video-card.tsx, videos/new/page.tsx, component-example.tsx

- Add next/dynamic for KeyboardShortcutsModal (lazy load on demand)
This commit is contained in:
Yusuf İpek
2026-02-10 14:08:52 +03:00
parent 062a0aec83
commit b22fd1476f
9 changed files with 2364 additions and 4429 deletions
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
import { ArrowLeft, Loader2, Link as LinkIcon, AlertCircle, CheckCircle2 } from 'lucide-react'; import { ArrowLeft, Loader2, Link as LinkIcon, AlertCircle, CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -177,10 +178,12 @@ export default function NewVideoPage() {
<div className="space-y-2"> <div className="space-y-2">
<Label>Preview</Label> <Label>Preview</Label>
<div className="relative aspect-video rounded-lg overflow-hidden bg-muted"> <div className="relative aspect-video rounded-lg overflow-hidden bg-muted">
<img <Image
src={thumbnailUrl} src={thumbnailUrl}
alt="Video thumbnail" alt="Video thumbnail"
className="object-cover w-full h-full" fill
sizes="(max-width: 768px) 100vw, 600px"
className="object-cover"
/> />
</div> </div>
</div> </div>
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,6 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import Image from "next/image"
import { import {
Example, Example,
@@ -81,10 +82,12 @@ function CardExample() {
<Example title="Card" className="items-center justify-center"> <Example title="Card" className="items-center justify-center">
<Card className="relative w-full max-w-sm overflow-hidden pt-0"> <Card className="relative w-full max-w-sm overflow-hidden pt-0">
<div className="bg-primary absolute inset-0 z-30 aspect-video opacity-50 mix-blend-color" /> <div className="bg-primary absolute inset-0 z-30 aspect-video opacity-50 mix-blend-color" />
<img <Image
src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Photo by mymind on Unsplash" alt="Photo by mymind on Unsplash"
title="Photo by mymind on Unsplash" title="Photo by mymind on Unsplash"
width={400}
height={225}
className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale" className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale"
/> />
<CardHeader> <CardHeader>
+6 -1
View File
@@ -2,6 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import dynamic from 'next/dynamic';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { import {
Video, Video,
@@ -25,9 +26,13 @@ import {
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { ThemeToggle } from '@/components/theme-toggle'; import { ThemeToggle } from '@/components/theme-toggle';
import { KeyboardShortcutsModal } from '@/components/keyboard-shortcuts-modal';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const KeyboardShortcutsModal = dynamic(
() => import('@/components/keyboard-shortcuts-modal').then(mod => mod.KeyboardShortcutsModal),
{ ssr: false }
);
interface NavItem { interface NavItem {
href: string; href: string;
label: string; label: string;
+5 -2
View File
@@ -2,6 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
Play, Play,
@@ -178,10 +179,12 @@ export function VideoCard({ video, projectId }: VideoCardProps) {
<Link href={`/projects/${projectId}/videos/${video.id}`}> <Link href={`/projects/${projectId}/videos/${video.id}`}>
{/* Thumbnail */} {/* Thumbnail */}
<div className="relative aspect-video bg-muted overflow-hidden"> <div className="relative aspect-video bg-muted overflow-hidden">
<img <Image
src={video.thumbnailUrl} src={video.thumbnailUrl}
alt={video.title} alt={video.title}
className="object-cover w-full h-full transition-transform group-hover:scale-105" fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform group-hover:scale-105"
/> />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"> <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Play className="h-12 w-12 text-white" fill="white" /> <Play className="h-12 w-12 text-white" fill="white" />
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -1,7 +1,15 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ images: {
remotePatterns: [
{ protocol: 'https', hostname: 'img.youtube.com' },
{ protocol: 'https', hostname: 'i.ytimg.com' },
{ protocol: 'https', hostname: 'i.vimeocdn.com' },
{ protocol: 'https', hostname: 'vumbnail.com' },
{ protocol: 'https', hostname: 'images.unsplash.com' },
],
},
}; };
export default nextConfig; export default nextConfig;
+1 -2
View File
@@ -35,10 +35,8 @@
"nodemailer": "^8.0.1", "nodemailer": "^8.0.1",
"pg": "^8.18.0", "pg": "^8.18.0",
"prisma": "^7.3.0", "prisma": "^7.3.0",
"radix-ui": "^1.4.3",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"shadcn": "^3.8.3",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
@@ -54,6 +52,7 @@
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"shadcn": "^3.8.3",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
}, },