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

@@ -3,7 +3,6 @@
import { useEffect, useState } from "react";
import { Navbar } from "@/components/public/Navbar";
import { HeroSection } from "@/components/public/HeroSection";
import { KnowledgeCards } from "@/components/public/KnowledgeCards";
import { AboutSection } from "@/components/public/AboutSection";
import { CommunityLinksSection } from "@/components/public/CommunityLinksSection";
import { MeetupsSection } from "@/components/public/MeetupsSection";
@@ -11,6 +10,7 @@ import { FAQSection } from "@/components/public/FAQSection";
import { FinalCTASection } from "@/components/public/FinalCTASection";
import { Footer } from "@/components/public/Footer";
import { api } from "@/lib/api";
import { formatMeetupCivilDate, getMeetupStartUtc } from "@/lib/meetupEventTime";
export default function HomePage() {
const [meetup, setMeetup] = useState<any>(null);
@@ -22,10 +22,18 @@ export default function HomePage() {
.then((data: any) => {
const all = Array.isArray(data) ? data : data?.meetups ?? [];
const now = new Date();
// Keep only PUBLISHED events with a future date, sorted closest-first
// Keep only PUBLISHED events with a future start (Brussels wall time → UTC), sorted closest-first
const upcoming = all
.filter((m: any) => m.status === "PUBLISHED" && m.date && new Date(m.date) > now)
.sort((a: any, b: any) => new Date(a.date).getTime() - new Date(b.date).getTime());
.filter((m: any) => {
if (m.status !== "PUBLISHED" || !m.date) return false;
const start = getMeetupStartUtc(m.date, m.time || "00:00");
return !Number.isNaN(start.getTime()) && start > now;
})
.sort(
(a: any, b: any) =>
getMeetupStartUtc(a.date, a.time || "00:00").getTime() -
getMeetupStartUtc(b.date, b.time || "00:00").getTime()
);
setAllMeetups(upcoming);
if (upcoming.length > 0) setMeetup(upcoming[0]);
})
@@ -36,11 +44,14 @@ export default function HomePage() {
.catch(() => {});
}, []);
const featuredCivil = meetup ? formatMeetupCivilDate(meetup.date) : null;
const meetupProps = meetup
? {
id: meetup.id,
month: new Date(meetup.date).toLocaleString("en-US", { month: "short" }),
day: String(new Date(meetup.date).getDate()),
month: featuredCivil
? featuredCivil.monthShort.charAt(0) + featuredCivil.monthShort.slice(1).toLowerCase()
: "TBD",
day: featuredCivil?.day ?? "--",
title: meetup.title,
location: meetup.location,
time: meetup.time,
@@ -57,7 +68,6 @@ export default function HomePage() {
<section id="about">
<AboutSection />
</section>
<KnowledgeCards />
<CommunityLinksSection settings={settings} />
<section id="upcoming-meetups">
<MeetupsSection meetups={allMeetups} />