adding volunteer page

This commit is contained in:
Sebastian 2026-02-20 00:05:26 +01:00
parent d15058f0c9
commit 1eb24d812e
8 changed files with 458 additions and 70 deletions

View file

@ -2,12 +2,12 @@ using Microsoft.AspNetCore.Mvc;
using Registration.API.Services;
using Registration.Domain.Models;
namespace Registration.API.Controllers
namespace Registration.API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class VolunteerController(IVbytesVolunteerRelayService relayService) : ControllerBase
{
[Route("api/[controller]")]
[ApiController]
public class VolunteerController(IVbytesVolunteerRelayService relayService) : ControllerBase
{
private readonly IVbytesVolunteerRelayService _relayService = relayService;
[HttpPost("register")]
@ -15,12 +15,11 @@ namespace Registration.API.Controllers
{
var result = await _relayService.RegisterVolunteerAsync(volunteer, cancellationToken);
if (result.Success)
if (result.StatusCode == 200 && !result.Message.Contains("401"))
{
return Ok();
}
return StatusCode(result.StatusCode, new { message = result.Message });
}
return Unauthorized();
}
}

View file

@ -10,4 +10,8 @@ public class EventContent
public string LocationAddress { get; set; } = string.Empty;
public string WhatToBring { get; set; } = string.Empty;
public string RulesAndGdpr { get; set; } = string.Empty;
public string AdditionalInfo { get; set; } = string.Empty;
public bool RegistrationEnabled { get; set; } = true;
public bool VisitorOnly { get; set; } = false;
public string VolunteerAreas { get; set; } = string.Empty;
}

View file

@ -46,7 +46,7 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository
CREATE INDEX IF NOT EXISTS idx_members_ssn_index ON members(ssn_index);
CREATE TABLE IF NOT EXISTS event_content (
id INT PRIMARY KEY,
id SERIAL PRIMARY KEY,
title TEXT,
sub_title TEXT,
event_date TEXT,
@ -54,11 +54,21 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository
location_name TEXT,
location_address TEXT,
what_to_bring TEXT,
rules_and_gdpr TEXT
rules_and_gdpr TEXT,
additional_info TEXT,
registration_enabled BOOLEAN DEFAULT TRUE,
visitor_only BOOLEAN DEFAULT FALSE,
volunteer_areas TEXT DEFAULT ''
);
ALTER TABLE event_content ADD COLUMN IF NOT EXISTS additional_info TEXT;
ALTER TABLE event_content ADD COLUMN IF NOT EXISTS registration_enabled BOOLEAN DEFAULT TRUE;
ALTER TABLE event_content ADD COLUMN IF NOT EXISTS visitor_only BOOLEAN DEFAULT FALSE;
ALTER TABLE event_content ADD COLUMN IF NOT EXISTS volunteer_areas TEXT DEFAULT '';
INSERT INTO event_content (id, title, sub_title, event_date, event_time, location_name, location_address, what_to_bring, rules_and_gdpr)
SELECT 1, '', '', '', '', '', '', '', ''
UPDATE event_content SET volunteer_areas = 'Kiosk & Kök' || chr(10) || 'Städning' || chr(10) || 'Entré & Incheckning' || chr(10) || 'Teknisk Support' || chr(10) || 'All-round' WHERE id = 1 AND (volunteer_areas IS NULL OR volunteer_areas = '');
INSERT INTO event_content (id, title, sub_title, event_date, event_time, location_name, location_address, what_to_bring, rules_and_gdpr, additional_info, registration_enabled, visitor_only, volunteer_areas)
SELECT 1, '', '', '', '', '', '', '', '', '', TRUE, FALSE, 'Kiosk & Kök' || chr(10) || 'Städning' || chr(10) || 'Entré & Incheckning' || chr(10) || 'Teknisk Support' || chr(10) || 'All-round'
WHERE NOT EXISTS (SELECT 1 FROM event_content WHERE id = 1);
";
@ -98,7 +108,7 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository
public async Task<EventContent> GetEventContent()
{
using var connection = CreateConnection();
var content = await connection.QueryFirstOrDefaultAsync<EventContent>("SELECT title, sub_title as SubTitle, event_date as EventDate, event_time as EventTime, location_name as LocationName, location_address as LocationAddress, what_to_bring as WhatToBring, rules_and_gdpr as RulesAndGdpr FROM event_content WHERE id = 1");
var content = await connection.QueryFirstOrDefaultAsync<EventContent>("SELECT title, sub_title as SubTitle, event_date as EventDate, event_time as EventTime, location_name as LocationName, location_address as LocationAddress, what_to_bring as WhatToBring, rules_and_gdpr as RulesAndGdpr, additional_info as AdditionalInfo, registration_enabled as RegistrationEnabled, visitor_only as VisitorOnly, volunteer_areas as VolunteerAreas FROM event_content WHERE id = 1");
return content ?? new EventContent();
}
@ -114,7 +124,11 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository
location_name = @LocationName,
location_address = @LocationAddress,
what_to_bring = @WhatToBring,
rules_and_gdpr = @RulesAndGdpr
rules_and_gdpr = @RulesAndGdpr,
additional_info = @AdditionalInfo,
registration_enabled = @RegistrationEnabled,
visitor_only = @VisitorOnly,
volunteer_areas = @VolunteerAreas
WHERE id = 1";
await connection.ExecuteAsync(sql, content);
}

