Add authentication #17

Manually merged
Webmaster merged 225 commits from auth into main 2025-03-08 10:37:06 +00:00
8 changed files with 0 additions and 3503 deletions
Showing only changes of commit 0540caca8c - Show all commits

View file

@ -1,335 +0,0 @@
import PocketBase from "pocketbase";
import yaml from "js-yaml";
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;
oauth2: {
redirectPath: string;
providerName: string;
};
};
}
// Parse YAML configuration
const config = yaml.load(pocketbaseConfig) as Config;
const text = yaml.load(textConfig) as any;
interface AuthElements {
eventCodeInput: HTMLInputElement;
checkInButton: HTMLButtonElement;
checkInStatus: HTMLParagraphElement;
}
export class EventCheckIn {
private pb: PocketBase;
private elements: AuthElements;
private logger: SendLog;
constructor() {
this.pb = new PocketBase(config.api.baseUrl);
this.elements = this.getElements();
this.logger = SendLog.getInstance();
// Add event listener for the check-in button
this.elements.checkInButton.addEventListener("click", () => this.handleCheckIn());
// Add event listener for the enter key on the input field
this.elements.eventCodeInput.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
this.handleCheckIn();
}
});
}
private getElements(): AuthElements {
// Get both skeleton and content elements
const eventCodeInput = document.getElementById("eventCodeInput") as HTMLInputElement;
const checkInButton = document.getElementById("checkInButton") as HTMLButtonElement;
const checkInStatus = document.getElementById("checkInStatus") as HTMLParagraphElement;
// Get skeleton elements
const skeletonEventCodeInput = document.getElementById("skeletonEventCodeInput") as HTMLInputElement;
const skeletonCheckInButton = document.getElementById("skeletonCheckInButton") as HTMLButtonElement;
const skeletonCheckInStatus = document.getElementById("skeletonCheckInStatus") as HTMLParagraphElement;
// Check for required elements (only need one set)
if ((!eventCodeInput || !checkInButton || !checkInStatus) &&
(!skeletonEventCodeInput || !skeletonCheckInButton || !skeletonCheckInStatus)) {
throw new Error("Required DOM elements not found");
}
// Return whichever set is available (prefer content over skeleton)
return {
eventCodeInput: eventCodeInput || skeletonEventCodeInput,
checkInButton: checkInButton || skeletonCheckInButton,
checkInStatus: checkInStatus || skeletonCheckInStatus,
};
}
private async validateEventCode(code: string): Promise<{ isValid: boolean; event?: any; message?: string }> {
try {
// Find event by code
const events = await this.pb.collection('events').getFullList({
filter: `event_code = "${code}"`,
});
if (events.length === 0) {
return {
isValid: false,
message: text.ui.messages.event.checkIn.invalid
};
}
const event = events[0];
const now = new Date();
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
// Check if current time is within event window
if (now < startDate) {
return {
isValid: false,
event,
message: text.ui.messages.event.checkIn.expired
};
}
if (now > endDate) {
return {
isValid: false,
event,
message: text.ui.messages.event.checkIn.expired
};
}
return { isValid: true, event };
} catch (err) {
console.error('Failed to validate event code:', err);
return {
isValid: false,
message: text.ui.messages.event.checkIn.error
};
}
}
private async handleCheckIn() {
const { eventCodeInput, checkInStatus } = this.elements;
const eventCode = eventCodeInput.value.trim();
if (!eventCode) {
this.showStatus(text.ui.messages.event.checkIn.invalid, "error");
return;
}
let validation: { isValid: boolean; event?: any; message?: string } | undefined;
try {
this.showStatus(text.ui.messages.event.checkIn.checking, "info");
// Get current user
const user = this.pb.authStore.model;
if (!user) {
this.showStatus(text.ui.messages.auth.notSignedIn, "error");
await this.logger.send(
"error",
"event check in",
"Check-in attempt failed: User not authenticated"
);
return;
}
// Validate event code and check time window
validation = await this.validateEventCode(eventCode);
if (!validation.isValid) {
this.showStatus(validation.message || text.ui.messages.event.checkIn.invalid, "error");
await this.logger.send(
"error",
"event check in",
`Invalid event code attempt: "${eventCode}". Reason: ${validation.message}`
);
return;
}
const event = validation.event;
// Get user's attended events and current points
const currentUser = await this.pb.collection("users").getOne(user.id);
let eventsAttended: string[] = [];
let currentPoints = currentUser.points || 0;
// Handle different cases for events_attended field
if (currentUser.events_attended) {
if (Array.isArray(currentUser.events_attended)) {
eventsAttended = currentUser.events_attended;
} else if (typeof currentUser.events_attended === 'string') {
try {
eventsAttended = JSON.parse(currentUser.events_attended);
} catch (err) {
eventsAttended = [];
}
}
}
// Ensure eventsAttended is an array
if (!Array.isArray(eventsAttended)) {
eventsAttended = [];
}
// Check if already checked in using event_id
const isAlreadyCheckedIn = eventsAttended.includes(event.event_id);
if (isAlreadyCheckedIn) {
this.showStatus(text.ui.messages.event.checkIn.alreadyCheckedIn, "info");
await this.logger.send(
"error",
"event check in",
`Duplicate check-in attempt for event "${event.event_name}" (${event.event_id})`
);
eventCodeInput.value = ""; // Clear input
return;
}
// Add event_id to user's attended events and update points
eventsAttended.push(event.event_id);
const pointsToAdd = event.points_to_reward || 0;
const newTotalPoints = currentPoints + pointsToAdd;
// Update user with new events_attended and points
await this.pb.collection("users").update(user.id, {
events_attended: JSON.stringify(eventsAttended),
points: newTotalPoints
});
// Update event's attendees list
let eventAttendees = [];
try {
eventAttendees = JSON.parse(event.attendees || '[]');
} catch (err) {
eventAttendees = [];
}
if (!Array.isArray(eventAttendees)) {
eventAttendees = [];
}
// Add user to attendees if not already present
if (!eventAttendees.includes(user.id)) {
eventAttendees.push(user.id);
await this.pb.collection("events").update(event.id, {
attendees: JSON.stringify(eventAttendees)
});
}
// Show success message with points
this.showStatus(text.ui.messages.event.checkIn.success, "success");
eventCodeInput.value = ""; // Clear input
// Log the successful check-in
await this.logger.send(
"create",
"event check in",
`Successfully checked in to event ${event.id}`
);
// Log the points update
await this.logger.send(
"update",
"loyalty points",
`Points updated from ${currentPoints} to ${newTotalPoints} (+${pointsToAdd} from event ${event.id})`
);
// Update event attendance count
const currentAttendance = event.attendance_count || 0;
await this.pb.collection('events').update(event.id, {
attendance_count: currentAttendance + 1
});
// Log the attendance update
await this.logger.send(
"update",
"event attendance",
`Event ${event.id} attendance updated to ${currentAttendance + 1}`
);
} catch (err) {
console.error("Check-in error:", err);
this.showStatus(text.ui.messages.event.checkIn.error, "error");
// Log any errors that occur during check-in
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"}`
);
}
}
}
private showStatus(message: string, type: "error" | "success" | "info") {
const { checkInStatus } = this.elements;
checkInStatus.textContent = message;
checkInStatus.className = "text-xs mt-1";
switch (type) {
case "error":
checkInStatus.classList.add("text-error");
break;
case "success":
checkInStatus.classList.add("text-success");
break;
case "info":
checkInStatus.classList.add("opacity-70");
break;
}
// Clear status after timeout
if (type !== "info") {
setTimeout(() => {
checkInStatus.textContent = "";
}, text.ui.messages.event.checkIn.messageTimeout);
}
}
/**
* Gets all events a user has checked into
* @param userId The ID of the user
*/
public async getUserEventHistory(userId: string) {
try {
const records = await this.pb.collection('event_checkins').getFullList({
filter: `user_id="${userId}"`,
sort: '-created',
expand: 'event_id'
});
// Log the history retrieval
await this.logger.send(
"update",
"event attendance",
`Retrieved attendance history for user: ${records.length} events found`
);
return records;
} catch (error) {
// Log any errors in retrieving history
await this.logger.send(
"error",
"event attendance",
`Failed to retrieve event history: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
}

View file

@ -1,354 +0,0 @@
<div>
<!-- Loading Skeleton -->
<div id="loadingSkeleton" class="card bg-base-100 shadow-xl">
<div class="card-body p-6">
<!-- Avatar and Name Section -->
<div class="flex flex-col items-center mb-6">
<div class="skeleton w-24 h-24 rounded-full mb-4"></div>
<div class="skeleton h-8 w-48"></div>
<div class="skeleton h-4 w-32 mt-2"></div>
</div>
<!-- Member Status -->
<div class="flex justify-center mb-6">
<div class="skeleton h-6 w-32"></div>
</div>
<!-- Stats Grid -->
<div class="stats stats-vertical shadow bg-base-200 mb-6">
<div class="stat px-6 py-2">
<div class="skeleton h-4 w-20 mb-1"></div>
<div class="skeleton h-8 w-24"></div>
</div>
<div class="stat px-6 py-2">
<div class="skeleton h-4 w-24 mb-1"></div>
<div class="skeleton h-8 w-32"></div>
</div>
</div>
<!-- Member Details -->
<div class="space-y-4">
<div class="space-y-2">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-10 w-full"></div>
</div>
<div class="space-y-2">
<div class="skeleton h-4 w-20"></div>
<div class="skeleton h-10 w-full"></div>
</div>
</div>
</div>
</div>
<!-- Actual Content -->
<div id="userInfo" class="card bg-base-100 shadow-xl opacity-0 hidden">
<div class="card-body p-6">
<!-- Avatar and Name Section -->
<div class="flex flex-col items-center mb-6">
<div class="avatar online placeholder mb-4">
<div
class="bg-gradient-to-br from-primary to-secondary text-primary-content rounded-full w-24 ring ring-primary ring-offset-base-100 ring-offset-2"
>
<span id="userInitials" class="text-3xl"></span>
</div>
</div>
<div class="text-center">
<h2 id="userName" class="text-2xl font-bold">Not signed in</h2>
<p id="userEmail" class="text-base-content/70">Not signed in</p>
</div>
</div>
<!-- Member Status -->
<div class="flex justify-center mb-6">
<div id="memberStatus" class="badge badge-lg gap-2">
<span class="loading loading-ring loading-xs"></span>
Not verified
</div>
</div>
<!-- Stats Grid -->
<div class="stats stats-vertical shadow bg-base-200 mb-6">
<div class="stat px-6 py-2">
<div class="stat-title text-xs">Last Login</div>
<div id="lastLogin" class="stat-value text-lg">Never</div>
</div>
<div class="stat px-6 py-2">
<div class="stat-title text-xs">Member Since</div>
<div class="stat-value text-lg" id="memberSince">-</div>
</div>
</div>
<!-- Auth Buttons -->
<div class="pt-4">
<button
id="contentLoginButton"
class="login-button btn btn-primary w-full gap-2 hidden"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
clip-rule="evenodd"></path>
</svg>
Sign in with IEEEUCSD SSO
</button>
<button
id="contentLogoutButton"
class="logout-button btn btn-error w-full gap-2 hidden"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
clip-rule="evenodd"></path>
</svg>
Sign Out
</button>
</div>
</div>
</div>
</div>
<style>
.hidden {
display: none;
}
#userInfo {
transition: all 0.3s ease-in-out;
}
</style>
<script>
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { Get } from "../pocketbase/Get";
import { SendLog } from "../pocketbase/SendLog";
// Initialize services
const auth = Authentication.getInstance();
const update = Update.getInstance();
const get = Get.getInstance();
const logger = SendLog.getInstance();
// Get DOM elements
const loadingSkeleton = document.getElementById("loadingSkeleton");
const userInfo = document.getElementById("userInfo");
const userInitials = document.getElementById("userInitials");
const userName = document.getElementById("userName");
const userEmail = document.getElementById("userEmail");
const memberStatus = document.getElementById("memberStatus");
const lastLogin = document.getElementById("lastLogin");
const memberSince = document.getElementById("memberSince");
const contentLoginButton = document.getElementById("contentLoginButton");
const contentLogoutButton = document.getElementById("contentLogoutButton");
// Function to format date
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleString();
}
// Function to get initials from name
function getInitials(name: string): string {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
}
// Function to update UI with user data
async function updateUI(user: any) {
if (!user) {
// Show login button if not authenticated
if (contentLoginButton) contentLoginButton.classList.remove("hidden");
if (contentLogoutButton) contentLogoutButton.classList.add("hidden");
if (userName) userName.textContent = "Not signed in";
if (userEmail) userEmail.textContent = "Not signed in";
if (userInitials) userInitials.textContent = "?";
if (memberStatus) {
memberStatus.textContent = "Not Verified";
memberStatus.className = "badge badge-lg gap-2";
}
if (lastLogin) lastLogin.textContent = "Never";
if (memberSince) memberSince.textContent = "-";
return;
}
try {
// Get full user data
const userData = await get.getOne("users", user.id);
// Update UI elements
if (userName) userName.textContent = userData.name || "Unnamed User";
if (userEmail) userEmail.textContent = userData.email || "No email";
if (userInitials)
userInitials.textContent = getInitials(userData.name || "?");
// Update member status based on member_type and email domain
if (memberStatus) {
let memberType = userData.member_type || "IEEE Member";
// Auto-assign officer status for @ieeeucsd.org emails if they're not already a higher rank
if (
userData.email?.endsWith("@ieeeucsd.org") &&
!["IEEE Executive", "IEEE Administrator", "IEEE Sponsor"].includes(
memberType,
)
) {
memberType = "IEEE Officer";
// Update the member type in the database if it's different
if (userData.member_type !== memberType) {
try {
await update.updateField(
"users",
userData.id,
"member_type",
memberType,
);
await logger.send(
"update",
"profile view",
`Updated member type to ${memberType} for user with @ieeeucsd.org email`,
);
} catch (error) {
console.error("Failed to update member type:", error);
}
}
}
// Set badge color based on member type
let badgeClass = "badge-primary"; // default
switch (memberType) {
case "IEEE Sponsor":
badgeClass = "badge-warning";
break;
case "IEEE Administrator":
badgeClass = "badge-error";
break;
case "IEEE Executive":
badgeClass = "badge-secondary";
break;
case "IEEE Officer":
badgeClass = "badge-info";
break;
case "IEEE Member":
badgeClass = "badge-success";
break;
}
memberStatus.textContent = memberType;
memberStatus.className = `badge badge-lg gap-2 ${badgeClass}`;
}
// Update timestamps
if (lastLogin) lastLogin.textContent = formatDate(userData.last_login);
if (memberSince) memberSince.textContent = formatDate(userData.created);
// Show logout button
if (contentLoginButton) contentLoginButton.classList.add("hidden");
if (contentLogoutButton) contentLogoutButton.classList.remove("hidden");
// Show the user info
if (loadingSkeleton) loadingSkeleton.style.display = "none";
if (userInfo) {
userInfo.classList.remove("hidden");
userInfo.style.opacity = "1";
}
} catch (error) {
console.error("Failed to update UI:", error);
await logger.send(
"error",
"profile view",
`Failed to update profile UI: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
// Initialize profile
document.addEventListener("DOMContentLoaded", async () => {
try {
// Show loading state
if (loadingSkeleton) loadingSkeleton.style.display = "block";
if (userInfo) {
userInfo.classList.add("hidden");
userInfo.style.opacity = "0";
}
// Update UI based on current auth state
const user = auth.getCurrentUser();
await updateUI(user);
// Add auth state change listener
auth.onAuthStateChange(async () => {
const currentUser = auth.getCurrentUser();
await updateUI(currentUser);
});
// Add login button handler
if (contentLoginButton) {
contentLoginButton.addEventListener("click", async () => {
try {
if (loadingSkeleton) loadingSkeleton.style.display = "block";
if (userInfo) {
userInfo.classList.add("hidden");
userInfo.style.opacity = "0";
}
await auth.login();
} catch (error) {
console.error("Login error:", error);
await logger.send(
"error",
"profile view",
`Login failed: ${error instanceof Error ? error.message : "Unknown error"}`,
);
if (loadingSkeleton) loadingSkeleton.style.display = "none";
if (userInfo) {
userInfo.classList.remove("hidden");
userInfo.style.opacity = "1";
}
}
});
}
// Add logout button handler
if (contentLogoutButton) {
contentLogoutButton.addEventListener("click", async () => {
try {
await auth.logout();
await logger.send(
"logout",
"profile view",
"User logged out successfully",
);
} catch (error) {
console.error("Logout error:", error);
await logger.send(
"error",
"profile view",
`Logout failed: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
});
}
} 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"}`,
);
}
});
</script>

View file

@ -1,580 +0,0 @@
---
// Define the component's props and setup
interface Props {}
const {} = Astro.props;
---
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full">
<!-- Left Column - Events List -->
<div class="card bg-base-200 shadow-xl h-full">
<div class="card-body p-6 flex flex-col h-full">
<!-- Fixed Header Section -->
<div class="flex-none">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title text-2xl">Events</h2>
</div>
<!-- Event Check-in Section -->
<div class="card bg-base-200 shadow-xl mb-8">
<div class="card-body p-6">
<h2 class="card-title text-2xl mb-4">Quick Check-in</h2>
<!-- Event Check-in Skeleton -->
<div id="eventCheckInSkeleton" class="animate-pulse">
<div class="h-6 bg-base-300 rounded w-2/3 mb-4"></div>
<div class="flex items-center gap-2">
<div class="h-8 bg-base-300 rounded flex-1"></div>
<div class="h-8 bg-base-300 rounded w-24"></div>
</div>
</div>
<!-- Event Check-in Content -->
<div id="eventCheckInContent" class="hidden">
<div id="eventCheckInSection" class="space-y-2">
<div class="flex items-center gap-2">
<input
type="text"
id="eventCodeInput"
placeholder="Enter event code"
class="input input-bordered flex-1"
value=""
/>
<button id="checkInButton" class="btn btn-primary"
>Check In</button
>
</div>
<p id="checkInStatus" class="text-sm mt-1 opacity-70"></p>
</div>
</div>
</div>
</div>
<div class="divider mt-0 mb-4"></div>
</div>
<!-- Scrollable Events List -->
<div class="flex-1 overflow-y-auto min-h-0">
<div id="eventsList" class="space-y-4">
<!-- Loading Skeletons -->
{
Array(3)
.fill(0)
.map(() => (
<div class="card bg-base-100 shadow-sm animate-pulse">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div class="space-y-3 w-full">
<div class="h-6 bg-base-300 rounded w-3/4" />
<div class="space-y-2">
<div class="h-4 bg-base-300 rounded w-1/2 opacity-70" />
<div class="h-4 bg-base-300 rounded w-2/3 opacity-70" />
<div class="h-4 bg-base-300 rounded w-1/3 opacity-70" />
</div>
</div>
<div class="h-6 w-20 bg-base-300 rounded" />
</div>
</div>
</div>
))
}
</div>
</div>
</div>
</div>
<!-- Right Column - Profile Information -->
<div class="card bg-base-200 shadow-xl h-full">
<div class="card-body p-6">
<h2 class="card-title text-2xl mb-6">Past Events</h2>
<div id="pastEventsList" class="space-y-4 overflow-y-auto">
<!-- Loading Skeletons -->
{
Array(3)
.fill(0)
.map(() => (
<div class="card bg-base-100 shadow-sm animate-pulse">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div class="space-y-3 w-full">
<div class="h-6 bg-base-300 rounded w-3/4" />
<div class="space-y-2">
<div class="h-4 bg-base-300 rounded w-1/2 opacity-70" />
<div class="h-4 bg-base-300 rounded w-2/3 opacity-70" />
<div class="h-4 bg-base-300 rounded w-1/3 opacity-70" />
</div>
</div>
<div class="h-6 w-20 bg-base-300 rounded" />
</div>
</div>
</div>
))
}
</div>
</div>
</div>
</div>
<script>
// TypeScript declarations
declare global {
interface Window {
showEventFiles: (eventId: string) => Promise<void>;
}
}
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 the modal state
window.fileViewerModal = {
isOpen: false,
files: [],
setIsOpen: (isOpen: boolean) => {
window.fileViewerModal.isOpen = isOpen;
const modal = document.querySelector("file-viewer-modal") as any;
if (modal) {
modal.setAttribute("isOpen", isOpen.toString());
}
},
setFiles: (files) => {
window.fileViewerModal.files = files;
const modal = document.querySelector("file-viewer-modal") as any;
if (modal) {
modal.setAttribute("files", JSON.stringify(files));
}
},
};
// Track if we're currently fetching events
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 {
await renderEvents();
} catch (error) {
console.error("Failed to render events:", error);
}
}, 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");
}
}
// Function to format date
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleString();
}
// Function to check if event is upcoming
function isUpcoming(startDate: string): boolean {
const now = new Date();
const start = new Date(startDate);
return start > now;
}
// Function to check if event is current
function isCurrent(startDate: string, endDate: string): boolean {
const now = new Date();
const start = new Date(startDate);
const end = new Date(endDate);
return start <= now && now <= end;
}
// Function to get event status
function getEventStatus(
event: any,
isAttended: boolean,
): { status: string; badge: string } {
if (isAttended) {
return {
status: "Attended",
badge: "badge-success",
};
}
if (isCurrent(event.start_date, event.end_date)) {
return {
status: "Current",
badge: "badge-warning",
};
}
if (isUpcoming(event.start_date)) {
return {
status: "Upcoming",
badge: "badge-info",
};
}
return {
status: "Past",
badge: "badge-ghost",
};
}
// Function to get status icon
function getStatusIcon(status: string): string {
const checkIcon = `<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="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>`;
const clockIcon = `<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="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>`;
const exclamationIcon = `<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="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>`;
switch (status) {
case "Attended":
return checkIcon;
case "Current":
return exclamationIcon;
case "Upcoming":
return clockIcon;
default:
return "";
}
}
// Function to render event card
function renderEventCard(event: Event, attendedEvents: string[]): string {
const isAttended =
Array.isArray(attendedEvents) && attendedEvents.includes(event.event_id);
const hasFiles =
event.files && Array.isArray(event.files) && event.files.length > 0;
const isPastEvent = new Date(event.end_date) < new Date();
const { status, badge } = getEventStatus(event, isAttended);
// Only show files button for past events
const filesButton =
hasFiles && isPastEvent
? `
<button
class="btn btn-ghost btn-xs gap-2"
onclick="window.showEventFiles('${event.id}')"
>
<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" />
</svg>
${event.files?.length || 0} File${(event.files?.length || 0) > 1 ? "s" : ""}
</button>
`
: "";
return `
<div class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow">
<div class="card-body p-4">
<div class="flex justify-between items-start gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<h3 class="font-medium text-lg truncate">${event.event_name}</h3>
<div class="badge ${badge} gap-1">
${getStatusIcon(status)}
${status}
</div>
</div>
<div class="text-sm opacity-70 space-y-1">
<p>Starts: ${formatDate(event.start_date)}</p>
<p>Ends: ${formatDate(event.end_date)}</p>
${
event.location
? `<p class="flex items-center gap-1">
<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="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
</svg>
${event.location}
</p>`
: ""
}
</div>
${hasFiles && isPastEvent ? `<div class="mt-3">${filesButton}</div>` : ""}
</div>
</div>
</div>
</div>
`;
}
// 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;
// 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);
}
}
// 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[] = [];
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 = [];
}
}
// Fetch all events
console.log("Fetching all events");
const events = await get.getAll<Event>(
"events",
undefined,
"-start_date",
);
console.log("Fetched events:", events);
if (!Array.isArray(events)) {
throw new Error("Failed to fetch events: Invalid response format");
}
// 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) => {
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>
`;
eventsList.innerHTML = errorMessage;
pastEventsList.innerHTML = errorMessage;
throw err; // Re-throw to handle in the outer catch
} finally {
isFetchingEvents = false;
}
})();
lastFetchPromise = fetchPromise;
try {
await fetchPromise;
} catch (err) {
// 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);
}
});
// Update the showEventFiles function to use custom events
window.showEventFiles = async (eventId: string) => {
try {
// Fetch the event data
const event = await get.getOne("events", eventId);
if (
!event.files ||
!Array.isArray(event.files) ||
event.files.length === 0
) {
console.warn("No files available for event:", eventId);
return;
}
// Convert files to the format expected by FileViewerModal
const files = event.files.map((file: string) => {
const fileName = file.split("/").pop() || "File";
const fileUrl = fileManager.getFileUrl("events", event.id, file);
const fileType = getFileType(fileName);
return {
url: fileUrl,
type: fileType,
name: fileName,
};
});
// Dispatch custom event to show files
const showFileViewerEvent = new CustomEvent("showFileViewer", {
detail: { files },
});
window.dispatchEvent(showFileViewerEvent);
} catch (err) {
console.error("Failed to load event files:", err);
await logger.send(
"error",
"show event files",
`Failed to load event files: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
// Helper function to determine file type
function getFileType(fileName: string): string {
const extension = fileName.split(".").pop()?.toLowerCase() || "";
const mimeTypes: Record<string, string> = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
mp4: "video/mp4",
webm: "video/webm",
mp3: "audio/mpeg",
wav: "audio/wav",
txt: "text/plain",
json: "application/json",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
};
return mimeTypes[extension] || "application/octet-stream";
}
</script>

View file

@ -1,446 +0,0 @@
---
import yaml from "js-yaml";
import textConfig from "../../config/text.yml?raw";
// Parse YAML configuration
const text = yaml.load(textConfig) as any;
const { editor_title, form } = text.ui.tables.events;
---
<!-- Event Editor Dialog -->
<dialog id="eventEditor" class="modal">
<div class="modal-box w-11/12 max-w-4xl">
<h3 class="font-bold text-lg mb-6">{editor_title}</h3>
<form
class="space-y-6"
id="eventForm"
novalidate
onsubmit="event.preventDefault();"
>
<!-- Basic Info Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control">
<label class="label" for="editorEventId">
<span class="label-text">{form.event_id.label}</span>
</label>
<input
type="text"
id="editorEventId"
class="input input-bordered w-full"
placeholder={form.event_id.placeholder}
pattern="[A-Za-z0-9_\-]+"
minlength="3"
maxlength="50"
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="eventIdError">This field is required</span
>
</label>
</div>
<div class="form-control">
<label class="label" for="editorEventCode">
<span class="label-text">{form.event_code.label}</span>
</label>
<input
type="text"
id="editorEventCode"
class="input input-bordered w-full"
placeholder={form.event_code.placeholder}
pattern="[A-Za-z0-9_\-]+"
minlength="3"
maxlength="20"
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="eventCodeError">This field is required</span
>
</label>
</div>
</div>
<div class="form-control">
<label class="label" for="editorEventName">
<span class="label-text">{form.event_name.label}</span>
</label>
<input
type="text"
id="editorEventName"
class="input input-bordered w-full"
placeholder={form.event_name.placeholder}
minlength="3"
maxlength="100"
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="eventNameError">This field is required</span
>
</label>
</div>
<!-- Date/Time Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<h4 class="font-medium text-sm opacity-70">
Start Date/Time
</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="editorStartDate">
<span class="label-text"
>{form.start_date.date_label}</span
>
</label>
<input
type="date"
id="editorStartDate"
class="input input-bordered w-full"
placeholder={form.start_date.date_placeholder}
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="startDateError"
>This field is required</span
>
</label>
</div>
<div class="form-control">
<label class="label" for="editorStartTime">
<span class="label-text"
>{form.start_date.time_label}</span
>
</label>
<input
type="time"
id="editorStartTime"
class="input input-bordered w-full"
placeholder={form.start_date.time_placeholder}
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="startTimeError"
>This field is required</span
>
</label>
</div>
</div>
</div>
<div class="space-y-4">
<h4 class="font-medium text-sm opacity-70">
End Date/Time
</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="editorEndDate">
<span class="label-text"
>{form.end_date.date_label}</span
>
</label>
<input
type="date"
id="editorEndDate"
class="input input-bordered w-full"
placeholder={form.end_date.date_placeholder}
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="endDateError"
>This field is required</span
>
</label>
</div>
<div class="form-control">
<label class="label" for="editorEndTime">
<span class="label-text"
>{form.end_date.time_label}</span
>
</label>
<input
type="time"
id="editorEndTime"
class="input input-bordered w-full"
placeholder={form.end_date.time_placeholder}
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="endTimeError"
>This field is required</span
>
</label>
</div>
</div>
</div>
</div>
<!-- Additional Info Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control">
<label class="label" for="editorPointsToReward">
<span class="label-text"
>{form.points_to_reward.label}</span
>
</label>
<input
type="number"
id="editorPointsToReward"
class="input input-bordered w-full"
placeholder={form.points_to_reward.placeholder}
min="0"
max="1000"
required
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="pointsError">This field is required</span
>
</label>
</div>
<div class="form-control">
<label class="label" for="editorLocation">
<span class="label-text">{form.location.label}</span>
</label>
<input
type="text"
id="editorLocation"
class="input input-bordered w-full"
placeholder={form.location.placeholder}
maxlength="200"
/>
<label class="label">
<span
class="label-text-alt text-error hidden"
id="locationError"></span>
</label>
</div>
</div>
<!-- Files Section -->
<div class="form-control">
<label class="label" for="editorFiles">
<span class="label-text">{form.files.label}</span>
</label>
<input
type="file"
id="editorFiles"
class="file-input file-input-bordered w-full"
multiple
accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png"
/>
<div id="uploadProgress" class="w-full mt-2 hidden">
<div class="flex justify-between mb-1 text-xs opacity-70">
<span>Uploading files...</span>
<span id="uploadProgressText">0%</span>
</div>
<progress
id="uploadProgressBar"
class="progress progress-primary w-full"
value="0"
max="100"></progress>
</div>
<div id="currentFiles" class="mt-4 space-y-2"></div>
<label class="label">
<span class="label-text-alt opacity-70"
>{form.files.help_text}</span
>
</label>
</div>
<div class="modal-action flex-wrap gap-2">
<button
type="button"
class="btn btn-ghost order-1 sm:order-none"
onclick="eventEditor.close()"
>
{form.buttons.cancel}
</button>
<button
type="submit"
id="saveEventButton"
class="btn btn-primary flex-1 sm:flex-none"
>
{form.buttons.save}
</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</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

