Skip to content
Open
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
24 changes: 24 additions & 0 deletions app/api/forgot-password/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";

export async function POST(req: Request) {
try {
const { email } = await req.json();

if (!email) {
return NextResponse.json(
{ message: "Email is required." },
{ status: 400 },
);
}
console.log("Password reset requested for:", email);

return NextResponse.json({
message: "If the email exists, reset link sent.",
});
} catch (err) {
return NextResponse.json(
{ message: "Internal server error." },
{ status: 500 },
);
}
}
64 changes: 42 additions & 22 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ export default function LoginPage() {
setError("Please fill in both email and password.");
return;
}

setLoading(true);
setError(null);

try {
const response = await signIn("credentials", {
email: trimmedEmail,
password: password,
password,
callbackUrl: "/dashboard",
redirect: false,
});
Expand All @@ -48,63 +49,74 @@ export default function LoginPage() {
if (response?.url) {
window.location.href = response.url;
}
} catch {
} catch (error) {
setError("Login failed. Please try again.");
} finally {
setLoading(false);
}
}

function isEmailAndPasswordEmpty() {
return !email.trim().length || !password.trim().length;
}
const isEmailAndPasswordEmpty = () =>
!email.trim().length || !password.trim().length;

return (
<>
<Navbar />
<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4">
<div className="w-full max-w-md space-y-3 rounded-xl border bg-background p-6 shadow-sm">
<div className="w-full max-w-md space-y-4 rounded-xl border bg-background p-6 shadow-sm">
{/* HEADER */}
<div className="text-center space-y-1">
<div className="space-y-1 text-center">
<h1 className="text-2xl font-bold">Welcome back</h1>
<p className="text-sm text-muted-foreground">
Login to your LinkID
</p>
</div>

{/* OAUTH */}
{/* OAUTH BUTTONS */}
<div className="space-y-2">
<Button
variant="outline"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
className="flex w-full items-center justify-center gap-2 cursor-pointer"
disabled={googleLoading || githubLoading}
onClick={async () => {
setGoogleLoading(true);
try {
await signIn(PLATFORMS.GOOGLE, { callbackUrl: "/dashboard" });
await signIn(PLATFORMS.GOOGLE, {
callbackUrl: "/dashboard",
});
} finally {
setGoogleLoading(false);
}
}}
>
{googleLoading ? <Spinner className="h-5 w-5" /> : <FcGoogle className="h-5 w-5" />}
{googleLoading ? (
<Spinner className="h-5 w-5" />
) : (
<FcGoogle className="h-5 w-5" />
)}
{googleLoading ? "Connecting..." : "Continue with Google"}
</Button>

<Button
variant="outline"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
className="flex w-full items-center justify-center gap-2 cursor-pointer"
disabled={googleLoading || githubLoading}
onClick={async () => {
setGithubLoading(true);
try {
await signIn(PLATFORMS.GITHUB, { callbackUrl: "/dashboard" });
await signIn(PLATFORMS.GITHUB, {
callbackUrl: "/dashboard",
});
} finally {
setGithubLoading(false);
}
}}
>
{githubLoading ? <Spinner className="h-5 w-5" /> : <FaGithub className="h-5 w-5" />}
{githubLoading ? (
<Spinner className="h-5 w-5" />
) : (
<FaGithub className="h-5 w-5" />
)}
{githubLoading ? "Connecting..." : "Continue with GitHub"}
</Button>
</div>
Expand All @@ -116,7 +128,7 @@ export default function LoginPage() {
<div className="h-px w-full bg-border" />
</div>

{/* FORM */}
{/* LOGIN FORM */}
<form
className="space-y-3"
onSubmit={(e) => {
Expand All @@ -143,7 +155,7 @@ export default function LoginPage() {
}}
/>

{/* PASSWORD WITH TOGGLE */}
{/* PASSWORD FIELD */}
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
Expand All @@ -161,7 +173,7 @@ export default function LoginPage() {

<button
type="button"
onClick={() => setShowPassword(!showPassword)}
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showPassword ? "Hide password" : "Show password"}
>
Expand All @@ -173,10 +185,11 @@ export default function LoginPage() {
</button>
</div>

{/* FORGOT PASSWORD LINK */}
<div className="flex justify-end">
<Link
href="/forgot-password"
className="text-sm text-muted-foreground hover:underline"
href="/password"
className="text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline"
>
Forgot password?
</Link>
Expand All @@ -187,15 +200,22 @@ export default function LoginPage() {
type="submit"
disabled={loading || isEmailAndPasswordEmpty()}
>
{loading ? "Logging in..." : "Login with Email"}
{loading ? (
<>
<Spinner className="mr-2 h-4 w-4" />
Logging in...
</>
) : (
"Login with Email"
)}
</Button>
</form>

{/* FOOTER */}
<p className="text-center text-sm text-muted-foreground">
Dont have an account?{" "}
Don&apos;t have an account?{" "}
<Link href="/register" className="font-medium hover:underline">
Signup
Sign up
</Link>
</p>
</div>
Expand Down
109 changes: 109 additions & 0 deletions app/password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import { FormEvent, useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Navbar } from "../components/Navbar";

export default function ForgotPasswordPage() {
const [email, setEmail] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [message, setMessage] = useState<string>("");
const [error, setError] = useState<string>("");

const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();

if (!email.trim()) {
setError("Please enter your email address.");
return;
}

setLoading(true);
setError("");
setMessage("");

try {
const response = await fetch("/api/forgot-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email.trim(),
}),
});

const data: { message?: string } = await response.json();

if (!response.ok) {
throw new Error(data.message || "Something went wrong.");
}

setMessage(
"If an account exists with that email, a password reset link has been sent.",
);

setEmail("");
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to send reset email.",
);
} finally {
setLoading(false);
}
};

return (
<>
<Navbar />

<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4">
<div className="w-full max-w-md rounded-xl border bg-background p-6 shadow-sm">
<div className="mb-6 text-center">
<h1 className="text-2xl font-bold">Forgot Password</h1>

<p className="mt-2 text-sm text-muted-foreground">
Enter your email address and we'll send you a password reset link.
</p>
</div>

<form onSubmit={handleSubmit} className="space-y-4">
{error && <p className="text-sm text-red-500">{error}</p>}

{message && <p className="text-sm text-green-600">{message}</p>}

<Input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError("");
}}
disabled={loading}
/>

<Button
type="submit"
className="w-full"
disabled={loading || !email.trim()}
>
{loading ? "Sending Reset Link..." : "Send Reset Link"}
</Button>
</form>

<div className="mt-4 text-center">
<Link
href="/login"
className="text-sm text-muted-foreground hover:underline"
>
Back to Login
</Link>
</div>
</div>
</div>
</>
);
}
Loading