fix default profile view

This commit is contained in:
chark1es 2025-02-03 03:07:46 -08:00
parent e57f76bd4c
commit 6b9d793590
5 changed files with 533 additions and 148 deletions

View file

@ -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<T = any>(
collectionName: string,
recordId: string,
field: string,
file: File
): Promise<T> {
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<T>(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<T = any>(
collectionName: string,
recordId: string,
field: string,
files: File[]
): Promise<T> {
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<T>(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<T = any>(
collectionName: string,
recordId: string,
field: string
): Promise<T> {
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<T>(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<Blob> {
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;
}
}
}

View file

@ -184,64 +184,68 @@
import { Authentication } from "../pocketbase/Authentication"; import { Authentication } from "../pocketbase/Authentication";
import { Get } from "../pocketbase/Get"; import { Get } from "../pocketbase/Get";
import { SendLog } from "../pocketbase/SendLog"; import { SendLog } from "../pocketbase/SendLog";
import { FileManager } from "../pocketbase/FileManager";
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const get = Get.getInstance(); const get = Get.getInstance();
const logger = SendLog.getInstance(); const logger = SendLog.getInstance();
const fileManager = FileManager.getInstance();
// Initialize event check-in // Track if we're currently fetching events
document.addEventListener("DOMContentLoaded", async () => { let isFetchingEvents = false;
let lastFetchPromise: Promise<void> | 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<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
// Create debounced version of renderEvents
const debouncedRenderEvents = debounce(async () => {
try { try {
// Get current user's events await renderEvents();
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 ...
}
}
} catch (error) { } catch (error) {
console.error("Failed to initialize profile:", error); console.error("Failed to render events:", error);
await logger.send(
"error",
"profile view",
`Failed to initialize profile: ${error instanceof Error ? error.message : "Unknown error"}`
);
} }
}); }, 300);
// Handle loading states
const eventCheckInSkeleton = document.getElementById(
"eventCheckInSkeleton"
);
const eventCheckInContent = document.getElementById("eventCheckInContent");
const pastEventsCount = document.getElementById("pastEventsCount");
// Function to show content and hide skeleton // Function to show content and hide skeleton
function showEventCheckIn() { function showEventCheckIn() {
const eventCheckInSkeleton = document.getElementById(
"eventCheckInSkeleton"
);
const eventCheckInContent = document.getElementById(
"eventCheckInContent"
);
if (eventCheckInSkeleton && eventCheckInContent) { if (eventCheckInSkeleton && eventCheckInContent) {
eventCheckInSkeleton.classList.add("hidden"); eventCheckInSkeleton.classList.add("hidden");
eventCheckInContent.classList.remove("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 to format date
function formatDate(dateStr: string): string { function formatDate(dateStr: string): string {
const date = new Date(dateStr); 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 to render event card
function renderEventCard(event: Event, attendedEvents: string[]): string { function renderEventCard(event: Event, attendedEvents: string[]): string {
const isAttended = const isAttended =
@ -350,7 +346,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
</svg> </svg>
${event.files.length} File${event.files.length > 1 ? "s" : ""} ${event.files?.length || 0} File${(event.files?.length || 0) > 1 ? "s" : ""}
</button> </button>
` `
: ""; : "";
@ -389,115 +385,181 @@
`; `;
} }
// Function to render events // Function to render events with request cancellation handling
async function renderEvents() { async function renderEvents() {
const eventsList = document.getElementById("eventsList"); const eventsList = document.getElementById("eventsList");
const pastEventsList = document.getElementById("pastEventsList"); const pastEventsList = document.getElementById("pastEventsList");
if (!eventsList || !pastEventsList) return; if (!eventsList || !pastEventsList) return;
try { // If we're already fetching, wait for the current fetch to complete
// Get current user's attended events with safe parsing if (isFetchingEvents && lastFetchPromise) {
const user = auth.getCurrentUser(); try {
let attendedEvents: string[] = []; await lastFetchPromise;
return;
if (user?.events_attended) { } catch (err) {
try { console.warn("Previous fetch failed:", err);
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 = [];
}
} }
}
// Fetch all events // Set up new fetch
const events = await get.getMany( isFetchingEvents = true;
"events", const fetchPromise = (async () => {
user.events_attended || [] try {
); // Get current user's attended events with safe parsing
const user = auth.getCurrentUser();
let attendedEvents: string[] = [];
// Clear loading skeletons if (user?.events_attended) {
eventsList.innerHTML = ""; try {
pastEventsList.innerHTML = ""; attendedEvents =
typeof user.events_attended === "string"
// Categorize events ? JSON.parse(user.events_attended)
const now = new Date(); : Array.isArray(user.events_attended)
const currentEvents: Event[] = []; ? user.events_attended
const upcomingEvents: Event[] = []; : [];
const pastEvents: Event[] = []; console.log("Attended events:", attendedEvents);
} catch (e) {
events.forEach((event) => { console.warn("Failed to parse events_attended:", e);
const typedEvent = event as Event; attendedEvents = [];
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);
} }
});
// Sort upcoming events by start date // Fetch all events
const sortedUpcomingEvents = upcomingEvents.sort( console.log("Fetching all events");
(a, b) => const events = await get.getAll<Event>(
new Date(a.start_date).getTime() - "events",
new Date(b.start_date).getTime() undefined,
); "-start_date"
);
console.log("Fetched events:", events);
// Sort past events by date descending (most recent first) if (!Array.isArray(events)) {
const sortedPastEvents = pastEvents.sort( throw new Error(
(a, b) => "Failed to fetch events: Invalid response format"
new Date(b.end_date).getTime() - );
new Date(a.end_date).getTime() }
);
// Update past events count // Clear loading skeletons
if (pastEventsCount) { eventsList.innerHTML = "";
pastEventsCount.textContent = pastEventsList.innerHTML = "";
sortedPastEvents.length.toString();
}
// Function to render section // Categorize events
function renderSection(events: Event[]): string { const now = new Date();
if (events.length === 0) { const currentEvents: Event[] = [];
return ` const upcomingEvents: Event[] = [];
<div class="text-center py-8 opacity-70"> const pastEvents: Event[] = [];
<p>No events found</p>
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 `
<div class="text-center py-8 opacity-70">
<p>No events found</p>
</div>
`;
}
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 = `
<div class="text-center py-8 text-error">
<p>Failed to load events. Please try again later.</p>
<p class="text-sm mt-2 opacity-70">${err instanceof Error ? err.message : "Unknown error"}</p>
</div> </div>
`; `;
} eventsList.innerHTML = errorMessage;
return events pastEventsList.innerHTML = errorMessage;
.map((event) => renderEventCard(event, attendedEvents)) throw err; // Re-throw to handle in the outer catch
.join(""); } finally {
isFetchingEvents = false;
} }
})();
// Update main events list (current & upcoming) lastFetchPromise = fetchPromise;
eventsList.innerHTML = renderSection([
...currentEvents,
...sortedUpcomingEvents,
]);
// Update past events list try {
pastEventsList.innerHTML = renderSection(sortedPastEvents); await fetchPromise;
} catch (err) { } catch (err) {
console.error("Failed to render events:", err); // Error already handled above
const errorMessage = ` console.debug("Fetch completed with error");
<div class="text-center py-8 text-error">
<p>Failed to load events. Please try again later.</p>
</div>
`;
eventsList.innerHTML = errorMessage;
pastEventsList.innerHTML = errorMessage;
} }
} }
// 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 // Add event listener for viewing files
interface ViewEventFilesEvent extends CustomEvent { interface ViewEventFilesEvent extends CustomEvent {
detail: { detail: {
@ -509,7 +571,10 @@
if (e instanceof CustomEvent && "eventId" in e.detail) { if (e instanceof CustomEvent && "eventId" in e.detail) {
(async () => { (async () => {
try { try {
const event = await get.getOne("events", e.detail.eventId); const event = await get.getOne<Event>(
"events",
e.detail.eventId
);
const fileViewerContent = const fileViewerContent =
document.getElementById("fileViewerContent"); document.getElementById("fileViewerContent");
const fileViewerTitle = const fileViewerTitle =
@ -533,7 +598,11 @@
fileViewerContent.innerHTML = event.files fileViewerContent.innerHTML = event.files
.map((file) => { .map((file) => {
const fileUrl = get.getFileURL(event, file); const fileUrl = fileManager.getFileUrl(
"events",
event.id,
file
);
const fileName = const fileName =
file.split("/").pop() || "File"; file.split("/").pop() || "File";
const fileExt = const fileExt =
@ -599,7 +668,4 @@
})(); })();
} }
}) as unknown as EventListener); }) as unknown as EventListener);
// Initial render
renderEvents();
</script> </script>

View file

@ -285,3 +285,162 @@ const { editor_title, form } = text.ui.tables.events;
<button>close</button> <button>close</button>
</form> </form>
</dialog> </dialog>
<script>
import { Authentication } from "../pocketbase/Authentication";
import { Get } from "../pocketbase/Get";
import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog";
import { FileManager } from "../pocketbase/FileManager";
const auth = Authentication.getInstance();
const get = Get.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
const fileManager = FileManager.getInstance();
// Handle file uploads
if (editorFiles) {
editorFiles.addEventListener("change", async (e) => {
const files = Array.from(
(e.target as HTMLInputElement).files || []
);
if (files.length > 0) {
const uploadProgress =
document.getElementById("uploadProgress");
const uploadProgressBar = document.getElementById(
"uploadProgressBar"
) as HTMLProgressElement;
const uploadProgressText =
document.getElementById("uploadProgressText");
if (uploadProgress) uploadProgress.classList.remove("hidden");
try {
const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated");
// Upload files
await fileManager.uploadFiles(
"events",
eventId,
"files",
files
);
// Update progress
if (uploadProgressBar) uploadProgressBar.value = 100;
if (uploadProgressText)
uploadProgressText.textContent = "100%";
// Log successful upload
await logger.send(
"update",
"event files",
`Successfully uploaded ${files.length} files to event ${eventId}`
);
// Refresh file list
await loadCurrentFiles();
} catch (err) {
console.error("File upload error:", err);
// Log upload error
await logger.send(
"error",
"event files",
`Failed to upload files to event ${eventId}. Error: ${err instanceof Error ? err.message : "Unknown error"}`
);
} finally {
if (uploadProgress) {
setTimeout(() => {
uploadProgress.classList.add("hidden");
if (uploadProgressBar) uploadProgressBar.value = 0;
if (uploadProgressText)
uploadProgressText.textContent = "0%";
}, 2000);
}
}
}
});
}
// Function to load current files
async function loadCurrentFiles() {
const currentFiles = document.getElementById("currentFiles");
if (!currentFiles || !eventId) return;
try {
const event = await get.getOne("events", eventId);
if (!event.files || !Array.isArray(event.files)) return;
currentFiles.innerHTML = event.files
.map((filename) => {
const fileUrl = fileManager.getFileUrl(
"events",
eventId,
filename
);
return `
<div class="flex justify-between items-center p-2 bg-base-200 rounded-lg">
<span class="truncate flex-1">${filename}</span>
<div class="flex gap-2">
<a href="${fileUrl}" target="_blank" class="btn btn-sm btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
<button class="btn btn-sm btn-ghost text-error" onclick="deleteFile('${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
`;
})
.join("");
} catch (err) {
console.error("Failed to load current files:", err);
}
}
// Function to delete a file
async function deleteFile(filename: string) {
if (!eventId) return;
try {
const event = await get.getOne("events", eventId);
if (!event.files || !Array.isArray(event.files)) return;
// Remove the file from the array
const updatedFiles = event.files.filter((f) => f !== filename);
await update.updateField("events", eventId, "files", updatedFiles);
// Log successful deletion
await logger.send(
"delete",
"event files",
`Successfully deleted file ${filename} from event ${eventId}`
);
// Refresh file list
await loadCurrentFiles();
} catch (err) {
console.error("Failed to delete file:", err);
// Log deletion error
await logger.send(
"error",
"event files",
`Failed to delete file ${filename} from event ${eventId}. Error: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
// Make deleteFile available globally
window.deleteFile = deleteFile;
// Load current files on page load
loadCurrentFiles();
</script>

View file

@ -274,10 +274,12 @@ const majorsList: string[] = allMajors
import { Authentication } from "../pocketbase/Authentication"; import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update"; import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog"; import { SendLog } from "../pocketbase/SendLog";
import { FileManager } from "../pocketbase/FileManager";
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const update = Update.getInstance(); const update = Update.getInstance();
const logger = SendLog.getInstance(); const logger = SendLog.getInstance();
const fileManager = FileManager.getInstance();
// Get form elements // Get form elements
const memberIdInput = document.getElementById( const memberIdInput = document.getElementById(
@ -355,10 +357,12 @@ const majorsList: string[] = allMajors
const user = auth.getCurrentUser(); const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated"); if (!user) throw new Error("User not authenticated");
const formData = new FormData(); await fileManager.uploadFile(
formData.append("resume", file); "users",
user.id,
await update.updateFields("users", user.id, formData); "resume",
file
);
uploadStatus.textContent = "Resume uploaded successfully"; uploadStatus.textContent = "Resume uploaded successfully";
if (currentResume) currentResume.textContent = file.name; if (currentResume) currentResume.textContent = file.name;

View file

@ -201,10 +201,10 @@ const text = yaml.load(textConfig) as any;
</div> </div>
<!-- Content Areas --> <!-- Content Areas -->
<!-- <div id="defaultView"> <div id="defaultView">
<DefaultProfileView /> <DefaultProfileView />
</div> </div>
<div id="settingsView" class="hidden"> <!-- <div id="settingsView" class="hidden">
<UserSettings /> <UserSettings />
</div> </div>
<div id="officerView" class="hidden"> <div id="officerView" class="hidden">