summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor')
-rw-r--r--apps/web/src/components/editor/media-panel.tsx133
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx11
-rw-r--r--apps/web/src/components/editor/properties-panel.tsx100
-rw-r--r--apps/web/src/components/editor/timeline.tsx173
4 files changed, 417 insertions, 0 deletions
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/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
new file mode 100644
index 00000000..4361e967
--- /dev/null
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -0,0 +1,11 @@
+export function PreviewPanel() {
+ return (
+ <div className="h-full flex items-center justify-center">
+ <div className="aspect-video bg-black/90 w-full max-w-4xl mx-4 rounded-lg shadow-lg relative group">
+ <div className="absolute inset-0 flex items-center justify-center text-muted-foreground/50 group-hover:text-muted-foreground/80 transition-colors">
+ Drop media here or click to import
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/properties-panel.tsx b/apps/web/src/components/editor/properties-panel.tsx
new file mode 100644
index 00000000..280b6dad
--- /dev/null
+++ b/apps/web/src/components/editor/properties-panel.tsx
@@ -0,0 +1,100 @@
+"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";
+
+export function PropertiesPanel() {
+ return (
+ <ScrollArea className="h-full">
+ <div className="space-y-6 p-5">
+ {/* Transform */}
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium">Transform</h3>
+ <div className="space-y-2">
+ <div className="grid grid-cols-2 gap-2">
+ <div className="space-y-1">
+ <Label htmlFor="x">X Position</Label>
+ <Input id="x" type="number" defaultValue="0" />
+ </div>
+ <div className="space-y-1">
+ <Label htmlFor="y">Y Position</Label>
+ <Input id="y" type="number" defaultValue="0" />
+ </div>
+ </div>
+ <div className="space-y-1">
+ <Label htmlFor="rotation">Rotation</Label>
+ <Slider
+ id="rotation"
+ max={360}
+ step={1}
+ defaultValue={[0]}
+ className="mt-2"
+ />
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* Effects */}
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium">Effects</h3>
+ <div className="space-y-4">
+ <div className="space-y-1">
+ <Label htmlFor="opacity">Opacity</Label>
+ <Slider
+ id="opacity"
+ max={100}
+ step={1}
+ defaultValue={[100]}
+ className="mt-2"
+ />
+ </div>
+ <div className="space-y-1">
+ <Label htmlFor="blur">Blur</Label>
+ <Slider
+ id="blur"
+ max={20}
+ step={0.5}
+ defaultValue={[0]}
+ className="mt-2"
+ />
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* Timing */}
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium">Timing</h3>
+ <div className="space-y-2">
+ <div className="space-y-1">
+ <Label htmlFor="duration">Duration (seconds)</Label>
+ <Input
+ id="duration"
+ type="number"
+ min="0"
+ step="0.1"
+ defaultValue="5"
+ />
+ </div>
+ <div className="space-y-1">
+ <Label htmlFor="delay">Delay (seconds)</Label>
+ <Input
+ id="delay"
+ type="number"
+ min="0"
+ step="0.1"
+ defaultValue="0"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </ScrollArea>
+ );
+}
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx
new file mode 100644
index 00000000..da44022e
--- /dev/null
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -0,0 +1,173 @@
+"use client";
+
+import { ScrollArea } from "../ui/scroll-area";
+import { Button } from "../ui/button";
+import {
+ Scissors,
+ ArrowLeftToLine,
+ ArrowRightToLine,
+ Trash2,
+ Snowflake,
+ Copy,
+ SplitSquareHorizontal,
+} from "lucide-react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+ TooltipProvider,
+} 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={500}>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <Scissors className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Split clip (S)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <ArrowLeftToLine className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Split and keep left (A)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <ArrowRightToLine className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Split and keep right (D)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <SplitSquareHorizontal className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Separate audio (E)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <Copy className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Duplicate clip (Ctrl+D)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <Snowflake className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Freeze frame (F)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Delete clip (Delete)</TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </div>
+
+ {/* Tracks Area */}
+ <ScrollArea className="flex-1">
+ <div className="min-w-[800px]">
+ {/* Time Markers */}
+ <div className="py-2 pt-1 flex items-center">
+ {Array.from({ length: 16 }).map((_, i) => (
+ <div
+ key={i}
+ className="w-[50px] flex items-end justify-center text-xs text-muted-foreground"
+ >
+ {i}s
+ </div>
+ ))}
+ </div>
+
+ {/* 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>
+ ) : (
+ <div className="flex flex-col gap-2.5">
+ {tracks.map((track) => (
+ <TimelineTrackComponent key={track.id} track={track} />
+ ))}
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+ </div>
+ );
+}
+
+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 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>
+ );
+}