feat: add feature carousel hero + enable Stripe billing
- 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:
@@ -21,6 +21,7 @@ ARG NEXT_PUBLIC_DEFAULT_LOCALE=de
|
|||||||
ARG NEXT_PUBLIC_ENABLE_FISCHEREI=true
|
ARG NEXT_PUBLIC_ENABLE_FISCHEREI=true
|
||||||
ARG NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS=true
|
ARG NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS=true
|
||||||
ARG NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG=true
|
ARG NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG=true
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||||
ENV NEXT_PUBLIC_CI=${NEXT_PUBLIC_CI}
|
ENV NEXT_PUBLIC_CI=${NEXT_PUBLIC_CI}
|
||||||
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
||||||
ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_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_FISCHEREI=${NEXT_PUBLIC_ENABLE_FISCHEREI}
|
||||||
ENV NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS=${NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS}
|
ENV NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS=${NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS}
|
||||||
ENV NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG=${NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG}
|
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 pnpm --filter web build
|
||||||
|
|
||||||
# --- Run ---
|
# --- Run ---
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ EMAIL_PASSWORD=password
|
|||||||
CONTACT_EMAIL=test@makerkit.dev
|
CONTACT_EMAIL=test@makerkit.dev
|
||||||
|
|
||||||
# STRIPE
|
# STRIPE
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V
|
||||||
|
|
||||||
# MAILER
|
# MAILER
|
||||||
MAILER_PROVIDER=nodemailer
|
MAILER_PROVIDER=nodemailer
|
||||||
|
# STRIPE SECRET KEY
|
||||||
|
STRIPE_SECRET_KEY=sk_test_51SMbesKttnWb7SsFTjCsPZMlxVe3WjrsDnpLDAQehVdzSoDaWMFmc3hiZOTp2IAKB1cleMPIOW9GmEJHEkhazJsq00FEfTr6BI
|
||||||
|
|||||||
@@ -9,4 +9,4 @@
|
|||||||
NEXT_PUBLIC_SUPABASE_URL=
|
NEXT_PUBLIC_SUPABASE_URL=
|
||||||
|
|
||||||
# STRIPE
|
# STRIPE
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V
|
||||||
|
|||||||
@@ -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 Seite 1 von 52 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +37,7 @@ import billingConfig from '~/config/billing.config';
|
|||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
|
||||||
import { AnimateOnScroll } from './_components/animate-on-scroll';
|
import { AnimateOnScroll } from './_components/animate-on-scroll';
|
||||||
|
import { FeatureCarousel } from './_components/feature-carousel';
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
return (
|
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"
|
className="bg-primary/10 absolute inset-0 -z-10 mx-auto max-w-3xl rounded-full blur-3xl"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Image
|
<FeatureCarousel />
|
||||||
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`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ services:
|
|||||||
NEXT_PUBLIC_SUPABASE_URL: http://localhost:8000
|
NEXT_PUBLIC_SUPABASE_URL: http://localhost:8000
|
||||||
NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0}
|
NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0}
|
||||||
NEXT_PUBLIC_DEFAULT_LOCALE: de
|
NEXT_PUBLIC_DEFAULT_LOCALE: de
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
@@ -350,6 +351,9 @@ services:
|
|||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: 'false'
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: 'false'
|
||||||
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: 'false'
|
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: 'false'
|
||||||
NEXT_PUBLIC_ENABLE_NOTIFICATIONS: 'true'
|
NEXT_PUBLIC_ENABLE_NOTIFICATIONS: 'true'
|
||||||
|
# Stripe
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_51SMbesKttnWb7SsFOR7cJ1jshdEaZiHmCflWndgLtL3gx1Cu8N4p5qxSJY8PHmpEJL8gf4VrqqX2Fr7pxJtQILUS00yYQ7Tx8V'
|
||||||
|
STRIPE_SECRET_KEY: 'sk_test_51SMbesKttnWb7SsFTjCsPZMlxVe3WjrsDnpLDAQehVdzSoDaWMFmc3hiZOTp2IAKB1cleMPIOW9GmEJHEkhazJsq00FEfTr6BI'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
supabase-db-data:
|
supabase-db-data:
|
||||||
|
|||||||
Reference in New Issue
Block a user