Multi-phase visual facelift (design tokens, dashboards, sidebar/navbar shell, per-page help guides, and a layout/content pass across every remaining page) plus backend fixes to the dashboard KPI stats: - Admin/Supervisor dashboard KPIs (revenue, donations, registrations, tickets sold) now use a rolling trailing-month window (today back one calendar month, e.g. 9 May - 8 June if today is 8 June) instead of calendar month-to-date, which under-counted for most of the month. The comparison window shifts the same way, so like is still compared with like. - Reports deep-links from those stat tiles now match the same window (range=trailing_month, replacing range=this_month). - Design tokens (brand-* Tailwind scale + shadcn CSS variables), a site-wide contextual help button, fixed dashboard sidebar/navbar, Admin/Supervisor/Staff/User dashboard rebuilds backed by a new GET /api/stats/overview endpoint, a dedicated Contact page, Site Settings restyle with WhatsApp config folded in, and an Account activity feed backed by a new SecurityEvent model. - Every remaining page (home, events, registration flow, auth, legal, payment results, and every Admin/Supervisor/Staff/User tool page) restyled onto the same design tokens, several with real layout upgrades (home hero, events list/detail, donate page, auth pages). - 20+ new dedicated help guides so the whole site has page-specific help content instead of falling back to a generic guide. - Assorted fixes surfaced along the way: donation-leg double-counting in payment stats, donations not counting toward revenue, refund netting in per-method report breakdowns, and donation over-allocation after a refund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState } from "react";
|
||
import { useAuth } from "@/hooks/useAuth";
|
||
import { useSearchParams } from "next/navigation";
|
||
import Link from "next/link";
|
||
|
||
export function LoginForm() {
|
||
const { login } = useAuth();
|
||
const searchParams = useSearchParams();
|
||
const raw = searchParams.get("redirect") || "";
|
||
const hasExplicitRedirect = raw.startsWith("/") && !raw.startsWith("//");
|
||
const redirectTo = hasExplicitRedirect ? raw : "/dashboard";
|
||
const registerHref = redirectTo !== "/dashboard" ? `/register?redirect=${encodeURIComponent(redirectTo)}` : "/register";
|
||
|
||
const [email, setEmail] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const getErrorMessage = (err: any) => {
|
||
if (!err) return "Login failed";
|
||
|
||
// If it's already a normal Error
|
||
if (err.message && typeof err.message === "string") {
|
||
// Handle case where message is accidentally JSON string
|
||
try {
|
||
const parsed = JSON.parse(err.message);
|
||
return parsed.message || err.message;
|
||
} catch {
|
||
return err.message;
|
||
}
|
||
}
|
||
|
||
// If API response object was thrown directly
|
||
if (err.message) return err.message;
|
||
|
||
return "Login failed";
|
||
};
|
||
|
||
const onSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setError(null);
|
||
setLoading(true);
|
||
|
||
try {
|
||
const loggedInUser = await login(email, password);
|
||
// Skip the generic /dashboard hop (which just redirects again once the role is known)
|
||
// and go straight to the role-specific dashboard, unless the caller asked for a specific
|
||
// page back (e.g. returning to a registration flow after login).
|
||
const target = hasExplicitRedirect ? redirectTo : `/dashboard/${loggedInUser.role || "user"}`;
|
||
window.location.href = target;
|
||
} catch (err: any) {
|
||
setError(getErrorMessage(err) || "Login failed. Please try again.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={onSubmit} className="space-y-4 max-w-sm w-full">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1">Email or phone</label>
|
||
<input
|
||
type="text"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
className="w-full border rounded px-3 py-2"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1">Password</label>
|
||
<div className="relative">
|
||
<input
|
||
type={showPassword ? "text" : "password"}
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
className="w-full border rounded px-3 py-2 pr-20"
|
||
required
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword((s) => !s)}
|
||
className="absolute right-2 top-1/2 -translate-y-1/2 text-sm text-brand-600"
|
||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||
>
|
||
{showPassword ? "Hide" : "Show"}
|
||
</button>
|
||
</div>
|
||
<div className="mt-1">
|
||
<a
|
||
href="/forgot-password"
|
||
className="text-xs text-brand-600 hover:underline"
|
||
>
|
||
Forgot your password?
|
||
</a>
|
||
</div>
|
||
<div className="mt-1">
|
||
<a
|
||
href={registerHref}
|
||
className="text-xs text-brand-600 hover:underline"
|
||
>
|
||
Don't have an account? Sign up here.
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="text-sm text-red-600">
|
||
<p>{error}</p>
|
||
{error.toLowerCase().includes('not yet active') && (
|
||
<p className="mt-1 text-gray-600">Check your inbox for an activation email. If you didn't receive it, try logging in again to resend it.</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="w-full bg-brand-600 hover:bg-brand-700 text-white rounded py-2 disabled:opacity-60"
|
||
>
|
||
{loading ? "Logging in..." : "Login"}
|
||
</button>
|
||
|
||
<p className="text-xs text-gray-500 text-center mt-3">
|
||
By signing in, you acknowledge that you’ve read and agree to our{" "}
|
||
<Link
|
||
href="/legal/terms"
|
||
target="_blank"
|
||
className="text-brand-600 hover:underline"
|
||
>
|
||
Terms of Use
|
||
</Link>{" "}
|
||
and{" "}
|
||
<Link
|
||
href="/legal/privacy"
|
||
target="_blank"
|
||
className="text-brand-600 hover:underline"
|
||
>
|
||
Privacy Policy
|
||
</Link>.
|
||
</p>
|
||
</form>
|
||
);
|
||
}
|