@ -1,683 +0,0 @@
---
import EventEditor from "./EventEditor.astro";
import yaml from "js-yaml";
import textConfig from "../../config/text.yml?raw";
import profileConfig from "../../config/profileConfig.yaml?raw";
// Parse YAML configuration
const text = yaml.load(textConfig) as any;
const config = yaml.load(profileConfig) as any;
const { title, columns } = config.ui.tables.events;
---
<div
class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 lg:gap-0 mb-4"
>
<h2 class="text-2xl font-bold">{title}</h2>
<div class="flex flex-col lg:flex-row gap-2">
<div class="form-control w-full">
<input
type="text"
id="eventSearch"
placeholder="Search events..."
class="input input-bordered input-sm w-full"
/>
</div>
<div class="flex gap-2 w-full lg:w-auto">
<button id="searchEvents" class="btn btn-sm flex-1 lg:flex-none">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<span class="lg:hidden ml-2">Search</span>
</button>
<button id="refreshEvents" class="btn btn-sm flex-1 lg:flex-none">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
></path>
</svg>
<span class="lg:hidden ml-2">Refresh</span>
</button>
<button
id="addEvent"
class="btn btn-primary btn-sm flex-1 lg:flex-none"
>
<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="M12 4v16m8-8H4"></path>
</svg>
<span class="lg:hidden ml-2">Add Event</span>
</button>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table
class="table table-zebra w-full [&_tr]:border-b [&_tr]:border-base-200"
>
<thead class="hidden lg:table-header-group">
<tr>
<th class="text-center">{columns.event_name}</th>
<th class="text-center">{columns.event_id}</th>
<th class="text-center">{columns.event_code}</th>
<th class="text-center">{columns.start_date}</th>
<th class="text-center">{columns.end_date}</th>
<th class="text-center">{columns.points_to_reward}</th>
<th class="text-center">{columns.location}</th>
<th class="text-center">Files</th>
<th class="text-center">Attendees</th>
<th class="text-center">{columns.actions}</th>
</tr>
</thead>
<tbody id="eventList" class="divide-y divide-base-200">
<!-- Event entries will be populated here -->
</tbody>
</table>
</div>
<EventEditor />
<!-- File Viewer Modal -->
<dialog id="fileViewer" class="modal">
<div class="modal-box w-11/12 max-w-5xl h-[80vh]">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg" id="fileTitle">File Preview</h3>
<div class="flex items-center gap-2">
<a
id="fileExternalLink"
href="#"
target="_blank"
class="btn btn-sm btn-ghost"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-1"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"
></path>
<path
d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"
></path>
</svg>
Open in New Tab
</a>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost">✕</button>
</form>
</div>
</div>
<div class="h-[calc(100%-4rem)] bg-base-200 rounded-lg">
<!-- PDF Preview -->
<iframe
id="fileFrame"
class="w-full h-full rounded-lg border-2 border-base-300 hidden"
src=""></iframe>
<!-- Image Preview -->
<img
id="imagePreview"
class="w-full h-full object-contain rounded-lg hidden"
src=""
alt="File preview"
/>
<!-- Text Preview -->
<div
id="textPreview"
class="w-full h-full p-4 font-mono text-sm overflow-auto whitespace-pre rounded-lg hidden"
>
</div>
<!-- Unsupported Format Message -->
<div
id="unsupportedPreview"
class="w-full h-full flex items-center justify-center text-center p-4 hidden"
>
<div class="space-y-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 mx-auto opacity-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
></path>
</svg>
<div>
<p class="text-lg font-medium">Preview not available</p>
<p class="text-sm opacity-70">
Please use the "Open in New Tab" button to view this
file
</p>
</div>
</div>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
import { Authentication } from "../pocketbase/Authentication";
import { Get } from "../pocketbase/Get";
import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog";
import JSZip from "jszip";
// Get instances
const auth = Authentication.getInstance();
const get = Get.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
// Show initial loading state
const eventsList = document.getElementById("eventList");
if (eventsList) {
eventsList.innerHTML = `
<tr>
<td colspan="10" class="text-center py-8">
<div class="flex flex-col items-center gap-2">
<span class="loading loading-spinner loading-lg"></span>
<span class="text-sm opacity-70">Loading events...</span>
</div>
</td>
</tr>
`;
}
// Initialize after DOM is fully loaded
document.addEventListener("DOMContentLoaded", () => {
loadEvents();
});
// Function to format date
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleString();
}
// Function to render event row
function renderEventRow(event: any) {
return `
<tr class="hover:bg-base-200">
<!-- Mobile View -->
<td class="block lg:table-cell">
<div class="lg:hidden space-y-2">
<div class="font-medium text-center">${event.event_name || "N/A"}</div>
<div class="text-sm opacity-70 text-center">${columns.event_id}: ${event.event_id || "N/A"}</div>
<div class="text-sm opacity-70 text-center">${columns.event_code}: ${event.event_code || "N/A"}</div>
<div class="text-sm opacity-70 text-center">${columns.start_date}: ${formatDate(event.start_date)}</div>
<div class="text-sm opacity-70 text-center">${columns.end_date}: ${formatDate(event.end_date)}</div>
<div class="text-sm opacity-70 text-center">${columns.points_to_reward}: ${event.points_to_reward || "0"}</div>
<div class="text-sm opacity-70 text-center">${columns.location}: ${event.location || "N/A"}</div>
<div class="text-sm opacity-70 text-center">Files: ${event.files?.length || 0}</div>
<div class="text-sm opacity-70 text-center">Attendees: ${event.attendees ? JSON.parse(event.attendees).length : 0}</div>
<div class="flex items-center justify-center gap-2 mt-2">
<button class="btn btn-ghost btn-xs view-attendees" data-event-id="${event.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
View Attendees
</button>
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
Edit
</button>
<button class="btn btn-ghost btn-xs text-error delete-event" data-event-id="${event.id}">
<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="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
Delete
</button>
</div>
</div>
<!-- Desktop View -->
<span class="hidden lg:block text-center">${event.event_name || "N/A"}</span>
</td>
<td class="hidden lg:table-cell text-center">${event.event_id || "N/A"}</td>
<td class="hidden lg:table-cell text-center">${event.event_code || "N/A"}</td>
<td class="hidden lg:table-cell text-center">${formatDate(event.start_date)}</td>
<td class="hidden lg:table-cell text-center">${formatDate(event.end_date)}</td>
<td class="hidden lg:table-cell text-center">${event.points_to_reward || "0"}</td>
<td class="hidden lg:table-cell text-center">${event.location || "N/A"}</td>
<td class="hidden lg:table-cell text-center">
${
event.files && event.files.length > 0
? `<button class="btn btn-ghost btn-xs view-files" data-event-id="${event.id}">
${event.files.length} File${event.files.length > 1 ? "s" : ""}
</button>`
: '<span class="text-sm opacity-50">No files</span>'
}
</td>
<td class="hidden lg:table-cell text-center">
<button class="btn btn-ghost btn-xs view-attendees" data-event-id="${event.id}">
<span class="mr-2">${event.attendees ? JSON.parse(event.attendees).length : 0}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
</button>
</td>
<td class="hidden lg:table-cell text-center">
<div class="flex justify-center gap-2">
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
Edit
</button>
<button class="btn btn-ghost btn-xs text-error delete-event" data-event-id="${event.id}">
<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="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
Delete
</button>
</div>
</td>
</tr>
`;
}
// Function to load events
async function loadEvents(searchQuery = "") {
if (!eventsList) return;
// Show loading state immediately
eventsList.innerHTML = `
<tr>
<td colspan="10" class="text-center py-8">
<div class="flex flex-col items-center gap-2">
<span class="loading loading-spinner loading-lg"></span>
<span class="text-sm opacity-70">Loading events...</span>
</div>
</td>
</tr>
`;
try {
// Fetch events with filter if search query exists
const filter = searchQuery
? `event_name ~ "${searchQuery}" || event_id ~ "${searchQuery}" || event_code ~ "${searchQuery}"`
: "";
const events = await get.getList(
"events",
1,
50,
filter,
"-created"
);
// Update table
if (events.items.length === 0) {
eventsList.innerHTML = `
<tr>
<td colspan="10" class="text-center py-4">
${searchQuery ? "No events found matching your search." : "No events found."}
</td>
</tr>
`;
return;
}
eventsList.innerHTML = events.items.map(renderEventRow).join("");
// Setup event listeners for buttons
setupEventListeners(events.items);
} catch (err) {
console.error("Failed to load events:", err);
await logger.send(
"error",
"event management",
`Failed to load events: ${err instanceof Error ? err.message : "Unknown error"}`
);
eventsList.innerHTML = `
<tr>
<td colspan="10" class="text-center py-4 text-error">
<div class="flex flex-col items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Failed to load events. Please try again.</span>
<button onclick="loadEvents()" class="btn btn-sm btn-ghost mt-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Retry
</button>
</div>
</td>
</tr>
`;
}
}
// Function to setup event listeners
function setupEventListeners(events: any[]) {
const editButtons = eventsList.querySelectorAll(".edit-event");
editButtons.forEach((button) => {
button.addEventListener("click", () => {
const eventId = (button as HTMLButtonElement).dataset.eventId;
if (eventId) {
handleEventEdit(eventId);
}
});
});
const deleteButtons = eventsList.querySelectorAll(".delete-event");
deleteButtons.forEach((button) => {
button.addEventListener("click", () => {
const eventId = (button as HTMLButtonElement).dataset.eventId;
if (eventId) {
handleEventDelete(eventId);
}
});
});
const viewAttendeesButtons =
eventsList.querySelectorAll(".view-attendees");
viewAttendeesButtons.forEach((button) => {
button.addEventListener("click", () => {
const eventId = (button as HTMLButtonElement).dataset.eventId;
if (eventId) {
handleViewAttendees(eventId);
}
});
});
const viewFilesButtons = eventsList.querySelectorAll(".view-files");
viewFilesButtons.forEach((button) => {
button.addEventListener("click", async () => {
const eventId = (button as HTMLButtonElement).dataset.eventId;
if (eventId) {
await handleViewFiles(eventId);
}
});
});
}
// Function to handle event edit
async function handleEventEdit(eventId: string) {
try {
const event = await get.getOne("events", eventId);
const {
eventEditor,
editorEventId,
editorEventName,
editorEventCode,
editorStartDate,
editorStartTime,
editorEndDate,
editorEndTime,
editorPointsToReward,
editorLocation,
saveEventButton,
} = getElements();
// Split start and end dates into separate date and time
const startDateTime = splitDateTime(event.start_date);
const endDateTime = 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 || "";
// Store the event ID for saving
saveEventButton.dataset.eventId = eventId;
// Show the dialog
eventEditor.showModal();
await logger.send(
"update",
"event management",
`Opened event editor for event: ${event.event_name} (${event.event_id})`
);
} catch (err) {
console.error("Failed to load event for editing:", err);
await logger.send(
"error",
"event management",
`Failed to load event for editing: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
// Function to handle event delete
async function handleEventDelete(eventId: string) {
if (confirm("Are you sure you want to delete this event?")) {
try {
const event = await get.getOne("events", eventId);
await update.deleteRecord("events", eventId);
await loadEvents();
await logger.send(
"delete",
"event management",
`Deleted event: ${event.event_name} (${event.event_id})`
);
} catch (err) {
console.error("Failed to delete event:", err);
await logger.send(
"error",
"event management",
`Failed to delete event: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
}
// Function to handle viewing attendees
async function handleViewAttendees(eventId: string) {
try {
const event = await get.getOne("events", eventId);
const attendees = JSON.parse(event.attendees || "[]");
// Fetch user details for each attendee
const userDetails = await Promise.all(
attendees.map(async (userId: string) => {
try {
const user = await get.getOne("users", 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
showAttendeesModal(event, userDetails);
await logger.send(
"update",
"event management",
`Viewed attendees for event: ${event.event_name} (${event.event_id})`
);
} catch (err) {
console.error("Failed to view attendees:", err);
await logger.send(
"error",
"event management",
`Failed to view attendees: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
// Function to handle viewing files
async function handleViewFiles(eventId: string) {
try {
const event = await get.getOne("events", eventId);
showFilesModal(event);
await logger.send(
"update",
"event management",
`Viewed files for event: ${event.event_name} (${event.event_id})`
);
} catch (err) {
console.error("Failed to view files:", err);
await logger.send(
"error",
"event management",
`Failed to view files: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
// Add event listeners
const searchInput = document.getElementById(
"eventSearch"
) as HTMLInputElement;
const searchButton = document.getElementById("searchEvents");
const refreshButton = document.getElementById("refreshEvents");
const addEventButton = document.getElementById("addEvent");
if (searchButton && searchInput) {
searchButton.addEventListener("click", () => {
loadEvents(searchInput.value);
});
searchInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
loadEvents(searchInput.value);
}
});
}
if (refreshButton) {
refreshButton.addEventListener("click", () => {
if (searchInput) searchInput.value = "";
loadEvents();
});
}
if (addEventButton) {
addEventButton.addEventListener("click", () => {
const dialog = document.getElementById(
"eventEditor"
) as HTMLDialogElement;
dialog?.showModal();
});
}
// Initial load
loadEvents();
// File preview handling
const fileViewer = document.getElementById(
"fileViewer"
) as HTMLDialogElement;
const fileFrame = document.getElementById("fileFrame") as HTMLIFrameElement;
const imagePreview = document.getElementById(
"imagePreview"
) as HTMLImageElement;
const textPreview = document.getElementById(
"textPreview"
) as HTMLDivElement;
const unsupportedPreview = document.getElementById(
"unsupportedPreview"
) as HTMLDivElement;
const fileTitle = document.getElementById(
"fileTitle"
) as HTMLHeadingElement;
const fileExternalLink = document.getElementById(
"fileExternalLink"
) as HTMLAnchorElement;
// Function to show file preview
async function showFilePreview(url: string, fileName: string) {
// Reset all preview elements
fileFrame.classList.add("hidden");
imagePreview.classList.add("hidden");
textPreview.classList.add("hidden");
unsupportedPreview.classList.add("hidden");
// Update title and external link
fileTitle.textContent = fileName;
fileExternalLink.href = url;
// Get file extension
const ext = fileName.split(".").pop()?.toLowerCase() || "";
// Handle different file types
if (["jpg", "jpeg", "png", "gif"].includes(ext)) {
imagePreview.src = url;
imagePreview.classList.remove("hidden");
} else if (ext === "pdf") {
fileFrame.src = url;
fileFrame.classList.remove("hidden");
} else if (["txt", "md", "json", "yaml", "yml"].includes(ext)) {
try {
const response = await fetch(url);
const text = await response.text();
textPreview.textContent = text;
textPreview.classList.remove("hidden");
} catch (err) {
console.error("Failed to load text file:", err);
unsupportedPreview.classList.remove("hidden");
}
} else {
unsupportedPreview.classList.remove("hidden");
}
fileViewer.showModal();
}
// Add global event listener for file preview
document.addEventListener("showFilePreview", ((e: CustomEvent) => {
showFilePreview(e.detail.url, e.detail.fileName);
}) as EventListener);
</script>

View file

@ -1,502 +0,0 @@
---
// 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;
---
<div
class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 lg:gap-0 mb-4"
>
<h2 class="text-2xl font-bold">Member Management</h2>
<div class="flex flex-col lg:flex-row gap-2">
<div class="form-control w-full">
<input
type="text"
id="resumeSearch"
placeholder="Search users..."
class="input input-bordered input-sm w-full"
/>
</div>
<div class="flex gap-2 w-full lg:w-auto">
<button id="searchResumes" class="btn btn-sm flex-1 lg:flex-none">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<span class="lg:hidden ml-2">Search</span>
</button>
<button
id="refreshResumes"
class="btn btn-ghost btn-sm flex-1 lg:flex-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clip-rule="evenodd"></path>
</svg>
<span class="lg:hidden ml-2">Refresh</span>
</button>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table
class="table table-zebra w-full [&_tr]:border-b [&_tr]:border-base-200 text-center"
>
<thead class="hidden lg:table-header-group">
<tr>
<th class="text-center">Name</th>
<th class="text-center">Email</th>
<th class="text-center">Member ID</th>
<th class="text-center">Major</th>
<th class="text-center">Grad Year</th>
<th class="text-center">Points</th>
<th class="text-center">Resume</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody id="resumeList" class="divide-y divide-base-200">
<!-- Resume entries will be populated here -->
</tbody>
</table>
</div>
<!-- Add Profile Editor Dialog -->
<dialog id="profileEditor" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Edit User Profile</h3>
<form method="dialog" class="space-y-4">
<div class="form-control">
<label class="label">
<span class="label-text">Name</span>
</label>
<input
type="text"
id="editorName"
class="input input-bordered"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
id="editorEmail"
class="input input-bordered"
readonly
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">IEEE Member ID</span>
</label>
<input
type="text"
id="editorMemberId"
class="input input-bordered"
placeholder="00000000"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Major</span>
</label>
<select id="editorMajor" class="select select-bordered w-full">
<option value="">Select major</option>
{
majorsList.map((major) => (
<option value={major.trim()}>{major.trim()}</option>
))
}
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Graduation Year</span>
</label>
<select
id="editorGradYear"
class="select select-bordered w-full"
>
<option value="">Select graduation year</option>
{
Array.from({ length: 6 }, (_, i) => {
const year = new Date().getFullYear() + i;
return <option value={year}>{year}</option>;
})
}
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Points</span>
</label>
<input
type="number"
id="editorPoints"
class="input input-bordered"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Events Attended</span>
</label>
<textarea
id="editorEvents"
class="textarea textarea-bordered"
readonly></textarea>
</div>
<div class="modal-action">
<button
class="btn"
onclick="document.getElementById('profileEditor').close()"
>Cancel</button
>
<button id="saveProfileButton" class="btn btn-primary"
>Save Changes</button
>
</div>
</form>
</div>
</dialog>
<script is:inline define:vars={{ majorsList }}>
// Make majorsList available globally
window.majorsList = majorsList;
</script>
<script>
import PocketBase from "pocketbase";
import type { RecordModel } from "pocketbase";
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";
// Define interface for user data structure
interface UserData {
id: string;
avatar: string;
collectionId: string;
collectionName: string;
created: string;
email: string;
emailVisibility: boolean;
events_attended: string[];
graduation_year: number;
last_login: string;
major: string;
member_id: string;
member_type: string;
name: string;
points: number;
resume: string[];
updated: string;
verified: boolean;
}
// Parse YAML configuration
const text = yaml.load(textConfig) as any;
const profile = yaml.load(profileConfig) as any;
const pbConfig = yaml.load(pocketbaseConfig) as any;
const pb = new PocketBase(pbConfig.api.baseUrl);
// Get DOM elements
const resumeList = document.getElementById("resumeList");
const searchInput = document.getElementById(
"resumeSearch"
) as HTMLInputElement;
const searchButton = document.getElementById("searchResumes");
const refreshButton = document.getElementById("refreshResumes");
// Function to format date
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleString();
}
// Make handleEditUser globally available
window.handleEditUser = function (user: UserData) {
console.log("Edit user data:", user);
const profileEditor = document.getElementById(
"profileEditor"
) as HTMLDialogElement;
const editorName = document.getElementById(
"editorName"
) as HTMLInputElement;
const editorEmail = document.getElementById(
"editorEmail"
) as HTMLInputElement;
const editorMemberId = document.getElementById(
"editorMemberId"
) as HTMLInputElement;
const editorMajor = document.getElementById(
"editorMajor"
) as HTMLSelectElement;
const editorGradYear = document.getElementById(
"editorGradYear"
) as HTMLSelectElement;
const editorPoints = document.getElementById(
"editorPoints"
) as HTMLInputElement;
const editorEvents = document.getElementById(
"editorEvents"
) as HTMLTextAreaElement;
const saveProfileButton = document.getElementById(
"saveProfileButton"
) as HTMLButtonElement;
// Check if all required elements exist
if (
!profileEditor ||
!editorName ||
!editorEmail ||
!editorMemberId ||
!editorMajor ||
!editorGradYear ||
!editorPoints ||
!editorEvents ||
!saveProfileButton
) {
console.error("Missing form elements:", {
profileEditor: !!profileEditor,
editorName: !!editorName,
editorEmail: !!editorEmail,
editorMemberId: !!editorMemberId,
editorMajor: !!editorMajor,
editorGradYear: !!editorGradYear,
editorPoints: !!editorPoints,
editorEvents: !!editorEvents,
saveProfileButton: !!saveProfileButton,
});
return;
}
try {
// Populate the form with user data
editorName.value = user.name || "";
editorEmail.value = user.email || "";
editorMemberId.value = user.member_id || "";
editorMajor.value = user.major?.trim() || ""; // Trim to remove any newlines
editorGradYear.value = user.graduation_year?.toString() || "";
editorPoints.value = user.points?.toString() || "0";
editorEvents.value = Array.isArray(user.events_attended)
? user.events_attended.join(", ")
: "No events attended";
saveProfileButton.dataset.userId = user.id;
// Show the dialog
profileEditor.showModal();
} catch (err) {
console.error("Error populating form:", err);
}
};
// Function to render user row
function renderUserRow(user: RecordModel) {
const userData = user as unknown as UserData;
const escapedUser = JSON.stringify(userData)
.replace(/'/g, "\\'")
.replace(/"/g, "&quot;");
return `
<tr class="hover:bg-base-200">
<td class="text-center">
<div class="font-medium">${userData.name || "N/A"}</div>
</td>
<td class="text-center">${userData.email || "N/A"}</td>
<td class="text-center">${userData.member_id || "N/A"}</td>
<td class="text-center">${userData.major?.trim() || "N/A"}</td>
<td class="text-center">${userData.graduation_year || "N/A"}</td>
<td class="text-center">${userData.points || "0"}</td>
<td class="text-center">
${
userData.resume && userData.resume.length > 0
? `
<button class="btn btn-ghost btn-xs" onclick="window.open('${pb.files.getUrl(userData, userData.resume[0])}', '_blank')">
<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" />
</svg>
View
</button>
`
: "No Resume"
}
</td>
<td class="text-center">
<button class="btn btn-ghost btn-xs" onclick="handleEditUser(${escapedUser})">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
Edit
</button>
</td>
</tr>
`;
}
// Function to handle saving profile changes
async function handleProfileSave() {
const profileEditor = document.getElementById(
"profileEditor"
) as HTMLDialogElement;
const editorName = document.getElementById(
"editorName"
) as HTMLInputElement;
const editorMemberId = document.getElementById(
"editorMemberId"
) as HTMLInputElement;
const editorMajor = document.getElementById(
"editorMajor"
) as HTMLSelectElement;
const editorGradYear = document.getElementById(
"editorGradYear"
) as HTMLSelectElement;
const editorPoints = document.getElementById(
"editorPoints"
) as HTMLInputElement;
const saveProfileButton = document.getElementById(
"saveProfileButton"
) as HTMLButtonElement;
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("member_id", editorMemberId.value);
formData.append("major", editorMajor.value);
formData.append("graduation_year", editorGradYear.value);
formData.append("points", editorPoints.value);
await pb.collection("users").update(userId, formData);
profileEditor.close();
// Refresh the user list
loadUsers();
} catch (err) {
console.error("Failed to save user profile:", err);
}
}
// Function to load users
async function loadUsers(searchQuery = "") {
if (!resumeList) return;
try {
// Show loading state
resumeList.innerHTML = `
<tr>
<td colspan="6" class="text-center py-4">
<span class="loading loading-spinner loading-md"></span>
Loading users...
</td>
</tr>
`;
// Fetch users with filter if search query exists
const filter = searchQuery
? `name ~ "${searchQuery}" || email ~ "${searchQuery}" || member_id ~ "${searchQuery}"`
: "";
const users = await pb.collection("users").getList(1, 50, {
filter,
sort: "-created",
});
// Update table
if (users.items.length === 0) {
resumeList.innerHTML = `
<tr>
<td colspan="6" class="text-center py-4">
No users found
</td>
</tr>
`;
return;
}
resumeList.innerHTML = users.items.map(renderUserRow).join("");
} catch (error) {
console.error("Failed to load users:", error);
resumeList.innerHTML = `
<tr>
<td colspan="6" class="text-center py-4 text-error">
Failed to load users. Please try again.
</td>
</tr>
`;
}
}
// Add event listeners
if (searchButton && searchInput) {
searchButton.addEventListener("click", () => {
loadUsers(searchInput.value);
});
searchInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
loadUsers(searchInput.value);
}
});
}
if (refreshButton) {
refreshButton.addEventListener("click", () => {
if (searchInput) searchInput.value = "";
loadUsers();
});
}
// Add event listener for save button
const saveProfileButton = document.getElementById("saveProfileButton");
if (saveProfileButton) {
saveProfileButton.addEventListener("click", (e) => {
e.preventDefault();
handleProfileSave();
});
}
// Initial load
loadUsers();
</script>
<script>
declare global {
interface Window {
handleEditUser: (user: any) => void;
majorsList: string[];
}
}
</script>

View file

@ -1,48 +0,0 @@
---
import MemberManagement from "./MemberManagement.astro";
import EventManagement from "./EventManagement.astro";
---
<div id="officerContent" class="hidden">
<div class="tabs tabs-boxed mb-6" id="officerViewTabs">
<button class="tab tab-active" data-officer-tab="members"
>Member Management</button
>
<button class="tab" data-officer-tab="events">Event Management</button>
</div>
<div class="tab-contents">
<div id="officerMembersTab" style="display: block;">
<MemberManagement />
</div>
<div id="officerEventsTab" style="display: none;">
<EventManagement />
</div>
</div>
</div>
<script>
const officerTabs = document.querySelectorAll("[data-officer-tab]");
const officerMembersTab = document.getElementById("officerMembersTab");
const officerEventsTab = document.getElementById("officerEventsTab");
if (officerTabs && officerMembersTab && officerEventsTab) {
officerTabs.forEach((tab) => {
tab.addEventListener("click", () => {
// Update tab styles
officerTabs.forEach((t) => t.classList.remove("tab-active"));
tab.classList.add("tab-active");
// Update content visibility
const tabId = (tab as HTMLElement).dataset.officerTab;
if (tabId === "members") {
officerMembersTab.style.display = "block";
officerEventsTab.style.display = "none";
} else {
officerMembersTab.style.display = "none";
officerEventsTab.style.display = "block";
}
});
});
}
</script>

View file

@ -1,555 +0,0 @@
---
// Import the majors list
import allMajors from "../../data/allUCSDMajors.txt?raw";
const majorsList: string[] = allMajors
.split("\n")
.filter((major: string) => major.trim())
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
---
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-6">
<!-- Header -->
<div class="flex items-center gap-4 mb-8">
<div class="flex-1">
<h2 class="card-title text-2xl">Profile Settings</h2>
<p class="text-base-content/70 mt-1">
Manage your academic information and preferences
</p>
</div>
<div class="flex-none">
<button class="btn btn-primary" id="saveSettings">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"></path>
</svg>
Save Changes
</button>
</div>
</div>
<!-- Settings Sections -->
<div class="space-y-8">
<!-- Academic Information Section -->
<div class="bg-base-200 rounded-box p-6">
<div class="flex items-center gap-2 mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z"
></path>
</svg>
<h3 class="text-xl font-medium">Academic Information</h3>
</div>
<div class="grid gap-6">
<!-- Major Selection -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Major</span>
<span class="label-text-alt text-base-content/70"
>Select your current major</span
>
</label>
<select id="majorSelect" class="select select-bordered w-full">
<option value="">Select your major</option>
{
majorsList.map((major: string) => (
<option value={major}>{major}</option>
))
}
</select>
</div>
<!-- Graduation Year -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Expected Graduation</span>
<span class="label-text-alt text-base-content/70"
>When do you plan to graduate?</span
>
</label>
<select id="gradYearSelect" class="select select-bordered w-full">
<option value="">Select graduation year</option>
{
Array.from({ length: 6 }, (_, i) => {
const year = new Date().getFullYear() + i;
return <option value={year}>{year}</option>;
})
}
</select>
</div>
</div>
</div>
<!-- IEEE Information Section -->
<div class="bg-base-200 rounded-box p-6">
<div class="flex items-center gap-2 mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z"
clip-rule="evenodd"></path>
</svg>
<h3 class="text-xl font-medium">IEEE Membership</h3>
</div>
<div class="grid gap-6">
<!-- IEEE Member ID -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">IEEE Member ID</span>
<span class="label-text-alt text-base-content/70"
>Your IEEE membership number</span
>
</label>
<input
type="text"
id="memberIdInput"
placeholder="Enter your IEEE Member ID"
class="input input-bordered w-full"
/>
</div>
<!-- Resume Upload -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Resume</span>
<span class="label-text-alt text-base-content/70"
>Upload your latest resume</span
>
</label>
<div class="space-y-4">
<!-- Current Resume Display -->
<div id="resumeDisplay" class="hidden">
<div
class="flex items-center gap-2 p-3 bg-base-100 rounded-lg border border-base-300"
>
<div class="flex-1 flex items-center gap-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
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-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"
clip-rule="evenodd"></path>
</svg>
<div class="flex-1 min-w-0">
<p
id="currentResume"
class="text-sm font-medium truncate"
>
No resume uploaded
</p>
<p class="text-xs text-base-content/70">
PDF, DOC, or DOCX
</p>
</div>
</div>
<div class="flex-none flex gap-2">
<button id="previewResume" class="btn btn-sm btn-ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
<path
fill-rule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clip-rule="evenodd"></path>
</svg>
Preview
</button>
</div>
</div>
</div>
<!-- Upload Input -->
<div class="flex items-center gap-2">
<input
type="file"
id="resumeUpload"
accept=".pdf,.doc,.docx"
class="file-input file-input-bordered file-input-primary w-full"
/>
</div>
<!-- Upload Status -->
<div class="text-sm">
<span id="uploadStatus" class="label-text-alt"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// TypeScript declarations
declare global {
interface Window {
showEventFiles: (eventId: string) => Promise<void>;
}
}
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog";
import { FileManager } from "../pocketbase/FileManager";
import { Get } from "../pocketbase/Get";
const auth = Authentication.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
const fileManager = FileManager.getInstance();
const get = Get.getInstance();
// Get form elements
const memberIdInput = document.getElementById(
"memberIdInput",
) as HTMLInputElement;
const majorSelect = document.getElementById(
"majorSelect",
) as HTMLSelectElement;
const gradYearSelect = document.getElementById(
"gradYearSelect",
) as HTMLSelectElement;
const resumeUpload = document.getElementById(
"resumeUpload",
) as HTMLInputElement;
const currentResume = document.getElementById("currentResume");
const uploadStatus = document.getElementById("uploadStatus");
const saveSettings = document.getElementById("saveSettings");
// Add resume preview functionality
const resumeDisplay = document.getElementById("resumeDisplay");
const previewResume = document.getElementById("previewResume");
// Helper function to get file type
const getFileType = (fileName: string): string => {
const extension = fileName.split(".").pop()?.toLowerCase() || "";
const mimeTypes: Record<string, string> = {
pdf: "application/pdf",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
};
return mimeTypes[extension] || "application/octet-stream";
};
// Load current user data
const loadUserData = async () => {
if (auth.isAuthenticated()) {
const user = auth.getCurrentUser();
if (user) {
try {
// Get full user data with auto-cancellation disabled
const userData = await get.getOne("users", user.id, {
disableAutoCancellation: true,
});
console.log("User major from server:", userData.major);
if (memberIdInput) memberIdInput.value = userData.member_id || "";
if (majorSelect) {
const serverMajor = userData.major?.trim();
console.log("Setting major select to:", serverMajor);
// Get all available options with their normalized values
const availableOptions = Array.from(majorSelect.options).map(
(opt) => ({
element: opt,
normalizedValue: opt.value.trim(),
displayValue: opt.textContent?.trim() || opt.value.trim(),
}),
);
console.log(
"Available options:",
availableOptions.map((o) => o.normalizedValue),
);
// First try exact match
let matchingOption = availableOptions.find(
(opt) => opt.normalizedValue === serverMajor,
);
// If no exact match, try case-insensitive match
if (!matchingOption && serverMajor) {
const serverMajorLower = serverMajor.toLowerCase();
matchingOption = availableOptions.find(
(opt) => opt.normalizedValue.toLowerCase() === serverMajorLower,
);
}
// If still no match, try partial match
if (!matchingOption && serverMajor) {
const serverMajorLower = serverMajor.toLowerCase();
matchingOption = availableOptions.find(
(opt) =>
opt.normalizedValue
.toLowerCase()
.includes(serverMajorLower) ||
serverMajorLower.includes(opt.normalizedValue.toLowerCase()),
);
}
if (matchingOption) {
console.log(
"Found matching major:",
matchingOption.normalizedValue,
);
majorSelect.value = matchingOption.normalizedValue;
} else {
console.log("No matching major found for:", serverMajor);
majorSelect.value = "";
}
}
if (gradYearSelect)
gradYearSelect.value = userData.graduation_year || "";
// Update resume display
if (currentResume && resumeDisplay) {
if (userData.resume) {
const fileName = userData.resume.toString();
currentResume.textContent = fileName || "Resume uploaded";
resumeDisplay.classList.remove("hidden");
// Get the file URL from PocketBase
const resumeUrl = fileManager.getFileUrl(
"users",
userData.id,
fileName,
);
// Update preview button to use new modal
if (previewResume) {
previewResume.onclick = () => {
// Dispatch custom event for FileViewerModal
const showFileViewerEvent = new CustomEvent(
"showFileViewer",
{
detail: {
files: {
url: resumeUrl,
name: fileName,
type: getFileType(fileName),
},
},
},
);
window.dispatchEvent(showFileViewerEvent);
};
}
} else {
currentResume.textContent = "No resume uploaded";
resumeDisplay.classList.add("hidden");
}
}
} catch (err) {
console.error("Failed to load user data:", err);
await logger.send(
"error",
"profile settings",
`Failed to load user data: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
}
}
};
// Handle resume upload
if (resumeUpload && uploadStatus) {
resumeUpload.addEventListener("change", async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
uploadStatus.textContent = "Uploading...";
try {
const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated");
await fileManager.uploadFile("users", user.id, "resume", file);
uploadStatus.textContent = "Resume uploaded successfully";
// Refresh the user data to show the new resume
await loadUserData();
// Log successful resume upload
await logger.send(
"update",
"resume upload",
`Successfully uploaded resume: ${file.name}`,
);
} catch (err) {
console.error("Resume upload error:", err);
uploadStatus.textContent = "Failed to upload resume";
// Log resume upload error
await logger.send(
"error",
"resume upload",
`Failed to upload resume: ${file.name}. Error: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
}
});
}
// Handle save settings
if (saveSettings) {
saveSettings.addEventListener("click", async () => {
const button = saveSettings as HTMLButtonElement;
button.classList.add("loading");
try {
const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated");
// Get current user data for comparison
const currentUserData = await get.getOne("users", user.id);
const oldData = {
major: currentUserData.major || null,
graduation_year: currentUserData.graduation_year || null,
member_id: currentUserData.member_id || null,
};
const newData = {
major: majorSelect?.value || null,
graduation_year: gradYearSelect?.value
? parseInt(gradYearSelect.value)
: null,
member_id: memberIdInput?.value || null,
} as const;
await update.updateFields("users", user.id, newData);
// Create detailed log message of changes
const changes = [];
if (oldData.major !== newData.major) {
changes.push(
`Major: "${oldData.major || "none"}" → "${newData.major || "none"}"`,
);
}
if (oldData.graduation_year !== newData.graduation_year) {
changes.push(
`Graduation Year: "${oldData.graduation_year || "none"}" → "${newData.graduation_year || "none"}"`,
);
}
if (oldData.member_id !== newData.member_id) {
changes.push(
`IEEE Member ID: "${oldData.member_id || "none"}" → "${newData.member_id || "none"}"`,
);
}
if (changes.length > 0) {
await logger.send(
"update",
"profile settings",
`Updated profile settings:\n${changes.join("\n")}`,
);
}
// Show success toast
const toast = document.createElement("div");
toast.className =
"alert alert-success fixed bottom-4 right-4 w-auto z-50";
toast.innerHTML = `
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>Settings saved successfully!</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
// Refresh the form data
await loadUserData();
// Ensure settings view stays visible
const defaultView = document.getElementById("defaultView");
const settingsView = document.getElementById("settingsView");
if (defaultView && settingsView) {
defaultView.classList.add("hidden");
settingsView.classList.remove("hidden");
}
} catch (err) {
console.error("Failed to save settings:", err);
// Log the settings update error with details
const errorDetails = {
attempted_major: majorSelect?.value || "none",
attempted_graduation_year: gradYearSelect?.value || "none",
attempted_member_id: memberIdInput?.value || "none",
error_message: err instanceof Error ? err.message : "Unknown error",
};
await logger.send(
"error",
"profile settings",
`Failed to update profile settings:\nAttempted changes:\n` +
`Major: ${errorDetails.attempted_major}\n` +
`Graduation Year: ${errorDetails.attempted_graduation_year}\n` +
`IEEE Member ID: ${errorDetails.attempted_member_id}\n` +
`Error: ${errorDetails.error_message}`,
);
// Show error toast
const toast = document.createElement("div");
toast.className =
"alert alert-error fixed bottom-4 right-4 w-auto z-50";
toast.innerHTML = `
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span>Failed to save settings. Please try again.</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
} finally {
button.classList.remove("loading");
}
});
}
// Load initial data
await loadUserData();
// Update when auth state changes
auth.onAuthStateChange(async () => {
await loadUserData();
});
</script>
<style>
.hidden {
display: none !important;
}
</style>