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
|
||||
+177
-45
@@ -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.',
|
||||
|
||||
@@ -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);
|
||||
@@ -42,7 +42,7 @@ export default function HomeClientContent() {
|
||||
</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>
|
||||
@@ -52,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"
|
||||
@@ -78,15 +78,15 @@ export default function HomeClientContent() {
|
||||
<FaFacebookSquare className="h-20 w-20" />
|
||||
</SocialIcon>
|
||||
|
||||
<SocialIcon title="Laget.se" href="https://www.laget.se/LVS" aria-label="Kolla träningstider på Laget.se">
|
||||
<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>
|
||||
);
|
||||
|
||||
+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",
|
||||
|
||||
+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
+4
-1
@@ -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: 38 KiB |
Reference in New Issue
Block a user