feat: add feature carousel hero + enable Stripe billing
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m5s
Workflow / ⚫️ Test (push) Has been skipped

- Replace static dashboard screenshot with interactive feature carousel
  9 slides: Dashboard, Mitglieder, Kurse, Finanzen, Veranstaltungen,
  Newsletter, Website, Buchungen, Dokumente
  Auto-advances every 6s, clickable sidebar + bottom tabs
  Virtual app UI rendered with shadcn components (no images needed)

- Enable Stripe test mode billing
  Add publishable key to .env.development, .env.production, docker-compose
  Add secret key to .env.development and docker-compose
  Add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY to Dockerfile build args
This commit is contained in:
Zaid Marzguioui
2026-04-02 18:54:58 +02:00
parent a6c9537195
commit d87fbb050f
6 changed files with 597 additions and 14 deletions

View File

@@ -21,6 +21,7 @@ ARG NEXT_PUBLIC_DEFAULT_LOCALE=de
ARG NEXT_PUBLIC_ENABLE_FISCHEREI=true
ARG NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS=true
ARG NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG=true
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
ENV NEXT_PUBLIC_CI=${NEXT_PUBLIC_CI}
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
@@ -29,6 +30,7 @@ ENV NEXT_PUBLIC_DEFAULT_LOCALE=${NEXT_PUBLIC_DEFAULT_LOCALE}
ENV NEXT_PUBLIC_ENABLE_FISCHEREI=${NEXT_PUBLIC_ENABLE_FISCHEREI}
ENV NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS=${NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS}
ENV NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG=${NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG}
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
RUN pnpm --filter web build
# --- Run ---

View File

@@ -21,7 +21,9 @@ EMAIL_PASSWORD=password
CONTACT_EMAIL=test@makerkit.dev
# STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V
# MAILER
MAILER_PROVIDER=nodemailer
# STRIPE SECRET KEY
STRIPE_SECRET_KEY=sk_test_51SMbesKttnWb7SsFTjCsPZMlxVe3WjrsDnpLDAQehVdzSoDaWMFmc3hiZOTp2IAKB1cleMPIOW9GmEJHEkhazJsq00FEfTr6BI

View File

@@ -9,4 +9,4 @@
NEXT_PUBLIC_SUPABASE_URL=
# STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V

View File

