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:
parent
172c56e023
commit
e57f76bd4c
19 changed files with 1563 additions and 2468 deletions
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
119
src/components/pocketbase/Authentication.ts
Normal file
119
src/components/pocketbase/Authentication.ts
Normal 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));
|
||||
}
|
||||
}
|
204
src/components/pocketbase/Get.ts
Normal file
204
src/components/pocketbase/Get.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
168
src/components/pocketbase/SendLog.ts
Normal file
168
src/components/pocketbase/SendLog.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
134
src/components/pocketbase/Update.ts
Normal file
134
src/components/pocketbase/Update.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 =
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
5
src/config/pocketbaseConfig.yml
Normal file
5
src/config/pocketbaseConfig.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
api:
|
||||
baseUrl: https://pocketbase.ieeeucsd.org
|
||||
oauth2:
|
||||
redirectPath: /oauth2-redirect
|
||||
providerName: oidc
|
|
@ -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
93
src/config/text.yml
Normal 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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue