Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from "next/server";
// The client you created from the Server-Side Auth instructions
import { createClient } from "@/lib/supabase/server";

export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
// if "next" is in param, use it as the redirect URL
let next = searchParams.get("next") ?? "/";
if (!next.startsWith("/")) {
// if "next" is not a relative URL, use the default
next = "/";
}

if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === "development";
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`);
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`);
} else {
return NextResponse.redirect(`${origin}${next}`);
}
}
}

// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}
7 changes: 7 additions & 0 deletions app/auth/callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { redirect } from "next/navigation";

const Page = () => {
redirect("/");
};

export default Page;
77 changes: 41 additions & 36 deletions app/generate/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Upload,
X,
} from "lucide-react";
import Image from "next/image";
import { useCallback, useEffect, useRef, useState } from "react";

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

const handleFiles = (files: FileList) => {
const file = files[0];
if (!file) return;
const handleFiles = useCallback(
(files: FileList) => {
const file = files[0];
if (!file) return;

if (!file.type.startsWith("image/")) {
setError("Please select an image file");
return;
}

// Check file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
setError("File size must be less than 5MB");
return;
}

// Create preview URL
const previewUrl = URL.createObjectURL(file);
if (!file.type.startsWith("image/")) {
setError("Please select an image file");
return;
}

const newFile: UploadedFile = {
file,
name: file.name,
size: formatFileSize(file.size),
progress: 0,
preview: previewUrl,
};
if (file.size > 5 * 1024 * 1024) {
setError("File size must be less than 5MB");
return;
}

setUploadedFiles([newFile]);
setError(null);
setSvgResult(null);
};
const previewUrl = URL.createObjectURL(file);

const newFile: UploadedFile = {
file,
name: file.name,
size: formatFileSize(file.size),
progress: 0,
preview: previewUrl,
};

setUploadedFiles([newFile]);
setError(null);
setSvgResult(null);
},
[setError, setUploadedFiles, setSvgResult],
);

const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragActive(false);
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragActive(false);

const files = e.dataTransfer.files;
if (files.length > 0) {
handleFiles(files);
}
}, []);
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFiles(files);
}
},
[handleFiles],
);

const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -329,7 +334,7 @@ export default function UploadPage() {
{/* Image Preview */}
<div className="mb-4">
<div className="bg-slate-100 dark:bg-neutral-700 rounded-lg p-4 flex items-center justify-center min-h-[200px]">
<img
<Image
src={file.preview}
alt={file.name}
className="max-w-full max-h-64 object-contain rounded-lg shadow-sm"
Expand Down
171 changes: 171 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"use client";

import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { createClient } from "@/lib/supabase/client";
import Image from "next/image";
import type { Provider } from "@supabase/supabase-js";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";

const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

const supabase = createClient();

const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);

try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
router.push("/");
} catch (error: unknown) {
setError(error instanceof Error ? error.message : "An error occurred");
} finally {
setIsLoading(false);
}
};

const handleOAuthLogin = async (provider: Provider) => {
setIsLoading(true);
setError(null);

try {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${process.env.SITE_URL}/auth/callback`,
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redirectTo points to /auth/callback and uses process.env.SITE_URL in a client component. Client-side env vars must be prefixed with NEXT_PUBLIC_, and your callback route is implemented at /api/auth/callback. Update to use the correct path and a client-safe origin, e.g.: redirectTo: ${window.location.origin}/api/auth/callback.

Suggested change
redirectTo: `${process.env.SITE_URL}/auth/callback`,
redirectTo: `${window.location.origin}/api/auth/callback`,

Copilot uses AI. Check for mistakes.
},
});
if (error) throw error;
} catch (error: unknown) {
setError(error instanceof Error ? error.message : "An error occurred");
} finally {
setIsLoading(false);
}
};

return (
<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">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Welcome back</CardTitle>
<CardDescription>
Login with your GitHub or Google account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
<FieldGroup>
<Field>
<Button
onClick={() => handleOAuthLogin("github")}
variant="outline"
type="button"
>
<Image
width="24"
height="24"
src="https://img.icons8.com/material-outlined/24/github.png"
alt="github"
/>
Login with GitHub
</Button>
<Button
onClick={() => handleOAuthLogin("google")}
variant="outline"
type="button"
>
<Image
width="24"
height="24"
src="https://img.icons8.com/material-rounded/24/google-logo.png"
alt="google-logo"
/>
Login with Google
</Button>
</Field>
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
Or continue with
</FieldSeparator>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
type="email"
placeholder="[email protected]"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</Field>
<Field>
<div className="flex items-center">
<FieldLabel htmlFor="password">Password</FieldLabel>
<Link
href="/forgot-password"
className="ml-auto text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Field>
{error && <p className="text-sm text-red-500">{error}</p>}
<Field>
<Button
type="submit"
className="w-full"
disabled={isLoading}
>
{isLoading ? "Logging in..." : "Login"}
</Button>
<FieldDescription className="text-center">
Don&apos;t have an account?{" "}
<Link href="/signup">Sign up</Link>
</FieldDescription>
</Field>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
</div>
</div>
);
};

export default Login;
5 changes: 5 additions & 0 deletions app/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Signup = () => {
return <div>Signup</div>;
};

export default Signup;
42 changes: 42 additions & 0 deletions components/NavbarComponents/AuthButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { createClient } from "@/lib/supabase/client";
import Link from "next/link";
import { useEffect, useState } from "react";

const AuthButtons = () => {
const supabase = createClient();
const [user, setUser] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchUser = async () => {
const { data } = await supabase.auth.getUser();
if (data.user) setUser(data.user.user_metadata.name);
setLoading(false);
};
fetchUser();
}, [supabase.auth]);

return (
<div>
{user ? (
<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">
<span>Logout</span>
{!loading && (
<span className="text-xs opacity-70">{`(${user})`}</span>
)}
</button>
) : (
<Link
href="/login"
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"
>
Login
</Link>
)}
</div>
);
};

export default AuthButtons;
4 changes: 3 additions & 1 deletion components/NavbarComponents/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Home, ImageUp } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import ThemeToggle from "./ThemeToggle";
import AuthButtons from "./AuthButtons";

export default function Navbar() {
const pathname = usePathname();
Expand Down Expand Up @@ -46,8 +47,9 @@ export default function Navbar() {
</div>
</div>

<div className="flex items-center space-x-2 ml-auto">
<div className="flex items-center space-x-2 ml-auto gap-3">
<ThemeToggle />
<AuthButtons />
</div>
</div>
</nav>
Expand Down
2 changes: 1 addition & 1 deletion components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
"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",
"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",
{
variants: {
variant: {
Expand Down
Loading
Loading