Better UI
This commit is contained in:
+115
-1
@@ -3,12 +3,50 @@
|
||||
'use client';
|
||||
|
||||
import { useActionState, useEffect, useState } from 'react';
|
||||
import { FaPlus, FaTrash, FaArrowUp, FaArrowDown } from 'react-icons/fa'; // Importera pilarna
|
||||
import { saveConfigAction } from './actions';
|
||||
|
||||
export default function AdminForm({ initialConfig }: { initialConfig: any }) {
|
||||
const [state, formAction, isPending] = useActionState(saveConfigAction, null);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const [classes, setClasses] = useState<Array<{ name: string, color: string, desc: string }>>(
|
||||
Array.isArray(initialConfig.beachPage.classes)
|
||||
? initialConfig.beachPage.classes
|
||||
: [
|
||||
{ name: 'Grön', color: '#16a34a', desc: 'Nybörjare' },
|
||||
{ name: 'Blå', color: '#2563eb', desc: 'Spelat lite' },
|
||||
{ name: 'Svart', color: '#111827', desc: 'Spelat mycket' }
|
||||
]
|
||||
);
|
||||
|
||||
const addClass = () => setClasses([...classes, { name: '', color: '#406185', desc: '' }]);
|
||||
|
||||
const removeClass = (index: number) => {
|
||||
setClasses(classes.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateClass = (index: number, field: keyof typeof classes[0], value: string) => {
|
||||
const newClasses = [...classes];
|
||||
newClasses[index][field] = value;
|
||||
setClasses(newClasses);
|
||||
};
|
||||
|
||||
// Funktioner för att byta plats på klasserna
|
||||
const moveUp = (index: number) => {
|
||||
if (index === 0) return;
|
||||
const newClasses = [...classes];
|
||||
[newClasses[index - 1], newClasses[index]] = [newClasses[index], newClasses[index - 1]];
|
||||
setClasses(newClasses);
|
||||
};
|
||||
|
||||
const moveDown = (index: number) => {
|
||||
if (index === classes.length - 1) return;
|
||||
const newClasses = [...classes];
|
||||
[newClasses[index + 1], newClasses[index]] = [newClasses[index], newClasses[index + 1]];
|
||||
setClasses(newClasses);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success) {
|
||||
setShowToast(true);
|
||||
@@ -70,7 +108,83 @@ export default function AdminForm({ initialConfig }: { initialConfig: any }) {
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<button type="submit" disabled={isPending} className="bg-[#406185] text-white py-4 rounded-lg font-bold text-lg hover:bg-black transition-all active:scale-95 disabled:bg-gray-400">
|
||||
{/* SPELKLASSER - Moderniserad för att matcha */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-xl font-bold border-l-4 border-[#fdb84b] pl-2 text-[#406185]">Spelklasser</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addClass}
|
||||
className="flex items-center gap-2 text-sm font-bold text-white bg-[#406185] px-4 py-2 rounded hover:brightness-110 transition-all active:scale-95"
|
||||
>
|
||||
<FaPlus size={12} /> Lägg till
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{classes.map((c, index) => (
|
||||
<div key={index} className="flex flex-col md:flex-row gap-3 items-center w-full">
|
||||
|
||||
{/* Sorteringspilar */}
|
||||
<div className="flex md:flex-col gap-1 text-gray-400">
|
||||
<button type="button" onClick={() => moveUp(index)} disabled={index === 0} className="hover:text-[#406185] disabled:opacity-30 disabled:hover:text-gray-400 p-1">
|
||||
<FaArrowUp size={14} />
|
||||
</button>
|
||||
<button type="button" onClick={() => moveDown(index)} disabled={index === classes.length - 1} className="hover:text-[#406185] disabled:opacity-30 disabled:hover:text-gray-400 p-1">
|
||||
<FaArrowDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex gap-2 w-full">
|
||||
{/* Färgplockare som ser ut som ett vanligt inputfält */}
|
||||
<div className="w-12 h-10 border rounded overflow-hidden shrink-0 focus-within:ring-2 focus-within:ring-[#406185]">
|
||||
<input
|
||||
type="color"
|
||||
value={c.color}
|
||||
onChange={(e) => updateClass(index, 'color', e.target.value)}
|
||||
className="w-16 h-16 -m-2 cursor-pointer outline-none"
|
||||
title="Välj färg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={c.name}
|
||||
onChange={(e) => updateClass(index, 'name', e.target.value)}
|
||||
placeholder="Lagnamn (t.ex. Grön)"
|
||||
required
|
||||
className="w-1/3 border p-2 rounded focus:ring-2 focus:ring-[#406185] outline-none min-w-25"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={c.desc}
|
||||
onChange={(e) => updateClass(index, 'desc', e.target.value)}
|
||||
placeholder="Beskrivning (t.ex. Nybörjare)"
|
||||
className="flex-1 border p-2 rounded focus:ring-2 focus:ring-[#406185] outline-none min-w-30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeClass(index)}
|
||||
className="text-gray-400 hover:text-red-500 p-2 rounded transition-colors self-end md:self-auto shrink-0"
|
||||
title="Ta bort klass"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{classes.length === 0 && (
|
||||
<p className="text-gray-500 italic text-sm">Inga klasser inlagda. Besökarna kommer inte kunna välja nivå.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dold JSON-sträng för Server Action */}
|
||||
<input type="hidden" name="classesJSON" value={JSON.stringify(classes)} />
|
||||
</section>
|
||||
|
||||
<button type="submit" disabled={isPending} className="bg-[#406185] text-white py-4 mt-4 rounded-lg font-bold text-lg hover:bg-black transition-all active:scale-95 disabled:bg-gray-400 shadow-lg">
|
||||
{isPending ? 'Sparar...' : 'Spara inställningar'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -8,6 +8,14 @@ import { revalidatePath } from 'next/cache';
|
||||
export async function saveConfigAction(prevState: any, formData: FormData) {
|
||||
const config = await getConfig();
|
||||
|
||||
let parsedClasses = [];
|
||||
try {
|
||||
const classesJSON = formData.get('classesJSON') as string;
|
||||
if (classesJSON) parsedClasses = JSON.parse(classesJSON);
|
||||
} catch (e) {
|
||||
console.error("Kunde inte parsa klasser", e);
|
||||
}
|
||||
|
||||
const newConfig = {
|
||||
...config,
|
||||
showBeachPromo: formData.get('showBeachPromo') === 'on',
|
||||
@@ -19,6 +27,7 @@ export async function saveConfigAction(prevState: any, formData: FormData) {
|
||||
otherInfo: formData.get('otherInfo') as string,
|
||||
prizesText: formData.get('prizesText') as string,
|
||||
isClosedOverride: formData.get('isClosedOverride') === 'on',
|
||||
classes: parsedClasses,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
// app/api/submit/route.js
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
|
||||
// 1. Extract form data (Matching your PHP POST variables)
|
||||
const turnstileToken = formData.get('cf-turnstile-response');
|
||||
const email = formData.get('email');
|
||||
const name = formData.get('name');
|
||||
const team = formData.get('team');
|
||||
const tel = formData.get('tel');
|
||||
const count = formData.get('count');
|
||||
const level = formData.get('level');
|
||||
const message = formData.get('message');
|
||||
const honeypot = formData.get('website');
|
||||
|
||||
// 2. Security Checks (Honeypot & Turnstile)
|
||||
if (honeypot) {
|
||||
return Response.json({ success: false, message: 'Spam detekterat.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify Turnstile
|
||||
const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
secret: process.env.TURNSTILE_SECRET || '', // Pulls from .env.local
|
||||
response: turnstileToken?.toString() || '',
|
||||
}),
|
||||
});
|
||||
|
||||
const turnstileData = await turnstileRes.json();
|
||||
|
||||
if (!turnstileData.success) {
|
||||
return Response.json({ success: false, message: 'Säkerhetsverifiering misslyckades (Turnstile).' }, { status: 400 });
|
||||
}
|
||||
|
||||
let formattedTel = tel.replace(/\D/g, '');
|
||||
if (formattedTel.startsWith('46')) formattedTel = formattedTel.substring(2);
|
||||
else if (formattedTel.startsWith('0')) formattedTel = formattedTel.substring(1);
|
||||
|
||||
if (formattedTel.length === 9) {
|
||||
formattedTel = `+46 (0)${formattedTel.substring(0, 2)}-${formattedTel.substring(2, 5)} ${formattedTel.substring(5, 7)} ${formattedTel.substring(7, 9)}`;
|
||||
} else {
|
||||
formattedTel = tel;
|
||||
}
|
||||
|
||||
// 4. Send Email via Nodemailer (Replicating PHPMailer)
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: '10.10.0.113', // Your internal SMTP Host
|
||||
port: 25,
|
||||
secure: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
const cleanMessage = message ? message.replace(/[\r\n;]/g, " ") : "";
|
||||
const dateStr = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
|
||||
const emailHtml = `
|
||||
<html>
|
||||
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>
|
||||
<h2 style='color: #cc8124;'>Beach-anmälan</h2>
|
||||
<table style='width: 100%; max-width: 600px; border-collapse: collapse;'>
|
||||
<tr><td style='padding: 5px; font-weight: bold; width: 150px;'>Lagnamn:</td><td>${team}</td></tr>
|
||||
<tr><td style='padding: 5px; font-weight: bold;'>Kontakt:</td><td>${name}</td></tr>
|
||||
<tr><td style='padding: 5px; font-weight: bold;'>E-post:</td><td>${email}</td></tr>
|
||||
<tr><td style='padding: 5px; font-weight: bold;'>Telefon:</td><td>${formattedTel}</td></tr>
|
||||
<tr><td style='padding: 5px; font-weight: bold;'>Spelare:</td><td>${count}</td></tr>
|
||||
<tr><td style='padding: 5px; font-weight: bold;'>Nivå:</td><td>${level}</td></tr>
|
||||
</table>
|
||||
<p><strong>Meddelande:</strong><br>${message ? message.replace(/\n/g, '<br>') : ''}</p>
|
||||
<hr style='border: none; border-top: 1px solid #ddd; margin: 30px 0;'>
|
||||
<table border='1' style='border-collapse: collapse; font-family: Calibri, sans-serif; font-size: 11pt; width: 100%;'>
|
||||
<tr>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${team}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${count}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${level}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${name}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${formattedTel}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${email}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${dateStr}</td>
|
||||
<td style='padding: 8px; border: 1px solid #ccc;'>${cleanMessage}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// 1. Send the data to the Club
|
||||
await transporter.sendMail({
|
||||
from: `"Anmälan: ${name}" <no-reply@lerbergetsvolleyboll.se>`,
|
||||
to: 'beach@lerbergetsvolleyboll.se',
|
||||
replyTo: email?.toString(),
|
||||
subject: `Beach turnering: ${team}`,
|
||||
html: emailHtml,
|
||||
});
|
||||
|
||||
// 2. Send the friendly auto-reply to the Applicant
|
||||
await transporter.sendMail({
|
||||
from: `"Lerbergets Volleybollsällskap" <no-reply@lerbergetsvolleyboll.se>`,
|
||||
to: email?.toString(),
|
||||
subject: `Bekräftelse på anmälan: ${team}`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; color: #333; padding: 20px;">
|
||||
<h2 style="color: #406185;">Hej ${name}!</h2>
|
||||
<p style="font-size: 16px;">Tack för din anmälan vi återkommer så fort vi kan.</p>
|
||||
<br>
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
Med vänliga hälsningar,<br>
|
||||
Lerbergets Volleybollsällskap
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return Response.json({ success: true, message: 'Tack så mycket för anmälan! Vi svarar med en bekräftelse så fort vi kan.' });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Response.json({ success: false, message: 'Ett tekniskt fel uppstod. Försök igen senare.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+170
-50
@@ -3,6 +3,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaClipboardList, FaClock, FaInfoCircle, FaLock, FaMapMarkerAlt, FaTicketAlt, FaTrophy, FaUsers } from "react-icons/fa";
|
||||
import { IoIosMail } from "react-icons/io";
|
||||
import Footer from '../components/Footer';
|
||||
import Header from '../components/Header';
|
||||
@@ -16,6 +17,14 @@ export default function BeachClientPage({ config }: { config: any }) {
|
||||
isClosed: false,
|
||||
});
|
||||
|
||||
const classesData = Array.isArray(config.beachPage?.classes)
|
||||
? config.beachPage.classes
|
||||
: [
|
||||
{ name: 'Grön', color: '#16a34a', desc: 'Nybörjare' },
|
||||
{ name: 'Blå', color: '#2563eb', desc: 'Spelat lite' },
|
||||
{ name: 'Svart', color: '#111827', desc: 'Spelat mycket' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const updateTournamentInfo = () => {
|
||||
const matchStart = new Date(config.beachPage.matchStart);
|
||||
@@ -39,73 +48,184 @@ export default function BeachClientPage({ config }: { config: any }) {
|
||||
<div className="min-h-screen flex flex-col font-sans bg-white text-black">
|
||||
<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]">Beachturnering</h1>
|
||||
<p className="font-bold italic text-[25px] mb-5 text-black">Samla ihop ett kompisgäng och anmäl er!</p>
|
||||
<main className="grow flex flex-col items-center bg-[#c8e9f2] bg-[url('/images/volleyball-net.svg')] bg-cover bg-center bg-no-repeat w-full pb-20">
|
||||
|
||||
<section className="flex flex-col items-center text-center w-full px-4 pt-16">
|
||||
{/* Logotyp med subtil hover-effekt */}
|
||||
<div className="relative group cursor-default">
|
||||
<div className="absolute -inset-4 bg-white/20 blur-2xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
||||
<img
|
||||
className="w-full max-w-62.5 md:max-w-75 relative drop-shadow-2xl transform hover:scale-105 transition-transform duration-500"
|
||||
src="/images/hk-mosf.webp"
|
||||
alt="Mat- & Sommarfesten"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Huvudrubrik med 3D-känsla */}
|
||||
<h1 className="font-beachday text-[clamp(45px,9vw,80px)] text-[#fdb84b] uppercase leading-[0.9] mt-8 mb-4 drop-shadow-[0_4px_4px_rgba(64,97,133,0.3)] tracking-wide wrap-break-word w-full max-w-full">
|
||||
Beach­turnering
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl font-bold text-[#39979f] bg-white/60 px-6 py-2 rounded-full backdrop-blur-sm shadow-sm">
|
||||
Samla ihop ett kompisgäng och anmäl er!
|
||||
</p>
|
||||
|
||||
{/* Moderniserad "Stängd"-box */}
|
||||
{timeInfo.isClosed && (
|
||||
<div className="bg-black p-5 rounded-[20px] my-5 mx-1.25 w-full max-w-150 shadow-xl border-l-8 border-red-600">
|
||||
<h3 className="text-red-500 text-[clamp(1.5rem,6.5vw,2rem)] font-bold pb-2.5 text-center">Anmälan är stängd!</h3>
|
||||
<p className="text-white text-center whitespace-pre-wrap">{config.beachPage.closeReason}</p>
|
||||
<div className="bg-white/95 backdrop-blur-md p-8 md:p-10 rounded-[30px] w-full max-w-2xl my-10 shadow-2xl border border-red-100 relative overflow-hidden flex flex-col items-center animate-fade-in">
|
||||
{/* Röd dekorationslist i toppen */}
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-linear-to-r from-red-500 to-orange-400" />
|
||||
|
||||
<div className="bg-red-50 p-4 rounded-full mb-4 shadow-inner">
|
||||
<FaLock className="text-red-500" size={32} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-red-600 text-[clamp(1.5rem,5vw,2.2rem)] font-black uppercase tracking-widest mb-3">
|
||||
Anmälan är stängd
|
||||
</h3>
|
||||
<p className="text-gray-700 text-lg font-medium leading-relaxed max-w-md">
|
||||
{config.beachPage.closeReason}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<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 shadow-lg">
|
||||
{/* INFORMATION TEXT (Var, När, Regler, etc.) */}
|
||||
<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>
|
||||
<section className="flex flex-col items-center text-center w-full px-1.25">
|
||||
<div className="bg-white/90 backdrop-blur-sm p-8 md:p-12 rounded-[30px] my-10 mx-2 flex flex-col items-center w-full max-w-4xl shadow-2xl border border-white/50">
|
||||
|
||||
<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={config.beachPage.festivalLink}>Höganäs Mat- & Sommarfest</a>
|
||||
</p>
|
||||
{/* Större och tydligare badge för Turneringsinfo */}
|
||||
<div className="bg-[#39979f] text-white px-8 py-2.5 rounded-full font-beachday tracking-widest text-3xl font-bold uppercase mb-10 shadow-lg">
|
||||
Turneringsinfo
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-12 w-full text-left">
|
||||
|
||||
<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>
|
||||
{/* VAR & NÄR - Logistik */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-[#cc8124]">
|
||||
<FaMapMarkerAlt size={24} />
|
||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Var</h4>
|
||||
</div>
|
||||
<p className="text-gray-700 text-lg leading-relaxed">
|
||||
<a target="_blank" rel="noreferrer" className="font-bold text-[#39979f] hover:underline decoration-2" 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">Kostnad:</h4>
|
||||
<p className="text-black text-[18px] m-[4px_10px]">Gratis!!!</p>
|
||||
<div className="flex items-center gap-3 text-[#cc8124] mt-6">
|
||||
<FaClock size={24} />
|
||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">När</h4>
|
||||
</div>
|
||||
<div className="text-gray-700 text-lg space-y-1">
|
||||
<p className="font-bold">{timeInfo.dateStr}</p>
|
||||
<p>{timeInfo.matchStr}</p>
|
||||
<p className="text-sm italic pt-3 border-t border-blue-100 mt-2">
|
||||
Samtidigt som <a target="_blank" rel="noreferrer" className="text-[#39979f] hover:underline" href={config.beachPage.festivalLink}>Höganäs Mat- & Sommarfest</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{/* REGLER & KLASSER - Spelet */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-[#cc8124]">
|
||||
<FaClipboardList size={24} />
|
||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Regler</h4>
|
||||
</div>
|
||||
<p className="text-gray-700 text-base leading-relaxed">
|
||||
Matcherna spelas 4v4 med <a target="_blank" rel="noreferrer" className="text-[#39979f] font-bold 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="text-[#39979f] font-bold hover:underline" href="https://www.volleyboll.se/forbundet/valkommen-till-volleyboll/grenar-och-spelformer/beachvolley">beachvolley</a>.
|
||||
</p>
|
||||
|
||||
{/* Övrig info sektionen */}
|
||||
<h4 className="text-black font-bold text-[1.25rem] mt-5 mb-0 italic">Övrig info:</h4>
|
||||
{(config.beachPage?.otherInfo || "").split('\n').map((line: string, i: number) => (
|
||||
line.trim() && <p key={i} className="text-black text-[18px] m-[4px_10px]">{line}</p>
|
||||
))}
|
||||
<div className="flex items-center gap-3 text-[#cc8124] mt-6">
|
||||
<FaUsers size={24} />
|
||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Klasser</h4>
|
||||
</div>
|
||||
<div className="text-gray-700 text-sm space-y-3">
|
||||
<p className="leading-snug">Turneringen kan delas upp i tre klasser beroende på antal anmälda lag och nivå:</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{classesData.map((c: any, i: number) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2.5 py-1 rounded-md font-bold border text-xs tracking-wider"
|
||||
style={{ backgroundColor: `${c.color}15`, color: c.color, borderColor: `${c.color}40` }}
|
||||
>
|
||||
{c.name.toUpperCase()} {c.desc && <span className="font-normal opacity-80">({c.desc})</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="italic text-xs text-gray-500">Gör en gissning på er nivå i anmälan – skriv gärna om ni är osäkra.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pris sektionen */}
|
||||
<h4 className="font-beachday uppercase text-[30px] font-medium p-2.5 mt-5 text-black">
|
||||
{config.beachPage?.prizesText || "Fina priser till alla pallplatser! 🏆"}
|
||||
</h4>
|
||||
{/* KOSTNAD & ANMÄLAN */}
|
||||
<div className="space-y-4 md:col-span-2 bg-[#fdb84b]/10 p-6 md:p-8 rounded-2xl border border-[#fdb84b]/20">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="flex items-center gap-3 text-[#cc8124]">
|
||||
<FaTicketAlt size={24} />
|
||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Anmälan & Kostnad</h4>
|
||||
</div>
|
||||
<p className="text-2xl font-black text-green-700 uppercase tracking-wider">Kostnad: Gratis!</p>
|
||||
<div className="text-gray-700 text-sm space-y-2">
|
||||
<p>Anmälan görs via formuläret nedan. Du får ett bekräftelsemejl när vi granskat er anmälan.</p>
|
||||
<p className="font-bold italic text-[#39979f]">{timeInfo.deadlineText}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 bg-white/60 p-5 rounded-xl shadow-inner border border-white flex-1">
|
||||
<FaInfoCircle className="text-[#39979f] shrink-0 mt-1" size={20} />
|
||||
<div className="text-sm text-gray-700">
|
||||
<p className="font-bold text-black mb-2 uppercase tracking-wide text-xs">Övrig info:</p>
|
||||
<div className="space-y-1.5">
|
||||
{(config.beachPage?.otherInfo || "").split('\n').map((line: string, i: number) => (
|
||||
line.trim() && <p key={i} className="leading-tight flex gap-2"><span>•</span> {line}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kompaktare PRIS-BANNER */}
|
||||
<div className="mt-10 w-full max-w-2xl bg-linear-to-r from-[#39979f] to-[#62b8bc] py-4 px-8 rounded-2xl shadow-xl border-b-4 border-black/20">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<FaTrophy className="text-[#fdb84b] shrink-0" size={28} />
|
||||
<h4 className="font-beachday uppercase text-[clamp(1.1rem,4vw,1.8rem)] tracking-[0.05em] font-medium text-white text-center leading-none">
|
||||
{config.beachPage?.prizesText || "Fina priser till alla pallplatser! 🏆"}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Rendera formuläret endast om anmälan är öppen */}
|
||||
{!timeInfo.isClosed && <BeachRegistrationForm />}
|
||||
{!timeInfo.isClosed && <BeachRegistrationForm config={config} />}
|
||||
|
||||
<section className="w-full flex justify-center px-1.25 mt-5">
|
||||
<div className="bg-[#FAAC4F] flex flex-col justify-center items-center py-2.5 rounded-[20px] m-[10px_5px] w-full max-w-100 shadow-md">
|
||||
<h3 className="text-white text-[clamp(1.5rem,6.5vw,2rem)] text-center font-bold">Kontakta oss</h3>
|
||||
<a href="mailto:beach@lerbergetsvolleyboll.se?subject=Beach%20turnering" className="group flex justify-center items-center text-black font-bold text-[1.1rem] mt-2 relative pb-1">
|
||||
<IoIosMail className="h-7 w-7 mr-2" size={24} />
|
||||
<span>beach@lerbergetsvolleyboll.se</span>
|
||||
<span className="absolute bottom-0 left-0 w-full h-0.5 bg-black transform scale-x-0 transition-transform duration-300 origin-left group-hover:scale-x-100" />
|
||||
<section className="w-full flex justify-center px-4 mt-10 mb-20">
|
||||
<div className="bg-linear-to-br from-[#39979f] to-[#62b8bc] p-10 md:p-12 rounded-[40px] shadow-2xl w-full max-w-2xl text-center transform transition-transform duration-500 border border-blue-400/20 relative overflow-hidden group">
|
||||
|
||||
{/* Dekorativ bakgrundscirkel */}
|
||||
<div className="absolute -right-10 -top-10 w-40 h-40 bg-white/5 rounded-full blur-2xl group-hover:bg-white/10 transition-colors duration-700" />
|
||||
|
||||
{/* Ikon-badge */}
|
||||
<div className="bg-linear-to-br from-[#fdb84b] to-[#e69c24] w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg border-4 border-[#62b8bc] relative z-10">
|
||||
<IoIosMail className="text-white" size={40} />
|
||||
</div>
|
||||
|
||||
<h3 className="font-beachday text-white text-[clamp(2rem,5vw,3rem)] uppercase tracking-widest mb-3 relative z-10">
|
||||
Kontakta oss
|
||||
</h3>
|
||||
|
||||
<p className="text-white mb-8 font-medium max-w-md mx-auto relative z-10">
|
||||
Har du frågor om turneringen, regler eller vill ändra något i er anmälan? Tveka inte att höra av dig!
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="mailto:beach@lerbergetsvolleyboll.se?subject=Beach%20turnering"
|
||||
className="inline-flex items-center gap-3 bg-white text-[#39979f] font-black text-lg px-8 py-4 rounded-2xl hover:bg-[#fdf5e6] hover:scale-105 transition-all shadow-[0_10px_20px_rgba(0,0,0,0.2)] relative z-10"
|
||||
>
|
||||
<IoIosMail className="text-[#fdb84b]" size={28} />
|
||||
<span>Skicka ett mejl</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+163
-128
@@ -3,157 +3,192 @@
|
||||
'use client';
|
||||
|
||||
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { PiCheckCircle, PiWarningCircle } from "react-icons/pi";
|
||||
|
||||
interface AlertState {
|
||||
show: boolean;
|
||||
type: 'success' | 'danger' | '';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SubmitResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export default function BeachRegistrationForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [wasValidated, setWasValidated] = useState<boolean>(false);
|
||||
const [alert, setAlert] = useState<AlertState>({ show: false, type: '', message: '' });
|
||||
import React, { useRef, useActionState, useEffect } from 'react';
|
||||
import { FaUser, FaEnvelope, FaPhone, FaUsers, FaTrophy, FaCheckCircle, FaExclamationTriangle } from "react-icons/fa";
|
||||
import { submitRegistration } from './actions';
|
||||
|
||||
export default function BeachRegistrationForm({ config }: { config: any }) {
|
||||
const [state, formAction, isPending] = useActionState(submitRegistration, null);
|
||||
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;
|
||||
const classesData = Array.isArray(config.beachPage?.classes)
|
||||
? config.beachPage.classes
|
||||
: [
|
||||
{ name: 'Grön', color: '#16a34a', desc: 'Nybörjare' },
|
||||
{ name: 'Blå', color: '#2563eb', desc: 'Spelat lite' },
|
||||
{ name: 'Svart', color: '#111827', desc: 'Spelat mycket' }
|
||||
];
|
||||
|
||||
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) {
|
||||
setAlert({
|
||||
show: true,
|
||||
type: 'danger',
|
||||
message: 'Ett tekniskt fel uppstod. Försök igen senare.',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
useEffect(() => {
|
||||
// Rensa endast vid lyckad inskickning
|
||||
if (state?.success) {
|
||||
formRef.current?.reset();
|
||||
turnstileRef.current?.reset();
|
||||
} else if (state?.success === false) {
|
||||
// Återställ Turnstile så de kan försöka igen vid fel
|
||||
turnstileRef.current?.reset();
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<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 shadow-2xl">
|
||||
<h2 className="font-beachday text-[clamp(1.875rem,5.2vw,50px)] text-white text-center m-[5px_0] uppercase tracking-wide">
|
||||
Anmälningsformulär
|
||||
</h2>
|
||||
<section className="w-full flex justify-center px-4 mb-20">
|
||||
<div className="bg-[#fdf5e6] p-8 md:p-12 rounded-[40px] w-full max-w-3xl shadow-2xl border-b-8 border-[#e6be8a]">
|
||||
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={handleSubmit}
|
||||
className={`group/form flex flex-col items-center w-full mt-5 ${wasValidated ? 'was-validated' : ''}`}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
{/* INPUT FÄLT (Namn, Email, Tel, Lagnamn, Antal) */}
|
||||
{[
|
||||
{ name: 'name', type: 'text', placeholder: 'Ditt namn', auto: 'name', error: 'Glöm inte ditt namn!' },
|
||||
{ name: 'email', type: 'email', placeholder: 'Email', auto: 'email', error: 'Emailadressen är inte giltig.' },
|
||||
{ name: 'tel', type: 'tel', placeholder: 'Telefonnummer', auto: 'tel', error: 'Glöm inte ett telefonnummer!' },
|
||||
{ name: 'team', type: 'text', placeholder: 'Lagnamn', auto: 'organization', error: 'Glöm inte ert lagnamn!' },
|
||||
].map((f) => (
|
||||
<div key={f.name} className="relative w-full mb-3.75">
|
||||
<div className="text-center mb-10 px-2">
|
||||
<h2 className="font-beachday text-[clamp(35px,8vw,60px)] text-[#39979f] uppercase tracking-wide leading-[0.9] wrap-break-word w-full max-w-full">
|
||||
Anmälnings­formulär
|
||||
</h2>
|
||||
<div className="h-1.5 w-24 bg-[#fdb84b] mx-auto mt-4 rounded-full" />
|
||||
</div>
|
||||
|
||||
<form action={formAction} ref={formRef} className="space-y-6">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InputGroup
|
||||
icon={<FaUser />}
|
||||
name="name"
|
||||
placeholder="Ditt namn"
|
||||
required
|
||||
minLength={2}
|
||||
defaultValue={state?.values?.name || ''}
|
||||
/>
|
||||
<InputGroup
|
||||
icon={<FaEnvelope />}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="namn@domän.com"
|
||||
required
|
||||
defaultValue={state?.values?.email || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InputGroup
|
||||
icon={<FaPhone />}
|
||||
name="tel"
|
||||
type="tel"
|
||||
placeholder="Telefonnummer"
|
||||
required
|
||||
pattern="[0-9+ \-]{8,}"
|
||||
defaultValue={state?.values?.tel || ''}
|
||||
/>
|
||||
<InputGroup
|
||||
icon={<FaTrophy />}
|
||||
name="team"
|
||||
placeholder="Lagnamn"
|
||||
required
|
||||
minLength={2}
|
||||
defaultValue={state?.values?.team || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8 bg-white/50 p-6 rounded-3xl border border-[#e6be8a]/30 shadow-inner">
|
||||
<div className="flex-1">
|
||||
<label className="flex items-center gap-2 font-bold text-[#39979f] mb-3 text-sm uppercase">
|
||||
<FaUsers /> Antal spelare
|
||||
</label>
|
||||
<input
|
||||
type={f.type} name={f.name} placeholder={f.placeholder} required autoComplete={f.auto}
|
||||
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 focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] transition-colors"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
name="count"
|
||||
required
|
||||
pattern="^([2-9]|10)$"
|
||||
title="Ange ett antal mellan 2 och 10"
|
||||
onInput={(e) => {
|
||||
e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '');
|
||||
}}
|
||||
className="w-full p-4 rounded-xl border-2 border-transparent bg-white focus:border-[#39979f] outline-none transition-all shadow-inner"
|
||||
placeholder="Minst 2"
|
||||
defaultValue={state?.values?.count || ''}
|
||||
/>
|
||||
<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">{f.error}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="relative w-full mb-3.75">
|
||||
<input type="number" name="count" placeholder="Antal spelare" min="2" required
|
||||
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 focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] transition-colors" />
|
||||
<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 className="flex-2">
|
||||
<label className="font-bold text-[#39979f] mb-3 block text-sm uppercase">Välj klass</label>
|
||||
<div className={`grid grid-cols-2 md:grid-cols-${Math.min(classesData.length, 3)} gap-3`}>
|
||||
{classesData.map((c: any) => (
|
||||
<LevelButton
|
||||
key={c.name}
|
||||
value={c.name}
|
||||
label={c.name}
|
||||
desc={c.desc}
|
||||
color={c.color}
|
||||
defaultChecked={state?.values?.level === c.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RADIOS */}
|
||||
<div className="w-full mt-2.5 text-left group/radios">
|
||||
{[
|
||||
{ id: 'level-gron', val: 'Grön', label: 'Grön', color: 'text-green-700', accent: 'accent-green-600', desc: '(Nybörjare)' },
|
||||
{ id: 'level-bla', val: 'Blå', label: 'Blå', color: 'text-[#0f65bf]', accent: 'accent-[#0f65bf]', desc: '(Spelat lite)' },
|
||||
{ id: 'level-svart', val: 'Svart', label: 'Svart', color: 'text-black', accent: 'accent-black', desc: '(Spelat mycket)' },
|
||||
].map((l) => (
|
||||
<React.Fragment key={l.id}>
|
||||
<input type="radio" id={l.id} name="level" value={l.val} required className={`inline-block align-middle m-[6px_5px] w-4 h-4 ${l.accent} cursor-pointer`} />
|
||||
<label htmlFor={l.id} className="text-white inline-block align-middle cursor-pointer select-none">
|
||||
<strong className={`${l.color} bg-white px-0.75 rounded-sm font-bold`}>{l.label}</strong> {l.desc}
|
||||
</label>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<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>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Övrig info till oss..."
|
||||
className="w-full h-32 p-4 rounded-2xl border-2 border-transparent bg-white focus:border-[#39979f] outline-none resize-none shadow-inner"
|
||||
defaultValue={state?.values?.message || ''}
|
||||
/>
|
||||
|
||||
<input type="text" name="website" className="hidden" tabIndex={-1} />
|
||||
|
||||
<div className="flex justify-center py-2">
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ''}
|
||||
options={{ theme: 'light' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea name="message" autoComplete="off" placeholder="Övrig info..." 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 mt-5" />
|
||||
|
||||
<div className="hidden">
|
||||
<input type="text" name="website" tabIndex={-1} autoComplete="off" />
|
||||
</div>
|
||||
|
||||
{alert.show && (
|
||||
<div className={`w-full p-4 rounded-md text-center font-bold my-4 ${alert.type === 'success' ? 'bg-[#d1e7dd] text-[#0a3622] border-[#a3cfbb]' : 'bg-[#f8d7da] text-[#58151c] border-[#f1aeb5]'}`}>
|
||||
{alert.message}
|
||||
{state && (
|
||||
<div className={`flex items-center gap-3 p-5 rounded-2xl font-bold transition-all ${state.success ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{state.success ? <FaCheckCircle size={24} className="shrink-0" /> : <FaExclamationTriangle size={24} className="shrink-0" />}
|
||||
{state.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
<button type="submit" disabled={isSubmitting} className="bg-white text-black uppercase w-full max-w-75 h-12.5 text-[20px] font-bold rounded-[10px] my-5 flex justify-center items-center cursor-pointer hover:bg-gray-200 transition-all active:scale-95 disabled:opacity-70">
|
||||
{isSubmitting && (
|
||||
<svg className="animate-spin h-5 w-5 text-black mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<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" />
|
||||
</svg>
|
||||
)}
|
||||
<span>Skicka anmälan</span>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full py-5 bg-[#39979f] text-white rounded-2xl font-bold text-xl uppercase tracking-wide shadow-xl hover:bg-[#fdb84b] hover:text-[#39979f] transition-all active:scale-[0.98] disabled:bg-gray-300 disabled:text-gray-500"
|
||||
>
|
||||
{isPending ? 'Skickar...' : 'Skicka anmälan'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Uppdaterad med defaultChecked support
|
||||
function LevelButton({ value, label, desc, color, defaultChecked }: any) {
|
||||
return (
|
||||
<label className="cursor-pointer flex-1 relative" style={{ '--class-color': color } as React.CSSProperties}>
|
||||
<input
|
||||
type="radio"
|
||||
name="level"
|
||||
value={value}
|
||||
required
|
||||
className="peer sr-only"
|
||||
defaultChecked={defaultChecked}
|
||||
/>
|
||||
|
||||
<div className="text-center p-3 rounded-xl border-2 border-white bg-white text-gray-400 font-black transition-all peer-checked:text-white peer-checked:bg-(--class-color) peer-checked:border-(--class-color) peer-checked:scale-[1.03] shadow-sm h-full flex flex-col justify-center items-center">
|
||||
<span className="uppercase tracking-tighter">{label}</span>
|
||||
{desc && <span className="text-[10px] font-medium opacity-80 mt-1 uppercase tracking-widest">{desc.replace(/[()]/g, '')}</span>}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroup({ icon, ...props }: any) {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-[#e6be8a] group-focus-within:text-[#39979f] transition-colors">
|
||||
{icon}
|
||||
</div>
|
||||
<input
|
||||
{...props}
|
||||
className="w-full pl-12 pr-4 py-4 rounded-xl border-2 border-transparent bg-white focus:border-[#39979f] outline-none transition-all shadow-inner placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// app/beach/actions.ts
|
||||
|
||||
'use server';
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export async function submitRegistration(prevState: any, formData: FormData) {
|
||||
// 1. Spara all rådata direkt för att kunna skicka tillbaka den vid fel
|
||||
const rawData = {
|
||||
name: formData.get('name') as string,
|
||||
email: formData.get('email') as string,
|
||||
tel: formData.get('tel') as string,
|
||||
team: formData.get('team') as string,
|
||||
count: formData.get('count') as string,
|
||||
level: formData.get('level') as string,
|
||||
message: formData.get('message') as string,
|
||||
};
|
||||
|
||||
const honeypot = formData.get('website');
|
||||
if (honeypot) {
|
||||
return { success: false, message: 'Spam detekterat.', values: rawData };
|
||||
}
|
||||
|
||||
// 2. Hämta Turnstile Token och Secret (MATCHING .env NAME)
|
||||
const turnstileToken = formData.get('cf-turnstile-response');
|
||||
const secretKey = process.env.TURNSTILE_SECRET; // Matchar din .env exakt
|
||||
|
||||
if (!secretKey) {
|
||||
console.error("Saknar TURNSTILE_SECRET i miljön.");
|
||||
return {
|
||||
success: false,
|
||||
message: 'Serverkonfiguration saknas. Kontakta administratör.',
|
||||
values: rawData
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Verifiera mot Cloudflare
|
||||
try {
|
||||
const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
secret: secretKey,
|
||||
response: turnstileToken?.toString() || '',
|
||||
}),
|
||||
});
|
||||
|
||||
const turnstileData = await turnstileRes.json();
|
||||
|
||||
if (!turnstileData.success) {
|
||||
console.error("Turnstile failed:", turnstileData);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Säkerhetsverifieringen misslyckades. Ladda om sidan och försök igen.',
|
||||
values: rawData // Returnerar värdena så användaren slipper skriva om dem
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Turnstile fetch error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Kunde inte nå verifieringsservern.',
|
||||
values: rawData
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Formatera telefonnummer
|
||||
let cleanTel = rawData.tel.replace(/\D/g, '');
|
||||
if (cleanTel.startsWith('46')) cleanTel = cleanTel.substring(2);
|
||||
else if (cleanTel.startsWith('0')) cleanTel = cleanTel.substring(1);
|
||||
|
||||
const formattedTel = cleanTel.length === 9
|
||||
? `+46 (0)${cleanTel.substring(0, 2)}-${cleanTel.substring(2, 5)} ${cleanTel.substring(5, 7)} ${cleanTel.substring(7, 9)}`
|
||||
: rawData.tel;
|
||||
|
||||
// 5. Nodemailer setup
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || '10.10.0.113', // Använder .env med fallback
|
||||
port: 25,
|
||||
secure: false,
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
try {
|
||||
const dateStr = new Date().toLocaleString('sv-SE');
|
||||
|
||||
// Mail till klubben
|
||||
await transporter.sendMail({
|
||||
from: `"Beach-anmälan: ${rawData.name}" <no-reply@lerbergetsvolleyboll.se>`,
|
||||
to: 'beach@lerbergetsvolleyboll.se',
|
||||
replyTo: rawData.email,
|
||||
subject: `Anmälan: ${rawData.team}`,
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; border: 1px solid #eee; padding: 20px;">
|
||||
<h2 style="color: #406185;">Ny anmälan: ${rawData.team}</h2>
|
||||
<p><strong>Kontaktperson:</strong> ${rawData.name}</p>
|
||||
<p><strong>E-post:</strong> ${rawData.email}</p>
|
||||
<p><strong>Telefon:</strong> ${formattedTel}</p>
|
||||
<p><strong>Antal spelare:</strong> ${rawData.count}</p>
|
||||
<p><strong>Nivå:</strong> ${rawData.level}</p>
|
||||
<p><strong>Meddelande:</strong><br>${rawData.message || 'Inget meddelande'}</p>
|
||||
<hr>
|
||||
<p style="font-size: 10px; color: #999;">Skickat: ${dateStr}</p>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
// Bekräftelse till användaren
|
||||
await transporter.sendMail({
|
||||
from: `"LVS" <no-reply@lerbergetsvolleyboll.se>`,
|
||||
to: rawData.email,
|
||||
subject: `Vi har tagit emot din anmälan för ${rawData.team}`,
|
||||
html: `<div style="font-family: sans-serif; padding: 20px;">
|
||||
<h2>Hej ${rawData.name}!</h2>
|
||||
<p>Tack för din anmälan till beachturneringen. Vi återkommer så snart vi har granskat era uppgifter.</p>
|
||||
<p>Ses på stranden!<br><strong>Lerbergets Volleybollsällskap</strong></p>
|
||||
</div>`
|
||||
});
|
||||
|
||||
// Returnerar INGA värden här, vilket gör att formuläret kan rensas rent
|
||||
return { success: true, message: 'Tack! Din anmälan har skickats. Håll utkik i din inkorg efter bekräftelse.' };
|
||||
} catch (e) {
|
||||
console.error("Nodemailer error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Kunde inte skicka mail. Kontakta oss manuellt istället.',
|
||||
values: rawData
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default function HomeClientContent() {
|
||||
</div>
|
||||
<h4 className="italic text-xl my-2.5 font-bold">Lerbergets Volleybollsällskap</h4>
|
||||
<p className="max-w-200 leading-[1.1]">
|
||||
Klubben startade den 1 mars 2018 och har i snabb takt utvecklats...
|
||||
Klubben startade den 1 mars 2018 och har i snabb takt utvecklats med både kidsvolley, 4-mannavolley och ett gott gäng killar och tjejer som tränar tillsammans minst två gånger i veckan och på sommaren spelas det beachvolley
|
||||
</p>
|
||||
<div className="flex flex-col gap-2.5 mt-10">
|
||||
<HeroButton onClick={() => setActiveModal('member')}>Bli Medlem</HeroButton>
|
||||
@@ -35,8 +35,8 @@ export default function HomeClientContent() {
|
||||
<HeroButton href="https://www.basesport.se/category/lerbergets-vbk">Klubbkollektion</HeroButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center p-2.5 justify-end">
|
||||
<img src="/images/player.webp" alt="Spelare" className="max-h-112.5 player-animation" />
|
||||
<div className="relative w-64 md:w-75 player-animation">
|
||||
<img src="/images/player.svg" alt="Spelare" className="w-full h-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user