feat: implement project management features with storage integration, including project creation, deletion, and renaming dialogs
This commit is contained in:
53
apps/web/src/components/delete-project-dialog.tsx
Normal file
53
apps/web/src/components/delete-project-dialog.tsx
Normal file
@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
onOpenAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this project? This action cannot be
|
||||
undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -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) => {
|
||||
|
@ -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)}
|
||||
>
|
||||
<div className="absolute inset-1 flex items-center p-1">
|
||||
{renderClipContent()}
|
||||
|
73
apps/web/src/components/rename-project-dialog.tsx
Normal file
73
apps/web/src/components/rename-project-dialog.tsx
Normal file
@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a new name for your project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onConfirm(name);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a new name"
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onConfirm(name)}>Rename</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
81
apps/web/src/components/storage-provider.tsx
Normal file
81
apps/web/src/components/storage-provider.tsx
Normal file
@ -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<StorageContextType | null>(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<StorageContextType>({
|
||||
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 (
|
||||
<StorageContext.Provider value={status}>{children}</StorageContext.Provider>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user