feat: implement media panel with tab navigation
This commit is contained in:
56
apps/web/src/components/editor/media-panel/index.tsx
Normal file
56
apps/web/src/components/editor/media-panel/index.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { TabBar } from "./tabbar";
|
||||
import { MediaView } from "./views/media";
|
||||
import { useMediaPanelStore, Tab } from "./store";
|
||||
|
||||
export function MediaPanel() {
|
||||
const { activeTab } = useMediaPanelStore();
|
||||
|
||||
const viewMap: Record<Tab, React.ReactNode> = {
|
||||
media: <MediaView />,
|
||||
audio: (
|
||||
<div className="p-4 text-muted-foreground">Audio view coming soon...</div>
|
||||
),
|
||||
text: (
|
||||
<div className="p-4 text-muted-foreground">Text view coming soon...</div>
|
||||
),
|
||||
stickers: (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
Stickers view coming soon...
|
||||
</div>
|
||||
),
|
||||
effects: (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
Effects view coming soon...
|
||||
</div>
|
||||
),
|
||||
transitions: (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
Transitions view coming soon...
|
||||
</div>
|
||||
),
|
||||
captions: (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
Captions view coming soon...
|
||||
</div>
|
||||
),
|
||||
filters: (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
Filters view coming soon...
|
||||
</div>
|
||||
),
|
||||
adjustment: (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
Adjustment view coming soon...
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<TabBar />
|
||||
<div className="flex-1">{viewMap[activeTab]}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
73
apps/web/src/components/editor/media-panel/store.ts
Normal file
73
apps/web/src/components/editor/media-panel/store.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
CaptionsIcon,
|
||||
ArrowLeftRightIcon,
|
||||
SparklesIcon,
|
||||
StickerIcon,
|
||||
TextIcon,
|
||||
MusicIcon,
|
||||
VideoIcon,
|
||||
BlendIcon,
|
||||
SlidersHorizontalIcon,
|
||||
LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { create } from "zustand";
|
||||
|
||||
export type Tab =
|
||||
| "media"
|
||||
| "audio"
|
||||
| "text"
|
||||
| "stickers"
|
||||
| "effects"
|
||||
| "transitions"
|
||||
| "captions"
|
||||
| "filters"
|
||||
| "adjustment";
|
||||
|
||||
export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
|
||||
media: {
|
||||
icon: VideoIcon,
|
||||
label: "Media",
|
||||
},
|
||||
audio: {
|
||||
icon: MusicIcon,
|
||||
label: "Audio",
|
||||
},
|
||||
text: {
|
||||
icon: TextIcon,
|
||||
label: "Text",
|
||||
},
|
||||
stickers: {
|
||||
icon: StickerIcon,
|
||||
label: "Stickers",
|
||||
},
|
||||
effects: {
|
||||
icon: SparklesIcon,
|
||||
label: "Effects",
|
||||
},
|
||||
transitions: {
|
||||
icon: ArrowLeftRightIcon,
|
||||
label: "Transitions",
|
||||
},
|
||||
captions: {
|
||||
icon: CaptionsIcon,
|
||||
label: "Captions",
|
||||
},
|
||||
filters: {
|
||||
icon: BlendIcon,
|
||||
label: "Filters",
|
||||
},
|
||||
adjustment: {
|
||||
icon: SlidersHorizontalIcon,
|
||||
label: "Adjustment",
|
||||
},
|
||||
};
|
||||
|
||||
interface MediaPanelStore {
|
||||
activeTab: Tab;
|
||||
setActiveTab: (tab: Tab) => void;
|
||||
}
|
||||
|
||||
export const useMediaPanelStore = create<MediaPanelStore>((set) => ({
|
||||
activeTab: "media",
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
}));
|
29
apps/web/src/components/editor/media-panel/tabbar.tsx
Normal file
29
apps/web/src/components/editor/media-panel/tabbar.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tab, tabs, useMediaPanelStore } from "./store";
|
||||
|
||||
export function TabBar() {
|
||||
const { activeTab, setActiveTab } = useMediaPanelStore();
|
||||
|
||||
return (
|
||||
<div className="h-12 bg-accent/50 px-3 flex justify-start items-center gap-6">
|
||||
{(Object.keys(tabs) as Tab[]).map((tabKey) => {
|
||||
const tab = tabs[tabKey];
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 items-center cursor-pointer",
|
||||
activeTab === tabKey ? "text-primary" : "text-muted-foreground"
|
||||
)}
|
||||
onClick={() => setActiveTab(tabKey)}
|
||||
key={tabKey}
|
||||
>
|
||||
<tab.icon className="!size-5" />
|
||||
<span className="text-[0.65rem]">{tab.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,32 +4,28 @@ import { useDragDrop } from "@/hooks/use-drag-drop";
|
||||
import { processMediaFiles } from "@/lib/media-processing";
|
||||
import { useMediaStore, type MediaItem } from "@/stores/media-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { Image, Music, MusicIcon, Plus, TextIcon, Upload, Video, VideoIcon } from "lucide-react";
|
||||
import { Image, Music, Plus, Upload, Video } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AspectRatio } from "../ui/aspect-ratio";
|
||||
import { Button } from "../ui/button";
|
||||
import { DragOverlay } from "../ui/drag-overlay";
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DragOverlay } from "@/components/ui/drag-overlay";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "../ui/context-menu";
|
||||
import { Input } from "../ui/input";
|
||||
} from "@/components/ui/context-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
} from "@/components/ui/select";
|
||||
|
||||
// MediaPanel lets users add, view, and drag media (images, videos, audio) into the project.
|
||||
// You can upload files or drag them from your computer. Dragging from here to the timeline adds them to your video project.
|
||||
|
||||
export function MediaPanel() {
|
||||
export function MediaView() {
|
||||
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
@ -226,17 +222,15 @@ export function MediaPanel() {
|
||||
{/* Show overlay when dragging files over the panel */}
|
||||
<DragOverlay isVisible={isDragOver} />
|
||||
|
||||
<TabBar />
|
||||
|
||||
<div className="p-3 pb-2">
|
||||
{/* Button to add/upload media */}
|
||||
<div className="flex gap-2">
|
||||
{/* Search and filter controls */}
|
||||
<Select value={mediaFilter} onValueChange={setMediaFilter}>
|
||||
<SelectTrigger className="w-[80px] h-7 text-xs">
|
||||
<SelectTrigger className="w-[80px] h-full text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent className="">
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="video">Video</SelectItem>
|
||||
<SelectItem value="audio">Audio</SelectItem>
|
||||
@ -246,7 +240,7 @@ export function MediaPanel() {
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search media..."
|
||||
className="min-w-[60px] flex-1 h-7 text-xs"
|
||||
className="min-w-[60px] flex-1 h-full text-xs"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@ -346,44 +340,3 @@ export function MediaPanel() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type Tab = "media" | "audio" | "text";
|
||||
|
||||
function TabBar() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("media");
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
icon: VideoIcon,
|
||||
label: "Media",
|
||||
},
|
||||
{
|
||||
icon: MusicIcon,
|
||||
label: "Audio",
|
||||
},
|
||||
{
|
||||
icon: TextIcon,
|
||||
label: "Text",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-accent/50 h-12 px-4 flex justify-start items-center gap-6">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 items-center cursor-pointer",
|
||||
activeTab === tab.label.toLowerCase()
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
)}
|
||||
onClick={() => setActiveTab(tab.label.toLowerCase() as Tab)}
|
||||
key={tab.label}
|
||||
>
|
||||
<tab.icon className="!size-5" />
|
||||
<span className="text-xs">{tab.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user