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:
@@ -51,10 +51,11 @@ export const api = {
|
||||
request<void>(`/posts/${id}`, { method: "DELETE" }),
|
||||
|
||||
// Meetups
|
||||
getMeetups: (params?: { status?: string; admin?: boolean }) => {
|
||||
getMeetups: (params?: { status?: string; admin?: boolean; organizerSlug?: string }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.status) searchParams.set("status", params.status);
|
||||
if (params?.admin) searchParams.set("admin", "true");
|
||||
if (params?.organizerSlug) searchParams.set("organizerSlug", params.organizerSlug);
|
||||
const qs = searchParams.toString();
|
||||
return request<any[]>(`/meetups${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
@@ -103,6 +104,17 @@ export const api = {
|
||||
deleteCategory: (id: string) =>
|
||||
request<void>(`/categories/${id}`, { method: "DELETE" }),
|
||||
|
||||
// Organizers
|
||||
getOrganizers: () => request<any[]>("/organizers"),
|
||||
getOrganizerBySlug: (slug: string) =>
|
||||
request<any>(`/organizers/by-slug/${encodeURIComponent(slug)}`),
|
||||
createOrganizer: (data: { name: string; slug: string }) =>
|
||||
request<any>("/organizers", { method: "POST", body: JSON.stringify(data) }),
|
||||
updateOrganizer: (id: string, data: { name?: string; slug?: string }) =>
|
||||
request<any>(`/organizers/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
||||
deleteOrganizer: (id: string) =>
|
||||
request<void>(`/organizers/${id}`, { method: "DELETE" }),
|
||||
|
||||
// Relays
|
||||
getRelays: () => request<any[]>("/relays"),
|
||||
addRelay: (data: { url: string; priority?: number }) =>
|
||||
|
||||
@@ -51,3 +51,44 @@ export function getMeetupStartUtc(dateStr: string, timeStr: string): Date {
|
||||
const utcStartH = startH - BRUSSELS_OFFSET_HOURS;
|
||||
return new Date(Date.UTC(year, month - 1, day, utcStartH, startM, 0));
|
||||
}
|
||||
|
||||
const UTC_CAL_OPTS = { timeZone: "UTC" } as const;
|
||||
|
||||
/**
|
||||
* Format the stored meetup calendar date (YYYY-MM-DD) for display without shifting
|
||||
* by the viewer's timezone. Date-only strings parsed as new Date("YYYY-MM-DD") are
|
||||
* UTC midnight and show the wrong local day in western zones.
|
||||
*/
|
||||
export function formatMeetupCivilDate(dateStr: string): {
|
||||
monthShort: string;
|
||||
day: string;
|
||||
full: string;
|
||||
} | null {
|
||||
const key = normalizeMeetupDateKey(dateStr);
|
||||
if (!key) return null;
|
||||
const parts = key.split("-").map(Number);
|
||||
const year = parts[0];
|
||||
const month = parts[1];
|
||||
const dayNum = parts[2];
|
||||
if (!year || !month || !dayNum) return null;
|
||||
|
||||
const ref = new Date(Date.UTC(year, month - 1, dayNum, 12, 0, 0));
|
||||
return {
|
||||
monthShort: ref
|
||||
.toLocaleString("en-US", { ...UTC_CAL_OPTS, month: "short" })
|
||||
.toUpperCase(),
|
||||
day: String(ref.getUTCDate()),
|
||||
full: ref.toLocaleString("en-US", {
|
||||
...UTC_CAL_OPTS,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Long single-line civil date for event detail (same rules as formatMeetupCivilDate). */
|
||||
export function formatMeetupCivilDateLong(dateStr: string): string {
|
||||
return formatMeetupCivilDate(dateStr)?.full ?? "—";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user