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 { processMediaFiles } from "@/lib/media-processing";
|
||||||
import { useMediaStore, type MediaItem } from "@/stores/media-store";
|
import { useMediaStore, type MediaItem } from "@/stores/media-store";
|
||||||
import { useTimelineStore } from "@/stores/timeline-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 { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AspectRatio } from "../ui/aspect-ratio";
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DragOverlay } from "../ui/drag-overlay";
|
import { DragOverlay } from "@/components/ui/drag-overlay";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "../ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "../ui/select";
|
} from "@/components/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
// MediaPanel lets users add, view, and drag media (images, videos, audio) into the project.
|
export function MediaView() {
|
||||||
// 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() {
|
|
||||||
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
@ -226,17 +222,15 @@ export function MediaPanel() {
|
|||||||
{/* Show overlay when dragging files over the panel */}
|
{/* Show overlay when dragging files over the panel */}
|
||||||
<DragOverlay isVisible={isDragOver} />
|
<DragOverlay isVisible={isDragOver} />
|
||||||
|
|
||||||
<TabBar />
|
|
||||||
|
|
||||||
<div className="p-3 pb-2">
|
<div className="p-3 pb-2">
|
||||||
{/* Button to add/upload media */}
|
{/* Button to add/upload media */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* Search and filter controls */}
|
{/* Search and filter controls */}
|
||||||
<Select value={mediaFilter} onValueChange={setMediaFilter}>
|
<Select value={mediaFilter} onValueChange={setMediaFilter}>
|
||||||
<SelectTrigger className="w-[80px] h-7 text-xs">
|
<SelectTrigger className="w-[80px] h-full text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="">
|
||||||
<SelectItem value="all">All</SelectItem>
|
<SelectItem value="all">All</SelectItem>
|
||||||
<SelectItem value="video">Video</SelectItem>
|
<SelectItem value="video">Video</SelectItem>
|
||||||
<SelectItem value="audio">Audio</SelectItem>
|
<SelectItem value="audio">Audio</SelectItem>
|
||||||
@ -246,7 +240,7 @@ export function MediaPanel() {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search media..."
|
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}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
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