v1.24: Anleitungsseite (Mieter/Verwaltung), Stand wird vor jedem Build automatisch aktualisiert
This commit is contained in:
@@ -36,6 +36,8 @@ shots/
|
||||
# Live-Stammdaten (PII) – nur lokal, nicht ins Git
|
||||
Master Data V.csv
|
||||
import-*.sql
|
||||
# Wird vor jedem Build automatisch erzeugt (scripts/anleitung-meta.mjs)
|
||||
src/data/anleitung-meta.ts
|
||||
.vite
|
||||
_ref/
|
||||
*_recalc_verify.xlsx
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hinterstein Mieterportal – Deploy auf hocks.eu (Workflow aus dem Schulportal)
|
||||
#
|
||||
# 0. Anleitungs-Stand aktualisieren (Hash + Datum der ausgelieferten Version)
|
||||
# 1. npm run build 2. Paket (dist + compose + nginx + server) hochladen
|
||||
# 3. Entpacken nach /opt/mieterportal, Container bauen & ersetzen
|
||||
# 4. Status prüfen (Container, Portal-Hash, API-Health)
|
||||
@@ -18,10 +19,13 @@ PSCP="/c/Program Files/PuTTY/pscp.exe"
|
||||
PLINK="/c/Program Files/PuTTY/plink.exe"
|
||||
REMOTE_DIR="/opt/mieterportal"
|
||||
|
||||
echo "==> 1/4 Frontend bauen"
|
||||
echo "==> 1/5 Anleitungs-Stand aktualisieren"
|
||||
node scripts/anleitung-meta.mjs
|
||||
|
||||
echo "==> 2/5 Frontend bauen"
|
||||
npm run build
|
||||
|
||||
echo "==> 2/4 Paket erstellen und hochladen"
|
||||
echo "==> 3/5 Paket erstellen und hochladen"
|
||||
rm -rf tmp-deploy && mkdir -p tmp-deploy/docker/templates
|
||||
cp -r dist tmp-deploy/dist
|
||||
cp docker-compose.yml tmp-deploy/
|
||||
@@ -34,7 +38,7 @@ rm -rf tmp-deploy
|
||||
mp-dist.tar.gz "$SERVER":/tmp/mp-dist.tar.gz
|
||||
rm -f mp-dist.tar.gz
|
||||
|
||||
echo "==> 3/4 Auf Server entpacken, API-Image bauen, Stack ersetzen"
|
||||
echo "==> 4/5 Auf Server entpacken, API-Image bauen, Stack ersetzen"
|
||||
"$PLINK" -batch -hostkey "$HOSTKEY" -i "$KEY" -P "$PORT" "$SERVER" "
|
||||
mkdir -p $REMOTE_DIR &&
|
||||
tar -xzf /tmp/mp-dist.tar.gz -C $REMOTE_DIR &&
|
||||
@@ -42,7 +46,7 @@ echo "==> 3/4 Auf Server entpacken, API-Image bauen, Stack ersetzen"
|
||||
cd $REMOTE_DIR && docker compose up -d --build
|
||||
"
|
||||
|
||||
echo "==> 4/4 Status prüfen"
|
||||
echo "==> 5/5 Status prüfen"
|
||||
sleep 5
|
||||
"$PLINK" -batch -hostkey "$HOSTKEY" -i "$KEY" -P "$PORT" "$SERVER" "
|
||||
docker ps --filter name=mp- --format 'table {{.Names}}\t{{.Status}}' &&
|
||||
|
||||
@@ -189,6 +189,16 @@ tar -czf mp-dist.tar.gz -C dist .
|
||||
|
||||
## 8. Verlauf / offene Punkte
|
||||
|
||||
**Erledigt 04.10. (v1.24):** Anleitungsseite /anleitung (Seite „Anleitung" im
|
||||
Menü, sichtbar für Mieter und Verwaltung). Inhalte rollenbasiert in
|
||||
src/data/anleitung.ts (Abschnitte je gruppe 'mieter'|'verwaltung'); Verwaltung
|
||||
hat Reiter „Für die Verwaltung" / „Mieter-Ansicht". Stand-Badge (Datum + Build-
|
||||
Hash) kommt aus src/data/anleitung-meta.ts, die von scripts/anleitung-meta.mjs
|
||||
erzeugt wird – Aufruf vor jedem Build: als package.json „prebuild"-Hook (läuft
|
||||
also bei JEDEM npm run build mit) plus eigener Schritt 1 in deploy.sh (1/5…5/5).
|
||||
Meta-Datei ist gitignored (sonst Hash-Henne-Ei: Committen ändert den Hash).
|
||||
Dunkelmodus-taugliche Klassen beachten.
|
||||
|
||||
**Erledigt 04.10. (v1.23):** Nebenkostenabrechnung als PDF direkt aus dem
|
||||
Kosten- und Zählerstandsmodul. Neues Modul `server/src/abrechnung.js`
|
||||
(pdfkit, neue Dependency – Achtung: Standardfont Helvetica/WinAnsi, kein
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"prebuild": "node scripts/anleitung-meta.mjs",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Aktualisiert den Build-Stand der Anleitungsseite.
|
||||
// Wird bei jedem Deploy (deploy.sh, Schritt 1) VOR dem Frontend-Build aufgerufen,
|
||||
// damit Anleitung und ausgelieferte Version garantiert zusammenpassen.
|
||||
import { execSync } from 'node:child_process'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
|
||||
const hash = execSync('git rev-parse --short HEAD').toString().trim()
|
||||
const datum = new Date().toLocaleString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
})
|
||||
|
||||
const inhalt = `// Von scripts/anleitung-meta.mjs erzeugt – nicht von Hand ändern.
|
||||
// Wird bei jedem Deploy vor dem Build aktualisiert.
|
||||
export const anleitungMeta = {
|
||||
hash: '${hash}',
|
||||
datum: '${datum}',
|
||||
}
|
||||
`
|
||||
|
||||
writeFileSync('src/data/anleitung-meta.ts', inhalt)
|
||||
console.log(`Anleitungs-Stand aktualisiert: Build ${hash} · ${datum}`)
|
||||
@@ -13,6 +13,7 @@ import Kosten from './pages/Kosten'
|
||||
import Bestand from './pages/Bestand'
|
||||
import BenutzerVerwaltung from './pages/BenutzerVerwaltung'
|
||||
import Profil from './pages/Profil'
|
||||
import Anleitung from './pages/Anleitung'
|
||||
|
||||
/** Fängt Render-Fehler ab – statt schwarzem Bildschirm: Hinweis + Reload. */
|
||||
class FehlerGrenze extends Component<{ children: ReactNode }, { fehler: boolean }> {
|
||||
@@ -89,6 +90,7 @@ export default function App() {
|
||||
<Route path="/bestand" element={<Bestand />} />
|
||||
<Route path="/dokumente" element={<Documents />} />
|
||||
<Route path="/nachrichten" element={<Messages />} />
|
||||
<Route path="/anleitung" element={<Anleitung />} />
|
||||
<Route path="/profil" element={<Profil />} />
|
||||
<Route path="/benutzer" element={<BenutzerVerwaltung />} />
|
||||
</Route>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Gauge,
|
||||
Calculator,
|
||||
UserRound,
|
||||
CircleHelp,
|
||||
} from 'lucide-react'
|
||||
import { useStore } from '../store/store'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
@@ -24,6 +25,7 @@ const navItems = [
|
||||
{ to: '/kosten', label: 'Steuer & Kosten', icon: Calculator },
|
||||
{ to: '/dokumente', label: 'Dokumente', icon: FileText },
|
||||
{ to: '/nachrichten', label: 'Nachrichten', icon: MessagesSquare },
|
||||
{ to: '/anleitung', label: 'Anleitung', icon: CircleHelp },
|
||||
{ to: '/profil', label: 'Mein Profil', icon: UserRound },
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Inhalt der Anleitungsseite (/anleitung).
|
||||
// Pro Abschnitt: Zielgruppe, Titel, kurze Punkte. Wird auf der Seite
|
||||
// rollenbasiert gefiltert (Mieter sehen nur Mieter-Inhalte).
|
||||
|
||||
export type AnleitungGruppe = 'mieter' | 'verwaltung'
|
||||
|
||||
export interface AnleitungPunkt {
|
||||
titel: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface AnleitungAbschnitt {
|
||||
gruppe: AnleitungGruppe
|
||||
titel: string
|
||||
punkte: AnleitungPunkt[]
|
||||
}
|
||||
|
||||
export const anleitungIntro: Record<AnleitungGruppe, string> = {
|
||||
mieter:
|
||||
'Willkommen im Mieterportal der Hinterstein Immobilien. Hier finden Sie eine Kurzanleitung ' +
|
||||
'zu allen Funktionen, die Ihnen als Mieter zur Verfügung stehen.',
|
||||
verwaltung:
|
||||
'Arbeitshilfe für die Verwaltung: Aufbau und Bedienung aller Verwaltungsfunktionen im Portal. ' +
|
||||
'Der Reiter „Mieter-Ansicht“ zeigt, was Mieter sehen.',
|
||||
}
|
||||
|
||||
export const anleitungAbschnitte: AnleitungAbschnitt[] = [
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Anmeldung & Zugang',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Einloggen',
|
||||
text: 'Auf der Startseite Benutzername und Passwort eingeben. Die Anmeldung bleibt im Browser erhalten; über „Abmelden“ in der Seitenleiste wird die Sitzung beendet.',
|
||||
},
|
||||
{
|
||||
titel: 'Passwort',
|
||||
text: 'Das Passwort erhalten Sie von der Verwaltung. Ändern Sie es nach dem ersten Login in „Mein Profil“.',
|
||||
},
|
||||
{
|
||||
titel: 'Passwort vergessen',
|
||||
text: 'Bitte die Verwaltung kontaktieren (joseph@hinterstein-immo.de) – sie setzt ein neues Passwort.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Dashboard',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Alles auf einen Blick',
|
||||
text: 'Ankündigungen der Verwaltung, offene Beträge und ungelesene Nachrichten finden Sie auf der Startseite nach dem Login.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Mietkonto & Zahlungen',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Saldo prüfen',
|
||||
text: 'Aktueller Kontostand, offene und erledigte Buchungen Ihrer Wohnung sind unter „Mietkonto & Zahlungen“ einsehbar.',
|
||||
},
|
||||
{
|
||||
titel: 'Überweisen',
|
||||
text: 'Überweisen Sie offene Beträge an die Bankverbindung der Verwaltung. Eingehende Zahlungen werden von der Verwaltung gebucht.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Schäden melden',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Schaden erfassen',
|
||||
text: 'Unter „Schadensmeldungen“ einen neuen Schaden mit Wohnung, Kategorie, Beschreibung und Foto melden.',
|
||||
},
|
||||
{
|
||||
titel: 'Automatische Benachrichtigung',
|
||||
text: 'Zu jeder Meldung wird automatisch eine Nachricht eröffnet und die Verwaltung per E-Mail informiert – Sie erhalten eine Bestätigungsmail.',
|
||||
},
|
||||
{
|
||||
titel: 'Verlauf verfolgen',
|
||||
text: 'Antworten und Status der Verarbeitung finden Sie im Nachrichten-Modul. Neue Antworten kommen zusätzlich per E-Mail.',
|
||||
},
|
||||
{
|
||||
titel: 'Erstantwort',
|
||||
text: 'Die erste Rückmeldung kann automatisch von einer KI kommen und wird anschließend von der Verwaltung geprüft.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Zählerstände',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Stände eintragen',
|
||||
text: 'Unter „Zählerstände“ sehen Sie die Strom-, Wasser- und Gaszähler Ihrer Wohnung und tragen neue Stände mit Datum ein.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Dokumente',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Unterlagen abrufen',
|
||||
text: 'Mietvertrag, Nebenkostenabrechnungen und weitere Dokumente Ihrer Wohnung können Sie hier ansehen und herunterladen.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Nachrichten',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Mit der Verwaltung schreiben',
|
||||
text: 'Alle Unterhaltungen mit der Verwaltung laufen gebündelt hier. Ungelesene Antworten sind im Menü mit einer Zahl gekennzeichnet.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'mieter',
|
||||
titel: 'Mein Profil',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Stammdaten pflegen',
|
||||
text: 'Halten Sie Telefonnummer, E-Mail-Adresse und Bankverbindung (Kontoinhaber, IBAN, Bankname) aktuell – die Verwaltung nutzt diese Daten für Abrechnung und Kontakt.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'verwaltung',
|
||||
titel: 'Benutzerverwaltung',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Mieter anlegen & pflegen',
|
||||
text: 'Neue Mieter mit Benutzername, Name und E-Mail-Adresse anlegen, bearbeiten oder deaktivieren. Jedem Mieter ist seine Wohneinheit zugeordnet.',
|
||||
},
|
||||
{
|
||||
titel: 'Passwort zurücksetzen',
|
||||
text: 'Bei „Passwort vergessen“ vergeben Sie ein neues Erst-Passwort (z. B. 0815) – der Mieter sollte es danach in „Mein Profil“ ändern.',
|
||||
},
|
||||
{
|
||||
titel: 'Registrierungen prüfen',
|
||||
text: 'Neue Registrierungen von Mietern werden automatisch per E-Mail an die Verwaltung gemeldet und hier geprüft.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'verwaltung',
|
||||
titel: 'Häuser & Wohnungen',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Haus anlegen',
|
||||
text: 'Pflichtangaben: Baujahr, Adresse, Etagenhöhe, Stockwerke, Anzahl Wohneinheiten und Quadratmeter.',
|
||||
},
|
||||
{
|
||||
titel: 'Wohnungen zuordnen',
|
||||
text: 'Jede Wohnung gehört zu einem Haus: Etage, Wohnfläche, Kalt-/Warmmiete, Mieter und Status (vermietet/frei) je Einheit pflegen. Der Bestand erscheint anschließend im Dashboard.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'verwaltung',
|
||||
titel: 'Steuer & Kosten',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Kosten erfassen',
|
||||
text: 'Kostenpositionen je Haus oder Wohnung anlegen und als umlegbar/nicht umlegbar markieren – Grundlage der Nebenkostenabrechnung.',
|
||||
},
|
||||
{
|
||||
titel: 'Belege aus Spark Receipt',
|
||||
text: 'Gescannte Belege werden per OCR analysiert und primär über die Stromzählernummer, sekundär über die Adresse der Wohnung zugeordnet.',
|
||||
},
|
||||
{
|
||||
titel: 'Abrechnung erzeugen',
|
||||
text: 'Im Tab „Kosten“ über „Abrechnung erzeugen“ Wohnung und Jahr wählen: Das Portal erstellt eine druckfertige Nebenkostenabrechnung als PDF und legt sie im Dokumentenmodul ab.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'verwaltung',
|
||||
titel: 'Schadensmeldungen & Kommunikation',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Meldungen bearbeiten',
|
||||
text: 'Jede Schadensmeldung startet automatisch eine Nachricht. Ihre Antworten werden per E-Mail an den Mieter versendet.',
|
||||
},
|
||||
{
|
||||
titel: 'KI-Unterstützung (n8n)',
|
||||
text: 'Das Automatisierungssystem erzeugt zu Schadensmeldungen automatisch eine Antwort, die ins Portal übernommen und von Ihnen geprüft bzw. ergänzt wird.',
|
||||
},
|
||||
{
|
||||
titel: 'Direkt-E-Mail an Mieter',
|
||||
text: 'Im Nachrichten-Modul können Sie E-Mails direkt an Mieter senden; die Verwaltung erhält automatisch eine Kopie (CC).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'verwaltung',
|
||||
titel: 'Dokumente & Zählerstände',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Dokumente hochladen',
|
||||
text: 'PDFs (z. B. Mietverträge, Abrechnungen) hochladen und Kategorie sowie Wohnung zuordnen. Mieter sehen nur Dokumente ihrer eigenen Wohnung.',
|
||||
},
|
||||
{
|
||||
titel: 'Zähler pflegen',
|
||||
text: 'Zähler je Wohnung mit Zählernummer und Zählerart anlegen und die von Mietern erfassten Stände prüfen.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
gruppe: 'verwaltung',
|
||||
titel: 'Darstellung',
|
||||
punkte: [
|
||||
{
|
||||
titel: 'Hell / Dunkel',
|
||||
text: 'Das Design lässt sich über den Schalter in der Seitenleiste (Desktop) bzw. oben rechts (Mobil) zwischen hellem und dunklem Modus wechseln.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
Building2,
|
||||
Calculator,
|
||||
CircleHelp,
|
||||
FileText,
|
||||
Gauge,
|
||||
Info,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
MessagesSquare,
|
||||
Paintbrush,
|
||||
UserRound,
|
||||
Users,
|
||||
Wallet,
|
||||
Wrench,
|
||||
} from 'lucide-react'
|
||||
import { useStore } from '../store/store'
|
||||
import {
|
||||
anleitungAbschnitte,
|
||||
anleitungIntro,
|
||||
type AnleitungAbschnitt,
|
||||
type AnleitungGruppe,
|
||||
} from '../data/anleitung'
|
||||
import { anleitungMeta } from '../data/anleitung-meta'
|
||||
|
||||
const Icons: Record<string, typeof Info> = {
|
||||
'Anmeldung & Zugang': KeyRound,
|
||||
Dashboard: LayoutDashboard,
|
||||
'Mietkonto & Zahlungen': Wallet,
|
||||
'Schäden melden': Wrench,
|
||||
Zählerstände: Gauge,
|
||||
Dokumente: FileText,
|
||||
Nachrichten: MessagesSquare,
|
||||
'Mein Profil': UserRound,
|
||||
Benutzerverwaltung: Users,
|
||||
'Häuser & Wohnungen': Building2,
|
||||
'Steuer & Kosten': Calculator,
|
||||
'Schadensmeldungen & Kommunikation': MessagesSquare,
|
||||
'Dokumente & Zählerstände': FileText,
|
||||
Darstellung: Paintbrush,
|
||||
}
|
||||
|
||||
function AbschnittKarte({ abschnitt }: { abschnitt: AnleitungAbschnitt }) {
|
||||
const Icon = Icons[abschnitt.titel] ?? Info
|
||||
return (
|
||||
<section className="rounded-2xl border bg-white p-5 shadow-sm dark:bg-slate-900">
|
||||
<h2 className="flex items-center gap-2 font-semibold">
|
||||
<span className="rounded-xl bg-teal-50 p-2 dark:bg-teal-950">
|
||||
<Icon className="h-4.5 w-4.5 text-teal-700 dark:text-teal-300" />
|
||||
</span>
|
||||
{abschnitt.titel}
|
||||
</h2>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{abschnitt.punkte.map((p) => (
|
||||
<li key={p.titel} className="text-sm">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{p.titel}</div>
|
||||
<p className="mt-0.5 leading-relaxed text-slate-500 dark:text-slate-400">{p.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Anleitung() {
|
||||
const { role } = useStore()
|
||||
const istVerwaltung = role === 'admin'
|
||||
const [tab, setTab] = useState<AnleitungGruppe>('verwaltung')
|
||||
|
||||
// Mieter sehen nur Mieter-Inhalte; die Verwaltung wählt zwischen beiden Ansichten.
|
||||
const gruppe: AnleitungGruppe = istVerwaltung ? tab : 'mieter'
|
||||
|
||||
const abschnitte = useMemo(
|
||||
() => anleitungAbschnitte.filter((a) => a.gruppe === gruppe),
|
||||
[gruppe],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="flex items-center gap-2 text-2xl font-bold tracking-tight">
|
||||
<CircleHelp className="h-6 w-6 text-teal-700 dark:text-teal-300" />
|
||||
Anleitung
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{anleitungIntro[gruppe]}</p>
|
||||
</div>
|
||||
<div className="rounded-full border bg-white px-4 py-1.5 text-xs text-slate-500 shadow-sm dark:bg-slate-900 dark:text-slate-400">
|
||||
Stand {anleitungMeta.datum} · Build {anleitungMeta.hash}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{istVerwaltung && (
|
||||
<div className="flex gap-2 border-b border-slate-200 dark:border-slate-800">
|
||||
{([['verwaltung', 'Für die Verwaltung'], ['mieter', 'Mieter-Ansicht']] as const).map(([wert, label]) => (
|
||||
<button
|
||||
key={wert}
|
||||
onClick={() => setTab(wert)}
|
||||
className={`-mb-px border-b-2 px-4 py-2 text-sm font-medium transition ${
|
||||
tab === wert
|
||||
? 'border-teal-600 text-teal-700 dark:text-teal-300'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{abschnitte.map((a) => (
|
||||
<AbschnittKarte key={a.titel} abschnitt={a} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user