From e44fceaf322099de97a74a8ca0943d1dd23ccecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20S=C3=B6derberg?= Date: Thu, 5 Mar 2026 17:35:27 +0100 Subject: [PATCH] Almost done --- .dockerignore | 4 + Dockerfile | 30 +++ app/admin/AdminForm.tsx | 78 ++++++ app/admin/actions.ts | 30 +++ app/admin/page.tsx | 47 ++++ app/beach/BeachClientPage.tsx | 117 +++++++++ app/beach/BeachRegistrationForm.tsx | 159 ++++++++++++ app/beach/page.tsx | 348 +------------------------- app/components/BeachPromo.tsx | 31 +++ app/components/Footer.tsx | 15 ++ app/components/Header.tsx | 18 ++ app/components/HeroButton.tsx | 49 ++++ app/components/HomeClientContent.tsx | 114 +++++++++ app/components/InfoCard.tsx | 61 +++++ app/components/Modals/Member.tsx | 19 ++ app/components/Modals/Modal.tsx | 38 +++ app/components/Modals/ModalAction.tsx | 27 ++ app/components/Modals/Subscribe.tsx | 22 ++ app/components/SocialIcon.tsx | 23 ++ app/css.d.ts | 3 + app/globals.css | 4 +- app/layout.tsx | 12 +- app/lib/config.ts | 30 +++ app/login/LoginForm.tsx | 38 +++ app/login/actions.ts | 29 +++ app/login/page.tsx | 31 +++ app/page.tsx | 241 ++---------------- app/types.ts | 5 + data/site-config.json | 11 + docker-compose.yml | 15 ++ 30 files changed, 1077 insertions(+), 572 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 app/admin/AdminForm.tsx create mode 100644 app/admin/actions.ts create mode 100644 app/admin/page.tsx create mode 100644 app/beach/BeachClientPage.tsx create mode 100644 app/beach/BeachRegistrationForm.tsx create mode 100644 app/components/BeachPromo.tsx create mode 100644 app/components/Footer.tsx create mode 100644 app/components/Header.tsx create mode 100644 app/components/HeroButton.tsx create mode 100644 app/components/HomeClientContent.tsx create mode 100644 app/components/InfoCard.tsx create mode 100644 app/components/Modals/Member.tsx create mode 100644 app/components/Modals/Modal.tsx create mode 100644 app/components/Modals/ModalAction.tsx create mode 100644 app/components/Modals/Subscribe.tsx create mode 100644 app/components/SocialIcon.tsx create mode 100644 app/css.d.ts create mode 100644 app/lib/config.ts create mode 100644 app/login/LoginForm.tsx create mode 100644 app/login/actions.ts create mode 100644 app/login/page.tsx create mode 100644 app/types.ts create mode 100644 data/site-config.json create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c4ecc49 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.next +.git +data/*.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0eff423 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# 1. Install dependencies +FROM node:20-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci + +# 2. Build the app +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Vi mappar in miljövariabler under build om det behövs +ENV NEXT_TELEMETRY_DISABLED 1 +RUN npm run build + +# 3. Production image +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV production + +# Skapa data-mappen för Micro-CMS +RUN mkdir -p data + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +EXPOSE 3000 +CMD ["npm", "start"] \ No newline at end of file diff --git a/app/admin/AdminForm.tsx b/app/admin/AdminForm.tsx new file mode 100644 index 0000000..aa95acb --- /dev/null +++ b/app/admin/AdminForm.tsx @@ -0,0 +1,78 @@ +// app/admin/AdminForm.tsx + +'use client'; + +import { useActionState, useEffect, useState } from 'react'; +import { saveConfigAction } from './actions'; + +export default function AdminForm({ initialConfig }: { initialConfig: any }) { + const [state, formAction, isPending] = useActionState(saveConfigAction, null); + const [showToast, setShowToast] = useState(false); + + useEffect(() => { + if (state?.success) { + setShowToast(true); + const timer = setTimeout(() => setShowToast(false), 3000); + return () => clearTimeout(timer); + } + }, [state]); + + return ( +
+ {showToast && ( +
+ ✓ Ändringar sparade! +
+ )} + + {/* Startsida */} +
+

Startsida

+ +
+ + {/* Beach-Turnering */} +
+

Beach-Turnering

+ +
+
+ + +
+
+ + +
+
+ +
+ + -
- - {/* HONEYPOT */} -
- - -
- - {/* ALERT */} - {alert.show && ( -
- {alert.message} -
- )} - - {/* TURNSTILE */} -
- -
- - {/* SUBMIT BUTTON */} - - - - -
- )} - - {/* Contact Section */} -
-
-

Kontakta oss

- - - beach@lerbergetsvolleyboll.se - -
-
- - - - {/* Footer */} - - - - ); +export default async function BeachPage() { + const config = await getConfig(); + return ; } \ No newline at end of file diff --git a/app/components/BeachPromo.tsx b/app/components/BeachPromo.tsx new file mode 100644 index 0000000..d9dbb51 --- /dev/null +++ b/app/components/BeachPromo.tsx @@ -0,0 +1,31 @@ +// app/components/BeachPromo.tsx + +import Link from 'next/link'; +import { FaVolleyballBall } from "react-icons/fa"; + +export default function BeachPromo() { + return ( +
+
+

+ + Kolla in vår + + + + Beachturnering + + + + +

+
+
+ ); +} \ No newline at end of file diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx new file mode 100644 index 0000000..7d5f106 --- /dev/null +++ b/app/components/Footer.tsx @@ -0,0 +1,15 @@ +// app/components/Footer.tsx +export default function Footer() { + return ( +
+
+

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

+ {/* En subtil linje som växer fram under footern när man hovrar */} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 0000000..df4d84c --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,18 @@ +// app/components/Header.tsx +import Link from 'next/link'; + +export default function Header() { + return ( +
+
+ + Lerbergets Volleybollsällskap + +
+
+ ); +} \ No newline at end of file diff --git a/app/components/HeroButton.tsx b/app/components/HeroButton.tsx new file mode 100644 index 0000000..862d8aa --- /dev/null +++ b/app/components/HeroButton.tsx @@ -0,0 +1,49 @@ +// app/components/HeroButton.tsx + +import Link from 'next/link'; +import React from 'react'; + +interface HeroButtonProps { + href?: string; + onClick?: () => void; + children: React.ReactNode; + className?: string; +} + +export default function HeroButton({ href, onClick, children, className = "" }: HeroButtonProps) { + const baseStyles = ` + text-[#406185] bg-white border-none py-3.5 px-7.5 + text-center font-bold uppercase w-fit text-[1.1rem] + transition-all duration-300 hover:scale-105 active:scale-95 + shadow-md hover:shadow-lg cursor-pointer inline-block + ${className} + `; + + if (onClick) { + return ( + + ); + } + + if (href) { + const isExternal = href.startsWith('http'); + + if (isExternal) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); + } + + return null; +} \ No newline at end of file diff --git a/app/components/HomeClientContent.tsx b/app/components/HomeClientContent.tsx new file mode 100644 index 0000000..8cc0765 --- /dev/null +++ b/app/components/HomeClientContent.tsx @@ -0,0 +1,114 @@ +// app/components/HomeClientContent.tsx + +'use client'; + +import { useState } from 'react'; +import HeroButton from './HeroButton'; +import InfoCard from './InfoCard'; +import SocialIcon from './SocialIcon'; +import MemberModal from './Modals/Member'; +import SubscribeModal from './Modals/Subscribe'; +import { FaFacebookSquare, FaInstagram } from "react-icons/fa"; +import { IoIosMail } from "react-icons/io"; + +export default function HomeClientContent() { + const [activeModal, setActiveModal] = useState<'member' | 'subscribe' | null>(null); + const closeModals = () => setActiveModal(null); + + return ( + <> + {/* Hero / Info Section */} +
+
+
+
+

LVS

+
+
+

Lerbergets Volleybollsällskap

+

+ Klubben startade den 1 mars 2018 och har i snabb takt utvecklats... +

+
+ setActiveModal('member')}>Bli Medlem + Träningstider + Klubbkollektion +
+
+
+ Spelare +
+
+
+ + {/* Träningstider Section */} +
+
+ + setActiveModal('subscribe')} + /> +
+
+ +
+
+ + {/* Sociala Medier Ikoner */} +
+ + + + + + + + + + Laget.se + +
+ + {/* Kontaktinformation */} +
+

Kontakta oss

+

Vi finns på Instagram, Facebook och laget.se

+ + {/* Klickbar Adress - Länkar till Google Maps */} + + Höganäs Sportcenter, Friluftsvägen 10, 263 54 Lerberget + + + {/* Levande E-postlänk med brand-gul färg */} + + + info@lerbergetsvolleyboll.se + {/* Underlinje som expanderar vid hovring */} + + +
+ +
+
+ + {activeModal === 'member' && } + {activeModal === 'subscribe' && } + + ); +} \ No newline at end of file diff --git a/app/components/InfoCard.tsx b/app/components/InfoCard.tsx new file mode 100644 index 0000000..62ba47f --- /dev/null +++ b/app/components/InfoCard.tsx @@ -0,0 +1,61 @@ +// app/components/InfoCard.tsx + +import { HiOutlineArrowNarrowRight } from "react-icons/hi"; + +interface InfoCardProps { + title: string; + description: string; + actionLabel: string; + href?: string; + onClick?: () => void; +} + +export default function InfoCard({ title, description, actionLabel, href, onClick }: InfoCardProps) { + const actionClasses = ` + group inline-flex items-center gap-2 + text-[#f1c50e] font-bold uppercase text-[1rem] + tracking-wider transition-all duration-300 + relative pb-1 + `; + + const content = ( + <> + {actionLabel} + + + + ); + + return ( +
+

