feat: implement project management features with storage integration, including project creation, deletion, and renaming dialogs

This commit is contained in:
Maze Winther
2025-06-30 19:58:36 +02:00
parent cd30c205b4
commit 09373eb4a3
15 changed files with 1114 additions and 269 deletions

View File

@ -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<MediaItem, "id">) => void;
removeMediaItem: (id: string) => void;
clearAllMedia: () => void;
addMediaItem: (item: Omit<MediaItem, "id">) => Promise<void>;
removeMediaItem: (id: string) => Promise<void>;
loadAllMedia: () => Promise<void>;
clearAllMedia: () => Promise<void>;
}
// Helper function to determine file type
@ -126,18 +129,32 @@ export const getMediaDuration = (file: File): Promise<number> => {
export const useMediaStore = create<MediaStore>((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<MediaStore>((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<MediaStore>((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);
}
},
}));

View File

@ -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<string>;
loadProject: (id: string) => Promise<void>;
saveCurrentProject: () => Promise<void>;
loadAllProjects: () => Promise<void>;
deleteProject: (id: string) => Promise<void>;
closeProject: () => void;
updateProjectName: (name: string) => void;
renameProject: (projectId: string, name: string) => Promise<void>;
duplicateProject: (projectId: string) => Promise<string>;
}
export const useProjectStore = create<ProjectStore>((set) => ({
export const useProjectStore = create<ProjectStore>((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<ProjectStore>((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;
}
},
}));