feat(admin-events): event timezone picker and zoned save/load
Add a USelectMenu for displayTimezone in Event Details (defaults to America/Toronto). On submit, convert each datetime-local string (startDate, endDate, registrationDeadline, earlyBirdDeadline) from the event's TZ to a UTC ISO string so the wall-clock time the admin entered is preserved regardless of their browser TZ. On edit, render stored UTC back through the event's TZ so the round-trip is stable. Reuses TIMEZONE_OPTIONS from ~/config/timezones and the picker pattern from member/profile.vue. Auto-imported helpers from app/utils/timezones do the math via Intl.
This commit is contained in:
parent
e6f05b5471
commit
a76ba2f8c7
1 changed files with 85 additions and 6 deletions
|
|
@ -110,6 +110,23 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label> Event Timezone <span class="required">*</span> </label>
|
||||||
|
<USelectMenu
|
||||||
|
v-model="eventForm.displayTimezone"
|
||||||
|
:items="timezoneItems"
|
||||||
|
value-key="value"
|
||||||
|
searchable
|
||||||
|
searchable-placeholder="Search timezones..."
|
||||||
|
placeholder="Select a timezone"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<p class="help-text">
|
||||||
|
Dates below are interpreted in this timezone. Attendees see the
|
||||||
|
event time in this zone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label> Location <span class="required">*</span> </label>
|
<label> Location <span class="required">*</span> </label>
|
||||||
<UInput
|
<UInput
|
||||||
|
|
@ -601,6 +618,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { TIMEZONE_OPTIONS } from "~/config/timezones";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
layout: "admin",
|
||||||
middleware: "admin",
|
middleware: "admin",
|
||||||
|
|
@ -630,6 +649,7 @@ const eventForm = reactive({
|
||||||
startDate: "",
|
startDate: "",
|
||||||
endDate: "",
|
endDate: "",
|
||||||
eventType: "community",
|
eventType: "community",
|
||||||
|
displayTimezone: "America/Toronto",
|
||||||
location: "",
|
location: "",
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
|
|
@ -672,6 +692,38 @@ const formatForDatetimeLocal = (value) => {
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Render the form's datetime fields in the event's display timezone.
|
||||||
|
const formatForEventTZ = (value) => {
|
||||||
|
if (!value) return "";
|
||||||
|
return utcToZonedLocal(value, eventForm.displayTimezone) || formatForDatetimeLocal(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const utcOffsetLabel = (tz) => {
|
||||||
|
try {
|
||||||
|
const parts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: tz,
|
||||||
|
timeZoneName: "longOffset",
|
||||||
|
}).formatToParts(new Date());
|
||||||
|
const name = parts.find((p) => p.type === "timeZoneName")?.value || "";
|
||||||
|
if (name === "GMT") return "UTC+00:00";
|
||||||
|
return name.replace("GMT", "UTC");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timezoneItems = computed(() => {
|
||||||
|
const list = TIMEZONE_OPTIONS.map((t) => {
|
||||||
|
const off = utcOffsetLabel(t.value);
|
||||||
|
return { ...t, label: off ? `${t.label} (${off})` : t.label };
|
||||||
|
});
|
||||||
|
const saved = eventForm.displayTimezone;
|
||||||
|
if (saved && !TIMEZONE_OPTIONS.some((t) => t.value === saved)) {
|
||||||
|
list.unshift({ label: saved, value: saved });
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
// Agenda management functions
|
// Agenda management functions
|
||||||
const addAgendaItem = () => {
|
const addAgendaItem = () => {
|
||||||
eventForm.agenda.push("");
|
eventForm.agenda.push("");
|
||||||
|
|
@ -725,14 +777,17 @@ function populateEditForm(payload) {
|
||||||
const event = payload?.data;
|
const event = payload?.data;
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
editingEvent.value = event;
|
editingEvent.value = event;
|
||||||
|
// Pin the form's timezone first so subsequent date conversions use it.
|
||||||
|
eventForm.displayTimezone = event.displayTimezone || "America/Toronto";
|
||||||
Object.assign(eventForm, {
|
Object.assign(eventForm, {
|
||||||
title: event.title,
|
title: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
content: event.content || "",
|
content: event.content || "",
|
||||||
featureImage: event.featureImage || null,
|
featureImage: event.featureImage || null,
|
||||||
startDate: formatForDatetimeLocal(event.startDate),
|
startDate: utcToZonedLocal(event.startDate, eventForm.displayTimezone),
|
||||||
endDate: formatForDatetimeLocal(event.endDate),
|
endDate: utcToZonedLocal(event.endDate, eventForm.displayTimezone),
|
||||||
eventType: event.eventType,
|
eventType: event.eventType,
|
||||||
|
displayTimezone: eventForm.displayTimezone,
|
||||||
location: event.location || "",
|
location: event.location || "",
|
||||||
isOnline: event.isOnline,
|
isOnline: event.isOnline,
|
||||||
isVisible: event.isVisible !== undefined ? event.isVisible : true,
|
isVisible: event.isVisible !== undefined ? event.isVisible : true,
|
||||||
|
|
@ -743,7 +798,7 @@ function populateEditForm(payload) {
|
||||||
tags: event.tags || [],
|
tags: event.tags || [],
|
||||||
maxAttendees: event.maxAttendees || "",
|
maxAttendees: event.maxAttendees || "",
|
||||||
registrationRequired: event.registrationRequired,
|
registrationRequired: event.registrationRequired,
|
||||||
registrationDeadline: formatForDatetimeLocal(event.registrationDeadline),
|
registrationDeadline: utcToZonedLocal(event.registrationDeadline, eventForm.displayTimezone),
|
||||||
agenda: event.agenda || [],
|
agenda: event.agenda || [],
|
||||||
tickets: event.tickets || {
|
tickets: event.tickets || {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
@ -765,8 +820,9 @@ function populateEditForm(payload) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (event.tickets?.public?.earlyBirdDeadline) {
|
if (event.tickets?.public?.earlyBirdDeadline) {
|
||||||
eventForm.tickets.public.earlyBirdDeadline = formatForDatetimeLocal(
|
eventForm.tickets.public.earlyBirdDeadline = utcToZonedLocal(
|
||||||
event.tickets.public.earlyBirdDeadline,
|
event.tickets.public.earlyBirdDeadline,
|
||||||
|
eventForm.displayTimezone,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -911,15 +967,37 @@ const saveEvent = async (redirect = true) => {
|
||||||
// Individual series creation is handled through the series management page
|
// Individual series creation is handled through the series management page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tz = eventForm.displayTimezone || "America/Toronto";
|
||||||
|
const toUTC = (v) => {
|
||||||
|
const d = zonedLocalToUTC(v, tz);
|
||||||
|
return d ? d.toISOString() : v;
|
||||||
|
};
|
||||||
|
const payload = {
|
||||||
|
...eventForm,
|
||||||
|
startDate: toUTC(eventForm.startDate),
|
||||||
|
endDate: toUTC(eventForm.endDate),
|
||||||
|
registrationDeadline: eventForm.registrationDeadline
|
||||||
|
? toUTC(eventForm.registrationDeadline)
|
||||||
|
: eventForm.registrationDeadline,
|
||||||
|
tickets: {
|
||||||
|
...eventForm.tickets,
|
||||||
|
public: {
|
||||||
|
...eventForm.tickets.public,
|
||||||
|
earlyBirdDeadline: eventForm.tickets.public.earlyBirdDeadline
|
||||||
|
? toUTC(eventForm.tickets.public.earlyBirdDeadline)
|
||||||
|
: eventForm.tickets.public.earlyBirdDeadline,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
if (editingEvent.value) {
|
if (editingEvent.value) {
|
||||||
await $fetch(`/api/admin/events/${editingEvent.value._id}`, {
|
await $fetch(`/api/admin/events/${editingEvent.value._id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: eventForm,
|
body: payload,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await $fetch("/api/admin/events", {
|
await $fetch("/api/admin/events", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: eventForm,
|
body: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -959,6 +1037,7 @@ const saveAndCreateAnother = async () => {
|
||||||
startDate: "",
|
startDate: "",
|
||||||
endDate: "",
|
endDate: "",
|
||||||
eventType: "community",
|
eventType: "community",
|
||||||
|
displayTimezone: "America/Toronto",
|
||||||
location: "",
|
location: "",
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue