From 6b9d7935907a3fa1c205924e519655286d444f2f Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 3 Feb 2025 03:07:46 -0800 Subject: [PATCH] fix default profile view --- src/components/pocketbase/FileManager.ts | 156 ++++++++ .../profile/DefaultProfileView.astro | 350 +++++++++++------- src/components/profile/EventEditor.astro | 159 ++++++++ src/components/profile/UserSettings.astro | 12 +- src/pages/profile.astro | 4 +- 5 files changed, 533 insertions(+), 148 deletions(-) create mode 100644 src/components/pocketbase/FileManager.ts diff --git a/src/components/pocketbase/FileManager.ts b/src/components/pocketbase/FileManager.ts new file mode 100644 index 0000000..4871bb3 --- /dev/null +++ b/src/components/pocketbase/FileManager.ts @@ -0,0 +1,156 @@ +import { Authentication } from "./Authentication"; + +export class FileManager { + private auth: Authentication; + private static instance: FileManager; + + private constructor() { + this.auth = Authentication.getInstance(); + } + + /** + * Get the singleton instance of FileManager + */ + public static getInstance(): FileManager { + if (!FileManager.instance) { + FileManager.instance = new FileManager(); + } + return FileManager.instance; + } + + /** + * Upload a single file to a record + * @param collectionName The name of the collection + * @param recordId The ID of the record to attach the file to + * @param field The field name for the file + * @param file The file to upload + * @returns The updated record + */ + public async uploadFile( + collectionName: string, + recordId: string, + field: string, + file: File + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to upload files"); + } + + try { + const pb = this.auth.getPocketBase(); + const formData = new FormData(); + formData.append(field, file); + + return await pb.collection(collectionName).update(recordId, formData); + } catch (err) { + console.error(`Failed to upload file to ${collectionName}:`, err); + throw err; + } + } + + /** + * Upload multiple files to a record + * @param collectionName The name of the collection + * @param recordId The ID of the record to attach the files to + * @param field The field name for the files + * @param files Array of files to upload + * @returns The updated record + */ + public async uploadFiles( + collectionName: string, + recordId: string, + field: string, + files: File[] + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to upload files"); + } + + try { + const pb = this.auth.getPocketBase(); + const formData = new FormData(); + + files.forEach(file => { + formData.append(field, file); + }); + + return await pb.collection(collectionName).update(recordId, formData); + } catch (err) { + console.error(`Failed to upload files to ${collectionName}:`, err); + throw err; + } + } + + /** + * Get the URL for a file + * @param collectionName The name of the collection + * @param recordId The ID of the record containing the file + * @param filename The name of the file + * @returns The URL to access the file + */ + public getFileUrl( + collectionName: string, + recordId: string, + filename: string + ): string { + const pb = this.auth.getPocketBase(); + return `${pb.baseUrl}/api/files/${collectionName}/${recordId}/${filename}`; + } + + /** + * Delete a file from a record + * @param collectionName The name of the collection + * @param recordId The ID of the record containing the file + * @param field The field name of the file to delete + * @returns The updated record + */ + public async deleteFile( + collectionName: string, + recordId: string, + field: string + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to delete files"); + } + + try { + const pb = this.auth.getPocketBase(); + const data = { [field]: null }; + return await pb.collection(collectionName).update(recordId, data); + } catch (err) { + console.error(`Failed to delete file from ${collectionName}:`, err); + throw err; + } + } + + /** + * Download a file + * @param collectionName The name of the collection + * @param recordId The ID of the record containing the file + * @param filename The name of the file + * @returns The file blob + */ + public async downloadFile( + collectionName: string, + recordId: string, + filename: string + ): Promise { + if (!this.auth.isAuthenticated()) { + throw new Error("User must be authenticated to download files"); + } + + try { + const url = this.getFileUrl(collectionName, recordId, filename); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.blob(); + } catch (err) { + console.error(`Failed to download file from ${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 64efa83..58ae144 100644 --- a/src/components/profile/DefaultProfileView.astro +++ b/src/components/profile/DefaultProfileView.astro @@ -184,64 +184,68 @@ import { Authentication } from "../pocketbase/Authentication"; import { Get } from "../pocketbase/Get"; import { SendLog } from "../pocketbase/SendLog"; + import { FileManager } from "../pocketbase/FileManager"; const auth = Authentication.getInstance(); const get = Get.getInstance(); const logger = SendLog.getInstance(); + const fileManager = FileManager.getInstance(); - // Initialize event check-in - document.addEventListener("DOMContentLoaded", async () => { + // Track if we're currently fetching events + let isFetchingEvents = false; + let lastFetchPromise: Promise | null = null; + + // Define interfaces + interface BaseRecord { + id: string; + [key: string]: any; + } + + interface Event extends BaseRecord { + id: string; + event_id: string; + event_name: string; + start_date: string; + end_date: string; + location: string; + files?: string[]; + } + + // Add debounce function + function debounce any>( + func: T, + wait: number + ): (...args: Parameters) => void { + let timeout: ReturnType | null = null; + return (...args: Parameters) => { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; + } + + // Create debounced version of renderEvents + const debouncedRenderEvents = debounce(async () => { try { - // Get current user's events - if (auth.isAuthenticated()) { - const user = auth.getCurrentUser(); - if (user) { - // Get user's events - const events = await get.getMany( - "events", - user.events_attended || [] - ); - - // Update UI with events data - // ... rest of the code ... - } - } + await renderEvents(); } catch (error) { - console.error("Failed to initialize profile:", error); - await logger.send( - "error", - "profile view", - `Failed to initialize profile: ${error instanceof Error ? error.message : "Unknown error"}` - ); + console.error("Failed to render events:", error); } - }); - - // Handle loading states - const eventCheckInSkeleton = document.getElementById( - "eventCheckInSkeleton" - ); - const eventCheckInContent = document.getElementById("eventCheckInContent"); - const pastEventsCount = document.getElementById("pastEventsCount"); + }, 300); // Function to show content and hide skeleton function showEventCheckIn() { + const eventCheckInSkeleton = document.getElementById( + "eventCheckInSkeleton" + ); + const eventCheckInContent = document.getElementById( + "eventCheckInContent" + ); if (eventCheckInSkeleton && eventCheckInContent) { eventCheckInSkeleton.classList.add("hidden"); eventCheckInContent.classList.remove("hidden"); } } - // Show content when auth state changes - auth.onAuthStateChange(() => { - showEventCheckIn(); - renderEvents(); - }); - - // Show content on initial load if already authenticated - if (auth.isAuthenticated()) { - showEventCheckIn(); - } - // Function to format date function formatDate(dateStr: string): string { const date = new Date(dateStr); @@ -321,14 +325,6 @@ } } - interface Event extends RecordModel { - event_id: string; - event_name: string; - start_date: string; - end_date: string; - location: string; - } - // Function to render event card function renderEventCard(event: Event, attendedEvents: string[]): string { const isAttended = @@ -350,7 +346,7 @@ - ${event.files.length} File${event.files.length > 1 ? "s" : ""} + ${event.files?.length || 0} File${(event.files?.length || 0) > 1 ? "s" : ""} ` : ""; @@ -389,115 +385,181 @@ `; } - // Function to render events + // Function to render events with request cancellation handling async function renderEvents() { const eventsList = document.getElementById("eventsList"); const pastEventsList = document.getElementById("pastEventsList"); if (!eventsList || !pastEventsList) return; - try { - // Get current user's attended events with safe parsing - const user = auth.getCurrentUser(); - let attendedEvents: string[] = []; - - if (user?.events_attended) { - try { - attendedEvents = - typeof user.events_attended === "string" - ? JSON.parse(user.events_attended) - : Array.isArray(user.events_attended) - ? user.events_attended - : []; - } catch (e) { - console.warn("Failed to parse events_attended:", e); - attendedEvents = []; - } + // If we're already fetching, wait for the current fetch to complete + if (isFetchingEvents && lastFetchPromise) { + try { + await lastFetchPromise; + return; + } catch (err) { + console.warn("Previous fetch failed:", err); } + } - // Fetch all events - const events = await get.getMany( - "events", - user.events_attended || [] - ); + // Set up new fetch + isFetchingEvents = true; + const fetchPromise = (async () => { + try { + // Get current user's attended events with safe parsing + const user = auth.getCurrentUser(); + let attendedEvents: string[] = []; - // Clear loading skeletons - eventsList.innerHTML = ""; - pastEventsList.innerHTML = ""; - - // Categorize events - const now = new Date(); - const currentEvents: Event[] = []; - const upcomingEvents: Event[] = []; - const pastEvents: Event[] = []; - - events.forEach((event) => { - const typedEvent = event as Event; - const startDate = new Date(typedEvent.start_date); - const endDate = new Date(typedEvent.end_date); - - if (startDate > now) { - upcomingEvents.push(typedEvent); - } else if (endDate >= now && startDate <= now) { - currentEvents.push(typedEvent); - } else { - pastEvents.push(typedEvent); + if (user?.events_attended) { + try { + attendedEvents = + typeof user.events_attended === "string" + ? JSON.parse(user.events_attended) + : Array.isArray(user.events_attended) + ? user.events_attended + : []; + console.log("Attended events:", attendedEvents); + } catch (e) { + console.warn("Failed to parse events_attended:", e); + attendedEvents = []; + } } - }); - // Sort upcoming events by start date - const sortedUpcomingEvents = upcomingEvents.sort( - (a, b) => - new Date(a.start_date).getTime() - - new Date(b.start_date).getTime() - ); + // Fetch all events + console.log("Fetching all events"); + const events = await get.getAll( + "events", + undefined, + "-start_date" + ); + console.log("Fetched events:", events); - // Sort past events by date descending (most recent first) - const sortedPastEvents = pastEvents.sort( - (a, b) => - new Date(b.end_date).getTime() - - new Date(a.end_date).getTime() - ); + if (!Array.isArray(events)) { + throw new Error( + "Failed to fetch events: Invalid response format" + ); + } - // Update past events count - if (pastEventsCount) { - pastEventsCount.textContent = - sortedPastEvents.length.toString(); - } + // Clear loading skeletons + eventsList.innerHTML = ""; + pastEventsList.innerHTML = ""; - // Function to render section - function renderSection(events: Event[]): string { - if (events.length === 0) { - return ` -
-

No events found

+ // Categorize events + const now = new Date(); + const currentEvents: Event[] = []; + const upcomingEvents: Event[] = []; + const pastEvents: Event[] = []; + + events.forEach((event) => { + if (!event.start_date || !event.end_date) { + console.warn("Event missing dates:", event); + return; + } + + const startDate = new Date(event.start_date); + const endDate = new Date(event.end_date); + + if (startDate > now) { + upcomingEvents.push(event); + } else if (endDate >= now && startDate <= now) { + currentEvents.push(event); + } else { + pastEvents.push(event); + } + }); + + // Sort upcoming events by start date + const sortedUpcomingEvents = upcomingEvents.sort( + (a, b) => + new Date(a.start_date).getTime() - + new Date(b.start_date).getTime() + ); + + // Sort past events by date descending (most recent first) + const sortedPastEvents = pastEvents.sort( + (a, b) => + new Date(b.end_date).getTime() - + new Date(a.end_date).getTime() + ); + + // Update past events count + const pastEventsCountElement = + document.getElementById("pastEventsCount"); + if (pastEventsCountElement) { + pastEventsCountElement.textContent = + sortedPastEvents.length.toString(); + } + + // Function to render section + function renderSection(events: Event[]): string { + if (events.length === 0) { + return ` +
+

No events found

+
+ `; + } + return events + .map((event) => renderEventCard(event, attendedEvents)) + .join(""); + } + + // Update main events list (current & upcoming) + eventsList.innerHTML = renderSection([ + ...currentEvents, + ...sortedUpcomingEvents, + ]); + + // Update past events list + pastEventsList.innerHTML = renderSection(sortedPastEvents); + } catch (err) { + console.error("Failed to render events:", err); + await logger.send( + "error", + "render events", + `Failed to render events: ${err instanceof Error ? err.message : "Unknown error"}` + ); + const errorMessage = ` +
+

Failed to load events. Please try again later.

+

${err instanceof Error ? err.message : "Unknown error"}

`; - } - return events - .map((event) => renderEventCard(event, attendedEvents)) - .join(""); + eventsList.innerHTML = errorMessage; + pastEventsList.innerHTML = errorMessage; + throw err; // Re-throw to handle in the outer catch + } finally { + isFetchingEvents = false; } + })(); - // Update main events list (current & upcoming) - eventsList.innerHTML = renderSection([ - ...currentEvents, - ...sortedUpcomingEvents, - ]); + lastFetchPromise = fetchPromise; - // Update past events list - pastEventsList.innerHTML = renderSection(sortedPastEvents); + try { + await fetchPromise; } catch (err) { - console.error("Failed to render events:", err); - const errorMessage = ` -
-

Failed to load events. Please try again later.

-
- `; - eventsList.innerHTML = errorMessage; - pastEventsList.innerHTML = errorMessage; + // Error already handled above + console.debug("Fetch completed with error"); } } + // Initialize event check-in + document.addEventListener("DOMContentLoaded", () => { + showEventCheckIn(); + // Only render events once at startup + renderEvents().catch(console.error); + }); + + // Show content when auth state changes + let lastAuthState = auth.isAuthenticated(); + auth.onAuthStateChange((isValid) => { + showEventCheckIn(); + // Only re-render if auth state actually changed + if (lastAuthState !== isValid) { + lastAuthState = isValid; + renderEvents().catch(console.error); + } + }); + // Add event listener for viewing files interface ViewEventFilesEvent extends CustomEvent { detail: { @@ -509,7 +571,10 @@ if (e instanceof CustomEvent && "eventId" in e.detail) { (async () => { try { - const event = await get.getOne("events", e.detail.eventId); + const event = await get.getOne( + "events", + e.detail.eventId + ); const fileViewerContent = document.getElementById("fileViewerContent"); const fileViewerTitle = @@ -533,7 +598,11 @@ fileViewerContent.innerHTML = event.files .map((file) => { - const fileUrl = get.getFileURL(event, file); + const fileUrl = fileManager.getFileUrl( + "events", + event.id, + file + ); const fileName = file.split("/").pop() || "File"; const fileExt = @@ -599,7 +668,4 @@ })(); } }) as unknown as EventListener); - - // Initial render - renderEvents(); diff --git a/src/components/profile/EventEditor.astro b/src/components/profile/EventEditor.astro index 7d16f0a..8d6471a 100644 --- a/src/components/profile/EventEditor.astro +++ b/src/components/profile/EventEditor.astro @@ -285,3 +285,162 @@ const { editor_title, form } = text.ui.tables.events; + + diff --git a/src/components/profile/UserSettings.astro b/src/components/profile/UserSettings.astro index 08c250f..7f0b904 100644 --- a/src/components/profile/UserSettings.astro +++ b/src/components/profile/UserSettings.astro @@ -274,10 +274,12 @@ const majorsList: string[] = allMajors import { Authentication } from "../pocketbase/Authentication"; import { Update } from "../pocketbase/Update"; import { SendLog } from "../pocketbase/SendLog"; + import { FileManager } from "../pocketbase/FileManager"; const auth = Authentication.getInstance(); const update = Update.getInstance(); const logger = SendLog.getInstance(); + const fileManager = FileManager.getInstance(); // Get form elements const memberIdInput = document.getElementById( @@ -355,10 +357,12 @@ const majorsList: string[] = allMajors const user = auth.getCurrentUser(); if (!user) throw new Error("User not authenticated"); - const formData = new FormData(); - formData.append("resume", file); - - await update.updateFields("users", user.id, formData); + await fileManager.uploadFile( + "users", + user.id, + "resume", + file + ); uploadStatus.textContent = "Resume uploaded successfully"; if (currentResume) currentResume.textContent = file.name; diff --git a/src/pages/profile.astro b/src/pages/profile.astro index 2d6672c..ed4827a 100644 --- a/src/pages/profile.astro +++ b/src/pages/profile.astro @@ -201,10 +201,10 @@ const text = yaml.load(textConfig) as any;
-