diff options
| author | Aashish Paruvada <79566248+aashishparuvada@users.noreply.github.com> | 2025-06-24 00:34:51 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-24 00:34:51 +0530 |
| commit | 5322ab9907025b42d834472f37cbdd3077e15180 (patch) | |
| tree | e718d91f10176c6c47752776171adacb929e686e | |
| parent | 360fdffa7637ccaf7e583dbf3e0d19d2c773425a (diff) | |
| parent | a6a520db70ef639e066d1012fa2ecf7e26c53049 (diff) | |
| download | opencut-5322ab9907025b42d834472f37cbdd3077e15180.tar.gz | |
Merge branch 'OpenCut-app:main' into main
| -rw-r--r-- | apps/web/.env.example | 6 | ||||
| -rw-r--r-- | apps/web/src/app/(auth)/login/page.tsx | 162 | ||||
| -rw-r--r-- | apps/web/src/app/(auth)/signup/page.tsx | 181 | ||||
| -rw-r--r-- | apps/web/src/app/auth/login/page.tsx | 98 | ||||
| -rw-r--r-- | apps/web/src/app/auth/signup/page.tsx | 108 | ||||
| -rw-r--r-- | apps/web/src/components/header.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/icons.tsx | 22 | ||||
| -rw-r--r-- | apps/web/src/lib/auth-client.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/lib/auth.ts | 6 | ||||
| -rw-r--r-- | apps/web/src/lib/ffmpeg-utils.ts | 64 |
10 files changed, 429 insertions, 224 deletions
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 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<string | null>(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 ( + <div className="flex flex-col space-y-6"> + {error && ( + <Alert variant="destructive"> + <AlertTitle>Error</AlertTitle> + <AlertDescription>{error}</AlertDescription> + </Alert> + )} + + <Button + onClick={handleGoogleLogin} + variant="outline" + size="lg" + disabled={isAnyLoading} + > + {isGoogleLoading ? <Loader2 className="animate-spin" /> : (<GoogleIcon />)} Continue with Google + </Button> + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <Separator className="w-full" /> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground">Or continue with</span> + </div> + </div> + <div className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="email">Email</Label> + <Input + id="email" + type="email" + placeholder="m@example.com" + value={email} + onChange={(e) => setEmail(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="password">Password</Label> + <Input + id="password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <Button + onClick={handleLogin} + disabled={isAnyLoading || !email || !password} + className="w-full h-11" + size="lg" + > + {isEmailLoading ? <Loader2 className="animate-spin" /> : "Sign in"} + </Button> + </div> + </div> + ); +} + +export default function LoginPage() { + const router = useRouter(); + + return ( + <div className="flex h-screen items-center justify-center relative"> + <Button + variant="ghost" + onClick={() => router.back()} + className="absolute top-6 left-6" + > + <ArrowLeft className="h-5 w-5" /> Back + </Button> + <Card className="w-[400px] shadow-lg border-0"> + <CardHeader className="text-center pb-4"> + <CardTitle className="text-2xl font-semibold">Welcome back</CardTitle> + <CardDescription className="text-base"> + Sign in to your account to continue + </CardDescription> + </CardHeader> + <CardContent className="pt-0"> + <Suspense fallback={<div className="text-center"> + <Loader2 className="animate-spin" /> + </div>}> + <LoginForm /> + </Suspense> + <div className="mt-6 text-center text-sm"> + Don't have an account?{" "} + <Link href="/auth/signup" className="font-medium text-primary underline-offset-4 hover:underline"> + Sign up + </Link> + </div> + </CardContent> + </Card> + </div> + ); +} 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<string | null>(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 ( + <div className="flex flex-col space-y-6"> + {error && ( + <Alert variant="destructive"> + <AlertTitle>Error</AlertTitle> + <AlertDescription>{error}</AlertDescription> + </Alert> + )} + + <Button + onClick={handleGoogleSignUp} + variant="outline" + size="lg" + disabled={isAnyLoading} + > + {isGoogleLoading ? <Loader2 className="animate-spin" /> : (<GoogleIcon />)} Continue with Google + </Button> + + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <Separator className="w-full" /> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground">Or continue with</span> + </div> + </div> + + <div className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="name">Full Name</Label> + <Input + id="name" + type="text" + placeholder="John Doe" + value={name} + onChange={(e) => setName(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="email">Email</Label> + <Input + id="email" + type="email" + placeholder="m@example.com" + value={email} + onChange={(e) => setEmail(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="password">Password</Label> + <Input + id="password" + type="password" + placeholder="Create a strong password" + value={password} + onChange={(e) => setPassword(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <Button + onClick={handleSignUp} + disabled={isAnyLoading || !name || !email || !password} + className="w-full h-11" + size="lg" + > + {isEmailLoading ? <Loader2 className="animate-spin" /> : "Create account"} + </Button> + </div> + </div> + ); +} + +export default function SignUpPage() { + const router = useRouter(); + + return ( + <div className="flex h-screen items-center justify-center relative"> + <Button + variant="ghost" + onClick={() => router.back()} + className="absolute top-6 left-6" + > + <ArrowLeft className="h-5 w-5" /> Back + </Button> + + <Card className="w-[400px] shadow-lg border-0"> + <CardHeader className="text-center pb-4"> + <CardTitle className="text-2xl font-semibold">Create your account</CardTitle> + <CardDescription className="text-base"> + Get started with your free account today + </CardDescription> + </CardHeader> + <CardContent className="pt-0"> + <Suspense fallback={<div className="text-center"> + <Loader2 className="animate-spin" /> + </div>}> + <SignUpForm /> + </Suspense> + <div className="mt-6 text-center text-sm"> + Already have an account?{" "} + <Link href="/auth/login" className="font-medium text-primary underline-offset-4 hover:underline"> + Sign in + </Link> + </div> + </CardContent> + </Card> + </div> + ); +} 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 5eadb85c..00000000 --- a/apps/web/src/app/auth/login/page.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import { useSearchParams, useRouter } from "next/navigation"; -import { authClient } 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 Link from "next/link"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; - -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<string | null>(null); - - const handleLogin = async () => { - setError(null); - const { error } = await authClient.signIn.email({ - email, - password, - }); - - if (error) { - setError(error.message || "An unexpected error occurred."); - return; - } - - router.push(redirectUrl || "/"); - }; - - return ( - <div className="flex flex-col space-y-4"> - {error && ( - <Alert variant="destructive"> - <AlertTitle>Error</AlertTitle> - <AlertDescription>{error}</AlertDescription> - </Alert> - )} - <div className="space-y-2"> - <Label htmlFor="email">Email</Label> - <Input - id="email" - type="email" - placeholder="m@example.com" - value={email} - onChange={(e) => setEmail(e.target.value)} - /> - </div> - <div className="space-y-2"> - <Label htmlFor="password">Password</Label> - <Input - id="password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - /> - </div> - <Button onClick={handleLogin}>Login</Button> - </div> - ); -} - -export default function LoginPage() { - return ( - <div className="flex h-screen items-center justify-center"> - <Card className="w-[350px]"> - <CardHeader> - <CardTitle>Login</CardTitle> - <CardDescription> - Enter your email and password to login. - </CardDescription> - </CardHeader> - <CardContent> - <Suspense fallback={<div>Loading...</div>}> - <LoginForm /> - </Suspense> - <div className="mt-4 text-center text-sm"> - Don't have an account?{" "} - <Link href="/auth/signup" className="underline"> - Sign up - </Link> - </div> - </CardContent> - </Card> - </div> - ); -} 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 f328851c..00000000 --- a/apps/web/src/app/auth/signup/page.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { useSearchParams, useRouter } from "next/navigation"; -import { authClient } 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 Link from "next/link"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; - -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<string | null>(null); - - const handleSignUp = async () => { - setError(null); - const { error } = await authClient.signUp.email({ - name, - email, - password, - }); - - if (error) { - setError(error.message || "An unexpected error occurred."); - return; - } - - router.push(redirectUrl || "/"); - }; - - return ( - <div className="flex flex-col space-y-4"> - {error && ( - <Alert variant="destructive"> - <AlertTitle>Error</AlertTitle> - <AlertDescription>{error}</AlertDescription> - </Alert> - )} - <div className="space-y-2"> - <Label htmlFor="name">Name</Label> - <Input - id="name" - type="text" - placeholder="John Doe" - value={name} - onChange={(e) => setName(e.target.value)} - /> - </div> - <div className="space-y-2"> - <Label htmlFor="email">Email</Label> - <Input - id="email" - type="email" - placeholder="m@example.com" - value={email} - onChange={(e) => setEmail(e.target.value)} - /> - </div> - <div className="space-y-2"> - <Label htmlFor="password">Password</Label> - <Input - id="password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - /> - </div> - <Button onClick={handleSignUp}>Sign Up</Button> - </div> - ); -} - -export default function SignUpPage() { - return ( - <div className="flex h-screen items-center justify-center"> - <Card className="w-[350px]"> - <CardHeader> - <CardTitle>Sign Up</CardTitle> - <CardDescription>Create an account to get started.</CardDescription> - </CardHeader> - <CardContent> - <Suspense fallback={<div>Loading...</div>}> - <SignUpForm /> - </Suspense> - <div className="mt-4 text-center text-sm"> - Already have an account?{" "} - <Link href="/auth/login" className="underline"> - Login - </Link> - </div> - </CardContent> - </Card> - </div> - ); -} 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 = (
<Link href="/" className="flex items-center gap-3">
<Image src="/logo.png" alt="OpenCut Logo" width={24} height={24} />
@@ -21,7 +23,7 @@ export function Header() { GitHub
</Button>
</Link>
- <Link href="/editor">
+ <Link href={session ? "/editor" : "/auth/login"}>
<Button size="sm" className="text-sm ml-4">
Start editing
<ArrowRight className="h-4 w-4" />
diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx new file mode 100644 index 00000000..5cc24157 --- /dev/null +++ b/apps/web/src/components/icons.tsx @@ -0,0 +1,22 @@ +export function GoogleIcon({ className }: { className?: string }) { + return ( + <svg className={className} viewBox="0 0 24 24"> + <path + fill="#4285F4" + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" + /> + <path + fill="#34A853" + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" + /> + <path + fill="#FBBC05" + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" + /> + <path + fill="#EA4335" + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" + /> + </svg> + ); +} diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index 7c467e7e..1ab4e636 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,4 +1,4 @@ import { createAuthClient } from "better-auth/react";
-export const authClient = createAuthClient({
+export const { signIn, signUp, useSession } = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL!,
});
diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index 4cfaa6f2..d0175c97 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -16,6 +16,12 @@ export const auth = betterAuth({ emailAndPassword: {
enabled: true,
},
+ socialProviders: {
+ google: {
+ clientId: process.env.GOOGLE_CLIENT_ID as string,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
+ },
+ },
appName: "OpenCut",
trustedOrigins: ["http://localhost:3000"],
});
diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts index 56085a21..ae0c267b 100644 --- a/apps/web/src/lib/ffmpeg-utils.ts +++ b/apps/web/src/lib/ffmpeg-utils.ts @@ -102,26 +102,60 @@ export const getVideoInfo = async (videoFile: File): Promise<{ fps: number; }> => { const ffmpeg = await initFFmpeg(); - + const inputName = 'input.mp4'; - + // Write input file await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); - - // Get video info - await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']); - - // Note: In a real implementation, you'd parse the FFmpeg output - // For now, we'll return default values and enhance this later - + + // Capture FFmpeg stderr output with a one-time listener pattern + let ffmpegOutput = ''; + let listening = true; + const listener = (data: string) => { + if (listening) ffmpegOutput += data; + }; + ffmpeg.on('log', ({ message }) => listener(message)); + + // Run ffmpeg to get info (stderr will contain the info) + try { + await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']); + } catch (error) { + listening = false; + await ffmpeg.deleteFile(inputName); + console.error('FFmpeg execution failed:', error); + throw new Error('Failed to extract video info. The file may be corrupted or in an unsupported format.'); + } + + // Disable listener after exec completes + listening = false; + // Cleanup await ffmpeg.deleteFile(inputName); - + + // Parse output for duration, resolution, and fps + // Example: Duration: 00:00:10.00, start: 0.000000, bitrate: 1234 kb/s + // Example: Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn, 60 tbc + + const durationMatch = ffmpegOutput.match(/Duration: (\d+):(\d+):([\d.]+)/); + let duration = 0; + if (durationMatch) { + const [, h, m, s] = durationMatch; + duration = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s); + } + + const videoStreamMatch = ffmpegOutput.match(/Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/); + let width = 0, height = 0, fps = 0; + if (videoStreamMatch) { + width = parseInt(videoStreamMatch[1]); + height = parseInt(videoStreamMatch[2]); + fps = parseFloat(videoStreamMatch[3]); + } + return { - duration: 10, // Placeholder - would parse from FFmpeg output - width: 1920, // Placeholder - height: 1080, // Placeholder - fps: 30 // Placeholder + duration, + width, + height, + fps }; }; @@ -194,4 +228,4 @@ export const extractAudio = async ( await ffmpeg.deleteFile(outputName); return blob; -};
\ No newline at end of file +};
\ No newline at end of file |
