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
98 changes: 62 additions & 36 deletions frontend/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { isSupabaseConfigured, supabase } from "@/app/lib/supabase";
import { useToast } from "@/app/context/ToastContext";
import { z } from "zod";

export default function Login() {
const router = useRouter();
Expand All @@ -13,6 +14,8 @@ export default function Login() {
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [nextPath, setNextPath] = useState<string | null>(null);
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");

const redirectPath = nextPath && nextPath.startsWith("/") ? nextPath : "/dashboard";

Expand All @@ -32,56 +35,76 @@ export default function Login() {
return;
}

setLoading(true);
setLoading(true);

const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});

console.log("LOGIN DATA:", data);
console.log("LOGIN ERROR:", error);
setLoading(false);

setLoading(false);
if (error) {
showToast(error.message, "error");
return;
}

const { data: { session } } = await supabase.auth.getSession();

if (session) {
router.push("/dashboard");
router.refresh();
}
};

if (error) {
console.error(error);
alert(error.message);
showToast(error.message, "error");
return;
}
const emailSchema = z.string().email("Invalid email address");
const passwordSchema = z.string().min(8, "Password must be at least 8 characters");

const {
data: { session },
} = await supabase.auth.getSession();
const validateEmail = () => {
const result = emailSchema.safeParse(email);
setEmailError(result.success ? "" : (result.error?.errors?.[0]?.message ?? "Invalid email"));
};

console.log("SESSION:", session);
const validatePassword = () => {
const result = passwordSchema.safeParse(password);
setPasswordError(result.success ? "" : (result.error?.errors?.[0]?.message ?? "Invalid password"));
};

if (session) {
router.push("/dashboard");
router.refresh();
}
};
const isFormValid =
emailSchema.safeParse(email).success &&
passwordSchema.safeParse(password).success;

return (
<div className="flex h-screen items-center justify-center bg-zinc-950 text-white">
<form onSubmit={(e) => { e.preventDefault(); handleLogin(); }} className="bg-zinc-900 p-6 rounded-xl w-80" >
<form
onSubmit={(e) => { e.preventDefault(); handleLogin(); }}
className="bg-zinc-900 p-6 rounded-xl w-80"
>
<h1 className="text-xl mb-4">Login</h1>

<input
value={email}
className="w-full p-2 mb-3 bg-zinc-800 rounded"
type="email"
className={`w-full p-2 bg-zinc-800 rounded ${emailError ? "border border-red-500 mb-1" : "mb-3"}`}
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
value={email}
onChange={(e) => { setEmail(e.target.value); setEmailError(""); }}
onBlur={validateEmail}
/>

{emailError && (
<p className="text-red-400 text-xs mb-3">{emailError}</p>
)}

<input
value={password}
type="password"
className="w-full p-2 mb-3 bg-zinc-800 rounded"
className={`w-full p-2 bg-zinc-800 rounded ${passwordError ? "border border-red-500 mb-1" : "mb-3"}`}
placeholder="Password"
onChange={(e) => setPassword(e.target.value)}
value={password}
onChange={(e) => { setPassword(e.target.value); setPasswordError(""); }}
onBlur={validatePassword}
/>
{passwordError && (
<p className="text-red-400 text-xs mb-3">{passwordError}</p>
)}

<div className="flex justify-end mb-4">
<Link
Expand All @@ -92,10 +115,10 @@ export default function Login() {
Forgot password?
</Link>
</div>

<button
type="submit"
disabled={loading || !isSupabaseConfigured}
disabled={loading || !isSupabaseConfigured || !isFormValid}
className="w-full rounded bg-indigo-600 p-2 disabled:cursor-not-allowed disabled:opacity-70"
>
{loading ? "Logging in..." : "Login"}
Expand All @@ -109,11 +132,14 @@ export default function Login() {

<p className="mt-4 text-center text-sm text-zinc-300">
New here?{" "}
<Link href={nextPath ? `/signup?next=${encodeURIComponent(nextPath)}` : "/signup"} className="text-indigo-400 hover:text-indigo-300">
<Link
href={nextPath ? `/signup?next=${encodeURIComponent(nextPath)}` : "/signup"}
className="text-indigo-400 hover:text-indigo-300"
>
Create an account
</Link>
</p>
</form>
</form>
</div>
);
}
}
59 changes: 54 additions & 5 deletions frontend/app/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { supabase } from "@/app/lib/supabase";
import { signupSchema } from "@/lib/validations/auth";

