Almost done
This commit is contained in:
+8
-340
@@ -1,344 +1,12 @@
|
||||
// app/beach/page.tsx
|
||||
import { getConfig } from '../lib/config';
|
||||
import BeachClientPage from './BeachClientPage';
|
||||
|
||||
'use client';
|
||||
export const metadata = {
|
||||
title: 'Beachturnering',
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
export default async function BeachPage() {
|
||||
const config = await getConfig();
|
||||
return <BeachClientPage config={config} />;
|
||||
}
|
||||
Reference in New Issue
Block a user