Merge branch 'main' of https://github.com/mazeincoding/AppCut
This commit is contained in:
commit
ce47e88e34
|
|
@ -4,11 +4,35 @@ Thank you for your interest in contributing to OpenCut! This document provides g
|
|||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/en/) (v18 or later)
|
||||
- [Bun](https://bun.sh/docs/installation)
|
||||
(for `npm` alternative)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
|
||||
|
||||
### Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork locally
|
||||
3. Navigate to the web app directory: `cd apps/web`
|
||||
4. Install dependencies: `bun install`
|
||||
5. Start the development server: `bun run dev`
|
||||
4. Copy `.env.example` to `.env.local`:
|
||||
|
||||
```bash
|
||||
# Unix/Linux/Mac
|
||||
cp .env.example .env.local
|
||||
|
||||
# Windows Command Prompt
|
||||
copy .env.example .env.local
|
||||
|
||||
# Windows PowerShell
|
||||
Copy-Item .env.example .env.local
|
||||
```
|
||||
|
||||
5. Install dependencies: `bun install`
|
||||
6. Start the development server: `bun run dev`
|
||||
|
||||
> **Note:** If you see an error like `Unsupported URL Type "workspace:*"` when running `npm install`, you have two options:
|
||||
>
|
||||
|
|
@ -18,6 +42,7 @@ Thank you for your interest in contributing to OpenCut! This document provides g
|
|||
## What to Focus On
|
||||
|
||||
**🎯 Good Areas to Contribute:**
|
||||
|
||||
- Timeline functionality and UI improvements
|
||||
- Project management features
|
||||
- Performance optimizations
|
||||
|
|
@ -26,6 +51,7 @@ Thank you for your interest in contributing to OpenCut! This document provides g
|
|||
- Documentation and testing
|
||||
|
||||
**⚠️ Areas to Avoid:**
|
||||
|
||||
- Preview panel enhancements (text fonts, stickers, effects)
|
||||
- Export functionality improvements
|
||||
- Preview rendering optimizations
|
||||
|
|
@ -38,12 +64,6 @@ If you're unsure whether your idea falls into the preview category, feel free to
|
|||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- Bun (latest version)
|
||||
- Docker (for local database)
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Start the database and Redis services:
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@
|
|||
|
||||
# debug
|
||||
/apps/web/npm-debug.log*
|
||||
/apps/web/yarn-debug.log*
|
||||
/apps/web/yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
/apps/web/.env*
|
||||
|
|
@ -18,7 +16,6 @@
|
|||
|
||||
# typescript
|
||||
/apps/web/next-env.d.ts
|
||||
/apps/web/yarn.lock
|
||||
|
||||
# asdf version management
|
||||
.tool-versions
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -39,26 +39,36 @@
|
|||
|
||||
Before you begin, ensure you have the following installed on your system:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/) (v18 or later)
|
||||
- [Bun](https://bun.sh/docs/installation)
|
||||
(for `npm` alternative)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
- [Node.js](https://nodejs.org/en/) (for `npm` alternative)
|
||||
|
||||
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to run the frontend or want to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
|
||||
|
||||
### Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork locally
|
||||
3. Navigate to the web app directory: `cd apps/web`
|
||||
4. Install dependencies: `bun install`
|
||||
5. Start the development server: `bun dev`
|
||||
4. Copy `.env.example` to `.env.local`:
|
||||
|
||||
```bash
|
||||
# Unix/Linux/Mac
|
||||
cp .env.example .env.local
|
||||
|
||||
# Windows Command Prompt
|
||||
copy .env.example .env.local
|
||||
|
||||
# Windows PowerShell
|
||||
Copy-Item .env.example .env.local
|
||||
```
|
||||
|
||||
5. Install dependencies: `bun install`
|
||||
6. Start the development server: `bun dev`
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- Bun (latest version)
|
||||
- Docker (for local database)
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Start the database and Redis services:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useState } from "react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Plus,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
Loader2,
|
||||
X,
|
||||
Trash2,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { TProject } from "@/types/project";
|
||||
import Image from "next/image";
|
||||
|
|
@ -28,6 +30,13 @@ 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,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const {
|
||||
|
|
@ -36,6 +45,7 @@ export default function ProjectsPage() {
|
|||
isLoading,
|
||||
isInitialized,
|
||||
deleteProject,
|
||||
getFilteredAndSortedProjects,
|
||||
} = useProjectStore();
|
||||
const router = useRouter();
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
|
|
@ -43,6 +53,8 @@ export default function ProjectsPage() {
|
|||
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");
|
||||
|
|
@ -62,7 +74,7 @@ export default function ProjectsPage() {
|
|||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedProjects(new Set(savedProjects.map((p) => p.id)));
|
||||
setSelectedProjects(new Set(sortedProjects.map((p) => p.id)));
|
||||
} else {
|
||||
setSelectedProjects(new Set());
|
||||
}
|
||||
|
|
@ -82,10 +94,13 @@ export default function ProjectsPage() {
|
|||
setIsBulkDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
const sortedProjects = getFilteredAndSortedProjects(searchQuery, sortOption);
|
||||
|
||||
const allSelected =
|
||||
savedProjects.length > 0 && selectedProjects.size === savedProjects.length;
|
||||
sortedProjects.length > 0 &&
|
||||
selectedProjects.size === sortedProjects.length;
|
||||
const someSelected =
|
||||
selectedProjects.size > 0 && selectedProjects.size < savedProjects.length;
|
||||
selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
|
|
@ -172,25 +187,40 @@ export default function ProjectsPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{isSelectionMode && savedProjects.length > 0 && (
|
||||
<div
|
||||
className="mb-6 p-4 bg-muted/30 rounded-lg border cursor-pointer hover:bg-muted/40 transition-colors"
|
||||
onClick={() => handleSelectAll(!allSelected)}
|
||||
>
|
||||
<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={allSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
checked={someSelected ? "indeterminate" : allSelected}
|
||||
onCheckedChange={(value) => handleSelectAll(!!value)}
|
||||
/>
|
||||
<div className="flex-1 flex items-center justify-start gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
{allSelected ? "Deselect All" : "Select All"}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({selectedProjects.size} of {savedProjects.length} selected)
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -201,15 +231,21 @@ export default function ProjectsPage() {
|
|||
</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">
|
||||
{savedProjects.map((project) => (
|
||||
{sortedProjects.map((project) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={selectedProjects.has(project.id)}
|
||||
onSelect={handleSelectProject}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -230,6 +266,7 @@ interface ProjectCardProps {
|
|||
isSelectionMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
onSelect?: (projectId: string, checked: boolean) => void;
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
function ProjectCard({
|
||||
|
|
@ -237,6 +274,7 @@ function ProjectCard({
|
|||
isSelectionMode = false,
|
||||
isSelected = false,
|
||||
onSelect,
|
||||
searchQuery,
|
||||
}: ProjectCardProps) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
|
|
@ -292,7 +330,7 @@ function ProjectCard({
|
|||
{/* Selection checkbox */}
|
||||
{isSelectionMode && (
|
||||
<div className="absolute top-3 left-3 z-10">
|
||||
<div className="w-5 h-5 rounded-full bg-background/80 backdrop-blur-sm border flex items-center justify-center">
|
||||
<div className="w-5 h-5 rounded bg-background/80 backdrop-blur-sm border flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) =>
|
||||
|
|
@ -322,10 +360,10 @@ function ProjectCard({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="px-0 pt-5 flex flex-col gap-1 p-2">
|
||||
<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}
|
||||
<Highlight text={project.name} query={searchQuery} />
|
||||
</h3>
|
||||
{!isSelectionMode && (
|
||||
<DropdownMenu
|
||||
|
|
@ -432,7 +470,7 @@ function ProjectCard({
|
|||
<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}
|
||||
<Highlight text={project.name} query={searchQuery} />
|
||||
</h3>
|
||||
<DropdownMenu
|
||||
open={isDropdownOpen}
|
||||
|
|
@ -519,6 +557,28 @@ function ProjectCard({
|
|||
);
|
||||
}
|
||||
|
||||
function Highlight({ text, query }: { text: string; query?: string }) {
|
||||
if (!query || query.trim() === "") {
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const parts = text.split(new RegExp(`(${escapedQuery})`, "gi"));
|
||||
return (
|
||||
<span>
|
||||
{parts.map((part, i) =>
|
||||
part.toLowerCase() === query.toLowerCase() ? (
|
||||
<mark key={i} className="bg-yellow-200 text-black">
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateButton({ onClick }: { onClick?: () => void }) {
|
||||
return (
|
||||
<Button className="flex" onClick={onClick}>
|
||||
|
|
@ -546,3 +606,26 @@ function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoResults({
|
||||
searchQuery,
|
||||
onClearSearch,
|
||||
}: {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ function PlusButton({
|
|||
e.stopPropagation();
|
||||
onClick?.();
|
||||
}}
|
||||
title={tooltipText}
|
||||
>
|
||||
<Plus className="!size-3" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ interface ProjectStore {
|
|||
options?: { backgroundColor?: string; blurIntensity?: number }
|
||||
) => Promise<void>;
|
||||
updateProjectFps: (fps: number) => Promise<void>;
|
||||
|
||||
getFilteredAndSortedProjects: (
|
||||
searchQuery: string,
|
||||
sortOption: string
|
||||
) => TProject[];
|
||||
}
|
||||
|
||||
export const useProjectStore = create<ProjectStore>((set, get) => ({
|
||||
|
|
@ -317,4 +322,40 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
});
|
||||
}
|
||||
},
|
||||
|
||||
getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => {
|
||||
const { savedProjects } = get();
|
||||
|
||||
// Filter projects by search query
|
||||
const filteredProjects = savedProjects.filter((project) =>
|
||||
project.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
// Sort filtered projects
|
||||
const sortedProjects = [...filteredProjects].sort((a, b) => {
|
||||
const [key, order] = sortOption.split("-");
|
||||
|
||||
if (key !== "createdAt" && key !== "name") {
|
||||
console.warn(`Invalid sort key: ${key}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const aValue = a[key];
|
||||
const bValue = b[key];
|
||||
|
||||
if (aValue === undefined || bValue === undefined) return 0;
|
||||
|
||||
if (order === "asc") {
|
||||
if (aValue < bValue) return -1;
|
||||
if (aValue > bValue) return 1;
|
||||
return 0;
|
||||
} else {
|
||||
if (aValue > bValue) return -1;
|
||||
if (aValue < bValue) return 1;
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return sortedProjects;
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue