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
@@ -0,0 +1,184 @@
"use client";
import React, { useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { isValidZAPhone } from "@/lib/phone";
export function RegisterForm() {
const { register } = useAuth();
const searchParams = useSearchParams();
const raw = searchParams.get("redirect") || "";
const redirectTo = raw.startsWith("/") && !raw.startsWith("//") ? raw : "/dashboard";
const loginHref = redirectTo !== "/dashboard" ? `/login?redirect=${encodeURIComponent(redirectTo)}` : "/login";
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [notificationPreference, setNotificationPreference] = useState<"email" | "whatsapp" | "both">("email");
const [accepted, setAccepted] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const hasValidPhone = isValidZAPhone(phoneNumber);
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 "Registration failed. Please try again.";
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!accepted) {
setError("Please accept the Terms of Use and Privacy Policy before continuing.");
return;
}
setError(null);
setLoading(true);
try {
await register({
name,
email,
password,
phoneNumber: phoneNumber || undefined,
notificationPreference: hasValidPhone ? notificationPreference : "email",
});
window.location.href = redirectTo;
} catch (err: any) {
setError(getErrorMessage(err) || "Registration 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">Full Name</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full border rounded px-3 py-2"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
type="email"
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>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full border rounded px-3 py-2"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Phone Number (optional)</label>
<input
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="e.g. 082 123 4567"
className="w-full border rounded px-3 py-2"
/>
</div>
{hasValidPhone && (
<div>
<label className="block text-sm font-medium mb-1">Notification Preference</label>
<select
value={notificationPreference}
onChange={(e) => setNotificationPreference(e.target.value as "email" | "whatsapp" | "both")}
className="w-full border rounded px-3 py-2"
>
<option value="email">Email only</option>
<option value="whatsapp">WhatsApp only</option>
<option value="both">Email &amp; WhatsApp</option>
</select>
</div>
)}
<div className="flex items-start gap-2">
<input
id="terms"
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
className="mt-1 h-4 w-4 border-gray-300 rounded accent-blue-600"
required
/>
<label htmlFor="terms" className="text-sm text-gray-700 leading-snug">
I confirm that Ive read and agree to the{" "}
<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>.
</label>
</div>
<div className="mt-1">
<a
href={loginHref}
className="text-xs text-blue-600 hover:underline"
>
Already have an account? Sign in here.
</a>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={loading || !accepted}
className={`w-full rounded py-2 text-white transition ${
accepted
? "bg-blue-600 hover:bg-blue-700"
: "bg-gray-400 cursor-not-allowed"
}`}
>
{loading ? "Creating account..." : "Create account"}
</button>
</form>
);
}