feat: organizers, meetups UI, Plausible analytics, and migration tooling

- Add organizer model/API, admin and public organizer pages, meetup cards
- Refresh events/home/contact; add calendar dialog and carousel components
- Optional Plausible via NEXT_PUBLIC_PLAUSIBLE_* env vars in root layout
- Prisma migration, seed updates, baseline-and-migrate script

Made-with: Cursor
This commit is contained in:
bbe
2026-04-04 21:55:34 +02:00
parent 586b572f73
commit 78271ea110
37 changed files with 1555 additions and 301 deletions

View File

@@ -0,0 +1,47 @@
-- CreateTable
CREATE TABLE "Organizer" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
CREATE UNIQUE INDEX "Organizer_slug_key" ON "Organizer"("slug");
INSERT INTO "Organizer" ("id", "name", "slug", "createdAt", "updatedAt")
VALUES (
'00000000-0000-4000-8000-000000000001',
'Belgian Bitcoin Embassy',
'belgian-bitcoin-embassy',
datetime('now'),
datetime('now')
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Meetup" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"date" TEXT NOT NULL,
"time" TEXT NOT NULL,
"location" TEXT NOT NULL,
"link" TEXT,
"imageId" TEXT,
"status" TEXT NOT NULL DEFAULT 'DRAFT',
"featured" BOOLEAN NOT NULL DEFAULT false,
"visibility" TEXT NOT NULL DEFAULT 'PUBLIC',
"organizerId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Meetup_organizerId_fkey" FOREIGN KEY ("organizerId") REFERENCES "Organizer" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Meetup" ("createdAt", "date", "description", "featured", "id", "imageId", "link", "location", "organizerId", "status", "time", "title", "updatedAt", "visibility")
SELECT "createdAt", "date", "description", "featured", "id", "imageId", "link", "location", '00000000-0000-4000-8000-000000000001', "status", "time", "title", "updatedAt", "visibility" FROM "Meetup";
DROP TABLE "Meetup";
ALTER TABLE "new_Meetup" RENAME TO "Meetup";
CREATE INDEX "Meetup_organizerId_idx" ON "Meetup"("organizerId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -17,8 +17,17 @@ model User {
updatedAt DateTime @updatedAt
}
model Organizer {
id String @id @default(uuid())
name String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
meetups Meetup[]
}
model Meetup {
id String @id @default(uuid())
id String @id @default(uuid())
title String
description String
date String
@@ -26,11 +35,13 @@ model Meetup {
location String
link String?
imageId String?
status String @default("DRAFT") // DRAFT, PUBLISHED, CANCELLED (Upcoming/Past derived from date)
featured Boolean @default(false)
visibility String @default("PUBLIC") // PUBLIC, HIDDEN
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
status String @default("DRAFT") // DRAFT, PUBLISHED, CANCELLED (Upcoming/Past derived from date)
featured Boolean @default(false)
visibility String @default("PUBLIC") // PUBLIC, HIDDEN
organizerId String
organizer Organizer @relation(fields: [organizerId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Media {

View File

@@ -56,6 +56,16 @@ async function main() {
});
}
const defaultOrganizer = await prisma.organizer.upsert({
where: { slug: 'belgian-bitcoin-embassy' },
update: {},
create: {
id: '00000000-0000-4000-8000-000000000001',
name: 'Belgian Bitcoin Embassy',
slug: 'belgian-bitcoin-embassy',
},
});
const existingMeetup = await prisma.meetup.findFirst({
where: { title: 'Monthly Bitcoin Meetup' },
});
@@ -70,8 +80,9 @@ async function main() {
time: '19:00',
location: 'Brussels, Belgium',
link: 'https://meetup.com/example',
status: 'UPCOMING',
status: 'PUBLISHED',
featured: true,
organizerId: defaultOrganizer.id,
},
});
}