mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
yeah
This commit is contained in:
@@ -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"
|
"/projects/page": "app/projects/page.js"
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { KeyboardEvent, MouseEvent } from "react";
|
import type { KeyboardEvent, MouseEvent } from "react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { DeleteProjectDialog } from "@/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/components/delete-project-dialog";
|
||||||
import { RenameProjectDialog } from "@/components/rename-project-dialog";
|
import { RenameProjectDialog } from "@/components/rename-project-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -39,6 +39,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import type { TProjectMetadata } from "@/types/project";
|
import type { TProjectMetadata } from "@/types/project";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { formatDate } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||||
@@ -51,8 +52,6 @@ export default function ProjectsPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
|
||||||
const hasSearchQuery = searchQuery.trim().length > 0;
|
|
||||||
|
|
||||||
const handleCreateProject = async () => {
|
const handleCreateProject = async () => {
|
||||||
try {
|
try {
|
||||||
const projectId = await editor.project.createNewProject({
|
const projectId = await editor.project.createNewProject({
|
||||||
@@ -98,7 +97,9 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
const handleSelectAll = ({ checked }: { checked: boolean }) => {
|
const handleSelectAll = ({ checked }: { checked: boolean }) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setSelectedProjects(new Set(sortedProjects.map((project) => project.id)));
|
setSelectedProjects(
|
||||||
|
new Set(projectsToDisplay.map((project) => project.id)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedProjects(new Set());
|
setSelectedProjects(new Set());
|
||||||
}
|
}
|
||||||
@@ -128,16 +129,18 @@ export default function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedProjects = editor.project.getFilteredAndSortedProjects({
|
const projectsToDisplay = editor.project.getFilteredAndSortedProjects({
|
||||||
searchQuery,
|
searchQuery,
|
||||||
sortOption,
|
sortOption,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAllSelected =
|
const isAllSelected =
|
||||||
sortedProjects.length > 0 &&
|
projectsToDisplay.length > 0 &&
|
||||||
selectedProjects.size === sortedProjects.length;
|
selectedProjects.size === projectsToDisplay.length;
|
||||||
|
|
||||||
const hasSomeSelected =
|
const hasSomeSelected =
|
||||||
selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length;
|
selectedProjects.size > 0 &&
|
||||||
|
selectedProjects.size < projectsToDisplay.length;
|
||||||
|
|
||||||
const isLoading = editor.project.getIsLoading();
|
const isLoading = editor.project.getIsLoading();
|
||||||
const isInitialized = editor.project.getIsInitialized();
|
const isInitialized = editor.project.getIsInitialized();
|
||||||
@@ -186,8 +189,8 @@ export default function ProjectsPage() {
|
|||||||
Your projects
|
Your projects
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{sortedProjects.length}{" "}
|
{projectsToDisplay.length}{" "}
|
||||||
{sortedProjects.length === 1 ? "project" : "projects"}
|
{projectsToDisplay.length === 1 ? "project" : "projects"}
|
||||||
{isSelectionMode && selectedProjects.size > 0 && (
|
{isSelectionMode && selectedProjects.size > 0 && (
|
||||||
<span className="text-primary ml-2">
|
<span className="text-primary ml-2">
|
||||||
• {selectedProjects.size} selected
|
• {selectedProjects.size} selected
|
||||||
@@ -217,7 +220,7 @@ export default function ProjectsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setIsSelectionMode(true)}
|
onClick={() => setIsSelectionMode(true)}
|
||||||
disabled={sortedProjects.length === 0}
|
disabled={projectsToDisplay.length === 0}
|
||||||
>
|
>
|
||||||
Select projects
|
Select projects
|
||||||
</Button>
|
</Button>
|
||||||
@@ -286,7 +289,7 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSelectionMode && sortedProjects.length > 0 && (
|
{isSelectionMode && projectsToDisplay.length > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelectAll({ checked: !isAllSelected })}
|
onClick={() => handleSelectAll({ checked: !isAllSelected })}
|
||||||
@@ -306,39 +309,24 @@ export default function ProjectsPage() {
|
|||||||
{isAllSelected ? "Deselect all" : "Select all"}
|
{isAllSelected ? "Deselect all" : "Select all"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
({selectedProjects.size} of {sortedProjects.length} selected)
|
({selectedProjects.size} of {projectsToDisplay.length} selected)
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading || !isInitialized ? (
|
{isLoading || !isInitialized ? (
|
||||||
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
<ProjectsLoader />
|
||||||
{Array.from({ length: 8 }, (_, index) => (
|
) : projectsToDisplay.length === 0 ? (
|
||||||
<div
|
<EmptyState
|
||||||
key={`skeleton-${index}`}
|
search={{
|
||||||
className="bg-background overflow-hidden border-none p-0"
|
query: searchQuery,
|
||||||
>
|
onClearSearch: () => setSearchQuery(""),
|
||||||
<Skeleton className="bg-muted/50 aspect-square w-full" />
|
}}
|
||||||
<div className="flex flex-col gap-1.5 px-0 pt-5">
|
onCreateProject={handleCreateProject}
|
||||||
<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("")}
|
|
||||||
/>
|
/>
|
||||||
) : 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">
|
<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
|
<ProjectCard
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
@@ -384,14 +372,6 @@ function ProjectCard({
|
|||||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
|
||||||
const formatDate = ({ date }: { date: Date }): string => {
|
|
||||||
return date.toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteProject = async () => {
|
const handleDeleteProject = async () => {
|
||||||
await editor.project.deleteProject({ id: project.id });
|
await editor.project.deleteProject({ id: project.id });
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
@@ -407,19 +387,24 @@ function ProjectCard({
|
|||||||
await editor.project.duplicateProject({ id: project.id });
|
await editor.project.duplicateProject({ id: project.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardClick = ({e}: {}) => {
|
const handleCardClick = ({
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
event: MouseEvent<HTMLButtonElement>;
|
||||||
|
}) => {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
onSelect?.({ projectId: project.id, checked: !isSelected });
|
onSelect?.({ projectId: project.id, checked: !isSelected });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardKeyDown = ({
|
const handleCardKeyDown = ({
|
||||||
key,
|
event,
|
||||||
preventDefault,
|
}: {
|
||||||
}: KeyboardEvent<HTMLButtonElement>) => {
|
event: KeyboardEvent<HTMLButtonElement>;
|
||||||
if (isSelectionMode && (key === "Enter" || key === " ")) {
|
}) => {
|
||||||
preventDefault();
|
if (isSelectionMode && (event.key === "Enter" || event.key === " ")) {
|
||||||
|
event.preventDefault();
|
||||||
onSelect?.({ projectId: project.id, checked: !isSelected });
|
onSelect?.({ projectId: project.id, checked: !isSelected });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -552,8 +537,8 @@ function ProjectCard({
|
|||||||
{isSelectionMode ? (
|
{isSelectionMode ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCardClick}
|
onClick={(event) => handleCardClick({ event })}
|
||||||
onKeyDown={handleCardKeyDown}
|
onKeyDown={(event) => handleCardKeyDown({ event })}
|
||||||
className="group block w-full cursor-pointer text-left"
|
className="group block w-full cursor-pointer text-left"
|
||||||
>
|
>
|
||||||
{cardContent}
|
{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 }) {
|
function CreateButton({ onClick }: { onClick?: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Button className="flex" onClick={onClick}>
|
<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 (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
|
<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="flex flex-col items-center gap-2">
|
||||||
@@ -601,34 +636,9 @@ function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button size="lg" className="gap-2" onClick={onCreateProject}>
|
<Button size="lg" className="gap-2" onClick={onCreateProject}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="size-4" />
|
||||||
Create your first project
|
Create your first project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 =
|
const isHorizontalScrollGesture =
|
||||||
event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY);
|
event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY);
|
||||||
|
|
||||||
// allow scrollarea to handle horizontal scroll
|
|
||||||
if (isHorizontalScrollGesture) {
|
if (isHorizontalScrollGesture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export function cn(...inputs: ClassValue[]): string {
|
|||||||
return twMerge(clsx(inputs));
|
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 }) {
|
export function capitalizeFirstLetter({ string }: { string: string }) {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
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.isContentEditable) return true;
|
||||||
|
|
||||||
if (element.tagName === "INPUT") {
|
if (element.tagName === "INPUT") {
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ Review every point below carefully for files to ensure they follow consistent co
|
|||||||
```
|
```
|
||||||
18. Components render UI. Domain logic (data transformations, business rules, state mutations) lives in hooks, utilities, or managers. Simple interaction logic (gesture detection, modifier keys) can stay in components if not too many lines of code/complex.
|
18. Components render UI. Domain logic (data transformations, business rules, state mutations) lives in hooks, utilities, or managers. Simple interaction logic (gesture detection, modifier keys) can stay in components if not too many lines of code/complex.
|
||||||
|
|
||||||
19. Readability matters more than short names in the age of AI coding. Do not shorten names for the sake of "typing less". Nobody is typing code these days. AI agents are. Hence, clarity and readability are the most important things. Example: "event" is better than "e". "element" is better than "el".
|
19. Readability matters more than short names in the age of AI coding. Do not shorten names for the sake of "typing less". Nobody is typing code these days. AI agents are. Hence, clarity and readability are the most important things. Example: "element" is better than "el".
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user