View file

@ -13,6 +13,10 @@ interface EventContent {
locationAddress: string;
whatToBring: string;
rulesAndGdpr: string;
additionalInfo: string;
registrationEnabled: boolean;
visitorOnly: boolean;
volunteerAreas: string;
}
export default function AdminDashboard() {
@ -97,8 +101,37 @@ export default function AdminDashboard() {
<form onSubmit={handleSave} className="bg-white p-8 rounded-lg shadow-md space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-2 flex flex-col sm:flex-row gap-6 bg-blue-50 p-4 rounded-md">
<div className="flex items-center">
<input
type="checkbox"
id="registrationEnabled"
name="registrationEnabled"
className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded cursor-pointer"
checked={content.registrationEnabled}
onChange={(e) => setContent(prev => prev ? { ...prev, registrationEnabled: e.target.checked } : null)}
/>
<label htmlFor="registrationEnabled" className="ml-3 text-sm font-semibold text-gray-900 cursor-pointer">
Visa Registrerings Knappar
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="visitorOnly"
name="visitorOnly"
className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded cursor-pointer"
checked={content.visitorOnly}
onChange={(e) => setContent(prev => prev ? { ...prev, visitorOnly: e.target.checked } : null)}
/>
<label htmlFor="visitorOnly" className="ml-3 text-sm font-semibold text-gray-900 cursor-pointer">
Endast Besöks-registrering (Ingen Dator)
</label>
</div>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Page Title</label>
<label className="block text-sm font-medium text-gray-700">HuvudSidan Titel</label>
<input
type="text"
name="title"
@ -118,7 +151,7 @@ export default function AdminDashboard() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Event Date</label>
<label className="block text-sm font-medium text-gray-700">LAN Datum</label>
<input
type="text"
name="eventDate"
@ -128,7 +161,7 @@ export default function AdminDashboard() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Event Time</label>
<label className="block text-sm font-medium text-gray-700">LAN Tid</label>
<input
type="text"
name="eventTime"
@ -138,7 +171,7 @@ export default function AdminDashboard() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Location Name</label>
<label className="block text-sm font-medium text-gray-700">Plats namn "Viskafors"</label>
<input
type="text"
name="locationName"
@ -148,7 +181,7 @@ export default function AdminDashboard() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Location Address</label>
<label className="block text-sm font-medium text-gray-700">Adress</label>
<input
type="text"
name="locationAddress"
@ -158,7 +191,17 @@ export default function AdminDashboard() {
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">What to Bring (One item per line)</label>
<label className="block text-sm font-medium text-gray-700">Extra Information (Visas under Info sektionen)</label>
<textarea
name="additionalInfo"
rows={4}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
value={content.additionalInfo}
onChange={handleChange}
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Att ta med till Lanet (En sak per rad)</label>
<textarea
name="whatToBring"
rows={6}
@ -168,7 +211,7 @@ export default function AdminDashboard() {
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Rules & GDPR</label>
<label className="block text-sm font-medium text-gray-700">Regler för LAN & GDPR</label>
<textarea
name="rulesAndGdpr"
rows={4}
@ -177,6 +220,16 @@ export default function AdminDashboard() {
onChange={handleChange}
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Funktionär Områden (En per rad)</label>
<textarea
name="volunteerAreas"
rows={4}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
value={content.volunteerAreas}
onChange={handleChange}
/>
</div>
</div>
{message.text && (

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Script from "next/script";
import "./globals.css";
const geistSans = Geist({
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Vbytes LAN Registration",
description: "Register for the upcoming Vbytes LAN event.",
};
export default function RootLayout({
@ -27,7 +28,36 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Script id="matomo-tracking" strategy="afterInteractive">
{`
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setCookieDomain", "*.vbytes.se"]);
_paq.push(["setDomains", ["*.vbytes.se","*.anmalan.vbytes.se","*.boka.vbytes.se","*.inventory.vbytes.se","*.kiosk.vbytes.se","*.lan.vbytes.se","*.www.vbytes.se","*.anmalan.vbytes.se","*.boka.vbytes.se","*.inventory.vbytes.se","*.kiosk.vbytes.se","*.lan.vbytes.se","*.vbytes.se","*.www.vbytes.se"]]);
_paq.push(["enableCrossDomainLinking"]);
_paq.push(["setDoNotTrack", true]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://analytics.vbytes.se/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '27']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
`}
</Script>
{children}
<noscript>
<p>
<img
referrerPolicy="no-referrer-when-downgrade"
src="https://analytics.vbytes.se/matomo.php?idsite=27&amp;rec=1"
style={{ border: 0 }}
alt=""
/>
</p>
</noscript>
</body>
</html>
);

View file

@ -13,6 +13,10 @@ interface EventContent {
locationAddress: string;
whatToBring: string;
rulesAndGdpr: string;
additionalInfo: string;
registrationEnabled: boolean;
visitorOnly: boolean;
volunteerAreas: string;
}
export default function LandingPage() {
@ -58,63 +62,81 @@ export default function LandingPage() {
{/* 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.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">Event 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">Date & Time</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>
)}
{(content.locationName || content.locationAddress) && (
<div className="space-y-2">
<h3 className="font-semibold text-lg text-blue-600">Location</h3>
<h3 className="font-semibold text-lg text-blue-600">Plats</h3>
{content.locationName && <p>{content.locationName}</p>}
{content.locationAddress && <p>{content.locationAddress}</p>}
</div>
)}
</div>
{content.additionalInfo && (
<div className="mt-8 text-lg text-gray-700 whitespace-pre-wrap leading-relaxed">
{content.additionalInfo}
</div>
)}
</section>
)}
{content.whatToBring && (
<section className="space-y-6">
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">What to Bring</h2>
<ul className="list-disc list-inside space-y-3 text-lg text-gray-700 whitespace-pre-line">
{content.whatToBring}
<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>
))}
</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">Rules & GDPR</h2>
<p className="text-lg text-gray-700 leading-relaxed">
<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>
</section>
)}
{/* Call to Action */}
{content.registrationEnabled && (
<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">Ready to join?</h2>
<p className="text-gray-600 text-lg">Seats are limited, so secure your spot today!</p>
<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>
</div>
<Link
href="/register"
className="bg-blue-600 hover:bg-blue-700 text-white text-xl font-bold py-4 px-12 rounded-full shadow-lg transform transition hover:scale-105 active:scale-95"
>
Register Now
Anmäl dig nu
</Link>
</section>
)}
</main>
{/* Footer */}
<footer className="bg-gray-50 py-12 text-center text-gray-500 border-t border-gray-100 relative">
<div className="mb-8">
<p className="text-gray-600 mb-4">Vill du hjälpa till?</p>
<Link
href="/volunteer"
className="inline-block bg-gray-800 hover:bg-black text-white font-bold py-2 px-6 rounded-md transition"
>
Bli Funktionär
</Link>
</div>
<p>© 2026 Vbytes Gaming. All rights reserved.</p>
</footer>
</div>

View file

@ -1,10 +1,19 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
interface EventContent {
registrationEnabled: boolean;
visitorOnly: boolean;
volunteerAreas: string;
}
export default function RegisterPage() {
const router = useRouter();
const [content, setContent] = useState<EventContent | null>(null);
const [formData, setFormData] = useState({
firstName: "",
surName: "",
@ -24,6 +33,30 @@ export default function RegisterPage() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState({ type: "", text: "" });
useEffect(() => {
fetch("/api/Content")
.then((res) => res.json())
.then((data) => {
setContent(data);
if (data.visitorOnly) {
setFormData((prev) => ({ ...prev, isVisitor: true }));
}
})
.catch((err) => console.error("Failed to fetch content", err));
}, []);
if (content && !content.registrationEnabled) {
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"> tillbaka till startsidan</Link>
</div>
</div>
);
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
const val = type === "checkbox" ? (e.target as HTMLInputElement).checked : value;
@ -108,7 +141,7 @@ export default function RegisterPage() {
<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">Person Nummer*</label>
<label htmlFor="ssn" className="block text-sm font-medium text-gray-700">Personnummer*</label>
<input
type="text"
name="ssn"
@ -119,11 +152,11 @@ export default function RegisterPage() {
value={formData.ssn}
onChange={handleInputChange}
/>
<p className="mt-1 text-xs text-gray-500">Required for membership verification. Not stored in event records.</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">First Name *</label>
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700">Förnamn *</label>
<input
type="text"
name="firstName"
@ -136,7 +169,7 @@ export default function RegisterPage() {
</div>
<div>
<label htmlFor="surName" className="block text-sm font-medium text-gray-700">Surname *</label>
<label htmlFor="surName" className="block text-sm font-medium text-gray-700">Efternamn *</label>
<input
type="text"
name="surName"
@ -149,7 +182,7 @@ export default function RegisterPage() {
</div>
<div>
<label htmlFor="grade" className="block text-sm font-medium text-gray-700">Grade *</label>
<label htmlFor="grade" className="block text-sm font-medium text-gray-700">Årskurs *</label>
<input
type="text"
name="grade"
@ -162,7 +195,7 @@ export default function RegisterPage() {
</div>
<div>
<label htmlFor="phoneNumber" className="block text-sm font-medium text-gray-700">Phone Number</label>
<label htmlFor="phoneNumber" className="block text-sm font-medium text-gray-700">Mobilnummer</label>
<input
type="tel"
name="phoneNumber"
@ -186,11 +219,11 @@ export default function RegisterPage() {
</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">Guardian 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">Guardian Name *</label>
<label htmlFor="guardianName" className="block text-sm font-medium text-gray-700">Vårdnadshavares Namn *</label>
<input
type="text"
name="guardianName"
@ -203,7 +236,7 @@ export default function RegisterPage() {
</div>
<div>
<label htmlFor="guardianPhoneNumber" className="block text-sm font-medium text-gray-700">Guardian Phone *</label>
<label htmlFor="guardianPhoneNumber" className="block text-sm font-medium text-gray-700">Vårdnadshavares Mobilnummer *</label>
<input
type="tel"
name="guardianPhoneNumber"
@ -216,7 +249,7 @@ export default function RegisterPage() {
</div>
<div className="sm:col-span-2">
<label htmlFor="guardianEmail" className="block text-sm font-medium text-gray-700">Guardian Email *</label>
<label htmlFor="guardianEmail" className="block text-sm font-medium text-gray-700">Vårdnadshavares Email *</label>
<input
type="email"
name="guardianEmail"
@ -229,11 +262,11 @@ export default function RegisterPage() {
</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">Additional Details</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">Friends (to sit with)</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"
@ -245,7 +278,7 @@ export default function RegisterPage() {
</div>
<div className="sm:col-span-2">
<label htmlFor="specialDiet" className="block text-sm font-medium text-gray-700">Special Diet / Allergies</label>
<label htmlFor="specialDiet" className="block text-sm font-medium text-gray-700">Specialkost / Allergier</label>
<textarea
name="specialDiet"
id="specialDiet"
@ -263,14 +296,19 @@ export default function RegisterPage() {
id="isVisitor"
name="isVisitor"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded disabled:bg-gray-100 disabled:cursor-not-allowed"
checked={formData.isVisitor}
onChange={handleInputChange}
disabled={content?.visitorOnly}
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="isVisitor" className="font-medium text-gray-700">Register as Visitor</label>
<p className="text-gray-500">Check this if you are visiting and not bringing a computer.</p>
<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."
: "Klicka i denna om du bara ska besöka och inte planerar på att ta med dator eller annan form av konsol."}
</p>
</div>
</div>
@ -286,8 +324,8 @@ export default function RegisterPage() {
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="hasApprovedGdpr" className="font-medium text-gray-700">GDPR Consent *</label>
<p className="text-gray-500">I agree to the processing of my personal data for the purpose of this event.</p>
<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>
</div>
</div>
</div>

View file

@ -0,0 +1,228 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
interface EventContent {
volunteerAreas: string;
}
export default function VolunteerPage() {
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: "" });
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,
}));
};
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 };
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (formData.selectedAreas.length === 0) {
setMessage({ type: "error", text: "Vänligen välj minst ett område du vill hjälpa till med." });
return;
}
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,
hasApprovedGdpr: formData.hasApprovedGdpr,
areasOfInterest: formData.selectedAreas.map(name => ({ name }))
};
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-6">
<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>
</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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
value={formData.firstName}
onChange={handleInputChange}
/>
</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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
value={formData.surName}
onChange={handleInputChange}
/>
</div>
<div>
<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"
value={formData.phoneNumber}
onChange={handleInputChange}
/>
</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 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
value={formData.email}
onChange={handleInputChange}
/>
</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">
{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>
</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"
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>
</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>
);
}