diff options
Diffstat (limited to 'apps/web/src/components/editor')
| -rw-r--r-- | apps/web/src/components/editor/media-panel.tsx | 133 | ||||
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 11 | ||||
| -rw-r--r-- | apps/web/src/components/editor/properties-panel.tsx | 100 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 173 |
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>
+ );
+}
|
