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_name || "N/A"}
- |
- ${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 = `
-
-
Attendees for ${event.event_name}
-
-
-
-
-
-
-
- Name |
- Email |
- Member ID |
-
-
-
- ${userDetails.length === 0
- ? 'No attendees yet |
'
- : userDetails.map(user => `
-
- ${user.name} |
- ${user.email} |
- ${user.member_id} |
-
- `).join("")
- }
-
-
-
-
-
-
-
-
- `;
-
- 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 = `
-
-

-
- `;
- } else {
- // For other file types, show an icon based on type
- const iconHtml = fileExt === 'txt' || fileExt === 'md'
- ? ``
- : ``;
-
- previewHtml = `
-
- ${iconHtml}
-
- `;
- }
-
- fileItem.innerHTML = `
- ${previewHtml}
-
-
- ${nameWithoutExt}
- ${fileExt}
-
-
-
- `;
-
- // 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 = `
-
-
-
Files for ${event.event_name}
- ${event.files && Array.isArray(event.files) && event.files.length > 0
- ? `
`
- : ''
- }
-
-
- ${event.files && Array.isArray(event.files) && event.files.length > 0
- ? event.files.map((file: string) => {
- const fileUrl = this.pb.files.getURL(event, file);
- const fileName = this.getFileNameFromUrl(file);
- const fileExt = fileName.split('.').pop()?.toLowerCase() || '';
-
- let previewHtml = '';
- if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) {
- previewHtml = `
-
-

-
- `;
- } else {
- // For other file types, show an icon based on type
- const iconHtml = fileExt === 'txt' || fileExt === 'md'
- ? `
`
- : `
`;
-
- previewHtml = `
-
- ${iconHtml}
-
- `;
- }
-
- return `
-
- ${previewHtml}
-
-
- ${fileName.substring(0, fileName.lastIndexOf("."))}
- ${fileExt}
-
-
-
-
- `;
- }).join("")
- : `
No files available
`
- }
-
-
-
-
-
-
- `;
-
- 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";