Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5c755ab
docs(events): Spec 2 design for e-Ventschau Events module
mrwind-up-bird Jun 15, 2026
17aabb9
docs(events): Spec 2 implementation plan (25 tasks)
mrwind-up-bird Jun 16, 2026
21d7ee7
feat(events): add Event/Stage/Appearance/PriceTier schema
mrwind-up-bird Jun 16, 2026
fcd812c
refactor(events): extract shared slug/url validators to slug.ts
mrwind-up-bird Jun 16, 2026
0c2e4f8
docs(events): sync plan slug.ts block with Nordic mappings
mrwind-up-bird Jun 16, 2026
4d961e9
feat(events): event/stage/appearance/tier validation
mrwind-up-bird Jun 16, 2026
2036883
feat(events): tenant-scoped event getters + lineup coupling
mrwind-up-bird Jun 16, 2026
2537bee
feat(events): buildEventJsonLd (schema.org MusicEvent)
mrwind-up-bird Jun 16, 2026
1e1160f
feat(events): admin events collection route + shared auth helper
mrwind-up-bird Jun 16, 2026
eaaca07
feat(events): admin event item route (GET/PUT/DELETE + tier replace-all)
mrwind-up-bird Jun 16, 2026
657a634
feat(events): granular stage sub-resource routes
mrwind-up-bird Jun 17, 2026
aa45a1d
feat(events): granular appearance sub-resource routes (XOR + IDOR gua…
mrwind-up-bird Jun 17, 2026
0dccb97
feat(events): EventCard list component
mrwind-up-bird Jun 17, 2026
ce23db6
feat(events): public /events list page
mrwind-up-bird Jun 17, 2026
ec0cc09
feat(events): responsive timetable (desktop grid + mobile agenda)
mrwind-up-bird Jun 17, 2026
a77109d
feat(events): public event detail page (timetable/lineup/prices/JSON-LD)
mrwind-up-bird Jun 17, 2026
5efbe14
feat(events): wire up artist Auftritte section
mrwind-up-bird Jun 17, 2026
3798c87
feat(events): add events to sitemap
mrwind-up-bird Jun 17, 2026
8a1fc4e
feat(events): admin nav Events entry
mrwind-up-bird Jun 17, 2026
c820306
feat(events): admin events list page
mrwind-up-bird Jun 17, 2026
62b454e
feat(events): admin event create page
mrwind-up-bird Jun 17, 2026
b7cc8b1
feat(events): admin stage manager (granular endpoints)
mrwind-up-bird Jun 17, 2026
aa5492f
feat(events): admin timetable builder (granular appearance endpoints)
mrwind-up-bird Jun 17, 2026
494991f
feat(events): admin event edit page (core/desc/hero/SEO/tiers)
mrwind-up-bird Jun 17, 2026
3392382
feat(events): couple homepage lineup to featured event + repoint Prog…
mrwind-up-bird Jun 17, 2026
247bc38
feat(events): redirect /programm-2026 to canonical event page
mrwind-up-bird Jun 17, 2026
166426d
fix(events): render festival dates/times in Europe/Berlin timezone
mrwind-up-bird Jun 17, 2026
5c14594
feat(events): seed e-Ventschau 2026 festival + nav (Events item, repo…
mrwind-up-bird Jun 17, 2026
99a4365
fix(events): prevent note/data loss, status-check admin loads, add SE…
mrwind-up-bird Jun 17, 2026
20ec0e5
feat(events): set artist hero photos from e-ventschau.de
mrwind-up-bird Jun 17, 2026
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
2,840 changes: 2,840 additions & 0 deletions docs/superpowers/plans/2026-06-15-events-modul.md

Large diffs are not rendered by default.

282 changes: 282 additions & 0 deletions docs/superpowers/specs/2026-06-15-e-ventschau-events-module-design.md

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ model Tenant {
categories ProductCategory[]
pageVersions PageVersion[]
artists Artist[]
events Event[]
}

model SiteSettings {
Expand Down Expand Up @@ -643,6 +644,7 @@ model Artist {
tenant Tenant @relation(fields: [tenantId], references: [id])
media ArtistMedia[]
sliderItems SliderItem[]
appearances Appearance[]

@@unique([tenantId, slug])
@@index([tenantId])
Expand All @@ -665,3 +667,95 @@ model ArtistMedia {

@@index([artistId])
}

model Event {
id String @id @default(cuid())
tenantId String
slug String
title String
subtitle String?
eventType String @default("festival")
startDate DateTime
endDate DateTime?
locationName String?
locationAddress String?
locationUrl String?
heroImage String?
excerpt String? @db.Text
description String? @db.Text
descriptionJson Json?
editorMode String? @default("markdown")
ticketUrl String?
metaTitle String?
metaDescription String? @db.Text
isPublished Boolean @default(false)
isFeatured Boolean @default(false)
isActive Boolean @default(true)
sortOrder Int @default(0)
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

tenant Tenant @relation(fields: [tenantId], references: [id])
stages Stage[]
appearances Appearance[]
priceTiers PriceTier[]

@@unique([tenantId, slug])
@@index([tenantId])
@@index([startDate])
}

model Stage {
id String @id @default(cuid())
eventId String
name String
color String?
sortOrder Int @default(0)

event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
appearances Appearance[]

@@index([eventId])
}

model Appearance {
id String @id @default(cuid())
eventId String
stageId String
artistId String?
title String?
role String @default("support")
startTime DateTime
endTime DateTime?
note String?
sortOrder Int @default(0)

event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
artist Artist? @relation(fields: [artistId], references: [id], onDelete: SetNull)

@@index([eventId])
@@index([stageId])
@@index([artistId])
}

model PriceTier {
id String @id @default(cuid())
eventId String
name String
description String?
price Float?
currency String @default("EUR")
validFrom DateTime?
validUntil DateTime?
isSoldOut Boolean @default(false)
isActive Boolean @default(true)
buyUrl String?
sortOrder Int @default(0)

event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)

@@index([eventId])
}
106 changes: 90 additions & 16 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,19 @@ async function main() {
data: { tenantId: tenant.id, label: 'Über uns', href: '/informationen', sortOrder: 1 },
})
await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Programm 2026', href: '/programm-2026', sortOrder: 2 },
data: { tenantId: tenant.id, label: 'Programm 2026', href: '/events/e-ventschau-2026', sortOrder: 2 },
})

