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/stores/media-store.ts | 62 ++++++++++-- apps/web/src/stores/project-store.ts | 186 ++++++++++++++++++++++++++++++++--- 2 files changed, 228 insertions(+), 20 deletions(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index bdb4d52a..bf465811 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { storageService } from "@/lib/storage/storage-service"; export interface MediaItem { id: string; @@ -13,11 +14,13 @@ export interface MediaItem { interface MediaStore { mediaItems: MediaItem[]; + isLoading: boolean; // Actions - addMediaItem: (item: Omit) => void; - removeMediaItem: (id: string) => void; - clearAllMedia: () => void; + addMediaItem: (item: Omit) => Promise; + removeMediaItem: (id: string) => Promise; + loadAllMedia: () => Promise; + clearAllMedia: () => Promise; } // Helper function to determine file type @@ -126,18 +129,32 @@ export const getMediaDuration = (file: File): Promise => { export const useMediaStore = create((set, get) => ({ mediaItems: [], + isLoading: false, - addMediaItem: (item) => { + addMediaItem: async (item) => { const newItem: MediaItem = { ...item, id: crypto.randomUUID(), }; + + // Add to local state immediately for UI responsiveness set((state) => ({ mediaItems: [...state.mediaItems, newItem], })); + + // Save to persistent storage in background + try { + await storageService.saveMediaItem(newItem); + } catch (error) { + console.error("Failed to save media item:", error); + // Remove from local state if save failed + set((state) => ({ + mediaItems: state.mediaItems.filter((item) => item.id !== newItem.id), + })); + } }, - removeMediaItem: (id) => { + removeMediaItem: async (id) => { const state = get(); const item = state.mediaItems.find((item) => item.id === id); @@ -149,12 +166,34 @@ export const useMediaStore = create((set, get) => ({ } } + // Remove from local state immediately set((state) => ({ mediaItems: state.mediaItems.filter((item) => item.id !== id), })); + + // Remove from persistent storage + try { + await storageService.deleteMediaItem(id); + } catch (error) { + console.error("Failed to delete media item:", error); + // Could re-add to local state here if needed + } }, - clearAllMedia: () => { + loadAllMedia: async () => { + set({ isLoading: true }); + + try { + const mediaItems = await storageService.loadAllMediaItems(); + set({ mediaItems }); + } catch (error) { + console.error("Failed to load media items:", error); + } finally { + set({ isLoading: false }); + } + }, + + clearAllMedia: async () => { const state = get(); // Cleanup all object URLs @@ -165,6 +204,17 @@ export const useMediaStore = create((set, get) => ({ } }); + // Clear local state set({ mediaItems: [] }); + + // Clear persistent storage + try { + const mediaIds = state.mediaItems.map((item) => item.id); + await Promise.all( + mediaIds.map((id) => storageService.deleteMediaItem(id)) + ); + } catch (error) { + console.error("Failed to clear media items from storage:", error); + } }, })); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 43113fc1..4fff83c2 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -1,19 +1,32 @@ import { TProject } from "@/types/project"; import { create } from "zustand"; +import { storageService } from "@/lib/storage/storage-service"; +import { toast } from "sonner"; interface ProjectStore { activeProject: TProject | null; + savedProjects: TProject[]; + isLoading: boolean; + isInitialized: boolean; // Actions - createNewProject: (name: string) => void; + createNewProject: (name: string) => Promise; + loadProject: (id: string) => Promise; + saveCurrentProject: () => Promise; + loadAllProjects: () => Promise; + deleteProject: (id: string) => Promise; closeProject: () => void; - updateProjectName: (name: string) => void; + renameProject: (projectId: string, name: string) => Promise; + duplicateProject: (projectId: string) => Promise; } -export const useProjectStore = create((set) => ({ +export const useProjectStore = create((set, get) => ({ activeProject: null, + savedProjects: [], + isLoading: true, + isInitialized: false, - createNewProject: (name: string) => { + createNewProject: async (name: string) => { const newProject: TProject = { id: crypto.randomUUID(), name, @@ -21,22 +34,167 @@ export const useProjectStore = create((set) => ({ createdAt: new Date(), updatedAt: new Date(), }; + set({ activeProject: newProject }); + + try { + await storageService.saveProject(newProject); + // Reload all projects to update the list + await get().loadAllProjects(); + return newProject.id; + } catch (error) { + toast.error("Failed to save new project"); + throw error; + } + }, + + loadProject: async (id: string) => { + if (!get().isInitialized) { + set({ isLoading: true }); + } + + try { + const project = await storageService.loadProject(id); + if (project) { + set({ activeProject: project }); + } + } catch (error) { + console.error("Failed to load project:", error); + } finally { + set({ isLoading: false }); + } + }, + + saveCurrentProject: async () => { + const { activeProject } = get(); + if (!activeProject) return; + + try { + await storageService.saveProject(activeProject); + await get().loadAllProjects(); // Refresh the list + } catch (error) { + console.error("Failed to save project:", error); + } + }, + + loadAllProjects: async () => { + if (!get().isInitialized) { + set({ isLoading: true }); + } + + try { + const projects = await storageService.loadAllProjects(); + set({ savedProjects: projects }); + } catch (error) { + console.error("Failed to load projects:", error); + } finally { + set({ isLoading: false, isInitialized: true }); + } + }, + + deleteProject: async (id: string) => { + try { + await storageService.deleteProject(id); + await get().loadAllProjects(); // Refresh the list + + // If we deleted the active project, close it + const { activeProject } = get(); + if (activeProject?.id === id) { + set({ activeProject: null }); + } + } catch (error) { + console.error("Failed to delete project:", error); + } }, closeProject: () => { set({ activeProject: null }); }, - updateProjectName: (name: string) => { - set((state) => ({ - activeProject: state.activeProject - ? { - ...state.activeProject, - name, - updatedAt: new Date(), - } - : null, - })); + renameProject: async (id: string, name: string) => { + const { savedProjects } = get(); + + // Find the project to rename + const projectToRename = savedProjects.find((p) => p.id === id); + if (!projectToRename) { + toast.error("Project not found", { + description: "Please try again", + }); + return; + } + + const updatedProject = { + ...projectToRename, + name, + updatedAt: new Date(), + }; + + try { + // Save to storage + await storageService.saveProject(updatedProject); + + await get().loadAllProjects(); + + // Update activeProject if it's the same project + const { activeProject } = get(); + if (activeProject?.id === id) { + set({ activeProject: updatedProject }); + } + } catch (error) { + console.error("Failed to rename project:", error); + toast.error("Failed to rename project", { + description: + error instanceof Error ? error.message : "Please try again", + }); + } + }, + + duplicateProject: async (projectId: string) => { + try { + const project = await storageService.loadProject(projectId); + if (!project) { + toast.error("Project not found", { + description: "Please try again", + }); + throw new Error("Project not found"); + } + + const { savedProjects } = get(); + + // Extract the base name (remove any existing numbering) + const numberMatch = project.name.match(/^\((\d+)\)\s+(.+)$/); + const baseName = numberMatch ? numberMatch[2] : project.name; + const existingNumbers: number[] = []; + + // Check for pattern "(number) baseName" in existing projects + savedProjects.forEach((p) => { + const match = p.name.match(/^\((\d+)\)\s+(.+)$/); + if (match && match[2] === baseName) { + existingNumbers.push(parseInt(match[1], 10)); + } + }); + + const nextNumber = + existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1; + + const newProject: TProject = { + id: crypto.randomUUID(), + name: `(${nextNumber}) ${baseName}`, + thumbnail: project.thumbnail, + createdAt: new Date(), + updatedAt: new Date(), + }; + + await storageService.saveProject(newProject); + await get().loadAllProjects(); + return newProject.id; + } catch (error) { + console.error("Failed to duplicate project:", error); + toast.error("Failed to duplicate project", { + description: + error instanceof Error ? error.message : "Please try again", + }); + throw error; + } }, })); -- cgit v1.3.1