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
66 changes: 0 additions & 66 deletions middleware.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
const next = searchParams.get("next") ?? "/map";
const next = searchParams.get("next") ?? "/";

if (code) {
const cookieStore = await cookies();
Expand Down
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Analytics } from "@/components/analytics";
import { cn } from "@/lib/utils";

export const metadata: Metadata = {
metadataBase: new URL(site.url),
title: {
default: `${site.name}: student places and benefits`,
template: `%s | ${site.name}`,
Expand Down
195 changes: 195 additions & 0 deletions src/app/login/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"use client";

import * as React from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";

import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

export function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const next = searchParams.get("next") ?? "/";

const supabase = createClient();

const [mode, setMode] = React.useState<"signin" | "signup">("signin");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [loading, setLoading] = React.useState(false);

async function handleEmailAuth(e: React.FormEvent) {
e.preventDefault();
setLoading(true);

if (mode === "signup") {
const { error } = await supabase.auth.signUp({ email, password });
if (error) {
toast.error(error.message);
} else {
toast.success("Check your email to confirm your account.");
}
} else {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
toast.error(error.message);
} else {
router.push(next);
router.refresh();
}
}

setLoading(false);
}

async function handleGoogle() {
const { error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/auth/callback?next=${encodeURIComponent(next)}`,
},
});
if (error) toast.error(error.message);
}

return (
<div className="flex min-h-[calc(100dvh-3.5rem)] items-center justify-center px-4">
<div className="w-full max-w-sm space-y-6">
{/* Logo */}
<div className="flex flex-col items-center gap-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/logo-light.svg"
alt="StudyMap"
width={160}
height={30}
className="h-8 w-auto dark:hidden"
/>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/logo-dark.svg"
alt="StudyMap"
width={160}
height={30}
className="hidden h-8 w-auto dark:block"
/>
<p className="text-sm text-muted-foreground">
{mode === "signin" ? "Sign in to continue" : "Create your account"}
</p>
</div>

{/* Card */}
<div className="rounded-xl border bg-card p-6 shadow-sm space-y-5">
{/* Google */}
<Button
type="button"
variant="outline"
className="w-full h-9 gap-2"
onClick={handleGoogle}
disabled={loading}
>
<svg viewBox="0 0 24 24" className="size-4 shrink-0">
<path
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"
fill="#4285F4"
/>
<path
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"
fill="#34A853"
/>
<path
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.93l3.66-2.84z"
fill="#FBBC05"
/>
<path
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"
fill="#EA4335"
/>
</svg>
Continue with Google
</Button>

<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="bg-card px-2 text-xs text-muted-foreground">
or
</span>
</div>
</div>

{/* Email form */}
<form onSubmit={handleEmailAuth} className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder={mode === "signup" ? "Min. 6 characters" : "••••••••"}
autoComplete={mode === "signin" ? "current-password" : "new-password"}
required
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</div>

<Button type="submit" className="w-full h-9" disabled={loading}>
{loading && <Loader2 className="size-4 animate-spin" />}
{mode === "signin" ? "Sign in" : "Create account"}
</Button>
</form>
</div>

{/* Toggle mode */}
<p className="text-center text-sm text-muted-foreground">
{mode === "signin" ? (
<>
No account?{" "}
<button
type="button"
onClick={() => setMode("signup")}
className="text-foreground underline underline-offset-4 hover:text-primary"
>
Sign up
</button>
</>
) : (
<>
Already have an account?{" "}
<button
type="button"
onClick={() => setMode("signin")}
className="text-foreground underline underline-offset-4 hover:text-primary"
>
Sign in
</button>
</>
)}
</p>
</div>
</div>
);
}
Loading
Loading