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,7 +61,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
||||
<Image
|
||||
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}
|
||||
alt="Mat- & Sommarfesten"
|
||||
alt="Mat- och Sommarfesten Höganäs Logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function BeachClientPage({ config, turnstileSiteKey }: { config:
|
||||
<p className="font-bold">{timeInfo.dateStr}</p>
|
||||
<p>{timeInfo.matchStr}</p>
|
||||
<p className="text-sm italic pt-3 border-t border-blue-100 mt-2">
|
||||
Samtidigt som <a target="_blank" rel="noreferrer" className="text-[#39979f] hover:underline" href={config.beachPage.festivalLink}>Höganäs Mat- & Sommarfest</a>
|
||||
Samtidigt som <a target="_blank" rel="noopener noreferrer" className="text-[#39979f] hover:underline" href={config.beachPage.festivalLink}>Höganäs Mat- & Sommarfest</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+179
-47
@@ -2,8 +2,6 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export async function submitRegistration(prevState: unknown, formData: FormData) {
|
||||
// 1. Spara all rådata direkt för att kunna skicka tillbaka den vid fel
|
||||
const rawData = {
|
||||
@@ -21,9 +19,9 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
||||
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 secretKey = process.env.TURNSTILE_SECRET; // Matchar din .env exakt
|
||||
const secretKey = process.env.TURNSTILE_SECRET;
|
||||
|
||||
if (!secretKey) {
|
||||
console.error("Saknar TURNSTILE_SECRET i miljön.");
|
||||
@@ -73,43 +71,166 @@ 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)}`
|
||||
: rawData.tel;
|
||||
|
||||
// 5. Nodemailer setup
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
// 5. Stalwart JMAP Setup
|
||||
const jmapUrl = process.env.STALWART_JMAP_URL;
|
||||
const jmapToken = process.env.STALWART_API_TOKEN;
|
||||
const accountId = process.env.STALWART_ACCOUNT_ID;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
async function sendMailJMAP(toEmail: string, replyToEmail: string, subject: string, htmlContent: string, fromName: string, fromEmail: string) {
|
||||
// 1. Hämta Mapp-ID (Drafts/Utkast) och Identitets-ID från Stalwart
|
||||
const setupPayload = {
|
||||
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
|
||||
methodCalls: [
|
||||
["Mailbox/query", { accountId: accountId, filter: { role: "drafts" } }, "getMailbox"],
|
||||
["Identity/get", { accountId: accountId }, "getIdentity"]
|
||||
]
|
||||
};
|
||||
|
||||
const setupRes = await fetch(jmapUrl as string, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jmapToken}` },
|
||||
body: JSON.stringify(setupPayload)
|
||||
});
|
||||
|
||||
const setupData = await setupRes.json();
|
||||
const mailboxRes = setupData.methodResponses.find((r: any) => r[0] === "Mailbox/query");
|
||||
const identityRes = setupData.methodResponses.find((r: any) => r[0] === "Identity/get");
|
||||
|
||||
let mailboxId = mailboxRes && mailboxRes[1].ids && mailboxRes[1].ids.length > 0 ? mailboxRes[1].ids[0] : null;
|
||||
|
||||
// ---- NY LOGIK HÄR: Hitta rätt identitet baserat på fromEmail ----
|
||||
let identityId = null;
|
||||
let actualFromEmail = fromEmail;
|
||||
|
||||
if (identityRes && identityRes[1].list) {
|
||||
// Leta efter identiteten som matchar adressen vi vill skicka från
|
||||
const matchedIdentity = identityRes[1].list.find((i: any) => i.email === fromEmail);
|
||||
|
||||
if (matchedIdentity) {
|
||||
identityId = matchedIdentity.id;
|
||||
} else if (identityRes[1].list.length > 0) {
|
||||
// Fallback om den exakta adressen inte fanns i listan
|
||||
identityId = identityRes[1].list[0].id;
|
||||
actualFromEmail = identityRes[1].list[0].email;
|
||||
}
|
||||
}
|
||||
|
||||
// Om ingen mapp med rollen "drafts" finns, hämta första bästa mapp
|
||||
if (!mailboxId) {
|
||||
const fallbackRes = await fetch(jmapUrl as string, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jmapToken}` },
|
||||
body: JSON.stringify({
|
||||
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
|
||||
methodCalls: [["Mailbox/query", { accountId: accountId }, "0"]]
|
||||
})
|
||||
});
|
||||
const fallbackData = await fallbackRes.json();
|
||||
mailboxId = fallbackData.methodResponses[0][1].ids[0];
|
||||
}
|
||||
|
||||
if (!mailboxId || !identityId) {
|
||||
throw new Error("Kunde inte hitta mailboxId eller identityId i Stalwart.");
|
||||
}
|
||||
|
||||
// 2. Skapa och skicka mailet
|
||||
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": {
|
||||
mailboxIds: { [mailboxId as string]: true },
|
||||
from: [{ email: actualFromEmail, name: fromName }], // Nu använder vi rätt adress
|
||||
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",
|
||||
identityId: identityId // Nu använder vi rätt ID
|
||||
}
|
||||
}
|
||||
},
|
||||
"1"
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
const res = await fetch(jmapUrl as string, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jmapToken}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const hasErrors = data.methodResponses.some(
|
||||
(response: any) =>
|
||||
response[0] === "error" ||
|
||||
(response[1] && response[1].notCreated)
|
||||
);
|
||||
|
||||
if (hasErrors) {
|
||||
console.error("--- JMAP METHOD ERROR ---");
|
||||
console.error(JSON.stringify(data.methodResponses, null, 2));
|
||||
console.error("-------------------------");
|
||||
throw new Error("JMAP accepterade anropet, men vägrade skapa/skicka mailet.");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 6. Skapa och skicka mail
|
||||
try {
|
||||
const dateStr = new Date().toLocaleString('sv-SE', {
|
||||
timeZone: 'Europe/Stockholm',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
});
|
||||
const cleanMessage = rawData.message ? rawData.message.replace(/[\r\n;]/g, " ") : "";
|
||||
|
||||
// Mail till klubben
|
||||
await transporter.sendMail({
|
||||
from: `"Beach-anmälan: ${rawData.name}" <${process.env.SMTP_USER}>`,
|
||||
to: 'beach@lerbergetsvolleyboll.se',
|
||||
replyTo: rawData.email,
|
||||
subject: `Anmälan: ${rawData.team}`,
|
||||
html: `
|
||||
// HTML för mail till klubben
|
||||
const mailTillKlubbHtml = `
|
||||
<html>
|
||||
<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>
|
||||
|
||||
<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;'>Kontakt:</td><td>${rawData.name}</td></tr>
|
||||
@@ -119,14 +240,11 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
||||
<tr><td style='padding: 5px; font-weight: bold;'>Nivå:</td><td>${rawData.level}</td></tr>
|
||||
</table>
|
||||
<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;'>
|
||||
|
||||
<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;">
|
||||
Visa kopierbar tabell för Excel
|
||||
</summary>
|
||||
|
||||
<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;'>
|
||||
<tr style="background-color: #f9f9f9;">
|
||||
@@ -144,26 +262,40 @@ export async function submitRegistration(prevState: unknown, formData: FormData)
|
||||
</details>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
});
|
||||
`;
|
||||
|
||||
// Bekräftelse till användaren
|
||||
await transporter.sendMail({
|
||||
from: `"LVS" <${process.env.SMTP_USER}>`,
|
||||
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;">
|
||||
// HTML för bekräftelse till användaren
|
||||
const mailTillAnvandareHtml = `
|
||||
<div style="font-family: sans-serif; padding: 20px; color: #333;">
|
||||
<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>Ses på stranden!<br><strong style="color: #fdb84b;">Lerbergets Volleybollsällskap</strong></p>
|
||||
</div>`
|
||||
});
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Skicka mailen parallellt
|
||||
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
|
||||
'beach@lerbergetsvolleyboll.se' // From Email (VIKTIGT!)
|
||||
),
|
||||
sendMailJMAP(
|
||||
rawData.email, // To
|
||||
'beach@lerbergetsvolleyboll.se', // ReplyTo
|
||||
`Vi har tagit emot din anmälan för ${rawData.team}`, // Subject
|
||||
mailTillAnvandareHtml, // HTML Content
|
||||
"LVS Beach", // From Name
|
||||
'beach@lerbergetsvolleyboll.se' // From Email (VIKTIGT!)
|
||||
)
|
||||
]);
|
||||
|
||||
// Returnerar INGA värden här, vilket gör att formuläret kan rensas rent
|
||||
return { success: true, message: 'Tack! Din anmälan har skickats. Håll utkik i din inkorg efter bekräftelse.' };
|
||||
} catch (e) {
|
||||
console.error("Nodemailer error:", e);
|
||||
console.error("JMAP Fetch error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Kunde inte skicka mail. Kontakta oss manuellt istället.',
|
||||
|
||||
+77
-6
@@ -1,17 +1,88 @@
|
||||
// app/beach/page.tsx
|
||||
import { Metadata } from 'next';
|
||||
import { getConfig } from '../lib/config';
|
||||
import BeachClientPage from './BeachClientPage';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Beachturnering',
|
||||
};
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
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() {
|
||||
const config = await getConfig();
|
||||
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { FaFacebookSquare, FaInstagram } from "react-icons/fa";
|
||||
import { IoIosMail } from "react-icons/io";
|
||||
import Image from 'next/image';
|
||||
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() {
|
||||
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 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>
|
||||
{/* Combine the abbreviation and full name for the H1 */}
|
||||
<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>
|
||||
<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]">
|
||||
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>
|
||||
<div className="flex flex-col gap-2.5 mt-10">
|
||||
<HeroButton onClick={() => setActiveModal('member')}>Bli Medlem</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 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>
|
||||
</section>
|
||||
@@ -49,9 +52,9 @@ export default function HomeClientContent() {
|
||||
<div className="flex flex-col items-center w-full my-7.5 gap-2">
|
||||
<InfoCard
|
||||
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"
|
||||
href="https://www.laget.se/LVS/Event/Month"
|
||||
href="https://www.svenskalag.se/lerbergetsvs/kalender/"
|
||||
/>
|
||||
<InfoCard
|
||||
title="Prenumerera på träningstider"
|
||||
@@ -67,23 +70,23 @@ export default function HomeClientContent() {
|
||||
|
||||
{/* Sociala Medier Ikoner */}
|
||||
<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" />
|
||||
</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" />
|
||||
</SocialIcon>
|
||||
|
||||
<SocialIcon title="Laget.se" href="https://www.laget.se/LVS">
|
||||
<Image src={laget} alt="Laget.se" className="h-18 w-18" />
|
||||
<SocialIcon title="Svenskalag.se" href="https://www.svenskalag.se/lerbergetsvs" aria-label="Kolla träningstider på Svenskalag.se">
|
||||
<Image src={svenskalag} alt="Svenskalag.se" className="h-18 w-18" />
|
||||
</SocialIcon>
|
||||
</div>
|
||||
|
||||
{/* Kontaktinformation */}
|
||||
<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>
|
||||
<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 */}
|
||||
<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>
|
||||
<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
|
||||
</ModalAction>
|
||||
</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>
|
||||
|
||||
<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.laget.se/lvs-level-7-8.ics">Level 7-8</ModalAction>
|
||||
<ModalAction href="webcal://cal.laget.se/lvs-level-8-9.ics">Level 8-9</ModalAction>
|
||||
<ModalAction href="webcal://cal.laget.se/lvs-level-10.ics">Level 10</ModalAction>
|
||||
<ModalAction href="webcal://cal.laget.se/LVS.ics">Hela klubben</ModalAction>
|
||||
<ModalAction href="webcal://cal.svenskalag.se/120351">Level 4-6</ModalAction>
|
||||
<ModalAction href="webcal://cal.svenskalag.se/120353">Level 7-9</ModalAction>
|
||||
<ModalAction href="webcal://cal.svenskalag.se/120355">Level 10</ModalAction>
|
||||
<ModalAction href="webcal://cal.svenskalag.se/120187">Hela klubben</ModalAction>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
+3
-2
@@ -10,11 +10,12 @@ const montserrat = Montserrat({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://lerbergetsvolleyboll.se'),
|
||||
title: {
|
||||
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: {
|
||||
icon: '/images/favicon.svg',
|
||||
shortcut: '/favicon.svg',
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export async function getConfig() {
|
||||
return {
|
||||
showBeachPromo: true,
|
||||
beachPage: {
|
||||
matchStart: "2026-06-13T10:00",
|
||||
matchStart: "2027-06-12T10:00",
|
||||
isClosedOverride: false,
|
||||
closeReason: "Det är hela 26 lag anmälda och vi kan tyvärr inte ta emot fler anmälningar.\r\n\r\nVi ä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.",
|
||||
festivalLink: "https://www.kullahalvon.com/upptacka--uppleva/kultur--noje/evenemang-pa-kullahalvon/mat---sommarfesten.html",
|
||||
|
||||
+53
-8
@@ -1,21 +1,66 @@
|
||||
// app/page.tsx
|
||||
import { Metadata } from 'next';
|
||||
import BeachPromo from './components/BeachPromo';
|
||||
import Footer from './components/Footer';
|
||||
import Header from './components/Header';
|
||||
import HomeClientContent from './components/HomeClientContent';
|
||||
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() {
|
||||
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 (
|
||||
<main>
|
||||
<Header />
|
||||
{config.showBeachPromo && (
|
||||
<BeachPromo />
|
||||
)}
|
||||
<HomeClientContent />
|
||||
<Footer />
|
||||
</main>
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<main>
|
||||
<Header />
|
||||
{config.showBeachPromo && <BeachPromo />}
|
||||
<HomeClientContent />
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -2,6 +2,6 @@ TURNSTILE_SITE_KEY=1x00000000000000000000AA
|
||||
TURNSTILE_SECRET=1x0000000000000000000000000000000AA
|
||||
|
||||
ADMIN_PASSWORD=PASSWORD
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_USER=USER
|
||||
SMTP_PASS=PASSWORD
|
||||
STALWART_JMAP_URL=https://mail.example.com/jmap/
|
||||
STALWART_ACCOUNT_ID=ID
|
||||
STALWART_API_TOKEN=API_XXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
Generated
+838
-679
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -11,7 +11,6 @@
|
||||
"dependencies": {
|
||||
"@marsidev/react-turnstile": "^1.4.2",
|
||||
"next": "^16",
|
||||
"nodemailer": "^8.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
@@ -27,5 +26,9 @@
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"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