diff --git a/app/api/submit/route.js b/app/api/submit/route.js
new file mode 100644
index 0000000..2073c95
--- /dev/null
+++ b/app/api/submit/route.js
@@ -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 = `
+
+
+ Beach-anmälan
+
+ Lagnamn: ${team}
+ Kontakt: ${name}
+ E-post: ${email}
+ Telefon: ${formattedTel}
+ Spelare: ${count}
+ Nivå: ${level}
+
+ Meddelande: ${message ? message.replace(/\n/g, ' ') : ''}
+
+
+
+ ${team}
+ ${count}
+ ${level}
+ ${name}
+ ${formattedTel}
+ ${email}
+ ${dateStr}
+ ${cleanMessage}
+
+
+
+
+ `;
+
+ // 1. Send the data to the Club
+ await transporter.sendMail({
+ from: `"Anmälan: ${name}" `,
+ 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" `,
+ to: email?.toString(),
+ subject: `Bekräftelse på anmälan: ${team}`,
+ html: `
+
+
Hej ${name}!
+
Tack för din anmälan vi återkommer så fort vi kan.
+
+
+ Med vänliga hälsningar,
+ Lerbergets Volleybollsällskap
+
+
+ `,
+ });
+
+ 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 });
+ }
+}
\ No newline at end of file
diff --git a/app/beach/page.tsx b/app/beach/page.tsx
new file mode 100644
index 0000000..62276e7
--- /dev/null
+++ b/app/beach/page.tsx
@@ -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({
+ 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(false);
+ const [wasValidated, setWasValidated] = useState(false);
+ const [alert, setAlert] = useState({ show: false, type: '', message: '' });
+
+ const formRef = useRef(null);
+ const turnstileRef = useRef(null);
+
+ const handleSubmit: React.FormEventHandler = 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Beachturnering
+ Samla ihop ett kompisgäng och anmäl er!
+
+ {timeInfo.isClosed && (
+
+
Anmälan är nu stängd!
+
Det är hela 26 lag anmälda och vi kan tyvärr inte ta emot fler anmälningar.
+
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.
+
+ )}
+
+
+
Var:
+
Kvickbadet i Höganäs
+
+
När:
+
{timeInfo.dateStr}
+
{timeInfo.matchStr}
+
Samtidigt som Höganäs Mat- & Sommarfest
+
+
Regler:
+
Matcherna spelas 4v4 med inomhusregler , förutom poängräkningen som följer reglerna för beachvolleyboll .
+
+
Klasser:
+
+ Turneringen kan komma att delas upp i tre olika klasser beroende på antal anmälda lag och baserat på nivå:
+ Grön (Nybörjare),{' '}
+ Blå (Amatör),{' '}
+ Svart (Proffs)
+
+
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.
+
+
Kostnad:
+
Gratis!!!
+
+
Anmälan:
+
Anmälan görs genom anmälningsformuläret nedan. Du får ett bekräftelsemejl när vi kollat igenom er anmälan.
+
{timeInfo.deadlineText}
+
+
Övrig info:
+
Turneringen upskattas hålla på till ca 17-18 tiden
+
Det kommer finnas duschmöjligheter i anslutning till stranden.
+
+
+ Det kommer att finnas fina priser till alla pallplatser! 🏆
+
+
+
+
+ {!timeInfo.isClosed && (
+
+
+
Anmälningsformulär
+
+ {/* Form starts here - uses group/form to track the .was-validated state natively */}
+
+
+
+ )}
+
+ {/* Contact Section */}
+
+
+
+
+ {/* Footer */}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/favicon.ico b/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/app/favicon.ico and /dev/null differ
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..e27a5e2 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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;
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index f7fa87e..c2e650f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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 (
-
-
+
+
{children}
);
-}
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 295f8fd..5da5a9b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -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 (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
);
-}
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index ee5a0a9..b3b9b14 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,18 +8,22 @@
"name": "lvs-site",
"version": "0.1.0",
"dependencies": {
+ "@marsidev/react-turnstile": "^1.4.2",
"next": "16.1.6",
+ "nodemailer": "^8.0.1",
+ "postcss": "^8.5.6",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "react-icons": "^5.5.0"
},
"devDependencies": {
- "@tailwindcss/postcss": "^4",
+ "@tailwindcss/postcss": "^4.2.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
- "tailwindcss": "^4",
+ "tailwindcss": "^4.2.1",
"typescript": "^5"
}
},
@@ -1021,6 +1025,16 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@marsidev/react-turnstile": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.4.2.tgz",
+ "integrity": "sha512-xs1qOuyeMOz6t9BXXCXWiukC0/0+48vR08B7uwNdG05wCMnbcNgxiFmdFKDOFbM76qFYFRYlGeRfhfq1U/iZmA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^17.0.2 || ^18.0.0 || ^19.0",
+ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -5090,6 +5104,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nodemailer": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5354,7 +5377,6 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -5453,6 +5475,15 @@
"react": "^19.2.3"
}
},
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index e4308e3..35d87bb 100644
--- a/package.json
+++ b/package.json
@@ -9,18 +9,22 @@
"lint": "eslint"
},
"dependencies": {
+ "@marsidev/react-turnstile": "^1.4.2",
"next": "16.1.6",
+ "nodemailer": "^8.0.1",
+ "postcss": "^8.5.6",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "react-icons": "^5.5.0"
},
"devDependencies": {
- "@tailwindcss/postcss": "^4",
+ "@tailwindcss/postcss": "^4.2.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
- "tailwindcss": "^4",
+ "tailwindcss": "^4.2.1",
"typescript": "^5"
}
}
diff --git a/public/file.svg b/public/file.svg
deleted file mode 100644
index 004145c..0000000
--- a/public/file.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/fonts/Beachday.ttf b/public/fonts/Beachday.ttf
new file mode 100644
index 0000000..f8f94c9
Binary files /dev/null and b/public/fonts/Beachday.ttf differ
diff --git a/public/fonts/Beachday.woff b/public/fonts/Beachday.woff
new file mode 100644
index 0000000..1ee2312
Binary files /dev/null and b/public/fonts/Beachday.woff differ
diff --git a/public/fonts/Beachday.woff2 b/public/fonts/Beachday.woff2
new file mode 100644
index 0000000..03039c7
Binary files /dev/null and b/public/fonts/Beachday.woff2 differ
diff --git a/public/globe.svg b/public/globe.svg
deleted file mode 100644
index 567f17b..0000000
--- a/public/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/images/favicon.svg b/public/images/favicon.svg
new file mode 100644
index 0000000..de17d8d
--- /dev/null
+++ b/public/images/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/hk-mosf.webp b/public/images/hk-mosf.webp
new file mode 100644
index 0000000..ecd79c9
Binary files /dev/null and b/public/images/hk-mosf.webp differ
diff --git a/public/images/laget.se.svg b/public/images/laget.se.svg
new file mode 100644
index 0000000..917b44b
--- /dev/null
+++ b/public/images/laget.se.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/images/logo-black.svg b/public/images/logo-black.svg
new file mode 100644
index 0000000..2f64cef
--- /dev/null
+++ b/public/images/logo-black.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/images/logo.svg b/public/images/logo.svg
new file mode 100644
index 0000000..81ad664
--- /dev/null
+++ b/public/images/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/player.webp b/public/images/player.webp
new file mode 100644
index 0000000..da24263
Binary files /dev/null and b/public/images/player.webp differ
diff --git a/public/images/volleyball-net.webp b/public/images/volleyball-net.webp
new file mode 100644
index 0000000..58ab428
Binary files /dev/null and b/public/images/volleyball-net.webp differ
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index 7705396..0000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/window.svg b/public/window.svg
deleted file mode 100644
index b2b2a44..0000000
--- a/public/window.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file