await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Künstler', href: '/kuenstler', sortOrder: 3 },
data: { tenantId: tenant.id, label: 'Events', href: '/events', sortOrder: 3 },
})

await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Künstler', href: '/kuenstler', sortOrder: 4 },
})

const rueckschau = await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Rückschau', href: '/rueckschau', sortOrder: 4 },
data: { tenantId: tenant.id, label: 'Rückschau', href: '/rueckschau', sortOrder: 5 },
})
await prisma.menuItem.createMany({
data: [
Expand All @@ -135,11 +139,11 @@ async function main() {
})

await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Presse', href: '/presse', sortOrder: 5 },
data: { tenantId: tenant.id, label: 'Presse', href: '/presse', sortOrder: 6 },
})

const unterstuetzung = await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Unterstützung', href: '/unterstuetzung', sortOrder: 6 },
data: { tenantId: tenant.id, label: 'Unterstützung', href: '/unterstuetzung', sortOrder: 7 },
})
await prisma.menuItem.createMany({
data: [
Expand All @@ -149,7 +153,7 @@ async function main() {
})

await prisma.menuItem.create({
data: { tenantId: tenant.id, label: 'Kontakt', href: '/kontakt', sortOrder: 7 },
data: { tenantId: tenant.id, label: 'Kontakt', href: '/kontakt', sortOrder: 8 },
})

// Footer
Expand All @@ -158,7 +162,7 @@ async function main() {
})
await prisma.menuItem.createMany({
data: [
{ tenantId: tenant.id, parentId: footerFestival.id, label: 'Programm 2026', href: '/programm-2026', location: 'footer', sortOrder: 0 },
{ tenantId: tenant.id, parentId: footerFestival.id, label: 'Programm 2026', href: '/events/e-ventschau-2026', location: 'footer', sortOrder: 0 },
{ tenantId: tenant.id, parentId: footerFestival.id, label: 'Über uns', href: '/informationen', location: 'footer', sortOrder: 1 },
{ tenantId: tenant.id, parentId: footerFestival.id, label: 'Rückschau', href: '/rueckschau', location: 'footer', sortOrder: 2 },
{ tenantId: tenant.id, parentId: footerFestival.id, label: 'Festival-Filme', href: '/festival-filme', location: 'footer', sortOrder: 3 },
Expand Down Expand Up @@ -190,26 +194,96 @@ async function main() {

// ── Artists (Line-up 2026) ──────────────────────────────
const lineup = [
{ slug: 'thorbjorn-risager', name: 'Thorbjørn Risager & The Black Tornado', origin: 'Dänemark', genres: ['Blues Rock', 'Soul'], featured: true },
{ slug: 'lebron-johnson', name: 'Lebron Johnson', origin: 'Italien', genres: ['Rock', 'Funk'], featured: true },
{ slug: 'killabeatmaker', name: 'Killabeatmaker', origin: 'Kolumbien', genres: ['Latin', 'Electronic'], featured: false },
{ slug: 'jed-thomas-band', name: 'Jed Thomas Band', origin: 'Großbritannien', genres: ['Heavy Blues Rock'], featured: false },
{ slug: 'rovar', name: 'ROVAR', origin: 'Münster', genres: ['Stoner', '70s Rock'], featured: false },
{ slug: 'nanny-goats', name: 'Nanny Goats', origin: 'Lüneburg', genres: ['Semi-Acoustic'], featured: false },
{ slug: 'the-klaxon', name: 'The Klaxon', origin: 'Kolumbien', genres: ['Ska', 'Latin'], featured: false },
{ slug: 'thorbjorn-risager', name: 'Thorbjørn Risager & The Black Tornado', origin: 'Dänemark', genres: ['Blues Rock', 'Soul'], featured: true, heroImage: 'https://e-ventschau.de/wp-content/uploads/2026/05/Thorbjorn-Risager-The-Black-Tornado-Daenemark-scaled.jpg' },
{ slug: 'lebron-johnson', name: 'Lebron Johnson', origin: 'Italien', genres: ['Rock', 'Funk'], featured: true, heroImage: 'https://e-ventschau.de/wp-content/uploads/2026/05/Lebron-Johnson-Italien.jpg' },
{ slug: 'killabeatmaker', name: 'Killabeatmaker', origin: 'Kolumbien', genres: ['Latin', 'Electronic'], featured: false, heroImage: 'https://e-ventschau.de/wp-content/uploads/2026/05/BMB4449-scaled-1-scaled.jpg' },
{ slug: 'jed-thomas-band', name: 'Jed Thomas Band', origin: 'Großbritannien', genres: ['Heavy Blues Rock'], featured: false, heroImage: 'https://e-ventschau.de/wp-content/uploads/2023/05/Jed-Thomas_small_CR_JRoberts.jpg' },
{ slug: 'rovar', name: 'ROVAR', origin: 'Münster', genres: ['Stoner', '70s Rock'], featured: false, heroImage: 'https://e-ventschau.de/wp-content/uploads/2026/05/Rovar-1.jpg' },
{ slug: 'nanny-goats', name: 'Nanny Goats', origin: 'Lüneburg', genres: ['Semi-Acoustic'], featured: false, heroImage: 'https://e-ventschau.de/wp-content/uploads/2026/05/Nanny-Goats-Sofa.jpg' },
{ slug: 'the-klaxon', name: 'The Klaxon', origin: 'Kolumbien', genres: ['Ska', 'Latin'], featured: false, heroImage: 'https://e-ventschau.de/wp-content/uploads/2026/05/The-Klaxon-2025-Edit.png' },
]
for (const [i, band] of lineup.entries()) {
await prisma.artist.upsert({
where: { tenantId_slug: { tenantId: tenant.id, slug: band.slug } },
update: { name: band.name, origin: band.origin, genres: band.genres, isFeatured: band.featured, isPublished: true },
update: { name: band.name, origin: band.origin, genres: band.genres, isFeatured: band.featured, isPublished: true, heroImage: band.heroImage },
create: {
tenantId: tenant.id, slug: band.slug, name: band.name, origin: band.origin,
genres: band.genres, isFeatured: band.featured, isPublished: true, isActive: true, sortOrder: i,
genres: band.genres, isFeatured: band.featured, isPublished: true, isActive: true, sortOrder: i, heroImage: band.heroImage,
},
})
}
console.log(`Artists seeded: ${lineup.length}`)

// ── Event: e-Ventschau 2026 ─────────────────────────────
const festival = await prisma.event.upsert({
where: { tenantId_slug: { tenantId: tenant.id, slug: 'e-ventschau-2026' } },
update: {
title: 'e-Ventschau 2026', subtitle: '11. Benefiz-Festival',
startDate: new Date('2026-08-07T17:00:00+02:00'), endDate: new Date('2026-08-08T23:59:00+02:00'),
locationName: 'Resthof Thiele, Ventschau', locationAddress: '21368 Ventschau, Landkreis Lüneburg',
excerpt: 'Zwei Tage internationale Live-Musik für den guten Zweck.',
eventType: 'festival', isPublished: true, isFeatured: true,
},
create: {
tenantId: tenant.id, slug: 'e-ventschau-2026',
title: 'e-Ventschau 2026', subtitle: '11. Benefiz-Festival', eventType: 'festival',
startDate: new Date('2026-08-07T17:00:00+02:00'), endDate: new Date('2026-08-08T23:59:00+02:00'),
locationName: 'Resthof Thiele, Ventschau', locationAddress: '21368 Ventschau, Landkreis Lüneburg',
excerpt: 'Zwei Tage internationale Live-Musik für den guten Zweck.',
isPublished: true, isFeatured: true, isActive: true, sortOrder: 0,
},
})

// children: delete-then-recreate (idempotent re-seed)
await prisma.appearance.deleteMany({ where: { eventId: festival.id } })
await prisma.priceTier.deleteMany({ where: { eventId: festival.id } })
await prisma.stage.deleteMany({ where: { eventId: festival.id } })

const haupt = await prisma.stage.create({ data: { eventId: festival.id, name: 'Hauptbühne', color: '#b87333', sortOrder: 0 } })
const zelt = await prisma.stage.create({ data: { eventId: festival.id, name: 'Zeltbühne', color: '#7c9a6c', sortOrder: 1 } })

const bySlug = (slug: string) =>
prisma.artist.findUnique({ where: { tenantId_slug: { tenantId: tenant.id, slug } } })

const [risager, lebron, killa, jed, rovar, nanny, klaxon] = await Promise.all([
bySlug('thorbjorn-risager'), bySlug('lebron-johnson'), bySlug('killabeatmaker'),
bySlug('jed-thomas-band'), bySlug('rovar'), bySlug('nanny-goats'), bySlug('the-klaxon'),
])

const eventAppearances: { stageId: string; artistId?: string; title?: string; role: string; startTime: Date }[] = [
// Freitag 07.08.
{ stageId: haupt.id, artistId: nanny?.id, role: 'support', startTime: new Date('2026-08-07T18:00:00+02:00') },
{ stageId: haupt.id, artistId: rovar?.id, role: 'support', startTime: new Date('2026-08-07T19:00:00+02:00') },
{ stageId: zelt.id, artistId: klaxon?.id, role: 'support', startTime: new Date('2026-08-07T19:00:00+02:00') },
{ stageId: haupt.id, artistId: jed?.id, role: 'support', startTime: new Date('2026-08-07T20:30:00+02:00') },
{ stageId: zelt.id, artistId: killa?.id, role: 'guest', startTime: new Date('2026-08-07T20:30:00+02:00') },
{ stageId: haupt.id, artistId: risager?.id, role: 'headliner', startTime: new Date('2026-08-07T22:00:00+02:00') },
// Samstag 08.08.
{ stageId: haupt.id, title: 'Soundcheck & Begrüßung', role: 'break', startTime: new Date('2026-08-08T17:30:00+02:00') },
{ stageId: haupt.id, artistId: lebron?.id, role: 'support', startTime: new Date('2026-08-08T19:00:00+02:00') },
{ stageId: zelt.id, artistId: nanny?.id, role: 'support', startTime: new Date('2026-08-08T19:00:00+02:00') },
{ stageId: haupt.id, artistId: killa?.id, role: 'headliner', startTime: new Date('2026-08-08T21:30:00+02:00') },
]
for (const [i, a] of eventAppearances.entries()) {
await prisma.appearance.create({
data: {
eventId: festival.id, stageId: a.stageId,
artistId: a.artistId ?? null, title: a.title ?? null,
role: a.role, startTime: a.startTime, sortOrder: i,
},
})
}

await prisma.priceTier.createMany({
data: [
{ eventId: festival.id, name: 'Festival-Pass (2 Tage)', price: 39, currency: 'EUR', sortOrder: 0 },
{ eventId: festival.id, name: 'Tagesticket', price: 22, currency: 'EUR', sortOrder: 1 },
{ eventId: festival.id, name: 'Early-Bird Festival-Pass', price: 29, currency: 'EUR', validFrom: new Date('2026-01-01T00:00:00+01:00'), validUntil: new Date('2026-04-30T23:59:00+02:00'), sortOrder: 2 },
{ eventId: festival.id, name: 'Förder-Ticket (Spende)', price: null, currency: 'EUR', sortOrder: 3 },
],
})
console.log(`Event seeded: e-Ventschau 2026 with ${eventAppearances.length} appearances`)

// ── Content pages from src/content/*.md ─────────────────
const contentDir = path.join(process.cwd(), 'src/content')
const mdFiles = fs.existsSync(contentDir)
Expand Down
Loading
Loading