diff options
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/app-provider.tsx | 42 | ||||
| -rw-r--r-- | apps/web/src/components/editor-header.tsx | 56 | ||||
| -rw-r--r-- | apps/web/src/components/editor/media-panel.tsx | 133 | ||||
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx (renamed from apps/web/src/components/preview-panel.tsx) | 0 | ||||
| -rw-r--r-- | apps/web/src/components/editor/properties-panel.tsx (renamed from apps/web/src/components/properties-panel.tsx) | 10 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx (renamed from apps/web/src/components/timeline.tsx) | 84 | ||||
| -rw-r--r-- | apps/web/src/components/header-base.tsx | 41 | ||||
| -rw-r--r-- | apps/web/src/components/header.tsx | 37 | ||||
| -rw-r--r-- | apps/web/src/components/landing/hero.tsx | 72 | ||||
| -rw-r--r-- | apps/web/src/components/media-panel.tsx | 39 |
10 files changed, 444 insertions, 70 deletions
diff --git a/apps/web/src/components/app-provider.tsx b/apps/web/src/components/app-provider.tsx new file mode 100644 index 00000000..257fc8fa --- /dev/null +++ b/apps/web/src/components/app-provider.tsx @@ -0,0 +1,42 @@ +"use client";
+
+import { useEffect } from "react";
+import { Loader2 } from "lucide-react";
+import { useAppStore } from "@/stores/app-store";
+import { usePanelStore } from "@/stores/panel-store";
+
+interface AppProviderProps {
+ children: React.ReactNode;
+}
+
+export function AppProvider({ children }: AppProviderProps) {
+ const { isInitializing, isPanelsReady, initializeApp } = useAppStore();
+ const { setInitialized } = usePanelStore();
+
+ useEffect(() => {
+ const initialize = async () => {
+ // Initialize the app
+ await initializeApp();
+
+ // Initialize panel store for future resize events
+ setInitialized();
+ };
+
+ initialize();
+ }, [initializeApp, setInitialized]);
+
+ // Show loading screen while initializing
+ if (isInitializing || !isPanelsReady) {
+ return (
+ <div className="h-screen w-screen flex items-center justify-center bg-background">
+ <div className="flex flex-col items-center gap-4">
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
+ <p className="text-sm text-muted-foreground">Loading editor...</p>
+ </div>
+ </div>
+ );
+ }
+
+ // App is ready, render children
+ return <>{children}</>;
+}
diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx new file mode 100644 index 00000000..f59193d0 --- /dev/null +++ b/apps/web/src/components/editor-header.tsx @@ -0,0 +1,56 @@ +"use client";
+
+import Link from "next/link";
+import { Button } from "./ui/button";
+import { ChevronLeft, Download } from "lucide-react";
+import { useProjectStore } from "@/stores/project-store";
+import { useMediaStore } from "@/stores/media-store";
+import { useTimelineStore } from "@/stores/timeline-store";
+import { HeaderBase } from "./header-base";
+
+export function EditorHeader() {
+ const { activeProject } = useProjectStore();
+ const { mediaItems } = useMediaStore();
+ const { tracks } = useTimelineStore();
+
+ const handleExport = () => {
+ // TODO: Implement export functionality
+ console.log("Export project");
+ };
+
+ const leftContent = (
+ <Link
+ href="/"
+ className="font-medium tracking-tight flex items-center gap-2 hover:opacity-80 transition-opacity"
+ >
+ <ChevronLeft className="h-4 w-4" />
+ <span className="text-sm">{activeProject?.name || "Loading..."}</span>
+ </Link>
+ );
+
+ const centerContent = (
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
+ <span>{mediaItems.length} media</span>
+ <span>•</span>
+ <span>{tracks.length} tracks</span>
+ </div>
+ );
+
+ const rightContent = (
+ <nav className="flex items-center gap-2">
+ <Button size="sm" onClick={handleExport}>
+ <Download className="h-4 w-4" />
+ <span className="text-sm">Export</span>
+ </Button>
+ </nav>
+ );
+
+ return (
+ <HeaderBase
+ leftContent={leftContent}
+ centerContent={centerContent}
+ rightContent={rightContent}
+ className="bg-background border-b"
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx new file mode 100644 index 00000000..1e5aa465 --- /dev/null +++ b/apps/web/src/components/editor/media-panel.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { Button } from "../ui/button"; +import { AspectRatio } from "../ui/aspect-ratio"; +import { useMediaStore } from "@/stores/media-store"; +import { Plus, Image, Video, Music, Upload } from "lucide-react"; +import { useState, useRef } from "react"; + +export function MediaPanel() { + const { mediaItems, addMediaItem } = useMediaStore(); + const [isDragOver, setIsDragOver] = useState(false); + const dragCounterRef = useRef(0); + + const handleAddSampleMedia = () => { + // Just for testing - add a sample media item + addMediaItem({ + name: `Sample ${mediaItems.length + 1}`, + type: "image", + }); + }; + + const getMediaIcon = (type: string) => { + switch (type) { + case "video": + return <Video className="h-4 w-4" />; + case "audio": + return <Music className="h-4 w-4" />; + default: + return <Image className="h-4 w-4" />; + } + }; + + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + dragCounterRef.current += 1; + if (!isDragOver) { + setIsDragOver(true); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + dragCounterRef.current -= 1; + if (dragCounterRef.current === 0) { + setIsDragOver(false); + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + dragCounterRef.current = 0; + // TODO: Handle file drop functionality + }; + + return ( + <div + className={`h-full overflow-y-auto transition-colors duration-200 relative ${ + isDragOver ? "bg-accent/30 border-accent" : "" + }`} + onDragEnter={handleDragEnter} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + > + {/* Drag Overlay */} + {isDragOver && ( + <div className="absolute inset-0 bg-accent/20 backdrop-blur-lg border-2 border-dashed border-accent rounded-lg flex items-center justify-center z-10 pointer-events-none"> + <div className="text-center"> + <Upload className="h-8 w-8 text-accent mx-auto mb-2" /> + <p className="text-sm font-medium text-accent">Drop files here</p> + <p className="text-xs text-muted-foreground"> + Images, videos, and audio files + </p> + </div> + </div> + )} + + <div className="space-y-4 p-2 h-full"> + {/* Media Grid */} + {mediaItems.length === 0 ? ( + <EmptyMedia onAddSample={handleAddSampleMedia} /> + ) : ( + <div className="grid grid-cols-2 gap-2"> + {mediaItems.map((item) => ( + <Button + key={item.id} + variant="outline" + className="flex flex-col gap-2 p-2 h-auto overflow-hidden" + > + <AspectRatio ratio={16 / 9} className="w-full"> + <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground"> + {getMediaIcon(item.type)} + <span className="text-xs mt-1 truncate max-w-full px-1"> + {item.name} + </span> + </div> + </AspectRatio> + </Button> + ))} + </div> + )} + </div> + </div> + ); +} + +function EmptyMedia({ onAddSample }: { onAddSample: () => void }) { + return ( + <div className="flex flex-col items-center justify-center py-8 text-center h-full"> + <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> + <Image className="h-8 w-8 text-muted-foreground" /> + </div> + <p className="text-sm text-muted-foreground">No media in project</p> + <p className="text-xs text-muted-foreground/70 mt-1"> + Drag files or click to add media + </p> + <Button + variant="outline" + size="sm" + className="mt-4" + onClick={onAddSample} + > + <Plus className="h-4 w-4 mr-2" /> + Add Sample + </Button> + </div> + ); +} diff --git a/apps/web/src/components/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 4361e967..4361e967 100644 --- a/apps/web/src/components/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx diff --git a/apps/web/src/components/properties-panel.tsx b/apps/web/src/components/editor/properties-panel.tsx index 612e2ef0..280b6dad 100644 --- a/apps/web/src/components/properties-panel.tsx +++ b/apps/web/src/components/editor/properties-panel.tsx @@ -1,10 +1,10 @@ "use client";
-import { Input } from "./ui/input";
-import { Label } from "./ui/label";
-import { Slider } from "./ui/slider";
-import { ScrollArea } from "./ui/scroll-area";
-import { Separator } from "./ui/separator";
+import { Input } from "../ui/input";
+import { Label } from "../ui/label";
+import { Slider } from "../ui/slider";
+import { ScrollArea } from "../ui/scroll-area";
+import { Separator } from "../ui/separator";
export function PropertiesPanel() {
return (
diff --git a/apps/web/src/components/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index cde982ae..da44022e 100644 --- a/apps/web/src/components/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1,7 +1,7 @@ "use client";
-import { ScrollArea } from "./ui/scroll-area";
-import { Button } from "./ui/button";
+import { ScrollArea } from "../ui/scroll-area";
+import { Button } from "../ui/button";
import {
Scissors,
ArrowLeftToLine,
@@ -16,14 +16,17 @@ import { TooltipContent,
TooltipTrigger,
TooltipProvider,
-} from "./ui/tooltip";
+} from "../ui/tooltip";
+import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store";
export function Timeline() {
+ const { tracks, addTrack } = useTimelineStore();
+
return (
<div className="h-full flex flex-col">
{/* Toolbar */}
<div className="border-b flex items-center px-2 py-1 gap-1">
- <TooltipProvider delayDuration={0}>
+ <TooltipProvider delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
@@ -104,38 +107,67 @@ export function Timeline() { ))}
</div>
- {/* Video Track */}
- <div className="flex flex-col gap-2.5">
- <div className="flex items-center px-2">
- <div className="flex-1">
- <TimelineClip />
+ {/* Timeline Tracks */}
+ {tracks.length === 0 ? (
+ <div className="flex flex-col items-center justify-center py-12 text-center">
+ <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
+ <SplitSquareHorizontal className="h-8 w-8 text-muted-foreground" />
</div>
+ <p className="text-sm text-muted-foreground">
+ No tracks in timeline
+ </p>
+ <p className="text-xs text-muted-foreground/70 mt-1">
+ Add a video or audio track to get started
+ </p>
</div>
-
- {/* Audio Track */}
- <div className="flex items-center px-2">
- <div className="flex-1">
- <TimelineClip />
- </div>
+ ) : (
+ <div className="flex flex-col gap-2.5">
+ {tracks.map((track) => (
+ <TimelineTrackComponent key={track.id} track={track} />
+ ))}
</div>
-
- {/* Effects Track */}
- <div className="flex items-center px-2">
- <div className="flex-1">
- <TimelineClip />
- </div>
- </div>
- </div>
+ )}
</div>
</ScrollArea>
</div>
);
}
-function TimelineClip() {
+function TimelineTrackComponent({ track }: { track: TimelineTrack }) {
+ const getTrackColor = (type: string) => {
+ switch (type) {
+ case "video":
+ return "bg-blue-500/20 border-blue-500/30";
+ case "audio":
+ return "bg-green-500/20 border-green-500/30";
+ case "effects":
+ return "bg-purple-500/20 border-purple-500/30";
+ default:
+ return "bg-gray-500/20 border-gray-500/30";
+ }
+ };
+
return (
- <div className="flex-1 h-[3.8rem]">
- <div className="h-full bg-blue-500/20 border border-blue-500/30 rounded-sm mx-3 cursor-pointer transition-colors" />
+ <div className="flex items-center px-2">
+ <div className="w-24 text-xs text-muted-foreground flex-shrink-0 mr-2">
+ {track.name}
+ </div>
+
+ <div className="flex-1 h-[60px]">
+ {track.clips.length === 0 ? (
+ <div className="h-full rounded-sm border-2 border-dashed border-muted/30 flex items-center justify-center text-xs text-muted-foreground">
+ Drop media here
+ </div>
+ ) : (
+ <div
+ className={`h-full rounded-sm border cursor-pointer transition-colors ${getTrackColor(track.type)} flex items-center px-2`}
+ >
+ <span className="text-xs text-foreground/80">
+ {track.clips.length} clip{track.clips.length !== 1 ? "s" : ""}
+ </span>
+ </div>
+ )}
+ </div>
</div>
);
}
diff --git a/apps/web/src/components/header-base.tsx b/apps/web/src/components/header-base.tsx new file mode 100644 index 00000000..5f7b2b8d --- /dev/null +++ b/apps/web/src/components/header-base.tsx @@ -0,0 +1,41 @@ +"use client";
+
+import { cn } from "@/lib/utils";
+import { ReactNode } from "react";
+
+interface HeaderBaseProps {
+ leftContent?: ReactNode;
+ centerContent?: ReactNode;
+ rightContent?: ReactNode;
+ className?: string;
+ children?: ReactNode;
+}
+
+export function HeaderBase({
+ leftContent,
+ centerContent,
+ rightContent,
+ className,
+ children,
+}: HeaderBaseProps) {
+ // If children is provided, render it directly without the grid layout
+ if (children) {
+ return (
+ <header className={cn("px-6 h-16 flex items-center", className)}>
+ {children}
+ </header>
+ );
+ }
+
+ return (
+ <header
+ className={cn("px-6 h-16 flex justify-between items-center", className)}
+ >
+ {leftContent && <div className="flex items-center">{leftContent}</div>}
+ {centerContent && (
+ <div className="flex items-center">{centerContent}</div>
+ )}
+ {rightContent && <div className="flex items-center">{rightContent}</div>}
+ </header>
+ );
+}
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx new file mode 100644 index 00000000..a2ac5016 --- /dev/null +++ b/apps/web/src/components/header.tsx @@ -0,0 +1,37 @@ +"use client";
+
+import Link from "next/link";
+import { Button } from "./ui/button";
+import { ArrowRight } from "lucide-react";
+import { HeaderBase } from "./header-base";
+
+export function Header() {
+ const leftContent = (
+ <Link href="/" className="font-medium tracking-tight">
+ AppCut
+ </Link>
+ );
+
+ const rightContent = (
+ <nav className="flex items-center">
+ <Link href="/editor">
+ <Button variant="ghost" className="text-sm">
+ Open editor
+ </Button>
+ </Link>
+ <Link href="https://github.com/mazeincoding/AppCut" target="_blank">
+ <Button variant="ghost" className="text-sm">
+ GitHub
+ </Button>
+ </Link>
+ <Link href="/editor">
+ <Button size="sm" className="text-sm ml-4">
+ Start editing
+ <ArrowRight className="h-4 w-4" />
+ </Button>
+ </Link>
+ </nav>
+ );
+
+ return <HeaderBase leftContent={leftContent} rightContent={rightContent} />;
+}
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx new file mode 100644 index 00000000..8821cb71 --- /dev/null +++ b/apps/web/src/components/landing/hero.tsx @@ -0,0 +1,72 @@ +"use client";
+
+import { motion } from "motion/react";
+import { Button } from "../ui/button";
+import { ArrowRight } from "lucide-react";
+import Link from "next/link";
+
+export function Hero() {
+ return (
+ <div className="relative min-h-screen flex flex-col items-center justify-center text-center px-4">
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ duration: 1 }}
+ className="max-w-3xl mx-auto"
+ >
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.2, duration: 0.8 }}
+ className="inline-block"
+ >
+ <h1 className="text-5xl sm:text-7xl font-bold tracking-tighter">
+ The open source
+ </h1>
+ <h1 className="text-5xl sm:text-7xl font-bold tracking-tighter mt-2">
+ CapCut alternative.
+ </h1>
+ </motion.div>
+
+ <motion.p
+ className="mt-12 text-lg sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.4, duration: 0.8 }}
+ >
+ A simple but powerful video editor that gets the job done. In your
+ browser.
+ </motion.p>
+
+ <motion.div
+ className="mt-12 flex gap-8 justify-center"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.6, duration: 0.8 }}
+ >
+ <Link href="/editor">
+ <Button size="lg" className="px-6 h-11 text-base">
+ <span className="relative z-10">Start editing</span>
+ <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
+ </Button>
+ </Link>
+ </motion.div>
+ </motion.div>
+
+ <motion.div
+ className="absolute bottom-12 left-0 right-0 text-center text-sm text-muted-foreground/60"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.8, duration: 0.8 }}
+ >
+ Currently in beta • Open source on{" "}
+ <Link
+ href="https://github.com/mazeincoding/AppCut"
+ className="text-foreground underline"
+ >
+ GitHub
+ </Link>
+ </motion.div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/media-panel.tsx b/apps/web/src/components/media-panel.tsx deleted file mode 100644 index 10fc0fb5..00000000 --- a/apps/web/src/components/media-panel.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client";
-
-import { Button } from "./ui/button";
-import { ScrollArea } from "./ui/scroll-area";
-import { AspectRatio } from "./ui/aspect-ratio";
-
-const mediaItems = [
- { name: "Sample 1", type: "image" },
- { name: "Sample 2", type: "image" },
- { name: "Sample 3", type: "image" },
- { name: "Sample 4", type: "image" },
- { name: "Sample 5", type: "image" },
- { name: "Sample 6", type: "image" },
-] as const;
-
-export function MediaPanel() {
- return (
- <ScrollArea className="h-full">
- <div className="space-y-4 p-2">
- {/* Media Grid */}
- <div className="grid grid-cols-2 gap-2">
- {mediaItems.map((item) => (
- <Button
- key={item.name}
- variant="outline"
- className="flex flex-col gap-2 p-0 h-auto overflow-hidden"
- >
- <AspectRatio ratio={16 / 9} className="w-full">
- <div className="w-full h-full bg-muted/30 flex items-center justify-center text-muted-foreground text-xs">
- {item.name}
- </div>
- </AspectRatio>
- </Button>
- ))}
- </div>
- </div>
- </ScrollArea>
- );
-}
|
