major rework

- Separated the components for pocketbase
- Removed the entire profile page temporarily and only added back basic user profile
- Fixed many components dependencies
This commit is contained in:
chark1es 2025-02-03 02:45:35 -08:00
parent 172c56e023
commit e57f76bd4c
19 changed files with 1563 additions and 2468 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,23 @@
import PocketBase from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
import { SendLog } from "./SendLog";
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
import textConfig from "../../config/text.yml?raw";
import { SendLog } from "../pocketbase/SendLog";
// Configuration type definitions
interface Config {
api: {
baseUrl: string;
};
ui: {
messages: {
event: {
checkIn: {
checking: string;
success: string;
error: string;
invalid: string;
expired: string;
alreadyCheckedIn: string;
messageTimeout: number;
};
};
oauth2: {
redirectPath: string;
providerName: string;
};
};
}
// Parse YAML configuration with type
const config = yaml.load(configYaml) as Config;
// Parse YAML configuration
const config = yaml.load(pocketbaseConfig) as Config;
const text = yaml.load(textConfig) as any;
interface AuthElements {
eventCodeInput: HTMLInputElement;
@ -42,7 +33,7 @@ export class EventCheckIn {
constructor() {
this.pb = new PocketBase(config.api.baseUrl);
this.elements = this.getElements();
this.logger = new SendLog();
this.logger = SendLog.getInstance();
// Add event listener for the check-in button
this.elements.checkInButton.addEventListener("click", () => this.handleCheckIn());
@ -90,7 +81,7 @@ export class EventCheckIn {
if (events.length === 0) {
return {
isValid: false,
message: `Event code "${code}" does not match any active events.`
message: text.ui.messages.event.checkIn.invalid
};
}
@ -104,7 +95,7 @@ export class EventCheckIn {
return {
isValid: false,
event,
message: `Event "${event.event_name}" check-in is not open yet. Check-in opens at ${startDate.toLocaleString()}.`
message: text.ui.messages.event.checkIn.expired
};
}
@ -112,7 +103,7 @@ export class EventCheckIn {
return {
isValid: false,
event,
message: `Event "${event.event_name}" check-in has closed. Check-in closed at ${endDate.toLocaleString()}.`
message: text.ui.messages.event.checkIn.expired
};
}
@ -121,7 +112,7 @@ export class EventCheckIn {
console.error('Failed to validate event code:', err);
return {
isValid: false,
message: `Failed to validate event code "${code}". Error: ${err instanceof Error ? err.message : "Unknown error"}`
message: text.ui.messages.event.checkIn.error
};
}
}
@ -131,17 +122,19 @@ export class EventCheckIn {
const eventCode = eventCodeInput.value.trim();
if (!eventCode) {
this.showStatus("Please enter an event code", "error");
this.showStatus(text.ui.messages.event.checkIn.invalid, "error");
return;
}
let validation: { isValid: boolean; event?: any; message?: string } | undefined;
try {
this.showStatus(config.ui.messages.event.checkIn.checking, "info");
this.showStatus(text.ui.messages.event.checkIn.checking, "info");
// Get current user
const user = this.pb.authStore.model;
if (!user) {
this.showStatus("Please sign in to check in to events", "error");
this.showStatus(text.ui.messages.auth.notSignedIn, "error");
await this.logger.send(
"error",
"event check in",
@ -151,9 +144,9 @@ export class EventCheckIn {
}
// Validate event code and check time window
const validation = await this.validateEventCode(eventCode);
validation = await this.validateEventCode(eventCode);
if (!validation.isValid) {
this.showStatus(validation.message || "Invalid event code.", "error");
this.showStatus(validation.message || text.ui.messages.event.checkIn.invalid, "error");
await this.logger.send(
"error",
"event check in",
@ -191,7 +184,7 @@ export class EventCheckIn {
const isAlreadyCheckedIn = eventsAttended.includes(event.event_id);
if (isAlreadyCheckedIn) {
this.showStatus(`You have already checked in to ${event.event_name}`, "info");
this.showStatus(text.ui.messages.event.checkIn.alreadyCheckedIn, "info");
await this.logger.send(
"error",
"event check in",
@ -232,10 +225,7 @@ export class EventCheckIn {
}
// Show success message with points
this.showStatus(
`Successfully checked in to ${event.event_name}! You earned ${pointsToAdd} points!`,
"success"
);
this.showStatus(text.ui.messages.event.checkIn.success, "success");
eventCodeInput.value = ""; // Clear input
// Log the successful check-in
@ -268,14 +258,22 @@ export class EventCheckIn {
} catch (err) {
console.error("Check-in error:", err);
this.showStatus(config.ui.messages.event.checkIn.error, "error");
this.showStatus(text.ui.messages.event.checkIn.error, "error");
// Log any errors that occur during check-in
await this.logger.send(
"error",
"event check in",
`Failed to check in to event ${event.id}: ${err instanceof Error ? err.message : "Unknown error"}`
);
if (validation?.event) {
await this.logger.send(
"error",
"event check in",
`Failed to check in to event ${validation.event.id}: ${err instanceof Error ? err.message : "Unknown error"}`
);
} else {
await this.logger.send(
"error",
"event check in",
`Failed to check in: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
}
@ -300,7 +298,7 @@ export class EventCheckIn {
if (type !== "info") {
setTimeout(() => {
checkInStatus.textContent = "";
}, config.ui.messages.event.checkIn.messageTimeout);
}, text.ui.messages.event.checkIn.messageTimeout);
}
}

View file

@ -1,73 +0,0 @@
import PocketBase from "pocketbase";
import { StoreAuth } from "./StoreAuth";
// Log interface
interface LogData {
user_id: string;
type: string; // Standard types: "error", "update", "delete", "create", "login", "logout"
part: string; // The specific part/section being logged (can be multiple words, e.g., "profile settings", "resume upload")
message: string;
}
export class SendLog {
private pb: PocketBase;
constructor() {
// Use the same PocketBase instance as StoreAuth to maintain authentication
const auth = new StoreAuth();
this.pb = auth["pb"];
}
/**
* Gets the current authenticated user's ID
* @returns The user ID or null if not authenticated
*/
private getCurrentUserId(): string | null {
return this.pb.authStore.model?.id || null;
}
/**
* Sends a log entry to PocketBase
* @param type The type of log entry. Standard types:
* - "error": For error conditions
* - "update": For successful updates/uploads
* - "delete": For deletion operations
* - "create": For creation operations
* - "login": For login events
* - "logout": For logout events
* @param part The specific part/section being logged. Can be multiple words:
* - "profile settings": Profile settings changes
* - "resume upload": Resume file operations
* - "notification settings": Notification preference changes
* - "user authentication": Auth-related actions
* - "event check in": Event attendance tracking
* - "loyalty points": Points updates from events/activities
* - "event management": Event creation/modification
* - "event attendance": Overall event attendance status
* @param message The log message
* @param overrideUserId Optional user ID to override the current user (for admin/system logs)
* @returns Promise that resolves when the log is created
*/
public async send(type: string, part: string, message: string, overrideUserId?: string) {
try {
const userId = overrideUserId || this.getCurrentUserId();
if (!userId) {
throw new Error("No user ID available. User must be authenticated to create logs.");
}
const logData: LogData = {
user_id: userId,
type,
part,
message
};
// Create the log entry in PocketBase
await this.pb.collection("logs").create(logData);
} catch (error) {
console.error("Failed to send log:", error);
throw error;
}
}
}

View file

@ -1,426 +0,0 @@
import PocketBase from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
// Configuration type definitions
interface Role {
name: string;
badge: string;
permissions: string[];
}
interface Config {
api: {
baseUrl: string;
oauth2: {
redirectPath: string;
providerName: string;
};
};
roles: {
administrator: Role;
officer: Role;
sponsor: Role;
member: Role;
};
resume: {
allowedTypes: string[];
maxSize: number;
viewer: {
width: string;
maxWidth: string;
height: string;
};
};
ui: {
transitions: {
fadeDelay: number;
};
messages: {
memberId: {
saving: string;
success: string;
error: string;
messageTimeout: number;
};
resume: {
uploading: string;
success: string;
error: string;
deleting: string;
deleteSuccess: string;
deleteError: string;
messageTimeout: number;
};
auth: {
loginError: string;
notSignedIn: string;
notVerified: string;
notProvided: string;
notAvailable: string;
never: string;
};
};
defaults: {
pageSize: number;
sortField: string;
};
};
autoDetection: {
officer: {
emailDomain: string;
};
};
}
// Parse YAML configuration with type
const config = yaml.load(configYaml) as Config;
interface AuthElements {
loginButton: HTMLButtonElement;
logoutButton: HTMLButtonElement;
userInfo: HTMLDivElement;
userName: HTMLParagraphElement;
userEmail: HTMLParagraphElement;
memberStatus: HTMLDivElement;
lastLogin: HTMLParagraphElement;
storeContent: HTMLDivElement;
officerViewToggle: HTMLDivElement;
officerViewCheckbox: HTMLInputElement;
officerContent: HTMLDivElement;
profileEditor: HTMLDialogElement;
editorName: HTMLInputElement;
editorEmail: HTMLInputElement;
editorPoints: HTMLInputElement;
saveProfileButton: HTMLButtonElement;
sponsorViewToggle: HTMLDivElement;
}
export class StoreAuth {
private pb: PocketBase;
private elements: AuthElements & { loadingSkeleton: HTMLDivElement };
private cachedUsers: any[] = [];
private config = config;
constructor() {
this.pb = new PocketBase(this.config.api.baseUrl);
this.elements = this.getElements();
this.init();
}
// Public method to get auth state
public getAuthState() {
return {
isValid: this.pb.authStore.isValid,
model: this.pb.authStore.model
};
}
// Public method to handle login
public async handleLogin() {
try {
const authMethods = await this.pb.collection("users").listAuthMethods();
const oidcProvider = authMethods.oauth2?.providers?.find(
(p: { name: string }) => p.name === this.config.api.oauth2.providerName
);
if (!oidcProvider) {
throw new Error("OIDC provider not found");
}
localStorage.setItem("provider", JSON.stringify(oidcProvider));
const redirectUrl = window.location.origin + this.config.api.oauth2.redirectPath;
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
window.location.href = authUrl;
} catch (err) {
console.error("Authentication error:", err);
this.elements.userEmail.textContent = this.config.ui.messages.auth.loginError;
this.elements.userName.textContent = "Error";
throw err;
}
}
// Public method to update profile settings
public async updateProfileSettings(data: {
major?: string | null;
graduation_year?: number | null;
member_id?: string | null;
}) {
const user = this.pb.authStore.model;
if (!user?.id) {
throw new Error("User ID not found");
}
return await this.pb.collection("users").update(user.id, data);
}
/**
* Handles uploading a resume file for the current user
* @param file The resume file to upload
* @returns Promise that resolves when the resume is uploaded
*/
public async handleResumeUpload(file: File) {
const user = this.pb.authStore.model;
if (!user?.id) {
throw new Error("User ID not found");
}
// Validate file type
const allowedTypes = ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"];
if (!allowedTypes.includes(file.type)) {
throw new Error("Invalid file type. Please upload a PDF or Word document.");
}
// Validate file size (5MB max)
const maxSize = 5 * 1024 * 1024; // 5MB in bytes
if (file.size > maxSize) {
throw new Error("File size too large. Maximum size is 5MB.");
}
// Create form data with the file
const formData = new FormData();
formData.append("resume", file);
// Update the user record with the new resume
return await this.pb.collection("users").update(user.id, formData);
}
private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } {
// Get all required elements
const loginButton = document.getElementById("contentLoginButton") as HTMLButtonElement;
const logoutButton = document.getElementById("contentLogoutButton") as HTMLButtonElement;
const userInfo = document.getElementById("userInfo") as HTMLDivElement;
const loadingSkeleton = document.getElementById("loadingSkeleton") as HTMLDivElement;
const userName = document.getElementById("userName") as HTMLParagraphElement;
const userEmail = document.getElementById("userEmail") as HTMLParagraphElement;
const memberStatus = document.getElementById("memberStatus") as HTMLDivElement;
const lastLogin = document.getElementById("lastLogin") as HTMLParagraphElement;
const storeContent = document.getElementById("storeContent") as HTMLDivElement;
const officerViewToggle = document.getElementById("officerViewToggle") as HTMLDivElement;
const officerViewCheckbox = officerViewToggle?.querySelector('input[type="checkbox"]') as HTMLInputElement;
const officerContent = document.getElementById("officerContent") as HTMLDivElement;
const profileEditor = document.getElementById("profileEditor") as HTMLDialogElement;
const editorName = document.getElementById("editorName") as HTMLInputElement;
const editorEmail = document.getElementById("editorEmail") as HTMLInputElement;
const editorPoints = document.getElementById("editorPoints") as HTMLInputElement;
const saveProfileButton = document.getElementById("saveProfileButton") as HTMLButtonElement;
const sponsorViewToggle = document.getElementById("sponsorViewToggle") as HTMLDivElement;
// Add CSS for loading state transitions
const style = document.createElement("style");
style.textContent = `
.loading-state {
opacity: 0.5;
transition: opacity 0.3s ease-in-out;
}
.content-ready {
opacity: 1;
}
`;
document.head.appendChild(style);
return {
loginButton,
logoutButton,
userInfo,
loadingSkeleton,
userName,
userEmail,
memberStatus,
lastLogin,
storeContent,
officerViewToggle,
officerViewCheckbox,
officerContent,
profileEditor,
editorName,
editorEmail,
editorPoints,
saveProfileButton,
sponsorViewToggle
};
}
private async init() {
// Initial UI update with loading state
await this.updateUI();
// Setup event listeners
this.elements.loginButton.addEventListener("click", () => this.handleLogin());
this.elements.logoutButton.addEventListener("click", () => this.handleLogout());
// Listen for auth state changes
this.pb.authStore.onChange(() => {
console.log("Auth state changed. IsValid:", this.pb.authStore.isValid);
this.updateUI();
});
// Profile editor event listeners
const { profileEditor, saveProfileButton } = this.elements;
// Close dialog when clicking outside
profileEditor.addEventListener("click", (e) => {
if (e.target === profileEditor) {
profileEditor.close();
}
});
// Save profile button
saveProfileButton.addEventListener("click", (e) => {
e.preventDefault();
this.handleProfileSave();
});
}
private async updateUI() {
const {
loginButton,
logoutButton,
userInfo,
userName,
userEmail,
memberStatus,
lastLogin,
loadingSkeleton,
officerViewToggle,
sponsorViewToggle,
} = this.elements;
// Get all login and logout buttons using classes
const allLoginButtons = document.querySelectorAll('.login-button');
const allLogoutButtons = document.querySelectorAll('.logout-button');
// Hide all buttons initially
allLoginButtons.forEach(btn => btn.classList.add("hidden"));
allLogoutButtons.forEach(btn => btn.classList.add("hidden"));
if (this.pb.authStore.isValid && this.pb.authStore.model) {
// Show logout buttons for authenticated users
allLogoutButtons.forEach(btn => btn.classList.remove("hidden"));
const user = this.pb.authStore.model;
const isSponsor = user.member_type === this.config.roles.sponsor.name;
const isOfficer = [
this.config.roles.officer.name,
this.config.roles.administrator.name
].includes(user.member_type || "");
userName.textContent = user.name || this.config.ui.messages.auth.notProvided;
userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable;
// Update member status badge
if (user.member_type) {
memberStatus.textContent = user.member_type;
memberStatus.classList.remove("badge-neutral");
if (isOfficer) {
memberStatus.classList.add("badge-primary");
} else if (isSponsor) {
memberStatus.classList.add("badge-warning");
} else {
memberStatus.classList.add("badge-info");
}
} else {
memberStatus.textContent = this.config.ui.messages.auth.notVerified;
memberStatus.classList.remove("badge-info", "badge-warning", "badge-primary");
memberStatus.classList.add("badge-neutral");
}
// Update last login
lastLogin.textContent = user.last_login
? new Date(user.last_login).toLocaleString()
: this.config.ui.messages.auth.never;
// Show/hide view toggles and update view visibility
officerViewToggle.style.display = isOfficer ? "block" : "none";
sponsorViewToggle.style.display = isSponsor ? "block" : "none";
// If not an officer, ensure default view is shown and officer view is hidden
if (!isOfficer) {
const defaultView = document.getElementById("defaultView");
const officerView = document.getElementById("officerView");
const mainTabs = document.querySelector(".tabs.tabs-boxed");
const officerContent = document.getElementById("officerContent");
const settingsView = document.getElementById("settingsView");
if (defaultView && officerView && mainTabs && officerContent && settingsView) {
// Show default view and its tabs
defaultView.classList.remove("hidden");
mainTabs.classList.remove("hidden");
// Hide officer view
officerView.classList.add("hidden");
officerContent.classList.add("hidden");
// Also uncheck the toggle if it exists
const officerViewCheckbox = officerViewToggle.querySelector('input[type="checkbox"]') as HTMLInputElement;
if (officerViewCheckbox) {
officerViewCheckbox.checked = false;
}
}
}
// After everything is updated, show the content
loadingSkeleton.style.display = "none";
userInfo.classList.remove("hidden");
setTimeout(() => {
userInfo.style.opacity = "1";
}, 50);
} else {
// Show login buttons for unauthenticated users
allLoginButtons.forEach(btn => btn.classList.remove("hidden"));
// Reset all fields to default state
userName.textContent = this.config.ui.messages.auth.notSignedIn;
userEmail.textContent = this.config.ui.messages.auth.notSignedIn;
memberStatus.textContent = this.config.ui.messages.auth.notVerified;
memberStatus.classList.remove("badge-info", "badge-warning", "badge-primary");
memberStatus.classList.add("badge-neutral");
lastLogin.textContent = this.config.ui.messages.auth.never;
// Hide view toggles
officerViewToggle.style.display = "none";
sponsorViewToggle.style.display = "none";
// Show content
loadingSkeleton.style.display = "none";
userInfo.classList.remove("hidden");
setTimeout(() => {
userInfo.style.opacity = "1";
}, 50);
}
}
private handleLogout() {
this.pb.authStore.clear();
this.cachedUsers = [];
this.updateUI();
}
private async handleProfileSave() {
const {
profileEditor,
editorName,
editorEmail,
editorPoints,
saveProfileButton,
} = this.elements;
const userId = saveProfileButton.dataset.userId;
if (!userId) {
console.error("No user ID found for saving");
return;
}
try {
const formData = new FormData();
formData.append("name", editorName.value);
formData.append("email", editorEmail.value);
formData.append("points", editorPoints.value);
await this.pb.collection("users").update(userId, formData);
profileEditor.close();
this.updateUI();
} catch (err) {
console.error("Failed to save user profile:", err);
}
}
}

View file

@ -310,188 +310,260 @@
</style>
<script>
import { StoreAuth } from "./StoreAuth";
import { EventCheckIn } from "./EventCheckIn";
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");
const officerViewToggle = document.getElementById("officerViewToggle");
const sponsorViewToggle = document.getElementById("sponsorViewToggle");
// 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;
}
// Initialize auth and event check-in
document.addEventListener("DOMContentLoaded", () => {
try {
const auth = new StoreAuth();
new EventCheckIn();
// Get full user data
const userData = await get.getOne("users", user.id);
// Add officer view toggle handler
const officerViewToggle =
document.getElementById("officerViewToggle");
const officerViewCheckbox = officerViewToggle?.querySelector(
'input[type="checkbox"]'
) as HTMLInputElement;
// 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 || "?");
if (officerViewCheckbox) {
officerViewCheckbox.addEventListener("change", () => {
const defaultView = document.getElementById("defaultView");
const officerView = document.getElementById("officerView");
const mainTabs = document.querySelector(".tabs.tabs-boxed");
const officerContent =
document.getElementById("officerContent");
const settingsView =
document.getElementById("settingsView");
// Update member status based on member_type and email domain
if (memberStatus) {
let memberType = userData.member_type || "IEEE Member";
if (
defaultView &&
officerView &&
mainTabs &&
officerContent &&
settingsView
) {
if (officerViewCheckbox.checked) {
// Hide default view, settings, and tabs
defaultView.classList.add("hidden");
settingsView.classList.add("hidden");
mainTabs.classList.add("hidden");
// Show officer view
officerView.classList.remove("hidden");
officerContent.classList.remove("hidden");
} else {
// Show default view and tabs
defaultView.classList.remove("hidden");
mainTabs.classList.remove("hidden");
// Hide officer view
officerView.classList.add("hidden");
officerContent.classList.add("hidden");
// 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}`;
}
// Add login button event listener
const loginButtons = document.querySelectorAll(".login-button");
loginButtons.forEach((button) => {
button.addEventListener("click", () => {
const loadingSkeleton =
document.getElementById("loadingSkeleton");
const userInfo = document.getElementById("userInfo");
// Update timestamps
if (lastLogin)
lastLogin.textContent = formatDate(userData.last_login);
if (memberSince)
memberSince.textContent = formatDate(userData.created);
// Show loading state
if (loadingSkeleton)
loadingSkeleton.style.display = "block";
if (userInfo) userInfo.classList.add("hidden");
// Show logout button
if (contentLoginButton) contentLoginButton.classList.add("hidden");
if (contentLogoutButton)
contentLogoutButton.classList.remove("hidden");
// Call the handleLogin method
auth.handleLogin().catch((error) => {
// Update view toggles based on member type
const isOfficerOrHigher = [
"IEEE Officer",
"IEEE Executive",
"IEEE Administrator",
].includes(userData.member_type);
if (officerViewToggle) {
officerViewToggle.classList.toggle(
"hidden",
!isOfficerOrHigher
);
}
if (sponsorViewToggle) {
sponsorViewToggle.classList.toggle(
"hidden",
userData.member_type !== "IEEE Sponsor"
);
}
// 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);
// Show error message
await logger.send(
"error",
"profile view",
`Login failed: ${error instanceof Error ? error.message : "Unknown error"}`
);
if (loadingSkeleton)
loadingSkeleton.style.display = "none";
if (userInfo) {
const errorMessage = document.createElement("div");
errorMessage.className = "alert alert-error";
errorMessage.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="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<span>Failed to sign in. Please try again.</span>
</div>
`;
userInfo.innerHTML = "";
userInfo.appendChild(errorMessage);
userInfo.classList.remove("hidden");
userInfo.style.opacity = "1";
}
});
}
});
});
}
// Add error handling for failed initialization
window.addEventListener("unhandledrejection", (event) => {
console.error("Profile loading error:", event.reason);
const userInfo = document.getElementById("userInfo");
const loadingSkeleton =
document.getElementById("loadingSkeleton");
const memberDetails = document.getElementById("memberDetails");
const errorMessage = document.createElement("div");
errorMessage.className = "alert alert-error";
errorMessage.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="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<span>Failed to load profile. Please refresh the page.</span>
</div>
`;
if (loadingSkeleton) loadingSkeleton.style.display = "none";
if (userInfo) {
if (memberDetails) memberDetails.style.display = "none";
userInfo.appendChild(errorMessage);
userInfo.classList.remove("hidden");
userInfo.style.opacity = "1";
}
});
// Check auth state and update UI accordingly
const authState = auth.getAuthState();
if (!authState.isValid || !authState.model) {
const userInfo = document.getElementById("userInfo");
const loadingSkeleton =
document.getElementById("loadingSkeleton");
const memberDetails = document.getElementById("memberDetails");
const contentLoginButton =
document.getElementById("contentLoginButton");
if (loadingSkeleton) loadingSkeleton.style.display = "none";
if (memberDetails) memberDetails.style.display = "none";
if (contentLoginButton)
contentLoginButton.classList.remove("hidden");
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"}`
);
}
});
// Add user initials generation
const userNameElement = document.getElementById("userName");
const userInitialsElement = document.getElementById("userInitials");
const memberSinceElement = document.getElementById("memberSince");
if (userNameElement && userInitialsElement && memberSinceElement) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "characterData" ||
mutation.type === "childList"
) {
const name = userNameElement.textContent || "";
if (name && name !== "Not signed in") {
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
userInitialsElement.textContent = initials;
} else {
userInitialsElement.textContent = "?";
}
}
});
});
observer.observe(userNameElement, {
characterData: true,
childList: true,
subtree: true,
});
}
// Set member since date
if (memberSinceElement) {
const created = new Date(); // Replace with actual user creation date
memberSinceElement.textContent = created.toLocaleDateString();
}
</script>

View file

@ -0,0 +1,119 @@
import PocketBase from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../config/pocketbaseConfig.yml?raw";
// Configuration type definitions
interface Config {
api: {
baseUrl: string;
oauth2: {
redirectPath: string;
providerName: string;
};
};
}
// Parse YAML configuration
const config = yaml.load(configYaml) as Config;
export class Authentication {
private pb: PocketBase;
private static instance: Authentication;
private authChangeCallbacks: ((isValid: boolean) => void)[] = [];
private constructor() {
// Use the baseUrl from the config file
this.pb = new PocketBase(config.api.baseUrl);
// Listen for auth state changes
this.pb.authStore.onChange(() => {
this.notifyAuthChange();
});
}
/**
* Get the singleton instance of Authentication
*/
public static getInstance(): Authentication {
if (!Authentication.instance) {
Authentication.instance = new Authentication();
}
return Authentication.instance;
}
/**
* Get the PocketBase instance
*/
public getPocketBase(): PocketBase {
return this.pb;
}
/**
* Handle user login through OAuth2
*/
public async login(): Promise<void> {
try {
const authMethods = await this.pb.collection("users").listAuthMethods();
const oidcProvider = authMethods.oauth2?.providers?.find(
(p: { name: string }) => p.name === config.api.oauth2.providerName
);
if (!oidcProvider) {
throw new Error("OIDC provider not found");
}
localStorage.setItem("provider", JSON.stringify(oidcProvider));
const redirectUrl = window.location.origin + config.api.oauth2.redirectPath;
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
window.location.href = authUrl;
} catch (err) {
console.error("Authentication error:", err);
throw err;
}
}
/**
* Handle user logout
*/
public logout(): void {
this.pb.authStore.clear();
}
/**
* Check if user is currently authenticated
*/
public isAuthenticated(): boolean {
return this.pb.authStore.isValid;
}
/**
* Get current user model
*/
public getCurrentUser(): any {
return this.pb.authStore.model;
}
/**
* Subscribe to auth state changes
* @param callback Function to call when auth state changes
*/
public onAuthStateChange(callback: (isValid: boolean) => void): void {
this.authChangeCallbacks.push(callback);
}
/**
* Remove auth state change subscription
* @param callback Function to remove from subscribers
*/
public offAuthStateChange(callback: (isValid: boolean) => void): void {
this.authChangeCallbacks = this.authChangeCallbacks.filter(cb => cb !== callback);
}
/**
* Notify all subscribers of auth state change
*/
private notifyAuthChange(): void {
const isValid = this.pb.authStore.isValid;
this.authChangeCallbacks.forEach(callback => callback(isValid));
}
}

View file

@ -0,0 +1,204 @@
import { Authentication } from "./Authentication";
// Base interface for PocketBase records
interface BaseRecord {
id: string;
[key: string]: any;
}
export class Get {
private auth: Authentication;
private static instance: Get;
private constructor() {
this.auth = Authentication.getInstance();
}
/**
* Get the singleton instance of Get
*/
public static getInstance(): Get {
if (!Get.instance) {
Get.instance = new Get();
}
return Get.instance;
}
/**
* Get a single record by ID
* @param collectionName The name of the collection
* @param recordId The ID of the record to retrieve
* @param fields Optional array of fields to select
* @returns The requested record
*/
public async getOne<T extends BaseRecord>(
collectionName: string,
recordId: string,
fields?: string[]
): Promise<T> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records");
}
try {
const pb = this.auth.getPocketBase();
const options = fields ? { fields: fields.join(",") } : undefined;
return await pb.collection(collectionName).getOne<T>(recordId, options);
} catch (err) {
console.error(`Failed to get record from ${collectionName}:`, err);
throw err;
}
}
/**
* Get multiple records by their IDs
* @param collectionName The name of the collection
* @param recordIds Array of record IDs to retrieve
* @param fields Optional array of fields to select
* @returns Array of requested records
*/
public async getMany<T extends BaseRecord>(
collectionName: string,
recordIds: string[],
fields?: string[]
): Promise<T[]> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records");
}
try {
const pb = this.auth.getPocketBase();
const filter = `id ?~ "${recordIds.join("|")}"`;
const options = {
filter,
...(fields && { fields: fields.join(",") })
};
const result = await pb.collection(collectionName).getFullList<T>(options);
// Sort results to match the order of requested IDs
const recordMap = new Map(result.map(record => [record.id, record]));
return recordIds.map(id => recordMap.get(id)).filter(Boolean) as T[];
} catch (err) {
console.error(`Failed to get records from ${collectionName}:`, err);
throw err;
}
}
/**
* Get records with pagination
* @param collectionName The name of the collection
* @param page Page number (1-based)
* @param perPage Number of items per page
* @param filter Optional filter string
* @param sort Optional sort string
* @param fields Optional array of fields to select
* @returns Paginated list of records
*/
public async getList<T extends BaseRecord>(
collectionName: string,
page: number = 1,
perPage: number = 20,
filter?: string,
sort?: string,
fields?: string[]
): Promise<{
page: number;
perPage: number;
totalItems: number;
totalPages: number;
items: T[];
}> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records");
}
try {
const pb = this.auth.getPocketBase();
const options = {
...(filter && { filter }),
...(sort && { sort }),
...(fields && { fields: fields.join(",") })
};
const result = await pb.collection(collectionName).getList<T>(page, perPage, options);
return {
page: result.page,
perPage: result.perPage,
totalItems: result.totalItems,
totalPages: result.totalPages,
items: result.items
};
} catch (err) {
console.error(`Failed to get list from ${collectionName}:`, err);
throw err;
}
}
/**
* Get all records from a collection
* @param collectionName The name of the collection
* @param filter Optional filter string
* @param sort Optional sort string
* @param fields Optional array of fields to select
* @returns Array of all matching records
*/
public async getAll<T extends BaseRecord>(
collectionName: string,
filter?: string,
sort?: string,
fields?: string[]
): Promise<T[]> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records");
}
try {
const pb = this.auth.getPocketBase();
const options = {
...(filter && { filter }),
...(sort && { sort }),
...(fields && { fields: fields.join(",") })
};
return await pb.collection(collectionName).getFullList<T>(options);
} catch (err) {
console.error(`Failed to get all records from ${collectionName}:`, err);
throw err;
}
}
/**
* Get the first record that matches a filter
* @param collectionName The name of the collection
* @param filter Filter string
* @param fields Optional array of fields to select
* @returns The first matching record or null if none found
*/
public async getFirst<T extends BaseRecord>(
collectionName: string,
filter: string,
fields?: string[]
): Promise<T | null> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve records");
}
try {
const pb = this.auth.getPocketBase();
const options = {
filter,
...(fields && { fields: fields.join(",") }),
sort: "created",
perPage: 1
};
const result = await pb.collection(collectionName).getList<T>(1, 1, options);
return result.items.length > 0 ? result.items[0] : null;
} catch (err) {
console.error(`Failed to get first record from ${collectionName}:`, err);
throw err;
}
}
}

View file

@ -0,0 +1,168 @@
import { Authentication } from "./Authentication";
// Log interface
interface LogData {
user_id: string;
type: string; // Standard types: "error", "update", "delete", "create", "login", "logout"
part: string; // The specific part/section being logged (can be multiple words, e.g., "profile settings", "resume upload")
message: string;
}
export class SendLog {
private auth: Authentication;
private static instance: SendLog;
private readonly COLLECTION_NAME = "logs"; // Make collection name a constant
private constructor() {
this.auth = Authentication.getInstance();
}
/**
* Get the singleton instance of SendLog
*/
public static getInstance(): SendLog {
if (!SendLog.instance) {
SendLog.instance = new SendLog();
}
return SendLog.instance;
}
/**
* Gets the current authenticated user's ID
* @returns The user ID or null if not authenticated
*/
private getCurrentUserId(): string | null {
const user = this.auth.getCurrentUser();
if (!user) {
console.debug("SendLog: No current user found");
return null;
}
console.debug("SendLog: Current user ID:", user.id);
return user.id;
}
/**
* Sends a log entry to PocketBase
* @param type The type of log entry
* @param part The specific part/section being logged
* @param message The log message
* @param overrideUserId Optional user ID to override the current user
* @returns Promise that resolves when the log is created
*/
public async send(type: string, part: string, message: string, overrideUserId?: string): Promise<void> {
try {
// Check authentication first
if (!this.auth.isAuthenticated()) {
console.error("SendLog: User not authenticated");
throw new Error("User must be authenticated to create logs");
}
// Get user ID
const userId = overrideUserId || this.getCurrentUserId();
if (!userId) {
console.error("SendLog: No user ID available");
throw new Error("No user ID available. User must be authenticated to create logs.");
}
// Prepare log data
const logData: LogData = {
user_id: userId,
type,
part,
message
};
console.debug("SendLog: Preparing to send log:", {
collection: this.COLLECTION_NAME,
data: logData,
authValid: this.auth.isAuthenticated(),
userId
});
// Get PocketBase instance
const pb = this.auth.getPocketBase();
// Create the log entry
await pb.collection(this.COLLECTION_NAME).create(logData);
console.debug("SendLog: Log created successfully");
} catch (error) {
// Enhanced error logging
if (error instanceof Error) {
console.error("SendLog: Failed to send log:", {
error: error.message,
stack: error.stack,
type,
part,
message
});
} else {
console.error("SendLog: Unknown error:", error);
}
throw error;
}
}
/**
* Get logs for a specific user
* @param userId The ID of the user to get logs for
* @param type Optional log type to filter by
* @param part Optional part/section to filter by
* @returns Array of log entries
*/
public async getUserLogs(userId: string, type?: string, part?: string): Promise<LogData[]> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve logs");
}
try {
let filter = `user_id = "${userId}"`;
if (type) filter += ` && type = "${type}"`;
if (part) filter += ` && part = "${part}"`;
const result = await this.auth.getPocketBase().collection(this.COLLECTION_NAME).getFullList<LogData>({
filter,
sort: "-created"
});
return result;
} catch (error) {
console.error("SendLog: Failed to get user logs:", error);
throw error;
}
}
/**
* Get recent logs for the current user
* @param limit Maximum number of logs to retrieve
* @param type Optional log type to filter by
* @param part Optional part/section to filter by
* @returns Array of recent log entries
*/
public async getRecentLogs(limit: number = 10, type?: string, part?: string): Promise<LogData[]> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to retrieve logs");
}
try {
const userId = this.getCurrentUserId();
if (!userId) {
throw new Error("No user ID available");
}
let filter = `user_id = "${userId}"`;
if (type) filter += ` && type = "${type}"`;
if (part) filter += ` && part = "${part}"`;
const result = await this.auth.getPocketBase().collection(this.COLLECTION_NAME).getList<LogData>(1, limit, {
filter,
sort: "-created"
});
return result.items;
} catch (error) {
console.error("SendLog: Failed to get recent logs:", error);
throw error;
}
}
}

