add validation on forms in frontend
This commit is contained in:
parent
1eb24d812e
commit
0f673c6a6e
4 changed files with 511 additions and 89 deletions
|
|
@ -40,7 +40,7 @@ export default function LandingPage() {
|
|||
return (
|
||||
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
||||
{/* Hero Section */}
|
||||
<header className="relative h-[70vh] flex items-center justify-center bg-black overflow-hidden">
|
||||
<header className="relative h-[52vh] flex items-center justify-center bg-black overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-50">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1542751371-adc38448a05e?auto=format&fit=crop&q=80&w=2070"
|
||||
|
|
@ -60,15 +60,34 @@ export default function LandingPage() {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-6 pt-6 flex justify-center">
|
||||
<Image
|
||||
src="/vBytes_Logotyp.png"
|
||||
alt="vBytes"
|
||||
width={320}
|
||||
height={128}
|
||||
className="h-auto w-40"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<main className="max-w-4xl mx-auto py-16 px-6 space-y-20">
|
||||
{(content.eventDate || content.eventTime || content.locationName || content.locationAddress || content.additionalInfo) && (
|
||||
<main className="max-w-4xl mx-auto py-12 px-6 space-y-20">
|
||||
{(content.eventDate ||
|
||||
content.eventTime ||
|
||||
content.locationName ||
|
||||
content.locationAddress ||
|
||||
content.additionalInfo) && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">Information</h2>
|
||||
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">
|
||||
Information
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-6">
|
||||
{(content.eventDate || content.eventTime) && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg text-blue-600">Tid/Datum</h3>
|
||||
<h3 className="font-semibold text-lg text-blue-600">
|
||||
Tid/Datum
|
||||
</h3>
|
||||
{content.eventDate && <p>{content.eventDate}</p>}
|
||||
{content.eventTime && <p>{content.eventTime}</p>}
|
||||
</div>
|
||||
|
|
@ -91,18 +110,25 @@ export default function LandingPage() {
|
|||
|
||||
{content.whatToBring && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">Ta Med(bokad LAN-plats)</h2>
|
||||
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">
|
||||
Ta Med(bokad LAN-plats)
|
||||
</h2>
|
||||
<ul className="list-disc list-inside space-y-3 text-lg text-gray-700">
|
||||
{content.whatToBring.split('\n').filter(line => line.trim()).map((line, i) => (
|
||||
<li key={i}>{line}</li>
|
||||
))}
|
||||
{content.whatToBring
|
||||
.split("\n")
|
||||
.filter((line) => line.trim())
|
||||
.map((line, i) => (
|
||||
<li key={i}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{content.rulesAndGdpr && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">Regler för LAN & GDPR</h2>
|
||||
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">
|
||||
Regler för LAN & GDPR
|
||||
</h2>
|
||||
<p className="text-lg text-gray-700 leading-relaxed whitespace-pre-wrap">
|
||||
{content.rulesAndGdpr}
|
||||
</p>
|
||||
|
|
@ -114,7 +140,9 @@ export default function LandingPage() {
|
|||
<section className="flex flex-col items-center justify-center space-y-8 pt-12 border-t border-gray-100">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-4xl font-bold">Vill du delta?</h2>
|
||||
<p className="text-gray-600 text-lg">Begränsade platser, Säkra din plats idag!</p>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Begränsade platser, Säkra din plats idag!
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/register"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
|
@ -11,6 +12,22 @@ interface EventContent {
|
|||
volunteerAreas: string;
|
||||
}
|
||||
|
||||
const normalizeSsn = (value: string) =>
|
||||
value.replace(/[\s\-+]/g, "").replace(/\D/g, "");
|
||||
const hasValidSsnLength = (value: string) =>
|
||||
value.length === 10 || value.length === 12;
|
||||
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 RegisterPage() {
|
||||
const router = useRouter();
|
||||
const [content, setContent] = useState<EventContent | null>(null);
|
||||
|
|
@ -32,6 +49,7 @@ export default function RegisterPage() {
|
|||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [message, setMessage] = useState({ type: "", text: "" });
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/Content")
|
||||
|
|
@ -49,53 +67,142 @@ export default function RegisterPage() {
|
|||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
||||
<div className="max-w-md w-full bg-white p-8 rounded-xl shadow-md text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Registreringen är stängd</h1>
|
||||
<p className="text-gray-600 mb-6">Vi tar tyvärr inte emot fler anmälningar just nu.</p>
|
||||
<Link href="/" className="text-blue-600 hover:underline">Gå tillbaka till startsidan</Link>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Registreringen är stängd
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Vi tar tyvärr inte emot fler anmälningar just nu.
|
||||
</p>
|
||||
<Link href="/" className="text-blue-600 hover:underline">
|
||||
Gå tillbaka till startsidan
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
>,
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
const val = type === "checkbox" ? (e.target as HTMLInputElement).checked : value;
|
||||
const val =
|
||||
type === "checkbox" ? (e.target as HTMLInputElement).checked : value;
|
||||
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: val,
|
||||
}));
|
||||
|
||||
setFieldErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: "",
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.ssn) {
|
||||
setMessage({ type: "error", text: "SSN is required for registration." });
|
||||
const normalizedSsn = normalizeSsn(formData.ssn);
|
||||
const trimmedFirstName = formData.firstName.trim();
|
||||
const trimmedSurName = formData.surName.trim();
|
||||
const trimmedGrade = formData.grade.trim();
|
||||
const trimmedPhoneNumber = formData.phoneNumber.trim();
|
||||
const trimmedGuardianName = formData.guardianName.trim();
|
||||
const trimmedGuardianPhone = formData.guardianPhoneNumber.trim();
|
||||
const trimmedGuardianEmail = formData.guardianEmail.trim();
|
||||
const normalizedPhoneNumber = normalizeMobileNumber(trimmedPhoneNumber);
|
||||
const normalizedGuardianPhone = normalizeMobileNumber(trimmedGuardianPhone);
|
||||
const nextFieldErrors: Record<string, string> = {};
|
||||
|
||||
if (!normalizedSsn) {
|
||||
nextFieldErrors.ssn = "Personnummer krävs för registrering.";
|
||||
} else if (!hasValidSsnLength(normalizedSsn)) {
|
||||
nextFieldErrors.ssn = "Personnummer måste vara 10 eller 12 siffror.";
|
||||
}
|
||||
|
||||
if (!trimmedFirstName)
|
||||
nextFieldErrors.firstName = "Förnamn är obligatoriskt.";
|
||||
if (!trimmedSurName)
|
||||
nextFieldErrors.surName = "Efternamn är obligatoriskt.";
|
||||
if (!trimmedGrade) nextFieldErrors.grade = "Årskurs är obligatorisk.";
|
||||
if (!trimmedGuardianName)
|
||||
nextFieldErrors.guardianName = "Vårdnadshavares namn är obligatoriskt.";
|
||||
if (!trimmedGuardianPhone)
|
||||
nextFieldErrors.guardianPhoneNumber =
|
||||
"Vårdnadshavares Mobilnummer är obligatoriskt.";
|
||||
if (!trimmedGuardianEmail)
|
||||
nextFieldErrors.guardianEmail = "Vårdnadshavares e-post är obligatorisk.";
|
||||
|
||||
if (trimmedPhoneNumber && !isValidMobileNumber(trimmedPhoneNumber)) {
|
||||
nextFieldErrors.phoneNumber =
|
||||
"Mobilnummer måste vara ett giltigt svenskt mobilnummer.";
|
||||
}
|
||||
|
||||
if (trimmedGuardianPhone && !isValidMobileNumber(trimmedGuardianPhone)) {
|
||||
nextFieldErrors.guardianPhoneNumber =
|
||||
"Vårdnadshavares Mobilnummer måste vara ett giltigt svenskt mobilnummer.";
|
||||
}
|
||||
|
||||
if (trimmedGuardianEmail && !isValidEmail(trimmedGuardianEmail)) {
|
||||
nextFieldErrors.guardianEmail =
|
||||
"Vårdnadshavarens e-postadress är ogiltig.";
|
||||
}
|
||||
|
||||
if (formData.email.trim() && !isValidEmail(formData.email.trim())) {
|
||||
nextFieldErrors.email = "Din e-postadress är ogiltig.";
|
||||
}
|
||||
|
||||
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: "Processing your registration..." });
|
||||
|
||||
try {
|
||||
// Returns Ok (200) if NOT registered, Conflict (409) if registered
|
||||
const checkRes = await fetch(`/api/Registration/registered/${formData.ssn}`);
|
||||
const checkRes = await fetch(
|
||||
`/api/Registration/registered/${normalizedSsn}`,
|
||||
);
|
||||
|
||||
if (checkRes.status === 409) {
|
||||
setMessage({ type: "error", text: "This SSN is already registered for the LAN." });
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "This SSN is already registered for the LAN.",
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Returns 200 (Ok) if member, 404 (NotFound) if not
|
||||
const validateRes = await fetch(`/api/Registration/register/${formData.ssn}`);
|
||||
const validateRes = await fetch(
|
||||
`/api/Registration/register/${normalizedSsn}`,
|
||||
);
|
||||
const isMember = validateRes.ok;
|
||||
|
||||
const { ssn, ...participantData } = formData;
|
||||
const finalPayload = {
|
||||
...participantData,
|
||||
isMember: isMember
|
||||
firstName: trimmedFirstName,
|
||||
surName: trimmedSurName,
|
||||
grade: trimmedGrade,
|
||||
phoneNumber: normalizedPhoneNumber,
|
||||
email: formData.email.trim(),
|
||||
guardianName: trimmedGuardianName,
|
||||
guardianPhoneNumber: normalizedGuardianPhone,
|
||||
guardianEmail: trimmedGuardianEmail,
|
||||
friends: formData.friends.trim(),
|
||||
specialDiet: formData.specialDiet.trim(),
|
||||
isMember: isMember,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/Participant/register", {
|
||||
|
|
@ -107,18 +214,27 @@ export default function RegisterPage() {
|
|||
});
|
||||
|
||||
if (response.ok) {
|
||||
setMessage({ type: "success", text: "Registration complete! You are now registered for the LAN." });
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "Registration complete! You are now registered for the LAN.",
|
||||
});
|
||||
|
||||
await fetch(`/api/Registration/register/${formData.ssn}`, {
|
||||
await fetch(`/api/Registration/register/${normalizedSsn}`, {
|
||||
method: "POST",
|
||||
});
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setMessage({ type: "error", text: errorData.message || "Event registration failed." });
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: errorData.message || "Event registration failed.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
setMessage({ type: "error", text: "A connection error occurred. Please check your internet and try again." });
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "A connection error occurred. Please check your internet and try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
|
@ -128,145 +244,249 @@ export default function RegisterPage() {
|
|||
<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-6">
|
||||
<Link href="/" className="text-blue-600 hover:text-blue-800 flex items-center gap-1 text-sm font-medium">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-blue-600 hover:text-blue-800 flex items-center gap-1 text-sm font-medium"
|
||||
>
|
||||
← Back to Information
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-center mb-10">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Lan Registrering</h1>
|
||||
<p className="mt-2 text-gray-600">Vänligen fyll i detta formulär för att anmäla dig till lanet.</p>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Vänligen fyll i detta formulär för att anmäla dig till 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 className="sm:col-span-2">
|
||||
<label htmlFor="ssn" className="block text-sm font-medium text-gray-700">Personnummer*</label>
|
||||
<label
|
||||
htmlFor="ssn"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Personnummer*
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ssn"
|
||||
id="ssn"
|
||||
placeholder="YYYYMMDD-XXXX"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.ssn ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
|
||||
value={formData.ssn}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">Används endast för att kolla om du redan är medlem eller inte. Sparas ej.</p>
|
||||
{fieldErrors.ssn && (
|
||||
<p className="mt-1 text-xs text-red-600">{fieldErrors.ssn}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Används endast för att kolla om du redan är medlem eller inte.
|
||||
Sparas ej.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700">Förnamn *</label>
|
||||
<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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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>
|
||||
<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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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="grade" className="block text-sm font-medium text-gray-700">Årskurs *</label>
|
||||
<label
|
||||
htmlFor="grade"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Årskurs *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="grade"
|
||||
id="grade"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.grade ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
|
||||
value={formData.grade}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{fieldErrors.grade && (
|
||||
<p className="mt-1 text-xs text-red-600">{fieldErrors.grade}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phoneNumber" className="block text-sm font-medium text-gray-700">Mobilnummer</label>
|
||||
<label
|
||||
htmlFor="phoneNumber"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Mobilnummer
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
id="phoneNumber"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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 className="sm:col-span-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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 pt-4 border-t border-gray-100 mt-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Vårdnadshavares Information</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Vårdnadshavares Information
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="guardianName" className="block text-sm font-medium text-gray-700">Vårdnadshavares Namn *</label>
|
||||
<label
|
||||
htmlFor="guardianName"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Vårdnadshavares Namn *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="guardianName"
|
||||
id="guardianName"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.guardianName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
|
||||
value={formData.guardianName}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{fieldErrors.guardianName && (
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
{fieldErrors.guardianName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="guardianPhoneNumber" className="block text-sm font-medium text-gray-700">Vårdnadshavares Mobilnummer *</label>
|
||||
<label
|
||||
htmlFor="guardianPhoneNumber"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Vårdnadshavares Mobilnummer *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="guardianPhoneNumber"
|
||||
id="guardianPhoneNumber"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
inputMode="tel"
|
||||
placeholder="07XXXXXXXX"
|
||||
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.guardianPhoneNumber ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
|
||||
value={formData.guardianPhoneNumber}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{fieldErrors.guardianPhoneNumber && (
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
{fieldErrors.guardianPhoneNumber}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<label htmlFor="guardianEmail" className="block text-sm font-medium text-gray-700">Vårdnadshavares Email *</label>
|
||||
<label
|
||||
htmlFor="guardianEmail"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Vårdnadshavares Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="guardianEmail"
|
||||
id="guardianEmail"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.guardianEmail ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
|
||||
value={formData.guardianEmail}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{fieldErrors.guardianEmail && (
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
{fieldErrors.guardianEmail}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 pt-4 border-t border-gray-100 mt-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Yttrligare uppgifter</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Yttrligare uppgifter
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<label htmlFor="friends" className="block text-sm font-medium text-gray-700">"Önska" Vänner (att sitta jämte)</label>
|
||||
<label
|
||||
htmlFor="friends"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
"Önska" Vänner (att sitta jämte)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="friends"
|
||||
|
|
@ -278,7 +498,12 @@ export default function RegisterPage() {
|
|||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<label htmlFor="specialDiet" className="block text-sm font-medium text-gray-700">Specialkost / Allergier</label>
|
||||
<label
|
||||
htmlFor="specialDiet"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Specialkost / Allergier
|
||||
</label>
|
||||
<textarea
|
||||
name="specialDiet"
|
||||
id="specialDiet"
|
||||
|
|
@ -303,7 +528,12 @@ export default function RegisterPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="isVisitor" className="font-medium text-gray-700">Registrera dig som besökare</label>
|
||||
<label
|
||||
htmlFor="isVisitor"
|
||||
className="font-medium text-gray-700"
|
||||
>
|
||||
Registrera dig som besökare
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
{content?.visitorOnly
|
||||
? "Just nu tillåts endast besöks-registreringar."
|
||||
|
|
@ -324,18 +554,31 @@ export default function RegisterPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="hasApprovedGdpr" className="font-medium text-gray-700">GDPR gokännande *</label>
|
||||
<p className="text-gray-500">Jag Godkänner att min personliga uppgifter sparas och hanteras i syfte för detta event.</p>
|
||||
<label
|
||||
htmlFor="hasApprovedGdpr"
|
||||
className="font-medium text-gray-700"
|
||||
>
|
||||
GDPR godkännande *
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
Jag Godkänner att min personliga uppgifter sparas och
|
||||
hanteras i syfte för detta event.
|
||||
</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"
|
||||
}`}>
|
||||
<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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
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 [content, setContent] = useState<EventContent | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
|
|
@ -20,6 +33,7 @@ export default function VolunteerPage() {
|
|||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [message, setMessage] = useState({ type: "", text: "" });
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/Content")
|
||||
|
|
@ -34,6 +48,11 @@ export default function VolunteerPage() {
|
|||
...prev,
|
||||
[name]: type === "checkbox" ? checked : value,
|
||||
}));
|
||||
|
||||
setFieldErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: "",
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAreaToggle = (area: string) => {
|
||||
|
|
@ -43,27 +62,69 @@ export default function VolunteerPage() {
|
|||
: [...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) {
|
||||
setMessage({ type: "error", text: "Vänligen välj minst ett område du vill hjälpa till med." });
|
||||
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: formData.firstName,
|
||||
surName: formData.surName,
|
||||
phoneNumber: formData.phoneNumber,
|
||||
email: formData.email,
|
||||
firstName: trimmedFirstName,
|
||||
surName: trimmedSurName,
|
||||
phoneNumber: normalizedPhoneNumber,
|
||||
email: trimmedEmail,
|
||||
hasApprovedGdpr: formData.hasApprovedGdpr,
|
||||
areasOfInterest: formData.selectedAreas.map(name => ({ name }))
|
||||
areasOfInterest: formData.selectedAreas.map((name) => ({
|
||||
name: name.trim(),
|
||||
})),
|
||||
};
|
||||
|
||||
const response = await fetch("/api/Volunteer/register", {
|
||||
|
|
@ -75,7 +136,10 @@ export default function VolunteerPage() {
|
|||
});
|
||||
|
||||
if (response.ok) {
|
||||
setMessage({ type: "success", text: "Tack för din ansökan! Vi kommer kontakta dig snart." });
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "Tack för din ansökan! Vi kommer kontakta dig snart.",
|
||||
});
|
||||
setFormData({
|
||||
firstName: "",
|
||||
surName: "",
|
||||
|
|
@ -86,90 +150,160 @@ export default function VolunteerPage() {
|
|||
});
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setMessage({ type: "error", text: errorData.message || "Ett fel uppstod vid registreringen." });
|
||||
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." });
|
||||
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()) || [];
|
||||
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="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="mb-6">
|
||||
<Link href="/" className="text-blue-600 hover:text-blue-800 flex items-center gap-1 text-sm font-medium">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-blue-600 hover:text-blue-800 flex items-center gap-1 text-sm font-medium"
|
||||
>
|
||||
← Tillbaka till startsidan
|
||||
</Link>
|
||||
</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>
|
||||
<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>
|
||||
<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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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>
|
||||
<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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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>
|
||||
<label
|
||||
htmlFor="phoneNumber"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Mobilnummer *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
id="phoneNumber"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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>
|
||||
<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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||
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">
|
||||
<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">
|
||||
<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"
|
||||
|
|
@ -180,6 +314,11 @@ export default function VolunteerPage() {
|
|||
</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">
|
||||
|
|
@ -189,25 +328,37 @@ export default function VolunteerPage() {
|
|||
id="hasApprovedGdpr"
|
||||
name="hasApprovedGdpr"
|
||||
type="checkbox"
|
||||
required
|
||||
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>
|
||||
<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"
|
||||
}`}>
|
||||
<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>
|
||||
)}
|
||||
|
|
|
|||
BIN
src/Web/lan-frontend/public/vBytes_Logotyp.png
Normal file
BIN
src/Web/lan-frontend/public/vBytes_Logotyp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
Loading…
Reference in a new issue