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..3dce048 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,14 @@ A free, open-source video editor for web, desktop, and mobile. ## Contributing +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) 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/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/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx deleted file mode 100644 index ebf2eb3..0000000 --- a/apps/web/src/app/(auth)/login/page.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { signIn } from "@/lib/auth-client"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Suspense, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -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"; - -function LoginForm() { - const router = useRouter(); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(null); - const [isEmailLoading, setIsEmailLoading] = useState(false); - const [isGoogleLoading, setIsGoogleLoading] = useState(false); - - const handleLogin = async () => { - setError(null); - setIsEmailLoading(true); - - const { error } = await signIn.email({ - email, - password, - }); - - if (error) { - setError(error.message || "An unexpected error occurred."); - setIsEmailLoading(false); - return; - } - - router.push("/editor"); - }; - - const handleGoogleLogin = async () => { - setError(null); - setIsGoogleLoading(true); - - try { - await signIn.social({ - provider: "google", - }); - router.push("/editor"); - } catch (error) { - setError("Failed to sign in with Google. Please try again."); - setIsGoogleLoading(false); - } - }; - - const isAnyLoading = isEmailLoading || isGoogleLoading; - - return ( -
- {error && ( - - Error - {error} - - )} - - -
-
- -
-
- Or continue with -
-
-
-
- - setEmail(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setPassword(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
- -
-
- ); -} - -export default function LoginPage() { - const router = useRouter(); - - return ( -
- - - - Welcome back - - Sign in to your account to continue - - - - - -
}> - - -
- Don't have an account?{" "} - - Sign up - -
- - - - ); -} diff --git a/apps/web/src/app/(auth)/signup/page.tsx b/apps/web/src/app/(auth)/signup/page.tsx deleted file mode 100644 index 3d125a0..0000000 --- a/apps/web/src/app/(auth)/signup/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"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 { Suspense, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import Link from "next/link"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Loader2, ArrowLeft } from "lucide-react"; -import { GoogleIcon } from "@/components/icons"; - -function SignUpForm() { - const router = useRouter(); - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(null); - const [isEmailLoading, setIsEmailLoading] = useState(false); - const [isGoogleLoading, setIsGoogleLoading] = useState(false); - - const handleSignUp = async () => { - setError(null); - setIsEmailLoading(true); - - const { error } = await signUp.email({ - name, - email, - password, - }); - - if (error) { - setError(error.message || "An unexpected error occurred."); - setIsEmailLoading(false); - return; - } - - router.push("/auth/login"); - }; - - const handleGoogleSignUp = async () => { - setError(null); - setIsGoogleLoading(true); - - try { - await signIn.social({ - provider: "google", - }); - - router.push("/editor"); - } catch (error) { - setError("Failed to sign up with Google. Please try again."); - setIsGoogleLoading(false); - } - }; - - const isAnyLoading = isEmailLoading || isGoogleLoading; - - return ( -
- {error && ( - - Error - {error} - - )} - - - -
-
- -
-
- Or continue with -
-
- -
-
- - setName(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setEmail(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setPassword(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
- -
-
- ); -} - -export default function SignUpPage() { - const router = useRouter(); - - return ( -
- - - - - Create your account - - Get started with your free account today - - - - - -
}> - - -
- Already have an account?{" "} - - Sign in - -
- - - - ); -} diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx new file mode 100644 index 0000000..18117f4 --- /dev/null +++ b/apps/web/src/app/auth/login/page.tsx @@ -0,0 +1,5 @@ +import { AuthForm } from "@/components/auth-form"; + +export default function LoginPage() { + return ; +} diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx new file mode 100644 index 0000000..109ae5c --- /dev/null +++ b/apps/web/src/app/auth/signup/page.tsx @@ -0,0 +1,5 @@ +import { AuthForm } from "@/components/auth-form"; + +export default function SignUpPage() { + return ; +} 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..182137e --- /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: "/auth/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: "/auth/login", + linkLabel: "Sign in", + successRedirect: "/auth/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/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