@@ -0,0 +1,584 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import {
BedDouble,
CalendarDays,
ChevronLeft,
ChevronRight,
FileText,
Globe,
GraduationCap,
LayoutDashboard,
Mail,
Users,
Wallet,
} from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { cn } from '@kit/ui/utils';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function MiniStat({ label, value }: { label: string; value: string }) {
return (
<div className="bg-background rounded-lg border p-3 text-center">
<div className="text-foreground text-lg font-bold">{value}</div>
<div className="text-muted-foreground text-[10px]">{label}</div>
</div>
);
}
function MiniRow({
cells,
highlighted,
}: {
cells: string[];
highlighted?: boolean;
}) {
return (
<div
className={cn(
'grid gap-2 border-b px-3 py-2 text-xs',
highlighted && 'bg-primary/5',
)}
style={{ gridTemplateColumns: `repeat(${cells.length}, 1fr)` }}
>
{cells.map((c, i) => (
<span
key={i}
className={i === 0 ? 'font-medium' : 'text-muted-foreground'}
>
{c}
</span>
))}
</div>
);
}
// ---------------------------------------------------------------------------
// Slide data
// ---------------------------------------------------------------------------
interface Slide {
id: string;
label: string;
icon: React.ReactNode;
content: React.ReactNode;
}
const IC = 'h-4 w-4';
const SLIDES: Slide[] = [
{
id: 'dashboard',
label: 'Dashboard',
icon: <LayoutDashboard className={IC} />,
content: (
<div className="space-y-4">
<div className="grid grid-cols-4 gap-3">
<MiniStat label="Mitglieder" value="1.247" />
<MiniStat label="Aktive Kurse" value="18" />
<MiniStat label="Offene Rechnungen" value="3.420 €" />
<MiniStat label="Newsletter" value="12" />
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-background space-y-2 rounded-lg border p-3">
<div className="text-xs font-semibold">Letzte Aktivität</div>
{[
'Max Müller — Beitritt',
'SEPA-Einzug #42 — erstellt',
'Schwimmkurs — 3 neue Teilnehmer',
].map((t) => (
<div
key={t}
className="text-muted-foreground flex items-center gap-2 text-[10px]"
>
<div className="bg-primary/20 h-1.5 w-1.5 rounded-full" />
{t}
</div>
))}
</div>
<div className="bg-background space-y-2 rounded-lg border p-3">
<div className="text-xs font-semibold">Schnellaktionen</div>
{['Neues Mitglied', 'Neuer Kurs', 'Newsletter erstellen'].map(
(t) => (
<div
key={t}
className="bg-muted flex items-center justify-between rounded px-2 py-1 text-[10px]"
>
{t} <span className="text-muted-foreground"></span>
</div>
),
)}
</div>
</div>
</div>
),
},
{
id: 'members',
label: 'Mitglieder',
icon: <Users className={IC} />,
content: (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="bg-muted text-muted-foreground flex items-center gap-2 rounded-md px-3 py-1.5 text-[10px]">
🔍 Mitglied suchen
</div>
<div className="flex gap-2">
<Badge variant="outline" className="text-[9px]">
CSV
</Badge>
<Badge variant="outline" className="text-[9px]">
Excel
</Badge>
<Badge variant="default" className="text-[9px]">
+ Neues Mitglied
</Badge>
</div>
</div>
<div className="rounded-lg border">
<div className="bg-muted/50 grid grid-cols-5 gap-2 border-b px-3 py-2 text-[10px] font-semibold">
<span>Nr</span>
<span>Name</span>
<span>E-Mail</span>
<span>Status</span>
<span>Eintritt</span>
</div>
{(
[
[
'M-001',
'Müller, Hans',
'h.mueller@web.de',
'Aktiv',
'15.01.2024',
],
[
'M-002',
'Schmidt, Anna',
'a.schmidt@gmx.de',
'Aktiv',
'01.03.2024',
],
[
'M-003',
'Weber, Thomas',
'weber@t-online.de',
'Passiv',
'22.06.2023',
],
[
'M-004',
'Fischer, Maria',
'm.fischer@mail.de',
'Aktiv',
'08.09.2024',
],
] as string[][]
).map((row, i) => (
<MiniRow key={i} cells={row} highlighted={i === 0} />
))}
</div>
<div className="text-muted-foreground text-center text-[10px]">
Zurück &nbsp; Seite 1 von 52 &nbsp; Weiter
</div>
</div>
),
},
{
id: 'courses',
label: 'Kurse',
icon: <GraduationCap className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-4 gap-3">
<MiniStat label="Gesamt" value="24" />
<MiniStat label="Aktiv" value="18" />
<MiniStat label="Teilnehmer" value="342" />
<MiniStat label="Auslastung" value="78%" />
</div>
<div className="rounded-lg border">
<div className="bg-muted/50 grid grid-cols-5 gap-2 border-b px-3 py-2 text-[10px] font-semibold">
<span>Kurs-Nr</span>
<span>Name</span>
<span>Beginn</span>
<span>Status</span>
<span>Gebühr</span>
</div>
{(
[
['SK-001', 'Schwimmkurs Anfänger', '01.05.2026', 'Aktiv', '50 €'],
['YG-003', 'Yoga für Senioren', '15.03.2026', 'Geplant', '35 €'],
['TN-012', 'Tenniskurs Jugend', '01.06.2026', 'Aktiv', '80 €'],
] as string[][]
).map((row, i) => (
<MiniRow key={i} cells={row} />
))}
</div>
</div>
),
},
{
id: 'finance',
label: 'Finanzen',
icon: <Wallet className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<MiniStat label="SEPA-Einzüge" value="8" />
<MiniStat label="Rechnungen" value="147" />
<MiniStat label="Offene Forderungen" value="3.420 €" />
</div>
<div className="rounded-lg border">
<div className="bg-muted/50 grid grid-cols-4 gap-2 border-b px-3 py-2 text-[10px] font-semibold">
<span>Rechnungs-Nr</span>
<span>Mitglied</span>
<span>Betrag</span>
<span>Status</span>
</div>
{(
[
['RE-2026-001', 'Hans Müller', '120,00 €', 'Bezahlt'],
['RE-2026-002', 'Anna Schmidt', '85,00 €', 'Offen'],
['RE-2026-003', 'Thomas Weber', '120,00 €', 'Überfällig'],
] as string[][]
).map((row, i) => (
<MiniRow key={i} cells={row} />
))}
</div>
</div>
),
},
{
id: 'events',
label: 'Veranstaltungen',
icon: <CalendarDays className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<MiniStat label="Veranstaltungen" value="12" />
<MiniStat label="Anmeldungen" value="284" />
<MiniStat label="Kapazität" value="500" />
</div>
<div className="space-y-2">
{[
{
name: 'Sommerfest 2026',
date: '21.06.2026',
spots: '84/120',
badge: 'Offen' as const,
},
{
name: 'Jahreshauptversammlung',
date: '15.03.2026',
spots: '200/200',
badge: 'Ausgebucht' as const,
},
{
name: 'Weihnachtsfeier',
date: '20.12.2026',
spots: '0/80',
badge: 'Geplant' as const,
},
].map((e) => (
<div
key={e.name}
className="bg-background flex items-center justify-between rounded-lg border px-3 py-2"
>
<div>
<div className="text-xs font-medium">{e.name}</div>
<div className="text-muted-foreground text-[10px]">
{e.date} · {e.spots} Plätze
</div>
</div>
<Badge
variant={
e.badge === 'Ausgebucht'
? 'destructive'
: e.badge === 'Offen'
? 'default'
: 'outline'
}
className="text-[9px]"
>
{e.badge}
</Badge>
</div>
))}
</div>
</div>
),
},
{
id: 'newsletter',
label: 'Newsletter',
icon: <Mail className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<MiniStat label="Kampagnen" value="12" />
<MiniStat label="Gesendet" value="8" />
<MiniStat label="Empfänger" value="1.180" />
</div>
<div className="rounded-lg border">
<div className="bg-muted/50 grid grid-cols-4 gap-2 border-b px-3 py-2 text-[10px] font-semibold">
<span>Betreff</span>
<span>Empfänger</span>
<span>Datum</span>
<span>Status</span>
</div>
{(
[
['Frühjahrsnewsletter', '1.180', '01.03.2026', 'Gesendet'],
['Einladung Sommerfest', '1.180', '15.05.2026', 'Entwurf'],
['Beitragsinfo 2026', '1.180', '15.01.2026', 'Gesendet'],
] as string[][]
).map((row, i) => (
<MiniRow key={i} cells={row} />
))}
</div>
</div>
),
},
{
id: 'website',
label: 'Website',
icon: <Globe className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<MiniStat label="Seiten" value="7" />
<MiniStat label="Veröffentlicht" value="5" />
<MiniStat label="Status" value="● Online" />
</div>
<div className="rounded-lg border">
<div className="bg-muted/50 grid grid-cols-4 gap-2 border-b px-3 py-2 text-[10px] font-semibold">
<span>Titel</span>
<span>URL</span>
<span>Status</span>
<span>Aktualisiert</span>
</div>
{(
[
['Startseite', '/home', 'Veröffentlicht', '30.03.2026'],
['Über uns', '/ueber-uns', 'Entwurf', '28.03.2026'],
['Kurse', '/kurse', 'Veröffentlicht', '25.03.2026'],
['Kontakt', '/kontakt', 'Veröffentlicht', '20.03.2026'],
] as string[][]
).map((row, i) => (
<MiniRow key={i} cells={row} />
))}
</div>
</div>
),
},
{
id: 'bookings',
label: 'Buchungen',
icon: <BedDouble className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<MiniStat label="Buchungen" value="36" />
<MiniStat label="Räume" value="8" />
<MiniStat label="Auslastung" value="64%" />
</div>
<div className="rounded-lg border">
<div className="bg-muted/50 grid grid-cols-4 gap-2 border-b px-3 py-2 text-[10px] font-semibold">
<span>Raum</span>
<span>Gast</span>
<span>Check-in</span>
<span>Status</span>
</div>
{(
[
['Seminarraum A', 'TV Musterstadt', '15.04.2026', 'Bestätigt'],
['Vereinsheim', 'Karin Bauer', '18.04.2026', 'Ausstehend'],
['Turnhalle', 'TSV Neustadt', '20.04.2026', 'Bestätigt'],
] as string[][]
).map((row, i) => (
<MiniRow key={i} cells={row} />
))}
</div>
</div>
),
},
{
id: 'documents',
label: 'Dokumente',
icon: <FileText className={IC} />,
content: (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<MiniStat label="Vorlagen" value="5" />
<MiniStat label="Generiert" value="324" />
<MiniStat label="Zuletzt" value="Heute" />
</div>
<div className="space-y-2">
{[
{ name: 'Mitgliedsausweis', count: '1.247 generiert', type: 'PDF' },
{ name: 'Beitragsrechnung', count: '324 generiert', type: 'PDF' },
{ name: 'SEPA-Mandat', count: '890 generiert', type: 'PDF' },
{ name: 'Mitgliederliste', count: '12 Exporte', type: 'Excel' },
].map((d) => (
<div
key={d.name}
className="bg-background flex items-center justify-between rounded-lg border px-3 py-2"
>
<div>
<div className="text-xs font-medium">{d.name}</div>
<div className="text-muted-foreground text-[10px]">
{d.count}
</div>
</div>
<Badge variant="outline" className="text-[9px]">
{d.type}
</Badge>
</div>
))}
</div>
</div>
),
},
];
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function FeatureCarousel() {
const [active, setActive] = useState(0);
const slide = SLIDES[active]!;
const next = useCallback(
() => setActive((i) => (i + 1) % SLIDES.length),
[],
);
const prev = useCallback(
() => setActive((i) => (i - 1 + SLIDES.length) % SLIDES.length),
[],
);
useEffect(() => {
const id = setInterval(next, 6000);
return () => clearInterval(id);
}, [next]);
return (
<div className="relative mx-auto w-full max-w-4xl">
<div
className="bg-primary/10 absolute inset-0 -z-10 mx-auto max-w-3xl rounded-full blur-3xl"
aria-hidden="true"
/>
<div className="dark:border-primary/10 overflow-hidden rounded-2xl border border-gray-200 shadow-2xl">
{/* Browser chrome */}
<div className="bg-muted/80 flex items-center gap-2 border-b px-4 py-2.5">
<div className="flex gap-1.5">
<div className="h-3 w-3 rounded-full bg-red-400" />
<div className="h-3 w-3 rounded-full bg-amber-400" />
<div className="h-3 w-3 rounded-full bg-green-400" />
</div>
<div className="bg-background mx-auto flex items-center gap-1.5 rounded-md px-4 py-1 text-xs">
<span className="text-muted-foreground/50">🔒</span>
<span className="text-muted-foreground">
myeasycms.de/home/mein-verein
</span>
</div>
</div>
{/* Layout */}
<div className="bg-background flex" style={{ minHeight: 380 }}>
{/* Sidebar */}
<div className="bg-muted/30 hidden w-40 shrink-0 border-r p-2 md:block">
<div className="text-foreground mb-3 px-2 text-[10px] font-bold">
MYeasyCMS
</div>
{SLIDES.map((s, i) => (
<button
key={s.id}
onClick={() => setActive(i)}
className={cn(
'flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-[10px] transition-colors',
i === active
? 'bg-primary/10 text-primary font-semibold'
: 'text-muted-foreground hover:bg-muted',
)}
>
{s.icon}
<span className="truncate">{s.label}</span>
</button>
))}
</div>
{/* Content */}
<div className="flex-1 p-5">
<div className="text-muted-foreground mb-4 flex items-center gap-1 text-[10px]">
<span>Home</span>
<span></span>
<span>Mein Verein</span>
<span></span>
<span className="text-foreground font-medium">{slide.label}</span>
</div>
<div
className="animate-in fade-in slide-in-from-right-2 duration-300"
key={slide.id}
>
{slide.content}
</div>
</div>
</div>
{/* Bottom tabs */}
<div className="border-t">
<div className="flex items-center justify-between px-4 py-2">
<Button
variant="ghost"
size="icon"
onClick={prev}
className="h-7 w-7"
aria-label="Vorheriges Feature"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex gap-1 overflow-x-auto">
{SLIDES.map((s, i) => (
<button
key={s.id}
onClick={() => setActive(i)}
className={cn(
'flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-[10px] transition-colors',
i === active
? 'bg-primary text-primary-foreground font-semibold'
: 'text-muted-foreground hover:bg-muted',
)}
>
{s.icon}
<span className="hidden sm:inline">{s.label}</span>
</button>
))}
</div>
<Button
variant="ghost"
size="icon"
onClick={next}
className="h-7 w-7"
aria-label="Nächstes Feature"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,3 @@
import Image from 'next/image';
import Link from 'next/link';
import {
@@ -38,6 +37,7 @@ import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { AnimateOnScroll } from './_components/animate-on-scroll';
import { FeatureCarousel } from './_components/feature-carousel';
function Home() {
return (
@@ -72,16 +72,7 @@ function Home() {
className="bg-primary/10 absolute inset-0 -z-10 mx-auto max-w-3xl rounded-full blur-3xl"
aria-hidden="true"
/>
<Image
priority
className={
'dark:border-primary/10 w-full rounded-2xl border border-gray-200 shadow-2xl'
}
width={3558}
height={2222}
src={`/images/dashboard.webp`}
alt={`MyEasyCMS Dashboard`}
/>
<FeatureCarousel />
</div>
}
/>

View File

@@ -323,6 +323,7 @@ services:
NEXT_PUBLIC_SUPABASE_URL: http://localhost:8000
NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0}
NEXT_PUBLIC_DEFAULT_LOCALE: de
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V'
restart: unless-stopped
ports:
- '3000:3000'
@@ -350,6 +351,9 @@ services:
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: 'false'
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: 'false'
NEXT_PUBLIC_ENABLE_NOTIFICATIONS: 'true'
# Stripe
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V'
STRIPE_SECRET_KEY: 'sk_test_51SMbesKttnWb7SsFTjCsPZMlxVe3WjrsDnpLDAQehVdzSoDaWMFmc3hiZOTp2IAKB1cleMPIOW9GmEJHEkhazJsq00FEfTr6BI'
volumes:
supabase-db-data: