Compare commits
@@ -0,0 +1,41 @@
|
|||||||
|
name: Publish Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push_to_registry:
|
||||||
|
name: Build and Push Docker image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.stws.cc
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: git.stws.cc/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
stages:
|
|
||||||
- Build
|
|
||||||
|
|
||||||
"Build Docker":
|
|
||||||
stage: Build
|
|
||||||
image: docker:24.0.5
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
|
||||||
script:
|
|
||||||
- echo "Building Docker-image..."
|
|
||||||
|
|
||||||
- >
|
|
||||||
docker build
|
|
||||||
--label "org.opencontainers.image.url=$CI_PROJECT_URL"
|
|
||||||
--label "org.opencontainers.image.source=$CI_PROJECT_URL"
|
|
||||||
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
-t $CI_REGISTRY_IMAGE:latest .
|
|
||||||
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:latest
|
|
||||||
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
@@ -61,13 +61,13 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
<Image
|
<Image
|
||||||
className="w-full max-w-62.5 md:max-w-75 relative drop-shadow-2xl transform hover:scale-105 transition-transform duration-500"
|
className="w-full max-w-62.5 md:max-w-75 relative drop-shadow-2xl transform hover:scale-105 transition-transform duration-500"
|
||||||
src={matsommar}
|
src={matsommar}
|
||||||
alt="Mat- & Sommarfesten"
|
alt="Mat- och Sommarfesten Höganäs Logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Huvudrubrik med 3D-känsla */}
|
{/* 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">
|
<h1 className="font-beachday text-[clamp(80px,14vw,120px)] text-[#fdb84b] uppercase leading-[0.8] mt-8 mb-12 tracking-widest [text-shadow:4px_4px_0_#39979f]">
|
||||||
Beach­turnering
|
Höganäs Beach Open
|
||||||
</h1>
|
</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">
|
<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!
|
Samla ihop ett kompisgäng och anmäl er!
|
||||||
@@ -107,7 +107,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 text-[#cc8124]">
|
<div className="flex items-center gap-3 text-[#cc8124]">
|
||||||
<FaMapMarkerAlt size={24} />
|
<FaMapMarkerAlt size={24} />
|
||||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Var</h4>
|
<h4 className="font-beachday text-2xl uppercase tracking-wider text-black">Var</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700 text-lg leading-relaxed">
|
<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">
|
<a target="_blank" rel="noreferrer" className="font-bold text-[#39979f] hover:underline decoration-2" href="https://maps.app.goo.gl/vJcdSDCbvdGYXPNY6">
|
||||||
@@ -117,13 +117,13 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
|
|
||||||
<div className="flex items-center gap-3 text-[#cc8124] mt-6">
|
<div className="flex items-center gap-3 text-[#cc8124] mt-6">
|
||||||
<FaClock size={24} />
|
<FaClock size={24} />
|
||||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">När</h4>
|
<h4 className="font-beachday text-2xl uppercase tracking-wider text-black">När</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-700 text-lg space-y-1">
|
<div className="text-gray-700 text-lg space-y-1">
|
||||||
<p className="font-bold">{timeInfo.dateStr}</p>
|
<p className="font-bold">{timeInfo.dateStr}</p>
|
||||||
<p>{timeInfo.matchStr}</p>
|
<p>{timeInfo.matchStr}</p>
|
||||||
<p className="text-sm italic pt-3 border-t border-blue-100 mt-2">
|
<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>
|
Samtidigt som <a target="_blank" rel="noopener noreferrer" className="text-[#39979f] hover:underline" href={config.beachPage.festivalLink}>Höganäs Mat- & Sommarfest</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,7 +132,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 text-[#cc8124]">
|
<div className="flex items-center gap-3 text-[#cc8124]">
|
||||||
<FaClipboardList size={24} />
|
<FaClipboardList size={24} />
|
||||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Regler</h4>
|
<h4 className="font-beachday text-2xl uppercase tracking-wider text-black">Regler</h4>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-gray-700 text-base leading-relaxed [&_a]:text-[#39979f] [&_a]:font-bold [&_a:hover]:underline [&_i]:italic [&_em]:italic"
|
className="text-gray-700 text-base leading-relaxed [&_a]:text-[#39979f] [&_a]:font-bold [&_a:hover]:underline [&_i]:italic [&_em]:italic"
|
||||||
@@ -141,7 +141,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
|
|
||||||
<div className="flex items-center gap-3 text-[#cc8124] mt-6">
|
<div className="flex items-center gap-3 text-[#cc8124] mt-6">
|
||||||
<FaUsers size={24} />
|
<FaUsers size={24} />
|
||||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Klasser</h4>
|
<h4 className="font-beachday text-2xl uppercase tracking-wider text-black">Klasser</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-700 text-sm space-y-3">
|
<div className="text-gray-700 text-sm space-y-3">
|
||||||
<p className="leading-snug">{config.beachPage?.classesIntroText}</p>
|
<p className="leading-snug">{config.beachPage?.classesIntroText}</p>
|
||||||
@@ -166,7 +166,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
<div className="flex-1 space-y-3">
|
<div className="flex-1 space-y-3">
|
||||||
<div className="flex items-center gap-3 text-[#cc8124]">
|
<div className="flex items-center gap-3 text-[#cc8124]">
|
||||||
<FaTicketAlt size={24} />
|
<FaTicketAlt size={24} />
|
||||||
<h4 className="font-beachday text-2xl uppercase tracking-[0.05em] text-black">Anmälan & Kostnad</h4>
|
<h4 className="font-beachday text-2xl uppercase tracking-wider text-black">Anmälan & Kostnad</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-black text-green-700 uppercase tracking-wider">Kostnad: Gratis!</p>
|
<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">
|
<div className="text-gray-700 text-sm space-y-2">
|
||||||
@@ -194,7 +194,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
|||||||
<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="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">
|
<div className="flex items-center justify-center gap-4">
|
||||||
<FaTrophy className="text-[#fdb84b] shrink-0" size={28} />
|
<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">
|
<h4 className="font-beachday uppercase text-[clamp(1.1rem,4vw,1.8rem)] tracking-wider font-medium text-white text-center leading-none">
|
||||||
{config.beachPage?.prizesText || "Fina priser till alla pallplatser! 🏆"}
|
{config.beachPage?.prizesText || "Fina priser till alla pallplatser! 🏆"}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||||
import React, { useRef, useActionState, useEffect } from 'react';
|
import React, { useRef, useActionState, useEffect, useState } from 'react';
|
||||||
import { FaUser, FaEnvelope, FaPhone, FaUsers, FaTrophy, FaCheckCircle, FaExclamationTriangle } from "react-icons/fa";
|
import { FaUser, FaEnvelope, FaPhone, FaUsers, FaTrophy, FaCheckCircle, FaExclamationTriangle } from "react-icons/fa";
|
||||||
import { submitRegistration } from './actions';
|
import { submitRegistration } from './actions';
|
||||||
import { SiteConfig } from '../lib/config';
|
import { SiteConfig } from '../lib/config';
|
||||||
@@ -12,6 +12,7 @@ export default function BeachRegistrationForm({ config, turnstileSiteKey }: { co
|
|||||||
const [state, formAction, isPending] = useActionState(submitRegistration, null);
|
const [state, formAction, isPending] = useActionState(submitRegistration, null);
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const turnstileRef = useRef<TurnstileInstance>(null);
|
const turnstileRef = useRef<TurnstileInstance>(null);
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
|
||||||
const classesData = Array.isArray(config.beachPage?.classes)
|
const classesData = Array.isArray(config.beachPage?.classes)
|
||||||
? config.beachPage.classes
|
? config.beachPage.classes
|
||||||
@@ -22,13 +23,13 @@ export default function BeachRegistrationForm({ config, turnstileSiteKey }: { co
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Rensa endast vid lyckad inskickning
|
|
||||||
if (state?.success) {
|
if (state?.success) {
|
||||||
formRef.current?.reset();
|
formRef.current?.reset();
|
||||||
turnstileRef.current?.reset();
|
turnstileRef.current?.reset();
|
||||||
|
setToken(null);
|
||||||
} else if (state?.success === false) {
|
} else if (state?.success === false) {
|
||||||
// Återställ Turnstile så de kan försöka igen vid fel
|
|
||||||
turnstileRef.current?.reset();
|
turnstileRef.current?.reset();
|
||||||
|
setToken(null);
|
||||||
}
|
}
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
@@ -131,7 +132,15 @@ export default function BeachRegistrationForm({ config, turnstileSiteKey }: { co
|
|||||||
|
|
||||||
<input type="text" name="website" className="hidden" tabIndex={-1} />
|
<input type="text" name="website" className="hidden" tabIndex={-1} />
|
||||||
|
|
||||||
<div className="flex justify-center py-2"><Turnstile ref={turnstileRef} siteKey={turnstileSiteKey} options={{ theme: 'light' }} /></div>
|
<div className="flex justify-center py-2">
|
||||||
|
<Turnstile
|
||||||
|
ref={turnstileRef}
|
||||||
|
siteKey={turnstileSiteKey}
|
||||||
|
onSuccess={(token) => setToken(token)}
|
||||||
|
onExpire={() => setToken(null)}
|
||||||
|
options={{ theme: 'light' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{state && (
|
{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'}`}>
|
<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'}`}>
|
||||||
@@ -142,7 +151,7 @@ export default function BeachRegistrationForm({ config, turnstileSiteKey }: { co
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isPending}
|
disabled={isPending || !token}
|
||||||
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"
|
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'}
|
{isPending ? 'Skickar...' : 'Skicka anmälan'}
|
||||||
|
|||||||
+109
-42
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
|
|
||||||
export async function submitRegistration(prevState: unknown, formData: FormData) {
|
export async function submitRegistration(prevState: unknown, formData: FormData) {
|
||||||
// 1. Spara all rådata direkt för att kunna skicka tillbaka den vid fel
|
// 1. Spara all rådata direkt för att kunna skicka tillbaka den vid fel
|
||||||
const rawData = {
|
const rawData = {
|
||||||
@@ -21,9 +19,9 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
|||||||
return { success: false, message: 'Spam detekterat.', values: rawData };
|
return { success: false, message: 'Spam detekterat.', values: rawData };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Hämta Turnstile Token och Secret (MATCHING .env NAME)
|
// 2. Hämta Turnstile Token och Secret
|
||||||
const turnstileToken = formData.get('cf-turnstile-response');
|
const turnstileToken = formData.get('cf-turnstile-response');
|
||||||
const secretKey = process.env.TURNSTILE_SECRET; // Matchar din .env exakt
|
const secretKey = process.env.TURNSTILE_SECRET;
|
||||||
|
|
||||||
if (!secretKey) {
|
if (!secretKey) {
|
||||||
console.error("Saknar TURNSTILE_SECRET i miljön.");
|
console.error("Saknar TURNSTILE_SECRET i miljön.");
|
||||||
@@ -41,7 +39,7 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
|||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
secret: secretKey,
|
secret: secretKey,
|
||||||
response: turnstileToken?.toString() || '',
|
response: String(turnstileToken || ''),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Säkerhetsverifieringen misslyckades. Ladda om sidan och försök igen.',
|
message: 'Säkerhetsverifieringen misslyckades. Ladda om sidan och försök igen.',
|
||||||
values: rawData // Returnerar värdena så användaren slipper skriva om dem
|
values: rawData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -73,37 +71,97 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
|||||||
? `+46 (0)${cleanTel.substring(0, 2)}-${cleanTel.substring(2, 5)} ${cleanTel.substring(5, 7)} ${cleanTel.substring(7, 9)}`
|
? `+46 (0)${cleanTel.substring(0, 2)}-${cleanTel.substring(2, 5)} ${cleanTel.substring(5, 7)} ${cleanTel.substring(7, 9)}`
|
||||||
: rawData.tel;
|
: rawData.tel;
|
||||||
|
|
||||||
// 5. Nodemailer setup
|
// 5. Stalwart JMAP Setup
|
||||||
const transporter = nodemailer.createTransport({
|
const jmapUrl = process.env.STALWART_JMAP_URL;
|
||||||
host: process.env.SMTP_HOST || 'localhost',
|
const jmapToken = process.env.STALWART_API_TOKEN;
|
||||||
port: 25,
|
const accountId = process.env.STALWART_ACCOUNT_ID;
|
||||||
secure: false,
|
|
||||||
tls: { rejectUnauthorized: false }
|
if (!jmapUrl || !jmapToken || !accountId) {
|
||||||
|
console.error("Saknar JMAP-konfiguration i miljön.");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'E-postserver är inte konfigurerad. Kontakta administratör.',
|
||||||
|
values: rawData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hjälpfunktion för att bygga och skicka via Stalwart JMAP
|
||||||
|
async function sendMailJMAP(toEmail: string, replyToEmail: string, subject: string, htmlContent: string, fromName: string) {
|
||||||
|
const payload = {
|
||||||
|
using: [
|
||||||
|
"urn:ietf:params:jmap:core",
|
||||||
|
"urn:ietf:params:jmap:mail",
|
||||||
|
"urn:ietf:params:jmap:submission"
|
||||||
|
],
|
||||||
|
methodCalls: [
|
||||||
|
[
|
||||||
|
"Email/set",
|
||||||
|
{
|
||||||
|
accountId: accountId,
|
||||||
|
create: {
|
||||||
|
"draft-1": {
|
||||||
|
from: [{ email: accountId, name: fromName }],
|
||||||
|
to: [{ email: toEmail }],
|
||||||
|
replyTo: [{ email: replyToEmail }],
|
||||||
|
subject: subject,
|
||||||
|
bodyValues: {
|
||||||
|
"body-html": {
|
||||||
|
value: htmlContent,
|
||||||
|
isTruncated: false,
|
||||||
|
isEncodingProblem: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
htmlBody: [{ partId: "body-html", type: "text/html" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"EmailSubmission/set",
|
||||||
|
{
|
||||||
|
accountId: accountId,
|
||||||
|
create: {
|
||||||
|
"sub-1": {
|
||||||
|
emailId: "#draft-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(jmapUrl as string, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${jmapToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`JMAP API Error: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Skapa och skicka mail
|
||||||
try {
|
try {
|
||||||
const dateStr = new Date().toLocaleString('sv-SE', {
|
const dateStr = new Date().toLocaleString('sv-SE', {
|
||||||
timeZone: 'Europe/Stockholm',
|
timeZone: 'Europe/Stockholm',
|
||||||
year: 'numeric',
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
month: '2-digit',
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
});
|
});
|
||||||
const cleanMessage = rawData.message ? rawData.message.replace(/[\r\n;]/g, " ") : "";
|
const cleanMessage = rawData.message ? rawData.message.replace(/[\r\n;]/g, " ") : "";
|
||||||
|
|
||||||
// Mail till klubben
|
// HTML för mail till klubben
|
||||||
await transporter.sendMail({
|
const mailTillKlubbHtml = `
|
||||||
from: `"Beach-anmälan: ${rawData.name}" <no-reply@lerbergetsvolleyboll.se>`,
|
|
||||||
to: 'beach@lerbergetsvolleyboll.se',
|
|
||||||
replyTo: rawData.email,
|
|
||||||
subject: `Anmälan: ${rawData.team}`,
|
|
||||||
html: `
|
|
||||||
<html>
|
<html>
|
||||||
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; padding: 20px;'>
|
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; padding: 20px;'>
|
||||||
<h2 style='color: #39979f;'>Beach-anmälan</h2>
|
<h2 style='color: #39979f;'>Beach-anmälan</h2>
|
||||||
|
|
||||||
<table style='width: 100%; max-width: 600px; border-collapse: collapse;'>
|
<table style='width: 100%; max-width: 600px; border-collapse: collapse;'>
|
||||||
<tr><td style='padding: 5px; font-weight: bold; width: 150px;'>Lagnamn:</td><td>${rawData.team}</td></tr>
|
<tr><td style='padding: 5px; font-weight: bold; width: 150px;'>Lagnamn:</td><td>${rawData.team}</td></tr>
|
||||||
<tr><td style='padding: 5px; font-weight: bold;'>Kontakt:</td><td>${rawData.name}</td></tr>
|
<tr><td style='padding: 5px; font-weight: bold;'>Kontakt:</td><td>${rawData.name}</td></tr>
|
||||||
@@ -113,14 +171,11 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
|||||||
<tr><td style='padding: 5px; font-weight: bold;'>Nivå:</td><td>${rawData.level}</td></tr>
|
<tr><td style='padding: 5px; font-weight: bold;'>Nivå:</td><td>${rawData.level}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<p><strong>Meddelande:</strong><br>${rawData.message ? rawData.message.replace(/\n/g, '<br>') : '<em>Inget meddelande</em>'}</p>
|
<p><strong>Meddelande:</strong><br>${rawData.message ? rawData.message.replace(/\n/g, '<br>') : '<em>Inget meddelande</em>'}</p>
|
||||||
|
|
||||||
<hr style='border: none; border-top: 1px solid #ddd; margin: 30px 0;'>
|
<hr style='border: none; border-top: 1px solid #ddd; margin: 30px 0;'>
|
||||||
|
|
||||||
<details style="margin-top: 20px;">
|
<details style="margin-top: 20px;">
|
||||||
<summary style="background-color: #fdb84b; color: #fff; padding: 12px 20px; border-radius: 8px; font-weight: bold; cursor: pointer; display: inline-block; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
|
<summary style="background-color: #fdb84b; color: #fff; padding: 12px 20px; border-radius: 8px; font-weight: bold; cursor: pointer; display: inline-block; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
|
||||||
Visa kopierbar tabell för Excel
|
Visa kopierbar tabell för Excel
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div style="margin-top: 15px; overflow-x: auto;">
|
<div style="margin-top: 15px; overflow-x: auto;">
|
||||||
<table border='1' style='border-collapse: collapse; font-family: Calibri, sans-serif; font-size: 11pt; width: 100%; text-align: left;'>
|
<table border='1' style='border-collapse: collapse; font-family: Calibri, sans-serif; font-size: 11pt; width: 100%; text-align: left;'>
|
||||||
<tr style="background-color: #f9f9f9;">
|
<tr style="background-color: #f9f9f9;">
|
||||||
@@ -138,26 +193,38 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
|||||||
</details>
|
</details>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`
|
`;
|
||||||
});
|
|
||||||
|
|
||||||
// Bekräftelse till användaren
|
// HTML för bekräftelse till användaren
|
||||||
await transporter.sendMail({
|
const mailTillAnvandareHtml = `
|
||||||
from: `"LVS" <no-reply@lerbergetsvolleyboll.se>`,
|
<div style="font-family: sans-serif; padding: 20px; color: #333;">
|
||||||
to: rawData.email,
|
|
||||||
replyTo: `"LVS Beach" <beach@lerbergetsvolleyboll.se>`,
|
|
||||||
subject: `Vi har tagit emot din anmälan för ${rawData.team}`,
|
|
||||||
html: `<div style="font-family: sans-serif; padding: 20px; color: #333;">
|
|
||||||
<h2 style="color: #39979f;">Hej ${rawData.name}!</h2>
|
<h2 style="color: #39979f;">Hej ${rawData.name}!</h2>
|
||||||
<p>Tack för din anmälan till beachturneringen. Vi återkommer så snart vi har granskat era uppgifter.</p>
|
<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 style="color: #fdb84b;">Lerbergets Volleybollsällskap</strong></p>
|
<p>Ses på stranden!<br><strong style="color: #fdb84b;">Lerbergets Volleybollsällskap</strong></p>
|
||||||
</div>`
|
</div>
|
||||||
});
|
`;
|
||||||
|
|
||||||
|
// Skicka mailen parallellt för prestanda (frivilligt, men oftast bättre än sekventiellt)
|
||||||
|
await Promise.all([
|
||||||
|
sendMailJMAP(
|
||||||
|
'beach@lerbergetsvolleyboll.se', // To
|
||||||
|
rawData.email, // ReplyTo
|
||||||
|
`Anmälan: ${rawData.team}`, // Subject
|
||||||
|
mailTillKlubbHtml, // HTML Content
|
||||||
|
`Beach-anmälan: ${rawData.name}` // From Name
|
||||||
|
),
|
||||||
|
sendMailJMAP(
|
||||||
|
rawData.email, // To
|
||||||
|
'beach@lerbergetsvolleyboll.se', // ReplyTo
|
||||||
|
`Vi har tagit emot din anmälan för ${rawData.team}`, // Subject
|
||||||
|
mailTillAnvandareHtml, // HTML Content
|
||||||
|
"LVS" // From Name
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
// 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.' };
|
return { success: true, message: 'Tack! Din anmälan har skickats. Håll utkik i din inkorg efter bekräftelse.' };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Nodemailer error:", e);
|
console.error("JMAP Fetch error:", e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Kunde inte skicka mail. Kontakta oss manuellt istället.',
|
message: 'Kunde inte skicka mail. Kontakta oss manuellt istället.',
|
||||||
|
|||||||
+76
-5
@@ -1,17 +1,88 @@
|
|||||||
// app/beach/page.tsx
|
// app/beach/page.tsx
|
||||||
|
import { Metadata } from 'next';
|
||||||
import { getConfig } from '../lib/config';
|
import { getConfig } from '../lib/config';
|
||||||
import BeachClientPage from './BeachClientPage';
|
import BeachClientPage from './BeachClientPage';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export const metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: 'Beachturnering',
|
const config = await getConfig();
|
||||||
};
|
const matchDate = new Date(config.beachPage.matchStart);
|
||||||
|
const year = matchDate.getFullYear();
|
||||||
|
const dateFormatted = matchDate.toLocaleDateString('sv-SE', { day: 'numeric', month: 'long' });
|
||||||
|
|
||||||
|
const isClosed = config.beachPage.isClosedOverride;
|
||||||
|
const title = isClosed
|
||||||
|
? `Höganäs Beach Open ${year} (Fulltecknat)`
|
||||||
|
: `Anmälan: Höganäs Beach Open ${year} - ${dateFormatted}`;
|
||||||
|
|
||||||
|
const description = isClosed
|
||||||
|
? `Turneringen den ${dateFormatted} är nu fullbokad. Välkommen ner till Kvickbadet för att titta på beachvolleyboll i toppklass!`
|
||||||
|
: `Välkommen till Höganäs Beach Open den ${dateFormatted}! Turneringsinfo, regler och anmälan för beachvolleyboll vid Kvickbadet.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
openGraph: {
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
url: 'https://lerbergetsvolleyboll.se/beach',
|
||||||
|
siteName: 'Lerbergets Volleybollsällskap',
|
||||||
|
locale: 'sv_SE',
|
||||||
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/images/beach-tournament.webp',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: `Höganäs Beach Open ${year}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
images: ['/images/beach-tournament.webp'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function BeachPage() {
|
export default async function BeachPage() {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || '';
|
const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || '';
|
||||||
|
|
||||||
return <BeachClientPage config={config} turnstileSiteKey={turnstileSiteKey} />;
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Event',
|
||||||
|
'name': 'Höganäs Beach Open',
|
||||||
|
'startDate': config.beachPage.matchStart,
|
||||||
|
'location': {
|
||||||
|
'@type': 'Place',
|
||||||
|
'name': 'Kvickbadet',
|
||||||
|
'address': {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
'addressLocality': 'Höganäs',
|
||||||
|
'addressCountry': 'SE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'eventStatus': config.beachPage.isClosedOverride
|
||||||
|
? 'https://schema.org/EventScheduled'
|
||||||
|
: 'https://schema.org/EventScheduled',
|
||||||
|
'organizer': {
|
||||||
|
'@type': 'Organization',
|
||||||
|
'name': 'Lerbergets Volleybollsällskap',
|
||||||
|
'url': 'https://lerbergetsvolleyboll.se'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
<BeachClientPage config={config} turnstileSiteKey={turnstileSiteKey} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -9,14 +9,14 @@ export default function BeachPromo() {
|
|||||||
<div className="p-2.5">
|
<div className="p-2.5">
|
||||||
<h2 className="font-beachday text-black text-center text-[clamp(1.2rem,6vw,2.25rem)] uppercase tracking-tight flex items-baseline justify-center flex-wrap gap-x-3 md:gap-x-5">
|
<h2 className="font-beachday text-black text-center text-[clamp(1.2rem,6vw,2.25rem)] uppercase tracking-tight flex items-baseline justify-center flex-wrap gap-x-3 md:gap-x-5">
|
||||||
<span className="whitespace-nowrap transition-all duration-300 group-hover/promo:opacity-80">
|
<span className="whitespace-nowrap transition-all duration-300 group-hover/promo:opacity-80">
|
||||||
Kolla in vår
|
Anmäl dig till
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href="/beach"
|
href="/beach"
|
||||||
className="relative text-[#406185] transition-all duration-300 inline-flex items-center gap-3 group/link hover:tracking-wider hover:-translate-y-1"
|
className="relative text-[#406185] transition-all duration-300 inline-flex items-center gap-3 group/link hover:tracking-wider hover:-translate-y-1"
|
||||||
>
|
>
|
||||||
<span className="relative">
|
<span className="relative">
|
||||||
Beachturnering
|
Höganäs Beach Open
|
||||||
<span className="absolute -bottom-1 left-0 w-full h-1 bg-[#406185] transform scale-x-0 transition-transform duration-300 origin-left group-hover/link:scale-x-100" />
|
<span className="absolute -bottom-1 left-0 w-full h-1 bg-[#406185] transform scale-x-0 transition-transform duration-300 origin-left group-hover/link:scale-x-100" />
|
||||||
</span>
|
</span>
|
||||||
<FaVolleyballBall
|
<FaVolleyballBall
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default function Header() {
|
|||||||
src={logo}
|
src={logo}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
alt="Lerbergets Volleybollsällskap"
|
alt="Lerbergets Volleybollsällskap"
|
||||||
className="max-h-24 min-h-16 my-4 md:max-h-32 md:min-h-24 md:my-6 mx-auto block transition-all duration-500 ease-out group-hover:scale-105 group-active:scale-95"
|
className="w-auto max-h-24 min-h-16 my-4 md:max-h-32 md:min-h-24 md:my-6 mx-auto block transition-all duration-500 ease-out group-hover:scale-105 group-active:scale-95"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { FaFacebookSquare, FaInstagram } from "react-icons/fa";
|
|||||||
import { IoIosMail } from "react-icons/io";
|
import { IoIosMail } from "react-icons/io";
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import player from "../../public/images/player.svg";
|
import player from "../../public/images/player.svg";
|
||||||
import laget from "../../public/images/laget.se.svg";
|
import svenskalag from "../../public/images/svenskalag.se.png";
|
||||||
|
|
||||||
export default function HomeClientContent() {
|
export default function HomeClientContent() {
|
||||||
const [activeModal, setActiveModal] = useState<'member' | 'subscribe' | null>(null);
|
const [activeModal, setActiveModal] = useState<'member' | 'subscribe' | null>(null);
|
||||||
@@ -25,21 +25,24 @@ export default function HomeClientContent() {
|
|||||||
<div className="flex flex-col md:flex-row max-w-300 my-12.5 mx-5 w-full">
|
<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="flex flex-col justify-center flex-1">
|
||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
<h1 className="uppercase text-[2.5rem] font-bold leading-[1.1] mb-2.5">LVS</h1>
|
{/* Combine the abbreviation and full name for the H1 */}
|
||||||
<div className="bg-white w-full h-1.5"></div>
|
<h1 className="uppercase text-[2.5rem] font-bold leading-[1.1] mb-2.5">
|
||||||
|
LVS <span className="sr-only">Lerbergets Volleybollsällskap</span>
|
||||||
|
</h1>
|
||||||
|
<div className="bg-white w-full h-1.5" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="italic text-xl my-2.5 font-bold">Lerbergets Volleybollsällskap</h4>
|
<h2 className="italic text-xl my-2.5 font-bold">Lerbergets Volleybollsällskap</h2>
|
||||||
<p className="max-w-200 leading-[1.1]">
|
<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 på sommaren spelas det beachvolley
|
Klubben startade den 1 mars 2018 och har i snabb takt utvecklats med både <strong>kidsvolley</strong>, <strong>4-mannavolley</strong> och träning för alla nivåer. På sommaren flyttar vi ner till stranden för <strong>beachvolleyboll</strong> i Höganäs.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-2.5 mt-10">
|
<div className="flex flex-col gap-2.5 mt-10">
|
||||||
<HeroButton onClick={() => setActiveModal('member')}>Bli Medlem</HeroButton>
|
<HeroButton onClick={() => setActiveModal('member')}>Bli Medlem</HeroButton>
|
||||||
<HeroButton href="#traningstider">Träningstider</HeroButton>
|
<HeroButton href="#traningstider">Träningstider</HeroButton>
|
||||||
<HeroButton href="https://www.basesport.se/category/lerbergets-vbk">Klubbkollektion</HeroButton>
|
<HeroButton href="https://www.basesport.se/category/lerbergets-vbs">Klubbkollektion</HeroButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:flex md:relative w-64 md:w-75 player-animation">
|
<div className="hidden md:flex md:relative w-64 md:w-75 player-animation">
|
||||||
<Image src={player} alt="Spelare" className="w-full h-auto" />
|
<Image src={player} alt="Spelare" className="w-full h-auto" loading='eager' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -49,9 +52,9 @@ export default function HomeClientContent() {
|
|||||||
<div className="flex flex-col items-center w-full my-7.5 gap-2">
|
<div className="flex flex-col items-center w-full my-7.5 gap-2">
|
||||||
<InfoCard
|
<InfoCard
|
||||||
title="Träningstider"
|
title="Träningstider"
|
||||||
description="Här hittar du alltid uppdaterade träningstider från Laget.se"
|
description="Här hittar du alltid uppdaterade träningstider från Svenskalag.se"
|
||||||
actionLabel="Se nästa träningstid"
|
actionLabel="Se nästa träningstid"
|
||||||
href="https://www.laget.se/LVS/Event/Month"
|
href="https://www.svenskalag.se/lerbergetsvs/kalender/"
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
title="Prenumerera på träningstider"
|
title="Prenumerera på träningstider"
|
||||||
@@ -67,23 +70,23 @@ export default function HomeClientContent() {
|
|||||||
|
|
||||||
{/* Sociala Medier Ikoner */}
|
{/* Sociala Medier Ikoner */}
|
||||||
<div className="flex flex-wrap justify-center items-center gap-10">
|
<div className="flex flex-wrap justify-center items-center gap-10">
|
||||||
<SocialIcon title="Instagram" href="https://www.instagram.com/lerbergets_volleyboll/">
|
<SocialIcon title="Instagram" href="https://www.instagram.com/lerbergets_volleyboll/" aria-label="Följ LVS på Instagram">
|
||||||
<FaInstagram className="h-20 w-20" />
|
<FaInstagram className="h-20 w-20" />
|
||||||
</SocialIcon>
|
</SocialIcon>
|
||||||
|
|
||||||
<SocialIcon title="Facebook" href="https://www.facebook.com/lerbergetsvolleyboll">
|
<SocialIcon title="Facebook" href="https://www.facebook.com/lerbergetsvolleyboll" aria-label="Följ LVS på Facebook">
|
||||||
<FaFacebookSquare className="h-20 w-20" />
|
<FaFacebookSquare className="h-20 w-20" />
|
||||||
</SocialIcon>
|
</SocialIcon>
|
||||||
|
|
||||||
<SocialIcon title="Laget.se" href="https://www.laget.se/LVS">
|
<SocialIcon title="Svenskalag.se" href="https://www.svenskalag.se/lerbergetsvs" aria-label="Kolla träningstider på Svenskalag.se">
|
||||||
<Image src={laget} alt="Laget.se" className="h-18 w-18" />
|
<Image src={svenskalag} alt="Svenskalag.se" className="h-18 w-18" />
|
||||||
</SocialIcon>
|
</SocialIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kontaktinformation */}
|
{/* Kontaktinformation */}
|
||||||
<div className="text-center p-2.5 flex flex-col justify-center items-center">
|
<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] mb-4">Kontakta oss</h3>
|
<h3 className="uppercase font-bold text-[1.875rem] leading-[1.2] mb-4">Kontakta oss</h3>
|
||||||
<p className="text-[1rem] leading-[1.6] opacity-90">Vi finns på Instagram, Facebook och laget.se</p>
|
<p className="text-[1rem] leading-[1.6] opacity-90">Vi finns på Instagram, Facebook och Svenskalag.se</p>
|
||||||
|
|
||||||
{/* Klickbar Adress - Länkar till Google Maps */}
|
{/* Klickbar Adress - Länkar till Google Maps */}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function MemberModal({ onClose }: ModalProps) {
|
|||||||
<h2 className="text-[#406185] my-2.5 text-2xl font-bold uppercase">Registrera dig</h2>
|
<h2 className="text-[#406185] my-2.5 text-2xl font-bold uppercase">Registrera dig</h2>
|
||||||
<p className="text-[#406185] my-2.5">Trycka på knappen nedan för att starta registreringen. Du kommer att skickas till laget.se</p>
|
<p className="text-[#406185] my-2.5">Trycka på knappen nedan för att starta registreringen. Du kommer att skickas till laget.se</p>
|
||||||
|
|
||||||
<ModalAction href="https://www.laget.se/LVS/Member" className="mt-5">
|
<ModalAction href="https://www.svenskalag.se/lerbergetsvs/formular/borja-trana-i-lerbergets-volleybollsallskap/38396" className="mt-5">
|
||||||
Bli medlem
|
Bli medlem
|
||||||
</ModalAction>
|
</ModalAction>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ export default function SubscribeModal({ onClose }: ModalProps) {
|
|||||||
<p className="text-[#406185] my-2.5">Välj vilken LEVEL du vill prenumerera på.</p>
|
<p className="text-[#406185] my-2.5">Välj vilken LEVEL du vill prenumerera på.</p>
|
||||||
|
|
||||||
<div className="flex flex-row flex-wrap justify-center gap-6.25 mt-5 max-w-162.5">
|
<div className="flex flex-row flex-wrap justify-center gap-6.25 mt-5 max-w-162.5">
|
||||||
<ModalAction href="webcal://cal.laget.se/lvs-level-5-6.ics">Level 5-6</ModalAction>
|
<ModalAction href="webcal://cal.svenskalag.se/120351">Level 4-6</ModalAction>
|
||||||
<ModalAction href="webcal://cal.laget.se/lvs-level-7-8.ics">Level 7-8</ModalAction>
|
<ModalAction href="webcal://cal.svenskalag.se/120353">Level 7-9</ModalAction>
|
||||||
<ModalAction href="webcal://cal.laget.se/lvs-level-8-9.ics">Level 8-9</ModalAction>
|
<ModalAction href="webcal://cal.svenskalag.se/120355">Level 10</ModalAction>
|
||||||
<ModalAction href="webcal://cal.laget.se/lvs-level-10.ics">Level 10</ModalAction>
|
<ModalAction href="webcal://cal.svenskalag.se/120187">Hela klubben</ModalAction>
|
||||||
<ModalAction href="webcal://cal.laget.se/LVS.ics">Hela klubben</ModalAction>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-2
@@ -10,11 +10,12 @@ const montserrat = Montserrat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL('https://lerbergetsvolleyboll.se'),
|
||||||
title: {
|
title: {
|
||||||
template: '%s | LVS',
|
template: '%s | LVS',
|
||||||
default: 'Lerbergets Volleybollsällskap',
|
default: 'LVS - Lerbergets Volleybollsällskap',
|
||||||
},
|
},
|
||||||
description: 'Klubben startade den 1 mars 2018 och har i snabb takt utvecklats...',
|
description: 'Volleybollklubben i Höganäs för barn, ungdomar och vuxna.',
|
||||||
icons: {
|
icons: {
|
||||||
icon: '/images/favicon.svg',
|
icon: '/images/favicon.svg',
|
||||||
shortcut: '/favicon.svg',
|
shortcut: '/favicon.svg',
|
||||||
|
|||||||
+48
-3
@@ -1,21 +1,66 @@
|
|||||||
// app/page.tsx
|
// app/page.tsx
|
||||||
|
import { Metadata } from 'next';
|
||||||
import BeachPromo from './components/BeachPromo';
|
import BeachPromo from './components/BeachPromo';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import HomeClientContent from './components/HomeClientContent';
|
import HomeClientContent from './components/HomeClientContent';
|
||||||
import { getConfig } from './lib/config';
|
import { getConfig } from './lib/config';
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const beachSuffix = config.showBeachPromo ? ' Just nu: Anmälan öppen till Höganäs Beach Open!' : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'LVS – Volleyboll för alla i Höganäs',
|
||||||
|
description: `Välkommen till Lerbergets Volleybollsällskap. Vi erbjuder kidsvolley och inomhusvolleyboll för alla åldrar i Höganäs.${beachSuffix}`,
|
||||||
|
openGraph: {
|
||||||
|
title: 'LVS | Lerbergets Volleybollsällskap',
|
||||||
|
description: 'Häng med och spela volleyboll med oss i Höganäs!',
|
||||||
|
url: 'https://lerbergetsvolleyboll.se',
|
||||||
|
siteName: 'Lerbergets Volleybollsällskap',
|
||||||
|
locale: 'sv_SE',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'SportsOrganization',
|
||||||
|
'name': 'Lerbergets Volleybollsällskap',
|
||||||
|
'alternateName': 'LVS',
|
||||||
|
'url': 'https://lerbergetsvolleyboll.se',
|
||||||
|
'logo': 'https://lerbergetsvolleyboll.se/images/favicon.svg',
|
||||||
|
'address': {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
'streetAddress': 'Friluftsvägen 14',
|
||||||
|
'addressLocality': 'Höganäs',
|
||||||
|
'postalCode': '263 54',
|
||||||
|
'addressCountry': 'SE',
|
||||||
|
},
|
||||||
|
'sameAs': [
|
||||||
|
'https://www.instagram.com/lerbergets_volleyboll/',
|
||||||
|
'https://www.facebook.com/lerbergetsvolleyboll',
|
||||||
|
'https://www.laget.se/LVS'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
<main>
|
<main>
|
||||||
<Header />
|
<Header />
|
||||||
{config.showBeachPromo && (
|
{config.showBeachPromo && <BeachPromo />}
|
||||||
<BeachPromo />
|
|
||||||
)}
|
|
||||||
<HomeClientContent />
|
<HomeClientContent />
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,3 +3,5 @@ TURNSTILE_SECRET=1x0000000000000000000000000000000AA
|
|||||||
|
|
||||||
ADMIN_PASSWORD=PASSWORD
|
ADMIN_PASSWORD=PASSWORD
|
||||||
SMTP_HOST=smtp.example.com
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_USER=USER
|
||||||
|
SMTP_PASS=PASSWORD
|
||||||
|
|||||||
Generated
+861
-702
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -11,7 +11,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marsidev/react-turnstile": "^1.4.2",
|
"@marsidev/react-turnstile": "^1.4.2",
|
||||||
"next": "^16",
|
"next": "^16",
|
||||||
"nodemailer": "^8.0.1",
|
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -27,5 +26,9 @@
|
|||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"sharp@0.34.5": true,
|
||||||
|
"unrs-resolver@1.12.2": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Reference in New Issue
Block a user