View file

@ -0,0 +1,134 @@
import { Authentication } from "./Authentication";
export class Update {
private auth: Authentication;
private static instance: Update;
private constructor() {
this.auth = Authentication.getInstance();
}
/**
* Get the singleton instance of Update
*/
public static getInstance(): Update {
if (!Update.instance) {
Update.instance = new Update();
}
return Update.instance;
}
/**
* Update a single field in a record
* @param collectionName The name of the collection
* @param recordId The ID of the record to update
* @param field The field to update
* @param value The new value for the field
* @returns The updated record
*/
public async updateField<T = any>(
collectionName: string,
recordId: string,
field: string,
value: any
): Promise<T> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to update records");
}
try {
const pb = this.auth.getPocketBase();
const data = { [field]: value };
return await pb.collection(collectionName).update<T>(recordId, data);
} catch (err) {
console.error(`Failed to update ${field} in ${collectionName}:`, err);
throw err;
}
}
/**
* Update multiple fields in a record
* @param collectionName The name of the collection
* @param recordId The ID of the record to update
* @param updates Object containing field-value pairs to update
* @returns The updated record
*/
public async updateFields<T = any>(
collectionName: string,
recordId: string,
updates: Record<string, any>
): Promise<T> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to update records");
}
try {
const pb = this.auth.getPocketBase();
return await pb.collection(collectionName).update<T>(recordId, updates);
} catch (err) {
console.error(`Failed to update fields in ${collectionName}:`, err);
throw err;
}
}
/**
* Update a field for multiple records
* @param collectionName The name of the collection
* @param recordIds Array of record IDs to update
* @param field The field to update
* @param value The new value for the field
* @returns Array of updated records
*/
public async batchUpdateField<T = any>(
collectionName: string,
recordIds: string[],
field: string,
value: any
): Promise<T[]> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to update records");
}
try {
const pb = this.auth.getPocketBase();
const data = { [field]: value };
const updates = recordIds.map(id =>
pb.collection(collectionName).update<T>(id, data)
);
return await Promise.all(updates);
} catch (err) {
console.error(`Failed to batch update ${field} in ${collectionName}:`, err);
throw err;
}
}
/**
* Update multiple fields for multiple records
* @param collectionName The name of the collection
* @param updates Array of objects containing record ID and updates
* @returns Array of updated records
*/
public async batchUpdateFields<T = any>(
collectionName: string,
updates: Array<{ id: string; data: Record<string, any> }>
): Promise<T[]> {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to update records");
}
try {
const pb = this.auth.getPocketBase();
const updatePromises = updates.map(({ id, data }) =>
pb.collection(collectionName).update<T>(id, data)
);
return await Promise.all(updatePromises);
} catch (err) {
console.error(`Failed to batch update fields in ${collectionName}:`, err);
throw err;
}
}
}

