fix datetime issues

This commit is contained in:
chark1es 2025-03-01 02:03:47 -08:00
parent 72ea5ba8df
commit c5925bd275
4 changed files with 113 additions and 99 deletions

View file

@ -11,6 +11,10 @@ interface ExtendedEvent extends Event {
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
const createToast = (
message: string,
@ -115,8 +119,8 @@ const EventCheckIn = () => {
// Check if the event is active (has started and hasn't ended yet)
const currentTime = new Date();
const eventStartDate = new Date(event.start_date);
const eventEndDate = new Date(event.end_date);
const eventStartDate = new Date(event.start_date); // Now properly converted to local time by Get
const eventEndDate = new Date(event.end_date); // Now properly converted to local time by Get
if (eventStartDate > currentTime) {
throw new Error("This event has not started yet");
@ -175,7 +179,7 @@ const EventCheckIn = () => {
// Create attendee entry with check-in details
const attendeeEntry: AttendeeEntry = {
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",
};
@ -290,7 +294,7 @@ const EventCheckIn = () => {
/>
<button
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}
>
{isLoading ? (
@ -308,85 +312,8 @@ const EventCheckIn = () => {
</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;

View file

@ -8,6 +8,11 @@ import { SendLog } from "../../../scripts/pocketbase/SendLog";
import FilePreview from "../universal/FilePreview";
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
interface Event extends Omit<SchemaEvent, 'created' | 'updated'> {
created?: string;
@ -151,7 +156,7 @@ const EventForm = memo(({
type="datetime-local"
name="editEventStartDate"
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)}
required
/>
@ -167,7 +172,7 @@ const EventForm = memo(({
type="datetime-local"
name="editEventEndDate"
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)}
required
/>
@ -510,8 +515,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
location: '',
files: [],
points_to_reward: 0,
start_date: new Date().toISOString(),
end_date: new Date().toISOString(),
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
published: false,
has_food: false,
attendees: []
@ -551,6 +556,20 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
try {
if (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);
} else {
setEvent({
@ -561,8 +580,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
location: '',
files: [],
points_to_reward: 0,
start_date: new Date().toISOString(),
end_date: new Date().toISOString(),
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
published: false,
has_food: false,
attendees: []
@ -623,8 +642,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
location: '',
files: [],
points_to_reward: 0,
start_date: new Date().toISOString(),
end_date: new Date().toISOString(),
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
published: false,
has_food: false,
attendees: []
@ -664,8 +683,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
event_description: formData.get("editEventDescription"),
location: formData.get("editEventLocation"),
points_to_reward: Number(formData.get("editEventPoints")),
start_date: new Date(formData.get("editEventStartDate") as string).toISOString(),
end_date: new Date(formData.get("editEventEndDate") as string).toISOString(),
start_date: formData.get("editEventStartDate") as string,
end_date: formData.get("editEventEndDate") as string,
published: formData.get("editEventPublished") === "on",
has_food: formData.get("editEventHasFood") === "on",
attendees: event.attendees || []
@ -758,8 +777,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
location: '',
files: [],
points_to_reward: 0,
start_date: new Date().toISOString(),
end_date: new Date().toISOString(),
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
published: false,
has_food: false,
attendees: []

View file

@ -20,14 +20,45 @@ function isUTCDateString(value: any): boolean {
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
function convertUTCToLocal<T>(data: T): T {
if (!data || typeof data !== "object") return data;
const converted = { ...data };
for (const [key, value] of Object.entries(converted)) {
if (isUTCDateString(value)) {
(converted as any)[key] = new Date(value).toISOString();
// Special handling for event date fields
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)) {
(converted as any)[key] = value.map((item) => convertUTCToLocal(item));
} else if (typeof value === "object" && value !== null) {
@ -73,6 +104,19 @@ export class Get {
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
* @param collectionName The name of the collection

View file

@ -3,7 +3,9 @@ import { Authentication } from "./Authentication";
// Utility function to check if a value is a date string
function isLocalDateString(value: any): boolean {
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);
}
@ -13,8 +15,19 @@ function convertLocalToUTC<T>(data: T): T {
const converted = { ...data };
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
if (key === "invoice_data") {
else if (key === "invoice_data") {
if (typeof value === "string") {
try {
// 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;
}
} 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)) {
(converted as any)[key] = value.map((item) => convertLocalToUTC(item));
} else if (typeof value === "object" && value !== null) {
@ -57,6 +72,15 @@ export class Update {
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
* @param collectionName The name of the collection