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 PocketBase from "pocketbase";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import configYaml from "../../data/storeConfig.yaml?raw";
|
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
|
||||||
import { SendLog } from "./SendLog";
|
import textConfig from "../../config/text.yml?raw";
|
||||||
|
import { SendLog } from "../pocketbase/SendLog";
|
||||||
|
|
||||||
// Configuration type definitions
|
// Configuration type definitions
|
||||||
interface Config {
|
interface Config {
|
||||||
api: {
|
api: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
};
|
oauth2: {
|
||||||
ui: {
|
redirectPath: string;
|
||||||
messages: {
|
providerName: string;
|
||||||
event: {
|
|
||||||
checkIn: {
|
|
||||||
checking: string;
|
|
||||||
success: string;
|
|
||||||
error: string;
|
|
||||||
invalid: string;
|
|
||||||
expired: string;
|
|
||||||
alreadyCheckedIn: string;
|
|
||||||
messageTimeout: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML configuration with type
|
// Parse YAML configuration
|
||||||
const config = yaml.load(configYaml) as Config;
|
const config = yaml.load(pocketbaseConfig) as Config;
|
||||||
|
const text = yaml.load(textConfig) as any;
|
||||||
|
|
||||||
interface AuthElements {
|
interface AuthElements {
|
||||||
eventCodeInput: HTMLInputElement;
|
eventCodeInput: HTMLInputElement;
|
||||||
|
@ -42,7 +33,7 @@ export class EventCheckIn {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.pb = new PocketBase(config.api.baseUrl);
|
this.pb = new PocketBase(config.api.baseUrl);
|
||||||
this.elements = this.getElements();
|
this.elements = this.getElements();
|
||||||
this.logger = new SendLog();
|
this.logger = SendLog.getInstance();
|
||||||
|
|
||||||
// Add event listener for the check-in button
|
// Add event listener for the check-in button
|
||||||
this.elements.checkInButton.addEventListener("click", () => this.handleCheckIn());
|
this.elements.checkInButton.addEventListener("click", () => this.handleCheckIn());
|
||||||
|
@ -90,7 +81,7 @@ export class EventCheckIn {
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
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 {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
event,
|
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 {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
event,
|
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);
|
console.error('Failed to validate event code:', err);
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
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();
|
const eventCode = eventCodeInput.value.trim();
|
||||||
|
|
||||||
if (!eventCode) {
|
if (!eventCode) {
|
||||||
this.showStatus("Please enter an event code", "error");
|
this.showStatus(text.ui.messages.event.checkIn.invalid, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let validation: { isValid: boolean; event?: any; message?: string } | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.showStatus(config.ui.messages.event.checkIn.checking, "info");
|
this.showStatus(text.ui.messages.event.checkIn.checking, "info");
|
||||||
|
|
||||||
// Get current user
|
// Get current user
|
||||||
const user = this.pb.authStore.model;
|
const user = this.pb.authStore.model;
|
||||||
if (!user) {
|
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(
|
await this.logger.send(
|
||||||
"error",
|
"error",
|
||||||
"event check in",
|
"event check in",
|
||||||
|
@ -151,9 +144,9 @@ export class EventCheckIn {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate event code and check time window
|
// Validate event code and check time window
|
||||||
const validation = await this.validateEventCode(eventCode);
|
validation = await this.validateEventCode(eventCode);
|
||||||
if (!validation.isValid) {
|
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(
|
await this.logger.send(
|
||||||
"error",
|
"error",
|
||||||
"event check in",
|
"event check in",
|
||||||
|
@ -191,7 +184,7 @@ export class EventCheckIn {
|
||||||
const isAlreadyCheckedIn = eventsAttended.includes(event.event_id);
|
const isAlreadyCheckedIn = eventsAttended.includes(event.event_id);
|
||||||
|
|
||||||
if (isAlreadyCheckedIn) {
|
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(
|
await this.logger.send(
|
||||||
"error",
|
"error",
|
||||||
"event check in",
|
"event check in",
|
||||||
|
@ -232,10 +225,7 @@ export class EventCheckIn {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message with points
|
// Show success message with points
|
||||||
this.showStatus(
|
this.showStatus(text.ui.messages.event.checkIn.success, "success");
|
||||||
`Successfully checked in to ${event.event_name}! You earned ${pointsToAdd} points!`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
eventCodeInput.value = ""; // Clear input
|
eventCodeInput.value = ""; // Clear input
|
||||||
|
|
||||||
// Log the successful check-in
|
// Log the successful check-in
|
||||||
|
@ -268,14 +258,22 @@ export class EventCheckIn {
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Check-in error:", 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
|
// Log any errors that occur during check-in
|
||||||
await this.logger.send(
|
if (validation?.event) {
|
||||||
"error",
|
await this.logger.send(
|
||||||
"event check in",
|
"error",
|
||||||
`Failed to check in to event ${event.id}: ${err instanceof Error ? err.message : "Unknown 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") {
|
if (type !== "info") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkInStatus.textContent = "";
|
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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { StoreAuth } from "./StoreAuth";
|
import { Authentication } from "../pocketbase/Authentication";
|
||||||
import { EventCheckIn } from "./EventCheckIn";
|
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 {
|
try {
|
||||||
const auth = new StoreAuth();
|
// Get full user data
|
||||||
new EventCheckIn();
|
const userData = await get.getOne("users", user.id);
|
||||||
|
|
||||||
// Add officer view toggle handler
|
// Update UI elements
|
||||||
const officerViewToggle =
|
if (userName)
|
||||||
document.getElementById("officerViewToggle");
|
userName.textContent = userData.name || "Unnamed User";
|
||||||
const officerViewCheckbox = officerViewToggle?.querySelector(
|
if (userEmail) userEmail.textContent = userData.email || "No email";
|
||||||
'input[type="checkbox"]'
|
if (userInitials)
|
||||||
) as HTMLInputElement;
|
userInitials.textContent = getInitials(userData.name || "?");
|
||||||
|
|
||||||
if (officerViewCheckbox) {
|
// Update member status based on member_type and email domain
|
||||||
officerViewCheckbox.addEventListener("change", () => {
|
if (memberStatus) {
|
||||||
const defaultView = document.getElementById("defaultView");
|
let memberType = userData.member_type || "IEEE Member";
|
||||||
const officerView = document.getElementById("officerView");
|
|
||||||
const mainTabs = document.querySelector(".tabs.tabs-boxed");
|
|
||||||
const officerContent =
|
|
||||||
document.getElementById("officerContent");
|
|
||||||
const settingsView =
|
|
||||||
document.getElementById("settingsView");
|
|
||||||
|
|
||||||
if (
|
// Auto-assign officer status for @ieeeucsd.org emails if they're not already a higher rank
|
||||||
defaultView &&
|
if (
|
||||||
officerView &&
|
userData.email?.endsWith("@ieeeucsd.org") &&
|
||||||
mainTabs &&
|
![
|
||||||
officerContent &&
|
"IEEE Executive",
|
||||||
settingsView
|
"IEEE Administrator",
|
||||||
) {
|
"IEEE Sponsor",
|
||||||
if (officerViewCheckbox.checked) {
|
].includes(memberType)
|
||||||
// Hide default view, settings, and tabs
|
) {
|
||||||
defaultView.classList.add("hidden");
|
memberType = "IEEE Officer";
|
||||||
settingsView.classList.add("hidden");
|
|
||||||
mainTabs.classList.add("hidden");
|
// Update the member type in the database if it's different
|
||||||
// Show officer view
|
if (userData.member_type !== memberType) {
|
||||||
officerView.classList.remove("hidden");
|
try {
|
||||||
officerContent.classList.remove("hidden");
|
await update.updateField(
|
||||||
} else {
|
"users",
|
||||||
// Show default view and tabs
|
userData.id,
|
||||||
defaultView.classList.remove("hidden");
|
"member_type",
|
||||||
mainTabs.classList.remove("hidden");
|
memberType
|
||||||
// Hide officer view
|
);
|
||||||
officerView.classList.add("hidden");
|
await logger.send(
|
||||||
officerContent.classList.add("hidden");
|
"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
|
// Update timestamps
|
||||||
const loginButtons = document.querySelectorAll(".login-button");
|
if (lastLogin)
|
||||||
loginButtons.forEach((button) => {
|
lastLogin.textContent = formatDate(userData.last_login);
|
||||||
button.addEventListener("click", () => {
|
if (memberSince)
|
||||||
const loadingSkeleton =
|
memberSince.textContent = formatDate(userData.created);
|
||||||
document.getElementById("loadingSkeleton");
|
|
||||||
const userInfo = document.getElementById("userInfo");
|
|
||||||
|
|
||||||
// Show loading state
|
// Show logout button
|
||||||
if (loadingSkeleton)
|
if (contentLoginButton) contentLoginButton.classList.add("hidden");
|
||||||
loadingSkeleton.style.display = "block";
|
if (contentLogoutButton)
|
||||||
if (userInfo) userInfo.classList.add("hidden");
|
contentLogoutButton.classList.remove("hidden");
|
||||||
|
|
||||||
// Call the handleLogin method
|
// Update view toggles based on member type
|
||||||
auth.handleLogin().catch((error) => {
|
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);
|
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)
|
if (loadingSkeleton)
|
||||||
loadingSkeleton.style.display = "none";
|
loadingSkeleton.style.display = "none";
|
||||||
if (userInfo) {
|
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.classList.remove("hidden");
|
||||||
userInfo.style.opacity = "1";
|
userInfo.style.opacity = "1";
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Add error handling for failed initialization
|
// Add logout button handler
|
||||||
window.addEventListener("unhandledrejection", (event) => {
|
if (contentLogoutButton) {
|
||||||
console.error("Profile loading error:", event.reason);
|
contentLogoutButton.addEventListener("click", async () => {
|
||||||
const userInfo = document.getElementById("userInfo");
|
try {
|
||||||
const loadingSkeleton =
|
await auth.logout();
|
||||||
document.getElementById("loadingSkeleton");
|
await logger.send(
|
||||||
const memberDetails = document.getElementById("memberDetails");
|
"logout",
|
||||||
const errorMessage = document.createElement("div");
|
"profile view",
|
||||||
|
"User logged out successfully"
|
||||||
errorMessage.className = "alert alert-error";
|
);
|
||||||
errorMessage.innerHTML = `
|
} catch (error) {
|
||||||
<div class="flex items-center gap-2">
|
console.error("Logout error:", error);
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
|
await logger.send(
|
||||||
<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" />
|
"error",
|
||||||
</svg>
|
"profile view",
|
||||||
<span>Failed to load profile. Please refresh the page.</span>
|
`Logout failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
</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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize profile:", 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>
|
</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>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { StoreAuth } from "../auth/StoreAuth";
|
import { Authentication } from "../pocketbase/Authentication";
|
||||||
import { EventCheckIn } from "../auth/EventCheckIn";
|
import { Get } from "../pocketbase/Get";
|
||||||
import PocketBase from "pocketbase";
|
import { SendLog } from "../pocketbase/SendLog";
|
||||||
import yaml from "js-yaml";
|
|
||||||
import configYaml from "../../data/storeConfig.yaml?raw";
|
|
||||||
import type { RecordModel } from "pocketbase";
|
|
||||||
|
|
||||||
// Parse YAML configuration with type assertion
|
const auth = Authentication.getInstance();
|
||||||
interface Config {
|
const get = Get.getInstance();
|
||||||
api: {
|
const logger = SendLog.getInstance();
|
||||||
baseUrl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const config = yaml.load(configYaml) as Config;
|
|
||||||
const pb = new PocketBase(config.api.baseUrl);
|
|
||||||
|
|
||||||
// Initialize auth and check-in
|
// Initialize event check-in
|
||||||
new StoreAuth();
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
new EventCheckIn();
|
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
|
// Handle loading states
|
||||||
const eventCheckInSkeleton = document.getElementById(
|
const eventCheckInSkeleton = document.getElementById(
|
||||||
|
@ -217,13 +232,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show content when auth state changes
|
// Show content when auth state changes
|
||||||
pb.authStore.onChange(() => {
|
auth.onAuthStateChange(() => {
|
||||||
showEventCheckIn();
|
showEventCheckIn();
|
||||||
renderEvents();
|
renderEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show content on initial load if already authenticated
|
// Show content on initial load if already authenticated
|
||||||
if (pb.authStore.isValid) {
|
if (auth.isAuthenticated()) {
|
||||||
showEventCheckIn();
|
showEventCheckIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +397,7 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get current user's attended events with safe parsing
|
// Get current user's attended events with safe parsing
|
||||||
const user = pb.authStore.model;
|
const user = auth.getCurrentUser();
|
||||||
let attendedEvents: string[] = [];
|
let attendedEvents: string[] = [];
|
||||||
|
|
||||||
if (user?.events_attended) {
|
if (user?.events_attended) {
|
||||||
|
@ -400,9 +415,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all events
|
// Fetch all events
|
||||||
const events = await pb.collection("events").getList(1, 50, {
|
const events = await get.getMany(
|
||||||
sort: "start_date", // Sort by start date ascending for upcoming events
|
"events",
|
||||||
});
|
user.events_attended || []
|
||||||
|
);
|
||||||
|
|
||||||
// Clear loading skeletons
|
// Clear loading skeletons
|
||||||
eventsList.innerHTML = "";
|
eventsList.innerHTML = "";
|
||||||
|
@ -414,7 +430,7 @@
|
||||||
const upcomingEvents: Event[] = [];
|
const upcomingEvents: Event[] = [];
|
||||||
const pastEvents: Event[] = [];
|
const pastEvents: Event[] = [];
|
||||||
|
|
||||||
events.items.forEach((event) => {
|
events.forEach((event) => {
|
||||||
const typedEvent = event as Event;
|
const typedEvent = event as Event;
|
||||||
const startDate = new Date(typedEvent.start_date);
|
const startDate = new Date(typedEvent.start_date);
|
||||||
const endDate = new Date(typedEvent.end_date);
|
const endDate = new Date(typedEvent.end_date);
|
||||||
|
@ -493,9 +509,7 @@
|
||||||
if (e instanceof CustomEvent && "eventId" in e.detail) {
|
if (e instanceof CustomEvent && "eventId" in e.detail) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const event = await pb
|
const event = await get.getOne("events", e.detail.eventId);
|
||||||
.collection("events")
|
|
||||||
.getOne(e.detail.eventId);
|
|
||||||
const fileViewerContent =
|
const fileViewerContent =
|
||||||
document.getElementById("fileViewerContent");
|
document.getElementById("fileViewerContent");
|
||||||
const fileViewerTitle =
|
const fileViewerTitle =
|
||||||
|
@ -519,7 +533,7 @@
|
||||||
|
|
||||||
fileViewerContent.innerHTML = event.files
|
fileViewerContent.innerHTML = event.files
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
const fileUrl = pb.files.getURL(event, file);
|
const fileUrl = get.getFileURL(event, file);
|
||||||
const fileName =
|
const fileName =
|
||||||
file.split("/").pop() || "File";
|
file.split("/").pop() || "File";
|
||||||
const fileExt =
|
const fileExt =
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
---
|
---
|
||||||
import yaml from "js-yaml";
|
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;
|
// Parse YAML configuration
|
||||||
const { editor_title, form } = config.ui.tables.events;
|
const text = yaml.load(textConfig) as any;
|
||||||
|
const { editor_title, form } = text.ui.tables.events;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Event Editor Dialog -->
|
<!-- Event Editor Dialog -->
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
---
|
---
|
||||||
import EventEditor from "./EventEditor.astro";
|
import EventEditor from "./EventEditor.astro";
|
||||||
import yaml from "js-yaml";
|
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;
|
const { title, columns } = config.ui.tables.events;
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -190,30 +193,37 @@ const { title, columns } = config.ui.tables.events;
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { EventAuth } from "../auth/EventAuth";
|
import { Authentication } from "../pocketbase/Authentication";
|
||||||
import PocketBase from "pocketbase";
|
import { Get } from "../pocketbase/Get";
|
||||||
import yaml from "js-yaml";
|
import { Update } from "../pocketbase/Update";
|
||||||
import configYaml from "../../data/storeConfig.yaml?raw";
|
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
|
// Show initial loading state
|
||||||
interface Config {
|
|
||||||
api: {
|
|
||||||
baseUrl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const config = yaml.load(configYaml) as Config;
|
|
||||||
const pb = new PocketBase(config.api.baseUrl);
|
|
||||||
|
|
||||||
// Get DOM elements
|
|
||||||
const eventsList = document.getElementById("eventList");
|
const eventsList = document.getElementById("eventList");
|
||||||
const searchInput = document.getElementById(
|
if (eventsList) {
|
||||||
"eventSearch"
|
eventsList.innerHTML = `
|
||||||
) as HTMLInputElement;
|
<tr>
|
||||||
const searchButton = document.getElementById("searchEvents");
|
<td colspan="10" class="text-center py-8">
|
||||||
const refreshButton = document.getElementById("refreshEvents");
|
<div class="flex flex-col items-center gap-2">
|
||||||
const addEventButton = document.getElementById("addEvent");
|
<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 to format date
|
||||||
function formatDate(dateStr: string): string {
|
function formatDate(dateStr: string): string {
|
||||||
|
@ -224,48 +234,81 @@ const { title, columns } = config.ui.tables.events;
|
||||||
function renderEventRow(event: any) {
|
function renderEventRow(event: any) {
|
||||||
return `
|
return `
|
||||||
<tr class="hover:bg-base-200">
|
<tr class="hover:bg-base-200">
|
||||||
<td class="text-center">
|
<!-- Mobile View -->
|
||||||
<div class="font-medium">${event.event_name || "N/A"}</div>
|
<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>
|
||||||
<td class="text-center">${event.event_id || "N/A"}</td>
|
<td class="hidden lg:table-cell text-center">${event.event_id || "N/A"}</td>
|
||||||
<td class="text-center">${event.event_code || "N/A"}</td>
|
<td class="hidden lg:table-cell text-center">${event.event_code || "N/A"}</td>
|
||||||
<td class="text-center">${formatDate(event.start_date)}</td>
|
<td class="hidden lg:table-cell text-center">${formatDate(event.start_date)}</td>
|
||||||
<td class="text-center">${formatDate(event.end_date)}</td>
|
<td class="hidden lg:table-cell text-center">${formatDate(event.end_date)}</td>
|
||||||
<td class="text-center">${event.points_to_reward || "0"}</td>
|
<td class="hidden lg:table-cell text-center">${event.points_to_reward || "0"}</td>
|
||||||
<td class="text-center">${event.location || "N/A"}</td>
|
<td class="hidden lg:table-cell text-center">${event.location || "N/A"}</td>
|
||||||
<td class="text-center">
|
<td class="hidden lg:table-cell text-center">
|
||||||
${
|
${
|
||||||
event.files && event.files.length > 0
|
event.files && event.files.length > 0
|
||||||
? event.files
|
? `<button class="btn btn-ghost btn-xs view-files" data-event-id="${event.id}">
|
||||||
.map(
|
${event.files.length} File${event.files.length > 1 ? "s" : ""}
|
||||||
(file: string) => `
|
</button>`
|
||||||
<button class="btn btn-ghost btn-xs" data-file-url="${pb.files.getUrl(event, file)}" data-file-name="${file}">
|
: '<span class="text-sm opacity-50">No files</span>'
|
||||||
<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"
|
|
||||||
}
|
}
|
||||||
</td>
|
</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}">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Attendees
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="hidden lg:table-cell text-center">
|
||||||
<button class="btn btn-ghost btn-xs edit-event" data-event="${encodeURIComponent(JSON.stringify(event))}">
|
<div class="flex justify-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
|
||||||
<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 xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
</svg>
|
<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" />
|
||||||
Edit
|
</svg>
|
||||||
</button>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
@ -275,33 +318,38 @@ const { title, columns } = config.ui.tables.events;
|
||||||
async function loadEvents(searchQuery = "") {
|
async function loadEvents(searchQuery = "") {
|
||||||
if (!eventsList) return;
|
if (!eventsList) return;
|
||||||
|
|
||||||
try {
|
// Show loading state immediately
|
||||||
// Show loading state
|
eventsList.innerHTML = `
|
||||||
eventsList.innerHTML = `
|
<tr>
|
||||||
<tr>
|
<td colspan="10" class="text-center py-8">
|
||||||
<td colspan="10" class="text-center py-8">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<span class="loading loading-spinner loading-md"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<div class="mt-2">Loading events...</div>
|
<span class="text-sm opacity-70">Loading events...</span>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
`;
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
// Fetch events with filter if search query exists
|
// Fetch events with filter if search query exists
|
||||||
const filter = searchQuery
|
const filter = searchQuery
|
||||||
? `event_name ~ "${searchQuery}" || event_id ~ "${searchQuery}" || event_code ~ "${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,
|
filter,
|
||||||
sort: "-created",
|
"-created"
|
||||||
});
|
);
|
||||||
|
|
||||||
// Update table
|
// Update table
|
||||||
if (events.items.length === 0) {
|
if (events.items.length === 0) {
|
||||||
eventsList.innerHTML = `
|
eventsList.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="text-center py-8">
|
<td colspan="10" class="text-center py-4">
|
||||||
No events found
|
${searchQuery ? "No events found matching your search." : "No events found."}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
@ -309,19 +357,234 @@ const { title, columns } = config.ui.tables.events;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventsList.innerHTML = events.items.map(renderEventRow).join("");
|
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 = `
|
eventsList.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="text-center py-8 text-error">
|
<td colspan="10" class="text-center py-4 text-error">
|
||||||
Failed to load events. Please try again.
|
<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>
|
</td>
|
||||||
</tr>
|
</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
|
// 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) {
|
if (searchButton && searchInput) {
|
||||||
searchButton.addEventListener("click", () => {
|
searchButton.addEventListener("click", () => {
|
||||||
loadEvents(searchInput.value);
|
loadEvents(searchInput.value);
|
||||||
|
@ -343,7 +606,10 @@ const { title, columns } = config.ui.tables.events;
|
||||||
|
|
||||||
if (addEventButton) {
|
if (addEventButton) {
|
||||||
addEventButton.addEventListener("click", () => {
|
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) => {
|
document.addEventListener("showFilePreview", ((e: CustomEvent) => {
|
||||||
showFilePreview(e.detail.url, e.detail.fileName);
|
showFilePreview(e.detail.url, e.detail.fileName);
|
||||||
}) as EventListener);
|
}) 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>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
---
|
---
|
||||||
// Import the majors list
|
// Import the majors list
|
||||||
import allMajors from "../../data/allUCSDMajors.txt?raw";
|
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
|
const majorsList: string[] = allMajors
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((major: string) => major.trim())
|
.filter((major: string) => major.trim())
|
||||||
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
|
.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
|
<div
|
||||||
|
@ -188,7 +198,9 @@ const majorsList: string[] = allMajors
|
||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
import type { RecordModel } from "pocketbase";
|
import type { RecordModel } from "pocketbase";
|
||||||
import yaml from "js-yaml";
|
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
|
// Define interface for user data structure
|
||||||
interface UserData {
|
interface UserData {
|
||||||
|
@ -213,13 +225,10 @@ const majorsList: string[] = allMajors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML configuration
|
// Parse YAML configuration
|
||||||
interface Config {
|
const text = yaml.load(textConfig) as any;
|
||||||
api: {
|
const profile = yaml.load(profileConfig) as any;
|
||||||
baseUrl: string;
|
const pbConfig = yaml.load(pocketbaseConfig) as any;
|
||||||
};
|
const pb = new PocketBase(pbConfig.api.baseUrl);
|
||||||
}
|
|
||||||
const config = yaml.load(configYaml) as Config;
|
|
||||||
const pb = new PocketBase(config.api.baseUrl);
|
|
||||||
|
|
||||||
// Get DOM elements
|
// Get DOM elements
|
||||||
const resumeList = document.getElementById("resumeList");
|
const resumeList = document.getElementById("resumeList");
|
||||||
|
@ -408,9 +417,9 @@ const majorsList: string[] = allMajors
|
||||||
// Show loading state
|
// Show loading state
|
||||||
resumeList.innerHTML = `
|
resumeList.innerHTML = `
|
||||||
<tr>
|
<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>
|
<span class="loading loading-spinner loading-md"></span>
|
||||||
<div class="mt-2">Loading users...</div>
|
Loading users...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
@ -429,7 +438,7 @@ const majorsList: string[] = allMajors
|
||||||
if (users.items.length === 0) {
|
if (users.items.length === 0) {
|
||||||
resumeList.innerHTML = `
|
resumeList.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center py-8">
|
<td colspan="6" class="text-center py-4">
|
||||||
No users found
|
No users found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -442,7 +451,7 @@ const majorsList: string[] = allMajors
|
||||||
console.error("Failed to load users:", error);
|
console.error("Failed to load users:", error);
|
||||||
resumeList.innerHTML = `
|
resumeList.innerHTML = `
|
||||||
<tr>
|
<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.
|
Failed to load users. Please try again.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -271,11 +271,13 @@ const majorsList: string[] = allMajors
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { StoreAuth } from "../auth/StoreAuth";
|
import { Authentication } from "../pocketbase/Authentication";
|
||||||
import { SendLog } from "../auth/SendLog";
|
import { Update } from "../pocketbase/Update";
|
||||||
import PocketBase from "pocketbase";
|
import { SendLog } from "../pocketbase/SendLog";
|
||||||
const auth = new StoreAuth();
|
|
||||||
const logger = new SendLog();
|
const auth = Authentication.getInstance();
|
||||||
|
const update = Update.getInstance();
|
||||||
|
const logger = SendLog.getInstance();
|
||||||
|
|
||||||
// Get form elements
|
// Get form elements
|
||||||
const memberIdInput = document.getElementById(
|
const memberIdInput = document.getElementById(
|
||||||
|
@ -309,32 +311,35 @@ const majorsList: string[] = allMajors
|
||||||
|
|
||||||
// Load current user data
|
// Load current user data
|
||||||
const loadUserData = () => {
|
const loadUserData = () => {
|
||||||
const authState = auth.getAuthState();
|
if (auth.isAuthenticated()) {
|
||||||
if (authState.isValid && authState.model) {
|
const user = auth.getCurrentUser();
|
||||||
const user = authState.model;
|
if (user) {
|
||||||
if (memberIdInput) memberIdInput.value = user.member_id || "";
|
if (memberIdInput) memberIdInput.value = user.member_id || "";
|
||||||
if (majorSelect) majorSelect.value = user.major || "";
|
if (majorSelect) majorSelect.value = user.major || "";
|
||||||
if (gradYearSelect)
|
if (gradYearSelect)
|
||||||
gradYearSelect.value = user.graduation_year || "";
|
gradYearSelect.value = user.graduation_year || "";
|
||||||
|
|
||||||
// Update resume display
|
// Update resume display
|
||||||
if (currentResume && resumeDisplay) {
|
if (currentResume && resumeDisplay) {
|
||||||
if (user.resume) {
|
if (user.resume) {
|
||||||
const fileName = user.resume.toString();
|
const fileName = user.resume.toString();
|
||||||
currentResume.textContent = fileName || "Resume uploaded";
|
currentResume.textContent =
|
||||||
resumeDisplay.classList.remove("hidden");
|
fileName || "Resume uploaded";
|
||||||
|
resumeDisplay.classList.remove("hidden");
|
||||||
|
|
||||||
// Set up preview URLs - using PocketBase's direct file URL
|
// Set up preview URLs - using PocketBase's direct file URL
|
||||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
const baseUrl = import.meta.env.POCKETBASE_URL;
|
||||||
const resumeUrl = `${baseUrl}/api/files/users/${user.id}/${user.resume}`;
|
const resumeUrl = `${baseUrl}/api/files/users/${user.id}/${user.resume}`;
|
||||||
|
|
||||||
if (resumeFrame) resumeFrame.src = resumeUrl;
|
if (resumeFrame) resumeFrame.src = resumeUrl;
|
||||||
if (resumeExternalLink) resumeExternalLink.href = resumeUrl;
|
if (resumeExternalLink)
|
||||||
} else {
|
resumeExternalLink.href = resumeUrl;
|
||||||
currentResume.textContent = "No resume uploaded";
|
} else {
|
||||||
resumeDisplay.classList.add("hidden");
|
currentResume.textContent = "No resume uploaded";
|
||||||
if (resumeFrame) resumeFrame.src = "";
|
resumeDisplay.classList.add("hidden");
|
||||||
if (resumeExternalLink) resumeExternalLink.href = "#";
|
if (resumeFrame) resumeFrame.src = "";
|
||||||
|
if (resumeExternalLink) resumeExternalLink.href = "#";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,7 +352,13 @@ const majorsList: string[] = allMajors
|
||||||
if (file) {
|
if (file) {
|
||||||
uploadStatus.textContent = "Uploading...";
|
uploadStatus.textContent = "Uploading...";
|
||||||
try {
|
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";
|
uploadStatus.textContent = "Resume uploaded successfully";
|
||||||
if (currentResume) currentResume.textContent = file.name;
|
if (currentResume) currentResume.textContent = file.name;
|
||||||
|
|
||||||
|
@ -379,11 +390,13 @@ const majorsList: string[] = allMajors
|
||||||
button.classList.add("loading");
|
button.classList.add("loading");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authState = auth.getAuthState();
|
const user = auth.getCurrentUser();
|
||||||
|
if (!user) throw new Error("User not authenticated");
|
||||||
|
|
||||||
const oldData = {
|
const oldData = {
|
||||||
major: authState.model?.major || null,
|
major: user.major || null,
|
||||||
graduation_year: authState.model?.graduation_year || null,
|
graduation_year: user.graduation_year || null,
|
||||||
member_id: authState.model?.member_id || null,
|
member_id: user.member_id || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newData = {
|
const newData = {
|
||||||
|
@ -394,7 +407,7 @@ const majorsList: string[] = allMajors
|
||||||
member_id: memberIdInput?.value || null,
|
member_id: memberIdInput?.value || null,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
await auth.updateProfileSettings(newData);
|
await update.updateFields("users", user.id, newData);
|
||||||
|
|
||||||
// Create detailed log message of changes
|
// Create detailed log message of changes
|
||||||
const changes = [];
|
const changes = [];
|
||||||
|
@ -484,10 +497,8 @@ const majorsList: string[] = allMajors
|
||||||
loadUserData();
|
loadUserData();
|
||||||
|
|
||||||
// Update when auth state changes
|
// Update when auth state changes
|
||||||
window.addEventListener("storage", (e) => {
|
auth.onAuthStateChange(() => {
|
||||||
if (e.key === "pocketbase_auth") {
|
loadUserData();
|
||||||
loadUserData();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</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:
|
roles:
|
||||||
administrator:
|
administrator:
|
||||||
name: IEEE 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>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { StoreAuth } from "../components/auth/StoreAuth";
|
import { Authentication } from "../components/pocketbase/Authentication";
|
||||||
new StoreAuth();
|
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>
|
</script>
|
||||||
|
|
|
@ -4,7 +4,13 @@ import UserProfile from "../components/auth/UserProfile.astro";
|
||||||
import DefaultProfileView from "../components/profile/DefaultProfileView.astro";
|
import DefaultProfileView from "../components/profile/DefaultProfileView.astro";
|
||||||
import OfficerProfileView from "../components/profile/OfficerView.astro";
|
import OfficerProfileView from "../components/profile/OfficerView.astro";
|
||||||
import UserSettings from "../components/profile/UserSettings.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 title = "User Profile";
|
||||||
|
const config = yaml.load(profileConfig) as any;
|
||||||
|
const text = yaml.load(textConfig) as any;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout {title}>
|
<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"
|
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>
|
clip-rule="evenodd"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span
|
<span>{text.ui.messages.auth.loginError}</span>
|
||||||
>Failed to load profile data. Please try refreshing
|
|
||||||
the page.</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -198,7 +201,7 @@ const title = "User Profile";
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content Areas -->
|
<!-- Content Areas -->
|
||||||
<div id="defaultView">
|
<!-- <div id="defaultView">
|
||||||
<DefaultProfileView />
|
<DefaultProfileView />
|
||||||
</div>
|
</div>
|
||||||
<div id="settingsView" class="hidden">
|
<div id="settingsView" class="hidden">
|
||||||
|
@ -206,7 +209,7 @@ const title = "User Profile";
|
||||||
</div>
|
</div>
|
||||||
<div id="officerView" class="hidden">
|
<div id="officerView" class="hidden">
|
||||||
<OfficerProfileView />
|
<OfficerProfileView />
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -214,8 +217,14 @@ const title = "User Profile";
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { StoreAuth } from "../components/auth/StoreAuth";
|
import { Authentication } from "../components/pocketbase/Authentication";
|
||||||
const auth = new StoreAuth();
|
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
|
// Initialize page state
|
||||||
const pageLoadingState = document.getElementById("pageLoadingState");
|
const pageLoadingState = document.getElementById("pageLoadingState");
|
||||||
|
@ -224,10 +233,9 @@ const title = "User Profile";
|
||||||
"notAuthenticatedState"
|
"notAuthenticatedState"
|
||||||
);
|
);
|
||||||
const mainContent = document.getElementById("mainContent");
|
const mainContent = document.getElementById("mainContent");
|
||||||
const tabs = document.querySelectorAll(".tab");
|
|
||||||
const defaultView = document.getElementById("defaultView");
|
const defaultView = document.getElementById("defaultView");
|
||||||
const officerView = document.getElementById("officerView");
|
const officerView = document.getElementById("officerView");
|
||||||
const officerTab = document.getElementById("officerTab");
|
const settingsView = document.getElementById("settingsView");
|
||||||
|
|
||||||
// Stats elements
|
// Stats elements
|
||||||
const eventsAttendedValue = document.getElementById("eventsAttendedValue");
|
const eventsAttendedValue = document.getElementById("eventsAttendedValue");
|
||||||
|
@ -236,20 +244,15 @@ const title = "User Profile";
|
||||||
const activityLevelValue = document.getElementById("activityLevelValue");
|
const activityLevelValue = document.getElementById("activityLevelValue");
|
||||||
const activityLevelDesc = document.getElementById("activityLevelDesc");
|
const activityLevelDesc = document.getElementById("activityLevelDesc");
|
||||||
|
|
||||||
// Hide officer tab by default
|
// Show officer view if user has appropriate role
|
||||||
if (officerTab) {
|
const showOfficerView = (user: any) => {
|
||||||
officerTab.style.display = "none";
|
if (!user) return false;
|
||||||
}
|
const userRole = user.role || "member";
|
||||||
|
const roleConfig = config.roles[userRole];
|
||||||
// Show officer tab if user is an officer
|
return (
|
||||||
const showOfficerTab = () => {
|
roleConfig?.permissions?.includes("manage") ||
|
||||||
if (officerTab) {
|
roleConfig?.permissions?.includes("edit")
|
||||||
const authState = auth.getAuthState();
|
);
|
||||||
const isOfficer =
|
|
||||||
authState.model?.member_type === "officer" ||
|
|
||||||
authState.model?.member_type === "administrator";
|
|
||||||
officerTab.style.display = isOfficer ? "inline-flex" : "none";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize page
|
// Initialize page
|
||||||
|
@ -263,16 +266,14 @@ const title = "User Profile";
|
||||||
if (mainContent) mainContent.classList.add("hidden");
|
if (mainContent) mainContent.classList.add("hidden");
|
||||||
|
|
||||||
// Check auth state
|
// Check auth state
|
||||||
const authState = auth.getAuthState();
|
if (!auth.isAuthenticated()) {
|
||||||
if (!authState.isValid || !authState.model) {
|
|
||||||
// Show not authenticated state
|
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||||
if (notAuthenticatedState)
|
if (notAuthenticatedState)
|
||||||
notAuthenticatedState.classList.remove("hidden");
|
notAuthenticatedState.classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = authState.model;
|
const user = auth.getCurrentUser();
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
if (eventsAttendedValue) {
|
if (eventsAttendedValue) {
|
||||||
|
@ -283,16 +284,14 @@ const title = "User Profile";
|
||||||
if (loyaltyPointsValue && loyaltyPointsChange) {
|
if (loyaltyPointsValue && loyaltyPointsChange) {
|
||||||
const points = user.points || 0;
|
const points = user.points || 0;
|
||||||
loyaltyPointsValue.textContent = points.toString();
|
loyaltyPointsValue.textContent = points.toString();
|
||||||
// Calculate points change (example logic)
|
const pointsChange = user.points_change_30d || 0;
|
||||||
const pointsChange = 10; // Replace with actual calculation
|
|
||||||
loyaltyPointsChange.textContent =
|
loyaltyPointsChange.textContent =
|
||||||
pointsChange > 0
|
pointsChange >= 0
|
||||||
? `↗︎ ${pointsChange} points (30 days)`
|
? `↗︎ ${pointsChange} points (30 days)`
|
||||||
: `↘︎ ${Math.abs(pointsChange)} points (30 days)`;
|
: `↘︎ ${Math.abs(pointsChange)} points (30 days)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activityLevelValue && activityLevelDesc) {
|
if (activityLevelValue && activityLevelDesc) {
|
||||||
// Calculate activity level based on events and points
|
|
||||||
const eventsAttended = user.events_attended?.length || 0;
|
const eventsAttended = user.events_attended?.length || 0;
|
||||||
const points = user.points || 0;
|
const points = user.points || 0;
|
||||||
let activityLevel = "Low";
|
let activityLevel = "Low";
|
||||||
|
@ -310,8 +309,14 @@ const title = "User Profile";
|
||||||
activityLevelDesc.textContent = description;
|
activityLevelDesc.textContent = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show officer tab if applicable
|
// Show appropriate view based on user role
|
||||||
showOfficerTab();
|
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
|
// Hide loading state and show content
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||||
|
@ -326,10 +331,8 @@ const title = "User Profile";
|
||||||
|
|
||||||
// Check on load and auth changes
|
// Check on load and auth changes
|
||||||
initializePage();
|
initializePage();
|
||||||
window.addEventListener("storage", (e) => {
|
auth.onAuthStateChange(() => {
|
||||||
if (e.key === "pocketbase_auth") {
|
initializePage();
|
||||||
initializePage();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle default view tab switching
|
// Handle default view tab switching
|
||||||
|
@ -342,8 +345,6 @@ const title = "User Profile";
|
||||||
|
|
||||||
// Update content visibility
|
// Update content visibility
|
||||||
const tabId = (tab as HTMLElement).dataset.defaultTab;
|
const tabId = (tab as HTMLElement).dataset.defaultTab;
|
||||||
const defaultView = document.getElementById("defaultView");
|
|
||||||
const settingsView = document.getElementById("settingsView");
|
|
||||||
|
|
||||||
if (defaultView && settingsView) {
|
if (defaultView && settingsView) {
|
||||||
defaultView.classList.add("hidden");
|
defaultView.classList.add("hidden");
|
||||||
|
@ -364,19 +365,18 @@ const title = "User Profile";
|
||||||
// Add login button event listener
|
// Add login button event listener
|
||||||
const loginButtons = document.querySelectorAll(".login-button");
|
const loginButtons = document.querySelectorAll(".login-button");
|
||||||
loginButtons.forEach((button) => {
|
loginButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", async () => {
|
||||||
// Show loading state while authentication is in progress
|
try {
|
||||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
if (pageLoadingState)
|
||||||
if (notAuthenticatedState)
|
pageLoadingState.classList.remove("hidden");
|
||||||
notAuthenticatedState.classList.add("hidden");
|
if (notAuthenticatedState)
|
||||||
|
notAuthenticatedState.classList.add("hidden");
|
||||||
// Call the handleLogin method from StoreAuth
|
await auth.login();
|
||||||
auth.handleLogin().catch((error) => {
|
} catch (error) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
// Show error state if login fails
|
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue