summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/CONTRIBUTING.md36
-rw-r--r--.gitignore3
-rw-r--r--README.md26
-rw-r--r--apps/web/src/app/projects/page.tsx131
-rw-r--r--apps/web/src/components/ui/draggable-item.tsx1
-rw-r--r--apps/web/src/stores/project-store.ts41
6 files changed, 195 insertions, 43 deletions
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 77d7a6b6..75c4120b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -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:
diff --git a/.gitignore b/.gitignore
index 4869b9bd..38711b81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index ad9a6d38..1378b15a 100644
--- a/README.md
+++ b/README.md
@@ -39,25 +39,35 @@
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`:
-## Development Setup
+ ```bash
+ # Unix/Linux/Mac
+ cp .env.example .env.local
-### Prerequisites
+ # Windows Command Prompt
+ copy .env.example .env.local
-- Node.js 18+
-- Bun (latest version)
-- Docker (for local database)
+ # Windows PowerShell
+ Copy-Item .env.example .env.local
+ ```
+
+5. Install dependencies: `bun install`
+6. Start the development server: `bun dev`
+
+## Development Setup
### Local Development
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index 499b999a..04270aee 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -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>
+ );
+}
diff --git a/apps/web/src/components/ui/draggable-item.tsx b/apps/web/src/components/ui/draggable-item.tsx
index b15e8df8..dfb4dce7 100644
--- a/apps/web/src/components/ui/draggable-item.tsx
+++ b/apps/web/src/components/ui/draggable-item.tsx
@@ -177,6 +177,7 @@ function PlusButton({
e.stopPropagation();
onClick?.();
}}
+ title={tooltipText}
>
<Plus className="!size-3" />
</Button>
diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts
index efb9cc71..a8833982 100644
--- a/apps/web/src/stores/project-store.ts
+++ b/apps/web/src/stores/project-store.ts
@@ -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;
+ },
}));