First commit

This commit is contained in:
2026-03-02 00:06:33 +01:00 Unverified
parent cc8d689e5d
commit 278471e53b
23 changed files with 857 additions and 105 deletions
+127
View File
@@ -0,0 +1,127 @@
// 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 });
}
}
+344
View File
@@ -0,0 +1,344 @@
// 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 antal anmälda lag och baserat 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 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>
);
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+37 -19
View File
@@ -1,26 +1,44 @@
/* app/globals.css */
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@layer base {
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
html,
body {
line-height: normal;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
@font-face {
font-family: 'Beachday';
src: url('/fonts/Beachday.woff2') format('woff2'),
url('/fonts/Beachday.woff') format('woff'),
url('/fonts/Beachday.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
/* 1. Add your exact keyframes from the old site */
@keyframes animateright {
from {
right: -300px;
opacity: 0;
}
to {
right: 0;
opacity: 1;
}
}
/* 2. Create a custom class to apply it */
.player-animation {
position: relative;
animation: animateright 1.5s;
}
:root {
--font-beachday: 'Beachday', sans-serif;
--font-sans: var(--font-montserrat), sans-serif;
}
+12 -17
View File
@@ -1,20 +1,17 @@
// app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Montserrat } from 'next/font/google';
import './globals.css';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
const montserrat = Montserrat({
subsets: ['latin'],
variable: '--font-montserrat',
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Lerbergets Volleybollsällskap',
description: 'Klubben startade den 1 mars 2018 och har i snabb takt utvecklats...',
};
export default function RootLayout({
@@ -23,12 +20,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<html lang="sv-SE" className={montserrat.variable}>
<body className="font-sans antialiased">
{children}
</body>
</html>
);
}
}
+226 -57
View File
@@ -1,65 +1,234 @@
import Image from "next/image";
// app/page.tsx
'use client';
import Link from 'next/link';
import { useState } from 'react';
import { FaFacebookSquare, FaInstagram } from "react-icons/fa";
import { IoIosMail } from "react-icons/io";
export default function Home() {
const [isMedlemOpen, setIsMedlemOpen] = useState(false);
const [isPrenumereraOpen, setIsPrenumereraOpen] = useState(false);
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
<div className="min-h-screen flex flex-col font-sans bg-white text-black">
{/* Header */}
<header className="bg-black flex justify-center">
<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"
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</Link>
</div>
</header>
{/* Main Content */}
<main className="grow flex flex-col">
{/* Promo Section (Hidden) */}
<section className="bg-[#fdb84b] justify-center">
<div className="p-2.5">
<h2 className="font-beachday text-black text-center text-[clamp(1.5rem,8vw,2.25rem)] uppercase leading-[1.1]">
Kolla in vår <Link href="/beach" className="text-[#406185] underline">Beachturnering</Link>
</h2>
</div>
</section>
{/* Hero / Info Section */}
<section className="bg-[#406185] flex justify-center text-white">
<div className="flex flex-col md:flex-row max-w-300 my-12.5 mx-5 w-full">
<div className="flex flex-col justify-center flex-1">
<div className="w-fit">
<h1 className="uppercase text-[2.5rem] font-bold leading-[1.1] mb-2.5">LVS</h1>
<div className="bg-white w-full h-1.5"></div>
</div>
<h4 className="italic text-xl my-2.5 leading-[1.2] 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 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 sommaren spelas det beachvolley
</p>
<div className="flex flex-col gap-2.5 mt-10">
<button
onClick={() => setIsMedlemOpen(true)}
className="text-[#406185] bg-white border-none py-3.5 px-7.5 text-center font-bold uppercase w-fit text-[1.1rem]"
>
Bli Medlem
</button>
<Link
href="#traningstider"
className="text-[#406185] bg-white border-none py-3.5 px-7.5 text-center font-bold uppercase w-fit text-[1.1rem]"
>
Träningstider
</Link>
<a
target="_blank"
rel="noreferrer"
href="https://www.basesport.se/category/lerbergets-vbk"
className="text-[#406185] bg-white border-none py-3.5 px-7.5 text-center font-bold uppercase w-fit text-[1.1rem]"
>
Klubbkollektion
</a>
</div>
</div>
{/* Player Image with restored animation class */}
<div className="hidden md:flex items-center p-2.5 justify-end max-h-full">
<img
src="/images/player.webp"
alt="Volleyboll spelare"
className="max-h-112.5 player-animation"
/>
</div>
</div>
</section>
{/* Träningstider Section - Restored to vertical stacking */}
<section id="traningstider" className="bg-white flex justify-center">
<div className="flex flex-col items-center w-full my-7.5 gap-2">
<div className="bg-[#406185] text-white w-full max-w-180 py-5 px-2.5 text-center">
<h2 className="uppercase text-[clamp(1.5rem,8vw,2.25rem)] font-bold">Träningstider</h2>
<p className="my-3.75 text-[1rem] leading-[1.1]">Här hittar du alltid uppdaterade träningstider från Laget.se</p>
<a
target="_blank"
rel="noreferrer"
href="https://www.laget.se/LVS/Event/Month"
className="text-[#f1c50e] border-b-2 border-[#f1c50e] font-bold uppercase"
>
Se nästa träningstid
</a>
</div>
<div className="bg-[#406185] text-white w-full max-w-180 py-5 px-2.5 text-center">
<h2 className="uppercase text-[clamp(1.5rem,8vw,2.25rem)] font-bold">Prenumerera träningstider</h2>
<p className="my-3.75 text-[1rem] leading-[1.1]">Våra träningstider finns som en kalender man kan prenumerera genom knappen nedan</p>
<button
onClick={() => setIsPrenumereraOpen(true)}
className="text-[#f1c50e] border-b-2 border-[#f1c50e] font-bold uppercase pb-0.5 cursor-pointer"
>
Prenumerera
</button>
</div>
</div>
</section>
{/* Kontakt Section */}
<section className="bg-[#406185] flex justify-center text-white">
<div className="flex flex-wrap-reverse justify-center gap-7.5 w-full max-w-300 my-10 px-5">
<div className="flex flex-wrap justify-center items-center gap-7.5 mx-5">
<a target="_blank" rel="noreferrer" title="Instagram" href="https://www.instagram.com/lerbergets_volleyboll/">
<FaInstagram className='h-20 w-20' />
</a>
<a target="_blank" rel="noreferrer" title="Facebook" href="https://www.facebook.com/lerbergetsvolleyboll">
<FaFacebookSquare className='h-20 w-20' />
</a>
<a target="_blank" rel="noreferrer" title="Laget.se" href="https://www.laget.se/LVS">
<img src="/images/laget.se.svg" alt="Laget.se" className="h-18 w-18" />
</a>
</div>
<div className="text-center p-2.5 flex flex-col justify-center items-center">
<h3 className="uppercase font-bold text-[1.875rem] leading-[1.2]">Kontakta oss</h3>
<p className="text-[1rem] leading-[1.6] my-1.25">Vi finns Instagram, Facebook och laget.se</p>
<p className="text-[1rem] leading-[1.6] my-1.25">Höganäs Sportcenter, Friluftsvägen 10, 263 54 Lerberget</p>
<a
href="mailto:info@lerbergetsvolleyboll.se?subject=Kontakta%20oss"
className="flex justify-center items-center text-[#f1c50e] font-bold text-[1rem] leading-[1.6] my-1.25"
>
<IoIosMail color='#f1c50e' className='h-6 w-6' />
info@lerbergetsvolleyboll.se
</a>
</div>
</div>
</section>
</main>
{/* Footer */}
<footer className="bg-black text-[#lightgrey] text-center py-3.75">
<p className="font-bold text-[#d3d3d3]">LVS - Lerbergets Volleybollsällskap {new Date().getFullYear()} ®</p>
</footer>
{/* --- MODALS --- */}
{/* Medlem Modal */}
{isMedlemOpen && (
<div
className="fixed inset-0 z-50 bg-black/60 flex justify-center items-center overflow-auto"
onClick={() => setIsMedlemOpen(false)}
>
<div
className="bg-white p-5 w-[80vw] max-w-max min-w-75 rounded-[15px] relative flex flex-col items-center text-center font-semibold text-[#406185]"
onClick={(e) => e.stopPropagation()}
>
<button
className="absolute right-3.75 top-3.75 w-5 h-5 block mx-auto"
onClick={() => setIsMedlemOpen(false)}
>
<svg viewBox="0 0 413.348 413.348" className="fill-black">
<path d="m413.348 24.354-24.354-24.354-182.32 182.32-182.32-182.32-24.354 24.354 182.32 182.32-182.32 182.32 24.354 24.354 182.32-182.32 182.32 182.32 24.354-24.354-182.32-182.32z" />
</svg>
</button>
<img src="/images/logo-black.svg" alt="Logo" className="h-25" />
<h2 className="text-[#406185] my-2.5 text-2xl font-bold uppercase">Registrera dig</h2>
<p className="text-[#406185] my-2.5">Trycka knappen nedan för att starta registreringen. Du kommer att skickas till laget.se</p>
<a
href="https://www.laget.se/LVS/Member"
target="_blank"
rel="noreferrer"
className="text-white bg-[#406185] uppercase font-bold py-2.25 px-11.5 mt-5 rounded-[50px] text-[1rem]"
>
Bli medlem
</a>
</div>
</div>
)}
{/* Prenumerera Modal */}
{isPrenumereraOpen && (
<div
className="fixed inset-0 z-50 bg-black/60 flex justify-center items-center overflow-auto"
onClick={() => setIsPrenumereraOpen(false)}
>
<div
className="bg-white p-5 w-[80vw] max-w-max min-w-75 rounded-[15px] relative flex flex-col items-center text-center font-semibold text-[#406185]"
onClick={(e) => e.stopPropagation()}
>
<button
className="absolute right-3.75 top-3.75 w-5 h-5 block mx-auto"
onClick={() => setIsPrenumereraOpen(false)}
>
<svg viewBox="0 0 413.348 413.348" className="fill-black">
<path d="m413.348 24.354-24.354-24.354-182.32 182.32-182.32-182.32-24.354 24.354 182.32 182.32-182.32 182.32 24.354 24.354 182.32-182.32 182.32 182.32 24.354-24.354-182.32-182.32z" />
</svg>
</button>
<h2 className="text-[#406185] my-2.5 text-2xl font-bold uppercase">Prenumerera</h2>
<p className="text-[#406185] my-2.5">Välj vilken LEVEL du vill prenumerera .</p>
<div className="flex flex-row flex-wrap justify-center gap-6.25 mt-5 max-w-162.5">
<a href="webcal://cal.laget.se/lvs-level-5-6.ics" className="text-white bg-[#406185] uppercase font-bold py-2.25 px-11.5 rounded-[50px] text-[1rem]">Level 5-6</a>
<a href="webcal://cal.laget.se/lvs-level-7-8.ics" className="text-white bg-[#406185] uppercase font-bold py-2.25 px-11.5 rounded-[50px] text-[1rem]">level 7-8</a>
<a href="webcal://cal.laget.se/lvs-level-8-9.ics" className="text-white bg-[#406185] uppercase font-bold py-2.25 px-11.5 rounded-[50px] text-[1rem]">Level 8-9</a>
<a href="webcal://cal.laget.se/lvs-level-10.ics" className="text-white bg-[#406185] uppercase font-bold py-2.25 px-11.5 rounded-[50px] text-[1rem]">Level 10</a>
<a href="webcal://cal.laget.se/LVS.ics" className="text-white bg-[#406185] uppercase font-bold py-2.25 px-11.5 rounded-[50px] text-[1rem]">Hela klubben</a>
</div>
</div>
</div>
)}
</div>
);
}
}