This commit is contained in:
chark1es 2025-04-10 19:31:47 -07:00
commit effb8e22d7
7 changed files with 1358 additions and 534 deletions

528
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ import type { Event, EventAttendee, LimitedUser } from "../../../schemas/pocketb
// Extended Event interface with additional properties needed for this component
interface ExtendedEvent extends Event {
description?: string; // This component uses 'description' but schema has 'event_description'
event_type: string; // Add event_type field from schema
}
// Note: Date conversion is now handled automatically by the Get and Update classes.
@ -156,7 +157,12 @@ const EventCheckIn = () => {
);
// Store event code in local storage for offline check-in
try {
await dataSync.storeEventCode(eventCode);
} catch (syncError) {
// Log the error but don't show a toast to the user
console.error("Error storing event code locally:", syncError);
}
// Show event details toast only for non-food events
// For food events, we'll show the toast after food selection
@ -301,14 +307,24 @@ const EventCheckIn = () => {
// Ensure local data is in sync with backend
// First sync the new attendance record
try {
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
// Then sync the updated user and LimitedUser data
await dataSync.syncCollection(Collections.USERS);
await dataSync.syncCollection(Collections.LIMITED_USERS);
} catch (syncError) {
// Log the error but don't show a toast to the user
console.error('Local sync failed:', syncError);
}
// Clear event code from local storage
try {
await dataSync.clearEventCode();
} catch (clearError) {
// Log the error but don't show a toast to the user
console.error("Error clearing event code from local storage:", clearError);
}
// Log successful check-in
await logger.send(
@ -461,7 +477,7 @@ const EventCheckIn = () => {
<div className="form-control">
<input
type="text"
placeholder="Enter your ference"
placeholder="Enter the food you will or are eating"
className="input input-bordered w-full"
value={foodInput}
onChange={(e) => setFoodInput(e.target.value)}

View file

@ -10,6 +10,7 @@ import type { Event, AttendeeEntry, EventAttendee } from "../../../schemas/pocke
// Extended Event interface with additional properties needed for this component
interface ExtendedEvent extends Event {
description?: string; // This component uses 'description' but schema has 'event_description'
event_type: string; // Add event_type field from schema
}
declare global {
@ -62,6 +63,19 @@ const EventLoad = () => {
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(new Set());
const toggleDescription = (eventId: string) => {
setExpandedDescriptions(prev => {
const newSet = new Set(prev);
if (newSet.has(eventId)) {
newSet.delete(eventId);
} else {
newSet.add(eventId);
}
return newSet;
});
};
// Function to clear the events cache and force a fresh sync
const refreshEvents = async () => {
@ -202,37 +216,76 @@ const EventLoad = () => {
const endDate = new Date(event.end_date);
const now = new Date();
const isPastEvent = endDate < now;
const isExpanded = expandedDescriptions.has(event.id);
const description = event.event_description || "No description available";
return (
<div id={`event-card-${event.id}`} key={event.id} className="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden">
<div className="card-body p-3 sm:p-4">
<div className="flex flex-col h-full">
<div className="flex flex-col gap-2">
<div className="flex-1">
<h3 className="card-title text-base sm:text-lg font-semibold mb-1 line-clamp-2">{event.event_name}</h3>
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-base-content/70">
<div className="badge badge-primary badge-sm">{event.points_to_reward} pts</div>
<div className="text-xs sm:text-sm opacity-75">
<div className="card-body p-4">
{/* Event Header */}
<div className="flex justify-between items-start mb-2">
<h3 className="card-title text-base sm:text-lg font-semibold line-clamp-2">{event.event_name}</h3>
<div className="badge badge-primary badge-sm flex-shrink-0">{event.points_to_reward} pts</div>
</div>
{/* Event Description */}
<div className="mb-3">
<p className={`text-xs sm:text-sm text-base-content/70 ${isExpanded ? '' : 'line-clamp-2'}`}>
{description}
</p>
{description.length > 80 && (
<button
onClick={() => toggleDescription(event.id)}
className="text-xs text-primary hover:text-primary-focus mt-1 flex items-center"
>
{isExpanded ? (
<>
<Icon icon="heroicons:chevron-up" className="h-3 w-3 mr-1" />
Show less
</>
) : (
<>
<Icon icon="heroicons:chevron-down" className="h-3 w-3 mr-1" />
Show more
</>
)}
</button>
)}
</div>
{/* Event Details */}
<div className="grid grid-cols-1 gap-1 mb-3 text-xs sm:text-sm">
<div className="flex items-center gap-2">
<Icon icon="heroicons:calendar" className="h-3.5 w-3.5 text-primary" />
<span>
{startDate.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
})}
{" • "}
</span>
</div>
<div className="flex items-center gap-2">
<Icon icon="heroicons:clock" className="h-3.5 w-3.5 text-primary" />
<span>
{startDate.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
})}
</span>
</div>
<div className="flex items-center gap-2">
<Icon icon="heroicons:map-pin" className="h-3.5 w-3.5 text-primary" />
<span className="line-clamp-1">{event.location || "No location specified"}</span>
</div>
<div className="flex items-center gap-2">
<Icon icon="heroicons:tag" className="h-3.5 w-3.5 text-primary" />
<span className="line-clamp-1 capitalize">{event.event_type || "Other"}</span>
</div>
</div>
<div className="text-xs sm:text-sm text-base-content/70 my-2 line-clamp-2">
{event.event_description || "No description available"}
</div>
<div className="flex flex-wrap items-center gap-2 mt-auto pt-2">
{/* Action Buttons */}
<div className="flex flex-wrap items-center gap-2 mt-auto">
{event.files && event.files.length > 0 && (
<button
onClick={() => window.openDetailsModal(event as ExtendedEvent)}
@ -243,7 +296,7 @@ const EventLoad = () => {
</button>
)}
{isPastEvent && (
<div id={`attendance-badge-${event.id}`} className={`badge ${hasAttended ? 'badge-success' : 'badge-ghost'} text-xs gap-1`}>
<div id={`attendance-badge-${event.id}`} className={`badge ${hasAttended ? 'badge-success' : 'badge-ghost'} text-xs gap-1 ml-auto`}>
<Icon
icon={hasAttended ? "heroicons:check-circle" : "heroicons:x-circle"}
className="h-3 w-3"
@ -251,10 +304,6 @@ const EventLoad = () => {
{hasAttended ? 'Attended' : 'Not Attended'}
</div>
)}
<div className="text-xs sm:text-sm opacity-75 ml-auto">
{event.location}
</div>
</div>
</div>
</div>
</div>

View file

@ -133,6 +133,28 @@ const EventForm = memo(({
/>
</div>
{/* Event Type */}
<div className="form-control">
<label className="label">
<span className="label-text">Event Type</span>
<span className="label-text-alt text-error">*</span>
</label>
<select
name="editEventType"
className="select select-bordered"
value={event?.event_type || "other"}
onChange={(e) => handleChange('event_type', e.target.value)}
required
>
<option value="social">Social</option>
<option value="technical">Technical</option>
<option value="outreach">Outreach</option>
<option value="professional">Professional</option>
<option value="workshop">Projects</option>
<option value="other">Other</option>
</select>
</div>
{/* Points to Reward */}
<div className="form-control">
<label className="label">
@ -435,6 +457,7 @@ interface EventChanges {
end_date?: string;
published?: boolean;
has_food?: boolean;
event_type?: string;
}
interface FileChanges {
@ -521,7 +544,8 @@ class ChangeTracker {
'start_date',
'end_date',
'published',
'has_food'
'has_food',
'event_type'
];
for (const field of fields) {
@ -588,7 +612,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
start_date: "",
end_date: "",
published: false,
has_food: false
has_food: false,
event_type: "other"
});
const [previewUrl, setPreviewUrl] = useState("");
@ -681,7 +706,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
start_date: eventData.start_date || '',
end_date: eventData.end_date || '',
published: eventData.published || false,
has_food: eventData.has_food || false
has_food: eventData.has_food || false,
event_type: eventData.event_type || 'other'
});
// Set up realtime subscription for this event
@ -727,7 +753,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
start_date: Get.formatLocalDate(now, false),
end_date: Get.formatLocalDate(oneHourLater, false),
published: false,
has_food: false
has_food: false,
event_type: "other"
});
}
setSelectedFiles(new Map());
@ -800,7 +827,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
start_date: "",
end_date: "",
published: false,
has_food: false
has_food: false,
event_type: "other"
});
setSelectedFiles(new Map());
setFilesToDelete(new Set());
@ -839,7 +867,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
start_date: "",
end_date: "",
published: false,
has_food: false
has_food: false,
event_type: "other"
});
setSelectedFiles(new Map());
setFilesToDelete(new Set());
@ -889,7 +918,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
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"
has_food: formData.get("editEventHasFood") === "on",
event_type: formData.get("editEventType") as string || "other"
};
// Log the update attempt

View file

@ -2,7 +2,7 @@
{
"title": "Quarterly Project",
"text": "QP is a quarter-long project competition. Students collaborate in teams of up to 6 to build a product aligned with a quarterly theme.",
"link": "/quarterly",
"link": "/projects/quarterly",
"number": "01",
"delay": "100"
},

View file

@ -68,6 +68,7 @@ export interface Event extends BaseRecord {
start_date: string;
end_date: string;
published: boolean;
event_type: string; // social, technical, outreach, professional, projects, other
has_food: boolean;
}