Skip to content

Commit 6a0efb9

Browse files
Merge pull request #6 from Shitanshukumar607/add-auth
Add auth with google
2 parents c4c8167 + 851c89a commit 6a0efb9

File tree

18 files changed

+1004
-41
lines changed

18 files changed

+1004
-41
lines changed

app/api/auth/callback/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextResponse } from "next/server";
2+
// The client you created from the Server-Side Auth instructions
3+
import { createClient } from "@/lib/supabase/server";
4+
5+
export async function GET(request: Request) {
6+
const { searchParams, origin } = new URL(request.url);
7+
const code = searchParams.get("code");
8+
// if "next" is in param, use it as the redirect URL
9+
let next = searchParams.get("next") ?? "/";
10+
if (!next.startsWith("/")) {
11+
// if "next" is not a relative URL, use the default
12+
next = "/";
13+
}
14+
15+
if (code) {
16+
const supabase = await createClient();
17+
const { error } = await supabase.auth.exchangeCodeForSession(code);
18+
if (!error) {
19+
const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
20+
const isLocalEnv = process.env.NODE_ENV === "development";
21+
if (isLocalEnv) {
22+
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
23+
return NextResponse.redirect(`${origin}${next}`);
24+
} else if (forwardedHost) {
25+
return NextResponse.redirect(`https://${forwardedHost}${next}`);
26+
} else {
27+
return NextResponse.redirect(`${origin}${next}`);
28+
}
29+
}
30+
}
31+
32+
// return the user to an error page with instructions
33+
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
34+
}

app/auth/callback/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { redirect } from "next/navigation";
2+
3+
const Page = () => {
4+
redirect("/");
5+
};
6+
7+
export default Page;

app/generate/page.tsx

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Upload,
1313
X,
1414
} from "lucide-react";
15+
import Image from "next/image";
1516
import { useCallback, useEffect, useRef, useState } from "react";
1617

1718
interface UploadedFile {
@@ -56,46 +57,50 @@ export default function UploadPage() {
5657
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
5758
};
5859

59-
const handleFiles = (files: FileList) => {
60-
const file = files[0];
61-
if (!file) return;
60+
const handleFiles = useCallback(
61+
(files: FileList) => {
62+
const file = files[0];
63+
if (!file) return;
6264

63-
if (!file.type.startsWith("image/")) {
64-
setError("Please select an image file");
65-
return;
66-
}
67-
68-
// Check file size (max 5MB)
69-
if (file.size > 5 * 1024 * 1024) {
70-
setError("File size must be less than 5MB");
71-
return;
72-
}
73-
74-
// Create preview URL
75-
const previewUrl = URL.createObjectURL(file);
65+
if (!file.type.startsWith("image/")) {
66+
setError("Please select an image file");
67+
return;
68+
}
7669

77-
const newFile: UploadedFile = {
78-
file,
79-
name: file.name,
80-
size: formatFileSize(file.size),
81-
progress: 0,
82-
preview: previewUrl,
83-
};
70+
if (file.size > 5 * 1024 * 1024) {
71+
setError("File size must be less than 5MB");
72+
return;
73+
}
8474

85-
setUploadedFiles([newFile]);
86-
setError(null);
87-
setSvgResult(null);
88-
};
75+
const previewUrl = URL.createObjectURL(file);
76+
77+
const newFile: UploadedFile = {
78+
file,
79+
name: file.name,
80+
size: formatFileSize(file.size),
81+
progress: 0,
82+
preview: previewUrl,
83+
};
84+
85+
setUploadedFiles([newFile]);
86+
setError(null);
87+
setSvgResult(null);
88+
},
89+
[setError, setUploadedFiles, setSvgResult],
90+
);
8991

90-
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
91-
e.preventDefault();
92-
setIsDragActive(false);
92+
const handleDrop = useCallback(
93+
(e: React.DragEvent<HTMLDivElement>) => {
94+
e.preventDefault();
95+
setIsDragActive(false);
9396

94-
const files = e.dataTransfer.files;
95-
if (files.length > 0) {
96-
handleFiles(files);
97-
}
98-
}, []);
97+
const files = e.dataTransfer.files;
98+
if (files.length > 0) {
99+
handleFiles(files);
100+
}
101+
},
102+
[handleFiles],
103+
);
99104

