Initial commit

Next.js + Express event management app for Hope Family Church.
This commit is contained in:
2026-07-23 15:26:47 +02:00
commit 3d381944d2
246 changed files with 57565 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
"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-blue-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-blue-600 hover:underline"
>
Forgot your password?
</a>
</div>
<div className="mt-1">
<a
href={registerHref}
className="text-xs text-blue-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&apos;t receive it, try logging in again to resend it.</p>
)}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 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 youve read and agree to our{" "}
<Link
href="/legal/terms"
target="_blank"
className="text-blue-600 hover:underline"
>
Terms of Use
</Link>{" "}
and{" "}
<Link
href="/legal/privacy"
target="_blank"
className="text-blue-600 hover:underline"
>
Privacy Policy
</Link>.
</p>
</form>
);
}