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
119 changes: 5 additions & 114 deletions src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,12 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { CheckCircle, AlertCircle, ArrowLeft, Loader2 } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import api from "@/lib/api";
import { getApiErrorMessage } from "@/types";

export default function ForgotPassword() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<{
type: "idle" | "loading" | "success" | "error";
message?: string;
}>({ type: "idle" });
const router = useRouter();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email.trim()) return;

setStatus({ type: "loading" });

try {
await api.post("/auth/forgot-password", { email: email.trim() });
setStatus({
type: "success",
message: "If an account exists, we've sent instructions to your email.",
});
// Optional: Redirect after delay
// setTimeout(() => router.push("/login"), 5000);
} catch (error: unknown) {
setStatus({
type: "error",
message: getApiErrorMessage(error, "Failed to send email."),
});
}
};
import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";

export default function ForgotPasswordPage() {
return (
<div className="flex min-h-screen w-full bg-white font-sans">
{/* Left Side - Visual Panel */}
<div className="hidden lg:flex w-1/2 bg-[var(--navy)] relative flex-col p-16 text-white overflow-hidden">
{/* Background Graphic */}
<div className="absolute inset-0 opacity-10 pointer-events-none">
<svg
className="h-full w-full"
Expand Down Expand Up @@ -75,10 +35,9 @@ export default function ForgotPassword() {
<h1 className="text-5xl font-bold leading-tight mb-5">
Account recovery.
</h1>

<p className="text-slate-400 text-lg xl:text-xl max-w-lg leading-relaxed">
Don't worry, it happens. Enter your email and we'll help you get
back to managing your projects in no time.
Don&apos;t worry, it happens. Enter your email and we&apos;ll help
you get back to managing your projects in no time.
</p>
</div>
</div>
Expand All @@ -90,75 +49,7 @@ export default function ForgotPassword() {

{/* Right Side - Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-8 sm:p-12">
<div className="w-full max-w-md space-y-8">
<div>
<Link
href="/login"
className="inline-flex items-center text-sm font-medium text-slate-500 hover:text-[var(--navy)] transition-colors mb-8 group"
>
<ArrowLeft className="w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform" />{" "}
Back to Login
</Link>
<h2 className="text-3xl font-bold text-slate-900">
Forgot Password?
</h2>
<p className="text-slate-500 mt-2">
Enter your email for reset instructions.
</p>
</div>

{status.type === "success" && (
<Alert className="bg-emerald-50 border-emerald-100 text-emerald-800 rounded-xl animate-in fade-in slide-in-from-top-2">
<CheckCircle className="h-4 w-4 text-emerald-600" />
<AlertDescription>{status.message}</AlertDescription>
</Alert>
)}

{status.type === "error" && (
<Alert
variant="destructive"
className="bg-red-50 border-red-100 text-red-800 rounded-xl animate-in fade-in slide-in-from-top-2"
>
<AlertCircle className="h-4 w-4 text-red-600" />
<AlertDescription>{status.message}</AlertDescription>
</Alert>
)}

<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email" className="text-slate-700 font-semibold">
Email Address
</Label>
<Input
id="email"
type="email"
placeholder="name@company.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={
status.type === "loading" || status.type === "success"
}
className="h-12 border-slate-200 focus-visible:ring-[var(--navy)] rounded-xl text-base"
/>
</div>

<Button
type="submit"
className="w-full h-12 bg-[var(--navy)] hover:bg-[var(--navy-hover)] text-white font-bold text-base rounded-xl shadow-lg shadow-slate-900/10 transition-all disabled:opacity-70"
disabled={status.type === "loading" || status.type === "success"}
>
{status.type === "loading" ? (
<div className="flex items-center gap-2">
<Loader2 className="animate-spin h-5 w-5" />
<span>Sending...</span>
</div>
) : (
"Send Reset Link"
)}
</Button>
</form>
</div>
<ForgotPasswordForm />
</div>
</div>
);
Expand Down
18 changes: 0 additions & 18 deletions src/app/(auth)/loading.tsx

This file was deleted.

213 changes: 9 additions & 204 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,14 @@
// app/login/page.tsx
"use client";

import { useState, useEffect } from "react";
import { useAuth } from "@/app/context/AuthContext";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { Eye, EyeOff, Loader2, CheckCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { reportError } from "@/lib/monitoring";

export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberEmail, setRememberEmail] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);

const { login, error, clearError, isAuthenticated, isLoading } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const justRegistered = searchParams.get("registered") === "true";

// Redirect if already authenticated
useEffect(() => {
if (isAuthenticated && !isLoading) {
router.replace("/home");
}
}, [isAuthenticated, isLoading, router]);

// Load remembered email
useEffect(() => {
const savedEmail = localStorage.getItem("rememberedEmail");
if (savedEmail) {
setEmail(savedEmail);
setRememberEmail(true);
}
}, []);

// Clear errors when inputs change
useEffect(() => {
if (error) clearError();
}, [email, password]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!email.trim() || !password.trim()) return;

setIsSubmitting(true);

try {
await login(email, password);

// Handle remember email (not sensitive - just email)
if (rememberEmail) {
localStorage.setItem("rememberedEmail", email);
} else {
localStorage.removeItem("rememberedEmail");
}
} catch (error: unknown) {
reportError(error, "Login page: login submission failed");
} finally {
setIsSubmitting(false);
}
};
import { LoginForm } from "@/components/auth/LoginForm";

// Show loading state while checking auth
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50">
<Loader2
className="h-8 w-8 animate-spin text-slate-400"
role="status"
aria-label="Loading"
/>
</div>
);
}
export default async function LoginPage({
searchParams,
}: {
searchParams: Promise<{ registered?: string }>;
}) {
const params = await searchParams;
const justRegistered = params.registered === "true";

return (
<div className="flex min-h-screen w-full bg-slate-50 font-sans">
Expand Down Expand Up @@ -113,7 +41,6 @@ export default function Login() {
<h1 className="text-5xl font-bold leading-tight mb-5">
Welcome back.
</h1>

<p className="text-slate-400 text-lg xl:text-xl max-w-lg leading-relaxed">
Experience the future of construction logistics. Log in to access
the booking calendar and asset management tools.
Expand All @@ -128,129 +55,7 @@ export default function Login() {

{/* Right Side - Login Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-6 sm:p-12">
<div className="w-full max-w-md space-y-8">
<div className="text-center lg:text-left">
<h2 className="text-3xl font-bold text-slate-900">Sign in</h2>
<p className="text-slate-500 mt-2">Access your dashboard</p>
</div>

{/* Success message for newly registered users */}
{justRegistered && (
<div
className="bg-emerald-50 border border-emerald-100 text-emerald-700 px-4 py-3 rounded-lg text-sm font-medium flex items-center gap-2 animate-in slide-in-from-top-2"
role="status"
>
<CheckCircle size={18} />
Account created successfully! Please sign in.
</div>
)}

{/* Error message */}
{error && (
<div
className="bg-red-50 border border-red-100 text-red-600 px-4 py-3 rounded-lg text-sm font-medium animate-in slide-in-from-top-2"
role="alert"
>
{error}
</div>
)}

<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email" className="text-slate-700 font-semibold">
Email
</Label>
<Input
id="email"
type="email"
placeholder="name@company.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isSubmitting}
autoComplete="email"
className="h-12 border-slate-200 focus-visible:ring-[var(--navy)] bg-white"
/>
</div>

<div className="space-y-2">
<div className="flex justify-between items-center">
<Label
htmlFor="password"
className="text-slate-700 font-semibold"
>
Password
</Label>
<Link
href="/forgot-password"
className="text-xs font-semibold text-[var(--navy)] hover:underline"
>
Forgot password?
</Link>
</div>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isSubmitting}
autoComplete="current-password"
className="h-12 pr-10 border-slate-200 focus-visible:ring-[var(--navy)] bg-white"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>

<div className="flex items-center space-x-2 pt-2">
<Checkbox
id="remember"
checked={rememberEmail}
onCheckedChange={(checked) =>
setRememberEmail(checked as boolean)
}
className="border-slate-300 data-[state=checked]:bg-[var(--navy)] data-[state=checked]:border-[var(--navy)]"
/>
<label
htmlFor="remember"
className="text-sm text-slate-600 font-medium cursor-pointer select-none"
>
Remember my email
</label>
</div>

<Button
type="submit"
className="w-full h-12 bg-[var(--navy)] hover:bg-[var(--navy-hover)] text-white font-bold text-base transition-all shadow-lg shadow-slate-900/10 disabled:opacity-70"
disabled={isSubmitting || !email.trim() || !password.trim()}
>
{isSubmitting ? (
<Loader2 className="animate-spin h-5 w-5" />
) : (
"Sign In"
)}
</Button>
</form>

<p className="text-center text-sm text-slate-500">
Don&apos;t have an account?{" "}
<Link
href="/register"
className="font-bold text-[var(--navy)] hover:underline"
>
Create one here
</Link>
</p>
</div>
<LoginForm justRegistered={justRegistered} />
</div>
</div>
);
Expand Down
Loading