From 294ba01abeb2d8c3d3a04cb7ecc54abf97a6d4d3 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Mon, 23 Jun 2025 15:39:56 +0300 Subject: feat: initial state for composition and overlay editing on the video player --- apps/web/src/app/editor/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'apps/web/src/app') diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx index 2f825a22..64186a3e 100644 --- a/apps/web/src/app/editor/page.tsx +++ b/apps/web/src/app/editor/page.tsx @@ -7,7 +7,7 @@ import { ResizableHandle, } from "../../components/ui/resizable"; import { MediaPanel } from "../../components/editor/media-panel"; -import { PropertiesPanel } from "../../components/editor/properties-panel"; +// import { PropertiesPanel } from "../../components/editor/properties-panel"; import { Timeline } from "../../components/editor/timeline"; import { PreviewPanel } from "../../components/editor/preview-panel"; import { EditorHeader } from "@/components/editor-header"; @@ -74,13 +74,13 @@ export default function Editor() { {/* Properties Panel */} - - + */} -- cgit v1.3.1 From 8bcd226bdb4eb3468df0590dbe6d3d8d68b2acbd Mon Sep 17 00:00:00 2001 From: Deepanshu Mishra Date: Mon, 23 Jun 2025 22:51:34 +0530 Subject: Added google auth and some auth page ui optimisations --- apps/web/.env.example | 6 +- apps/web/src/app/auth/login/page.tsx | 134 ++++++++++++++++++++-------- apps/web/src/app/auth/signup/page.tsx | 163 ++++++++++++++++++++++++---------- apps/web/src/components/header.tsx | 4 +- apps/web/src/components/icons.tsx | 22 +++++ apps/web/src/lib/auth-client.ts | 2 +- apps/web/src/lib/auth.ts | 6 ++ 7 files changed, 254 insertions(+), 83 deletions(-) create mode 100644 apps/web/src/components/icons.tsx (limited to 'apps/web/src/app') diff --git a/apps/web/.env.example b/apps/web/.env.example index 8477646c..0b4c649b 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -7,9 +7,13 @@ DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET=your-secret-key-here +#Google Clients +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + # Development Environment NODE_ENV=development # Redis UPSTASH_REDIS_REST_URL=http://localhost:8079 -UPSTASH_REDIS_REST_TOKEN=example_token \ No newline at end of file +UPSTASH_REDIS_REST_TOKEN=example_token diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx index 5eadb85c..ebf2eb3f 100644 --- a/apps/web/src/app/auth/login/page.tsx +++ b/apps/web/src/app/auth/login/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; -import { authClient } from "@/lib/auth-client"; +import { useRouter } from "next/navigation"; +import { signIn } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,81 +13,145 @@ import { 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 searchParams = useSearchParams(); const router = useRouter(); - const redirectUrl = searchParams.get("redirect"); 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); - const { error } = await authClient.signIn.email({ + setIsEmailLoading(true); + + const { error } = await signIn.email({ email, password, }); if (error) { setError(error.message || "An unexpected error occurred."); + setIsEmailLoading(false); return; } - router.push(redirectUrl || "/"); + 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} )} -
- - setEmail(e.target.value)} - /> + + +
+
+ +
+
+ Or continue with +
-
- - setPassword(e.target.value)} - /> +
+
+ + 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 ( -
- - - Login - - Enter your email and password to login. +
+ + + + Welcome back + + Sign in to your account to continue - - Loading...
}> + + + +
}> -
+
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 index f328851c..3d125a07 100644 --- a/apps/web/src/app/auth/signup/page.tsx +++ b/apps/web/src/app/auth/signup/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; -import { authClient } from "@/lib/auth-client"; +import { useRouter } from "next/navigation"; +import { signUp, signIn } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,21 +13,26 @@ import { 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 searchParams = useSearchParams(); const router = useRouter(); - const redirectUrl = searchParams.get("redirect"); 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); - const { error } = await authClient.signUp.email({ + setIsEmailLoading(true); + + const { error } = await signUp.email({ name, email, password, @@ -35,70 +40,138 @@ function SignUpForm() { if (error) { setError(error.message || "An unexpected error occurred."); + setIsEmailLoading(false); return; } - router.push(redirectUrl || "/"); + 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} )} -
- - setName(e.target.value)} - /> -
-
- - setEmail(e.target.value)} - /> + + + +
+
+ +
+
+ Or continue with +
-
- - setPassword(e.target.value)} - /> + +
+
+ + 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 ( -
- - - Sign Up - Create an account to get started. +
+ + + + + Create your account + + Get started with your free account today + - - Loading...
}> + + + +
}> -
+
Already have an account?{" "} - - Login + + Sign in
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 004863d9..109d022e 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -5,8 +5,10 @@ import Image from "next/image"; import { Button } from "./ui/button"; import { ArrowRight } from "lucide-react"; import { HeaderBase } from "./header-base"; +import { useSession } from "@/lib/auth-client"; export function Header() { + const { data: session } = useSession(); const leftContent = ( OpenCut Logo @@ -21,7 +23,7 @@ export function Header() { GitHub - + +
+
+ +
+
+ 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 + +
+ + +
+ ); +} 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 - -
- - -
- ); -} -- cgit v1.3.1 From f959ba87e20aaae42bc270edc127ae89b97241cb Mon Sep 17 00:00:00 2001 From: 3raphat <96657413+3raphat@users.noreply.github.com> Date: Tue, 24 Jun 2025 01:38:53 +0700 Subject: fix: update auth routes --- apps/web/src/app/(auth)/login/page.tsx | 26 ++++++++++++++++------ apps/web/src/app/(auth)/signup/page.tsx | 38 +++++++++++++++++++++++++-------- apps/web/src/components/header.tsx | 2 +- apps/web/src/middleware.ts | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) (limited to 'apps/web/src/app') diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index ebf2eb3f..5f37ced9 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -77,14 +77,21 @@ function LoginForm() { size="lg" disabled={isAnyLoading} > - {isGoogleLoading ? : ()} Continue with Google + {isGoogleLoading ? ( + + ) : ( + + )}{" "} + Continue with Google
- Or continue with + + Or continue with +
@@ -144,14 +151,21 @@ export default function LoginPage() { - - -
}> + + +
+ } + >
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 index 3d125a07..d1feece2 100644 --- a/apps/web/src/app/(auth)/signup/page.tsx +++ b/apps/web/src/app/(auth)/signup/page.tsx @@ -44,7 +44,7 @@ function SignUpForm() { return; } - router.push("/auth/login"); + router.push("/login"); }; const handleGoogleSignUp = async () => { @@ -80,7 +80,12 @@ function SignUpForm() { size="lg" disabled={isAnyLoading} > - {isGoogleLoading ? : ()} Continue with Google + {isGoogleLoading ? ( + + ) : ( + + )}{" "} + Continue with Google
@@ -88,7 +93,9 @@ function SignUpForm() {
- Or continue with + + Or continue with +
@@ -135,7 +142,11 @@ function SignUpForm() { className="w-full h-11" size="lg" > - {isEmailLoading ? : "Create account"} + {isEmailLoading ? ( + + ) : ( + "Create account" + )}
@@ -157,20 +168,29 @@ export default function SignUpPage() { - Create your account + + Create your account + Get started with your free account today - - - }> + + + + } + >
Already have an account?{" "} - + Sign in
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 109d022e..2ccc53fa 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -23,7 +23,7 @@ export function Header() { GitHub - + -
-
- -
-
- 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 4260d4be08817393af96e7843ed828183137119f Mon Sep 17 00:00:00 2001 From: Maze Winther 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 (limited to 'apps/web/src/app') 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 From dc35619017ed7fbaa5e83151ba99c143da8cf1f1 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Tue, 24 Jun 2025 08:10:11 +0300 Subject: clean up and simplifying more logic --- apps/web/src/app/editor/page.tsx | 104 +++--- apps/web/src/components/editor-provider.tsx | 14 +- apps/web/src/components/editor/preview-panel.tsx | 407 +++-------------------- apps/web/src/components/ui/video-player.tsx | 113 ++----- apps/web/src/stores/panel-store.ts | 62 +--- 5 files changed, 156 insertions(+), 544 deletions(-) (limited to 'apps/web/src/app') diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx index 64186a3e..19df364c 100644 --- a/apps/web/src/app/editor/page.tsx +++ b/apps/web/src/app/editor/page.tsx @@ -42,59 +42,71 @@ export default function Editor() { return ( -
+
- - - {/* Main content area */} - - {/* Tools Panel */} - - - +
+ + + {/* Main content area */} + + {/* Tools Panel */} + + + - + - {/* Preview Area */} - - - + {/* Preview Area */} + + + - + - {/* Properties Panel */} - {/* - - */} - - + {/* Properties Panel - Hidden for now but ready */} + {/* + + */} + + - + - {/* Timeline */} - - - - + {/* Timeline */} + + + + +
); diff --git a/apps/web/src/components/editor-provider.tsx b/apps/web/src/components/editor-provider.tsx index ab55c7d7..38255956 100644 --- a/apps/web/src/components/editor-provider.tsx +++ b/apps/web/src/components/editor-provider.tsx @@ -3,7 +3,6 @@ import { useEffect } from "react"; import { Loader2 } from "lucide-react"; import { useEditorStore } from "@/stores/editor-store"; -import { usePanelStore } from "@/stores/panel-store"; interface EditorProviderProps { children: React.ReactNode; @@ -11,19 +10,10 @@ interface EditorProviderProps { export function EditorProvider({ children }: EditorProviderProps) { const { isInitializing, isPanelsReady, initializeApp } = useEditorStore(); - const { setInitialized } = usePanelStore(); useEffect(() => { - const initialize = async () => { - // Initialize the app - await initializeApp(); - - // Initialize panel store for future resize events - setInitialized(); - }; - - initialize(); - }, [initializeApp, setInitialized]); + initializeApp(); + }, [initializeApp]); // Show loading screen while initializing if (isInitializing || !isPanelsReady) { diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 5d03e941..101f1ff1 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -5,53 +5,25 @@ import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; import { VideoPlayer } from "@/components/ui/video-player"; import { Button } from "@/components/ui/button"; -import { Play, Pause, Move, RotateCw, Crop, ZoomIn, ZoomOut } from "lucide-react"; -import { useState, useRef, useEffect, useCallback } from "react"; - -interface ClipTransform { - x: number; - y: number; - scale: number; - rotation: number; - opacity: number; - width: number; - height: number; - blendMode: string; - cropTop: number; - cropBottom: number; - cropLeft: number; - cropRight: number; -} - -interface DragState { - isDragging: boolean; - dragType: 'move' | 'resize-nw' | 'resize-ne' | 'resize-sw' | 'resize-se' | 'rotate' | 'scale' | 'crop-n' | 'crop-s' | 'crop-e' | 'crop-w'; - startMouseX: number; - startMouseY: number; - startTransform: ClipTransform; - clipId: string; -} +import { Play, Pause } from "lucide-react"; +import { useState, useRef } from "react"; export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); const { isPlaying, toggle, currentTime } = usePlaybackStore(); - - const [clipTransforms, setClipTransforms] = useState>({}); - const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); // Default 16:9 - const [dragState, setDragState] = useState(null); + const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); const previewRef = useRef(null); - // Get all active clips at current time (for overlaying) + // Get active clips at current time const getActiveClips = () => { const activeClips: Array<{ clip: any; track: any; mediaItem: any; - layer: number; }> = []; - tracks.forEach((track, trackIndex) => { + tracks.forEach((track) => { track.clips.forEach((clip) => { const clipStart = clip.startTime; const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); @@ -62,338 +34,78 @@ export function PreviewPanel() { : mediaItems.find((item) => item.id === clip.mediaId); if (mediaItem || clip.mediaId === "test") { - activeClips.push({ - clip, - track, - mediaItem, - layer: trackIndex, // Track index determines layer order - }); + activeClips.push({ clip, track, mediaItem }); } } }); }); - // Sort by layer (track order) - higher index = on top - return activeClips.sort((a, b) => a.layer - b.layer); + return activeClips; }; const activeClips = getActiveClips(); const aspectRatio = canvasSize.width / canvasSize.height; - // Get or create transform for a clip - const getClipTransform = (clipId: string): ClipTransform => { - return clipTransforms[clipId] || { - x: 0, - y: 0, - scale: 1, - rotation: 0, - opacity: 1, - width: 100, // Percentage of canvas - height: 100, - blendMode: 'normal', - cropTop: 0, - cropBottom: 0, - cropLeft: 0, - cropRight: 0, - }; - }; - - // Update clip transform - const updateClipTransform = useCallback((clipId: string, updates: Partial) => { - setClipTransforms(prev => { - const currentTransform = prev[clipId] || { - x: 0, - y: 0, - scale: 1, - rotation: 0, - opacity: 1, - width: 100, - height: 100, - blendMode: 'normal', - cropTop: 0, - cropBottom: 0, - cropLeft: 0, - cropRight: 0, - }; - - return { - ...prev, - [clipId]: { ...currentTransform, ...updates } - }; - }); - }, []); - - // Mouse event handlers - const handleMouseDown = (e: React.MouseEvent, clipId: string, dragType: DragState['dragType']) => { - e.preventDefault(); - e.stopPropagation(); - - setDragState({ - isDragging: true, - dragType, - startMouseX: e.clientX, - startMouseY: e.clientY, - startTransform: getClipTransform(clipId), - clipId - }); - }; - - const handleMouseMove = useCallback((e: MouseEvent) => { - if (!dragState || !dragState.isDragging) return; - - const deltaX = e.clientX - dragState.startMouseX; - const deltaY = e.clientY - dragState.startMouseY; - const { startTransform, clipId, dragType } = dragState; - - switch (dragType) { - case 'move': - updateClipTransform(clipId, { - x: Math.max(-100, Math.min(100, startTransform.x + deltaX * 0.3)), - y: Math.max(-100, Math.min(100, startTransform.y + deltaY * 0.3)) - }); - break; - - case 'resize-nw': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width - deltaX * 0.5), - height: Math.max(20, startTransform.height - deltaY * 0.5) - }); - break; - - case 'resize-ne': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width + deltaX * 0.5), - height: Math.max(20, startTransform.height - deltaY * 0.5) - }); - break; - - case 'resize-sw': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width - deltaX * 0.5), - height: Math.max(20, startTransform.height + deltaY * 0.5) - }); - break; - - case 'resize-se': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width + deltaX * 0.5), - height: Math.max(20, startTransform.height + deltaY * 0.5) - }); - break; - - case 'rotate': - updateClipTransform(clipId, { - rotation: (startTransform.rotation + deltaX * 2) % 360 - }); - break; - - case 'scale': - updateClipTransform(clipId, { - scale: Math.max(0.1, Math.min(3, startTransform.scale + deltaX * 0.01)) - }); - break; - - case 'crop-n': - updateClipTransform(clipId, { - cropTop: Math.max(0, Math.min(40, startTransform.cropTop + deltaY * 0.2)) - }); - break; - - case 'crop-s': - updateClipTransform(clipId, { - cropBottom: Math.max(0, Math.min(40, startTransform.cropBottom - deltaY * 0.2)) - }); - break; - - case 'crop-e': - updateClipTransform(clipId, { - cropRight: Math.max(0, Math.min(40, startTransform.cropRight - deltaX * 0.2)) - }); - break; - - case 'crop-w': - updateClipTransform(clipId, { - cropLeft: Math.max(0, Math.min(40, startTransform.cropLeft + deltaX * 0.2)) - }); - break; - } - }, [dragState, updateClipTransform]); - - const handleMouseUp = useCallback(() => { - setDragState(null); - }, []); - - // Add global mouse event listeners - useEffect(() => { - if (dragState?.isDragging) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - } - }, [dragState, handleMouseMove, handleMouseUp]); - - // Initialize transforms for new clips - useEffect(() => { - const activeClips = getActiveClips(); - const newTransforms: Record = {}; - let hasNewClips = false; - - activeClips.forEach(({ clip }) => { - if (!clipTransforms[clip.id]) { - hasNewClips = true; - newTransforms[clip.id] = { - x: 0, - y: 0, - scale: 1, - rotation: 0, - opacity: 1, - width: 100, - height: 100, - blendMode: 'normal', - cropTop: 0, - cropBottom: 0, - cropLeft: 0, - cropRight: 0, - }; - } - }); - - if (hasNewClips) { - setClipTransforms(prev => ({ ...prev, ...newTransforms })); - } - }, [tracks, currentTime]); // Re-run when tracks or time changes - - - - // Render a single clip layer - const renderClipLayer = (clipData: any, index: number) => { + // Render a clip + const renderClip = (clipData: any, index: number) => { const { clip, mediaItem } = clipData; - const transform = getClipTransform(clip.id); - - const layerStyle = { - position: 'absolute' as const, - left: '50%', - top: '50%', - width: `${transform.width}%`, - height: `${transform.height}%`, - transform: `translate(-50%, -50%) translate(${transform.x}%, ${transform.y}%) scale(${transform.scale}) rotate(${transform.rotation}deg)`, - opacity: transform.opacity, - mixBlendMode: transform.blendMode as any, - clipPath: `inset(${transform.cropTop}% ${transform.cropRight}% ${transform.cropBottom}% ${transform.cropLeft}%)`, - zIndex: index + 10, - cursor: dragState?.isDragging && dragState.clipId === clip.id ? 'grabbing' : 'grab', - userSelect: 'none' as const, - }; - const handleClipMouseDown = (e: React.MouseEvent) => { - e.stopPropagation(); - handleMouseDown(e, clip.id, 'move'); - }; - - // Handle test clips + // Test clips if (!mediaItem || clip.mediaId === "test") { return (
-
-
🎬
+
+
🎬

{clip.name}

- - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - // Render video + // Video clips if (mediaItem.type === "video") { return ( -
+
- - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - // Render image + // Image clips if (mediaItem.type === "image") { return ( -
+
{mediaItem.name} - - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - // Render audio (visual representation) + // Audio clips (visual representation) if (mediaItem.type === "audio") { return (
-
-
🎵
+
+
🎵

{mediaItem.name}

- - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } @@ -401,7 +113,7 @@ export function PreviewPanel() { return null; }; - // Canvas size presets + // Canvas presets const canvasPresets = [ { name: "16:9 HD", width: 1920, height: 1080 }, { name: "16:9 4K", width: 3840, height: 2160 }, @@ -411,9 +123,9 @@ export function PreviewPanel() { ]; return ( -
- {/* Canvas Controls */} -
+
+ {/* Controls */} +
Canvas: -
- {/* Preview Area - Full Width */} -
+ {/* Preview Area */} +
1 ? "100%" : "auto", - height: aspectRatio <= 1 ? "100%" : "auto", - maxWidth: "100%", - maxHeight: "100%", - background: '#000000', - border: '1px solid #374151' + width: "100%", + height: "100%", }} - > - - - {/* Render all active clips as layers */} {activeClips.length === 0 ? (
{tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"}
) : ( - activeClips.map((clipData, index) => renderClipLayer(clipData, index)) + activeClips.map((clipData, index) => renderClip(clipData, index)) )} - -
- {/* Bottom Info Panel */} -
- {/* Layer List */} -
-
Active Layers ({activeClips.length})
-
- {activeClips.map((clipData, index) => ( -
- - {index + 1} - - {clipData.clip.name} - {clipData.track.name} -
- ))} -
+ {/* Info Panel */} +
+
Active Clips ({activeClips.length})
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} +
+ ))}
- -
); diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx index 8731e1a8..f489f70a 100644 --- a/apps/web/src/components/ui/video-player.tsx +++ b/apps/web/src/components/ui/video-player.tsx @@ -1,8 +1,6 @@ "use client"; import { useRef, useEffect } from "react"; -import { Button } from "./button"; -import { Play, Pause, Volume2 } from "lucide-react"; import { usePlaybackStore } from "@/stores/playback-store"; interface VideoPlayerProps { @@ -25,128 +23,87 @@ export function VideoPlayer({ clipDuration }: VideoPlayerProps) { const videoRef = useRef(null); - const { isPlaying, currentTime, volume, speed, play, pause, setVolume } = usePlaybackStore(); + const { isPlaying, currentTime, volume, speed } = usePlaybackStore(); // Calculate if we're within this clip's timeline range const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime; - // Calculate the video's internal time based on timeline position - const videoTime = Math.max(trimStart, Math.min( - clipDuration - trimEnd, - currentTime - clipStartTime + trimStart - )); - + // Sync playback events useEffect(() => { const video = videoRef.current; - if (!video) return; + if (!video || !isInClipRange) return; - const handleSeekEvent = (e: CustomEvent) => { - if (!isInClipRange) return; + const handleSeek = (e: CustomEvent) => { const timelineTime = e.detail.time; - const newVideoTime = Math.max(trimStart, Math.min( + const videoTime = Math.max(trimStart, Math.min( clipDuration - trimEnd, timelineTime - clipStartTime + trimStart )); - video.currentTime = newVideoTime; + video.currentTime = videoTime; }; - const handleUpdateEvent = (e: CustomEvent) => { - if (!isInClipRange) return; + const handleUpdate = (e: CustomEvent) => { const timelineTime = e.detail.time; - const targetVideoTime = Math.max(trimStart, Math.min( + const targetTime = Math.max(trimStart, Math.min( clipDuration - trimEnd, timelineTime - clipStartTime + trimStart )); - // Only sync if there's a significant difference to avoid micro-adjustments - if (Math.abs(video.currentTime - targetVideoTime) > 0.5) { - video.currentTime = targetVideoTime; + if (Math.abs(video.currentTime - targetTime) > 0.5) { + video.currentTime = targetTime; } }; - const handleSpeedEvent = (e: CustomEvent) => { - if (!isInClipRange) return; - // Set playbackRate directly without any additional checks + const handleSpeed = (e: CustomEvent) => { video.playbackRate = e.detail.speed; }; - window.addEventListener("playback-seek", handleSeekEvent as EventListener); - window.addEventListener("playback-update", handleUpdateEvent as EventListener); - window.addEventListener("playback-speed", handleSpeedEvent as EventListener); + window.addEventListener("playback-seek", handleSeek as EventListener); + window.addEventListener("playback-update", handleUpdate as EventListener); + window.addEventListener("playback-speed", handleSpeed as EventListener); return () => { - window.removeEventListener("playback-seek", handleSeekEvent as EventListener); - window.removeEventListener("playback-update", handleUpdateEvent as EventListener); - window.removeEventListener("playback-speed", handleSpeedEvent as EventListener); + window.removeEventListener("playback-seek", handleSeek as EventListener); + window.removeEventListener("playback-update", handleUpdate as EventListener); + window.removeEventListener("playback-speed", handleSpeed as EventListener); }; }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); - // Sync video playback state - only play if in clip range + // Sync playback state useEffect(() => { const video = videoRef.current; if (!video) return; if (isPlaying && isInClipRange) { - video.play().catch(console.error); + video.play().catch(() => { }); } else { video.pause(); } }, [isPlaying, isInClipRange]); - // Sync volume + // Sync volume and speed useEffect(() => { const video = videoRef.current; if (!video) return; - video.volume = volume; - }, [volume]); - // Sync speed immediately when it changes - useEffect(() => { - const video = videoRef.current; - if (!video) return; + video.volume = volume; video.playbackRate = speed; - }, [speed]); + }, [volume, speed]); return ( -
-