Merge branch 'feature/event-timezone' into main
This commit is contained in:
commit
877ef1a220
13 changed files with 318 additions and 159 deletions
|
|
@ -110,6 +110,23 @@
|
|||
</p>
|
||||
</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">
|
||||
<label> Location <span class="required">*</span> </label>
|
||||
<UInput
|
||||
|
|
@ -601,6 +618,8 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TIMEZONE_OPTIONS } from "~/config/timezones";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
middleware: "admin",
|
||||
|
|
@ -630,6 +649,7 @@ const eventForm = reactive({
|
|||
startDate: "",
|
||||
endDate: "",
|
||||
eventType: "community",
|
||||
displayTimezone: "America/Toronto",
|
||||
location: "",
|
||||
isOnline: 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())}`;
|
||||
};
|
||||
|
||||
// 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
|
||||
const addAgendaItem = () => {
|
||||
eventForm.agenda.push("");
|
||||
|
|
@ -725,14 +777,17 @@ function populateEditForm(payload) {
|
|||
const event = payload?.data;
|
||||
if (!event) return;
|
||||
editingEvent.value = event;
|
||||
// Pin the form's timezone first so subsequent date conversions use it.
|
||||
eventForm.displayTimezone = event.displayTimezone || "America/Toronto";
|
||||
Object.assign(eventForm, {
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
content: event.content || "",
|
||||
featureImage: event.featureImage || null,
|
||||
startDate: formatForDatetimeLocal(event.startDate),
|
||||
endDate: formatForDatetimeLocal(event.endDate),
|
||||
startDate: utcToZonedLocal(event.startDate, eventForm.displayTimezone),
|
||||
endDate: utcToZonedLocal(event.endDate, eventForm.displayTimezone),
|
||||
eventType: event.eventType,
|
||||
displayTimezone: eventForm.displayTimezone,
|
||||
location: event.location || "",
|
||||
isOnline: event.isOnline,
|
||||
isVisible: event.isVisible !== undefined ? event.isVisible : true,
|
||||
|
|
@ -743,7 +798,7 @@ function populateEditForm(payload) {
|
|||
tags: event.tags || [],
|
||||
maxAttendees: event.maxAttendees || "",
|
||||
registrationRequired: event.registrationRequired,
|
||||
registrationDeadline: formatForDatetimeLocal(event.registrationDeadline),
|
||||
registrationDeadline: utcToZonedLocal(event.registrationDeadline, eventForm.displayTimezone),
|
||||
agenda: event.agenda || [],
|
||||
tickets: event.tickets || {
|
||||
enabled: false,
|
||||
|
|
@ -765,8 +820,9 @@ function populateEditForm(payload) {
|
|||
},
|
||||
});
|
||||
if (event.tickets?.public?.earlyBirdDeadline) {
|
||||
eventForm.tickets.public.earlyBirdDeadline = formatForDatetimeLocal(
|
||||
eventForm.tickets.public.earlyBirdDeadline = utcToZonedLocal(
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
await $fetch(`/api/admin/events/${editingEvent.value._id}`, {
|
||||
method: "PUT",
|
||||
body: eventForm,
|
||||
body: payload,
|
||||
});
|
||||
} else {
|
||||
await $fetch("/api/admin/events", {
|
||||
method: "POST",
|
||||
body: eventForm,
|
||||
body: payload,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -959,6 +1037,7 @@ const saveAndCreateAnother = async () => {
|
|||
startDate: "",
|
||||
endDate: "",
|
||||
eventType: "community",
|
||||
displayTimezone: "America/Toronto",
|
||||
location: "",
|
||||
isOnline: true,
|
||||
isVisible: true,
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@
|
|||
<span class="badge" :class="event.eventType">{{ event.eventType }}</span>
|
||||
</td>
|
||||
<td class="col-date">
|
||||
<span class="date-main">{{ formatDate(event.startDate) }}</span>
|
||||
<span class="date-time">{{ formatTime(event.startDate) }}</span>
|
||||
<span class="date-main">{{ formatDate(event) }}</span>
|
||||
<span class="date-time">{{ formatTime(event) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['status-pill', `status-${getEventStatus(event).toLowerCase()}`]">
|
||||
|
|
@ -190,8 +190,8 @@
|
|||
<span class="badge" :class="event.eventType">{{ event.eventType }}</span>
|
||||
</td>
|
||||
<td class="col-date">
|
||||
<span class="date-main">{{ formatDate(event.startDate) }}</span>
|
||||
<span class="date-time">{{ formatTime(event.startDate) }}</span>
|
||||
<span class="date-main">{{ formatDate(event) }}</span>
|
||||
<span class="date-time">{{ formatTime(event) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['status-pill', `status-${getEventStatus(event).toLowerCase()}`]">
|
||||
|
|
@ -349,19 +349,23 @@ watch([searchQuery, typeFilter, seriesFilter], () => {
|
|||
pastPage.value = 1
|
||||
})
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
const formatDate = (event) => {
|
||||
if (!event?.startDate) return ''
|
||||
return new Date(event.startDate).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone: event.displayTimezone || 'America/Toronto',
|
||||
})
|
||||
}
|
||||
|
||||
const formatTime = (dateString) => {
|
||||
return new Date(dateString).toLocaleTimeString('en-US', {
|
||||
const formatTime = (event) => {
|
||||
if (!event?.startDate) return ''
|
||||
return new Date(event.startDate).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
timeZone: event.displayTimezone || 'America/Toronto',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@
|
|||
:event-id="event._id || event.id"
|
||||
:event-start-date="event.startDate"
|
||||
:event-title="event.title"
|
||||
:event-timezone="eventTimeZone"
|
||||
:user-email="memberData?.email"
|
||||
:user-name="memberData?.name"
|
||||
@success="handleTicketSuccess"
|
||||
|
|
@ -197,21 +198,29 @@ onMounted(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
const eventTimeZone = computed(
|
||||
() => event.value?.displayTimezone || "America/Toronto",
|
||||
);
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const d = new Date(dateStr);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: eventTimeZone.value,
|
||||
}).format(d);
|
||||
};
|
||||
|
||||
const formatTime = (start, end) => {
|
||||
if (!start || !end) return "";
|
||||
const fmt = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
timeZone: eventTimeZone.value,
|
||||
});
|
||||
return `${fmt.format(new Date(start))} – ${fmt.format(new Date(end))}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@
|
|||
:class="{ 'is-cancelled': event.isCancelled }"
|
||||
>
|
||||
<div class="event-date-col">
|
||||
<span class="event-date">{{ formatDate(event.startDate) }}</span>
|
||||
<span class="event-time">{{ formatTime(event.startDate) }}</span>
|
||||
<span class="event-date">{{ formatDate(event) }}</span>
|
||||
<span class="event-time">{{ formatTime(event) }}</span>
|
||||
</div>
|
||||
<div class="event-info">
|
||||
<div class="event-title">
|
||||
|
|
@ -152,18 +152,24 @@ const activeSeries = computed(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const d = new Date(dateStr);
|
||||
const opts = { month: "short", day: "numeric" };
|
||||
if (d.getFullYear() !== new Date().getFullYear()) opts.year = "numeric";
|
||||
const formatDate = (event) => {
|
||||
if (!event?.startDate) return "";
|
||||
const tz = event.displayTimezone || "America/Toronto";
|
||||
const d = new Date(event.startDate);
|
||||
const opts = { month: "short", day: "numeric", timeZone: tz };
|
||||
const dYear = d.toLocaleDateString("en-US", { year: "numeric", timeZone: tz });
|
||||
const nowYear = new Date().toLocaleDateString("en-US", { year: "numeric", timeZone: tz });
|
||||
if (dYear !== nowYear) opts.year = "numeric";
|
||||
return d.toLocaleDateString("en-US", opts);
|
||||
};
|
||||
|
||||
const formatTime = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
|
||||
const formatTime = (event) => {
|
||||
if (!event?.startDate) return "";
|
||||
return new Date(event.startDate).toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: event.displayTimezone || "America/Toronto",
|
||||
});
|
||||
};
|
||||
|
||||
const formatLocation = (event) => {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
<div v-if="events?.length" class="event-list">
|
||||
<div v-for="event in events" :key="event._id" class="event-item">
|
||||
<div class="block-inset event-item-inner">
|
||||
<span class="event-date">{{ formatDate(event.startDate) }}</span>
|
||||
<span class="event-date">{{ formatDate(event) }}</span>
|
||||
<span class="event-title">
|
||||
<NuxtLink :to="`/events/${event.slug || event._id}`">{{
|
||||
event.title
|
||||
|
|
@ -168,10 +168,13 @@ const circleData = [
|
|||
},
|
||||
];
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
const formatDate = (event) => {
|
||||
if (!event?.startDate) return "";
|
||||
return new Date(event.startDate).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: event.displayTimezone || "America/Toronto",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -60,9 +60,7 @@
|
|||
:to="`/events/${evt.slug || evt._id}`"
|
||||
class="event-item"
|
||||
>
|
||||
<span class="event-date">{{
|
||||
formatEventDate(evt.startDate)
|
||||
}}</span>
|
||||
<span class="event-date">{{ formatEventDate(evt) }}</span>
|
||||
<span class="event-title">{{ evt.title }}</span>
|
||||
<CircleBadge v-if="evt.circle" :circle="evt.circle" />
|
||||
</NuxtLink>
|
||||
|
|
@ -365,20 +363,22 @@ const getEventImageUrl = (featureImage) => {
|
|||
return "";
|
||||
};
|
||||
|
||||
const formatEventDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const formatEventDate = (event) => {
|
||||
if (!event?.startDate) return "";
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
timeZone: event.displayTimezone || "America/Toronto",
|
||||
}).format(new Date(event.startDate));
|
||||
};
|
||||
|
||||
const formatEventTime = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const formatEventTime = (event) => {
|
||||
if (!event?.startDate) return "";
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
timeZone: event.displayTimezone || "America/Toronto",
|
||||
}).format(new Date(event.startDate));
|
||||
};
|
||||
|
||||
const formatMemberSince = (dateString) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue