This commit is contained in:
Maze Winther
2026-01-15 11:45:53 +01:00
parent deef784b76
commit dba74fa20f
6 changed files with 112 additions and 88 deletions
@@ -1,7 +1,3 @@
{
"/_not-found/page": "app/_not-found/page.js",
"/api/sounds/search/route": "app/api/sounds/search/route.js",
"/editor/[project_id]/page": "app/editor/[project_id]/page.js",
"/page": "app/page.js",
"/projects/page": "app/projects/page.js"
}
File diff suppressed because one or more lines are too long
+91 -81
View File
@@ -15,7 +15,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { KeyboardEvent, MouseEvent } from "react";
import React, { useState } from "react";
import { useState } from "react";
import { DeleteProjectDialog } from "@/components/delete-project-dialog";
import { RenameProjectDialog } from "@/components/rename-project-dialog";
import { Button } from "@/components/ui/button";
@@ -39,6 +39,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import type { TProjectMetadata } from "@/types/project";
import { toast } from "sonner";
import { useEditor } from "@/hooks/use-editor";
import { formatDate } from "@/lib/utils";
export default function ProjectsPage() {
const [isSelectionMode, setIsSelectionMode] = useState(false);
@@ -51,8 +52,6 @@ export default function ProjectsPage() {
const router = useRouter();
const editor = useEditor();
const hasSearchQuery = searchQuery.trim().length > 0;
const handleCreateProject = async () => {
try {
const projectId = await editor.project.createNewProject({
@@ -98,7 +97,9 @@ export default function ProjectsPage() {
const handleSelectAll = ({ checked }: { checked: boolean }) => {
if (checked) {
setSelectedProjects(new Set(sortedProjects.map((project) => project.id)));
setSelectedProjects(
new Set(projectsToDisplay.map((project) => project.id)),
);
} else {
setSelectedProjects(new Set());
}
@@ -128,16 +129,18 @@ export default function ProjectsPage() {
}
};
const sortedProjects = editor.project.getFilteredAndSortedProjects({
const projectsToDisplay = editor.project.getFilteredAndSortedProjects({
searchQuery,
sortOption,
});
const isAllSelected =
sortedProjects.length > 0 &&
selectedProjects.size === sortedProjects.length;
projectsToDisplay.length > 0 &&
selectedProjects.size === projectsToDisplay.length;
const hasSomeSelected =
selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length;
selectedProjects.size > 0 &&
selectedProjects.size < projectsToDisplay.length;
const isLoading = editor.project.getIsLoading();
const isInitialized = editor.project.getIsInitialized();
@@ -186,8 +189,8 @@ export default function ProjectsPage() {
Your projects
</h1>
<p className="text-muted-foreground">
{sortedProjects.length}{" "}
{sortedProjects.length === 1 ? "project" : "projects"}
{projectsToDisplay.length}{" "}
{projectsToDisplay.length === 1 ? "project" : "projects"}
{isSelectionMode && selectedProjects.size > 0 && (
<span className="text-primary ml-2">
{selectedProjects.size} selected
@@ -217,7 +220,7 @@ export default function ProjectsPage() {
<Button
variant="outline"
onClick={() => setIsSelectionMode(true)}
disabled={sortedProjects.length === 0}
disabled={projectsToDisplay.length === 0}
>
Select projects
</Button>
@@ -286,7 +289,7 @@ export default function ProjectsPage() {
</div>
</div>
{isSelectionMode && sortedProjects.length > 0 && (
{isSelectionMode && projectsToDisplay.length > 0 && (
<button
type="button"
onClick={() => handleSelectAll({ checked: !isAllSelected })}
@@ -306,39 +309,24 @@ export default function ProjectsPage() {
{isAllSelected ? "Deselect all" : "Select all"}
</span>
<span className="text-muted-foreground text-sm">
({selectedProjects.size} of {sortedProjects.length} selected)
({selectedProjects.size} of {projectsToDisplay.length} selected)
</span>
</button>
)}
{isLoading || !isInitialized ? (
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 8 }, (_, index) => (
<div
key={`skeleton-${index}`}
className="bg-background overflow-hidden border-none p-0"
>
<Skeleton className="bg-muted/50 aspect-square w-full" />
<div className="flex flex-col gap-1.5 px-0 pt-5">
<Skeleton className="bg-muted/50 h-4 w-3/4" />
<div className="flex items-center gap-1.5">
<Skeleton className="bg-muted/50 h-4 w-4" />
<Skeleton className="bg-muted/50 h-4 w-24" />
</div>
</div>
</div>
))}
</div>
) : sortedProjects.length === 0 && hasSearchQuery ? (
<NoResults
searchQuery={searchQuery}
onClearSearch={() => setSearchQuery("")}
<ProjectsLoader />
) : projectsToDisplay.length === 0 ? (
<EmptyState
search={{
query: searchQuery,
onClearSearch: () => setSearchQuery(""),
}}
onCreateProject={handleCreateProject}
/>
) : sortedProjects.length === 0 ? (
<NoProjects onCreateProject={handleCreateProject} />
) : (
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{sortedProjects.map((project) => (
{projectsToDisplay.map((project) => (
<ProjectCard
key={project.id}
project={project}
@@ -384,14 +372,6 @@ function ProjectCard({
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const editor = useEditor();
const formatDate = ({ date }: { date: Date }): string => {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
const handleDeleteProject = async () => {
await editor.project.deleteProject({ id: project.id });
setIsDropdownOpen(false);
@@ -407,19 +387,24 @@ function ProjectCard({
await editor.project.duplicateProject({ id: project.id });
};
const handleCardClick = ({e}: {}) => {
const handleCardClick = ({
event,
}: {
event: MouseEvent<HTMLButtonElement>;
}) => {
if (isSelectionMode) {
e.preventDefault();
event.preventDefault();
onSelect?.({ projectId: project.id, checked: !isSelected });
}
};
const handleCardKeyDown = ({
key,
preventDefault,
}: KeyboardEvent<HTMLButtonElement>) => {
if (isSelectionMode && (key === "Enter" || key === " ")) {
preventDefault();
event,
}: {
event: KeyboardEvent<HTMLButtonElement>;
}) => {
if (isSelectionMode && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
onSelect?.({ projectId: project.id, checked: !isSelected });
}
};
@@ -552,8 +537,8 @@ function ProjectCard({
{isSelectionMode ? (
<button
type="button"
onClick={handleCardClick}
onKeyDown={handleCardKeyDown}
onClick={(event) => handleCardClick({ event })}
onKeyDown={(event) => handleCardKeyDown({ event })}
className="group block w-full cursor-pointer text-left"
>
{cardContent}
@@ -578,6 +563,28 @@ function ProjectCard({
);
}
function ProjectsLoader() {
return (
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 8 }, (_, index) => (
<div
key={`skeleton-${index}`}
className="bg-background overflow-hidden border-none p-0"
>
<Skeleton className="bg-muted/50 aspect-square w-full" />
<div className="flex flex-col gap-1.5 px-0 pt-5">
<Skeleton className="bg-muted/50 h-4 w-3/4" />
<div className="flex items-center gap-1.5">
<Skeleton className="bg-muted/50 h-4 w-4" />
<Skeleton className="bg-muted/50 h-4 w-24" />
</div>
</div>
</div>
))}
</div>
);
}
function CreateButton({ onClick }: { onClick?: () => void }) {
return (
<Button className="flex" onClick={onClick}>
@@ -587,7 +594,35 @@ function CreateButton({ onClick }: { onClick?: () => void }) {
);
}
function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
function EmptyState({
search,
onCreateProject,
}: {
search: { query: string; onClearSearch: () => void };
onCreateProject: () => void;
}) {
const editor = useEditor();
const savedProjects = editor.project.getSavedProjects();
if (savedProjects.length > 0) {
return (
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
<div className="flex flex-col items-center gap-2">
<div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
<Search className="text-muted-foreground size-8" />
</div>
<h3 className="text-lg font-medium">No results found</h3>
<p className="text-muted-foreground max-w-md">
Your search for "{search.query}" did not return any results.
</p>
</div>
<Button onClick={search.onClearSearch} variant="outline">
Clear search
</Button>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
<div className="flex flex-col items-center gap-2">
@@ -601,34 +636,9 @@ function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
</p>
</div>
<Button size="lg" className="gap-2" onClick={onCreateProject}>
<Plus className="h-4 w-4" />
<Plus className="size-4" />
Create your first project
</Button>
</div>
);
}
function NoResults({
searchQuery,
onClearSearch,
}: {
searchQuery: string;
onClearSearch: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
<div className="flex flex-col items-center gap-2">
<div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
<Search className="text-muted-foreground size-8" />
</div>
<h3 className="text-lg font-medium">No results found</h3>
<p className="text-muted-foreground max-w-md">
Your search for "{searchQuery}" did not return any results.
</p>
</div>
<Button onClick={onClearSearch} variant="outline">
Clear search
</Button>
</div>
);
}
@@ -29,7 +29,6 @@ export function useTimelineZoom({
const isHorizontalScrollGesture =
event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY);
// allow scrollarea to handle horizontal scroll
if (isHorizontalScrollGesture) {
return;
}
+13 -1
View File
@@ -5,6 +5,14 @@ export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
export function formatDate({ date }: { date: Date }): string {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
export function capitalizeFirstLetter({ string }: { string: string }) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
@@ -38,7 +46,11 @@ export function generateUUID(): string {
);
}
export function isTypableDOMElement({ element }: { element: HTMLElement }): boolean {
export function isTypableDOMElement({
element,
}: {
element: HTMLElement;
}): boolean {
if (element.isContentEditable) return true;
if (element.tagName === "INPUT") {