100105
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
101106
e.preventDefault();
@@ -329,7 +334,7 @@ export default function UploadPage() {
329334
{/* Image Preview */}
330335
<div className="mb-4">
331336
<div className="bg-slate-100 dark:bg-neutral-700 rounded-lg p-4 flex items-center justify-center min-h-[200px]">
332-
<img
337+
<Image
333338
src={file.preview}
334339
alt={file.name}
335340
className="max-w-full max-h-64 object-contain rounded-lg shadow-sm"

app/login/page.tsx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardHeader,
9+
CardTitle,
10+
} from "@/components/ui/card";
11+
import {
12+
Field,
13+
FieldDescription,
14+
FieldGroup,
15+
FieldLabel,
16+
FieldSeparator,
17+
} from "@/components/ui/field";
18+
import { Input } from "@/components/ui/input";
19+
import { createClient } from "@/lib/supabase/client";
20+
import Image from "next/image";
21+
import type { Provider } from "@supabase/supabase-js";
22+
import Link from "next/link";
23+
import { useRouter } from "next/navigation";
24+
import { useState } from "react";
25+
26+
const Login = () => {
27+
const [email, setEmail] = useState("");
28+
const [password, setPassword] = useState("");
29+
const [error, setError] = useState<string | null>(null);
30+
const [isLoading, setIsLoading] = useState(false);
31+
const router = useRouter();
32+
33+
const supabase = createClient();
34+
35+
const handleLogin = async (e: React.FormEvent) => {
36+
e.preventDefault();
37+
setIsLoading(true);
38+
setError(null);
39+
40+
try {
41+
const { error } = await supabase.auth.signInWithPassword({
42+
email,
43+
password,
44+
});
45+
if (error) throw error;
46+
router.push("/");
47+
} catch (error: unknown) {
48+
setError(error instanceof Error ? error.message : "An error occurred");
49+
} finally {
50+
setIsLoading(false);
51+
}
52+
};
53+
54+
const handleOAuthLogin = async (provider: Provider) => {
55+
setIsLoading(true);
56+
setError(null);
57+
58+
try {
59+
const { error } = await supabase.auth.signInWithOAuth({
60+
provider,
61+
options: {
62+
redirectTo: `${process.env.SITE_URL}/auth/callback`,
63+
},
64+
});
65+
if (error) throw error;
66+
} catch (error: unknown) {
67+
setError(error instanceof Error ? error.message : "An error occurred");
68+
} finally {
69+
setIsLoading(false);
70+
}
71+
};
72+
73+
return (
74+
<div className="bg-zinc-50 dark:bg-neutral-900/80 flex min-h-svh w-full items-center justify-center p-6 md:p-10">
75+
<div className="w-full max-w-sm">
76+
<div className="flex flex-col gap-6">
77+
<Card>
78+
<CardHeader className="text-center">
79+
<CardTitle className="text-xl">Welcome back</CardTitle>
80+
<CardDescription>
81+
Login with your GitHub or Google account
82+
</CardDescription>
83+
</CardHeader>
84+
<CardContent>
85+
<form onSubmit={handleLogin}>
86+
<FieldGroup>
87+
<Field>
88+
<Button
89+
onClick={() => handleOAuthLogin("github")}
90+
variant="outline"
91+
type="button"
92+
>
93+
<Image
94+
width="24"
95+
height="24"
96+
src="https://img.icons8.com/material-outlined/24/github.png"
97+
alt="github"
98+
/>
99+
Login with GitHub
100+
</Button>
101+
<Button
102+
onClick={() => handleOAuthLogin("google")}
103+
variant="outline"
104+
type="button"
105+
>
106+
<Image
107+
width="24"
108+
height="24"
109+
src="https://img.icons8.com/material-rounded/24/google-logo.png"
110+
alt="google-logo"
111+
/>
112+
Login with Google
113+
</Button>
114+
</Field>
115+
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
116+
Or continue with
117+
</FieldSeparator>
118+
<Field>
119+
<FieldLabel htmlFor="email">Email</FieldLabel>
120+
<Input
121+
id="email"
122+
type="email"
123+
placeholder="[email protected]"
124+
required
125+
value={email}
126+
onChange={(e) => setEmail(e.target.value)}
127+
/>
128+
</Field>
129+
<Field>
130+
<div className="flex items-center">
131+
<FieldLabel htmlFor="password">Password</FieldLabel>
132+
<Link
133+
href="/forgot-password"
134+
className="ml-auto text-sm underline-offset-4 hover:underline"
135+
>
136+
Forgot your password?
137+
</Link>
138+
</div>
139+
<Input
140+
id="password"
141+
type="password"
142+
required
143+
value={password}
144+
onChange={(e) => setPassword(e.target.value)}
145+
/>
146+
</Field>
147+
{error && <p className="text-sm text-red-500">{error}</p>}
148+
<Field>
149+
<Button
150+
type="submit"
151+
className="w-full"
152+
disabled={isLoading}
153+
>
154+
{isLoading ? "Logging in..." : "Login"}
155+
</Button>
156+
<FieldDescription className="text-center">
157+
Don&apos;t have an account?{" "}
158+
<Link href="/signup">Sign up</Link>
159+
</FieldDescription>
160+
</Field>
161+
</FieldGroup>
162+
</form>
163+
</CardContent>
164+
</Card>
165+
</div>
166+
</div>
167+
</div>
168+
);
169+
};
170+
171+
export default Login;

