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 ( +
+ +
+
+ + Lerbergets Volleybollsällskap + +
+
+ +
+ +
+

Mat- & Sommarfesten

+

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 */} +
+ + {/* NAME */} +
+ +
+
+ +
Glöm inte ditt namn!
+
+ + {/* EMAIL */} +
+ +
+
+ +
Email addressen är inte giltig.
+
+ + {/* PHONE */} +
+ +
+
+ +
Glöm inte ett telefonnummer!
+
+ + {/* TEAM */} +
+ +
+
+ +
Glöm inte ert Lagnamn!
+
+ + {/* COUNT */} +
+ +
+
+ +
Glöm inte ange antal lagmedlemmar!
+
+ + {/* RADIOS */} +
+ + +
+ + + +
+ + + +
Glöm inte ange spelarnivå!
+
+ + {/* TEXTAREA */} +
+ +
+ + {/* HONEYPOT */} +
+ + +
+ + {/* ALERT */} + {alert.show && ( +
+ {alert.message} +
+ )} + + {/* TURNSTILE */} +
+ +
+ + {/* SUBMIT BUTTON */} + + +
+
+
+ )} + + {/* Contact Section */} +
+ +
+ +
+ + {/* Footer */} +
+

LVS - Lerbergets Volleybollsällskap {new Date().getFullYear()} ®

+
+ +
+ ); +} \ 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 ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark + + {/* Header */} +
+ +
+ + {/* Main Content */} +
+ + {/* Promo Section (Hidden) */} +
+
+

+ Kolla in vår Beachturnering +

+
+
+ + {/* Hero / Info Section */} +
+
+ +
+
+

LVS

+
+
+ +

Lerbergets Volleybollsällskap

+

+ 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 +

+ +
+ + + Träningstider + + + Klubbkollektion + +
+
+ + {/* Player Image with restored animation class */} +
+ Volleyboll spelare +
+
+
+ + {/* Träningstider Section - Restored to vertical stacking */} +
+
+ +
+

Träningstider

+

Här hittar du alltid uppdaterade träningstider från Laget.se

+ + Se nästa träningstid + +
+ +
+

Prenumerera på träningstider

+

Våra träningstider finns som en kalender man kan prenumerera på genom knappen nedan

+ +
+ +
+
+ + {/* Kontakt Section */} +
+
+ + + +
+

Kontakta oss

+

Vi finns på Instagram, Facebook och laget.se

+

Höganäs Sportcenter, Friluftsvägen 10, 263 54 Lerberget

+ + + info@lerbergetsvolleyboll.se + +
+ +
+
+
+ + {/* Footer */} +
+

LVS - Lerbergets Volleybollsällskap {new Date().getFullYear()} ®

+
+ + {/* --- MODALS --- */} + {/* Medlem Modal */} + {isMedlemOpen && ( +
setIsMedlemOpen(false)} + > +
e.stopPropagation()} + > + + + Logo +

Registrera dig

+

Trycka på knappen nedan för att starta registreringen. Du kommer att skickas till laget.se

+ + Bli medlem + +
+
+ )} + + {/* Prenumerera Modal */} + {isPrenumereraOpen && ( +
setIsPrenumereraOpen(false)} + > +
e.stopPropagation()} + > + + +

Prenumerera

+

Välj vilken LEVEL du vill prenumerera på.

+ + +
+
+ )} +
); -} +} \ 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