344 lines
23 KiB
TypeScript
344 lines
23 KiB
TypeScript
// app/beach/page.tsx
|
|
|
|
'use client';
|
|
|
|
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
|
import Link from 'next/link';
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import { IoIosMail } from "react-icons/io";
|
|
import { PiWarningCircle } from "react-icons/pi";
|
|
import { PiCheckCircle } from "react-icons/pi";
|
|
|
|
interface TimeInfo {
|
|
dateStr: string;
|
|
matchStr: string;
|
|
deadlineText: string;
|
|
isClosed: boolean;
|
|
}
|
|
|
|
interface AlertState {
|
|
show: boolean;
|
|
type: 'success' | 'danger' | '';
|
|
message: string;
|
|
}
|
|
|
|
interface SubmitResponse {
|
|
success: boolean;
|
|
message: string;
|
|
detail?: string;
|
|
}
|
|
|
|
export default function BeachPage() {
|
|
const [timeInfo, setTimeInfo] = useState<TimeInfo>({
|
|
dateStr: 'Laddar datum...',
|
|
matchStr: 'Laddar tider...',
|
|
deadlineText: 'Anmälningar kan göras senast...',
|
|
isClosed: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const updateTournamentInfo = () => {
|
|
const matchStart = new Date('2026-06-13T10:00:00+02:00');
|
|
const now = new Date();
|
|
|
|
const infoGathering = new Date(matchStart.getTime() - 30 * 60000);
|
|
const closingDate = new Date(matchStart.getTime() - 24 * 60 * 60000);
|
|
|
|
const dateOptions: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' };
|
|
const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
|
|
|
|
const dateStr = matchStart.toLocaleDateString('sv-SE', dateOptions);
|
|
const matchTime = matchStart.toLocaleTimeString('sv-SE', timeOptions);
|
|
const infoTime = infoGathering.toLocaleTimeString('sv-SE', timeOptions);
|
|
const deadlineDate = closingDate.toLocaleDateString('sv-SE', dateOptions);
|
|
const deadlineTime = closingDate.toLocaleTimeString('sv-SE', timeOptions);
|
|
|
|
setTimeInfo({
|
|
dateStr,
|
|
matchStr: `Matchstart kl.${matchTime} (Infosamling kl.${infoTime})`,
|
|
deadlineText: `Anmälan stänger den ${deadlineDate} kl.${deadlineTime} (24 timmar innan start) eller om alla platser har blivit fyllda.`,
|
|
isClosed: now >= closingDate,
|
|
});
|
|
};
|
|
|
|
updateTournamentInfo();
|
|
const interval = setInterval(updateTournamentInfo, 60000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
const [wasValidated, setWasValidated] = useState<boolean>(false);
|
|
const [alert, setAlert] = useState<AlertState>({ show: false, type: '', message: '' });
|
|
|
|
const formRef = useRef<HTMLFormElement>(null);
|
|
const turnstileRef = useRef<TurnstileInstance>(null);
|
|
|
|
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
|
|
e.preventDefault();
|
|
const form = formRef.current;
|
|
if (!form) return;
|
|
|
|
setWasValidated(true);
|
|
|
|
if (!form.checkValidity()) {
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setAlert({ show: false, type: '', message: '' });
|
|
const formData = new FormData(form);
|
|
|
|
try {
|
|
const response = await fetch('/api/submit', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`Network error: ${response.status}`);
|
|
|
|
const result = (await response.json()) as SubmitResponse;
|
|
const message = result.detail ? `${result.message} ${result.detail}` : result.message;
|
|
|
|
setAlert({
|
|
show: true,
|
|
type: result.success ? 'success' : 'danger',
|
|
message: message,
|
|
});
|
|
|
|
if (result.success) {
|
|
form.reset();
|
|
setWasValidated(false);
|
|
if (turnstileRef.current) turnstileRef.current.reset();
|
|
}
|
|
} catch (error) {
|
|
console.error('An error occurred:', error);
|
|
setAlert({
|
|
show: true,
|
|
type: 'danger',
|
|
message: 'Ett tekniskt fel uppstod. Försök igen senare.',
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// SVG Icons for validation (Extracted from your original CSS)
|
|
// const CheckmarkIcon = () => #198754
|
|
|
|
// const ErrorIcon = () => #dc3545
|
|
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col font-sans bg-white text-black">
|
|
|
|
<header className="bg-black flex justify-center py-2.5">
|
|
<div className="w-85 mx-auto text-center">
|
|
<Link href="/">
|
|
<img src="/images/logo.svg" alt="Lerbergets Volleybollsällskap" className="max-h-45 min-h-20 my-5.75 mx-auto block" />
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="grow flex flex-col items-center bg-[#c8e9f2] bg-[url('/images/volleyball-net.webp')] bg-cover bg-center bg-no-repeat w-full pb-20">
|
|
|
|
<section className="flex flex-col items-center text-center w-full px-1.25">
|
|
<h2 className="mt-12.5"><img className="max-w-62.5" src="/images/hk-mosf.webp" alt="Mat- & Sommarfesten" /></h2>
|
|
<h1 className="font-beachday text-[clamp(45px,9vw,80px)] text-[#fdb84b] uppercase leading-none m-[20px_0_10px_0]" style={{ fontFamily: "'Beachday', sans-serif" }}>Beachturnering</h1>
|
|
<p className="font-bold italic text-[25px] mb-5 text-black">Samla ihop ett kompisgäng och anmäl er!</p>
|
|
|
|
{timeInfo.isClosed && (
|
|
<div className="bg-black p-5 rounded-[20px] my-5 mx-1.25 w-full max-w-150">
|
|
<h3 className="text-red-500 text-[clamp(1.5rem,6.5vw,2rem)] font-bold pb-2.5">Anmälan är nu stängd!</h3>
|
|
<p className="text-white text-center pb-2.5">Det är hela 26 lag anmälda och vi kan tyvärr inte ta emot fler anmälningar.</p>
|
|
<p className="text-white text-center">Vi är glada över förväntan och bjuder in alla andra till att komma och kolla men vi har tyvärr inte kapaciteten till att ha med fler lag.</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-[#b1e0e8] p-[0_20px_20px_20px] rounded-[20px] my-7.5 mx-1.25 flex flex-col items-center text-center w-full max-w-200">
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Var:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px]"><a target="_blank" rel="noreferrer" className="font-medium text-[#cc8124] hover:underline" href="https://maps.app.goo.gl/vJcdSDCbvdGYXPNY6">Kvickbadet</a> i Höganäs</p>
|
|
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">När:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px]">{timeInfo.dateStr}</p>
|
|
<p className="text-black text-[18px] m-[4px_10px]">{timeInfo.matchStr}</p>
|
|
<p className="text-black text-[18px] m-[4px_10px]">Samtidigt som <a target="_blank" rel="noreferrer" className="font-medium text-[#cc8124] hover:underline" href="https://www.kullahalvon.com/upptacka--uppleva/kultur--noje/evenemang-pa-kullahalvon/mat---sommarfesten.html">Höganäs Mat- & Sommarfest</a></p>
|
|
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Regler:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px] max-w-150">Matcherna spelas 4v4 med <a target="_blank" rel="noreferrer" className="font-medium text-[#cc8124] hover:underline" href="https://www.volleyboll.se/forbundet/valkommen-till-volleyboll/grenar-och-spelformer/volleyboll">inomhusregler</a>, förutom poängräkningen som följer reglerna för <a target="_blank" rel="noreferrer" className="font-medium text-[#cc8124] hover:underline" href="https://www.volleyboll.se/forbundet/valkommen-till-volleyboll/grenar-och-spelformer/beachvolley">beachvolleyboll</a>.</p>
|
|
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Klasser:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px] max-w-150">
|
|
Turneringen kan komma att delas upp i tre olika klasser beroende på antal anmälda lag och baserat på nivå:<br />
|
|
<strong className="text-green-700">Grön</strong> (Nybörjare),{' '}
|
|
<strong className="text-[#0f65bf]">Blå</strong> (Amatör),{' '}
|
|
<strong className="text-black">Svart</strong> (Proffs)
|
|
</p>
|
|
<p className="text-black text-[18px] m-[4px_10px] max-w-150 italic">Gör en gissning i vilken nivå ni tror att ert lag skulle passa och skriv gärna något i anmälan om ni är osäkra.</p>
|
|
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Kostnad:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px]">Gratis!!!</p>
|
|
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Anmälan:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px] max-w-150">Anmälan görs genom anmälningsformuläret nedan. Du får ett bekräftelsemejl när vi kollat igenom er anmälan.</p>
|
|
<p className="text-black text-[18px] m-[4px_10px] italic">{timeInfo.deadlineText}</p>
|
|
|
|
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Övrig info:</h4>
|
|
<p className="text-black text-[18px] m-[4px_10px]">Turneringen upskattas hålla på till ca 17-18 tiden</p>
|
|
<p className="text-black text-[18px] m-[4px_10px]">Det kommer finnas duschmöjligheter i anslutning till stranden.</p>
|
|
|
|
<h4 className="font-beachday uppercase text-[30px] font-medium p-2.5 mt-5 text-black" style={{ fontFamily: "'Beachday', sans-serif" }}>
|
|
Det kommer att finnas fina priser till alla pallplatser! 🏆
|
|
</h4>
|
|
</div>
|
|
</section>
|
|
|
|
{!timeInfo.isClosed && (
|
|
<section className="w-full flex justify-center px-1.25">
|
|
<div className="bg-black p-5 rounded-[20px] my-7.5 mx-1.25 w-full max-w-180">
|
|
<h2 className="font-beachday text-[clamp(1.875rem,5.2vw,50px)] text-white text-center m-[5px_0] uppercase" style={{ fontFamily: "'Beachday', sans-serif" }}>Anmälningsformulär</h2>
|
|
|
|
{/* Form starts here - uses group/form to track the .was-validated state natively */}
|
|
<form
|
|
ref={formRef}
|
|
onSubmit={handleSubmit}
|
|
className={`group/form flex flex-col items-center w-full mt-5 ${wasValidated ? 'was-validated' : ''}`}
|
|
noValidate
|
|
autoComplete="off"
|
|
>
|
|
|
|
{/* NAME */}
|
|
<div className="relative w-full mb-3.75">
|
|
<input type="text" name="name" placeholder="Ditt namn" required autoComplete="name"
|
|
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none appearance-none shadow-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] group-[.was-validated]/form:pr-10 transition-colors" />
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block absolute right-2.5 top-2.25"><PiWarningCircle color='#dc3545' size={28} /></div>
|
|
<div className="hidden group-[.was-validated]/form:peer-valid:block absolute right-2.5 top-2.25"><PiCheckCircle color='#198754' size={28} /></div>
|
|
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">Glöm inte ditt namn!</div>
|
|
</div>
|
|
|
|
{/* EMAIL */}
|
|
<div className="relative w-full mb-3.75">
|
|
<input type="email" name="email" placeholder="Email" required autoComplete="email"
|
|
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none appearance-none shadow-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] group-[.was-validated]/form:pr-10 transition-colors" />
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block absolute right-2.5 top-2.25"><PiWarningCircle color='#dc3545' size={28} /></div>
|
|
<div className="hidden group-[.was-validated]/form:peer-valid:block absolute right-2.5 top-2.25"><PiCheckCircle color='#198754' size={28} /></div>
|
|
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">Email addressen är inte giltig.</div>
|
|
</div>
|
|
|
|
{/* PHONE */}
|
|
<div className="relative w-full mb-3.75">
|
|
<input type="tel" name="tel" placeholder="Telefonnummer" required autoComplete="tel"
|
|
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none appearance-none shadow-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] group-[.was-validated]/form:pr-10 transition-colors" />
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block absolute right-2.5 top-2.25"><PiWarningCircle color='#dc3545' size={28} /></div>
|
|
<div className="hidden group-[.was-validated]/form:peer-valid:block absolute right-2.5 top-2.25"><PiCheckCircle color='#198754' size={28} /></div>
|
|
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">Glöm inte ett telefonnummer!</div>
|
|
</div>
|
|
|
|
{/* TEAM */}
|
|
<div className="relative w-full mb-3.75">
|
|
<input type="text" name="team" placeholder="Lagnamn" required autoComplete="organization"
|
|
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none appearance-none shadow-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] group-[.was-validated]/form:pr-10 transition-colors" />
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block absolute right-2.5 top-2.25"><PiWarningCircle color='#dc3545' size={28} /></div>
|
|
<div className="hidden group-[.was-validated]/form:peer-valid:block absolute right-2.5 top-2.25"><PiCheckCircle color='#198754' size={28} /></div>
|
|
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">Glöm inte ert Lagnamn!</div>
|
|
</div>
|
|
|
|
{/* COUNT */}
|
|
<div className="relative w-full mb-3.75">
|
|
<input type="number" name="count" placeholder="Antal spelare" min="2" required autoComplete="off"
|
|
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none appearance-none shadow-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] group-[.was-validated]/form:pr-10 transition-colors" />
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block absolute right-2.5 top-2.25"><PiWarningCircle color='#dc3545' size={28} /></div>
|
|
<div className="hidden group-[.was-validated]/form:peer-valid:block absolute right-2.5 top-2.25"><PiCheckCircle color='#198754' size={28} /></div>
|
|
|
|
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">Glöm inte ange antal lagmedlemmar!</div>
|
|
</div>
|
|
|
|
{/* RADIOS */}
|
|
<div className="w-full mt-2.5 text-left group/radios">
|
|
<input type="radio" id="level-gron" name="level" value="Grön" required className="inline-block align-middle m-[6px_5px] w-4 h-4 accent-green-600 cursor-pointer" />
|
|
<label htmlFor="level-gron" className="text-white inline-block align-middle cursor-pointer select-none">
|
|
<strong className="text-green-700 bg-white px-0.75 rounded-sm font-bold">Grön</strong> (Nybörjare)
|
|
</label>
|
|
<br />
|
|
|
|
<input type="radio" id="level-bla" name="level" value="Blå" required className="inline-block align-middle m-[6px_5px] w-4 h-4 accent-[#0f65bf] cursor-pointer" />
|
|
<label htmlFor="level-bla" className="text-white inline-block align-middle cursor-pointer select-none">
|
|
<strong className="text-[#0f65bf] bg-white px-0.75 rounded-sm font-bold">Blå</strong> (Spelat lite)
|
|
</label>
|
|
<br />
|
|
|
|
<input type="radio" id="level-svart" name="level" value="Svart" required className="inline-block align-middle m-[6px_5px] w-4 h-4 accent-black cursor-pointer" />
|
|
<label htmlFor="level-svart" className="text-white inline-block align-middle cursor-pointer select-none">
|
|
<strong className="text-black bg-white px-0.75 rounded-sm font-bold">Svart</strong> (Spelat mycket)
|
|
</label>
|
|
<div className="hidden group-[.was-validated]/form:group-has-invalid/radios:block text-[#e60b20] text-[14px] pl-2.5 mt-2">Glöm inte ange spelarnivå!</div>
|
|
</div>
|
|
|
|
{/* TEXTAREA */}
|
|
<div className="w-full mt-2.5">
|
|
<textarea name="message" placeholder="Övrig info..." autoComplete="off"
|
|
className="w-full min-h-37.5 p-[12px_16px] border-[5px] border-white rounded-[10px] bg-transparent text-white text-[15px] resize-y outline-none appearance-none mt-5"></textarea>
|
|
</div>
|
|
|
|
{/* HONEYPOT */}
|
|
<div className="hidden">
|
|
<label htmlFor="website">Website</label>
|
|
<input type="text" name="website" tabIndex={-1} autoComplete="off" />
|
|
</div>
|
|
|
|
{/* ALERT */}
|
|
{alert.show && (
|
|
<div className={`w-full p-4 rounded-md text-center font-bold my-4 ${alert.type === 'success' ? 'bg-[#d1e7dd] text-[#0a3622] border border-[#a3cfbb]' : 'bg-[#f8d7da] text-[#58151c] border border-[#f1aeb5]'}`}>
|
|
{alert.message}
|
|
</div>
|
|
)}
|
|
|
|
{/* TURNSTILE */}
|
|
<div className="w-full flex justify-center mt-2.5">
|
|
<Turnstile
|
|
ref={turnstileRef}
|
|
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ''}
|
|
options={{ theme: 'dark' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* SUBMIT BUTTON */}
|
|
<button type="submit" className="bg-white text-black uppercase border-none w-full max-w-75 h-12.5 outline-none text-[20px] font-bold text-center rounded-[10px] my-5 flex justify-center items-center cursor-pointer hover:bg-gray-200 transition disabled:opacity-70" disabled={isSubmitting}>
|
|
{isSubmitting ? (
|
|
<svg className="animate-spin h-5 w-5 text-black mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
) : null}
|
|
<span>Skicka anmälan</span>
|
|
</button>
|
|
|
|
</form>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Contact Section */}
|
|
<section className="w-full flex justify-center px-1.25">
|
|
<div className="bg-[#fdb84b] flex flex-col justify-center items-center py-2.5 rounded-[20px] m-[30px_5px_60px_5px] w-full max-w-100">
|
|
<h3 className="text-black text-[clamp(1.5rem,6.5vw,2rem)] text-center font-bold m-[5px_0]">Kontakta oss</h3>
|
|
<a href="mailto:beach@lerbergetsvolleyboll.se?subject=Beach%20turnering" className="flex justify-center items-center my-1.25 text-[clamp(1rem,5vw,1.1rem)] font-bold leading-[1.6] text-black hover:underline">
|
|
<IoIosMail className='h-[clamp(1.5rem,5vw,2rem)] mr-2' />
|
|
beach@lerbergetsvolleyboll.se
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="bg-black text-center py-3.75">
|
|
<p className="text-white font-bold">LVS - Lerbergets Volleybollsällskap {new Date().getFullYear()} ®</p>
|
|
</footer>
|
|
|
|
</div>
|
|
);
|
|
} |