+ {title} +

+

+ {description} +

+ +
+ {href ? ( + + {content} + + ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/Modals/Member.tsx b/app/components/Modals/Member.tsx new file mode 100644 index 0000000..b9c268e --- /dev/null +++ b/app/components/Modals/Member.tsx @@ -0,0 +1,19 @@ +// app/components/Modals/Member.tsx + +import { ModalProps } from "@/app/types"; +import Modal from "./Modal"; +import ModalAction from "./ModalAction"; + +export default function MemberModal({ onClose }: ModalProps) { + return ( + + Logo +

Registrera dig

+

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

+ + + Bli medlem + +
+ ); +} \ No newline at end of file diff --git a/app/components/Modals/Modal.tsx b/app/components/Modals/Modal.tsx new file mode 100644 index 0000000..7a0262e --- /dev/null +++ b/app/components/Modals/Modal.tsx @@ -0,0 +1,38 @@ +// app/components/Modals/Modal.tsx + +import { ModalProps } from "@/app/types"; +import React from "react"; + + +interface BaseModalProps extends ModalProps { + onClose: () => void; + children: React.ReactNode; +} + +export default function Modal({ onClose, children }: BaseModalProps) { + return ( +
+
e.stopPropagation()} + > + + {children} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/Modals/ModalAction.tsx b/app/components/Modals/ModalAction.tsx new file mode 100644 index 0000000..aefaac0 --- /dev/null +++ b/app/components/Modals/ModalAction.tsx @@ -0,0 +1,27 @@ +// app/components/Modals/ModalAction.tsx + +import React from 'react'; + +interface ModalActionProps { + href: string; + children: React.ReactNode; + className?: string; // To allow for specific margins like mt-5 +} + +export default function ModalAction({ href, children, className = "" }: ModalActionProps) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/app/components/Modals/Subscribe.tsx b/app/components/Modals/Subscribe.tsx new file mode 100644 index 0000000..bd318ac --- /dev/null +++ b/app/components/Modals/Subscribe.tsx @@ -0,0 +1,22 @@ +// app/components/Modals/Subscribe.tsx + +import { ModalProps } from "@/app/types"; +import Modal from "./Modal"; +import ModalAction from "./ModalAction"; + +export default function SubscribeModal({ onClose }: ModalProps) { + return ( + +

Prenumerera

+

Välj vilken LEVEL du vill prenumerera på.

+ +
+ Level 5-6 + Level 7-8 + Level 8-9 + Level 10 + Hela klubben +
+
+ ); +} \ No newline at end of file diff --git a/app/components/SocialIcon.tsx b/app/components/SocialIcon.tsx new file mode 100644 index 0000000..3006f4e --- /dev/null +++ b/app/components/SocialIcon.tsx @@ -0,0 +1,23 @@ +// app/components/SocialIcon.tsx + +import React from 'react'; + +interface SocialIconProps { + href: string; + title: string; + children: React.ReactNode; +} + +export default function SocialIcon({ href, title, children }: SocialIconProps) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/app/css.d.ts b/app/css.d.ts new file mode 100644 index 0000000..17e71f0 --- /dev/null +++ b/app/css.d.ts @@ -0,0 +1,3 @@ +// app/css.d.ts + +declare module '*.css'; \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index e27a5e2..03f17bf 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,7 +19,6 @@ font-style: normal; } -/* 1. Add your exact keyframes from the old site */ @keyframes animateright { from { right: -300px; @@ -32,13 +31,12 @@ } } -/* 2. Create a custom class to apply it */ .player-animation { position: relative; animation: animateright 1.5s; } -:root { +@theme { --font-beachday: 'Beachday', sans-serif; --font-sans: var(--font-montserrat), sans-serif; } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index c2e650f..199067d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,8 +10,16 @@ const montserrat = Montserrat({ }); export const metadata: Metadata = { - title: 'Lerbergets Volleybollsällskap', + title: { + template: '%s | LVS', + default: 'Lerbergets Volleybollsällskap', + }, description: 'Klubben startade den 1 mars 2018 och har i snabb takt utvecklats...', + icons: { + icon: '/images/favicon.svg', + shortcut: '/favicon.svg', + apple: '/images/favicon.svg', + }, }; export default function RootLayout({ @@ -26,4 +34,4 @@ export default function RootLayout({ ); -} \ No newline at end of file +} diff --git a/app/lib/config.ts b/app/lib/config.ts new file mode 100644 index 0000000..259ae49 --- /dev/null +++ b/app/lib/config.ts @@ -0,0 +1,30 @@ +// app/lib/config.ts + +import fs from 'node:fs/promises'; +import path from 'node:path'; + +const configPath = path.join(process.cwd(), 'data', 'site-config.json'); + +export async function getConfig() { + try { + const data = await fs.readFile(configPath, 'utf-8'); + return JSON.parse(data); + } catch (error) { + return { + showBeachPromo: true, + beachPage: { + matchStart: "2026-06-13T10: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", + otherInfo: "Turneringen upskattas hålla på till ca 17-18 tiden\r\nDet kommer finnas duschmöjligheter i anslutning till stranden.", + prizesText: "Det kommer att finnas fina priser till alla pallplatser! 🏆" + } + }; + }; +} + + +export async function updateConfig(newConfig: any) { + await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2)); +} \ No newline at end of file diff --git a/app/login/LoginForm.tsx b/app/login/LoginForm.tsx new file mode 100644 index 0000000..7961421 --- /dev/null +++ b/app/login/LoginForm.tsx @@ -0,0 +1,38 @@ +// app/login/LoginForm.tsx + +'use client'; + +import { useActionState } from 'react'; +import { loginAction } from './actions'; + +export default function LoginForm() { + const [state, formAction, isPending] = useActionState(loginAction, null); + + return ( +
+

Admin Login

+ + {state?.error && ( +
+ {state.error} +
+ )} + + + + +
+ ); +} \ No newline at end of file diff --git a/app/login/actions.ts b/app/login/actions.ts new file mode 100644 index 0000000..4738992 --- /dev/null +++ b/app/login/actions.ts @@ -0,0 +1,29 @@ +// app/login/actions.ts + +'use server'; + +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; + +export async function loginAction(prevState: any, formData: FormData) { + const password = formData.get('password'); + + if (password === process.env.ADMIN_PASSWORD) { + const cookieStore = await cookies(); + cookieStore.set('admin_session', 'true', { + httpOnly: true, + secure: true, + sameSite: 'strict', + maxAge: 60 * 60 * 24 // 24 timmar + }); + redirect('/admin'); + } + + return { error: 'Felaktigt lösenord' }; +} + +export async function logoutAction() { + const cookieStore = await cookies(); + cookieStore.delete('admin_session'); + redirect('/login'); +} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..2e1c878 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,31 @@ +// app/login/page.tsx +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import Header from '../components/Header'; +import LoginForm from './LoginForm'; +import Footer from '../components/Footer'; + +export const metadata = { + title: 'Logga in', +}; + +export default async function LoginPage() { + const cookieStore = await cookies(); + const isLoggedIn = cookieStore.get('admin_session')?.value === 'true'; + + if (isLoggedIn) { + redirect('/admin'); + } + + return ( +
+
+ +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 5da5a9b..0ede6e4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,234 +1,21 @@ // app/page.tsx +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'; -'use client'; - -import Link from 'next/link'; -import { useState } from 'react'; -import { FaFacebookSquare, FaInstagram } from "react-icons/fa"; -import { IoIosMail } from "react-icons/io"; - -export default function Home() { - const [isMedlemOpen, setIsMedlemOpen] = useState(false); - const [isPrenumereraOpen, setIsPrenumereraOpen] = useState(false); +export default async function Home() { + const config = await getConfig(); return ( -
- - {/* Header */} -
-
- - Lerbergets Volleybollsällskap - -
-
- - {/* Main Content */} -
- - {/* Promo Section (Hidden) */} -
-
-

- Kolla in vår Beachturnering -

-
-
- - {/* Hero / Info Section */} -
-
- -
-
-

LVS

-
-
- -

Lerbergets Volleybollsällskap

-

- Klubben startade den 1 mars 2018 och har i snabb takt utvecklats med både kidsvolley, - 4-mannavolley och ett gott gäng killar och tjejer som tränar tillsammans minst två gånger i - veckan och på sommaren spelas det beachvolley -

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

Träningstider

-

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

- - Se nästa träningstid - -
- -
-

Prenumerera på träningstider

-

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

- -
- -
-
- - {/* Kontakt Section */} -
-
- - - -
-

Kontakta oss

-

Vi finns på Instagram, Facebook och laget.se

-

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

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

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

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

Registrera dig

-

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

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

Prenumerera

-

Välj vilken LEVEL du vill prenumerera på.

- - -
-
- )} - -
+ +