View file

@ -181,25 +181,40 @@
</dialog>
<script>
import { StoreAuth } from "../auth/StoreAuth";
import { EventCheckIn } from "../auth/EventCheckIn";
import PocketBase from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
import type { RecordModel } from "pocketbase";
import { Authentication } from "../pocketbase/Authentication";
import { Get } from "../pocketbase/Get";
import { SendLog } from "../pocketbase/SendLog";
// Parse YAML configuration with type assertion
interface Config {
api: {
baseUrl: string;
};
}
const config = yaml.load(configYaml) as Config;
const pb = new PocketBase(config.api.baseUrl);
const auth = Authentication.getInstance();
const get = Get.getInstance();
const logger = SendLog.getInstance();
// Initialize auth and check-in
new StoreAuth();
new EventCheckIn();
// Initialize event check-in
document.addEventListener("DOMContentLoaded", async () => {
try {
// Get current user's events
if (auth.isAuthenticated()) {
const user = auth.getCurrentUser();
if (user) {
// Get user's events
const events = await get.getMany(
"events",
user.events_attended || []
);
// Update UI with events data
// ... rest of the code ...
}
}
} 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"}`
);
}
});
// Handle loading states
const eventCheckInSkeleton = document.getElementById(
@ -217,13 +232,13 @@
}
// Show content when auth state changes
pb.authStore.onChange(() => {
auth.onAuthStateChange(() => {
showEventCheckIn();
renderEvents();
});
// Show content on initial load if already authenticated
if (pb.authStore.isValid) {
if (auth.isAuthenticated()) {
showEventCheckIn();
}
@ -382,7 +397,7 @@
try {
// Get current user's attended events with safe parsing
const user = pb.authStore.model;
const user = auth.getCurrentUser();
let attendedEvents: string[] = [];
if (user?.events_attended) {
@ -400,9 +415,10 @@
}
// Fetch all events
const events = await pb.collection("events").getList(1, 50, {
sort: "start_date", // Sort by start date ascending for upcoming events
});
const events = await get.getMany(
"events",
user.events_attended || []
);
// Clear loading skeletons
eventsList.innerHTML = "";
@ -414,7 +430,7 @@
const upcomingEvents: Event[] = [];
const pastEvents: Event[] = [];
events.items.forEach((event) => {
events.forEach((event) => {
const typedEvent = event as Event;
const startDate = new Date(typedEvent.start_date);
const endDate = new Date(typedEvent.end_date);
@ -493,9 +509,7 @@
if (e instanceof CustomEvent && "eventId" in e.detail) {
(async () => {
try {
const event = await pb
.collection("events")
.getOne(e.detail.eventId);
const event = await get.getOne("events", e.detail.eventId);
const fileViewerContent =
document.getElementById("fileViewerContent");
const fileViewerTitle =
@ -519,7 +533,7 @@
fileViewerContent.innerHTML = event.files
.map((file) => {
const fileUrl = pb.files.getURL(event, file);
const fileUrl = get.getFileURL(event, file);
const fileName =
file.split("/").pop() || "File";
const fileExt =

View file

@ -1,9 +1,10 @@
---
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
import textConfig from "../../config/text.yml?raw";
const config = yaml.load(configYaml) as any;
const { editor_title, form } = config.ui.tables.events;
// Parse YAML configuration
const text = yaml.load(textConfig) as any;
const { editor_title, form } = text.ui.tables.events;
---
<!-- Event Editor Dialog -->

View file

@ -1,9 +1,12 @@
---
import EventEditor from "./EventEditor.astro";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
import textConfig from "../../config/text.yml?raw";
import profileConfig from "../../config/profileConfig.yaml?raw";
const config = yaml.load(configYaml) as any;
// Parse YAML configuration
const text = yaml.load(textConfig) as any;
const config = yaml.load(profileConfig) as any;
const { title, columns } = config.ui.tables.events;
---
@ -190,30 +193,37 @@ const { title, columns } = config.ui.tables.events;
</dialog>
<script>
import { EventAuth } from "../auth/EventAuth";
import PocketBase from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
import { Authentication } from "../pocketbase/Authentication";
import { Get } from "../pocketbase/Get";
import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog";
import JSZip from "jszip";
new EventAuth();
// Get instances
const auth = Authentication.getInstance();
const get = Get.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
// Parse YAML configuration
interface Config {
api: {
baseUrl: string;
};
}
const config = yaml.load(configYaml) as Config;
const pb = new PocketBase(config.api.baseUrl);
// Get DOM elements
// Show initial loading state
const eventsList = document.getElementById("eventList");
const searchInput = document.getElementById(
"eventSearch"
) as HTMLInputElement;
const searchButton = document.getElementById("searchEvents");
const refreshButton = document.getElementById("refreshEvents");
const addEventButton = document.getElementById("addEvent");
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 {
@ -224,48 +234,81 @@ const { title, columns } = config.ui.tables.events;
function renderEventRow(event: any) {
return `
<tr class="hover:bg-base-200">
<td class="text-center">
<div class="font-medium">${event.event_name || "N/A"}</div>
<!-- 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="text-center">${event.event_id || "N/A"}</td>
<td class="text-center">${event.event_code || "N/A"}</td>
<td class="text-center">${formatDate(event.start_date)}</td>
<td class="text-center">${formatDate(event.end_date)}</td>
<td class="text-center">${event.points_to_reward || "0"}</td>
<td class="text-center">${event.location || "N/A"}</td>
<td class="text-center">
<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
? event.files
.map(
(file: string) => `
<button class="btn btn-ghost btn-xs" data-file-url="${pb.files.getUrl(event, file)}" data-file-name="${file}">
<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>
`
)
.join("")
: "No files"
? `<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="text-center">
<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>
Attendees
</button>
</td>
<td class="text-center">
<button class="btn btn-ghost btn-xs edit-event" data-event="${encodeURIComponent(JSON.stringify(event))}">
<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 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>
`;
@ -275,33 +318,38 @@ const { title, columns } = config.ui.tables.events;
async function loadEvents(searchQuery = "") {
if (!eventsList) return;
try {
// Show loading state
eventsList.innerHTML = `
<tr>
<td colspan="10" class="text-center py-8">
<span class="loading loading-spinner loading-md"></span>
<div class="mt-2">Loading events...</div>
</td>
</tr>
`;
// 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 pb.collection("events").getList(1, 50, {
const events = await get.getList(
"events",
1,
50,
filter,
sort: "-created",
});
"-created"
);
// Update table
if (events.items.length === 0) {
eventsList.innerHTML = `
<tr>
<td colspan="10" class="text-center py-8">
No events found
<td colspan="10" class="text-center py-4">
${searchQuery ? "No events found matching your search." : "No events found."}
</td>
</tr>
`;
@ -309,19 +357,234 @@ const { title, columns } = config.ui.tables.events;
}
eventsList.innerHTML = events.items.map(renderEventRow).join("");
} catch (error) {
console.error("Failed to load events:", error);
// 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-8 text-error">
Failed to load events. Please try again.
<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);
@ -343,7 +606,10 @@ const { title, columns } = config.ui.tables.events;
if (addEventButton) {
addEventButton.addEventListener("click", () => {
document.getElementById("eventEditor")?.showModal();
const dialog = document.getElementById(
"eventEditor"
) as HTMLDialogElement;
dialog?.showModal();
});
}
@ -414,48 +680,4 @@ const { title, columns } = config.ui.tables.events;
document.addEventListener("showFilePreview", ((e: CustomEvent) => {
showFilePreview(e.detail.url, e.detail.fileName);
}) as EventListener);
// Add event listeners for dynamic elements
document.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
// Handle file view buttons
const fileButton = target.closest("button[data-file-url]");
if (fileButton) {
const url = fileButton.getAttribute("data-file-url");
const fileName = fileButton.getAttribute("data-file-name");
if (url && fileName) {
showFilePreview(url, fileName);
}
}
// Handle view attendees button
const attendeesButton = target.closest(".view-attendees");
if (attendeesButton) {
const eventId = attendeesButton.getAttribute("data-event-id");
if (eventId) {
document.dispatchEvent(
new CustomEvent("viewAttendees", { detail: eventId })
);
}
}
// Handle edit event button
const editButton = target.closest(".edit-event");
if (editButton) {
const eventData = editButton.getAttribute("data-event");
if (eventData) {
const event = JSON.parse(decodeURIComponent(eventData));
const eventEditor = document.getElementById(
"eventEditor"
) as HTMLDialogElement;
if (eventEditor) {
eventEditor.showModal();
document.dispatchEvent(
new CustomEvent("editEvent", { detail: event })
);
}
}
}
});
</script>

View file

@ -1,10 +1,20 @@
---
// Import the majors list
import allMajors from "../../data/allUCSDMajors.txt?raw";
import yaml from "js-yaml";
import textConfig from "../../config/text.yml?raw";
import profileConfig from "../../config/profileConfig.yaml?raw";
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
const majorsList: string[] = allMajors
.split("\n")
.filter((major: string) => major.trim())
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
// Parse configurations
const text = yaml.load(textConfig) as any;
const profile = yaml.load(profileConfig) as any;
const pbConfig = yaml.load(pocketbaseConfig) as any;
---
<div
@ -188,7 +198,9 @@ const majorsList: string[] = allMajors
import PocketBase from "pocketbase";
import type { RecordModel } from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
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 {
@ -213,13 +225,10 @@ const majorsList: string[] = allMajors
}
// Parse YAML configuration
interface Config {
api: {
baseUrl: string;
};
}
const config = yaml.load(configYaml) as Config;
const pb = new PocketBase(config.api.baseUrl);
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");
@ -408,9 +417,9 @@ const majorsList: string[] = allMajors
// Show loading state
resumeList.innerHTML = `
<tr>
<td colspan="8" class="text-center py-8">
<td colspan="6" class="text-center py-4">
<span class="loading loading-spinner loading-md"></span>
<div class="mt-2">Loading users...</div>
Loading users...
</td>
</tr>
`;
@ -429,7 +438,7 @@ const majorsList: string[] = allMajors
if (users.items.length === 0) {
resumeList.innerHTML = `
<tr>
<td colspan="8" class="text-center py-8">
<td colspan="6" class="text-center py-4">
No users found
</td>
</tr>
@ -442,7 +451,7 @@ const majorsList: string[] = allMajors
console.error("Failed to load users:", error);
resumeList.innerHTML = `
<tr>
<td colspan="8" class="text-center py-8 text-error">
<td colspan="6" class="text-center py-4 text-error">
Failed to load users. Please try again.
</td>
</tr>

View file

@ -271,11 +271,13 @@ const majorsList: string[] = allMajors
</dialog>
<script>
import { StoreAuth } from "../auth/StoreAuth";
import { SendLog } from "../auth/SendLog";
import PocketBase from "pocketbase";
const auth = new StoreAuth();
const logger = new SendLog();
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog";
const auth = Authentication.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
// Get form elements
const memberIdInput = document.getElementById(
@ -309,32 +311,35 @@ const majorsList: string[] = allMajors
// Load current user data
const loadUserData = () => {
const authState = auth.getAuthState();
if (authState.isValid && authState.model) {
const user = authState.model;
if (memberIdInput) memberIdInput.value = user.member_id || "";
if (majorSelect) majorSelect.value = user.major || "";
if (gradYearSelect)
gradYearSelect.value = user.graduation_year || "";
if (auth.isAuthenticated()) {
const user = auth.getCurrentUser();
if (user) {
if (memberIdInput) memberIdInput.value = user.member_id || "";
if (majorSelect) majorSelect.value = user.major || "";
if (gradYearSelect)
gradYearSelect.value = user.graduation_year || "";
// Update resume display
if (currentResume && resumeDisplay) {
if (user.resume) {
const fileName = user.resume.toString();
currentResume.textContent = fileName || "Resume uploaded";
resumeDisplay.classList.remove("hidden");
// Update resume display
if (currentResume && resumeDisplay) {
if (user.resume) {
const fileName = user.resume.toString();
currentResume.textContent =
fileName || "Resume uploaded";
resumeDisplay.classList.remove("hidden");
// Set up preview URLs - using PocketBase's direct file URL
const baseUrl = "https://pocketbase.ieeeucsd.org";
const resumeUrl = `${baseUrl}/api/files/users/${user.id}/${user.resume}`;
// Set up preview URLs - using PocketBase's direct file URL
const baseUrl = import.meta.env.POCKETBASE_URL;
const resumeUrl = `${baseUrl}/api/files/users/${user.id}/${user.resume}`;
if (resumeFrame) resumeFrame.src = resumeUrl;
if (resumeExternalLink) resumeExternalLink.href = resumeUrl;
} else {
currentResume.textContent = "No resume uploaded";
resumeDisplay.classList.add("hidden");
if (resumeFrame) resumeFrame.src = "";
if (resumeExternalLink) resumeExternalLink.href = "#";
if (resumeFrame) resumeFrame.src = resumeUrl;
if (resumeExternalLink)
resumeExternalLink.href = resumeUrl;
} else {
currentResume.textContent = "No resume uploaded";
resumeDisplay.classList.add("hidden");
if (resumeFrame) resumeFrame.src = "";
if (resumeExternalLink) resumeExternalLink.href = "#";
}
}
}
}
@ -347,7 +352,13 @@ const majorsList: string[] = allMajors
if (file) {
uploadStatus.textContent = "Uploading...";
try {
await auth.handleResumeUpload(file);
const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated");
const formData = new FormData();
formData.append("resume", file);
await update.updateFields("users", user.id, formData);
uploadStatus.textContent = "Resume uploaded successfully";
if (currentResume) currentResume.textContent = file.name;
@ -379,11 +390,13 @@ const majorsList: string[] = allMajors
button.classList.add("loading");
try {
const authState = auth.getAuthState();
const user = auth.getCurrentUser();
if (!user) throw new Error("User not authenticated");
const oldData = {
major: authState.model?.major || null,
graduation_year: authState.model?.graduation_year || null,
member_id: authState.model?.member_id || null,
major: user.major || null,
graduation_year: user.graduation_year || null,
member_id: user.member_id || null,
};
const newData = {
@ -394,7 +407,7 @@ const majorsList: string[] = allMajors
member_id: memberIdInput?.value || null,
} as const;
await auth.updateProfileSettings(newData);
await update.updateFields("users", user.id, newData);
// Create detailed log message of changes
const changes = [];
@ -484,10 +497,8 @@ const majorsList: string[] = allMajors
loadUserData();
// Update when auth state changes
window.addEventListener("storage", (e) => {
if (e.key === "pocketbase_auth") {
loadUserData();
}
auth.onAuthStateChange(() => {
loadUserData();
});
</script>

View file

@ -0,0 +1,5 @@
api:
baseUrl: https://pocketbase.ieeeucsd.org
oauth2:
redirectPath: /oauth2-redirect
providerName: oidc

View file

@ -1,9 +1,3 @@
api:
baseUrl: https://pocketbase.ieeeucsd.org
oauth2:
redirectPath: /oauth2-redirect
providerName: oidc
roles:
administrator:
name: IEEE Administrator

93
src/config/text.yml Normal file
View file

@ -0,0 +1,93 @@
ui:
transitions:
fadeDelay: 50
messages:
memberId:
saving: Saving member ID...
success: IEEE Member ID saved successfully!
error: Failed to save IEEE Member ID. Please try again.
messageTimeout: 3000
resume:
uploading: Uploading resume...
success: Resume uploaded successfully!
error: Failed to upload resume. Please try again.
deleting: Deleting resume...
deleteSuccess: Resume deleted successfully!
deleteError: Failed to delete resume. Please try again.
messageTimeout: 3000
event:
saving: Saving event...
success: Event saved successfully!
error: Failed to save event. Please try again.
deleting: Deleting event...
deleteSuccess: Event deleted successfully!
deleteError: Failed to delete event. Please try again.
messageTimeout: 3000
checkIn:
checking: Checking event code...
success: Successfully checked in to event!
error: Failed to check in. Please try again.
invalid: Invalid event code. Please try again.
expired: This event is not currently active.
alreadyCheckedIn: You have already checked in to this event.
messageTimeout: 3000
auth:
loginError: Failed to start authentication
notSignedIn: Not signed in
notVerified: Not verified
notProvided: Not provided
notAvailable: Not available
never: Never
tables:
events:
title: Event Management
editor_title: Event Details
columns:
event_name: Event Name
event_id: Event ID
event_code: Event Code
start_date: Start Date
end_date: End Date
points_to_reward: Points to Reward
location: Location
registered_users: Registered
actions: Actions
form:
event_id:
label: Event ID
placeholder: Enter unique event ID
event_name:
label: Event Name
placeholder: Enter event name
event_code:
label: Event Code
placeholder: Enter check-in code
start_date:
date_label: Start Date for check-in
time_label: Start Time for check-in
date_placeholder: Select start date for check-in
time_placeholder: Select start time for check-in
end_date:
date_label: End Date for check-in
time_label: End Time for check-in
date_placeholder: Select end date for check-in
time_placeholder: Select end time for check-in
points_to_reward:
label: Points to Reward
placeholder: Enter points value
location:
label: Location
placeholder: Enter event location
files:
label: Event Files
help_text: Upload event-related files (PDF, DOC, DOCX, TXT, JPG, JPEG, PNG)
buttons:
save: Save
cancel: Cancel
edit: Edit
delete: Delete

View file

@ -25,6 +25,69 @@ const title = "IEEE Online Store";
</Layout>
<script>
import { StoreAuth } from "../components/auth/StoreAuth";
new StoreAuth();
import { Authentication } from "../components/pocketbase/Authentication";
const auth = Authentication.getInstance();
// Initialize page state
const pageLoadingState = document.getElementById("pageLoadingState");
const pageErrorState = document.getElementById("pageErrorState");
const notAuthenticatedState = document.getElementById(
"notAuthenticatedState"
);
const mainContent = document.getElementById("mainContent");
// Initialize page
const initializePage = async () => {
try {
// Show loading state
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
if (pageErrorState) pageErrorState.classList.add("hidden");
if (notAuthenticatedState)
notAuthenticatedState.classList.add("hidden");
if (mainContent) mainContent.classList.add("hidden");
// Check auth state
if (!auth.isAuthenticated()) {
// Show not authenticated state
if (pageLoadingState) pageLoadingState.classList.add("hidden");
if (notAuthenticatedState)
notAuthenticatedState.classList.remove("hidden");
return;
}
// Hide loading state and show content
if (pageLoadingState) pageLoadingState.classList.add("hidden");
if (mainContent) mainContent.classList.remove("hidden");
} catch (error) {
console.error("Failed to initialize page:", error);
if (pageLoadingState) pageLoadingState.classList.add("hidden");
if (pageErrorState) pageErrorState.classList.remove("hidden");
if (mainContent) mainContent.classList.add("hidden");
}
};
// Check on load and auth changes
initializePage();
auth.onAuthStateChange(() => {
initializePage();
});
// Add login button event listener
const loginButtons = document.querySelectorAll(".login-button");
loginButtons.forEach((button) => {
button.addEventListener("click", () => {
// Show loading state while authentication is in progress
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
if (notAuthenticatedState)
notAuthenticatedState.classList.add("hidden");
// Call the login method
auth.login().catch((error) => {
console.error("Login error:", error);
// Show error state if login fails
if (pageLoadingState) pageLoadingState.classList.add("hidden");
if (pageErrorState) pageErrorState.classList.remove("hidden");
});
});
});
</script>

View file

@ -4,7 +4,13 @@ import UserProfile from "../components/auth/UserProfile.astro";
import DefaultProfileView from "../components/profile/DefaultProfileView.astro";
import OfficerProfileView from "../components/profile/OfficerView.astro";
import UserSettings from "../components/profile/UserSettings.astro";
import yaml from "js-yaml";
import profileConfig from "../config/profileConfig.yaml?raw";
import textConfig from "../config/text.yml?raw";
const title = "User Profile";
const config = yaml.load(profileConfig) as any;
const text = yaml.load(textConfig) as any;
---
<Layout {title}>
@ -43,10 +49,7 @@ const title = "User Profile";
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span
>Failed to load profile data. Please try refreshing
the page.</span
>
<span>{text.ui.messages.auth.loginError}</span>
</div>
</div>
</div>
@ -198,7 +201,7 @@ const title = "User Profile";
</div>
<!-- Content Areas -->
<div id="defaultView">
<!-- <div id="defaultView">
<DefaultProfileView />
</div>
<div id="settingsView" class="hidden">
@ -206,7 +209,7 @@ const title = "User Profile";
</div>
<div id="officerView" class="hidden">
<OfficerProfileView />
</div>
</div> -->
</div>
</div>
</div>
@ -214,8 +217,14 @@ const title = "User Profile";
</Layout>
<script>
import { StoreAuth } from "../components/auth/StoreAuth";
const auth = new StoreAuth();
import { Authentication } from "../components/pocketbase/Authentication";
import yaml from "js-yaml";
import profileConfig from "../config/profileConfig.yaml?raw";
import textConfig from "../config/text.yml?raw";
const config = yaml.load(profileConfig) as any;
const text = yaml.load(textConfig) as any;
const auth = Authentication.getInstance();
// Initialize page state
const pageLoadingState = document.getElementById("pageLoadingState");
@ -224,10 +233,9 @@ const title = "User Profile";
"notAuthenticatedState"
);
const mainContent = document.getElementById("mainContent");
const tabs = document.querySelectorAll(".tab");
const defaultView = document.getElementById("defaultView");
const officerView = document.getElementById("officerView");
const officerTab = document.getElementById("officerTab");
const settingsView = document.getElementById("settingsView");
// Stats elements
const eventsAttendedValue = document.getElementById("eventsAttendedValue");
@ -236,20 +244,15 @@ const title = "User Profile";
const activityLevelValue = document.getElementById("activityLevelValue");
const activityLevelDesc = document.getElementById("activityLevelDesc");
// Hide officer tab by default
if (officerTab) {
officerTab.style.display = "none";
}
// Show officer tab if user is an officer
const showOfficerTab = () => {
if (officerTab) {
const authState = auth.getAuthState();
const isOfficer =
authState.model?.member_type === "officer" ||
authState.model?.member_type === "administrator";
officerTab.style.display = isOfficer ? "inline-flex" : "none";
}
// Show officer view if user has appropriate role
const showOfficerView = (user: any) => {
if (!user) return false;
const userRole = user.role || "member";
const roleConfig = config.roles[userRole];
return (
roleConfig?.permissions?.includes("manage") ||
roleConfig?.permissions?.includes("edit")
);
};
// Initialize page
@ -263,16 +266,14 @@ const title = "User Profile";
if (mainContent) mainContent.classList.add("hidden");
// Check auth state
const authState = auth.getAuthState();
if (!authState.isValid || !authState.model) {
// Show not authenticated state
if (!auth.isAuthenticated()) {
if (pageLoadingState) pageLoadingState.classList.add("hidden");
if (notAuthenticatedState)
notAuthenticatedState.classList.remove("hidden");
return;
}
const user = authState.model;
const user = auth.getCurrentUser();
// Update stats
if (eventsAttendedValue) {
@ -283,16 +284,14 @@ const title = "User Profile";
if (loyaltyPointsValue && loyaltyPointsChange) {
const points = user.points || 0;
loyaltyPointsValue.textContent = points.toString();
// Calculate points change (example logic)
const pointsChange = 10; // Replace with actual calculation
const pointsChange = user.points_change_30d || 0;
loyaltyPointsChange.textContent =
pointsChange > 0
pointsChange >= 0
? `↗︎ ${pointsChange} points (30 days)`
: `↘︎ ${Math.abs(pointsChange)} points (30 days)`;
}
if (activityLevelValue && activityLevelDesc) {
// Calculate activity level based on events and points
const eventsAttended = user.events_attended?.length || 0;
const points = user.points || 0;
let activityLevel = "Low";
@ -310,8 +309,14 @@ const title = "User Profile";
activityLevelDesc.textContent = description;
}
// Show officer tab if applicable
showOfficerTab();
// Show appropriate view based on user role
if (showOfficerView(user)) {
if (officerView) officerView.classList.remove("hidden");
if (defaultView) defaultView.classList.add("hidden");
} else {
if (officerView) officerView.classList.add("hidden");
if (defaultView) defaultView.classList.remove("hidden");
}
// Hide loading state and show content
if (pageLoadingState) pageLoadingState.classList.add("hidden");
@ -326,10 +331,8 @@ const title = "User Profile";
// Check on load and auth changes
initializePage();
window.addEventListener("storage", (e) => {
if (e.key === "pocketbase_auth") {
initializePage();
}
auth.onAuthStateChange(() => {
initializePage();
});
// Handle default view tab switching
@ -342,8 +345,6 @@ const title = "User Profile";
// Update content visibility
const tabId = (tab as HTMLElement).dataset.defaultTab;
const defaultView = document.getElementById("defaultView");
const settingsView = document.getElementById("settingsView");
if (defaultView && settingsView) {
defaultView.classList.add("hidden");
@ -364,19 +365,18 @@ const title = "User Profile";
// Add login button event listener
const loginButtons = document.querySelectorAll(".login-button");
loginButtons.forEach((button) => {
button.addEventListener("click", () => {
// Show loading state while authentication is in progress
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
if (notAuthenticatedState)
notAuthenticatedState.classList.add("hidden");
// Call the handleLogin method from StoreAuth
auth.handleLogin().catch((error) => {
button.addEventListener("click", async () => {
try {
if (pageLoadingState)
pageLoadingState.classList.remove("hidden");
if (notAuthenticatedState)
notAuthenticatedState.classList.add("hidden");
await auth.login();
} catch (error) {
console.error("Login error:", error);
// Show error state if login fails
if (pageLoadingState) pageLoadingState.classList.add("hidden");
if (pageErrorState) pageErrorState.classList.remove("hidden");
});
}
});
});
</script>