diff --git a/src/components/auth/EventAuth.ts b/src/components/auth/EventAuth.ts deleted file mode 100644 index 119e45e..0000000 --- a/src/components/auth/EventAuth.ts +++ /dev/null @@ -1,1513 +0,0 @@ -import PocketBase from "pocketbase"; -import type { RecordModel } from "pocketbase"; -import yaml from "js-yaml"; -import configYaml from "../../data/storeConfig.yaml?raw"; -import JSZip from 'jszip'; - -// Configuration type definitions -interface Config { - api: { - baseUrl: string; - }; - ui: { - messages: { - event: { - saving: string; - success: string; - error: string; - deleting: string; - deleteSuccess: string; - deleteError: string; - messageTimeout: number; - }; - }; - tables: { - events: { - title: string; - columns: { - event_name: string; - event_id: string; - event_code: string; - start_date: string; - end_date: string; - points_to_reward: string; - location: string; - registered_users: string; - actions: string; - }; - form: { - buttons: { - edit: string; - delete: string; - }; - }; - }; - }; - defaults: { - pageSize: number; - sortField: string; - }; - }; -} - -// Parse YAML configuration with type -const config = yaml.load(configYaml) as Config; -const { columns, form } = config.ui.tables.events; - -interface Event { - id: string; - event_id: string; - event_name: string; - event_code: string; - attendees: string; // JSON string of attendee IDs - points_to_reward: number; - start_date: string; - end_date: string; - location: string; - files: string[]; // Array of file URLs - collectionId: string; - collectionName: string; -} - -interface AuthElements { - eventList: HTMLTableSectionElement; - eventSearch: HTMLInputElement; - searchEvents: HTMLButtonElement; - addEvent: HTMLButtonElement; - eventEditor: HTMLDialogElement; - editorEventId: HTMLInputElement; - editorEventName: HTMLInputElement; - editorEventCode: HTMLInputElement; - editorStartDate: HTMLInputElement; - editorStartTime: HTMLInputElement; - editorEndDate: HTMLInputElement; - editorEndTime: HTMLInputElement; - editorPointsToReward: HTMLInputElement; - editorLocation: HTMLInputElement; - editorFiles: HTMLInputElement; - currentFiles: HTMLDivElement; - saveEventButton: HTMLButtonElement; -} - -interface ValidationErrors { - eventId?: string; - eventName?: string; - eventCode?: string; - startDate?: string; - startTime?: string; - endDate?: string; - endTime?: string; - points?: string; -} - -export class EventAuth { - private pb: PocketBase; - private elements: AuthElements; - private cachedEvents: Event[] = []; - private abortController: AbortController | null = null; - private currentUploadXHR: XMLHttpRequest | null = null; - - constructor() { - this.pb = new PocketBase(config.api.baseUrl); - this.elements = this.getElements(); - this.init(); - } - - private getElements(): AuthElements { - const eventList = document.getElementById("eventList") as HTMLTableSectionElement; - const eventSearch = document.getElementById("eventSearch") as HTMLInputElement; - const searchEvents = document.getElementById("searchEvents") as HTMLButtonElement; - const addEvent = document.getElementById("addEvent") as HTMLButtonElement; - const eventEditor = document.getElementById("eventEditor") as HTMLDialogElement; - const editorEventId = document.getElementById("editorEventId") as HTMLInputElement; - const editorEventName = document.getElementById("editorEventName") as HTMLInputElement; - const editorEventCode = document.getElementById("editorEventCode") as HTMLInputElement; - const editorStartDate = document.getElementById("editorStartDate") as HTMLInputElement; - const editorStartTime = document.getElementById("editorStartTime") as HTMLInputElement; - const editorEndDate = document.getElementById("editorEndDate") as HTMLInputElement; - const editorEndTime = document.getElementById("editorEndTime") as HTMLInputElement; - const editorPointsToReward = document.getElementById("editorPointsToReward") as HTMLInputElement; - const editorLocation = document.getElementById("editorLocation") as HTMLInputElement; - const editorFiles = document.getElementById("editorFiles") as HTMLInputElement; - const currentFiles = document.getElementById("currentFiles") as HTMLDivElement; - const saveEventButton = document.getElementById("saveEventButton") as HTMLButtonElement; - - if ( - !eventList || - !eventSearch || - !searchEvents || - !addEvent || - !eventEditor || - !editorEventId || - !editorEventName || - !editorEventCode || - !editorStartDate || - !editorStartTime || - !editorEndDate || - !editorEndTime || - !editorPointsToReward || - !editorLocation || - !editorFiles || - !currentFiles || - !saveEventButton - ) { - throw new Error("Required DOM elements not found"); - } - - return { - eventList, - eventSearch, - searchEvents, - addEvent, - eventEditor, - editorEventId, - editorEventName, - editorEventCode, - editorStartDate, - editorStartTime, - editorEndDate, - editorEndTime, - editorPointsToReward, - editorLocation, - editorFiles, - currentFiles, - saveEventButton, - }; - } - - private getRegisteredUsersCount(registeredUsers: string): number { - // Handle different cases for registered_users field - if (!registeredUsers) return 0; - - try { - // Try to parse if it's a JSON string - const users = JSON.parse(registeredUsers); - // Ensure users is an array - if (!Array.isArray(users)) { - return 0; - } - return users.length; - } catch (err) { - console.warn("Failed to parse registered_users, using 0"); - return 0; - } - } - - private parseArrayField(field: any, defaultValue: any[] = []): any[] { - if (!field) return defaultValue; - if (Array.isArray(field)) return field; - try { - const parsed = JSON.parse(field); - return Array.isArray(parsed) ? parsed : defaultValue; - } catch (err) { - console.warn("Failed to parse array field:", err); - return defaultValue; - } - } - - private async fetchEvents(searchQuery: string = "") { - try { - // Only fetch from API if we don't have cached data - if (this.cachedEvents.length === 0) { - const records = await this.pb.collection("events").getList(1, config.ui.defaults.pageSize, { - sort: config.ui.defaults.sortField, - }); - this.cachedEvents = records.items as Event[]; - } - - // Filter cached data based on search query - let filteredEvents = this.cachedEvents; - if (searchQuery) { - const terms = searchQuery.toLowerCase().split(" ").filter(term => term.length > 0); - if (terms.length > 0) { - filteredEvents = this.cachedEvents.filter(event => { - return terms.every(term => - (event.event_name?.toLowerCase().includes(term) || - event.event_id?.toLowerCase().includes(term) || - event.event_code?.toLowerCase().includes(term) || - event.location?.toLowerCase().includes(term)) - ); - }); - } - } - - const { eventList } = this.elements; - const fragment = document.createDocumentFragment(); - - if (filteredEvents.length === 0) { - const row = document.createElement("tr"); - row.innerHTML = ` - - ${searchQuery ? "No events found matching your search." : "No events found."} - - `; - fragment.appendChild(row); - } else { - filteredEvents.forEach((event) => { - const row = document.createElement("tr"); - const startDate = event.start_date ? new Date(event.start_date).toLocaleString() : "N/A"; - const endDate = event.end_date ? new Date(event.end_date).toLocaleString() : "N/A"; - - // Parse attendees using the new helper method - const attendees = this.parseArrayField(event.attendees); - const attendeeCount = attendees.length; - - // Handle files display - const filesHtml = event.files && Array.isArray(event.files) && event.files.length > 0 - ? `` - : 'No files'; - - // Format dates for display - const formatDateTime = (dateStr: string) => { - if (!dateStr) return { dateDisplay: 'N/A', timeDisplay: 'N/A' }; - const date = new Date(dateStr); - return { - dateDisplay: date.toLocaleDateString(), - timeDisplay: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - }; - }; - - const startDateTime = formatDateTime(event.start_date); - const endDateTime = formatDateTime(event.end_date); - - row.innerHTML = ` - - -
-
${event.event_name || "N/A"}
-
${columns.event_id}: ${event.event_id || "N/A"}
-
${columns.event_code}: ${event.event_code || "N/A"}
-
${columns.start_date}: ${startDateTime.dateDisplay}
${startDateTime.timeDisplay}
-
${columns.end_date}: ${endDateTime.dateDisplay}
${endDateTime.timeDisplay}
-
${columns.points_to_reward}: ${event.points_to_reward || 0}
-
${columns.location}: ${event.location || "N/A"}
-
Files: ${filesHtml}
-
Attendees: ${attendeeCount}
-
- - - -
-
- - - - - ${event.event_id || "N/A"} - ${event.event_code || "N/A"} - -
- ${startDateTime.dateDisplay} - ${startDateTime.timeDisplay} -
- - -
- ${endDateTime.dateDisplay} - ${endDateTime.timeDisplay} -
- - ${event.points_to_reward || 0} - ${event.location || "N/A"} - ${filesHtml} - - - - -
- - -
- - `; - - fragment.appendChild(row); - }); - } - - eventList.innerHTML = ""; - eventList.appendChild(fragment); - - // Setup event listeners for buttons - const editButtons = eventList.querySelectorAll(".edit-event"); - editButtons.forEach((button) => { - button.addEventListener("click", () => { - const eventId = (button as HTMLButtonElement).dataset.eventId; - if (eventId) { - this.handleEventEdit(eventId); - } - }); - }); - - const deleteButtons = eventList.querySelectorAll(".delete-event"); - deleteButtons.forEach((button) => { - button.addEventListener("click", () => { - const eventId = (button as HTMLButtonElement).dataset.eventId; - if (eventId) { - this.handleEventDelete(eventId); - } - }); - }); - - const viewAttendeesButtons = eventList.querySelectorAll(".view-attendees"); - viewAttendeesButtons.forEach((button) => { - button.addEventListener("click", () => { - const eventId = (button as HTMLButtonElement).dataset.eventId; - if (eventId) { - this.handleViewAttendees(eventId); - } - }); - }); - - const viewFilesButtons = eventList.querySelectorAll(".view-files"); - viewFilesButtons.forEach((button) => { - button.addEventListener("click", async () => { - const eventId = (button as HTMLButtonElement).dataset.eventId; - if (eventId) { - await this.handleViewFiles(eventId); - } - }); - }); - } catch (err) { - console.error("Failed to fetch events:", err); - const { eventList } = this.elements; - eventList.innerHTML = ` - - - Failed to fetch events. Please try again. - - - `; - } - } - - private splitDateTime(dateTimeStr: string): { date: string; time: string } { - if (!dateTimeStr) return { date: "", time: "" }; - - // Create a date object in local timezone - const dateTime = new Date(dateTimeStr); - - // Format date as YYYY-MM-DD - const date = dateTime.toLocaleDateString('en-CA'); // en-CA gives YYYY-MM-DD format - - // Format time as HH:mm - const time = dateTime.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit' - }); - - return { date, time }; - } - - private combineDateTime(date: string, time: string): string { - if (!date || !time) return ""; - - // Create a new Date object from the date and time strings - const [year, month, day] = date.split('-').map(Number); - const [hours, minutes] = time.split(':').map(Number); - - // Create date in local timezone - const dateTime = new Date(year, month - 1, day, hours, minutes); - - // Format the date to ISO string with timezone offset - return dateTime.toISOString(); - } - - private getFileNameFromUrl(url: string): string { - try { - const urlObj = new URL(url); - const pathParts = urlObj.pathname.split("/"); - return decodeURIComponent(pathParts[pathParts.length - 1]); - } catch (e) { - // If URL parsing fails, try to get the filename from the path directly - return url.split("/").pop() || "Unknown File"; - } - } - - private async handleEventEdit(eventId: string) { - try { - const event = await this.pb.collection("events").getOne(eventId); - const { - eventEditor, - editorEventId, - editorEventName, - editorEventCode, - editorStartDate, - editorStartTime, - editorEndDate, - editorEndTime, - editorPointsToReward, - editorLocation, - currentFiles, - saveEventButton, - } = this.elements; - - // Split start and end dates into separate date and time - const startDateTime = this.splitDateTime(event.start_date); - const endDateTime = this.splitDateTime(event.end_date); - - // Populate the form - editorEventId.value = event.event_id || ""; - editorEventName.value = event.event_name || ""; - editorEventCode.value = event.event_code || ""; - editorStartDate.value = startDateTime.date; - editorStartTime.value = startDateTime.time; - editorEndDate.value = endDateTime.date; - editorEndTime.value = endDateTime.time; - editorPointsToReward.value = event.points_to_reward?.toString() || "0"; - editorLocation.value = event.location || ""; - - // Display current files - this.updateFilesDisplay(event); - - // Update file input to support multiple files - const fileInput = this.elements.editorFiles; - fileInput.setAttribute('multiple', 'true'); - fileInput.setAttribute('accept', '*/*'); - - // Store the event ID for saving - saveEventButton.dataset.eventId = eventId; - - // Disable event_id field for existing events - editorEventId.disabled = true; - - // Show the dialog - eventEditor.showModal(); - } catch (err) { - console.error("Failed to load event for editing:", err); - } - } - - private validateForm(): ValidationErrors | null { - const { - editorEventId, - editorEventName, - editorEventCode, - editorStartDate, - editorStartTime, - editorEndDate, - editorEndTime, - editorPointsToReward, - } = this.elements; - - const errors: ValidationErrors = {}; - - // Reset all error messages - const errorElements = document.querySelectorAll('.label-text-alt.text-error'); - errorElements.forEach(el => el.classList.add('hidden')); - - // Event ID validation - if (!editorEventId.disabled) { // Only validate if it's a new event - if (!editorEventId.value) { - errors.eventId = "Event ID is required"; - } else if (!editorEventId.value.match(/^[A-Za-z0-9_\-]+$/)) { - errors.eventId = "Event ID can only contain letters, numbers, underscores, and hyphens"; - } else if (editorEventId.value.length < 3) { - errors.eventId = "Event ID must be at least 3 characters"; - } else if (editorEventId.value.length > 50) { - errors.eventId = "Event ID must be less than 50 characters"; - } - } - - // Event Name validation - if (!editorEventName.value) { - errors.eventName = "Event Name is required"; - } else if (editorEventName.value.length < 3) { - errors.eventName = "Event Name must be at least 3 characters"; - } else if (editorEventName.value.length > 100) { - errors.eventName = "Event Name must be less than 100 characters"; - } - - // Event Code validation - if (!editorEventCode.value) { - errors.eventCode = "Event Code is required"; - } else if (!editorEventCode.value.match(/^[A-Za-z0-9_\-]+$/)) { - errors.eventCode = "Event Code can only contain letters, numbers, underscores, and hyphens"; - } else if (editorEventCode.value.length < 3) { - errors.eventCode = "Event Code must be at least 3 characters"; - } else if (editorEventCode.value.length > 20) { - errors.eventCode = "Event Code must be less than 20 characters"; - } - - // Date and Time validation - if (!editorStartDate.value) { - errors.startDate = "Start Date is required"; - } - if (!editorStartTime.value) { - errors.startTime = "Start Time is required"; - } - if (!editorEndDate.value) { - errors.endDate = "End Date is required"; - } - if (!editorEndTime.value) { - errors.endTime = "End Time is required"; - } - - // Validate that end date/time is after start date/time - if (editorStartDate.value && editorStartTime.value && editorEndDate.value && editorEndTime.value) { - const startDateTime = new Date(`${editorStartDate.value}T${editorStartTime.value}`); - const endDateTime = new Date(`${editorEndDate.value}T${editorEndTime.value}`); - - if (endDateTime <= startDateTime) { - errors.endDate = "End date/time must be after start date/time"; - } - } - - // Points validation - const points = parseInt(editorPointsToReward.value); - if (!editorPointsToReward.value) { - errors.points = "Points are required"; - } else if (isNaN(points) || points < 0) { - errors.points = "Points must be a positive number"; - } else if (points > 1000) { - errors.points = "Points must be less than 1000"; - } - - // Show error messages - if (errors.eventId) { - const errorEl = document.getElementById('eventIdError'); - if (errorEl) { - errorEl.textContent = errors.eventId; - errorEl.classList.remove('hidden'); - } - } - if (errors.eventName) { - const errorEl = document.getElementById('eventNameError'); - if (errorEl) { - errorEl.textContent = errors.eventName; - errorEl.classList.remove('hidden'); - } - } - if (errors.eventCode) { - const errorEl = document.getElementById('eventCodeError'); - if (errorEl) { - errorEl.textContent = errors.eventCode; - errorEl.classList.remove('hidden'); - } - } - if (errors.startDate) { - const errorEl = document.getElementById('startDateError'); - if (errorEl) { - errorEl.textContent = errors.startDate; - errorEl.classList.remove('hidden'); - } - } - if (errors.startTime) { - const errorEl = document.getElementById('startTimeError'); - if (errorEl) { - errorEl.textContent = errors.startTime; - errorEl.classList.remove('hidden'); - } - } - if (errors.endDate) { - const errorEl = document.getElementById('endDateError'); - if (errorEl) { - errorEl.textContent = errors.endDate; - errorEl.classList.remove('hidden'); - } - } - if (errors.endTime) { - const errorEl = document.getElementById('endTimeError'); - if (errorEl) { - errorEl.textContent = errors.endTime; - errorEl.classList.remove('hidden'); - } - } - if (errors.points) { - const errorEl = document.getElementById('pointsError'); - if (errorEl) { - errorEl.textContent = errors.points; - errorEl.classList.remove('hidden'); - } - } - - return Object.keys(errors).length > 0 ? errors : null; - } - - private async handleEventSave() { - const { - eventEditor, - editorEventId, - editorEventName, - editorEventCode, - editorStartDate, - editorStartTime, - editorEndDate, - editorEndTime, - editorPointsToReward, - editorLocation, - editorFiles, - saveEventButton, - } = this.elements; - - const eventId = saveEventButton.dataset.eventId; - const isNewEvent = !eventId; - - // Validate form before proceeding - const errors = this.validateForm(); - if (errors) { - return; // Stop if there are validation errors - } - - try { - // Combine date and time inputs - const startDateTime = this.combineDateTime(editorStartDate.value, editorStartTime.value); - const endDateTime = this.combineDateTime(editorEndDate.value, editorEndTime.value); - - // Create FormData for file upload - const formData = new FormData(); - formData.append("event_name", editorEventName.value); - formData.append("event_code", editorEventCode.value); - formData.append("start_date", startDateTime); - formData.append("end_date", endDateTime); - formData.append("points_to_reward", editorPointsToReward.value); - formData.append("location", editorLocation.value); - - // For new events, set event_id and initialize attendees as empty array - if (isNewEvent) { - formData.append("event_id", editorEventId.value); - formData.append("attendees", "[]"); // Initialize with empty array - } else { - // For existing events, preserve current attendees and files - const currentEvent = await this.pb.collection("events").getOne(eventId); - const currentAttendees = this.parseArrayField(currentEvent.attendees, []); - formData.append("attendees", JSON.stringify(currentAttendees)); - - // Preserve existing files if no new files are being uploaded - if (currentEvent.files && (!editorFiles.files || editorFiles.files.length === 0)) { - formData.append("files", JSON.stringify(currentEvent.files)); - } - } - - // Handle file uploads - if (editorFiles.files && editorFiles.files.length > 0) { - Array.from(editorFiles.files).forEach(file => { - formData.append("files", file); - }); - } - - if (isNewEvent) { - await this.pb.collection("events").create(formData); - } else { - await this.pb.collection("events").update(eventId, formData); - } - - // Close the dialog and refresh the table - eventEditor.close(); - this.cachedEvents = []; // Clear cache to force refresh - this.fetchEvents(); - } catch (err) { - console.error("Failed to save event:", err); - } - } - - private async handleEventDelete(eventId: string) { - if (confirm("Are you sure you want to delete this event?")) { - try { - await this.pb.collection("events").delete(eventId); - this.cachedEvents = []; // Clear cache to force refresh - this.fetchEvents(); - } catch (err) { - console.error("Failed to delete event:", err); - } - } - } - - private async handleViewAttendees(eventId: string) { - try { - const event = await this.pb.collection("events").getOne(eventId); - const attendees = this.parseArrayField(event.attendees); - - // Fetch user details for each attendee - const userDetails = await Promise.all( - attendees.map(async (userId: string) => { - try { - const user = await this.pb.collection("users").getOne(userId); - return { - name: user.name || "N/A", - email: user.email || "N/A", - member_id: user.member_id || "N/A" - }; - } catch (err) { - console.warn(`Failed to fetch user ${userId}:`, err); - return { - name: "Unknown User", - email: "N/A", - member_id: "N/A" - }; - } - }) - ); - - // Create and show modal - const modal = document.createElement("dialog"); - modal.className = "modal"; - modal.innerHTML = ` - - - `; - - document.body.appendChild(modal); - modal.showModal(); - - // Add search functionality - const searchInput = modal.querySelector("#attendeeSearch") as HTMLInputElement; - const attendeeRows = modal.querySelectorAll(".attendee-row"); - - if (searchInput) { - searchInput.addEventListener("input", () => { - const searchTerm = searchInput.value.toLowerCase(); - attendeeRows.forEach((row) => { - const text = row.textContent?.toLowerCase() || ""; - if (text.includes(searchTerm)) { - (row as HTMLElement).style.display = ""; - } else { - (row as HTMLElement).style.display = "none"; - } - }); - }); - } - - // Remove modal when closed - modal.addEventListener("close", () => { - document.body.removeChild(modal); - }); - } catch (err) { - console.error("Failed to view attendees:", err); - } - } - - private async syncAttendees() { - try { - // Cancel any existing sync operation - if (this.abortController) { - this.abortController.abort(); - } - - // Create new abort controller for this sync operation - this.abortController = new AbortController(); - - console.log("=== STARTING ATTENDEE SYNC ==="); - - // Fetch all events first with abort signal - const events = await this.pb.collection("events").getFullList({ - sort: config.ui.defaults.sortField, - $cancelKey: "syncAttendees", - }); - - // Early return if aborted - if (this.abortController.signal.aborted) { - console.log("Sync operation was cancelled"); - return; - } - - console.log("=== EVENTS DATA ==="); - events.forEach(event => { - console.log(`Event: ${event.event_name} (ID: ${event.id})`); - console.log("- event_id:", event.event_id); - console.log("- Raw attendees field:", event.attendees); - }); - - // Create a map of event_id to event record for faster lookup - const eventMap = new Map(); - events.forEach(event => { - const currentAttendees = this.parseArrayField(event.attendees); - console.log(`Parsed attendees for event ${event.event_name}:`, currentAttendees); - - eventMap.set(event.event_id, { - id: event.id, - event_id: event.event_id, - event_name: event.event_name, - attendees: new Set(currentAttendees) - }); - console.log(`Mapped event ${event.event_name} with event_id ${event.event_id}`); - }); - - // Check if aborted before fetching users - if (this.abortController.signal.aborted) { - console.log("Sync operation was cancelled"); - return; - } - - // Fetch all users with abort signal - const users = await this.pb.collection("users").getFullList({ - fields: "id,name,email,member_id,events_attended", - $cancelKey: "syncAttendees", - }); - - console.log("=== USERS DATA ==="); - users.forEach(user => { - console.log(`User: ${user.name || 'Unknown'} (ID: ${user.id})`); - console.log("- Raw events_attended:", user.events_attended); - }); - - // Process each user's events_attended - for (const user of users) { - // Check if aborted before processing each user - if (this.abortController.signal.aborted) { - console.log("Sync operation was cancelled"); - return; - } - - console.log(`\nProcessing user: ${user.name || 'Unknown'} (ID: ${user.id})`); - const eventsAttended = this.parseArrayField(user.events_attended); - console.log("Parsed events_attended:", eventsAttended); - - // For each event the user attended - for (const eventId of eventsAttended) { - console.log(`\nChecking event ${eventId} for user ${user.id}`); - - // Find the event by event_id - const eventRecord = eventMap.get(eventId); - if (eventRecord) { - console.log(`Found event record:`, eventRecord); - // If user not already in attendees, add them - if (!eventRecord.attendees.has(user.id)) { - eventRecord.attendees.add(user.id); - console.log(`Added user ${user.id} to event ${eventId}`); - } else { - console.log(`User ${user.id} already in event ${eventId}`); - } - } else { - console.log(`Event ${eventId} not found in event map. Available event_ids:`, - Array.from(eventMap.keys())); - } - } - } - - // Update all events with new attendee lists - console.log("\n=== UPDATING EVENTS ==="); - for (const [eventId, record] of eventMap.entries()) { - // Check if aborted before each update - if (this.abortController.signal.aborted) { - console.log("Sync operation was cancelled"); - return; - } - - try { - const attendeeArray = Array.from(record.attendees); - console.log(`Updating event ${eventId}:`); - console.log("- Current attendees:", attendeeArray); - - await this.pb.collection("events").update(record.id, { - attendees: JSON.stringify(attendeeArray) - }, { - $cancelKey: "syncAttendees", - }); - console.log(`Successfully updated event ${eventId}`); - } catch (err: any) { - if (err?.name === "AbortError") { - console.log("Update was cancelled"); - return; - } - console.error(`Failed to update attendees for event ${eventId}:`, err); - console.error("Failed record:", record); - } - } - - // Clear the cache to force a refresh of the events list - this.cachedEvents = []; - console.log("\n=== SYNC COMPLETE ==="); - await this.fetchEvents(); - } catch (err: any) { - if (err?.name === "AbortError") { - console.log("Sync operation was cancelled"); - return; - } - console.error("Failed to sync attendees:", err); - console.error("Full error:", err); - } finally { - // Clear the abort controller when done - this.abortController = null; - } - } - - private cleanup() { - if (this.abortController) { - this.abortController.abort(); - this.abortController = null; - } - } - - private async refreshEventsAndSync() { - try { - // Clear the cache to force a fresh fetch - this.cachedEvents = []; - - // Check if user is authorized to sync - const user = this.pb.authStore.model; - if (user && (user.member_type === "IEEE Officer" || user.member_type === "IEEE Administrator")) { - await this.syncAttendees().catch(console.error); - } else { - // If not authorized to sync, just refresh the events - await this.fetchEvents(); - } - } catch (err) { - console.error("Failed to refresh events:", err); - } - } - - private init() { - // Add file input change handler for automatic upload - const fileInput = this.elements.editorFiles; - - // Add dialog close handler to cancel uploads - this.elements.eventEditor.addEventListener('close', () => { - if (this.currentUploadXHR) { - this.currentUploadXHR.abort(); - this.currentUploadXHR = null; - - // Hide progress bar - const progressContainer = document.getElementById('uploadProgress'); - if (progressContainer) { - progressContainer.classList.add('hidden'); - } - - // Clear file input - fileInput.value = ''; - } - }); - - fileInput.addEventListener('change', async () => { - const selectedFiles = fileInput.files; - if (selectedFiles && selectedFiles.length > 0) { - try { - // Validate file sizes - const MAX_FILE_SIZE = 999999999; // ~1GB max per file (server limit) - const MAX_TOTAL_SIZE = 999999999; // ~1GB max total (server limit) - let totalSize = 0; - - for (const file of selectedFiles) { - if (file.size > MAX_FILE_SIZE) { - throw new Error(`File "${file.name}" is too large. Maximum size per file is ${(MAX_FILE_SIZE / 1024 / 1024).toFixed(0)}MB.`); - } - totalSize += file.size; - } - - if (totalSize > MAX_TOTAL_SIZE) { - throw new Error(`Total file size (${(totalSize / 1024 / 1024).toFixed(1)}MB) exceeds the limit of ${(MAX_TOTAL_SIZE / 1024 / 1024).toFixed(0)}MB.`); - } - - // Get the current event ID - const eventId = this.elements.saveEventButton.dataset.eventId; - if (!eventId) { - throw new Error('No event ID found'); - } - - // Get current event to preserve existing files - const currentEvent = await this.pb.collection("events").getOne(eventId); - const formData = new FormData(); - - // Preserve existing files by adding them to formData - if (currentEvent.files && Array.isArray(currentEvent.files)) { - formData.append("files", JSON.stringify(currentEvent.files)); - } - - // Add new files to the formData - Array.from(selectedFiles).forEach(file => { - formData.append("files", file); - }); - - // Show progress bar container - const progressContainer = document.getElementById('uploadProgress'); - const progressBar = document.getElementById('uploadProgressBar') as HTMLProgressElement; - const progressText = document.getElementById('uploadProgressText'); - if (progressContainer) { - progressContainer.classList.remove('hidden'); - // Reset progress - if (progressBar) progressBar.value = 0; - if (progressText) progressText.textContent = '0%'; - } - - // Create XMLHttpRequest for better progress tracking - const xhr = new XMLHttpRequest(); - this.currentUploadXHR = xhr; // Store the XHR request - const url = `${this.pb.baseUrl}/api/collections/events/records/${eventId}`; - - xhr.upload.onprogress = (e) => { - if (e.lengthComputable) { - const progress = Math.round((e.loaded * 100) / e.total); - if (progressBar) progressBar.value = progress; - if (progressText) progressText.textContent = `${progress}%`; - } - }; - - xhr.onload = async () => { - this.currentUploadXHR = null; // Clear the XHR reference - if (xhr.status === 200) { - // Update was successful - const response = JSON.parse(xhr.responseText); - const event = await this.pb.collection("events").getOne(eventId); - this.updateFilesDisplay(event); - - // Clear the file input - fileInput.value = ''; - - // Hide progress bar after a short delay - setTimeout(() => { - if (progressContainer) { - progressContainer.classList.add('hidden'); - if (progressBar) progressBar.value = 0; - if (progressText) progressText.textContent = '0%'; - } - }, 1000); - } else { - let errorMessage = 'Failed to upload files. '; - try { - const errorResponse = JSON.parse(xhr.responseText); - errorMessage += errorResponse.message || 'Please try again.'; - } catch { - if (xhr.status === 413) { - errorMessage += 'Files are too large. Please reduce file sizes and try again.'; - } else if (xhr.status === 401) { - errorMessage += 'Your session has expired. Please log in again.'; - } else { - errorMessage += 'Please try again.'; - } - } - console.error('Upload failed:', errorMessage); - alert(errorMessage); - if (progressContainer) progressContainer.classList.add('hidden'); - fileInput.value = ''; - } - }; - - xhr.onerror = () => { - this.currentUploadXHR = null; // Clear the XHR reference - console.error('Upload failed'); - let errorMessage = 'Failed to upload files. '; - if (xhr.status === 413) { - errorMessage += 'Files are too large. Please reduce file sizes and try again.'; - } else if (xhr.status === 0) { - errorMessage += 'Network error or CORS issue. Please try again later.'; - } else { - errorMessage += 'Please try again.'; - } - alert(errorMessage); - if (progressContainer) progressContainer.classList.add('hidden'); - fileInput.value = ''; - }; - - xhr.onabort = () => { - this.currentUploadXHR = null; // Clear the XHR reference - console.log('Upload cancelled'); - if (progressContainer) progressContainer.classList.add('hidden'); - fileInput.value = ''; - }; - - // Get the auth token - const token = this.pb.authStore.token; - - // Send the request - xhr.open('PATCH', url, true); - xhr.setRequestHeader('Authorization', token); - xhr.send(formData); - } catch (err) { - console.error('Failed to upload files:', err); - alert(err instanceof Error ? err.message : 'Failed to upload files. Please try again.'); - - // Hide progress bar on error - const progressContainer = document.getElementById('uploadProgress'); - if (progressContainer) progressContainer.classList.add('hidden'); - - // Clear file input - fileInput.value = ''; - } - } - }); - - // Only sync attendees if user is an officer or administrator - setTimeout(async () => { - const user = this.pb.authStore.model; - if (user && (user.member_type === "IEEE Officer" || user.member_type === "IEEE Administrator")) { - await this.syncAttendees().catch(console.error); - } - }, 100); - - // Initial fetch - this.fetchEvents(); - - // Search functionality - const handleSearch = () => { - const searchQuery = this.elements.eventSearch.value.trim(); - this.fetchEvents(searchQuery); - }; - - // Real-time search - this.elements.eventSearch.addEventListener("input", handleSearch); - - // Search button click handler - this.elements.searchEvents.addEventListener("click", handleSearch); - - // Refresh button click handler - const refreshButton = document.getElementById("refreshEvents"); - if (refreshButton) { - refreshButton.addEventListener("click", () => { - this.refreshEventsAndSync(); - }); - } - - // Add event button - this.elements.addEvent.addEventListener("click", () => { - const { eventEditor, editorEventId, saveEventButton } = this.elements; - - // Clear the form - this.elements.editorEventId.value = ""; - this.elements.editorEventName.value = ""; - this.elements.editorEventCode.value = ""; - this.elements.editorStartDate.value = ""; - this.elements.editorStartTime.value = ""; - this.elements.editorEndDate.value = ""; - this.elements.editorEndTime.value = ""; - this.elements.editorPointsToReward.value = "0"; - - // Enable event_id field for new events - editorEventId.disabled = false; - - // Clear the event ID to indicate this is a new event - saveEventButton.dataset.eventId = ""; - - // Show the dialog - eventEditor.showModal(); - }); - - // Event editor dialog - const { eventEditor, saveEventButton } = this.elements; - - // Close dialog when clicking outside - eventEditor.addEventListener("click", (e) => { - if (e.target === eventEditor) { - eventEditor.close(); - } - }); - - // Save event button - saveEventButton.addEventListener("click", (e) => { - e.preventDefault(); - this.handleEventSave(); - }); - } - - // Add this new method to handle files display update - private updateFilesDisplay(event: RecordModel) { - const { currentFiles } = this.elements; - currentFiles.innerHTML = ""; - - if (event.files && Array.isArray(event.files) && event.files.length > 0) { - const filesList = document.createElement("div"); - filesList.className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"; - - event.files.forEach((file: string) => { - const fileUrl = this.pb.files.getURL(event, file); - const fileName = this.getFileNameFromUrl(file); - const fileExt = fileName.split('.').pop()?.toLowerCase() || ''; - const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); - - const fileItem = document.createElement("div"); - fileItem.className = "bg-base-200 rounded-lg overflow-hidden"; - - // Generate preview based on file type - let previewHtml = ''; - if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) { - previewHtml = ` -
- ${fileName} -
- `; - } else { - // For other file types, show an icon based on type - const iconHtml = fileExt === 'txt' || fileExt === 'md' - ? ` - - ` - : ` - - `; - - previewHtml = ` -
- ${iconHtml} -
- `; - } - - fileItem.innerHTML = ` - ${previewHtml} -
-

