Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 4 additions & 14 deletions src/app/(public)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getHomepageSections } from '@/lib/sections'
import { NOIR_DEFAULT_LAYOUT } from '@/lib/noir-home-defaults'
import FAQ from '@/components/sections/FAQ'
import { getFeaturedEvent } from '@/lib/events'
import { festivalFaqDefaults, formatFestivalDateLabel } from '@/lib/faq'
import { resolveFestivalFaq } from '@/lib/faq'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -55,17 +55,13 @@ export default async function HomePage() {
// SectionStructuredData render + emit on their own). This guarantees exactly
// one visible FAQ and one FAQPage in every layout.
const hasDbFaq = sections.some((s) => s.type === 'faq' && s.isVisible)
const dateLabel = featured
? formatFestivalDateLabel(featured.startDate, featured.endDate)
: '7. und 8. August 2026'
const location = featured?.locationName || 'Hof Thiele, Ventschau'
const faqItems = festivalFaqDefaults({ dateLabel, location })
const faq = resolveFestivalFaq(featured)

return (
<div className="nh">
<JsonLd data={websiteJsonLd} />
{festivalJsonLd && <JsonLd data={festivalJsonLd} />}
{!hasDbFaq && <JsonLd data={buildFaqJsonLd(faqItems)} />}
{!hasDbFaq && <JsonLd data={buildFaqJsonLd(faq.items)} />}
{sections.length > 0 ? (
// Editor-composed homepage: ordered/visible elements from /admin/sections
<HomepageSectionRenderer sections={sections} />
Expand All @@ -75,13 +71,7 @@ export default async function HomePage() {
NOIR_DEFAULT_LAYOUT.map((type) => <NoirElement key={type} type={type} />)
)}
{!hasDbFaq && (
<FAQ
data={{
title: 'Häufig gestellte Fragen',
subtitle: 'Alles Wichtige zu Termin, Ort, Anreise und Eintritt.',
items: faqItems,
}}
/>
<FAQ data={faq} />
)}
</div>
)
Expand Down
34 changes: 34 additions & 0 deletions src/app/admin/sections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,30 @@ export default function AdminSectionsPage() {
}
}

// Pre-fill the add-form with the current default homepage FAQ so an editor can
// review and save it as a real, editable `faq` section. Nothing is persisted
// until they hit Speichern (the normal create flow).
async function seedFaqFromDefaults() {
setError(null)
try {
const res = await fetch('/api/admin/sections/faq-defaults')
if (!res.ok) throw new Error('FAQ-Vorlage konnte nicht geladen werden')
const data = await res.json()
resetForm()
setFormType('faq')
setFormTitle(data.title || '')
setFormSubtitle(data.subtitle || '')
setFaqItems(
Array.isArray(data.items) && data.items.length > 0
? data.items
: [{ question: '', answer: '' }],
)
setShowAddForm(true)
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler')
}
}

// ---------------------------------------------------------------------------
// Section list detail helpers
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1233,6 +1257,16 @@ export default function AdminSectionsPage() {
Aktuelle Startseite übernehmen
</button>
)}
{!sections.some((s) => s.type === 'faq') && (
<button
onClick={seedFaqFromDefaults}
className="inline-flex items-center gap-1.5 px-4 py-2 glass text-brand-text rounded-xl text-sm font-medium hover:bg-brand-bg-dark transition-all"
title="Automatische Homepage-FAQ als bearbeitbare Sektion vorbefüllen"
>
<QuestionMarkCircleIcon className="w-4 h-4" />
FAQ aus Vorlage
</button>
)}
<button
onClick={() => {
resetForm()
Expand Down
27 changes: 27 additions & 0 deletions src/app/api/admin/sections/faq-defaults/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { getToken } from 'next-auth/jwt'
import { getFeaturedEvent } from '@/lib/events'
import { resolveFestivalFaq } from '@/lib/faq'

async function getSessionToken() {
const cookieStore = await cookies()
return getToken({
req: {
cookies: Object.fromEntries(cookieStore.getAll().map((c) => [c.name, c.value])),
} as any, // eslint-disable-line @typescript-eslint/no-explicit-any
secret: process.env.NEXTAUTH_SECRET,
})
}

// Read-only: returns the current default homepage FAQ (title/subtitle + Q&A
// items) so the admin can pre-fill and save an editable `faq` section from it.
export async function GET() {
const token = await getSessionToken()
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const featured = await getFeaturedEvent()
return NextResponse.json(resolveFestivalFaq(featured))
}
2 changes: 1 addition & 1 deletion src/components/noir/sections/NoirDonateSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function NoirDonateSection({ content }: { content?: NoirDonateCon
<div className="nh-lab">Tickets</div>
<h3>KombiTicket – beide Nächte</h3>
<p className="nh-card-sub">
Online buchen per PayPal. An der Abendkasse gilt: zahl, was du kannst – kein Mindestpreis.
Online buchen per PayPal.
</p>
<PayPalHostedButton
hostedButtonId={PAYPAL_TICKET_BUTTON}
Expand Down
28 changes: 27 additions & 1 deletion src/lib/__tests__/faq.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { formatFestivalDateLabel, festivalFaqDefaults } from '../faq'
import {
formatFestivalDateLabel,
festivalFaqDefaults,
resolveFestivalFaq,
HOMEPAGE_FAQ_TITLE,
HOMEPAGE_FAQ_SUBTITLE,
} from '../faq'

test('two-day range in one month → "7. und 8. August 2026"', () => {
const start = new Date('2026-08-07T10:00:00+02:00')
Expand Down Expand Up @@ -33,3 +39,23 @@ test('festivalFaqDefaults interpolates date + location and carries region terms'
assert.match(wo, /Niedersachsen/)
assert.match(wo, /Norddeutschland/)
})

test('resolveFestivalFaq: uses the featured event date + location', () => {
const featured = {
startDate: new Date('2026-08-07T10:00:00+02:00'),
endDate: new Date('2026-08-08T10:00:00+02:00'),
locationName: 'Hof Meyer, Ventschau',
}
const faq = resolveFestivalFaq(featured)
assert.equal(faq.title, HOMEPAGE_FAQ_TITLE)
assert.equal(faq.subtitle, HOMEPAGE_FAQ_SUBTITLE)
assert.equal(faq.items.length, 5)
assert.match(faq.items[0].answer, /7\. und 8\. August 2026/)
assert.match(faq.items[1].answer, /Hof Meyer, Ventschau/)
})

test('resolveFestivalFaq: falls back to defaults when no featured event', () => {
const faq = resolveFestivalFaq(null)
assert.match(faq.items[0].answer, /7\. und 8\. August 2026/)
assert.match(faq.items[1].answer, /Hof Thiele, Ventschau/)
})
20 changes: 20 additions & 0 deletions src/lib/faq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ export function formatFestivalDateLabel(start: Date, end: Date | null): string {
return `${day(start)}. ${month(start)} und ${day(end)}. ${month(end)} ${year(end)}`
}

/** Homepage FAQ heading + intro — shared by the public page and the admin seed. */
export const HOMEPAGE_FAQ_TITLE = 'Häufig gestellte Fragen'
export const HOMEPAGE_FAQ_SUBTITLE = 'Alles Wichtige zu Termin, Ort, Anreise und Eintritt.'

/** Resolve the homepage FAQ (title/subtitle + items) from the featured event,
* falling back to static defaults when no event is set. Pure — no Prisma. */
export function resolveFestivalFaq(
featured: { startDate: Date; endDate: Date | null; locationName: string | null } | null,
): { title: string; subtitle: string; items: FaqItem[] } {
const dateLabel = featured
? formatFestivalDateLabel(featured.startDate, featured.endDate)
: '7. und 8. August 2026'
const location = featured?.locationName || 'Hof Thiele, Ventschau'
return {
title: HOMEPAGE_FAQ_TITLE,
subtitle: HOMEPAGE_FAQ_SUBTITLE,
items: festivalFaqDefaults({ dateLabel, location }),
}
}

/** The default festival FAQ. The "Wo?" answer carries the broad region terms. */
export function festivalFaqDefaults(input: { dateLabel: string; location: string }): FaqItem[] {
const { dateLabel, location } = input
Expand Down