diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..633521a --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +!apps/web/.env.example \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 793d175..b9e2275 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -13,33 +13,104 @@ Thank you for your interest in contributing to OpenCut! This document provides g ## Development Setup ### Prerequisites -- Node.js 18+ + +- Node.js 18+ - Bun (latest version) - Docker (for local database) ### Local Development -1. Copy `.env.example` to `.env.local` and configure your environment variables -2. Start the database: `docker-compose up -d` (run from project root) -3. Navigate to the web app: `cd apps/web` -4. Run database migrations: `bun run db:migrate` -5. Start the development server: `bun run dev` + +1. Start the database and Redis services: + + ```bash + # From project root + docker-compose up -d + ``` + +2. Navigate to the web app directory: + + ```bash + cd apps/web + ``` + +3. 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 + ``` + +4. Configure required environment variables in `.env.local`: + + **Required Variables:** + + ```bash + # Database (matches docker-compose.yaml) + DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" + + # Generate a secure secret for Better Auth + BETTER_AUTH_SECRET="your-generated-secret-here" + BETTER_AUTH_URL="http://localhost:3000" + + # Redis (matches docker-compose.yaml) + UPSTASH_REDIS_REST_URL="http://localhost:8079" + UPSTASH_REDIS_REST_TOKEN="example_token" + + # Development + NODE_ENV="development" + ``` + + **Generate BETTER_AUTH_SECRET:** + + ```bash + # Unix/Linux/Mac + openssl rand -base64 32 + + # Windows PowerShell (simple method) + [System.Web.Security.Membership]::GeneratePassword(32, 0) + + # Cross-platform (using Node.js) + node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" + + # Or use an online generator: https://generate-secret.vercel.app/32 + ``` + + **Optional Variables (for Google OAuth):** + + ```bash + # Only needed if you want to test Google login + GOOGLE_CLIENT_ID="your-google-client-id" + GOOGLE_CLIENT_SECRET="your-google-client-secret" + ``` + +5. Run database migrations: `bun run db:migrate` +6. Start the development server: `bun run dev` ## How to Contribute ### Reporting Bugs + - Use the bug report template - Include steps to reproduce - Provide screenshots if applicable ### Suggesting Features + - Use the feature request template - Explain the use case - Consider implementation details ### Code Contributions + 1. Create a new branch: `git checkout -b feature/your-feature-name` 2. Make your changes -3. Navigate to the web app directory: `cd apps/web` +3. Navigate to the web app directory: `cd apps/web` 4. Run the linter: `bun run lint` 5. Format your code: `bunx biome format --write .` 6. Commit your changes with a descriptive message @@ -66,4 +137,4 @@ Thank you for your interest in contributing to OpenCut! This document provides g - Follow our Code of Conduct - Help others in discussions and issues -Thank you for contributing! \ No newline at end of file +Thank you for contributing! diff --git a/README.md b/README.md index 3ccb2b0..407f530 100644 --- a/README.md +++ b/README.md @@ -26,29 +26,82 @@ A free, open-source video editor for web, desktop, and mobile. ## Getting Started -1. **Clone the repository:** - ```bash - git clone - cd OpenCut - ``` -2. **Install dependencies:** - ```bash - cd apps/web - npm install - # or, with Bun - bun install - ``` -3. **Run the development server:** - ```bash - npm run dev - # or, with Bun - bun run dev - ``` -4. **Open in browser:** - Visit [http://localhost:3000](http://localhost:3000) +### Prerequisites + +Before you begin, ensure you have the following installed on your system: + +- [Bun](https://bun.sh/docs/installation) +- [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) + +### Setup + +1. **Clone the repository** + ```bash + git clone + cd OpenCut + ``` + +2. **Start backend services** + From the project root, start the PostgreSQL and Redis services: + ```bash + docker-compose up -d + ``` + +3. **Set up environment variables** + Navigate into the web app's directory and create a `.env` file from the example: + ```bash + cd apps/web + cp .env.example .env + ``` + *The default values in the `.env` file should work for local development.* + +4. **Install dependencies** + Install the project dependencies using `bun` (recommended) or `npm`. + ```bash + # With bun + bun install + + # Or with npm + npm install + ``` + +5. **Run database migrations** + Apply the database schema to your local database: + ```bash + # With bun + bun run db:push:local + + # Or with npm + npm run db:push:local + ``` + +6. **Start the development server** + ```bash + # With bun + bun run dev + + # Or with npm + npm run dev + ``` + +The application will be available at [http://localhost:3000](http://localhost:3000). + +======= + ## Contributing +Visit [CONTRIBUTING.md](.github/CONTRIBUTING.md) +======= +We welcome contributions! Please see our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions and development guidelines. + +Quick start for contributors: + +- Fork the repo and clone locally +- Follow the setup instructions in CONTRIBUTING.md +- Create a feature branch and submit a PR + ## License -MIT [Details](LICENSE) +[MIT LICENSE](LICENSE) diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..ca1f098 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,36 @@ +FROM oven/bun:latest AS base + +# Install dependencies +FROM base AS deps +WORKDIR /app +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +# Build the application +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN bun run build + +# Production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["bun", "server.js"] \ No newline at end of file diff --git a/apps/web/bun.lock b/apps/web/bun.lock index 141e7db..3aba715 100644 --- a/apps/web/bun.lock +++ b/apps/web/bun.lock @@ -41,6 +41,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.1", + "zod": "^3.25.67", "zustand": "^5.0.2", }, "devDependencies": { diff --git a/apps/web/bun.lockb b/apps/web/bun.lockb new file mode 100755 index 0000000..30e5a2f Binary files /dev/null and b/apps/web/bun.lockb differ diff --git a/apps/web/package.json b/apps/web/package.json index 581aa02..0a2aa20 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,7 +2,7 @@ "name": "next-template", "version": "0.1.0", "private": true, - "packageManager": "bun@1.2.2", + "packageManager": "bun@1.2.2", "scripts": { "dev": "next dev --turbopack", "build": "next build", @@ -51,6 +51,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.1", + "zod": "^3.25.67", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png index a0af7a1..fc9e790 100644 Binary files a/apps/web/public/logo.png and b/apps/web/public/logo.png differ diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico index 66b4ee0..cb20127 100644 Binary files a/apps/web/src/app/favicon.ico and b/apps/web/src/app/favicon.ico differ diff --git a/apps/web/src/components/auth-form.tsx b/apps/web/src/components/auth-form.tsx new file mode 100644 index 0000000..3dd5603 --- /dev/null +++ b/apps/web/src/components/auth-form.tsx @@ -0,0 +1,398 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { signUp, signIn } from "@/lib/auth-client"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import Link from "next/link"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { ArrowLeft, Loader2 } from "lucide-react"; +import { GoogleIcon } from "@/components/icons"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +// Zod schemas +const loginSchema = z.object({ + email: z.string().email("Please enter a valid email address"), + password: z.string().min(1, "Password is required"), +}); + +const signupSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Please enter a valid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), +}); + +type LoginFormData = z.infer; +type SignupFormData = z.infer; + +interface AuthFormProps { + mode: "login" | "signup"; +} + +const authConfig = { + login: { + title: "Welcome back", + description: "Sign in to your account to continue", + buttonText: "Sign in", + linkText: "Don't have an account?", + linkHref: "/signup", + linkLabel: "Sign up", + successRedirect: "/editor", + }, + signup: { + title: "Create your account", + description: "Get started with your free account today", + buttonText: "Create account", + linkText: "Already have an account?", + linkHref: "/login", + linkLabel: "Sign in", + successRedirect: "/login", + }, +} as const; + +interface AuthFormContentProps { + error: string | null; + setError: (error: string | null) => void; + isGoogleLoading: boolean; + config: typeof authConfig.login | typeof authConfig.signup; + router: ReturnType; +} + +function LoginFormContent({ + error, + setError, + isGoogleLoading, + config, + router, +}: AuthFormContentProps) { + const form = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { email: "", password: "" }, + }); + + const { isSubmitting } = form.formState; + const isAnyLoading = isSubmitting || isGoogleLoading; + + const onSubmit = async (data: LoginFormData) => { + setError(null); + + try { + const { error } = await signIn.email({ + email: data.email, + password: data.password, + }); + + if (error) { + setError(error.message || "An unexpected error occurred."); + return; + } + + router.push(config.successRedirect); + } catch (error) { + setError("An unexpected error occurred. Please try again."); + } + }; + + return ( +
+ + ( + + Email + + + + + + )} + /> + + ( + + Password + + + + + + )} + /> + + + + + ); +} + +function SignupFormContent({ + error, + setError, + isGoogleLoading, + config, + router, +}: AuthFormContentProps) { + const form = useForm({ + resolver: zodResolver(signupSchema), + defaultValues: { email: "", password: "", name: "" }, + }); + + const { isSubmitting } = form.formState; + const isAnyLoading = isSubmitting || isGoogleLoading; + + const onSubmit = async (data: SignupFormData) => { + setError(null); + + try { + const { error } = await signUp.email({ + name: data.name, + email: data.email, + password: data.password, + }); + + if (error) { + setError(error.message || "An unexpected error occurred."); + return; + } + + router.push(config.successRedirect); + } catch (error) { + setError("An unexpected error occurred. Please try again."); + } + }; + + return ( +
+ + ( + + Full Name + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + ( + + Password + + + + + + )} + /> + + + + + ); +} + +export function AuthForm({ mode }: AuthFormProps) { + const router = useRouter(); + const [error, setError] = useState(null); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const config = authConfig[mode]; + + const handleGoogleAuth = async () => { + setError(null); + setIsGoogleLoading(true); + + try { + await signIn.social({ + provider: "google", + }); + + router.push(config.successRedirect); + } catch (error) { + setError( + `Failed to ${mode === "login" ? "sign in" : "sign up"} with Google. Please try again.` + ); + setIsGoogleLoading(false); + } + }; + + return ( +
+ + + + + + {config.title} + + + {config.description} + + + +
+ {error && ( + + Error + {error} + + )} + + + +
+
+ +
+
+ + Or continue with + +
+
+ + {mode === "login" ? ( + + ) : ( + + )} +
+ +
+ {config.linkText}{" "} + + {config.linkLabel} + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/editor/properties-panel.tsx b/apps/web/src/components/editor/properties-panel.tsx index 2b211c2..c784dab 100644 --- a/apps/web/src/components/editor/properties-panel.tsx +++ b/apps/web/src/components/editor/properties-panel.tsx @@ -38,7 +38,6 @@ export function PropertiesPanel() { ? mediaItems.find((item) => item.id === firstVideoClip.mediaId) : null; - // Get the first image clip for preview (simplified) const firstImageClip = tracks .flatMap((track) => track.clips) .find((clip) => { diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 675b398..9af7a20 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -557,8 +557,26 @@ export function Timeline() { {/* Playhead in ruler */}
{ + e.preventDefault(); + e.stopPropagation(); + const handleMouseMove = (e: MouseEvent) => { + const timeline = timelineRef.current; // Get timeline element ref to track the position + if (!timeline) return; // If no timeline element, exit + const rect = timeline.getBoundingClientRect(); // Get the bounding rect of the timeline element + const mouseX = Math.max(0, e.clientX - rect.left); // Calculate the mouse position relative to the timeline element + const newTime = mouseX / (50 * zoomLevel); // Calculate the time based on the mouse position + seek(newTime); // Set the current time + }; + const handleMouseUp = () => { + window.removeEventListener("mousemove", handleMouseMove); // Remove the mousemove event listener + window.removeEventListener("mouseup", handleMouseUp); // Remove the mouseup event listener + }; + window.addEventListener("mousemove", handleMouseMove); // Add the mousemove event listener + window.addEventListener("mouseup", handleMouseUp); // Add the mouseup event listener + }} >
@@ -670,7 +688,7 @@ export function Timeline() { {/* Playhead for tracks area */}
); -} \ No newline at end of file +} diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index f2c70f5..714d432 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -84,12 +84,12 @@ export function Hero({ signupCount }: HeroProps) { The open source

- CapCut alternative. + video editor