Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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`);
}
170 changes: 170 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"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 { Provider } from "@supabase/supabase-js";
import Link from "next/link";
import { redirect, useRouter } from "next/navigation";
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.

redirect is imported but not used. Remove it to avoid dead imports.

Suggested change
import { redirect, useRouter } from "next/navigation";
import { useRouter } from "next/navigation";

Copilot uses AI. Check for mistakes.
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"
>
<img
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"
>
<img
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;
43 changes: 43 additions & 0 deletions components/NavbarComponents/AuthButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"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<any>(null);
const [loading, setLoading] = useState(true);

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

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.user_metadata.name})`}</span>
)}
</button>
Comment on lines 24 to 29
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.

The Logout button has no onClick handler, so it won't sign the user out. Add a sign-out call, e.g., onClick={async () => { await supabase.auth.signOut(); location.reload(); }} or use router.refresh()/push after sign-out.

Copilot uses AI. Check for mistakes.
) : (
<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
92 changes: 92 additions & 0 deletions components/ui/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from "react";

import { cn } from "@/lib/utils";
Comment on lines +1 to +3
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.

Card is used in a Client Component (login page). Mark this file with 'use client' so it can be imported by Client Components.

Copilot uses AI. Check for mistakes.

function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}

function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}

export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
Loading
Loading