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:
@@ -2,25 +2,17 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, MapPin, Clock, Calendar, ExternalLink } from "lucide-react";
|
||||
import { ArrowLeft, MapPin, Clock, Calendar, ExternalLink, Building2 } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
import { Navbar } from "@/components/public/Navbar";
|
||||
import { Footer } from "@/components/public/Footer";
|
||||
|
||||
function formatFullDate(dateStr: string) {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
import { UpcomingEventsCarousel } from "@/components/public/UpcomingEventsCarousel";
|
||||
import { formatMeetupCivilDate, formatMeetupCivilDateLong, getMeetupStartUtc } from "@/lib/meetupEventTime";
|
||||
|
||||
function DateBadge({ dateStr }: { dateStr: string }) {
|
||||
const d = new Date(dateStr);
|
||||
const month = d.toLocaleString("en-US", { month: "short" }).toUpperCase();
|
||||
const day = String(d.getDate());
|
||||
const civil = formatMeetupCivilDate(dateStr);
|
||||
const month = civil?.monthShort ?? "—";
|
||||
const day = civil?.day ?? "--";
|
||||
return (
|
||||
<div className="bg-zinc-800 rounded-xl px-4 py-3 text-center shrink-0 min-w-[60px]">
|
||||
<span className="block text-[11px] font-bold uppercase text-primary tracking-wider leading-none mb-1">
|
||||
@@ -61,7 +53,12 @@ export default function EventDetailClient({ id }: { id: string }) {
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const isPast = meetup ? new Date(meetup.date) < new Date() : false;
|
||||
const isPast = meetup
|
||||
? (() => {
|
||||
const start = getMeetupStartUtc(meetup.date, meetup.time || "00:00");
|
||||
return !Number.isNaN(start.getTime()) && start < new Date();
|
||||
})()
|
||||
: false;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -115,10 +112,35 @@ export default function EventDetailClient({ id }: { id: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mb-6 text-sm text-on-surface-variant">
|
||||
{meetup.organizer?.slug ? (
|
||||
<Link
|
||||
href={`/events/organizer/${meetup.organizer.slug}`}
|
||||
className="flex items-center gap-2 text-primary hover:underline underline-offset-4"
|
||||
>
|
||||
<Building2 size={15} className="text-primary/70 shrink-0" />
|
||||
<span>
|
||||
Organized by{" "}
|
||||
<span className="font-semibold">
|
||||
{meetup.organizer.name || "Belgian Bitcoin Embassy"}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 size={15} className="text-primary/70 shrink-0" />
|
||||
<span>
|
||||
Organized by{" "}
|
||||
{meetup.organizer?.name || "Belgian Bitcoin Embassy"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mb-10 text-sm text-on-surface-variant">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={15} className="text-primary/70 shrink-0" />
|
||||
{formatFullDate(meetup.date)}
|
||||
{formatMeetupCivilDateLong(meetup.date)}
|
||||
</div>
|
||||
{meetup.time && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -152,6 +174,8 @@ export default function EventDetailClient({ id }: { id: string }) {
|
||||
Register for this event <ExternalLink size={16} />
|
||||
</a>
|
||||
)}
|
||||
|
||||
<UpcomingEventsCarousel excludeId={meetup.id} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user