function generatePassword(length = 14): string {
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Expand Down Expand Up @@ -45,6 +46,9 @@ export default function Signup() {
const [copied, setCopied] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
const [emailError, setEmailError] = useState("");
const [usernameError, setUsernameError] = useState("");
const [passwordStrength, setPasswordStrength] = useState<"" | "weak" | "medium" | "strong">("");

useEffect(() => {
const params = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -138,6 +142,26 @@ export default function Signup() {
}
};

const validateEmailField = () => {
const result = signupSchema.shape.email.safeParse(email);
setEmailError(result.success ? "" : (result.error?.errors?.[0]?.message ?? "Invalid email"));
};

const validateUsernameField = () => {
const result = signupSchema.shape.username.safeParse(username);
setUsernameError(result.success ? "" : (result.error?.errors?.[0]?.message ?? "Invalid username"));
};

const getPasswordStrength = (pwd: string): "" | "weak" | "medium" | "strong" => {
if (!pwd) return "";
const hasUpper = /[A-Z]/.test(pwd);
const hasLower = /[a-z]/.test(pwd);
const hasNumber = /[0-9]/.test(pwd);
if (pwd.length >= 10 && hasUpper && hasLower && hasNumber) return "strong";
if (pwd.length >= 8) return "medium";
return "weak";
};

const inputClass =
"w-full p-2 mb-3 bg-zinc-800 rounded border border-zinc-700 focus:outline-none focus:border-indigo-500";

Expand All @@ -157,20 +181,28 @@ export default function Signup() {

{/* Username */}
<input
className={inputClass}
className={`w-full p-2 bg-zinc-800 rounded border ${usernameError ? "border-red-500 mb-1" : "border-zinc-700 mb-3"} focus:outline-none focus:border-indigo-500`}
placeholder="Username *"
value={username}
onChange={(e) => setUsername(e.target.value.replace(/\s/g, ""))}
onChange={(e) => { setUsername(e.target.value.replace(/\s/g, "")); setUsernameError(""); }}
onBlur={validateUsernameField}
/>
{usernameError && (
<p className="text-red-400 text-xs mb-3">{usernameError}</p>
)}

{/* Email */}
<input
type="email"
className={inputClass}
className={`w-full p-2 bg-zinc-800 rounded border ${emailError ? "border-red-500 mb-1" : "border-zinc-700 mb-3"} focus:outline-none focus:border-indigo-500`}
placeholder="Email *"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => { setEmail(e.target.value); setEmailError(""); }}
onBlur={validateEmailField}
/>
{emailError && (
<p className="text-red-400 text-xs mb-3">{emailError}</p>
)}

{/* Mobile */}
<input
Expand Down Expand Up @@ -208,7 +240,10 @@ export default function Signup() {
className="w-full p-2 pr-16 bg-zinc-800 rounded border border-zinc-700 focus:outline-none focus:border-indigo-500"
placeholder="Password *"
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => {
setPassword(e.target.value);
setPasswordStrength(getPasswordStrength(e.target.value));
}}
/>
<button
type="button"
Expand All @@ -219,6 +254,20 @@ export default function Signup() {
</button>
</div>

{/* Password strength indicator */}
{passwordStrength && (
<div className="flex items-center gap-2 mb-3 -mt-1">
<div className="flex gap-1">
<div className={`h-1 w-8 rounded ${passwordStrength === "weak" ? "bg-red-500" : passwordStrength === "medium" ? "bg-yellow-400" : "bg-green-500"}`} />
<div className={`h-1 w-8 rounded ${passwordStrength === "medium" || passwordStrength === "strong" ? (passwordStrength === "medium" ? "bg-yellow-400" : "bg-green-500") : "bg-zinc-600"}`} />
<div className={`h-1 w-8 rounded ${passwordStrength === "strong" ? "bg-green-500" : "bg-zinc-600"}`} />
</div>
<span className={`text-xs ${passwordStrength === "weak" ? "text-red-400" : passwordStrength === "medium" ? "text-yellow-400" : "text-green-400"}`}>
{passwordStrength === "weak" ? "Weak" : passwordStrength === "medium" ? "Medium" : "Strong"}
</span>
</div>
)}

{/* Confirm Password */}
<div className="relative mb-4">
<input
Expand Down
Loading