diff options
Diffstat (limited to 'apps/web/src/app')
| -rw-r--r-- | apps/web/src/app/editor/[project_id]/page.tsx (renamed from apps/web/src/app/editor/page.tsx) | 14 | ||||
| -rw-r--r-- | apps/web/src/app/layout.tsx | 3 | ||||
| -rw-r--r-- | apps/web/src/app/projects/page.tsx | 488 |
3 files changed, 270 insertions, 235 deletions
diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index e0013937..3970d72a 100644 --- a/apps/web/src/app/editor/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -1,16 +1,15 @@ "use client"; import { useEffect } from "react"; -import "./editor.css"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, -} from "../../components/ui/resizable"; -import { MediaPanel } from "../../components/editor/media-panel"; +} from "../../../components/ui/resizable"; +import { MediaPanel } from "../../../components/editor/media-panel"; // import { PropertiesPanel } from "../../components/editor/properties-panel"; -import { Timeline } from "../../components/editor/timeline"; -import { PreviewPanel } from "../../components/editor/preview-panel"; +import { Timeline } from "../../../components/editor/timeline"; +import { PreviewPanel } from "../../../components/editor/preview-panel"; import { EditorHeader } from "@/components/editor-header"; import { usePanelStore } from "@/stores/panel-store"; import { useProjectStore } from "@/stores/project-store"; @@ -55,7 +54,10 @@ export default function Editor() { className="min-h-0" > {/* Main content area */} - <ResizablePanelGroup direction="horizontal" className="h-full w-full"> + <ResizablePanelGroup + direction="horizontal" + className="h-full w-full" + > {/* Tools Panel */} <ResizablePanel defaultSize={toolsPanel} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 8188d31b..4f8243b5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -6,6 +6,7 @@ import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; import { DevelopmentDebug } from "../components/development-debug"; +import { StorageProvider } from "../components/storage-provider"; import { baseMetaData } from "./metadata"; const inter = Inter({ @@ -25,7 +26,7 @@ export default function RootLayout({ <body className={`${inter.variable} font-sans antialiased`}> <ThemeProvider attribute="class" forcedTheme="dark" enableSystem> <TooltipProvider> - {children} + <StorageProvider>{children}</StorageProvider> <Analytics /> <Toaster /> <DevelopmentDebug /> diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 82bae5d1..c983894d 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -1,228 +1,260 @@ -"use client";
-
-import Link from "next/link";
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- ChevronLeft,
- Plus,
- Calendar,
- MoreHorizontal,
- Video,
-} from "lucide-react";
-import { TProject } from "@/types/project";
-import Image from "next/image";
-import {
- DropdownMenu,
- DropdownMenuItem,
- DropdownMenuContent,
- DropdownMenuTrigger,
- DropdownMenuSeparator,
-} from "@/components/ui/dropdown-menu";
-
-// Hard-coded project data
-const mockProjects: TProject[] = [
- {
- id: "1",
- name: "Summer Vacation Highlights",
- createdAt: new Date("2024-12-15"),
- updatedAt: new Date("2024-12-20"),
- thumbnail:
- "https://plus.unsplash.com/premium_photo-1750854354243-81f40af63a73?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
- },
- {
- id: "2",
- name: "Product Demo Video",
- createdAt: new Date("2024-12-10"),
- updatedAt: new Date("2024-12-18"),
- thumbnail:
- "https://images.unsplash.com/photo-1750875936215-0c35c1742cd6?q=80&w=688&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
- },
- {
- id: "3",
- name: "Wedding Ceremony Edit",
- createdAt: new Date("2024-12-05"),
- updatedAt: new Date("2024-12-16"),
- thumbnail:
- "https://images.unsplash.com/photo-1750967991618-7b64a3025381?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
- },
- {
- id: "4",
- name: "Travel Vlog - Japan",
- createdAt: new Date("2024-11-28"),
- updatedAt: new Date("2024-12-14"),
- thumbnail:
- "https://images.unsplash.com/photo-1750639258774-9a714379a093?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
- },
-];
-
-// Mock duration data (in seconds)
-const mockDurations: Record<string, number> = {
- "1": 245, // 4:05
- "2": 120, // 2:00
- "3": 1800, // 30:00
- "4": 780, // 13:00
- "5": 360, // 6:00
- "6": 180, // 3:00
-};
-
-export default function ProjectsPage() {
- return (
- <div className="min-h-screen bg-background">
- <div className="pt-6 px-6 flex items-center justify-between w-full h-16">
- <Link
- href="/"
- className="flex items-center gap-1 hover:text-muted-foreground transition-colors"
- >
- <ChevronLeft className="!size-5 shrink-0" />
- <span className="text-sm font-medium">Back</span>
- </Link>
- <div className="block md:hidden">
- <CreateButton />
- </div>
- </div>
- <main className="max-w-6xl mx-auto px-6 pt-6 pb-6">
- <div className="mb-8 flex items-center justify-between">
- <div className="flex flex-col gap-3">
- <h1 className="text-2xl md:text-3xl font-bold tracking-tight">
- Your Projects
- </h1>
- <p className="text-muted-foreground">
- {mockProjects.length}{" "}
- {mockProjects.length === 1 ? "project" : "projects"}
- </p>
- </div>
- <div className="hidden md:block">
- <CreateButton />
- </div>
- </div>
-
- {mockProjects.length === 0 ? (
- <div className="flex flex-col items-center justify-center py-16 text-center">
- <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
- <Video className="h-8 w-8 text-muted-foreground" />
- </div>
- <h3 className="text-lg font-medium mb-2">No projects yet</h3>
- <p className="text-muted-foreground mb-6 max-w-md">
- Start creating your first video project. Import media, edit, and
- export professional videos.
- </p>
- <Link href="/editor">
- <Button size="lg" className="gap-2">
- <Plus className="h-4 w-4" />
- Create Your First Project
- </Button>
- </Link>
- </div>
- ) : (
- <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
- {mockProjects.map((project, index) => (
- <ProjectCard key={project.id} project={project} />
- ))}
- </div>
- )}
- </main>
- </div>
- );
-}
-
-function ProjectCard({ project }: { project: TProject }) {
- const [isDropdownOpen, setIsDropdownOpen] = useState(false);
-
- const formatDuration = (seconds: number): string => {
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = Math.floor(seconds % 60);
- return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
- };
-
- const formatDate = (date: Date): string => {
- return date.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- });
- };
-
- return (
- <Link href={`/editor/${project.id}`} className="block group">
- <Card className="overflow-hidden bg-background border-none p-0">
- <div
- className={`relative aspect-square bg-muted transition-opacity ${
- isDropdownOpen ? "opacity-65" : "opacity-100 group-hover:opacity-65"
- }`}
- >
- {/* Thumbnail preview */}
- <div className="absolute inset-0">
- <Image
- src={project.thumbnail}
- alt="Project thumbnail"
- fill
- className="object-cover"
- />
- </div>
-
- {/* Duration badge */}
- <div className="absolute bottom-3 right-3 bg-background text-foreground text-xs px-2 py-1 rounded">
- {formatDuration(mockDurations[project.id] || 0)}
- </div>
- </div>
-
- <CardContent className="px-0 pt-5">
- <div className="flex items-start justify-between mb-2">
- <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2">
- {project.name}
- </h3>
- <DropdownMenu onOpenChange={setIsDropdownOpen}>
- <DropdownMenuTrigger asChild>
- <Button
- variant="text"
- size="sm"
- className={`size-6 p-0 transition-all shrink-0 ml-2 ${
- isDropdownOpen
- ? "opacity-100"
- : "opacity-0 group-hover:opacity-100"
- }`}
- onClick={(e) => e.preventDefault()}
- >
- <MoreHorizontal />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent
- align="end"
- onCloseAutoFocus={(e) => {
- e.preventDefault();
- e.stopPropagation();
- console.log("close");
- }}
- >
- <DropdownMenuItem>Rename</DropdownMenuItem>
- <DropdownMenuItem>Duplicate</DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem variant="destructive">
- Delete
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
-
- <div className="space-y-1">
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
- <Calendar className="!size-4" />
- <span>Created {formatDate(project.createdAt)}</span>
- </div>
- </div>
- </CardContent>
- </Card>
- </Link>
- );
-}
-
-function CreateButton() {
- return (
- <Button className="flex">
- <Plus className="!size-4" />
- <span className="text-sm font-medium">New project</span>
- </Button>
- );
-}
+"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + ChevronLeft, + Plus, + Calendar, + MoreHorizontal, + Video, + Loader2, +} from "lucide-react"; +import { TProject } from "@/types/project"; +import Image from "next/image"; +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { useProjectStore } from "@/stores/project-store"; +import { useRouter } from "next/navigation"; +import { DeleteProjectDialog } from "@/components/delete-project-dialog"; +import { RenameProjectDialog } from "@/components/rename-project-dialog"; + +export default function ProjectsPage() { + const { createNewProject, savedProjects, isLoading, isInitialized } = + useProjectStore(); + const router = useRouter(); + + const handleCreateProject = async () => { + const projectId = await createNewProject("New Project"); + console.log("projectId", projectId); + router.push(`/editor/${projectId}`); + }; + + return ( + <div className="min-h-screen bg-background"> + <div className="pt-6 px-6 flex items-center justify-between w-full h-16"> + <Link + href="/" + className="flex items-center gap-1 hover:text-muted-foreground transition-colors" + > + <ChevronLeft className="!size-5 shrink-0" /> + <span className="text-sm font-medium">Back</span> + </Link> + <div className="block md:hidden"> + <CreateButton onClick={handleCreateProject} /> + </div> + </div> + <main className="max-w-6xl mx-auto px-6 pt-6 pb-6"> + <div className="mb-8 flex items-center justify-between"> + <div className="flex flex-col gap-3"> + <h1 className="text-2xl md:text-3xl font-bold tracking-tight"> + Your Projects + </h1> + <p className="text-muted-foreground"> + {savedProjects.length}{" "} + {savedProjects.length === 1 ? "project" : "projects"} + </p> + </div> + <div className="hidden md:block"> + <CreateButton onClick={handleCreateProject} /> + </div> + </div> + + {isLoading || !isInitialized ? ( + <div className="flex items-center justify-center py-16"> + <Loader2 className="h-8 w-8 text-muted-foreground animate-spin" /> + </div> + ) : savedProjects.length === 0 ? ( + <NoProjects onCreateProject={handleCreateProject} /> + ) : ( + <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6"> + {savedProjects.map((project) => ( + <ProjectCard key={project.id} project={project} /> + ))} + </div> + )} + </main> + </div> + ); +} + +function ProjectCard({ project }: { project: TProject }) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const { deleteProject, renameProject, duplicateProject } = useProjectStore(); + + const formatDate = (date: Date): string => { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + + const handleDeleteProject = async () => { + await deleteProject(project.id); + setIsDropdownOpen(false); + }; + + const handleRenameProject = async (newName: string) => { + await renameProject(project.id, newName); + setIsRenameDialogOpen(false); + }; + + const handleDuplicateProject = async () => { + setIsDropdownOpen(false); + await duplicateProject(project.id); + }; + + return ( + <> + <Link href={`/editor/${project.id}`} className="block group"> + <Card className="overflow-hidden bg-background border-none p-0"> + <div + className={`relative aspect-square bg-muted transition-opacity ${ + isDropdownOpen + ? "opacity-65" + : "opacity-100 group-hover:opacity-65" + }`} + > + {/* Thumbnail preview or placeholder */} + <div className="absolute inset-0"> + {project.thumbnail ? ( + <Image + src={project.thumbnail} + alt="Project thumbnail" + fill + className="object-cover" + /> + ) : ( + <div className="w-full h-full bg-muted/50 flex items-center justify-center"> + <Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" /> + </div> + )} + </div> + </div> + + <CardContent className="px-0 pt-5 flex flex-col gap-1"> + <div className="flex items-start justify-between"> + <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2"> + {project.name} + </h3> + <DropdownMenu + open={isDropdownOpen} + onOpenChange={setIsDropdownOpen} + > + <DropdownMenuTrigger asChild> + <Button + variant="text" + size="sm" + className={`size-6 p-0 transition-all shrink-0 ml-2 ${ + isDropdownOpen + ? "opacity-100" + : "opacity-0 group-hover:opacity-100" + }`} + onClick={(e) => e.preventDefault()} + > + <MoreHorizontal /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent + align="end" + onCloseAutoFocus={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + > + <DropdownMenuItem + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropdownOpen(false); + setIsRenameDialogOpen(true); + }} + > + Rename + </DropdownMenuItem> + <DropdownMenuItem + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleDuplicateProject(); + }} + > + Duplicate + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + variant="destructive" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropdownOpen(false); + setIsDeleteDialogOpen(true); + }} + > + Delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> + + <div className="space-y-1"> + <div className="flex items-center gap-1.5 text-sm text-muted-foreground"> + <Calendar className="!size-4" /> + <span>Created {formatDate(project.createdAt)}</span> + </div> + </div> + </CardContent> + </Card> + </Link> + <DeleteProjectDialog + isOpen={isDeleteDialogOpen} + onOpenChange={setIsDeleteDialogOpen} + onConfirm={handleDeleteProject} + /> + <RenameProjectDialog + isOpen={isRenameDialogOpen} + onOpenChange={setIsRenameDialogOpen} + onConfirm={handleRenameProject} + projectName={project.name} + /> + </> + ); +} + +function CreateButton({ onClick }: { onClick?: () => void }) { + return ( + <Button className="flex" onClick={onClick}> + <Plus className="!size-4" /> + <span className="text-sm font-medium">New project</span> + </Button> + ); +} + +function NoProjects({ onCreateProject }: { onCreateProject: () => void }) { + return ( + <div className="flex flex-col items-center justify-center py-16 text-center"> + <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> + <Video className="h-8 w-8 text-muted-foreground" /> + </div> + <h3 className="text-lg font-medium mb-2">No projects yet</h3> + <p className="text-muted-foreground mb-6 max-w-md"> + Start creating your first video project. Import media, edit, and export + professional videos. + </p> + <Button size="lg" className="gap-2" onClick={onCreateProject}> + <Plus className="h-4 w-4" /> + Create Your First Project + </Button> + </div> + ); +} |