app/signup/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const Signup = () => {
2+
return <div>Signup</div>;
3+
};
4+
5+
export default Signup;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import { createClient } from "@/lib/supabase/client";
4+
import Link from "next/link";
5+
import { useEffect, useState } from "react";
6+
7+
const AuthButtons = () => {
8+
const supabase = createClient();
9+
const [user, setUser] = useState<string | null>(null);
10+
const [loading, setLoading] = useState(true);
11+
12+
useEffect(() => {
13+
const fetchUser = async () => {
14+
const { data } = await supabase.auth.getUser();
15+
if (data.user) setUser(data.user.user_metadata.name);
16+
setLoading(false);
17+
};
18+
fetchUser();
19+
}, [supabase.auth]);
20+
21+
return (
22+
<div>
23+
{user ? (
24+
<button className="hidden sm:flex text-xs border border-emerald-500 dark:border-purple-500 px-3 py-1.5 rounded-md hover:bg-emerald-100 dark:hover:bg-violet-900 transition-colors duration-200 items-center gap-1.5">
25+
<span>Logout</span>
26+
{!loading && (
27+
<span className="text-xs opacity-70">{`(${user})`}</span>
28+
)}
29+
</button>
30+
) : (
31+
<Link
32+
href="/login"
33+
className="text-sm border border-emerald-500 dark:border-purple-500 px-4 py-2 rounded-md hover:bg-emerald-100 dark:hover:bg-violet-900 transition-colors duration-200"
34+
>
35+
Login
36+
</Link>
37+
)}
38+
</div>
39+
);
40+
};
41+
42+
export default AuthButtons;

components/NavbarComponents/Navbar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Home, ImageUp } from "lucide-react";
44
import Link from "next/link";
55
import { usePathname } from "next/navigation";
66
import ThemeToggle from "./ThemeToggle";
7+
import AuthButtons from "./AuthButtons";
78

89
export default function Navbar() {
910
const pathname = usePathname();
@@ -46,8 +47,9 @@ export default function Navbar() {
4647
</div>
4748
</div>
4849

49-
<div className="flex items-center space-x-2 ml-auto">
50+
<div className="flex items-center space-x-2 ml-auto gap-3">
5051
<ThemeToggle />
52+
<AuthButtons />
5153
</div>
5254
</div>
5355
</nav>

components/ui/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
55
import { cn } from "@/lib/utils";
66

77
const buttonVariants = cva(
8-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
99
{
1010
variants: {
1111
variant: {

0 commit comments

Comments
 (0)