From 09373eb4a3f792d96d97d81f4fb86539d76e5702 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 30 Jun 2025 19:58:36 +0200 Subject: feat: implement project management features with storage integration, including project creation, deletion, and renaming dialogs --- apps/web/src/components/delete-project-dialog.tsx | 53 +++++++++++++++ apps/web/src/components/editor/media-panel.tsx | 15 +++-- apps/web/src/components/editor/timeline-clip.tsx | 3 +- apps/web/src/components/rename-project-dialog.tsx | 73 ++++++++++++++++++++ apps/web/src/components/storage-provider.tsx | 81 +++++++++++++++++++++++ 5 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/components/delete-project-dialog.tsx create mode 100644 apps/web/src/components/rename-project-dialog.tsx create mode 100644 apps/web/src/components/storage-provider.tsx (limited to 'apps/web/src/components') diff --git a/apps/web/src/components/delete-project-dialog.tsx b/apps/web/src/components/delete-project-dialog.tsx new file mode 100644 index 00000000..db709647 --- /dev/null +++ b/apps/web/src/components/delete-project-dialog.tsx @@ -0,0 +1,53 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +export function DeleteProjectDialog({ + isOpen, + onOpenChange, + onConfirm, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; +}) { + return ( + + { + e.preventDefault(); + e.stopPropagation(); + }} + > + + Delete Project + + Are you sure you want to delete this project? This action cannot be + undone. + + + + + + + + + ); +} diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx index f717234d..dd12faff 100644 --- a/apps/web/src/components/editor/media-panel.tsx +++ b/apps/web/src/components/editor/media-panel.tsx @@ -32,7 +32,9 @@ export function MediaPanel() { setProgress(p) ); // Add each processed media item to the store - processedItems.forEach((item) => addMediaItem(item)); + for (const item of processedItems) { + await addMediaItem(item); + } } catch (error) { // Show error toast if processing fails console.error("Error processing files:", error); @@ -56,12 +58,11 @@ export function MediaPanel() { e.target.value = ""; // Reset input }; - const handleRemove = (e: React.MouseEvent, id: string) => { + const handleRemove = async (e: React.MouseEvent, id: string) => { // Remove a media item from the store e.stopPropagation(); - - // Remove tracks automatically when delete media + // Remove tracks automatically when delete media const { tracks, removeTrack } = useTimelineStore.getState(); tracks.forEach((track) => { const clipsToRemove = track.clips.filter((clip) => clip.mediaId === id); @@ -69,12 +70,14 @@ export function MediaPanel() { useTimelineStore.getState().removeClipFromTrack(track.id, clip.id); }); // Only remove track if it becomes empty and has no other clips - const updatedTrack = useTimelineStore.getState().tracks.find(t => t.id === track.id); + const updatedTrack = useTimelineStore + .getState() + .tracks.find((t) => t.id === track.id); if (updatedTrack && updatedTrack.clips.length === 0) { removeTrack(track.id); } }); - removeMediaItem(id); + await removeMediaItem(id); }; const formatDuration = (duration: number) => { diff --git a/apps/web/src/components/editor/timeline-clip.tsx b/apps/web/src/components/editor/timeline-clip.tsx index b9d3b9a6..9b5f20f6 100644 --- a/apps/web/src/components/editor/timeline-clip.tsx +++ b/apps/web/src/components/editor/timeline-clip.tsx @@ -34,7 +34,6 @@ export function TimelineClip({ track, zoomLevel, isSelected, - onContextMenu, onClipMouseDown, onClipClick, }: TimelineClipProps) { @@ -299,7 +298,7 @@ export function TimelineClip({ )} ${isSelected ? "ring-2 ring-primary ring-offset-1" : ""}`} onClick={(e) => onClipClick && onClipClick(e, clip)} onMouseDown={handleClipMouseDown} - onContextMenu={(e) => onContextMenu && onContextMenu(e, clip.id)} + onContextMenu={(e) => onClipMouseDown && onClipMouseDown(e, clip)} >
{renderClipContent()} diff --git a/apps/web/src/components/rename-project-dialog.tsx b/apps/web/src/components/rename-project-dialog.tsx new file mode 100644 index 00000000..d84bad71 --- /dev/null +++ b/apps/web/src/components/rename-project-dialog.tsx @@ -0,0 +1,73 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; + +export function RenameProjectDialog({ + isOpen, + onOpenChange, + onConfirm, + projectName, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (name: string) => void; + projectName: string; +}) { + const [name, setName] = useState(projectName); + + // Reset the name when dialog opens - this is better UX than syncing with every prop change + const handleOpenChange = (open: boolean) => { + if (open) { + setName(projectName); + } + onOpenChange(open); + }; + + return ( + + + + Rename Project + + Enter a new name for your project. + + + + setName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + onConfirm(name); + } + }} + placeholder="Enter a new name" + className="mt-4" + /> + + + + + + + + ); +} diff --git a/apps/web/src/components/storage-provider.tsx b/apps/web/src/components/storage-provider.tsx new file mode 100644 index 00000000..e007ca96 --- /dev/null +++ b/apps/web/src/components/storage-provider.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { createContext, useContext, useEffect, useState } from "react"; +import { useProjectStore } from "@/stores/project-store"; +import { useMediaStore } from "@/stores/media-store"; +import { storageService } from "@/lib/storage/storage-service"; +import { toast } from "sonner"; + +interface StorageContextType { + isInitialized: boolean; + isLoading: boolean; + hasSupport: boolean; + error: string | null; +} + +const StorageContext = createContext(null); + +export function useStorage() { + const context = useContext(StorageContext); + if (!context) { + throw new Error("useStorage must be used within StorageProvider"); + } + return context; +} + +interface StorageProviderProps { + children: React.ReactNode; +} + +export function StorageProvider({ children }: StorageProviderProps) { + const [status, setStatus] = useState({ + isInitialized: false, + isLoading: true, + hasSupport: false, + error: null, + }); + + const loadAllProjects = useProjectStore((state) => state.loadAllProjects); + const loadAllMedia = useMediaStore((state) => state.loadAllMedia); + + useEffect(() => { + const initializeStorage = async () => { + setStatus((prev) => ({ ...prev, isLoading: true })); + + try { + // Check browser support + const hasSupport = storageService.isFullySupported(); + + if (!hasSupport) { + toast.warning( + "Storage not fully supported. Some features may not work." + ); + } + + // Load saved data in parallel + await Promise.all([loadAllProjects(), loadAllMedia()]); + + setStatus({ + isInitialized: true, + isLoading: false, + hasSupport, + error: null, + }); + } catch (error) { + console.error("Failed to initialize storage:", error); + setStatus({ + isInitialized: false, + isLoading: false, + hasSupport: storageService.isFullySupported(), + error: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + initializeStorage(); + }, [loadAllProjects, loadAllMedia]); + + return ( + {children} + ); +} -- cgit v1.3.1