Merge branch 'main' of https://git.ieeeucsd.org/Webmaster/dev-ieeeucsd-org
This commit is contained in:
commit
effb8e22d7
7 changed files with 1358 additions and 534 deletions
|
@ -12,6 +12,7 @@ import type { Event, EventAttendee, LimitedUser } from "../../../schemas/pocketb
|
||||||
// Extended Event interface with additional properties needed for this component
|
// Extended Event interface with additional properties needed for this component
|
||||||
interface ExtendedEvent extends Event {
|
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'
|
||||||
|
event_type: string; // Add event_type field from schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Date conversion is now handled automatically by the Get and Update classes.
|
// 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
|
// Store event code in local storage for offline check-in
|
||||||
await dataSync.storeEventCode(eventCode);
|
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
|
// Show event details toast only for non-food events
|
||||||
// For food events, we'll show the toast after food selection
|
// 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
|
// Ensure local data is in sync with backend
|
||||||
// First sync the new attendance record
|
// First sync the new attendance record
|
||||||
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
|
try {
|
||||||
|
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
|
||||||
|
|
||||||
// Then sync the updated user and LimitedUser data
|
// Then sync the updated user and LimitedUser data
|
||||||
await dataSync.syncCollection(Collections.USERS);
|
await dataSync.syncCollection(Collections.USERS);
|
||||||
await dataSync.syncCollection(Collections.LIMITED_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
|
// Clear event code from local storage
|
||||||
await dataSync.clearEventCode();
|
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
|
// Log successful check-in
|
||||||
await logger.send(
|
await logger.send(
|
||||||
|
@ -461,7 +477,7 @@ const EventCheckIn = () => {
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter your ference"
|
placeholder="Enter the food you will or are eating"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
value={foodInput}
|
value={foodInput}
|
||||||
onChange={(e) => setFoodInput(e.target.value)}
|
onChange={(e) => setFoodInput(e.target.value)}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { Event, AttendeeEntry, EventAttendee } from "../../../schemas/pocke
|
||||||
// Extended Event interface with additional properties needed for this component
|
// Extended Event interface with additional properties needed for this component
|
||||||
interface ExtendedEvent extends Event {
|
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'
|
||||||
|
event_type: string; // Add event_type field from schema
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -62,6 +63,19 @@ const EventLoad = () => {
|
||||||
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle');
|
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle');
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
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
|
// Function to clear the events cache and force a fresh sync
|
||||||
const refreshEvents = async () => {
|
const refreshEvents = async () => {
|
||||||
|
@ -202,59 +216,94 @@ const EventLoad = () => {
|
||||||
const endDate = new Date(event.end_date);
|
const endDate = new Date(event.end_date);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isPastEvent = endDate < now;
|
const isPastEvent = endDate < now;
|
||||||
|
const isExpanded = expandedDescriptions.has(event.id);
|
||||||
|
const description = event.event_description || "No description available";
|
||||||
|
|
||||||
return (
|
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 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="card-body p-4">
|
||||||
<div className="flex flex-col h-full">
|
{/* Event Header */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<div className="flex-1">
|
<h3 className="card-title text-base sm:text-lg font-semibold line-clamp-2">{event.event_name}</h3>
|
||||||
<h3 className="card-title text-base sm:text-lg font-semibold mb-1 line-clamp-2">{event.event_name}</h3>
|
<div className="badge badge-primary badge-sm flex-shrink-0">{event.points_to_reward} pts</div>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-base-content/70">
|
</div>
|
||||||
<div className="badge badge-primary badge-sm">{event.points_to_reward} pts</div>
|
|
||||||
<div className="text-xs sm:text-sm opacity-75">
|
|
||||||
{startDate.toLocaleDateString("en-US", {
|
|
||||||
weekday: "short",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
{" • "}
|
|
||||||
{startDate.toLocaleTimeString("en-US", {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs sm:text-sm text-base-content/70 my-2 line-clamp-2">
|
{/* Event Description */}
|
||||||
{event.event_description || "No description available"}
|
<div className="mb-3">
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 mt-auto pt-2">
|
{/* Event Details */}
|
||||||
{event.files && event.files.length > 0 && (
|
<div className="grid grid-cols-1 gap-1 mb-3 text-xs sm:text-sm">
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => window.openDetailsModal(event as ExtendedEvent)}
|
<Icon icon="heroicons:calendar" className="h-3.5 w-3.5 text-primary" />
|
||||||
className="btn btn-accent btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-3 shadow-md hover:shadow-lg transition-all duration-300 rounded-full"
|
<span>
|
||||||
>
|
{startDate.toLocaleDateString("en-US", {
|
||||||
<Icon icon="heroicons:document-duplicate" className="h-3 w-3 sm:h-4 sm:w-4" />
|
weekday: "short",
|
||||||
Files ({event.files.length})
|
month: "short",
|
||||||
</button>
|
day: "numeric",
|
||||||
)}
|
})}
|
||||||
{isPastEvent && (
|
</span>
|
||||||
<div id={`attendance-badge-${event.id}`} className={`badge ${hasAttended ? 'badge-success' : 'badge-ghost'} text-xs gap-1`}>
|
|
||||||
<Icon
|
|
||||||
icon={hasAttended ? "heroicons:check-circle" : "heroicons:x-circle"}
|
|
||||||
className="h-3 w-3"
|
|
||||||
/>
|
|
||||||
{hasAttended ? 'Attended' : 'Not Attended'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-xs sm:text-sm opacity-75 ml-auto">
|
|
||||||
{event.location}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
{/* 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)}
|
||||||
|
className="btn btn-accent btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-3 shadow-md hover:shadow-lg transition-all duration-300 rounded-full"
|
||||||
|
>
|
||||||
|
<Icon icon="heroicons:document-duplicate" className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
Files ({event.files.length})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isPastEvent && (
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
{hasAttended ? 'Attended' : 'Not Attended'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -133,6 +133,28 @@ const EventForm = memo(({
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Points to Reward */}
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
@ -435,6 +457,7 @@ interface EventChanges {
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
published?: boolean;
|
published?: boolean;
|
||||||
has_food?: boolean;
|
has_food?: boolean;
|
||||||
|
event_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileChanges {
|
interface FileChanges {
|
||||||
|
@ -521,7 +544,8 @@ class ChangeTracker {
|
||||||
'start_date',
|
'start_date',
|
||||||
'end_date',
|
'end_date',
|
||||||
'published',
|
'published',
|
||||||
'has_food'
|
'has_food',
|
||||||
|
'event_type'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
@ -588,7 +612,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
start_date: "",
|
start_date: "",
|
||||||
end_date: "",
|
end_date: "",
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false
|
has_food: false,
|
||||||
|
event_type: "other"
|
||||||
});
|
});
|
||||||
|
|
||||||
const [previewUrl, setPreviewUrl] = useState("");
|
const [previewUrl, setPreviewUrl] = useState("");
|
||||||
|
@ -681,7 +706,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
start_date: eventData.start_date || '',
|
start_date: eventData.start_date || '',
|
||||||
end_date: eventData.end_date || '',
|
end_date: eventData.end_date || '',
|
||||||
published: eventData.published || false,
|
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
|
// Set up realtime subscription for this event
|
||||||
|
@ -727,7 +753,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
start_date: Get.formatLocalDate(now, false),
|
start_date: Get.formatLocalDate(now, false),
|
||||||
end_date: Get.formatLocalDate(oneHourLater, false),
|
end_date: Get.formatLocalDate(oneHourLater, false),
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false
|
has_food: false,
|
||||||
|
event_type: "other"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSelectedFiles(new Map());
|
setSelectedFiles(new Map());
|
||||||
|
@ -800,7 +827,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
start_date: "",
|
start_date: "",
|
||||||
end_date: "",
|
end_date: "",
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false
|
has_food: false,
|
||||||
|
event_type: "other"
|
||||||
});
|
});
|
||||||
setSelectedFiles(new Map());
|
setSelectedFiles(new Map());
|
||||||
setFilesToDelete(new Set());
|
setFilesToDelete(new Set());
|
||||||
|
@ -839,7 +867,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
start_date: "",
|
start_date: "",
|
||||||
end_date: "",
|
end_date: "",
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false
|
has_food: false,
|
||||||
|
event_type: "other"
|
||||||
});
|
});
|
||||||
setSelectedFiles(new Map());
|
setSelectedFiles(new Map());
|
||||||
setFilesToDelete(new Set());
|
setFilesToDelete(new Set());
|
||||||
|
@ -889,7 +918,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
start_date: formData.get("editEventStartDate") as string,
|
start_date: formData.get("editEventStartDate") as string,
|
||||||
end_date: formData.get("editEventEndDate") as string,
|
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",
|
||||||
|
event_type: formData.get("editEventType") as string || "other"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the update attempt
|
// Log the update attempt
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{
|
{
|
||||||
"title": "Quarterly Project",
|
"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.",
|
"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",
|
"number": "01",
|
||||||
"delay": "100"
|
"delay": "100"
|
||||||
},
|
},
|
||||||
|
|
|
@ -68,6 +68,7 @@ export interface Event extends BaseRecord {
|
||||||
start_date: string;
|
start_date: string;
|
||||||
end_date: string;
|
end_date: string;
|
||||||
published: boolean;
|
published: boolean;
|
||||||
|
event_type: string; // social, technical, outreach, professional, projects, other
|
||||||
has_food: boolean;
|
has_food: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue