diff options
Diffstat (limited to 'apps/web/src/app/projects')
| -rw-r--r-- | apps/web/src/app/projects/page.tsx | 1086 |
1 files changed, 543 insertions, 543 deletions
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 4dd15ee7..7ac08107 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -7,601 +7,601 @@ import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { - ChevronLeft, - Plus, - Calendar, - MoreHorizontal, - Video, - Loader2, - X, - Trash2, - Search, + ChevronLeft, + Plus, + Calendar, + MoreHorizontal, + Video, + Loader2, + X, + Trash2, + Search, } from "lucide-react"; import { TProject } from "@/types/project"; import Image from "next/image"; import { - DropdownMenu, - DropdownMenuItem, - DropdownMenuContent, - DropdownMenuTrigger, - DropdownMenuSeparator, + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { useProjectStore } from "@/stores/project-store"; import { useRouter } from "next/navigation"; import { DeleteProjectDialog } from "@/components/delete-project-dialog"; import { RenameProjectDialog } from "@/components/rename-project-dialog"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; export default function ProjectsPage() { - const { - createNewProject, - savedProjects, - isLoading, - isInitialized, - deleteProject, - getFilteredAndSortedProjects, - } = useProjectStore(); - const router = useRouter(); - const [isSelectionMode, setIsSelectionMode] = useState(false); - const [selectedProjects, setSelectedProjects] = useState<Set<string>>( - new Set(), - ); - const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const [sortOption, setSortOption] = useState("createdAt-desc"); + const { + createNewProject, + savedProjects, + isLoading, + isInitialized, + deleteProject, + getFilteredAndSortedProjects, + } = useProjectStore(); + const router = useRouter(); + const [isSelectionMode, setIsSelectionMode] = useState(false); + const [selectedProjects, setSelectedProjects] = useState<Set<string>>( + new Set() + ); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [sortOption, setSortOption] = useState("createdAt-desc"); - const handleCreateProject = async () => { - const projectId = await createNewProject("New Project"); - console.log("projectId", projectId); - router.push(`/editor/${projectId}`); - }; + const handleCreateProject = async () => { + const projectId = await createNewProject("New Project"); + console.log("projectId", projectId); + router.push(`/editor/${projectId}`); + }; - const handleSelectProject = (projectId: string, checked: boolean) => { - const newSelected = new Set(selectedProjects); - if (checked) { - newSelected.add(projectId); - } else { - newSelected.delete(projectId); - } - setSelectedProjects(newSelected); - }; + const handleSelectProject = (projectId: string, checked: boolean) => { + const newSelected = new Set(selectedProjects); + if (checked) { + newSelected.add(projectId); + } else { + newSelected.delete(projectId); + } + setSelectedProjects(newSelected); + }; - const handleSelectAll = (checked: boolean) => { - if (checked) { - setSelectedProjects(new Set(sortedProjects.map((p) => p.id))); - } else { - setSelectedProjects(new Set()); - } - }; + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedProjects(new Set(sortedProjects.map((p) => p.id))); + } else { + setSelectedProjects(new Set()); + } + }; - const handleCancelSelection = () => { - setIsSelectionMode(false); - setSelectedProjects(new Set()); - }; + const handleCancelSelection = () => { + setIsSelectionMode(false); + setSelectedProjects(new Set()); + }; - const handleBulkDelete = async () => { - await Promise.all( - Array.from(selectedProjects).map((projectId) => deleteProject(projectId)), - ); - setSelectedProjects(new Set()); - setIsSelectionMode(false); - setIsBulkDeleteDialogOpen(false); - }; + const handleBulkDelete = async () => { + await Promise.all( + Array.from(selectedProjects).map((projectId) => deleteProject(projectId)) + ); + setSelectedProjects(new Set()); + setIsSelectionMode(false); + setIsBulkDeleteDialogOpen(false); + }; - const sortedProjects = getFilteredAndSortedProjects(searchQuery, sortOption); + const sortedProjects = getFilteredAndSortedProjects(searchQuery, sortOption); - const allSelected = - sortedProjects.length > 0 && - selectedProjects.size === sortedProjects.length; - const someSelected = - selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length; + const allSelected = + sortedProjects.length > 0 && + selectedProjects.size === sortedProjects.length; + const someSelected = + selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length; - return ( - <div className="min-h-screen bg-background"> - <div className="pt-6 px-6 flex items-center justify-between w-full h-16"> - <Link - href="/" - className="flex items-center gap-1 hover:text-muted-foreground transition-colors" - > - <ChevronLeft className="!size-5 shrink-0" /> - <span className="text-sm font-medium">Back</span> - </Link> - <div className="block md:hidden"> - {isSelectionMode ? ( - <div className="flex items-center gap-2"> - <Button - variant="outline" - size="sm" - onClick={handleCancelSelection} - > - <X className="!size-4" /> - Cancel - </Button> - {selectedProjects.size > 0 && ( - <Button - variant="destructive" - size="sm" - onClick={() => setIsBulkDeleteDialogOpen(true)} - > - <Trash2 className="!size-4" /> - Delete ({selectedProjects.size}) - </Button> - )} - </div> - ) : ( - <CreateButton onClick={handleCreateProject} /> - )} - </div> - </div> - <main className="max-w-6xl mx-auto px-6 pt-6 pb-6"> - <div className="mb-8 flex items-center justify-between"> - <div className="flex flex-col gap-3"> - <h1 className="text-2xl md:text-3xl font-bold tracking-tight"> - Your Projects - </h1> - <p className="text-muted-foreground"> - {savedProjects.length}{" "} - {savedProjects.length === 1 ? "project" : "projects"} - {isSelectionMode && selectedProjects.size > 0 && ( - <span className="ml-2 text-primary"> - • {selectedProjects.size} selected - </span> - )} - </p> - </div> - <div className="hidden md:block"> - {isSelectionMode ? ( - <div className="flex items-center gap-2"> - <Button variant="outline" onClick={handleCancelSelection}> - <X className="!size-4" /> - Cancel - </Button> - {selectedProjects.size > 0 && ( - <Button - variant="destructive" - onClick={() => setIsBulkDeleteDialogOpen(true)} - > - <Trash2 className="!size-4" /> - Delete Selected ({selectedProjects.size}) - </Button> - )} - </div> - ) : ( - <div className="flex items-center gap-2"> - <Button - variant="outline" - onClick={() => setIsSelectionMode(true)} - disabled={savedProjects.length === 0} - > - Select Projects - </Button> - <CreateButton onClick={handleCreateProject} /> - </div> - )} - </div> - </div> + return ( + <div className="min-h-screen bg-background"> + <div className="pt-6 px-6 flex items-center justify-between w-full h-16"> + <Link + href="/" + className="flex items-center gap-1 hover:text-muted-foreground transition-colors" + > + <ChevronLeft className="!size-5 shrink-0" /> + <span className="text-sm font-medium">Back</span> + </Link> + <div className="block md:hidden"> + {isSelectionMode ? ( + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={handleCancelSelection} + > + <X className="!size-4" /> + Cancel + </Button> + {selectedProjects.size > 0 && ( + <Button + variant="destructive" + size="sm" + onClick={() => setIsBulkDeleteDialogOpen(true)} + > + <Trash2 className="!size-4" /> + Delete ({selectedProjects.size}) + </Button> + )} + </div> + ) : ( + <CreateButton onClick={handleCreateProject} /> + )} + </div> + </div> + <main className="max-w-6xl mx-auto px-6 pt-6 pb-6"> + <div className="mb-8 flex items-center justify-between"> + <div className="flex flex-col gap-3"> + <h1 className="text-2xl md:text-3xl font-bold tracking-tight"> + Your Projects + </h1> + <p className="text-muted-foreground"> + {savedProjects.length}{" "} + {savedProjects.length === 1 ? "project" : "projects"} + {isSelectionMode && selectedProjects.size > 0 && ( + <span className="ml-2 text-primary"> + • {selectedProjects.size} selected + </span> + )} + </p> + </div> + <div className="hidden md:block"> + {isSelectionMode ? ( + <div className="flex items-center gap-2"> + <Button variant="outline" onClick={handleCancelSelection}> + <X className="!size-4" /> + Cancel + </Button> + {selectedProjects.size > 0 && ( + <Button + variant="destructive" + onClick={() => setIsBulkDeleteDialogOpen(true)} + > + <Trash2 className="!size-4" /> + Delete Selected ({selectedProjects.size}) + </Button> + )} + </div> + ) : ( + <div className="flex items-center gap-2"> + <Button + variant="outline" + onClick={() => setIsSelectionMode(true)} + disabled={savedProjects.length === 0} + > + Select Projects + </Button> + <CreateButton onClick={handleCreateProject} /> + </div> + )} + </div> + </div> - <div className="mb-4 flex items-center justify-between gap-4"> - <div className="flex-1 max-w-72"> - <Input - placeholder="Search projects..." - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - /> - </div> - <Select value={sortOption} onValueChange={setSortOption}> - <SelectTrigger className="w-[180px]"> - <SelectValue placeholder="Sort by" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="createdAt-desc">Newest to Oldest</SelectItem> - <SelectItem value="createdAt-asc">Oldest to Newest</SelectItem> - <SelectItem value="name-asc">Name (A-Z)</SelectItem> - <SelectItem value="name-desc">Name (Z-A)</SelectItem> - </SelectContent> - </Select> - </div> + <div className="mb-4 flex items-center justify-between gap-4"> + <div className="flex-1 max-w-72"> + <Input + placeholder="Search projects..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + /> + </div> + <Select value={sortOption} onValueChange={setSortOption}> + <SelectTrigger className="w-[180px]"> + <SelectValue placeholder="Sort by" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="createdAt-desc">Newest to Oldest</SelectItem> + <SelectItem value="createdAt-asc">Oldest to Newest</SelectItem> + <SelectItem value="name-asc">Name (A-Z)</SelectItem> + <SelectItem value="name-desc">Name (Z-A)</SelectItem> + </SelectContent> + </Select> + </div> - {isSelectionMode && sortedProjects.length > 0 && ( - <div className="mb-6 p-4 bg-muted/30 rounded-lg border"> - <div className="flex items-center gap-3"> - <Checkbox - checked={someSelected ? "indeterminate" : allSelected} - onCheckedChange={(value) => handleSelectAll(!!value)} - /> - <span className="text-sm font-medium"> - {allSelected ? "Deselect All" : "Select All"} - </span> - <span className="text-sm text-muted-foreground"> - ({selectedProjects.size} of {sortedProjects.length} selected) - </span> - </div> - </div> - )} + {isSelectionMode && sortedProjects.length > 0 && ( + <div className="mb-6 p-4 bg-muted/30 rounded-lg border"> + <div className="flex items-center gap-3"> + <Checkbox + checked={someSelected ? "indeterminate" : allSelected} + onCheckedChange={(value) => handleSelectAll(!!value)} + /> + <span className="text-sm font-medium"> + {allSelected ? "Deselect All" : "Select All"} + </span> + <span className="text-sm text-muted-foreground"> + ({selectedProjects.size} of {sortedProjects.length} selected) + </span> + </div> + </div> + )} - {isLoading || !isInitialized ? ( - <div className="flex items-center justify-center py-16"> - <Loader2 className="h-8 w-8 text-muted-foreground animate-spin" /> - </div> - ) : savedProjects.length === 0 ? ( - <NoProjects onCreateProject={handleCreateProject} /> - ) : sortedProjects.length === 0 ? ( - <NoResults - searchQuery={searchQuery} - onClearSearch={() => setSearchQuery("")} - /> - ) : ( - <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6"> - {sortedProjects.map((project) => ( - <ProjectCard - key={project.id} - project={project} - isSelectionMode={isSelectionMode} - isSelected={selectedProjects.has(project.id)} - onSelect={handleSelectProject} - /> - ))} - </div> - )} - </main> + {isLoading || !isInitialized ? ( + <div className="flex items-center justify-center py-16"> + <Loader2 className="h-8 w-8 text-muted-foreground animate-spin" /> + </div> + ) : savedProjects.length === 0 ? ( + <NoProjects onCreateProject={handleCreateProject} /> + ) : sortedProjects.length === 0 ? ( + <NoResults + searchQuery={searchQuery} + onClearSearch={() => setSearchQuery("")} + /> + ) : ( + <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6"> + {sortedProjects.map((project) => ( + <ProjectCard + key={project.id} + project={project} + isSelectionMode={isSelectionMode} + isSelected={selectedProjects.has(project.id)} + onSelect={handleSelectProject} + /> + ))} + </div> + )} + </main> - <DeleteProjectDialog - isOpen={isBulkDeleteDialogOpen} - onOpenChange={setIsBulkDeleteDialogOpen} - onConfirm={handleBulkDelete} - /> - </div> - ); + <DeleteProjectDialog + isOpen={isBulkDeleteDialogOpen} + onOpenChange={setIsBulkDeleteDialogOpen} + onConfirm={handleBulkDelete} + /> + </div> + ); } interface ProjectCardProps { - project: TProject; - isSelectionMode?: boolean; - isSelected?: boolean; - onSelect?: (projectId: string, checked: boolean) => void; + project: TProject; + isSelectionMode?: boolean; + isSelected?: boolean; + onSelect?: (projectId: string, checked: boolean) => void; } function ProjectCard({ - project, - isSelectionMode = false, - isSelected = false, - onSelect, + project, + isSelectionMode = false, + isSelected = false, + onSelect, }: ProjectCardProps) { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); - const { deleteProject, renameProject, duplicateProject } = useProjectStore(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const { deleteProject, renameProject, duplicateProject } = useProjectStore(); - const formatDate = (date: Date): string => { - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); - }; + const formatDate = (date: Date): string => { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; - const handleDeleteProject = async () => { - await deleteProject(project.id); - setIsDropdownOpen(false); - }; + const handleDeleteProject = async () => { + await deleteProject(project.id); + setIsDropdownOpen(false); + }; - const handleRenameProject = async (newName: string) => { - await renameProject(project.id, newName); - setIsRenameDialogOpen(false); - }; + const handleRenameProject = async (newName: string) => { + await renameProject(project.id, newName); + setIsRenameDialogOpen(false); + }; - const handleDuplicateProject = async () => { - setIsDropdownOpen(false); - await duplicateProject(project.id); - }; + const handleDuplicateProject = async () => { + setIsDropdownOpen(false); + await duplicateProject(project.id); + }; - const handleCardClick = (e: React.MouseEvent) => { - if (isSelectionMode) { - e.preventDefault(); - onSelect?.(project.id, !isSelected); - } - }; + const handleCardClick = (e: React.MouseEvent) => { + if (isSelectionMode) { + e.preventDefault(); + onSelect?.(project.id, !isSelected); + } + }; - return ( - <> - {isSelectionMode ? ( - <div onClick={handleCardClick} className="block group cursor-pointer"> - <Card - className={`overflow-hidden bg-background border-none p-0 transition-all ${ - isSelectionMode && isSelected ? "ring-2 ring-primary" : "" - }`} - > - <div - className={`relative aspect-square bg-muted transition-opacity ${ - isDropdownOpen - ? "opacity-65" - : "opacity-100 group-hover:opacity-65" - }`} - > - {/* Selection checkbox */} - {isSelectionMode && ( - <div className="absolute top-3 left-3 z-10"> - <div className="w-5 h-5 rounded bg-background/80 backdrop-blur-sm border flex items-center justify-center"> - <Checkbox - checked={isSelected} - onCheckedChange={(checked) => - onSelect?.(project.id, checked as boolean) - } - onClick={(e) => e.stopPropagation()} - className="w-4 h-4" - /> - </div> - </div> - )} + return ( + <> + {isSelectionMode ? ( + <div onClick={handleCardClick} className="block group cursor-pointer"> + <Card + className={`overflow-hidden bg-background border-none p-0 transition-all ${ + isSelectionMode && isSelected ? "ring-2 ring-primary" : "" + }`} + > + <div + className={`relative aspect-square bg-muted transition-opacity ${ + isDropdownOpen + ? "opacity-65" + : "opacity-100 group-hover:opacity-65" + }`} + > + {/* Selection checkbox */} + {isSelectionMode && ( + <div className="absolute top-3 left-3 z-10"> + <div className="w-5 h-5 rounded bg-background/80 backdrop-blur-sm border flex items-center justify-center"> + <Checkbox + checked={isSelected} + onCheckedChange={(checked) => + onSelect?.(project.id, checked as boolean) + } + onClick={(e) => e.stopPropagation()} + className="w-4 h-4" + /> + </div> + </div> + )} - {/* Thumbnail preview or placeholder */} - <div className="absolute inset-0"> - {project.thumbnail ? ( - <Image - src={project.thumbnail} - alt="Project thumbnail" - fill - className="object-cover" - /> - ) : ( - <div className="w-full h-full bg-muted/50 flex items-center justify-center"> - <Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" /> - </div> - )} - </div> - </div> + {/* Thumbnail preview or placeholder */} + <div className="absolute inset-0"> + {project.thumbnail ? ( + <Image + src={project.thumbnail} + alt="Project thumbnail" + fill + className="object-cover" + /> + ) : ( + <div className="w-full h-full bg-muted/50 flex items-center justify-center"> + <Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" /> + </div> + )} + </div> + </div> - <CardContent className="px-0 pt-5 flex flex-col gap-1"> - <div className="flex items-start justify-between"> - <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2"> - {project.name} - </h3> - {!isSelectionMode && ( - <DropdownMenu - open={isDropdownOpen} - onOpenChange={setIsDropdownOpen} - > - <DropdownMenuTrigger asChild> - <Button - variant="text" - size="sm" - className={`size-6 p-0 transition-all shrink-0 ml-2 ${ - isDropdownOpen - ? "opacity-100" - : "opacity-0 group-hover:opacity-100" - }`} - onClick={(e) => e.preventDefault()} - > - <MoreHorizontal /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent - align="end" - onCloseAutoFocus={(e) => { - e.preventDefault(); - e.stopPropagation(); - }} - > - <DropdownMenuItem - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropdownOpen(false); - setIsRenameDialogOpen(true); - }} - > - Rename - </DropdownMenuItem> - <DropdownMenuItem - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - handleDuplicateProject(); - }} - > - Duplicate - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem - variant="destructive" - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropdownOpen(false); - setIsDeleteDialogOpen(true); - }} - > - Delete - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - )} - </div> + <CardContent className="px-0 pt-5 flex flex-col gap-1"> + <div className="flex items-start justify-between"> + <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2"> + {project.name} + </h3> + {!isSelectionMode && ( + <DropdownMenu + open={isDropdownOpen} + onOpenChange={setIsDropdownOpen} + > + <DropdownMenuTrigger asChild> + <Button + variant="text" + size="sm" + className={`size-6 p-0 transition-all shrink-0 ml-2 ${ + isDropdownOpen + ? "opacity-100" + : "opacity-0 group-hover:opacity-100" + }`} + onClick={(e) => e.preventDefault()} + > + <MoreHorizontal /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent + align="end" + onCloseAutoFocus={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + > + <DropdownMenuItem + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropdownOpen(false); + setIsRenameDialogOpen(true); + }} + > + Rename + </DropdownMenuItem> + <DropdownMenuItem + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleDuplicateProject(); + }} + > + Duplicate + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + variant="destructive" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropdownOpen(false); + setIsDeleteDialogOpen(true); + }} + > + Delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + )} + </div> - <div className="space-y-1"> - <div className="flex items-center gap-1.5 text-sm text-muted-foreground"> - <Calendar className="!size-4" /> - <span>Created {formatDate(project.createdAt)}</span> - </div> - </div> - </CardContent> - </Card> - </div> - ) : ( - <Link href={`/editor/${project.id}`} className="block group"> - <Card - className={`overflow-hidden bg-background border-none p-0 transition-all ${ - isSelectionMode && isSelected ? "ring-2 ring-primary" : "" - }`} - > - <div - className={`relative aspect-square bg-muted transition-opacity ${ - isDropdownOpen - ? "opacity-65" - : "opacity-100 group-hover:opacity-65" - }`} - > - {/* Thumbnail preview or placeholder */} - <div className="absolute inset-0"> - {project.thumbnail ? ( - <Image - src={project.thumbnail} - alt="Project thumbnail" - fill - className="object-cover" - /> - ) : ( - <div className="w-full h-full bg-muted/50 flex items-center justify-center"> - <Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" /> - </div> - )} - </div> - </div> + <div className="space-y-1"> + <div className="flex items-center gap-1.5 text-sm text-muted-foreground"> + <Calendar className="!size-4" /> + <span>Created {formatDate(project.createdAt)}</span> + </div> + </div> + </CardContent> + </Card> + </div> + ) : ( + <Link href={`/editor/${project.id}`} className="block group"> + <Card + className={`overflow-hidden bg-background border-none p-0 transition-all ${ + isSelectionMode && isSelected ? "ring-2 ring-primary" : "" + }`} + > + <div + className={`relative aspect-square bg-muted transition-opacity ${ + isDropdownOpen + ? "opacity-65" + : "opacity-100 group-hover:opacity-65" + }`} + > + {/* Thumbnail preview or placeholder */} + <div className="absolute inset-0"> + {project.thumbnail ? ( + <Image + src={project.thumbnail} + alt="Project thumbnail" + fill + className="object-cover" + /> + ) : ( + <div className="w-full h-full bg-muted/50 flex items-center justify-center"> + <Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" /> + </div> + )} + </div> + </div> - <CardContent className="px-0 pt-5 flex flex-col gap-1"> - <div className="flex items-start justify-between"> - <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2"> - {project.name} - </h3> - <DropdownMenu - open={isDropdownOpen} - onOpenChange={setIsDropdownOpen} - > - <DropdownMenuTrigger asChild> - <Button - variant="text" - size="sm" - className={`size-6 p-0 transition-all shrink-0 ml-2 ${ - isDropdownOpen - ? "opacity-100" - : "opacity-0 group-hover:opacity-100" - }`} - onClick={(e) => e.preventDefault()} - > - <MoreHorizontal /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent - align="end" - onCloseAutoFocus={(e) => { - e.preventDefault(); - e.stopPropagation(); - }} - > - <DropdownMenuItem - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropdownOpen(false); - setIsRenameDialogOpen(true); - }} - > - Rename - </DropdownMenuItem> - <DropdownMenuItem - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - handleDuplicateProject(); - }} - > - Duplicate - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem - variant="destructive" - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropdownOpen(false); - setIsDeleteDialogOpen(true); - }} - > - Delete - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - </div> + <CardContent className="px-0 pt-5 flex flex-col gap-1"> + <div className="flex items-start justify-between"> + <h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2"> + {project.name} + </h3> + <DropdownMenu + open={isDropdownOpen} + onOpenChange={setIsDropdownOpen} + > + <DropdownMenuTrigger asChild> + <Button + variant="text" + size="sm" + className={`size-6 p-0 transition-all shrink-0 ml-2 ${ + isDropdownOpen + ? "opacity-100" + : "opacity-0 group-hover:opacity-100" + }`} + onClick={(e) => e.preventDefault()} + > + <MoreHorizontal /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent + align="end" + onCloseAutoFocus={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + > + <DropdownMenuItem + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropdownOpen(false); + setIsRenameDialogOpen(true); + }} + > + Rename + </DropdownMenuItem> + <DropdownMenuItem + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleDuplicateProject(); + }} + > + Duplicate + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + variant="destructive" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropdownOpen(false); + setIsDeleteDialogOpen(true); + }} + > + Delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> - <div className="space-y-1"> - <div className="flex items-center gap-1.5 text-sm text-muted-foreground"> - <Calendar className="!size-4" /> - <span>Created {formatDate(project.createdAt)}</span> - </div> - </div> - </CardContent> - </Card> - </Link> - )} - <DeleteProjectDialog - isOpen={isDeleteDialogOpen} - onOpenChange={setIsDeleteDialogOpen} - onConfirm={handleDeleteProject} - projectName={project.name} - /> - <RenameProjectDialog - isOpen={isRenameDialogOpen} - onOpenChange={setIsRenameDialogOpen} - onConfirm={handleRenameProject} - projectName={project.name} - /> - </> - ); + <div className="space-y-1"> + <div className="flex items-center gap-1.5 text-sm text-muted-foreground"> + <Calendar className="!size-4" /> + <span>Created {formatDate(project.createdAt)}</span> + </div> + </div> + </CardContent> + </Card> + </Link> + )} + <DeleteProjectDialog + isOpen={isDeleteDialogOpen} + onOpenChange={setIsDeleteDialogOpen} + onConfirm={handleDeleteProject} + projectName={project.name} + /> + <RenameProjectDialog + isOpen={isRenameDialogOpen} + onOpenChange={setIsRenameDialogOpen} + onConfirm={handleRenameProject} + projectName={project.name} + /> + </> + ); } function CreateButton({ onClick }: { onClick?: () => void }) { - return ( - <Button className="flex" onClick={onClick}> - <Plus className="!size-4" /> - <span className="text-sm font-medium">New project</span> - </Button> - ); + return ( + <Button className="flex" onClick={onClick}> + <Plus className="!size-4" /> + <span className="text-sm font-medium">New project</span> + </Button> + ); } function NoProjects({ onCreateProject }: { onCreateProject: () => void }) { - return ( - <div className="flex flex-col items-center justify-center py-16 text-center"> - <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> - <Video className="h-8 w-8 text-muted-foreground" /> - </div> - <h3 className="text-lg font-medium mb-2">No projects yet</h3> - <p className="text-muted-foreground mb-6 max-w-md"> - Start creating your first video project. Import media, edit, and export - professional videos. - </p> - <Button size="lg" className="gap-2" onClick={onCreateProject}> - <Plus className="h-4 w-4" /> - Create Your First Project - </Button> - </div> - ); + return ( + <div className="flex flex-col items-center justify-center py-16 text-center"> + <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> + <Video className="h-8 w-8 text-muted-foreground" /> + </div> + <h3 className="text-lg font-medium mb-2">No projects yet</h3> + <p className="text-muted-foreground mb-6 max-w-md"> + Start creating your first video project. Import media, edit, and export + professional videos. + </p> + <Button size="lg" className="gap-2" onClick={onCreateProject}> + <Plus className="h-4 w-4" /> + Create Your First Project + </Button> + </div> + ); } function NoResults({ - searchQuery, - onClearSearch, + searchQuery, + onClearSearch, }: { - searchQuery: string; - onClearSearch: () => void; + searchQuery: string; + onClearSearch: () => void; }) { - return ( - <div className="flex flex-col items-center justify-center py-16 text-center"> - <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> - <Search className="h-8 w-8 text-muted-foreground" /> - </div> - <h3 className="text-lg font-medium mb-2">No results found</h3> - <p className="text-muted-foreground mb-6 max-w-md"> - Your search for "{searchQuery}" did not return any results. - </p> - <Button onClick={onClearSearch} variant="outline"> - Clear Search - </Button> - </div> - ); + return ( + <div className="flex flex-col items-center justify-center py-16 text-center"> + <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> + <Search className="h-8 w-8 text-muted-foreground" /> + </div> + <h3 className="text-lg font-medium mb-2">No results found</h3> + <p className="text-muted-foreground mb-6 max-w-md"> + Your search for "{searchQuery}" did not return any results. + </p> + <Button onClick={onClearSearch} variant="outline"> + Clear Search + </Button> + </div> + ); } |
