vbytes-lan-registration/src/Web/lan-frontend/app/volunteer/page.tsx
2026-02-23 21:56:14 +01:00

384 lines
13 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
interface EventContent {
volunteerAreas: string;
}
const isValidEmail = (value: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const normalizeMobileNumber = (value: string) => {
const digitsOnly = value.replace(/\D/g, "");
if (digitsOnly.startsWith("0046")) return `0${digitsOnly.slice(4)}`;
if (digitsOnly.startsWith("46")) return `0${digitsOnly.slice(2)}`;
if (digitsOnly.startsWith("7")) return `0${digitsOnly}`;
return digitsOnly;
};
const isValidMobileNumber = (value: string) =>
/^07\d{8}$/.test(normalizeMobileNumber(value));
export default function VolunteerPage() {
const router = useRouter();
const [content, setContent] = useState<EventContent | null>(null);
const [formData, setFormData] = useState({
firstName: "",
surName: "",
phoneNumber: "",
email: "",
hasApprovedGdpr: false,
selectedAreas: [] as string[],
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState({ type: "", text: "" });
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
useEffect(() => {
fetch("/api/Content")
.then((res) => res.json())
.then((data) => setContent(data))
.catch((err) => console.error("Failed to fetch content", err));
}, []);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
setFieldErrors((prev) => ({
...prev,
[name]: "",
}));
};
const handleAreaToggle = (area: string) => {
setFormData((prev) => {
const areas = prev.selectedAreas.includes(area)
? prev.selectedAreas.filter((a) => a !== area)
: [...prev.selectedAreas, area];
return { ...prev, selectedAreas: areas };
});
setFieldErrors((prev) => ({
...prev,
selectedAreas: "",
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const trimmedFirstName = formData.firstName.trim();
const trimmedSurName = formData.surName.trim();
const trimmedPhoneNumber = formData.phoneNumber.trim();
const trimmedEmail = formData.email.trim();
const normalizedPhoneNumber = normalizeMobileNumber(trimmedPhoneNumber);
const nextFieldErrors: Record<string, string> = {};
if (!trimmedFirstName)
nextFieldErrors.firstName = "Förnamn är obligatoriskt.";
if (!trimmedSurName)
nextFieldErrors.surName = "Efternamn är obligatoriskt.";
if (!trimmedPhoneNumber)
nextFieldErrors.phoneNumber = "Mobilnummer är obligatoriskt.";
if (!trimmedEmail) nextFieldErrors.email = "E-post är obligatorisk.";
if (trimmedEmail && !isValidEmail(trimmedEmail)) {
nextFieldErrors.email = "E-postadressen är ogiltig.";
}
if (trimmedPhoneNumber && !isValidMobileNumber(trimmedPhoneNumber)) {
nextFieldErrors.phoneNumber =
"Mobilnummer måste vara ett giltigt svenskt mobilnummer.";
}
if (formData.selectedAreas.length === 0) {
nextFieldErrors.selectedAreas =
"Vänligen välj minst ett område du vill hjälpa till med.";
}
if (Object.keys(nextFieldErrors).length > 0) {
setFieldErrors(nextFieldErrors);
setMessage({
type: "error",
text: "Kontrollera markerade fält och försök igen.",
});
return;
}
setFieldErrors({});
setIsSubmitting(true);
setMessage({ type: "info", text: "Skickar in din ansökan..." });
try {
const payload = {
firstName: trimmedFirstName,
surName: trimmedSurName,
phoneNumber: normalizedPhoneNumber,
email: trimmedEmail,
hasApprovedGdpr: formData.hasApprovedGdpr,
areasOfInterest: formData.selectedAreas.map((name) => ({
name: name.trim(),
})),
};
const response = await fetch("/api/Volunteer/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.ok) {
setMessage({
type: "success",
text: "Tack för din ansökan! Vi kommer kontakta dig snart.",
});
setFormData({
firstName: "",
surName: "",
phoneNumber: "",
email: "",
hasApprovedGdpr: false,
selectedAreas: [],
});
} else {
const errorData = await response.json();
setMessage({
type: "error",
text: errorData.message || "Ett fel uppstod vid registreringen.",
});
}
} catch (error) {
console.error("Volunteer registration error:", error);
setMessage({
type: "error",
text: "Kunde inte ansluta till servern. Försök igen senare.",
});
} finally {
setIsSubmitting(false);
}
};
const areas =
content?.volunteerAreas?.split("\n").filter((a) => a.trim()) || [];
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto bg-white p-8 rounded-xl shadow-md">
<div className="mb-3">
<button
type="button"
onClick={() => router.push("/")}
className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
>
<span></span>
<span className="sm:hidden">Tillbaka</span>
<span className="hidden sm:inline">Tillbaka till information</span>
</button>
</div>
<div className="flex justify-center mb-6">
<Image
src="/vBytes_Logotyp.png"
alt="vBytes"
width={320}
height={128}
className="h-auto w-44"
priority
/>
</div>
<div className="text-center mb-10">
<h1 className="text-3xl font-bold text-gray-900">Bli Funktionär</h1>
<p className="mt-2 text-gray-600">
Fyll i formuläret för att anmäla ditt intresse som funktionär under
LANet.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2">
<div>
<label
htmlFor="firstName"
className="block text-sm font-medium text-gray-700"
>
Förnamn *
</label>
<input
type="text"
name="firstName"
id="firstName"
required
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.firstName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
value={formData.firstName}
onChange={handleInputChange}
/>
{fieldErrors.firstName && (
<p className="mt-1 text-xs text-red-600">
{fieldErrors.firstName}
</p>
)}
</div>
<div>
<label
htmlFor="surName"
className="block text-sm font-medium text-gray-700"
>
Efternamn *
</label>
<input
type="text"
name="surName"
id="surName"
required
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.surName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
value={formData.surName}
onChange={handleInputChange}
/>
{fieldErrors.surName && (
<p className="mt-1 text-xs text-red-600">
{fieldErrors.surName}
</p>
)}
</div>
<div>
<label
htmlFor="phoneNumber"
className="block text-sm font-medium text-gray-700"
>
Mobilnummer *
</label>
<input
type="tel"
name="phoneNumber"
id="phoneNumber"
required
inputMode="tel"
placeholder="07XXXXXXXX"
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.phoneNumber ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
value={formData.phoneNumber}
onChange={handleInputChange}
/>
{fieldErrors.phoneNumber && (
<p className="mt-1 text-xs text-red-600">
{fieldErrors.phoneNumber}
</p>
)}
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
E-post *
</label>
<input
type="email"
name="email"
id="email"
required
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.email ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
value={formData.email}
onChange={handleInputChange}
/>
{fieldErrors.email && (
<p className="mt-1 text-xs text-red-600">{fieldErrors.email}</p>
)}
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Områden jag kan hjälpa till med *
</label>
<div
className={`grid grid-cols-1 sm:grid-cols-2 gap-2 rounded-md ${fieldErrors.selectedAreas ? "border border-red-500 p-2" : ""}`}
>
{areas.map((area) => (
<label
key={area}
className="flex items-center space-x-3 p-3 border rounded-md hover:bg-gray-50 cursor-pointer transition"
>
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
checked={formData.selectedAreas.includes(area)}
onChange={() => handleAreaToggle(area)}
/>
<span className="text-sm text-gray-700">{area}</span>
</label>
))}
</div>
{fieldErrors.selectedAreas && (
<p className="mt-2 text-xs text-red-600">
{fieldErrors.selectedAreas}
</p>
)}
</div>
<div className="sm:col-span-2 space-y-4 pt-4 border-t border-gray-100">
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="hasApprovedGdpr"
name="hasApprovedGdpr"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
checked={formData.hasApprovedGdpr}
onChange={handleInputChange}
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor="hasApprovedGdpr"
className="font-medium text-gray-700"
>
GDPR-godkännande *
</label>
<p className="text-gray-500">
Jag godkänner att mina personuppgifter behandlas i syfte för
detta evenemang.
</p>
</div>
</div>
</div>
</div>
{message.text && (
<div
className={`p-4 rounded-md ${
message.type === "success"
? "bg-green-50 text-green-800 border border-green-200"
: message.type === "error"
? "bg-red-50 text-red-800 border border-red-200"
: "bg-blue-50 text-blue-800 border border-blue-200"
}`}
>
{message.text}
</div>
)}
<div className="pt-6">
<button
type="submit"
disabled={isSubmitting}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-blue-400"
>
{isSubmitting ? "Skickar..." : "Anmäl dig som funktionär"}
</button>
</div>
</form>
</div>
</div>
);
}