fix datetime issues
This commit is contained in:
parent
72ea5ba8df
commit
c5925bd275
4 changed files with 113 additions and 99 deletions
|
@ -11,6 +11,10 @@ interface ExtendedEvent extends Event {
|
||||||
description?: string; // This component uses 'description' but schema has 'event_description'
|
description?: string; // This component uses 'description' but schema has 'event_description'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Date conversion is now handled automatically by the Get and Update classes.
|
||||||
|
// When fetching events, UTC dates are converted to local time.
|
||||||
|
// When saving events, local dates are converted back to UTC.
|
||||||
|
|
||||||
// Toast management system
|
// Toast management system
|
||||||
const createToast = (
|
const createToast = (
|
||||||
message: string,
|
message: string,
|
||||||
|
@ -115,8 +119,8 @@ const EventCheckIn = () => {
|
||||||
|
|
||||||
// Check if the event is active (has started and hasn't ended yet)
|
// Check if the event is active (has started and hasn't ended yet)
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
const eventStartDate = new Date(event.start_date);
|
const eventStartDate = new Date(event.start_date); // Now properly converted to local time by Get
|
||||||
const eventEndDate = new Date(event.end_date);
|
const eventEndDate = new Date(event.end_date); // Now properly converted to local time by Get
|
||||||
|
|
||||||
if (eventStartDate > currentTime) {
|
if (eventStartDate > currentTime) {
|
||||||
throw new Error("This event has not started yet");
|
throw new Error("This event has not started yet");
|
||||||
|
@ -175,7 +179,7 @@ const EventCheckIn = () => {
|
||||||
// Create attendee entry with check-in details
|
// Create attendee entry with check-in details
|
||||||
const attendeeEntry: AttendeeEntry = {
|
const attendeeEntry: AttendeeEntry = {
|
||||||
user_id: currentUser.id,
|
user_id: currentUser.id,
|
||||||
time_checked_in: new Date().toISOString(),
|
time_checked_in: new Date().toISOString(), // Will be properly converted to UTC by Update
|
||||||
food: foodSelection || "none",
|
food: foodSelection || "none",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -290,7 +294,7 @@ const EventCheckIn = () => {
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={`btn btn-primary h-10 min-h-[2.5rem] text-sm sm:text-base w-full sm:w-auto`}
|
className="btn btn-primary h-10 min-h-[2.5rem] text-sm sm:text-base w-full sm:w-auto"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
@ -308,85 +312,8 @@ const EventCheckIn = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dialog id="foodSelectionModal" className="modal">
|
|
||||||
<div className="modal-box max-w-[90vw] sm:max-w-lg p-4 sm:p-6">
|
|
||||||
<h3 className="font-bold text-base sm:text-lg mb-3 sm:mb-4">Food Selection</h3>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-3 sm:space-y-4">
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text text-sm sm:text-base">What food would you like?</span>
|
|
||||||
<span className="label-text-alt text-error">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={foodInput}
|
|
||||||
onChange={(e) => setFoodInput(e.target.value)}
|
|
||||||
className="input input-bordered text-sm sm:text-base h-10 min-h-[2.5rem] w-full"
|
|
||||||
placeholder="Enter your food choice or 'none'"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-info text-xs sm:text-sm">
|
|
||||||
Enter 'none' if you don't want any food
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="modal-action flex flex-col sm:flex-row gap-2 sm:gap-3">
|
|
||||||
<button type="submit" className="btn btn-primary text-sm sm:text-base h-10 min-h-[2.5rem] w-full sm:w-auto">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn text-sm sm:text-base h-10 min-h-[2.5rem] w-full sm:w-auto"
|
|
||||||
onClick={() => {
|
|
||||||
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
|
|
||||||
modal.close();
|
|
||||||
setCurrentCheckInEvent(null);
|
|
||||||
setFoodInput("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" className="modal-backdrop">
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<style>{`
|
|
||||||
.toast-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.toast-exit {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.toast.translate-x-full {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
.toast-container .toast {
|
|
||||||
transform: translateY(calc((1 - attr(data-index number)) * -0.25rem));
|
|
||||||
}
|
|
||||||
.toast-container .toast[data-index="0"] {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
.toast-container .toast[data-index="1"] {
|
|
||||||
transform: translateY(-0.025rem);
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EventCheckIn;
|
export default EventCheckIn;
|
|
@ -8,6 +8,11 @@ import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
||||||
import FilePreview from "../universal/FilePreview";
|
import FilePreview from "../universal/FilePreview";
|
||||||
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
|
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
|
// Note: Date conversion is now handled automatically by the Get and Update classes.
|
||||||
|
// When fetching events, UTC dates are converted to local time by the Get class.
|
||||||
|
// When saving events, local dates are converted back to UTC by the Update class.
|
||||||
|
// For datetime-local inputs, we format dates without seconds (YYYY-MM-DDThh:mm).
|
||||||
|
|
||||||
// Extended Event interface with optional created and updated fields
|
// Extended Event interface with optional created and updated fields
|
||||||
interface Event extends Omit<SchemaEvent, 'created' | 'updated'> {
|
interface Event extends Omit<SchemaEvent, 'created' | 'updated'> {
|
||||||
created?: string;
|
created?: string;
|
||||||
|
@ -151,7 +156,7 @@ const EventForm = memo(({
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
name="editEventStartDate"
|
name="editEventStartDate"
|
||||||
className="input input-bordered"
|
className="input input-bordered"
|
||||||
value={event?.start_date ? new Date(event.start_date).toISOString().slice(0, 16) : ""}
|
value={event?.start_date ? event.start_date.slice(0, 16) : ""}
|
||||||
onChange={(e) => handleChange('start_date', e.target.value)}
|
onChange={(e) => handleChange('start_date', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
@ -167,7 +172,7 @@ const EventForm = memo(({
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
name="editEventEndDate"
|
name="editEventEndDate"
|
||||||
className="input input-bordered"
|
className="input input-bordered"
|
||||||
value={event?.end_date ? new Date(event.end_date).toISOString().slice(0, 16) : ""}
|
value={event?.end_date ? event.end_date.slice(0, 16) : ""}
|
||||||
onChange={(e) => handleChange('end_date', e.target.value)}
|
onChange={(e) => handleChange('end_date', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
@ -510,8 +515,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
location: '',
|
location: '',
|
||||||
files: [],
|
files: [],
|
||||||
points_to_reward: 0,
|
points_to_reward: 0,
|
||||||
start_date: new Date().toISOString(),
|
start_date: Get.formatLocalDate(new Date(), false),
|
||||||
end_date: new Date().toISOString(),
|
end_date: Get.formatLocalDate(new Date(), false),
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false,
|
has_food: false,
|
||||||
attendees: []
|
attendees: []
|
||||||
|
@ -551,6 +556,20 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
try {
|
try {
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
const eventData = await services.get.getOne<Event>("events", eventId);
|
const eventData = await services.get.getOne<Event>("events", eventId);
|
||||||
|
|
||||||
|
// Ensure dates are properly formatted for datetime-local input
|
||||||
|
if (eventData.start_date) {
|
||||||
|
// Convert to Date object first to ensure proper formatting
|
||||||
|
const startDate = new Date(eventData.start_date);
|
||||||
|
eventData.start_date = Get.formatLocalDate(startDate, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.end_date) {
|
||||||
|
// Convert to Date object first to ensure proper formatting
|
||||||
|
const endDate = new Date(eventData.end_date);
|
||||||
|
eventData.end_date = Get.formatLocalDate(endDate, false);
|
||||||
|
}
|
||||||
|
|
||||||
setEvent(eventData);
|
setEvent(eventData);
|
||||||
} else {
|
} else {
|
||||||
setEvent({
|
setEvent({
|
||||||
|
@ -561,8 +580,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
location: '',
|
location: '',
|
||||||
files: [],
|
files: [],
|
||||||
points_to_reward: 0,
|
points_to_reward: 0,
|
||||||
start_date: new Date().toISOString(),
|
start_date: Get.formatLocalDate(new Date(), false),
|
||||||
end_date: new Date().toISOString(),
|
end_date: Get.formatLocalDate(new Date(), false),
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false,
|
has_food: false,
|
||||||
attendees: []
|
attendees: []
|
||||||
|
@ -623,8 +642,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
location: '',
|
location: '',
|
||||||
files: [],
|
files: [],
|
||||||
points_to_reward: 0,
|
points_to_reward: 0,
|
||||||
start_date: new Date().toISOString(),
|
start_date: Get.formatLocalDate(new Date(), false),
|
||||||
end_date: new Date().toISOString(),
|
end_date: Get.formatLocalDate(new Date(), false),
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false,
|
has_food: false,
|
||||||
attendees: []
|
attendees: []
|
||||||
|
@ -664,8 +683,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
event_description: formData.get("editEventDescription"),
|
event_description: formData.get("editEventDescription"),
|
||||||
location: formData.get("editEventLocation"),
|
location: formData.get("editEventLocation"),
|
||||||
points_to_reward: Number(formData.get("editEventPoints")),
|
points_to_reward: Number(formData.get("editEventPoints")),
|
||||||
start_date: new Date(formData.get("editEventStartDate") as string).toISOString(),
|
start_date: formData.get("editEventStartDate") as string,
|
||||||
end_date: new Date(formData.get("editEventEndDate") as string).toISOString(),
|
end_date: formData.get("editEventEndDate") as string,
|
||||||
published: formData.get("editEventPublished") === "on",
|
published: formData.get("editEventPublished") === "on",
|
||||||
has_food: formData.get("editEventHasFood") === "on",
|
has_food: formData.get("editEventHasFood") === "on",
|
||||||
attendees: event.attendees || []
|
attendees: event.attendees || []
|
||||||
|
@ -758,8 +777,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
location: '',
|
location: '',
|
||||||
files: [],
|
files: [],
|
||||||
points_to_reward: 0,
|
points_to_reward: 0,
|
||||||
start_date: new Date().toISOString(),
|
start_date: Get.formatLocalDate(new Date(), false),
|
||||||
end_date: new Date().toISOString(),
|
end_date: Get.formatLocalDate(new Date(), false),
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false,
|
has_food: false,
|
||||||
attendees: []
|
attendees: []
|
||||||
|
|
|
@ -20,14 +20,45 @@ function isUTCDateString(value: any): boolean {
|
||||||
return isoDateRegex.test(value);
|
return isoDateRegex.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility function to format a date to local ISO-like string
|
||||||
|
function formatLocalDate(date: Date, includeSeconds: boolean = true): string {
|
||||||
|
const pad = (num: number) => num.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = pad(date.getMonth() + 1);
|
||||||
|
const day = pad(date.getDate());
|
||||||
|
const hours = pad(date.getHours());
|
||||||
|
const minutes = pad(date.getMinutes());
|
||||||
|
|
||||||
|
// Format for datetime-local input (YYYY-MM-DDThh:mm)
|
||||||
|
if (!includeSeconds) {
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = pad(date.getSeconds());
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Utility function to convert UTC date strings to local time
|
// Utility function to convert UTC date strings to local time
|
||||||
function convertUTCToLocal<T>(data: T): T {
|
function convertUTCToLocal<T>(data: T): T {
|
||||||
if (!data || typeof data !== "object") return data;
|
if (!data || typeof data !== "object") return data;
|
||||||
|
|
||||||
const converted = { ...data };
|
const converted = { ...data };
|
||||||
for (const [key, value] of Object.entries(converted)) {
|
for (const [key, value] of Object.entries(converted)) {
|
||||||
if (isUTCDateString(value)) {
|
// Special handling for event date fields
|
||||||
(converted as any)[key] = new Date(value).toISOString();
|
if (
|
||||||
|
(key === "start_date" ||
|
||||||
|
key === "end_date" ||
|
||||||
|
key === "time_checked_in") &&
|
||||||
|
isUTCDateString(value)
|
||||||
|
) {
|
||||||
|
// Convert UTC date string to local date string
|
||||||
|
const date = new Date(value);
|
||||||
|
(converted as any)[key] = formatLocalDate(date, false);
|
||||||
|
} else if (isUTCDateString(value)) {
|
||||||
|
// Convert UTC date string to local date string
|
||||||
|
const date = new Date(value);
|
||||||
|
(converted as any)[key] = formatLocalDate(date);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
(converted as any)[key] = value.map((item) => convertUTCToLocal(item));
|
(converted as any)[key] = value.map((item) => convertUTCToLocal(item));
|
||||||
} else if (typeof value === "object" && value !== null) {
|
} else if (typeof value === "object" && value !== null) {
|
||||||
|
@ -73,6 +104,19 @@ export class Get {
|
||||||
return isUTCDateString(value);
|
return isUTCDateString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date to local ISO-like string
|
||||||
|
* @param date The date to format
|
||||||
|
* @param includeSeconds Whether to include seconds in the formatted string
|
||||||
|
* @returns The formatted date string
|
||||||
|
*/
|
||||||
|
public static formatLocalDate(
|
||||||
|
date: Date,
|
||||||
|
includeSeconds: boolean = true,
|
||||||
|
): string {
|
||||||
|
return formatLocalDate(date, includeSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single record by ID
|
* Get a single record by ID
|
||||||
* @param collectionName The name of the collection
|
* @param collectionName The name of the collection
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { Authentication } from "./Authentication";
|
||||||
// Utility function to check if a value is a date string
|
// Utility function to check if a value is a date string
|
||||||
function isLocalDateString(value: any): boolean {
|
function isLocalDateString(value: any): boolean {
|
||||||
if (typeof value !== "string") return false;
|
if (typeof value !== "string") return false;
|
||||||
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
|
// Match ISO format without the Z suffix (local time)
|
||||||
|
const isoDateRegex =
|
||||||
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:(?:-|\+)\d{2}:\d{2})?$/;
|
||||||
return isoDateRegex.test(value);
|
return isoDateRegex.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +15,19 @@ function convertLocalToUTC<T>(data: T): T {
|
||||||
|
|
||||||
const converted = { ...data };
|
const converted = { ...data };
|
||||||
for (const [key, value] of Object.entries(converted)) {
|
for (const [key, value] of Object.entries(converted)) {
|
||||||
|
// Special handling for event date fields to ensure proper UTC conversion
|
||||||
|
if (
|
||||||
|
(key === "start_date" ||
|
||||||
|
key === "end_date" ||
|
||||||
|
key === "time_checked_in") &&
|
||||||
|
typeof value === "string"
|
||||||
|
) {
|
||||||
|
// Ensure we're converting to UTC
|
||||||
|
const date = new Date(value);
|
||||||
|
(converted as any)[key] = date.toISOString();
|
||||||
|
}
|
||||||
// Special handling for invoice_data to ensure it's a proper JSON object
|
// Special handling for invoice_data to ensure it's a proper JSON object
|
||||||
if (key === "invoice_data") {
|
else if (key === "invoice_data") {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
try {
|
try {
|
||||||
// If it's a string representation of JSON, parse it
|
// If it's a string representation of JSON, parse it
|
||||||
|
@ -29,7 +42,9 @@ function convertLocalToUTC<T>(data: T): T {
|
||||||
(converted as any)[key] = value;
|
(converted as any)[key] = value;
|
||||||
}
|
}
|
||||||
} else if (isLocalDateString(value)) {
|
} else if (isLocalDateString(value)) {
|
||||||
(converted as any)[key] = new Date(value).toISOString();
|
// Convert local date string to UTC
|
||||||
|
const date = new Date(value);
|
||||||
|
(converted as any)[key] = date.toISOString();
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
(converted as any)[key] = value.map((item) => convertLocalToUTC(item));
|
(converted as any)[key] = value.map((item) => convertLocalToUTC(item));
|
||||||
} else if (typeof value === "object" && value !== null) {
|
} else if (typeof value === "object" && value !== null) {
|
||||||
|
@ -57,6 +72,15 @@ export class Update {
|
||||||
return Update.instance;
|
return Update.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert local time to UTC
|
||||||
|
* @param data The data to convert
|
||||||
|
* @returns The converted data
|
||||||
|
*/
|
||||||
|
public static convertLocalToUTC<T>(data: T): T {
|
||||||
|
return convertLocalToUTC(data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new record
|
* Create a new record
|
||||||
* @param collectionName The name of the collection
|
* @param collectionName The name of the collection
|
||||||
|
|
Loading…
Reference in a new issue