- ${nameWithoutExt} - ${fileExt} -

-
-
- - - - - - Open - - -
- `; - - // Add delete handler - const deleteButton = fileItem.querySelector('.text-error'); - if (deleteButton) { - deleteButton.addEventListener('click', async (e) => { - e.preventDefault(); - if (confirm('Are you sure you want to remove this file?')) { - try { - const fileToRemove = deleteButton.getAttribute('data-file'); - if (!fileToRemove) throw new Error('File not found'); - - // Get the current event data - const currentEvent = await this.pb.collection('events').getOne(event.id); - - // Filter out the file to be removed - const updatedFiles = currentEvent.files.filter((f: string) => f !== fileToRemove); - - // Update the event with the new files array - await this.pb.collection('events').update(event.id, { - files: updatedFiles - }); - - // Update the local event object - event.files = updatedFiles; - - // Remove the file item from the UI - fileItem.remove(); - - // If no files left, show the "No files" message - if (!event.files || event.files.length === 0) { - currentFiles.innerHTML = 'No files'; - } - } catch (err) { - console.error('Failed to remove file:', err); - alert('Failed to remove file. Please try again.'); - } - } - }); - } - - filesList.appendChild(fileItem); - }); - - currentFiles.appendChild(filesList); - } else { - currentFiles.innerHTML = 'No files'; - } - } - - private async handleViewFiles(eventId: string): Promise { - try { - const event = await this.pb.collection("events").getOne(eventId); - - // Create and show modal - const modal = document.createElement("dialog"); - modal.className = "modal"; - const modalContent = ` - - - `; - - modal.innerHTML = modalContent; - document.body.appendChild(modal); - modal.showModal(); - - // Add download all functionality - const downloadAllButton = modal.querySelector('.download-all') as HTMLButtonElement; - if (downloadAllButton) { - downloadAllButton.addEventListener('click', async () => { - try { - // Create a new JSZip instance - const zip = new JSZip(); - - // Add each file to the zip - for (const file of event.files) { - const fileUrl = this.pb.files.getURL(event, file); - const fileName = this.getFileNameFromUrl(file); - const response = await fetch(fileUrl); - const blob = await response.blob(); - zip.file(fileName, blob); - } - - // Generate the zip file - const content = await zip.generateAsync({ type: "blob" }); - - // Create a download link and trigger it - const downloadUrl = URL.createObjectURL(content); - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = `${event.event_name} Files.zip`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(downloadUrl); - } catch (err) { - console.error('Failed to download files:', err); - alert('Failed to download files. Please try again.'); - } - }); - } - - // Remove modal when closed - modal.addEventListener("close", () => { - document.body.removeChild(modal); - }); - } catch (err) { - console.error("Failed to view files:", err); - } - } -} \ No newline at end of file diff --git a/src/components/auth/EventCheckIn.ts b/src/components/auth/EventCheckIn.ts index 8370615..b7c12af 100644 --- a/src/components/auth/EventCheckIn.ts +++ b/src/components/auth/EventCheckIn.ts @@ -1,32 +1,23 @@ import PocketBase from "pocketbase"; import yaml from "js-yaml"; -import configYaml from "../../data/storeConfig.yaml?raw"; -import { SendLog } from "./SendLog"; +import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw"; +import textConfig from "../../config/text.yml?raw"; +import { SendLog } from "../pocketbase/SendLog"; // Configuration type definitions interface Config { api: { baseUrl: string; - }; - ui: { - messages: { - event: { - checkIn: { - checking: string; - success: string; - error: string; - invalid: string; - expired: string; - alreadyCheckedIn: string; - messageTimeout: number; - }; - }; + oauth2: { + redirectPath: string; + providerName: string; }; }; } -// Parse YAML configuration with type -const config = yaml.load(configYaml) as Config; +// Parse YAML configuration +const config = yaml.load(pocketbaseConfig) as Config; +const text = yaml.load(textConfig) as any; interface AuthElements { eventCodeInput: HTMLInputElement; @@ -42,7 +33,7 @@ export class EventCheckIn { constructor() { this.pb = new PocketBase(config.api.baseUrl); this.elements = this.getElements(); - this.logger = new SendLog(); + this.logger = SendLog.getInstance(); // Add event listener for the check-in button this.elements.checkInButton.addEventListener("click", () => this.handleCheckIn()); @@ -90,7 +81,7 @@ export class EventCheckIn { if (events.length === 0) { return { isValid: false, - message: `Event code "${code}" does not match any active events.` + message: text.ui.messages.event.checkIn.invalid }; } @@ -104,7 +95,7 @@ export class EventCheckIn { return { isValid: false, event, - message: `Event "${event.event_name}" check-in is not open yet. Check-in opens at ${startDate.toLocaleString()}.` + message: text.ui.messages.event.checkIn.expired }; } @@ -112,7 +103,7 @@ export class EventCheckIn { return { isValid: false, event, - message: `Event "${event.event_name}" check-in has closed. Check-in closed at ${endDate.toLocaleString()}.` + message: text.ui.messages.event.checkIn.expired }; } @@ -121,7 +112,7 @@ export class EventCheckIn { console.error('Failed to validate event code:', err); return { isValid: false, - message: `Failed to validate event code "${code}". Error: ${err instanceof Error ? err.message : "Unknown error"}` + message: text.ui.messages.event.checkIn.error }; } } @@ -131,17 +122,19 @@ export class EventCheckIn { const eventCode = eventCodeInput.value.trim(); if (!eventCode) { - this.showStatus("Please enter an event code", "error"); + this.showStatus(text.ui.messages.event.checkIn.invalid, "error"); return; } + let validation: { isValid: boolean; event?: any; message?: string } | undefined; + try { - this.showStatus(config.ui.messages.event.checkIn.checking, "info"); + this.showStatus(text.ui.messages.event.checkIn.checking, "info"); // Get current user const user = this.pb.authStore.model; if (!user) { - this.showStatus("Please sign in to check in to events", "error"); + this.showStatus(text.ui.messages.auth.notSignedIn, "error"); await this.logger.send( "error", "event check in", @@ -151,9 +144,9 @@ export class EventCheckIn { } // Validate event code and check time window - const validation = await this.validateEventCode(eventCode); + validation = await this.validateEventCode(eventCode); if (!validation.isValid) { - this.showStatus(validation.message || "Invalid event code.", "error"); + this.showStatus(validation.message || text.ui.messages.event.checkIn.invalid, "error"); await this.logger.send( "error", "event check in", @@ -191,7 +184,7 @@ export class EventCheckIn { const isAlreadyCheckedIn = eventsAttended.includes(event.event_id); if (isAlreadyCheckedIn) { - this.showStatus(`You have already checked in to ${event.event_name}`, "info"); + this.showStatus(text.ui.messages.event.checkIn.alreadyCheckedIn, "info"); await this.logger.send( "error", "event check in", @@ -232,10 +225,7 @@ export class EventCheckIn { } // Show success message with points - this.showStatus( - `Successfully checked in to ${event.event_name}! You earned ${pointsToAdd} points!`, - "success" - ); + this.showStatus(text.ui.messages.event.checkIn.success, "success"); eventCodeInput.value = ""; // Clear input // Log the successful check-in @@ -268,14 +258,22 @@ export class EventCheckIn { } catch (err) { console.error("Check-in error:", err); - this.showStatus(config.ui.messages.event.checkIn.error, "error"); + this.showStatus(text.ui.messages.event.checkIn.error, "error"); // Log any errors that occur during check-in - await this.logger.send( - "error", - "event check in", - `Failed to check in to event ${event.id}: ${err instanceof Error ? err.message : "Unknown error"}` - ); + if (validation?.event) { + await this.logger.send( + "error", + "event check in", + `Failed to check in to event ${validation.event.id}: ${err instanceof Error ? err.message : "Unknown error"}` + ); + } else { + await this.logger.send( + "error", + "event check in", + `Failed to check in: ${err instanceof Error ? err.message : "Unknown error"}` + ); + } } } @@ -300,7 +298,7 @@ export class EventCheckIn { if (type !== "info") { setTimeout(() => { checkInStatus.textContent = ""; - }, config.ui.messages.event.checkIn.messageTimeout); + }, text.ui.messages.event.checkIn.messageTimeout); } } diff --git a/src/components/auth/SendLog.ts b/src/components/auth/SendLog.ts deleted file mode 100644 index 0621a1d..0000000 --- a/src/components/auth/SendLog.ts +++ /dev/null @@ -1,73 +0,0 @@ -import PocketBase from "pocketbase"; -import { StoreAuth } from "./StoreAuth"; - -// Log interface -interface LogData { - user_id: string; - type: string; // Standard types: "error", "update", "delete", "create", "login", "logout" - part: string; // The specific part/section being logged (can be multiple words, e.g., "profile settings", "resume upload") - message: string; -} - -export class SendLog { - private pb: PocketBase; - - constructor() { - // Use the same PocketBase instance as StoreAuth to maintain authentication - const auth = new StoreAuth(); - this.pb = auth["pb"]; - } - - /** - * Gets the current authenticated user's ID - * @returns The user ID or null if not authenticated - */ - private getCurrentUserId(): string | null { - return this.pb.authStore.model?.id || null; - } - - /** - * Sends a log entry to PocketBase - * @param type The type of log entry. Standard types: - * - "error": For error conditions - * - "update": For successful updates/uploads - * - "delete": For deletion operations - * - "create": For creation operations - * - "login": For login events - * - "logout": For logout events - * @param part The specific part/section being logged. Can be multiple words: - * - "profile settings": Profile settings changes - * - "resume upload": Resume file operations - * - "notification settings": Notification preference changes - * - "user authentication": Auth-related actions - * - "event check in": Event attendance tracking - * - "loyalty points": Points updates from events/activities - * - "event management": Event creation/modification - * - "event attendance": Overall event attendance status - * @param message The log message - * @param overrideUserId Optional user ID to override the current user (for admin/system logs) - * @returns Promise that resolves when the log is created - */ - public async send(type: string, part: string, message: string, overrideUserId?: string) { - try { - const userId = overrideUserId || this.getCurrentUserId(); - - if (!userId) { - throw new Error("No user ID available. User must be authenticated to create logs."); - } - - const logData: LogData = { - user_id: userId, - type, - part, - message - }; - - // Create the log entry in PocketBase - await this.pb.collection("logs").create(logData); - } catch (error) { - console.error("Failed to send log:", error); - throw error; - } - } -} \ No newline at end of file diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts deleted file mode 100644 index 10c06db..0000000 --- a/src/components/auth/StoreAuth.ts +++ /dev/null @@ -1,426 +0,0 @@ -import PocketBase from "pocketbase"; -import yaml from "js-yaml"; -import configYaml from "../../data/storeConfig.yaml?raw"; - -// Configuration type definitions -interface Role { - name: string; - badge: string; - permissions: string[]; -} - -interface Config { - api: { - baseUrl: string; - oauth2: { - redirectPath: string; - providerName: string; - }; - }; - roles: { - administrator: Role; - officer: Role; - sponsor: Role; - member: Role; - }; - resume: { - allowedTypes: string[]; - maxSize: number; - viewer: { - width: string; - maxWidth: string; - height: string; - }; - }; - ui: { - transitions: { - fadeDelay: number; - }; - messages: { - memberId: { - saving: string; - success: string; - error: string; - messageTimeout: number; - }; - resume: { - uploading: string; - success: string; - error: string; - deleting: string; - deleteSuccess: string; - deleteError: string; - messageTimeout: number; - }; - auth: { - loginError: string; - notSignedIn: string; - notVerified: string; - notProvided: string; - notAvailable: string; - never: string; - }; - }; - defaults: { - pageSize: number; - sortField: string; - }; - }; - autoDetection: { - officer: { - emailDomain: string; - }; - }; -} - -// Parse YAML configuration with type -const config = yaml.load(configYaml) as Config; - -interface AuthElements { - loginButton: HTMLButtonElement; - logoutButton: HTMLButtonElement; - userInfo: HTMLDivElement; - userName: HTMLParagraphElement; - userEmail: HTMLParagraphElement; - memberStatus: HTMLDivElement; - lastLogin: HTMLParagraphElement; - storeContent: HTMLDivElement; - officerViewToggle: HTMLDivElement; - officerViewCheckbox: HTMLInputElement; - officerContent: HTMLDivElement; - profileEditor: HTMLDialogElement; - editorName: HTMLInputElement; - editorEmail: HTMLInputElement; - editorPoints: HTMLInputElement; - saveProfileButton: HTMLButtonElement; - sponsorViewToggle: HTMLDivElement; -} - -export class StoreAuth { - private pb: PocketBase; - private elements: AuthElements & { loadingSkeleton: HTMLDivElement }; - private cachedUsers: any[] = []; - private config = config; - - constructor() { - this.pb = new PocketBase(this.config.api.baseUrl); - this.elements = this.getElements(); - this.init(); - } - - // Public method to get auth state - public getAuthState() { - return { - isValid: this.pb.authStore.isValid, - model: this.pb.authStore.model - }; - } - - // Public method to handle login - public async handleLogin() { - try { - const authMethods = await this.pb.collection("users").listAuthMethods(); - const oidcProvider = authMethods.oauth2?.providers?.find( - (p: { name: string }) => p.name === this.config.api.oauth2.providerName - ); - - if (!oidcProvider) { - throw new Error("OIDC provider not found"); - } - - localStorage.setItem("provider", JSON.stringify(oidcProvider)); - const redirectUrl = window.location.origin + this.config.api.oauth2.redirectPath; - const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl); - window.location.href = authUrl; - } catch (err) { - console.error("Authentication error:", err); - this.elements.userEmail.textContent = this.config.ui.messages.auth.loginError; - this.elements.userName.textContent = "Error"; - throw err; - } - } - - // Public method to update profile settings - public async updateProfileSettings(data: { - major?: string | null; - graduation_year?: number | null; - member_id?: string | null; - }) { - const user = this.pb.authStore.model; - if (!user?.id) { - throw new Error("User ID not found"); - } - - return await this.pb.collection("users").update(user.id, data); - } - - /** - * Handles uploading a resume file for the current user - * @param file The resume file to upload - * @returns Promise that resolves when the resume is uploaded - */ - public async handleResumeUpload(file: File) { - const user = this.pb.authStore.model; - if (!user?.id) { - throw new Error("User ID not found"); - } - - // Validate file type - const allowedTypes = ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]; - if (!allowedTypes.includes(file.type)) { - throw new Error("Invalid file type. Please upload a PDF or Word document."); - } - - // Validate file size (5MB max) - const maxSize = 5 * 1024 * 1024; // 5MB in bytes - if (file.size > maxSize) { - throw new Error("File size too large. Maximum size is 5MB."); - } - - // Create form data with the file - const formData = new FormData(); - formData.append("resume", file); - - // Update the user record with the new resume - return await this.pb.collection("users").update(user.id, formData); - } - - private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } { - // Get all required elements - const loginButton = document.getElementById("contentLoginButton") as HTMLButtonElement; - const logoutButton = document.getElementById("contentLogoutButton") as HTMLButtonElement; - const userInfo = document.getElementById("userInfo") as HTMLDivElement; - const loadingSkeleton = document.getElementById("loadingSkeleton") as HTMLDivElement; - const userName = document.getElementById("userName") as HTMLParagraphElement; - const userEmail = document.getElementById("userEmail") as HTMLParagraphElement; - const memberStatus = document.getElementById("memberStatus") as HTMLDivElement; - const lastLogin = document.getElementById("lastLogin") as HTMLParagraphElement; - const storeContent = document.getElementById("storeContent") as HTMLDivElement; - const officerViewToggle = document.getElementById("officerViewToggle") as HTMLDivElement; - const officerViewCheckbox = officerViewToggle?.querySelector('input[type="checkbox"]') as HTMLInputElement; - const officerContent = document.getElementById("officerContent") as HTMLDivElement; - const profileEditor = document.getElementById("profileEditor") as HTMLDialogElement; - const editorName = document.getElementById("editorName") as HTMLInputElement; - const editorEmail = document.getElementById("editorEmail") as HTMLInputElement; - const editorPoints = document.getElementById("editorPoints") as HTMLInputElement; - const saveProfileButton = document.getElementById("saveProfileButton") as HTMLButtonElement; - const sponsorViewToggle = document.getElementById("sponsorViewToggle") as HTMLDivElement; - - // Add CSS for loading state transitions - const style = document.createElement("style"); - style.textContent = ` - .loading-state { - opacity: 0.5; - transition: opacity 0.3s ease-in-out; - } - .content-ready { - opacity: 1; - } - `; - document.head.appendChild(style); - - return { - loginButton, - logoutButton, - userInfo, - loadingSkeleton, - userName, - userEmail, - memberStatus, - lastLogin, - storeContent, - officerViewToggle, - officerViewCheckbox, - officerContent, - profileEditor, - editorName, - editorEmail, - editorPoints, - saveProfileButton, - sponsorViewToggle - }; - } - - private async init() { - // Initial UI update with loading state - await this.updateUI(); - - // Setup event listeners - this.elements.loginButton.addEventListener("click", () => this.handleLogin()); - this.elements.logoutButton.addEventListener("click", () => this.handleLogout()); - - // Listen for auth state changes - this.pb.authStore.onChange(() => { - console.log("Auth state changed. IsValid:", this.pb.authStore.isValid); - this.updateUI(); - }); - - // Profile editor event listeners - const { profileEditor, saveProfileButton } = this.elements; - - // Close dialog when clicking outside - profileEditor.addEventListener("click", (e) => { - if (e.target === profileEditor) { - profileEditor.close(); - } - }); - - // Save profile button - saveProfileButton.addEventListener("click", (e) => { - e.preventDefault(); - this.handleProfileSave(); - }); - } - - private async updateUI() { - const { - loginButton, - logoutButton, - userInfo, - userName, - userEmail, - memberStatus, - lastLogin, - loadingSkeleton, - officerViewToggle, - sponsorViewToggle, - } = this.elements; - - // Get all login and logout buttons using classes - const allLoginButtons = document.querySelectorAll('.login-button'); - const allLogoutButtons = document.querySelectorAll('.logout-button'); - - // Hide all buttons initially - allLoginButtons.forEach(btn => btn.classList.add("hidden")); - allLogoutButtons.forEach(btn => btn.classList.add("hidden")); - - if (this.pb.authStore.isValid && this.pb.authStore.model) { - // Show logout buttons for authenticated users - allLogoutButtons.forEach(btn => btn.classList.remove("hidden")); - - const user = this.pb.authStore.model; - const isSponsor = user.member_type === this.config.roles.sponsor.name; - const isOfficer = [ - this.config.roles.officer.name, - this.config.roles.administrator.name - ].includes(user.member_type || ""); - - userName.textContent = user.name || this.config.ui.messages.auth.notProvided; - userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable; - - // Update member status badge - if (user.member_type) { - memberStatus.textContent = user.member_type; - memberStatus.classList.remove("badge-neutral"); - - if (isOfficer) { - memberStatus.classList.add("badge-primary"); - } else if (isSponsor) { - memberStatus.classList.add("badge-warning"); - } else { - memberStatus.classList.add("badge-info"); - } - } else { - memberStatus.textContent = this.config.ui.messages.auth.notVerified; - memberStatus.classList.remove("badge-info", "badge-warning", "badge-primary"); - memberStatus.classList.add("badge-neutral"); - } - - // Update last login - lastLogin.textContent = user.last_login - ? new Date(user.last_login).toLocaleString() - : this.config.ui.messages.auth.never; - - // Show/hide view toggles and update view visibility - officerViewToggle.style.display = isOfficer ? "block" : "none"; - sponsorViewToggle.style.display = isSponsor ? "block" : "none"; - - // If not an officer, ensure default view is shown and officer view is hidden - if (!isOfficer) { - const defaultView = document.getElementById("defaultView"); - const officerView = document.getElementById("officerView"); - const mainTabs = document.querySelector(".tabs.tabs-boxed"); - const officerContent = document.getElementById("officerContent"); - const settingsView = document.getElementById("settingsView"); - - if (defaultView && officerView && mainTabs && officerContent && settingsView) { - // Show default view and its tabs - defaultView.classList.remove("hidden"); - mainTabs.classList.remove("hidden"); - // Hide officer view - officerView.classList.add("hidden"); - officerContent.classList.add("hidden"); - // Also uncheck the toggle if it exists - const officerViewCheckbox = officerViewToggle.querySelector('input[type="checkbox"]') as HTMLInputElement; - if (officerViewCheckbox) { - officerViewCheckbox.checked = false; - } - } - } - - // After everything is updated, show the content - loadingSkeleton.style.display = "none"; - userInfo.classList.remove("hidden"); - setTimeout(() => { - userInfo.style.opacity = "1"; - }, 50); - } else { - // Show login buttons for unauthenticated users - allLoginButtons.forEach(btn => btn.classList.remove("hidden")); - - // Reset all fields to default state - userName.textContent = this.config.ui.messages.auth.notSignedIn; - userEmail.textContent = this.config.ui.messages.auth.notSignedIn; - memberStatus.textContent = this.config.ui.messages.auth.notVerified; - memberStatus.classList.remove("badge-info", "badge-warning", "badge-primary"); - memberStatus.classList.add("badge-neutral"); - lastLogin.textContent = this.config.ui.messages.auth.never; - - // Hide view toggles - officerViewToggle.style.display = "none"; - sponsorViewToggle.style.display = "none"; - - // Show content - loadingSkeleton.style.display = "none"; - userInfo.classList.remove("hidden"); - setTimeout(() => { - userInfo.style.opacity = "1"; - }, 50); - } - } - - private handleLogout() { - this.pb.authStore.clear(); - this.cachedUsers = []; - this.updateUI(); - } - - private async handleProfileSave() { - const { - profileEditor, - editorName, - editorEmail, - editorPoints, - saveProfileButton, - } = this.elements; - const userId = saveProfileButton.dataset.userId; - - if (!userId) { - console.error("No user ID found for saving"); - return; - } - - try { - const formData = new FormData(); - formData.append("name", editorName.value); - formData.append("email", editorEmail.value); - formData.append("points", editorPoints.value); - - await this.pb.collection("users").update(userId, formData); - profileEditor.close(); - this.updateUI(); - } catch (err) { - console.error("Failed to save user profile:", err); - } - } -} \ No newline at end of file diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro index 017ba84..1753153 100644 --- a/src/components/auth/UserProfile.astro +++ b/src/components/auth/UserProfile.astro @@ -310,188 +310,260 @@ diff --git a/src/components/pocketbase/Authentication.ts b/src/components/pocketbase/Authentication.ts new file mode 100644 index 0000000..27c57ed --- /dev/null +++ b/src/components/pocketbase/Authentication.ts @@ -0,0 +1,119 @@ +import PocketBase from "pocketbase"; +import yaml from "js-yaml"; +import configYaml from "../../config/pocketbaseConfig.yml?raw"; + +// Configuration type definitions +interface Config { + api: { + baseUrl: string; + oauth2: { + redirectPath: string; + providerName: string; + }; + }; +} + +// Parse YAML configuration +const config = yaml.load(configYaml) as Config; + +export class Authentication { + private pb: PocketBase; + private static instance: Authentication; + private authChangeCallbacks: ((isValid: boolean) => void)[] = []; + + private constructor() { + // Use the baseUrl from the config file + this.pb = new PocketBase(config.api.baseUrl); + + // Listen for auth state changes + this.pb.authStore.onChange(() => { + this.notifyAuthChange(); + }); + } + + /** + * Get the singleton instance of Authentication + */ + public static getInstance(): Authentication { + if (!Authentication.instance) { + Authentication.instance = new Authentication(); + } + return Authentication.instance; + } + + /** + * Get the PocketBase instance + */ + public getPocketBase(): PocketBase { + return this.pb; + } + + /** + * Handle user login through OAuth2 + */ + public async login(): Promise { + try { + const authMethods = await this.pb.collection("users").listAuthMethods(); + const oidcProvider = authMethods.oauth2?.providers?.find( + (p: { name: string }) => p.name === config.api.oauth2.providerName + ); + + if (!oidcProvider) { + throw new Error("OIDC provider not found"); + } + + localStorage.setItem("provider", JSON.stringify(oidcProvider)); + const redirectUrl = window.location.origin + config.api.oauth2.redirectPath; + const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl); + window.location.href = authUrl; + } catch (err) { + console.error("Authentication error:", err); + throw err; + } + } + + /** + * Handle user logout + */ + public logout(): void { + this.pb.authStore.clear(); + } + + /** + * Check if user is currently authenticated + */ + public isAuthenticated(): boolean { + return this.pb.authStore.isValid; + } + + /** + * Get current user model + */ + public getCurrentUser(): any { + return this.pb.authStore.model; + } + + /** + * Subscribe to auth state changes + * @param callback Function to call when auth state changes + */ + public onAuthStateChange(callback: (isValid: boolean) => void): void { + this.authChangeCallbacks.push(callback); + } + + /** + * Remove auth state change subscription + * @param callback Function to remove from subscribers + */ + public offAuthStateChange(callback: (isValid: boolean) => void): void { + this.authChangeCallbacks = this.authChangeCallbacks.filter(cb => cb !== callback); + } + + /** + * Notify all subscribers of auth state change + */ + private notifyAuthChange(): void { + const isValid = this.pb.authStore.isValid; + this.authChangeCallbacks.forEach(callback => callback(isValid)); + } +} \ No newline at end of file diff --git a/src/components/pocketbase/Get.ts b/src/components/pocketbase/Get.ts new file mode 100644 index 0000000..1f6832e --- /dev/null +++ b/src/components/pocketbase/Get.ts @@ -0,0 +1,204 @@ +import { Authentication } from "./Authentication"; + +// Base interface for PocketBase records +interface BaseRecord { + id: string; + [key: string]: any; +} + +export class Get { + private auth: Authentication; + private static instance: Get; + + private constructor() { + this.auth = Authentication.getInstance(); + } + + /** + * Get the singleton instance of Get + */ + public static getInstance(): Get { + if (!Get.instance) { + Get.instance = new Get(); + } + return Get.instance; + } + + /** + * Get a single record by ID + * @param collectionName The name of the collection + * @param recordId The ID of the record to retrieve + * @param fields Optional array of fields to select + * @returns The requested record + */ + public async getOne( + collectionName: string, + recordId: string, + fields?: string[] + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve records"); + } + + try { + const pb = this.auth.getPocketBase(); + const options = fields ? { fields: fields.join(",") } : undefined; + return await pb.collection(collectionName).getOne(recordId, options); + } catch (err) { + console.error(`Failed to get record from ${collectionName}:`, err); + throw err; + } + } + + /** + * Get multiple records by their IDs + * @param collectionName The name of the collection + * @param recordIds Array of record IDs to retrieve + * @param fields Optional array of fields to select + * @returns Array of requested records + */ + public async getMany( + collectionName: string, + recordIds: string[], + fields?: string[] + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve records"); + } + + try { + const pb = this.auth.getPocketBase(); + const filter = `id ?~ "${recordIds.join("|")}"`; + const options = { + filter, + ...(fields && { fields: fields.join(",") }) + }; + + const result = await pb.collection(collectionName).getFullList(options); + + // Sort results to match the order of requested IDs + const recordMap = new Map(result.map(record => [record.id, record])); + return recordIds.map(id => recordMap.get(id)).filter(Boolean) as T[]; + } catch (err) { + console.error(`Failed to get records from ${collectionName}:`, err); + throw err; + } + } + + /** + * Get records with pagination + * @param collectionName The name of the collection + * @param page Page number (1-based) + * @param perPage Number of items per page + * @param filter Optional filter string + * @param sort Optional sort string + * @param fields Optional array of fields to select + * @returns Paginated list of records + */ + public async getList( + collectionName: string, + page: number = 1, + perPage: number = 20, + filter?: string, + sort?: string, + fields?: string[] + ): Promise<{ + page: number; + perPage: number; + totalItems: number; + totalPages: number; + items: T[]; + }> { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve records"); + } + + try { + const pb = this.auth.getPocketBase(); + const options = { + ...(filter && { filter }), + ...(sort && { sort }), + ...(fields && { fields: fields.join(",") }) + }; + + const result = await pb.collection(collectionName).getList(page, perPage, options); + + return { + page: result.page, + perPage: result.perPage, + totalItems: result.totalItems, + totalPages: result.totalPages, + items: result.items + }; + } catch (err) { + console.error(`Failed to get list from ${collectionName}:`, err); + throw err; + } + } + + /** + * Get all records from a collection + * @param collectionName The name of the collection + * @param filter Optional filter string + * @param sort Optional sort string + * @param fields Optional array of fields to select + * @returns Array of all matching records + */ + public async getAll( + collectionName: string, + filter?: string, + sort?: string, + fields?: string[] + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve records"); + } + + try { + const pb = this.auth.getPocketBase(); + const options = { + ...(filter && { filter }), + ...(sort && { sort }), + ...(fields && { fields: fields.join(",") }) + }; + + return await pb.collection(collectionName).getFullList(options); + } catch (err) { + console.error(`Failed to get all records from ${collectionName}:`, err); + throw err; + } + } + + /** + * Get the first record that matches a filter + * @param collectionName The name of the collection + * @param filter Filter string + * @param fields Optional array of fields to select + * @returns The first matching record or null if none found + */ + public async getFirst( + collectionName: string, + filter: string, + fields?: string[] + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve records"); + } + + try { + const pb = this.auth.getPocketBase(); + const options = { + filter, + ...(fields && { fields: fields.join(",") }), + sort: "created", + perPage: 1 + }; + + const result = await pb.collection(collectionName).getList(1, 1, options); + return result.items.length > 0 ? result.items[0] : null; + } catch (err) { + console.error(`Failed to get first record from ${collectionName}:`, err); + throw err; + } + } +} \ No newline at end of file diff --git a/src/components/pocketbase/SendLog.ts b/src/components/pocketbase/SendLog.ts new file mode 100644 index 0000000..2cd0727 --- /dev/null +++ b/src/components/pocketbase/SendLog.ts @@ -0,0 +1,168 @@ +import { Authentication } from "./Authentication"; + +// Log interface +interface LogData { + user_id: string; + type: string; // Standard types: "error", "update", "delete", "create", "login", "logout" + part: string; // The specific part/section being logged (can be multiple words, e.g., "profile settings", "resume upload") + message: string; +} + +export class SendLog { + private auth: Authentication; + private static instance: SendLog; + private readonly COLLECTION_NAME = "logs"; // Make collection name a constant + + private constructor() { + this.auth = Authentication.getInstance(); + } + + /** + * Get the singleton instance of SendLog + */ + public static getInstance(): SendLog { + if (!SendLog.instance) { + SendLog.instance = new SendLog(); + } + return SendLog.instance; + } + + /** + * Gets the current authenticated user's ID + * @returns The user ID or null if not authenticated + */ + private getCurrentUserId(): string | null { + const user = this.auth.getCurrentUser(); + if (!user) { + console.debug("SendLog: No current user found"); + return null; + } + console.debug("SendLog: Current user ID:", user.id); + return user.id; + } + + /** + * Sends a log entry to PocketBase + * @param type The type of log entry + * @param part The specific part/section being logged + * @param message The log message + * @param overrideUserId Optional user ID to override the current user + * @returns Promise that resolves when the log is created + */ + public async send(type: string, part: string, message: string, overrideUserId?: string): Promise { + try { + // Check authentication first + if (!this.auth.isAuthenticated()) { + console.error("SendLog: User not authenticated"); + throw new Error("User must be authenticated to create logs"); + } + + // Get user ID + const userId = overrideUserId || this.getCurrentUserId(); + if (!userId) { + console.error("SendLog: No user ID available"); + throw new Error("No user ID available. User must be authenticated to create logs."); + } + + // Prepare log data + const logData: LogData = { + user_id: userId, + type, + part, + message + }; + + console.debug("SendLog: Preparing to send log:", { + collection: this.COLLECTION_NAME, + data: logData, + authValid: this.auth.isAuthenticated(), + userId + }); + + // Get PocketBase instance + const pb = this.auth.getPocketBase(); + + // Create the log entry + await pb.collection(this.COLLECTION_NAME).create(logData); + + console.debug("SendLog: Log created successfully"); + } catch (error) { + // Enhanced error logging + if (error instanceof Error) { + console.error("SendLog: Failed to send log:", { + error: error.message, + stack: error.stack, + type, + part, + message + }); + } else { + console.error("SendLog: Unknown error:", error); + } + throw error; + } + } + + /** + * Get logs for a specific user + * @param userId The ID of the user to get logs for + * @param type Optional log type to filter by + * @param part Optional part/section to filter by + * @returns Array of log entries + */ + public async getUserLogs(userId: string, type?: string, part?: string): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve logs"); + } + + try { + let filter = `user_id = "${userId}"`; + if (type) filter += ` && type = "${type}"`; + if (part) filter += ` && part = "${part}"`; + + const result = await this.auth.getPocketBase().collection(this.COLLECTION_NAME).getFullList({ + filter, + sort: "-created" + }); + + return result; + } catch (error) { + console.error("SendLog: Failed to get user logs:", error); + throw error; + } + } + + /** + * Get recent logs for the current user + * @param limit Maximum number of logs to retrieve + * @param type Optional log type to filter by + * @param part Optional part/section to filter by + * @returns Array of recent log entries + */ + public async getRecentLogs(limit: number = 10, type?: string, part?: string): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to retrieve logs"); + } + + try { + const userId = this.getCurrentUserId(); + if (!userId) { + throw new Error("No user ID available"); + } + + let filter = `user_id = "${userId}"`; + if (type) filter += ` && type = "${type}"`; + if (part) filter += ` && part = "${part}"`; + + const result = await this.auth.getPocketBase().collection(this.COLLECTION_NAME).getList(1, limit, { + filter, + sort: "-created" + }); + + return result.items; + } catch (error) { + console.error("SendLog: Failed to get recent logs:", error); + throw error; + } + } +} \ No newline at end of file diff --git a/src/components/pocketbase/Update.ts b/src/components/pocketbase/Update.ts new file mode 100644 index 0000000..6d5d5cf --- /dev/null +++ b/src/components/pocketbase/Update.ts @@ -0,0 +1,134 @@ +import { Authentication } from "./Authentication"; + +export class Update { + private auth: Authentication; + private static instance: Update; + + private constructor() { + this.auth = Authentication.getInstance(); + } + + /** + * Get the singleton instance of Update + */ + public static getInstance(): Update { + if (!Update.instance) { + Update.instance = new Update(); + } + return Update.instance; + } + + /** + * Update a single field in a record + * @param collectionName The name of the collection + * @param recordId The ID of the record to update + * @param field The field to update + * @param value The new value for the field + * @returns The updated record + */ + public async updateField( + collectionName: string, + recordId: string, + field: string, + value: any + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to update records"); + } + + try { + const pb = this.auth.getPocketBase(); + const data = { [field]: value }; + return await pb.collection(collectionName).update(recordId, data); + } catch (err) { + console.error(`Failed to update ${field} in ${collectionName}:`, err); + throw err; + } + } + + /** + * Update multiple fields in a record + * @param collectionName The name of the collection + * @param recordId The ID of the record to update + * @param updates Object containing field-value pairs to update + * @returns The updated record + */ + public async updateFields( + collectionName: string, + recordId: string, + updates: Record + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to update records"); + } + + try { + const pb = this.auth.getPocketBase(); + return await pb.collection(collectionName).update(recordId, updates); + } catch (err) { + console.error(`Failed to update fields in ${collectionName}:`, err); + throw err; + } + } + + /** + * Update a field for multiple records + * @param collectionName The name of the collection + * @param recordIds Array of record IDs to update + * @param field The field to update + * @param value The new value for the field + * @returns Array of updated records + */ + public async batchUpdateField( + collectionName: string, + recordIds: string[], + field: string, + value: any + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to update records"); + } + + try { + const pb = this.auth.getPocketBase(); + const data = { [field]: value }; + + const updates = recordIds.map(id => + pb.collection(collectionName).update(id, data) + ); + + return await Promise.all(updates); + } catch (err) { + console.error(`Failed to batch update ${field} in ${collectionName}:`, err); + throw err; + } + } + + /** + * Update multiple fields for multiple records + * @param collectionName The name of the collection + * @param updates Array of objects containing record ID and updates + * @returns Array of updated records + */ + public async batchUpdateFields( + collectionName: string, + updates: Array<{ id: string; data: Record }> + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to update records"); + } + + try { + const pb = this.auth.getPocketBase(); + + const updatePromises = updates.map(({ id, data }) => + pb.collection(collectionName).update(id, data) + ); + + return await Promise.all(updatePromises); + } catch (err) { + console.error(`Failed to batch update fields in ${collectionName}:`, err); + throw err; + } + } +} \ No newline at end of file diff --git a/src/components/profile/DefaultProfileView.astro b/src/components/profile/DefaultProfileView.astro index 031f70d..64efa83 100644 --- a/src/components/profile/DefaultProfileView.astro +++ b/src/components/profile/DefaultProfileView.astro @@ -181,25 +181,40 @@ diff --git a/src/components/profile/MemberManagement.astro b/src/components/profile/MemberManagement.astro index a723f58..52e505c 100644 --- a/src/components/profile/MemberManagement.astro +++ b/src/components/profile/MemberManagement.astro @@ -1,10 +1,20 @@ --- // Import the majors list import allMajors from "../../data/allUCSDMajors.txt?raw"; +import yaml from "js-yaml"; +import textConfig from "../../config/text.yml?raw"; +import profileConfig from "../../config/profileConfig.yaml?raw"; +import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw"; + const majorsList: string[] = allMajors .split("\n") .filter((major: string) => major.trim()) .sort((a, b) => a.localeCompare(b)); // Sort alphabetically + +// Parse configurations +const text = yaml.load(textConfig) as any; +const profile = yaml.load(profileConfig) as any; +const pbConfig = yaml.load(pocketbaseConfig) as any; ---
- + -
Loading users...
+ Loading users... `; @@ -429,7 +438,7 @@ const majorsList: string[] = allMajors if (users.items.length === 0) { resumeList.innerHTML = ` - + No users found @@ -442,7 +451,7 @@ const majorsList: string[] = allMajors console.error("Failed to load users:", error); resumeList.innerHTML = ` - + Failed to load users. Please try again. diff --git a/src/components/profile/UserSettings.astro b/src/components/profile/UserSettings.astro index 88f6a7f..08c250f 100644 --- a/src/components/profile/UserSettings.astro +++ b/src/components/profile/UserSettings.astro @@ -271,11 +271,13 @@ const majorsList: string[] = allMajors diff --git a/src/config/pocketbaseConfig.yml b/src/config/pocketbaseConfig.yml new file mode 100644 index 0000000..145c1ef --- /dev/null +++ b/src/config/pocketbaseConfig.yml @@ -0,0 +1,5 @@ +api: + baseUrl: https://pocketbase.ieeeucsd.org + oauth2: + redirectPath: /oauth2-redirect + providerName: oidc diff --git a/src/data/storeConfig.yaml b/src/config/profileConfig.yaml similarity index 96% rename from src/data/storeConfig.yaml rename to src/config/profileConfig.yaml index a431843..bfcc649 100644 --- a/src/data/storeConfig.yaml +++ b/src/config/profileConfig.yaml @@ -1,9 +1,3 @@ -api: - baseUrl: https://pocketbase.ieeeucsd.org - oauth2: - redirectPath: /oauth2-redirect - providerName: oidc - roles: administrator: name: IEEE Administrator diff --git a/src/config/text.yml b/src/config/text.yml new file mode 100644 index 0000000..d118d92 --- /dev/null +++ b/src/config/text.yml @@ -0,0 +1,93 @@ +ui: + transitions: + fadeDelay: 50 + + messages: + memberId: + saving: Saving member ID... + success: IEEE Member ID saved successfully! + error: Failed to save IEEE Member ID. Please try again. + messageTimeout: 3000 + + resume: + uploading: Uploading resume... + success: Resume uploaded successfully! + error: Failed to upload resume. Please try again. + deleting: Deleting resume... + deleteSuccess: Resume deleted successfully! + deleteError: Failed to delete resume. Please try again. + messageTimeout: 3000 + + event: + saving: Saving event... + success: Event saved successfully! + error: Failed to save event. Please try again. + deleting: Deleting event... + deleteSuccess: Event deleted successfully! + deleteError: Failed to delete event. Please try again. + messageTimeout: 3000 + checkIn: + checking: Checking event code... + success: Successfully checked in to event! + error: Failed to check in. Please try again. + invalid: Invalid event code. Please try again. + expired: This event is not currently active. + alreadyCheckedIn: You have already checked in to this event. + messageTimeout: 3000 + + auth: + loginError: Failed to start authentication + notSignedIn: Not signed in + notVerified: Not verified + notProvided: Not provided + notAvailable: Not available + never: Never + + tables: + events: + title: Event Management + editor_title: Event Details + columns: + event_name: Event Name + event_id: Event ID + event_code: Event Code + start_date: Start Date + end_date: End Date + points_to_reward: Points to Reward + location: Location + registered_users: Registered + actions: Actions + form: + event_id: + label: Event ID + placeholder: Enter unique event ID + event_name: + label: Event Name + placeholder: Enter event name + event_code: + label: Event Code + placeholder: Enter check-in code + start_date: + date_label: Start Date for check-in + time_label: Start Time for check-in + date_placeholder: Select start date for check-in + time_placeholder: Select start time for check-in + end_date: + date_label: End Date for check-in + time_label: End Time for check-in + date_placeholder: Select end date for check-in + time_placeholder: Select end time for check-in + points_to_reward: + label: Points to Reward + placeholder: Enter points value + location: + label: Location + placeholder: Enter event location + files: + label: Event Files + help_text: Upload event-related files (PDF, DOC, DOCX, TXT, JPG, JPEG, PNG) + buttons: + save: Save + cancel: Cancel + edit: Edit + delete: Delete diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro index de3db0e..80cdf2e 100644 --- a/src/pages/online-store.astro +++ b/src/pages/online-store.astro @@ -25,6 +25,69 @@ const title = "IEEE Online Store"; diff --git a/src/pages/profile.astro b/src/pages/profile.astro index 547a003..2d6672c 100644 --- a/src/pages/profile.astro +++ b/src/pages/profile.astro @@ -4,7 +4,13 @@ import UserProfile from "../components/auth/UserProfile.astro"; import DefaultProfileView from "../components/profile/DefaultProfileView.astro"; import OfficerProfileView from "../components/profile/OfficerView.astro"; import UserSettings from "../components/profile/UserSettings.astro"; +import yaml from "js-yaml"; +import profileConfig from "../config/profileConfig.yaml?raw"; +import textConfig from "../config/text.yml?raw"; + const title = "User Profile"; +const config = yaml.load(profileConfig) as any; +const text = yaml.load(textConfig) as any; --- @@ -43,10 +49,7 @@ const title = "User Profile"; d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"> - Failed to load profile data. Please try refreshing - the page. + {text.ui.messages.auth.loginError}
@@ -198,7 +201,7 @@ const title = "User Profile"; -
+
@@ -214,8 +217,14 @@ const title = "User Profile";