From b772c6f68e81c4c88186219dc841a14ab278cf50 Mon Sep 17 00:00:00 2001 From: YaoSiQian <2229561981@qq.com> Date: Tue, 24 Jun 2025 05:00:34 +0800 Subject: fix(docker): web service --- README.md | 14 ++++++++++++++ apps/web/Dockerfile | 36 ++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 41 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 apps/web/Dockerfile diff --git a/README.md b/README.md index 3ccb2b04..998acd36 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,20 @@ A free, open-source video editor for web, desktop, and mobile. 4. **Open in browser:** Visit [http://localhost:3000](http://localhost:3000) +## Run with Docker +1. **Prepare environment variables:** + Edit [docker-compose.yaml](https://github.com/OpenCut-app/OpenCut/blob/main/docker-compose.yaml#L57-L64) +2. **Build and run:** + ```bash + docker-compose up -d --build + ``` +3. *(Optional)* **Migrate database:** + ```bash + docker-compose exec web bun run db:migrate + ``` +4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) + ## Contributing ## License diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 00000000..ca1f0987 --- /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/docker-compose.yaml b/docker-compose.yaml index c1c61a47..c654926a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,7 +18,8 @@ services: start_period: 10s redis: - image: redis + image: redis:7-alpine + restart: unless-stopped ports: - "6379:6379" healthcheck: @@ -36,12 +37,46 @@ services: SRH_MODE: env SRH_TOKEN: example_token SRH_CONNECTION_STRING: "redis://redis:6379" + depends_on: + redis: + condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:80 || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 10s - -volumes: + web: + build: + context: ./apps/web + dockerfile: ./apps/web/Dockerfile + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://opencut:opencutthegoat@db:5432/opencut + - BETTER_AUTH_URL=http://localhost:3000 + - BETTER_AUTH_SECRET=your-production-secret-key-here + - UPSTASH_REDIS_REST_URL=http://serverless-redis-http:80 + - UPSTASH_REDIS_REST_TOKEN=example_token + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + depends_on: + db: + condition: service_healthy + serverless-redis-http: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + +volumes: postgres_data: + +networks: + default: + name: opencut-network -- cgit v1.3.1 From 79ac2c28238642436bc57a55671731460ccf2640 Mon Sep 17 00:00:00 2001 From: YaoSiQian Date: Tue, 24 Jun 2025 05:06:21 +0800 Subject: style(README): hot fix at line break --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 998acd36..1c377b60 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ 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 @@ -39,30 +41,37 @@ A free, open-source video editor for web, desktop, and mobile. bun install ``` 3. **Run the development server:** + ```bash npm run dev # or, with Bun bun run dev ``` -4. **Open in browser:** +4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) ## Run with Docker -1. **Prepare environment variables:** +1. **Prepare environment variables:** + Edit [docker-compose.yaml](https://github.com/OpenCut-app/OpenCut/blob/main/docker-compose.yaml#L57-L64) 2. **Build and run:** + ```bash docker-compose up -d --build ``` 3. *(Optional)* **Migrate database:** + ```bash docker-compose exec web bun run db:migrate ``` 4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) ## Contributing +Visit [CONTRIBUTING.md](.github/CONTRIBUTING.md) ## License -MIT [Details](LICENSE) +[MIT LICENSE](LICENSE) -- cgit v1.3.1 From 63b698fe12338d441becbb47561a1f5705f579b9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 01:48:06 +0200 Subject: chore: add .cursorignore file to exclude specific files from cursor tracking --- .cursorignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .cursorignore diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000..633521a2 --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +!apps/web/.env.example \ No newline at end of file -- cgit v1.3.1 From 69566ffa83b9f192397073f37de0d9a8a9437fef Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 01:48:46 +0200 Subject: fix: update hero section text from "CapCut alternative" to "video editor" --- apps/web/src/components/landing/hero.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index f2c70f5d..b02ce1be 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -84,7 +84,7 @@ export function Hero({ signupCount }: HeroProps) { The open source

- CapCut alternative. + video editor

-- cgit v1.3.1 From f2879be973e9f62b5c2c14e7a3e982bf0492f573 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 01:55:40 +0200 Subject: docs: enhance contributing guidelines with detailed setup instructions and environment variable configuration --- .github/CONTRIBUTING.md | 87 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 793d175e..b9e22759 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! -- cgit v1.3.1 From 37ceaaed5ec4f5484aadb31c1798708bf5e992ee Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 02:09:43 +0200 Subject: docs: update README with contributing guidelines and quick start instructions for contributors --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3ccb2b04..3dce0483 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) -- cgit v1.3.1 From c64480ac79f9ccda7b5e108bdbb9503d972cddd3 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 02:15:00 +0200 Subject: chore: update logo image in public assets --- apps/web/public/logo.png | Bin 7088 -> 1789 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png index a0af7a14..fc9e7902 100644 Binary files a/apps/web/public/logo.png and b/apps/web/public/logo.png differ -- cgit v1.3.1 From b5e8e8ce628dee51bcdb9bbeb8139333e5169d54 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 02:19:11 +0200 Subject: chore: update favicon.ico for improved branding --- apps/web/src/app/favicon.ico | Bin 4286 -> 4286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico index 66b4ee0f..cb20127b 100644 Binary files a/apps/web/src/app/favicon.ico and b/apps/web/src/app/favicon.ico differ -- cgit v1.3.1 From 2b06fd97d601e15c4dfb3008fcfd69bbb711a4d6 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 02:20:41 +0200 Subject: fix: 404 on auth pages --- apps/web/src/app/(auth)/login/page.tsx | 162 ---------------------------- apps/web/src/app/(auth)/signup/page.tsx | 181 -------------------------------- apps/web/src/app/auth/login/page.tsx | 162 ++++++++++++++++++++++++++++ apps/web/src/app/auth/signup/page.tsx | 181 ++++++++++++++++++++++++++++++++ 4 files changed, 343 insertions(+), 343 deletions(-) delete mode 100644 apps/web/src/app/(auth)/login/page.tsx delete mode 100644 apps/web/src/app/(auth)/signup/page.tsx create mode 100644 apps/web/src/app/auth/login/page.tsx create mode 100644 apps/web/src/app/auth/signup/page.tsx 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 ebf2eb3f..00000000 --- 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 3d125a07..00000000 --- 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 00000000..ebf2eb3f --- /dev/null +++ b/apps/web/src/app/auth/login/page.tsx @@ -0,0 +1,162 @@ +"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 new file mode 100644 index 00000000..3d125a07 --- /dev/null +++ b/apps/web/src/app/auth/signup/page.tsx @@ -0,0 +1,181 @@ +"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 + +
+ + + + ); +} -- cgit v1.3.1 From 6d29228c8ca1dc554505e566cc266c717df9bbdb Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 02:45:59 +0200 Subject: style: adjust margin in hero section for improved layout --- apps/web/src/components/landing/hero.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index b02ce1be..714d432b 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -89,7 +89,7 @@ export function Hero({ signupCount }: HeroProps) { Date: Tue, 24 Jun 2025 02:48:14 +0200 Subject: feat: implement unified AuthForm component for login and signup pages --- apps/web/bun.lock | 1 + apps/web/package.json | 3 +- apps/web/src/app/auth/login/page.tsx | 161 +------------- apps/web/src/app/auth/signup/page.tsx | 180 +-------------- apps/web/src/components/auth-form.tsx | 398 ++++++++++++++++++++++++++++++++++ 5 files changed, 405 insertions(+), 338 deletions(-) create mode 100644 apps/web/src/components/auth-form.tsx diff --git a/apps/web/bun.lock b/apps/web/bun.lock index 141e7db9..3aba7158 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 581aa023..0a2aa202 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/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx index ebf2eb3f..18117f44 100644 --- a/apps/web/src/app/auth/login/page.tsx +++ b/apps/web/src/app/auth/login/page.tsx @@ -1,162 +1,5 @@ -"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" - /> -
- -
-
- ); -} +import { AuthForm } from "@/components/auth-form"; export default function LoginPage() { - const router = useRouter(); - - return ( -
- - - - Welcome back - - Sign in to your account to continue - - - - - -
}> - - -
- Don't have an account?{" "} - - Sign up - -
- - - - ); + return ; } diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx index 3d125a07..109ae5cd 100644 --- a/apps/web/src/app/auth/signup/page.tsx +++ b/apps/web/src/app/auth/signup/page.tsx @@ -1,181 +1,5 @@ -"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" - /> -
- -
-
- ); -} +import { AuthForm } from "@/components/auth-form"; export default function SignUpPage() { - const router = useRouter(); - - return ( -
- - - - - Create your account - - Get started with your free account today - - - - - -
}> - - -
- Already have an account?{" "} - - Sign in - -
- - - - ); + return ; } diff --git a/apps/web/src/components/auth-form.tsx b/apps/web/src/components/auth-form.tsx new file mode 100644 index 00000000..182137ef --- /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} + +
+
+
+
+ ); +} -- cgit v1.3.1