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

@@ -1,5 +1,7 @@
import { MapPin, Clock, ArrowRight, CalendarPlus } from "lucide-react";
import { MapPin, Clock, ArrowRight } from "lucide-react";
import Link from "next/link";
import { AddToCalendarButton } from "@/components/public/AddToCalendarDialog";
import { formatMeetupCivilDate } from "@/lib/meetupEventTime";
interface MeetupData {
id?: string;
@@ -9,21 +11,13 @@ interface MeetupData {
location?: string;
link?: string;
description?: string;
organizer?: { name: string; slug?: string };
}
interface MeetupsSectionProps {
meetups: MeetupData[];
}
function formatMeetupDate(dateStr: string) {
const d = new Date(dateStr);
return {
month: d.toLocaleString("en-US", { month: "short" }).toUpperCase(),
day: String(d.getDate()),
full: d.toLocaleString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" }),
};
}
export function MeetupsSection({ meetups }: MeetupsSectionProps) {
return (
<section className="py-24 px-8 border-t border-zinc-800/50">
@@ -36,14 +30,7 @@ export function MeetupsSection({ meetups }: MeetupsSectionProps) {
<h2 className="text-3xl font-black tracking-tight">Upcoming Meetups</h2>
</div>
<div className="hidden md:flex items-center gap-4">
<a
href="/calendar.ics"
title="Subscribe to get all future meetups automatically"
className="flex items-center gap-1.5 text-xs text-on-surface-variant/60 hover:text-primary border border-zinc-700 hover:border-primary/50 rounded-lg px-3 py-1.5 transition-all"
>
<CalendarPlus size={14} />
Add to Calendar
</a>
<AddToCalendarButton />
<Link
href="/events"
className="flex items-center gap-2 text-sm text-primary font-semibold hover:gap-3 transition-all"
@@ -62,7 +49,10 @@ export function MeetupsSection({ meetups }: MeetupsSectionProps) {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{meetups.map((meetup, i) => {
const { month, day, full } = formatMeetupDate(meetup.date);
const civil = formatMeetupCivilDate(meetup.date);
const month = civil?.monthShort ?? "—";
const day = civil?.day ?? "--";
const full = civil?.full ?? "";
const href = meetup.id ? `/events/${meetup.id}` : "#upcoming-meetups";
return (
@@ -92,6 +82,13 @@ export function MeetupsSection({ meetups }: MeetupsSectionProps) {
</p>
)}
<p className="text-[11px] text-on-surface-variant/50 font-medium uppercase tracking-wide mb-2">
Organized by{" "}
<span className="text-on-surface-variant/70 normal-case tracking-normal">
{meetup.organizer?.name || "Belgian Bitcoin Embassy"}
</span>
</p>
<div className="flex flex-col gap-1.5 mt-auto pt-4 border-t border-zinc-800/60">
{meetup.location && (
<p className="flex items-center gap-1.5 text-xs text-on-surface-variant/60">
@@ -123,13 +120,7 @@ export function MeetupsSection({ meetups }: MeetupsSectionProps) {
>
All events <ArrowRight size={16} />
</Link>
<a
href="/calendar.ics"
className="flex items-center gap-1.5 text-xs text-on-surface-variant/60 hover:text-primary border border-zinc-700 hover:border-primary/50 rounded-lg px-3 py-1.5 transition-all"
>
<CalendarPlus size={14} />
Add to Calendar
</a>
<AddToCalendarButton />
</div>
</div>
</section>