Add authentication #17
8 changed files with 0 additions and 3503 deletions
|
@ -1,335 +0,0 @@
|
||||||
import PocketBase from "pocketbase";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
|
|
||||||
import textConfig from "../../config/text.yml?raw";
|
|
||||||
import { SendLog } from "../pocketbase/SendLog";
|
|
||||||
|
|
||||||
// Configuration type definitions
|
|
||||||
interface Config {
|
|
||||||
api: {
|
|
||||||
baseUrl: string;
|
|
||||||
oauth2: {
|
|
||||||
redirectPath: string;
|
|
||||||
providerName: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse YAML configuration
|
|
||||||
const config = yaml.load(pocketbaseConfig) as Config;
|
|
||||||
const text = yaml.load(textConfig) as any;
|
|
||||||
|
|
||||||
interface AuthElements {
|
|
||||||
eventCodeInput: HTMLInputElement;
|
|
||||||
checkInButton: HTMLButtonElement;
|
|
||||||
checkInStatus: HTMLParagraphElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EventCheckIn {
|
|
||||||
private pb: PocketBase;
|
|
||||||
private elements: AuthElements;
|
|
||||||
private logger: SendLog;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.pb = new PocketBase(config.api.baseUrl);
|
|
||||||
this.elements = this.getElements();
|
|
||||||
this.logger = SendLog.getInstance();
|
|
||||||
|
|
||||||
// Add event listener for the check-in button
|
|
||||||
this.elements.checkInButton.addEventListener("click", () => this.handleCheckIn());
|
|
||||||
|
|
||||||
// Add event listener for the enter key on the input field
|
|
||||||
this.elements.eventCodeInput.addEventListener("keypress", (event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
this.handleCheckIn();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getElements(): AuthElements {
|
|
||||||
// Get both skeleton and content elements
|
|
||||||
const eventCodeInput = document.getElementById("eventCodeInput") as HTMLInputElement;
|
|
||||||
const checkInButton = document.getElementById("checkInButton") as HTMLButtonElement;
|
|
||||||
const checkInStatus = document.getElementById("checkInStatus") as HTMLParagraphElement;
|
|
||||||
|
|
||||||
// Get skeleton elements
|
|
||||||
const skeletonEventCodeInput = document.getElementById("skeletonEventCodeInput") as HTMLInputElement;
|
|
||||||
const skeletonCheckInButton = document.getElementById("skeletonCheckInButton") as HTMLButtonElement;
|
|
||||||
const skeletonCheckInStatus = document.getElementById("skeletonCheckInStatus") as HTMLParagraphElement;
|
|
||||||
|
|
||||||
// Check for required elements (only need one set)
|
|
||||||
if ((!eventCodeInput || !checkInButton || !checkInStatus) &&
|
|
||||||
(!skeletonEventCodeInput || !skeletonCheckInButton || !skeletonCheckInStatus)) {
|
|
||||||
throw new Error("Required DOM elements not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return whichever set is available (prefer content over skeleton)
|
|
||||||
return {
|
|
||||||
eventCodeInput: eventCodeInput || skeletonEventCodeInput,
|
|
||||||
checkInButton: checkInButton || skeletonCheckInButton,
|
|
||||||
checkInStatus: checkInStatus || skeletonCheckInStatus,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateEventCode(code: string): Promise<{ isValid: boolean; event?: any; message?: string }> {
|
|
||||||
try {
|
|
||||||
// Find event by code
|
|
||||||
const events = await this.pb.collection('events').getFullList({
|
|
||||||
filter: `event_code = "${code}"`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (events.length === 0) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
message: text.ui.messages.event.checkIn.invalid
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = events[0];
|
|
||||||
const now = new Date();
|
|
||||||
const startDate = new Date(event.start_date);
|
|
||||||
const endDate = new Date(event.end_date);
|
|
||||||
|
|
||||||
// Check if current time is within event window
|
|
||||||
if (now < startDate) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
event,
|
|
||||||
message: text.ui.messages.event.checkIn.expired
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now > endDate) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
event,
|
|
||||||
message: text.ui.messages.event.checkIn.expired
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isValid: true, event };
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to validate event code:', err);
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
message: text.ui.messages.event.checkIn.error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleCheckIn() {
|
|
||||||
const { eventCodeInput, checkInStatus } = this.elements;
|
|
||||||
const eventCode = eventCodeInput.value.trim();
|
|
||||||
|
|
||||||
if (!eventCode) {
|
|
||||||
this.showStatus(text.ui.messages.event.checkIn.invalid, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let validation: { isValid: boolean; event?: any; message?: string } | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.showStatus(text.ui.messages.event.checkIn.checking, "info");
|
|
||||||
|
|
||||||
// Get current user
|
|
||||||
const user = this.pb.authStore.model;
|
|
||||||
if (!user) {
|
|
||||||
this.showStatus(text.ui.messages.auth.notSignedIn, "error");
|
|
||||||
await this.logger.send(
|
|
||||||
"error",
|
|
||||||
"event check in",
|
|
||||||
"Check-in attempt failed: User not authenticated"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate event code and check time window
|
|
||||||
validation = await this.validateEventCode(eventCode);
|
|
||||||
if (!validation.isValid) {
|
|
||||||
this.showStatus(validation.message || text.ui.messages.event.checkIn.invalid, "error");
|
|
||||||
await this.logger.send(
|
|
||||||
"error",
|
|
||||||
"event check in",
|
|
||||||
`Invalid event code attempt: "${eventCode}". Reason: ${validation.message}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = validation.event;
|
|
||||||
|
|
||||||
// Get user's attended events and current points
|
|
||||||
const currentUser = await this.pb.collection("users").getOne(user.id);
|
|
||||||
let eventsAttended: string[] = [];
|
|
||||||
let currentPoints = currentUser.points || 0;
|
|
||||||
|
|
||||||
// Handle different cases for events_attended field
|
|
||||||
if (currentUser.events_attended) {
|
|
||||||
if (Array.isArray(currentUser.events_attended)) {
|
|
||||||
eventsAttended = currentUser.events_attended;
|
|
||||||
} else if (typeof currentUser.events_attended === 'string') {
|
|
||||||
try {
|
|
||||||
eventsAttended = JSON.parse(currentUser.events_attended);
|
|
||||||
} catch (err) {
|
|
||||||
eventsAttended = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure eventsAttended is an array
|
|
||||||
if (!Array.isArray(eventsAttended)) {
|
|
||||||
eventsAttended = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already checked in using event_id
|
|
||||||
const isAlreadyCheckedIn = eventsAttended.includes(event.event_id);
|
|
||||||
|
|
||||||
if (isAlreadyCheckedIn) {
|
|
||||||
this.showStatus(text.ui.messages.event.checkIn.alreadyCheckedIn, "info");
|
|
||||||
await this.logger.send(
|
|
||||||
"error",
|
|
||||||
"event check in",
|
|
||||||
`Duplicate check-in attempt for event "${event.event_name}" (${event.event_id})`
|
|
||||||
);
|
|
||||||
eventCodeInput.value = ""; // Clear input
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event_id to user's attended events and update points
|
|
||||||
eventsAttended.push(event.event_id);
|
|
||||||
const pointsToAdd = event.points_to_reward || 0;
|
|
||||||
const newTotalPoints = currentPoints + pointsToAdd;
|
|
||||||
|
|
||||||
// Update user with new events_attended and points
|
|
||||||
await this.pb.collection("users").update(user.id, {
|
|
||||||
events_attended: JSON.stringify(eventsAttended),
|
|
||||||
points: newTotalPoints
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update event's attendees list
|
|
||||||
let eventAttendees = [];
|
|
||||||
try {
|
|
||||||
eventAttendees = JSON.parse(event.attendees || '[]');
|
|
||||||
} catch (err) {
|
|
||||||
eventAttendees = [];
|
|
||||||
}
|
|
||||||
if (!Array.isArray(eventAttendees)) {
|
|
||||||
eventAttendees = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user to attendees if not already present
|
|
||||||
if (!eventAttendees.includes(user.id)) {
|
|
||||||
eventAttendees.push(user.id);
|
|
||||||
await this.pb.collection("events").update(event.id, {
|
|
||||||
attendees: JSON.stringify(eventAttendees)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show success message with points
|
|
||||||
this.showStatus(text.ui.messages.event.checkIn.success, "success");
|
|
||||||
eventCodeInput.value = ""; // Clear input
|
|
||||||
|
|
||||||
// Log the successful check-in
|
|
||||||
await this.logger.send(
|
|
||||||
"create",
|
|
||||||
"event check in",
|
|
||||||
`Successfully checked in to event ${event.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log the points update
|
|
||||||
await this.logger.send(
|
|
||||||
"update",
|
|
||||||
"loyalty points",
|
|
||||||
`Points updated from ${currentPoints} to ${newTotalPoints} (+${pointsToAdd} from event ${event.id})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update event attendance count
|
|
||||||
const currentAttendance = event.attendance_count || 0;
|
|
||||||
|
|
||||||
await this.pb.collection('events').update(event.id, {
|
|
||||||
attendance_count: currentAttendance + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the attendance update
|
|
||||||
await this.logger.send(
|
|
||||||
"update",
|
|
||||||
"event attendance",
|
|
||||||
`Event ${event.id} attendance updated to ${currentAttendance + 1}`
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Check-in error:", err);
|
|
||||||
this.showStatus(text.ui.messages.event.checkIn.error, "error");
|
|
||||||
|
|
||||||
// Log any errors that occur during check-in
|
|
||||||
if (validation?.event) {
|
|
||||||
await this.logger.send(
|
|
||||||
"error",
|
|
||||||
"event check in",
|
|
||||||
`Failed to check in to event ${validation.event.id}: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await this.logger.send(
|
|
||||||
"error",
|
|
||||||
"event check in",
|
|
||||||
`Failed to check in: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private showStatus(message: string, type: "error" | "success" | "info") {
|
|
||||||
const { checkInStatus } = this.elements;
|
|
||||||
checkInStatus.textContent = message;
|
|
||||||
checkInStatus.className = "text-xs mt-1";
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "error":
|
|
||||||
checkInStatus.classList.add("text-error");
|
|
||||||
break;
|
|
||||||
case "success":
|
|
||||||
checkInStatus.classList.add("text-success");
|
|
||||||
break;
|
|
||||||
case "info":
|
|
||||||
checkInStatus.classList.add("opacity-70");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear status after timeout
|
|
||||||
if (type !== "info") {
|
|
||||||
setTimeout(() => {
|
|
||||||
checkInStatus.textContent = "";
|
|
||||||
}, text.ui.messages.event.checkIn.messageTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all events a user has checked into
|
|
||||||
* @param userId The ID of the user
|
|
||||||
*/
|
|
||||||
public async getUserEventHistory(userId: string) {
|
|
||||||
try {
|
|
||||||
const records = await this.pb.collection('event_checkins').getFullList({
|
|
||||||
filter: `user_id="${userId}"`,
|
|
||||||
sort: '-created',
|
|
||||||
expand: 'event_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the history retrieval
|
|
||||||
await this.logger.send(
|
|
||||||
"update",
|
|
||||||
"event attendance",
|
|
||||||
`Retrieved attendance history for user: ${records.length} events found`
|
|
||||||
);
|
|
||||||
|
|
||||||
return records;
|
|
||||||
} catch (error) {
|
|
||||||
// Log any errors in retrieving history
|
|
||||||
await this.logger.send(
|
|
||||||
"error",
|
|
||||||
"event attendance",
|
|
||||||
`Failed to retrieve event history: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,354 +0,0 @@
|
||||||
<div>
|
|
||||||
<!-- Loading Skeleton -->
|
|
||||||
<div id="loadingSkeleton" class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<!-- Avatar and Name Section -->
|
|
||||||
<div class="flex flex-col items-center mb-6">
|
|
||||||
<div class="skeleton w-24 h-24 rounded-full mb-4"></div>
|
|
||||||
<div class="skeleton h-8 w-48"></div>
|
|
||||||
<div class="skeleton h-4 w-32 mt-2"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Member Status -->
|
|
||||||
<div class="flex justify-center mb-6">
|
|
||||||
<div class="skeleton h-6 w-32"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
|
||||||
<div class="stats stats-vertical shadow bg-base-200 mb-6">
|
|
||||||
<div class="stat px-6 py-2">
|
|
||||||
<div class="skeleton h-4 w-20 mb-1"></div>
|
|
||||||
<div class="skeleton h-8 w-24"></div>
|
|
||||||
</div>
|
|
||||||
<div class="stat px-6 py-2">
|
|
||||||
<div class="skeleton h-4 w-24 mb-1"></div>
|
|
||||||
<div class="skeleton h-8 w-32"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Member Details -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="skeleton h-4 w-24"></div>
|
|
||||||
<div class="skeleton h-10 w-full"></div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="skeleton h-4 w-20"></div>
|
|
||||||
<div class="skeleton h-10 w-full"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actual Content -->
|
|
||||||
<div id="userInfo" class="card bg-base-100 shadow-xl opacity-0 hidden">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<!-- Avatar and Name Section -->
|
|
||||||
<div class="flex flex-col items-center mb-6">
|
|
||||||
<div class="avatar online placeholder mb-4">
|
|
||||||
<div
|
|
||||||
class="bg-gradient-to-br from-primary to-secondary text-primary-content rounded-full w-24 ring ring-primary ring-offset-base-100 ring-offset-2"
|
|
||||||
>
|
|
||||||
<span id="userInitials" class="text-3xl"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<h2 id="userName" class="text-2xl font-bold">Not signed in</h2>
|
|
||||||
<p id="userEmail" class="text-base-content/70">Not signed in</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Member Status -->
|
|
||||||
<div class="flex justify-center mb-6">
|
|
||||||
<div id="memberStatus" class="badge badge-lg gap-2">
|
|
||||||
<span class="loading loading-ring loading-xs"></span>
|
|
||||||
Not verified
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
|
||||||
<div class="stats stats-vertical shadow bg-base-200 mb-6">
|
|
||||||
<div class="stat px-6 py-2">
|
|
||||||
<div class="stat-title text-xs">Last Login</div>
|
|
||||||
<div id="lastLogin" class="stat-value text-lg">Never</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat px-6 py-2">
|
|
||||||
<div class="stat-title text-xs">Member Since</div>
|
|
||||||
<div class="stat-value text-lg" id="memberSince">-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Auth Buttons -->
|
|
||||||
<div class="pt-4">
|
|
||||||
<button
|
|
||||||
id="contentLoginButton"
|
|
||||||
class="login-button btn btn-primary w-full gap-2 hidden"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
Sign in with IEEEUCSD SSO
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="contentLogoutButton"
|
|
||||||
class="logout-button btn btn-error w-full gap-2 hidden"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#userInfo {
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Authentication } from "../pocketbase/Authentication";
|
|
||||||
import { Update } from "../pocketbase/Update";
|
|
||||||
import { Get } from "../pocketbase/Get";
|
|
||||||
import { SendLog } from "../pocketbase/SendLog";
|
|
||||||
|
|
||||||
// Initialize services
|
|
||||||
const auth = Authentication.getInstance();
|
|
||||||
const update = Update.getInstance();
|
|
||||||
const get = Get.getInstance();
|
|
||||||
const logger = SendLog.getInstance();
|
|
||||||
|
|
||||||
// Get DOM elements
|
|
||||||
const loadingSkeleton = document.getElementById("loadingSkeleton");
|
|
||||||
const userInfo = document.getElementById("userInfo");
|
|
||||||
const userInitials = document.getElementById("userInitials");
|
|
||||||
const userName = document.getElementById("userName");
|
|
||||||
const userEmail = document.getElementById("userEmail");
|
|
||||||
const memberStatus = document.getElementById("memberStatus");
|
|
||||||
const lastLogin = document.getElementById("lastLogin");
|
|
||||||
const memberSince = document.getElementById("memberSince");
|
|
||||||
const contentLoginButton = document.getElementById("contentLoginButton");
|
|
||||||
const contentLogoutButton = document.getElementById("contentLogoutButton");
|
|
||||||
|
|
||||||
// Function to format date
|
|
||||||
function formatDate(dateStr: string): string {
|
|
||||||
return new Date(dateStr).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get initials from name
|
|
||||||
function getInitials(name: string): string {
|
|
||||||
return name
|
|
||||||
.split(" ")
|
|
||||||
.map((n) => n[0])
|
|
||||||
.join("")
|
|
||||||
.toUpperCase()
|
|
||||||
.slice(0, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to update UI with user data
|
|
||||||
async function updateUI(user: any) {
|
|
||||||
if (!user) {
|
|
||||||
// Show login button if not authenticated
|
|
||||||
if (contentLoginButton) contentLoginButton.classList.remove("hidden");
|
|
||||||
if (contentLogoutButton) contentLogoutButton.classList.add("hidden");
|
|
||||||
if (userName) userName.textContent = "Not signed in";
|
|
||||||
if (userEmail) userEmail.textContent = "Not signed in";
|
|
||||||
if (userInitials) userInitials.textContent = "?";
|
|
||||||
if (memberStatus) {
|
|
||||||
memberStatus.textContent = "Not Verified";
|
|
||||||
memberStatus.className = "badge badge-lg gap-2";
|
|
||||||
}
|
|
||||||
if (lastLogin) lastLogin.textContent = "Never";
|
|
||||||
if (memberSince) memberSince.textContent = "-";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get full user data
|
|
||||||
const userData = await get.getOne("users", user.id);
|
|
||||||
|
|
||||||
// Update UI elements
|
|
||||||
if (userName) userName.textContent = userData.name || "Unnamed User";
|
|
||||||
if (userEmail) userEmail.textContent = userData.email || "No email";
|
|
||||||
if (userInitials)
|
|
||||||
userInitials.textContent = getInitials(userData.name || "?");
|
|
||||||
|
|
||||||
// Update member status based on member_type and email domain
|
|
||||||
if (memberStatus) {
|
|
||||||
let memberType = userData.member_type || "IEEE Member";
|
|
||||||
|
|
||||||
// Auto-assign officer status for @ieeeucsd.org emails if they're not already a higher rank
|
|
||||||
if (
|
|
||||||
userData.email?.endsWith("@ieeeucsd.org") &&
|
|
||||||
!["IEEE Executive", "IEEE Administrator", "IEEE Sponsor"].includes(
|
|
||||||
memberType,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
memberType = "IEEE Officer";
|
|
||||||
|
|
||||||
// Update the member type in the database if it's different
|
|
||||||
if (userData.member_type !== memberType) {
|
|
||||||
try {
|
|
||||||
await update.updateField(
|
|
||||||
"users",
|
|
||||||
userData.id,
|
|
||||||
"member_type",
|
|
||||||
memberType,
|
|
||||||
);
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"profile view",
|
|
||||||
`Updated member type to ${memberType} for user with @ieeeucsd.org email`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update member type:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set badge color based on member type
|
|
||||||
let badgeClass = "badge-primary"; // default
|
|
||||||
switch (memberType) {
|
|
||||||
case "IEEE Sponsor":
|
|
||||||
badgeClass = "badge-warning";
|
|
||||||
break;
|
|
||||||
case "IEEE Administrator":
|
|
||||||
badgeClass = "badge-error";
|
|
||||||
break;
|
|
||||||
case "IEEE Executive":
|
|
||||||
badgeClass = "badge-secondary";
|
|
||||||
break;
|
|
||||||
case "IEEE Officer":
|
|
||||||
badgeClass = "badge-info";
|
|
||||||
break;
|
|
||||||
case "IEEE Member":
|
|
||||||
badgeClass = "badge-success";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
memberStatus.textContent = memberType;
|
|
||||||
memberStatus.className = `badge badge-lg gap-2 ${badgeClass}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update timestamps
|
|
||||||
if (lastLogin) lastLogin.textContent = formatDate(userData.last_login);
|
|
||||||
if (memberSince) memberSince.textContent = formatDate(userData.created);
|
|
||||||
|
|
||||||
// Show logout button
|
|
||||||
if (contentLoginButton) contentLoginButton.classList.add("hidden");
|
|
||||||
if (contentLogoutButton) contentLogoutButton.classList.remove("hidden");
|
|
||||||
|
|
||||||
// Show the user info
|
|
||||||
if (loadingSkeleton) loadingSkeleton.style.display = "none";
|
|
||||||
if (userInfo) {
|
|
||||||
userInfo.classList.remove("hidden");
|
|
||||||
userInfo.style.opacity = "1";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update UI:", error);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"profile view",
|
|
||||||
`Failed to update profile UI: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize profile
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
|
||||||
try {
|
|
||||||
// Show loading state
|
|
||||||
if (loadingSkeleton) loadingSkeleton.style.display = "block";
|
|
||||||
if (userInfo) {
|
|
||||||
userInfo.classList.add("hidden");
|
|
||||||
userInfo.style.opacity = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update UI based on current auth state
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
await updateUI(user);
|
|
||||||
|
|
||||||
// Add auth state change listener
|
|
||||||
auth.onAuthStateChange(async () => {
|
|
||||||
const currentUser = auth.getCurrentUser();
|
|
||||||
await updateUI(currentUser);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add login button handler
|
|
||||||
if (contentLoginButton) {
|
|
||||||
contentLoginButton.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
if (loadingSkeleton) loadingSkeleton.style.display = "block";
|
|
||||||
if (userInfo) {
|
|
||||||
userInfo.classList.add("hidden");
|
|
||||||
userInfo.style.opacity = "0";
|
|
||||||
}
|
|
||||||
await auth.login();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login error:", error);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"profile view",
|
|
||||||
`Login failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
if (loadingSkeleton) loadingSkeleton.style.display = "none";
|
|
||||||
if (userInfo) {
|
|
||||||
userInfo.classList.remove("hidden");
|
|
||||||
userInfo.style.opacity = "1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add logout button handler
|
|
||||||
if (contentLogoutButton) {
|
|
||||||
contentLogoutButton.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
await auth.logout();
|
|
||||||
await logger.send(
|
|
||||||
"logout",
|
|
||||||
"profile view",
|
|
||||||
"User logged out successfully",
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Logout error:", error);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"profile view",
|
|
||||||
`Logout failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to initialize profile:", error);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"profile view",
|
|
||||||
`Failed to initialize profile: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,580 +0,0 @@
|
||||||
---
|
|
||||||
// Define the component's props and setup
|
|
||||||
interface Props {}
|
|
||||||
const {} = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full">
|
|
||||||
<!-- Left Column - Events List -->
|
|
||||||
<div class="card bg-base-200 shadow-xl h-full">
|
|
||||||
<div class="card-body p-6 flex flex-col h-full">
|
|
||||||
<!-- Fixed Header Section -->
|
|
||||||
<div class="flex-none">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h2 class="card-title text-2xl">Events</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Check-in Section -->
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-8">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<h2 class="card-title text-2xl mb-4">Quick Check-in</h2>
|
|
||||||
|
|
||||||
<!-- Event Check-in Skeleton -->
|
|
||||||
<div id="eventCheckInSkeleton" class="animate-pulse">
|
|
||||||
<div class="h-6 bg-base-300 rounded w-2/3 mb-4"></div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="h-8 bg-base-300 rounded flex-1"></div>
|
|
||||||
<div class="h-8 bg-base-300 rounded w-24"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Check-in Content -->
|
|
||||||
<div id="eventCheckInContent" class="hidden">
|
|
||||||
<div id="eventCheckInSection" class="space-y-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="eventCodeInput"
|
|
||||||
placeholder="Enter event code"
|
|
||||||
class="input input-bordered flex-1"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<button id="checkInButton" class="btn btn-primary"
|
|
||||||
>Check In</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<p id="checkInStatus" class="text-sm mt-1 opacity-70"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider mt-0 mb-4"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scrollable Events List -->
|
|
||||||
<div class="flex-1 overflow-y-auto min-h-0">
|
|
||||||
<div id="eventsList" class="space-y-4">
|
|
||||||
<!-- Loading Skeletons -->
|
|
||||||
{
|
|
||||||
Array(3)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => (
|
|
||||||
<div class="card bg-base-100 shadow-sm animate-pulse">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="flex justify-between items-start">
|
|
||||||
<div class="space-y-3 w-full">
|
|
||||||
<div class="h-6 bg-base-300 rounded w-3/4" />
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="h-4 bg-base-300 rounded w-1/2 opacity-70" />
|
|
||||||
<div class="h-4 bg-base-300 rounded w-2/3 opacity-70" />
|
|
||||||
<div class="h-4 bg-base-300 rounded w-1/3 opacity-70" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-6 w-20 bg-base-300 rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column - Profile Information -->
|
|
||||||
<div class="card bg-base-200 shadow-xl h-full">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<h2 class="card-title text-2xl mb-6">Past Events</h2>
|
|
||||||
<div id="pastEventsList" class="space-y-4 overflow-y-auto">
|
|
||||||
<!-- Loading Skeletons -->
|
|
||||||
{
|
|
||||||
Array(3)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => (
|
|
||||||
<div class="card bg-base-100 shadow-sm animate-pulse">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="flex justify-between items-start">
|
|
||||||
<div class="space-y-3 w-full">
|
|
||||||
<div class="h-6 bg-base-300 rounded w-3/4" />
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="h-4 bg-base-300 rounded w-1/2 opacity-70" />
|
|
||||||
<div class="h-4 bg-base-300 rounded w-2/3 opacity-70" />
|
|
||||||
<div class="h-4 bg-base-300 rounded w-1/3 opacity-70" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-6 w-20 bg-base-300 rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// TypeScript declarations
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
showEventFiles: (eventId: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import { Authentication } from "../pocketbase/Authentication";
|
|
||||||
import { Get } from "../pocketbase/Get";
|
|
||||||
import { SendLog } from "../pocketbase/SendLog";
|
|
||||||
import { FileManager } from "../pocketbase/FileManager";
|
|
||||||
|
|
||||||
const auth = Authentication.getInstance();
|
|
||||||
const get = Get.getInstance();
|
|
||||||
const logger = SendLog.getInstance();
|
|
||||||
const fileManager = FileManager.getInstance();
|
|
||||||
|
|
||||||
// Initialize the modal state
|
|
||||||
window.fileViewerModal = {
|
|
||||||
isOpen: false,
|
|
||||||
files: [],
|
|
||||||
setIsOpen: (isOpen: boolean) => {
|
|
||||||
window.fileViewerModal.isOpen = isOpen;
|
|
||||||
const modal = document.querySelector("file-viewer-modal") as any;
|
|
||||||
if (modal) {
|
|
||||||
modal.setAttribute("isOpen", isOpen.toString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setFiles: (files) => {
|
|
||||||
window.fileViewerModal.files = files;
|
|
||||||
const modal = document.querySelector("file-viewer-modal") as any;
|
|
||||||
if (modal) {
|
|
||||||
modal.setAttribute("files", JSON.stringify(files));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Track if we're currently fetching events
|
|
||||||
let isFetchingEvents = false;
|
|
||||||
let lastFetchPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
// Define interfaces
|
|
||||||
interface BaseRecord {
|
|
||||||
id: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Event extends BaseRecord {
|
|
||||||
id: string;
|
|
||||||
event_id: string;
|
|
||||||
event_name: string;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
location: string;
|
|
||||||
files?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add debounce function
|
|
||||||
function debounce<T extends (...args: any[]) => any>(
|
|
||||||
func: T,
|
|
||||||
wait: number,
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
return (...args: Parameters<T>) => {
|
|
||||||
if (timeout) clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => func(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create debounced version of renderEvents
|
|
||||||
const debouncedRenderEvents = debounce(async () => {
|
|
||||||
try {
|
|
||||||
await renderEvents();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to render events:", error);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// Function to show content and hide skeleton
|
|
||||||
function showEventCheckIn() {
|
|
||||||
const eventCheckInSkeleton = document.getElementById(
|
|
||||||
"eventCheckInSkeleton",
|
|
||||||
);
|
|
||||||
const eventCheckInContent = document.getElementById("eventCheckInContent");
|
|
||||||
if (eventCheckInSkeleton && eventCheckInContent) {
|
|
||||||
eventCheckInSkeleton.classList.add("hidden");
|
|
||||||
eventCheckInContent.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to format date
|
|
||||||
function formatDate(dateStr: string): string {
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return date.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to check if event is upcoming
|
|
||||||
function isUpcoming(startDate: string): boolean {
|
|
||||||
const now = new Date();
|
|
||||||
const start = new Date(startDate);
|
|
||||||
return start > now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to check if event is current
|
|
||||||
function isCurrent(startDate: string, endDate: string): boolean {
|
|
||||||
const now = new Date();
|
|
||||||
const start = new Date(startDate);
|
|
||||||
const end = new Date(endDate);
|
|
||||||
return start <= now && now <= end;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get event status
|
|
||||||
function getEventStatus(
|
|
||||||
event: any,
|
|
||||||
isAttended: boolean,
|
|
||||||
): { status: string; badge: string } {
|
|
||||||
if (isAttended) {
|
|
||||||
return {
|
|
||||||
status: "Attended",
|
|
||||||
badge: "badge-success",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCurrent(event.start_date, event.end_date)) {
|
|
||||||
return {
|
|
||||||
status: "Current",
|
|
||||||
badge: "badge-warning",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpcoming(event.start_date)) {
|
|
||||||
return {
|
|
||||||
status: "Upcoming",
|
|
||||||
badge: "badge-info",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: "Past",
|
|
||||||
badge: "badge-ghost",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get status icon
|
|
||||||
function getStatusIcon(status: string): string {
|
|
||||||
const checkIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
const clockIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
const exclamationIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case "Attended":
|
|
||||||
return checkIcon;
|
|
||||||
case "Current":
|
|
||||||
return exclamationIcon;
|
|
||||||
case "Upcoming":
|
|
||||||
return clockIcon;
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to render event card
|
|
||||||
function renderEventCard(event: Event, attendedEvents: string[]): string {
|
|
||||||
const isAttended =
|
|
||||||
Array.isArray(attendedEvents) && attendedEvents.includes(event.event_id);
|
|
||||||
const hasFiles =
|
|
||||||
event.files && Array.isArray(event.files) && event.files.length > 0;
|
|
||||||
const isPastEvent = new Date(event.end_date) < new Date();
|
|
||||||
const { status, badge } = getEventStatus(event, isAttended);
|
|
||||||
|
|
||||||
// Only show files button for past events
|
|
||||||
const filesButton =
|
|
||||||
hasFiles && isPastEvent
|
|
||||||
? `
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-xs gap-2"
|
|
||||||
onclick="window.showEventFiles('${event.id}')"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
${event.files?.length || 0} File${(event.files?.length || 0) > 1 ? "s" : ""}
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="flex justify-between items-start gap-4">
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<h3 class="font-medium text-lg truncate">${event.event_name}</h3>
|
|
||||||
<div class="badge ${badge} gap-1">
|
|
||||||
${getStatusIcon(status)}
|
|
||||||
${status}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm opacity-70 space-y-1">
|
|
||||||
<p>Starts: ${formatDate(event.start_date)}</p>
|
|
||||||
<p>Ends: ${formatDate(event.end_date)}</p>
|
|
||||||
${
|
|
||||||
event.location
|
|
||||||
? `<p class="flex items-center gap-1">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
${event.location}
|
|
||||||
</p>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
${hasFiles && isPastEvent ? `<div class="mt-3">${filesButton}</div>` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to render events with request cancellation handling
|
|
||||||
async function renderEvents() {
|
|
||||||
const eventsList = document.getElementById("eventsList");
|
|
||||||
const pastEventsList = document.getElementById("pastEventsList");
|
|
||||||
if (!eventsList || !pastEventsList) return;
|
|
||||||
|
|
||||||
// If we're already fetching, wait for the current fetch to complete
|
|
||||||
if (isFetchingEvents && lastFetchPromise) {
|
|
||||||
try {
|
|
||||||
await lastFetchPromise;
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Previous fetch failed:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up new fetch
|
|
||||||
isFetchingEvents = true;
|
|
||||||
const fetchPromise = (async () => {
|
|
||||||
try {
|
|
||||||
// Get current user's attended events with safe parsing
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
let attendedEvents: string[] = [];
|
|
||||||
|
|
||||||
if (user?.events_attended) {
|
|
||||||
try {
|
|
||||||
attendedEvents =
|
|
||||||
typeof user.events_attended === "string"
|
|
||||||
? JSON.parse(user.events_attended)
|
|
||||||
: Array.isArray(user.events_attended)
|
|
||||||
? user.events_attended
|
|
||||||
: [];
|
|
||||||
console.log("Attended events:", attendedEvents);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to parse events_attended:", e);
|
|
||||||
attendedEvents = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all events
|
|
||||||
console.log("Fetching all events");
|
|
||||||
const events = await get.getAll<Event>(
|
|
||||||
"events",
|
|
||||||
undefined,
|
|
||||||
"-start_date",
|
|
||||||
);
|
|
||||||
console.log("Fetched events:", events);
|
|
||||||
|
|
||||||
if (!Array.isArray(events)) {
|
|
||||||
throw new Error("Failed to fetch events: Invalid response format");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear loading skeletons
|
|
||||||
eventsList.innerHTML = "";
|
|
||||||
pastEventsList.innerHTML = "";
|
|
||||||
|
|
||||||
// Categorize events
|
|
||||||
const now = new Date();
|
|
||||||
const currentEvents: Event[] = [];
|
|
||||||
const upcomingEvents: Event[] = [];
|
|
||||||
const pastEvents: Event[] = [];
|
|
||||||
|
|
||||||
events.forEach((event) => {
|
|
||||||
if (!event.start_date || !event.end_date) {
|
|
||||||
console.warn("Event missing dates:", event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDate = new Date(event.start_date);
|
|
||||||
const endDate = new Date(event.end_date);
|
|
||||||
|
|
||||||
if (startDate > now) {
|
|
||||||
upcomingEvents.push(event);
|
|
||||||
} else if (endDate >= now && startDate <= now) {
|
|
||||||
currentEvents.push(event);
|
|
||||||
} else {
|
|
||||||
pastEvents.push(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort upcoming events by start date
|
|
||||||
const sortedUpcomingEvents = upcomingEvents.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(a.start_date).getTime() - new Date(b.start_date).getTime(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort past events by date descending (most recent first)
|
|
||||||
const sortedPastEvents = pastEvents.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.end_date).getTime() - new Date(a.end_date).getTime(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update past events count
|
|
||||||
const pastEventsCountElement =
|
|
||||||
document.getElementById("pastEventsCount");
|
|
||||||
if (pastEventsCountElement) {
|
|
||||||
pastEventsCountElement.textContent =
|
|
||||||
sortedPastEvents.length.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to render section
|
|
||||||
function renderSection(events: Event[]): string {
|
|
||||||
if (events.length === 0) {
|
|
||||||
return `
|
|
||||||
<div class="text-center py-8 opacity-70">
|
|
||||||
<p>No events found</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return events
|
|
||||||
.map((event) => renderEventCard(event, attendedEvents))
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update main events list (current & upcoming)
|
|
||||||
eventsList.innerHTML = renderSection([
|
|
||||||
...currentEvents,
|
|
||||||
...sortedUpcomingEvents,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update past events list
|
|
||||||
pastEventsList.innerHTML = renderSection(sortedPastEvents);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to render events:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"render events",
|
|
||||||
`Failed to render events: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
const errorMessage = `
|
|
||||||
<div class="text-center py-8 text-error">
|
|
||||||
<p>Failed to load events. Please try again later.</p>
|
|
||||||
<p class="text-sm mt-2 opacity-70">${err instanceof Error ? err.message : "Unknown error"}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
eventsList.innerHTML = errorMessage;
|
|
||||||
pastEventsList.innerHTML = errorMessage;
|
|
||||||
throw err; // Re-throw to handle in the outer catch
|
|
||||||
} finally {
|
|
||||||
isFetchingEvents = false;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
lastFetchPromise = fetchPromise;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetchPromise;
|
|
||||||
} catch (err) {
|
|
||||||
// Error already handled above
|
|
||||||
console.debug("Fetch completed with error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize event check-in
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
showEventCheckIn();
|
|
||||||
// Only render events once at startup
|
|
||||||
renderEvents().catch(console.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show content when auth state changes
|
|
||||||
let lastAuthState = auth.isAuthenticated();
|
|
||||||
auth.onAuthStateChange((isValid) => {
|
|
||||||
showEventCheckIn();
|
|
||||||
// Only re-render if auth state actually changed
|
|
||||||
if (lastAuthState !== isValid) {
|
|
||||||
lastAuthState = isValid;
|
|
||||||
renderEvents().catch(console.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the showEventFiles function to use custom events
|
|
||||||
window.showEventFiles = async (eventId: string) => {
|
|
||||||
try {
|
|
||||||
// Fetch the event data
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!event.files ||
|
|
||||||
!Array.isArray(event.files) ||
|
|
||||||
event.files.length === 0
|
|
||||||
) {
|
|
||||||
console.warn("No files available for event:", eventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert files to the format expected by FileViewerModal
|
|
||||||
const files = event.files.map((file: string) => {
|
|
||||||
const fileName = file.split("/").pop() || "File";
|
|
||||||
const fileUrl = fileManager.getFileUrl("events", event.id, file);
|
|
||||||
const fileType = getFileType(fileName);
|
|
||||||
return {
|
|
||||||
url: fileUrl,
|
|
||||||
type: fileType,
|
|
||||||
name: fileName,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dispatch custom event to show files
|
|
||||||
const showFileViewerEvent = new CustomEvent("showFileViewer", {
|
|
||||||
detail: { files },
|
|
||||||
});
|
|
||||||
window.dispatchEvent(showFileViewerEvent);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load event files:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"show event files",
|
|
||||||
`Failed to load event files: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to determine file type
|
|
||||||
function getFileType(fileName: string): string {
|
|
||||||
const extension = fileName.split(".").pop()?.toLowerCase() || "";
|
|
||||||
const mimeTypes: Record<string, string> = {
|
|
||||||
pdf: "application/pdf",
|
|
||||||
jpg: "image/jpeg",
|
|
||||||
jpeg: "image/jpeg",
|
|
||||||
png: "image/png",
|
|
||||||
gif: "image/gif",
|
|
||||||
mp4: "video/mp4",
|
|
||||||
webm: "video/webm",
|
|
||||||
mp3: "audio/mpeg",
|
|
||||||
wav: "audio/wav",
|
|
||||||
txt: "text/plain",
|
|
||||||
json: "application/json",
|
|
||||||
doc: "application/msword",
|
|
||||||
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
};
|
|
||||||
|
|
||||||
return mimeTypes[extension] || "application/octet-stream";
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,446 +0,0 @@
|
||||||
---
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import textConfig from "../../config/text.yml?raw";
|
|
||||||
|
|
||||||
// Parse YAML configuration
|
|
||||||
const text = yaml.load(textConfig) as any;
|
|
||||||
const { editor_title, form } = text.ui.tables.events;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Event Editor Dialog -->
|
|
||||||
<dialog id="eventEditor" class="modal">
|
|
||||||
<div class="modal-box w-11/12 max-w-4xl">
|
|
||||||
<h3 class="font-bold text-lg mb-6">{editor_title}</h3>
|
|
||||||
<form
|
|
||||||
class="space-y-6"
|
|
||||||
id="eventForm"
|
|
||||||
novalidate
|
|
||||||
onsubmit="event.preventDefault();"
|
|
||||||
>
|
|
||||||
<!-- Basic Info Section -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorEventId">
|
|
||||||
<span class="label-text">{form.event_id.label}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="editorEventId"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.event_id.placeholder}
|
|
||||||
pattern="[A-Za-z0-9_\-]+"
|
|
||||||
minlength="3"
|
|
||||||
maxlength="50"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="eventIdError">This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorEventCode">
|
|
||||||
<span class="label-text">{form.event_code.label}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="editorEventCode"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.event_code.placeholder}
|
|
||||||
pattern="[A-Za-z0-9_\-]+"
|
|
||||||
minlength="3"
|
|
||||||
maxlength="20"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="eventCodeError">This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorEventName">
|
|
||||||
<span class="label-text">{form.event_name.label}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="editorEventName"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.event_name.placeholder}
|
|
||||||
minlength="3"
|
|
||||||
maxlength="100"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="eventNameError">This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Date/Time Section -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<h4 class="font-medium text-sm opacity-70">
|
|
||||||
Start Date/Time
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorStartDate">
|
|
||||||
<span class="label-text"
|
|
||||||
>{form.start_date.date_label}</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
id="editorStartDate"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.start_date.date_placeholder}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="startDateError"
|
|
||||||
>This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorStartTime">
|
|
||||||
<span class="label-text"
|
|
||||||
>{form.start_date.time_label}</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
id="editorStartTime"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.start_date.time_placeholder}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="startTimeError"
|
|
||||||
>This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<h4 class="font-medium text-sm opacity-70">
|
|
||||||
End Date/Time
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorEndDate">
|
|
||||||
<span class="label-text"
|
|
||||||
>{form.end_date.date_label}</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
id="editorEndDate"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.end_date.date_placeholder}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="endDateError"
|
|
||||||
>This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorEndTime">
|
|
||||||
<span class="label-text"
|
|
||||||
>{form.end_date.time_label}</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
id="editorEndTime"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.end_date.time_placeholder}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="endTimeError"
|
|
||||||
>This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Info Section -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorPointsToReward">
|
|
||||||
<span class="label-text"
|
|
||||||
>{form.points_to_reward.label}</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="editorPointsToReward"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.points_to_reward.placeholder}
|
|
||||||
min="0"
|
|
||||||
max="1000"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="pointsError">This field is required</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorLocation">
|
|
||||||
<span class="label-text">{form.location.label}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="editorLocation"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder={form.location.placeholder}
|
|
||||||
maxlength="200"
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error hidden"
|
|
||||||
id="locationError"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Files Section -->
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="editorFiles">
|
|
||||||
<span class="label-text">{form.files.label}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="editorFiles"
|
|
||||||
class="file-input file-input-bordered w-full"
|
|
||||||
multiple
|
|
||||||
accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png"
|
|
||||||
/>
|
|
||||||
<div id="uploadProgress" class="w-full mt-2 hidden">
|
|
||||||
<div class="flex justify-between mb-1 text-xs opacity-70">
|
|
||||||
<span>Uploading files...</span>
|
|
||||||
<span id="uploadProgressText">0%</span>
|
|
||||||
</div>
|
|
||||||
<progress
|
|
||||||
id="uploadProgressBar"
|
|
||||||
class="progress progress-primary w-full"
|
|
||||||
value="0"
|
|
||||||
max="100"></progress>
|
|
||||||
</div>
|
|
||||||
<div id="currentFiles" class="mt-4 space-y-2"></div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text-alt opacity-70"
|
|
||||||
>{form.files.help_text}</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-ghost order-1 sm:order-none"
|
|
||||||
onclick="eventEditor.close()"
|
|
||||||
>
|
|
||||||
{form.buttons.cancel}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
id="saveEventButton"
|
|
||||||
class="btn btn-primary flex-1 sm:flex-none"
|
|
||||||
>
|
|
||||||
{form.buttons.save}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Authentication } from "../pocketbase/Authentication";
|
|
||||||
import { Get } from "../pocketbase/Get";
|
|
||||||
import { Update } from "../pocketbase/Update";
|
|
||||||
import { SendLog } from "../pocketbase/SendLog";
|
|
||||||
import { FileManager } from "../pocketbase/FileManager";
|
|
||||||
|
|
||||||
const auth = Authentication.getInstance();
|
|
||||||
const get = Get.getInstance();
|
|
||||||
const update = Update.getInstance();
|
|
||||||
const logger = SendLog.getInstance();
|
|
||||||
const fileManager = FileManager.getInstance();
|
|
||||||
|
|
||||||
// Handle file uploads
|
|
||||||
if (editorFiles) {
|
|
||||||
editorFiles.addEventListener("change", async (e) => {
|
|
||||||
const files = Array.from(
|
|
||||||
(e.target as HTMLInputElement).files || []
|
|
||||||
);
|
|
||||||
if (files.length > 0) {
|
|
||||||
const uploadProgress =
|
|
||||||
document.getElementById("uploadProgress");
|
|
||||||
const uploadProgressBar = document.getElementById(
|
|
||||||
"uploadProgressBar"
|
|
||||||
) as HTMLProgressElement;
|
|
||||||
const uploadProgressText =
|
|
||||||
document.getElementById("uploadProgressText");
|
|
||||||
|
|
||||||
if (uploadProgress) uploadProgress.classList.remove("hidden");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
if (!user) throw new Error("User not authenticated");
|
|
||||||
|
|
||||||
// Upload files
|
|
||||||
await fileManager.uploadFiles(
|
|
||||||
"events",
|
|
||||||
eventId,
|
|
||||||
"files",
|
|
||||||
files
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
if (uploadProgressBar) uploadProgressBar.value = 100;
|
|
||||||
if (uploadProgressText)
|
|
||||||
uploadProgressText.textContent = "100%";
|
|
||||||
|
|
||||||
// Log successful upload
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"event files",
|
|
||||||
`Successfully uploaded ${files.length} files to event ${eventId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh file list
|
|
||||||
await loadCurrentFiles();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("File upload error:", err);
|
|
||||||
|
|
||||||
// Log upload error
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event files",
|
|
||||||
`Failed to upload files to event ${eventId}. Error: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (uploadProgress) {
|
|
||||||
setTimeout(() => {
|
|
||||||
uploadProgress.classList.add("hidden");
|
|
||||||
if (uploadProgressBar) uploadProgressBar.value = 0;
|
|
||||||
if (uploadProgressText)
|
|
||||||
uploadProgressText.textContent = "0%";
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to load current files
|
|
||||||
async function loadCurrentFiles() {
|
|
||||||
const currentFiles = document.getElementById("currentFiles");
|
|
||||||
if (!currentFiles || !eventId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
if (!event.files || !Array.isArray(event.files)) return;
|
|
||||||
|
|
||||||
currentFiles.innerHTML = event.files
|
|
||||||
.map((filename) => {
|
|
||||||
const fileUrl = fileManager.getFileUrl(
|
|
||||||
"events",
|
|
||||||
eventId,
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
return `
|
|
||||||
<div class="flex justify-between items-center p-2 bg-base-200 rounded-lg">
|
|
||||||
<span class="truncate flex-1">${filename}</span>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<a href="${fileUrl}" target="_blank" class="btn btn-sm btn-ghost">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<button class="btn btn-sm btn-ghost text-error" onclick="deleteFile('${filename}')">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load current files:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to delete a file
|
|
||||||
async function deleteFile(filename: string) {
|
|
||||||
if (!eventId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
if (!event.files || !Array.isArray(event.files)) return;
|
|
||||||
|
|
||||||
// Remove the file from the array
|
|
||||||
const updatedFiles = event.files.filter((f) => f !== filename);
|
|
||||||
await update.updateField("events", eventId, "files", updatedFiles);
|
|
||||||
|
|
||||||
// Log successful deletion
|
|
||||||
await logger.send(
|
|
||||||
"delete",
|
|
||||||
"event files",
|
|
||||||
`Successfully deleted file ${filename} from event ${eventId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh file list
|
|
||||||
await loadCurrentFiles();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to delete file:", err);
|
|
||||||
|
|
||||||
// Log deletion error
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event files",
|
|
||||||
`Failed to delete file ${filename} from event ${eventId}. Error: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make deleteFile available globally
|
|
||||||
window.deleteFile = deleteFile;
|
|
||||||
|
|
||||||
// Load current files on page load
|
|
||||||
loadCurrentFiles();
|
|
||||||
</script>
|
|
|
@ -1,683 +0,0 @@
|
||||||
---
|
|
||||||
import EventEditor from "./EventEditor.astro";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import textConfig from "../../config/text.yml?raw";
|
|
||||||
import profileConfig from "../../config/profileConfig.yaml?raw";
|
|
||||||
|
|
||||||
// Parse YAML configuration
|
|
||||||
const text = yaml.load(textConfig) as any;
|
|
||||||
const config = yaml.load(profileConfig) as any;
|
|
||||||
const { title, columns } = config.ui.tables.events;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 lg:gap-0 mb-4"
|
|
||||||
>
|
|
||||||
<h2 class="text-2xl font-bold">{title}</h2>
|
|
||||||
<div class="flex flex-col lg:flex-row gap-2">
|
|
||||||
<div class="form-control w-full">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="eventSearch"
|
|
||||||
placeholder="Search events..."
|
|
||||||
class="input input-bordered input-sm w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 w-full lg:w-auto">
|
|
||||||
<button id="searchEvents" class="btn btn-sm flex-1 lg:flex-none">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="lg:hidden ml-2">Search</span>
|
|
||||||
</button>
|
|
||||||
<button id="refreshEvents" class="btn btn-sm flex-1 lg:flex-none">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span class="lg:hidden ml-2">Refresh</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="addEvent"
|
|
||||||
class="btn btn-primary btn-sm flex-1 lg:flex-none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 4v16m8-8H4"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="lg:hidden ml-2">Add Event</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table
|
|
||||||
class="table table-zebra w-full [&_tr]:border-b [&_tr]:border-base-200"
|
|
||||||
>
|
|
||||||
<thead class="hidden lg:table-header-group">
|
|
||||||
<tr>
|
|
||||||
<th class="text-center">{columns.event_name}</th>
|
|
||||||
<th class="text-center">{columns.event_id}</th>
|
|
||||||
<th class="text-center">{columns.event_code}</th>
|
|
||||||
<th class="text-center">{columns.start_date}</th>
|
|
||||||
<th class="text-center">{columns.end_date}</th>
|
|
||||||
<th class="text-center">{columns.points_to_reward}</th>
|
|
||||||
<th class="text-center">{columns.location}</th>
|
|
||||||
<th class="text-center">Files</th>
|
|
||||||
<th class="text-center">Attendees</th>
|
|
||||||
<th class="text-center">{columns.actions}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="eventList" class="divide-y divide-base-200">
|
|
||||||
<!-- Event entries will be populated here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EventEditor />
|
|
||||||
|
|
||||||
<!-- File Viewer Modal -->
|
|
||||||
<dialog id="fileViewer" class="modal">
|
|
||||||
<div class="modal-box w-11/12 max-w-5xl h-[80vh]">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h3 class="font-bold text-lg" id="fileTitle">File Preview</h3>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
id="fileExternalLink"
|
|
||||||
href="#"
|
|
||||||
target="_blank"
|
|
||||||
class="btn btn-sm btn-ghost"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4 mr-1"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Open in New Tab
|
|
||||||
</a>
|
|
||||||
<form method="dialog">
|
|
||||||
<button class="btn btn-sm btn-circle btn-ghost">✕</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-[calc(100%-4rem)] bg-base-200 rounded-lg">
|
|
||||||
<!-- PDF Preview -->
|
|
||||||
<iframe
|
|
||||||
id="fileFrame"
|
|
||||||
class="w-full h-full rounded-lg border-2 border-base-300 hidden"
|
|
||||||
src=""></iframe>
|
|
||||||
<!-- Image Preview -->
|
|
||||||
<img
|
|
||||||
id="imagePreview"
|
|
||||||
class="w-full h-full object-contain rounded-lg hidden"
|
|
||||||
src=""
|
|
||||||
alt="File preview"
|
|
||||||
/>
|
|
||||||
<!-- Text Preview -->
|
|
||||||
<div
|
|
||||||
id="textPreview"
|
|
||||||
class="w-full h-full p-4 font-mono text-sm overflow-auto whitespace-pre rounded-lg hidden"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<!-- Unsupported Format Message -->
|
|
||||||
<div
|
|
||||||
id="unsupportedPreview"
|
|
||||||
class="w-full h-full flex items-center justify-center text-center p-4 hidden"
|
|
||||||
>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-16 w-16 mx-auto opacity-50"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<p class="text-lg font-medium">Preview not available</p>
|
|
||||||
<p class="text-sm opacity-70">
|
|
||||||
Please use the "Open in New Tab" button to view this
|
|
||||||
file
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Authentication } from "../pocketbase/Authentication";
|
|
||||||
import { Get } from "../pocketbase/Get";
|
|
||||||
import { Update } from "../pocketbase/Update";
|
|
||||||
import { SendLog } from "../pocketbase/SendLog";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
// Get instances
|
|
||||||
const auth = Authentication.getInstance();
|
|
||||||
const get = Get.getInstance();
|
|
||||||
const update = Update.getInstance();
|
|
||||||
const logger = SendLog.getInstance();
|
|
||||||
|
|
||||||
// Show initial loading state
|
|
||||||
const eventsList = document.getElementById("eventList");
|
|
||||||
if (eventsList) {
|
|
||||||
eventsList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="10" class="text-center py-8">
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
|
||||||
<span class="text-sm opacity-70">Loading events...</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize after DOM is fully loaded
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
loadEvents();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to format date
|
|
||||||
function formatDate(dateStr: string): string {
|
|
||||||
return new Date(dateStr).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to render event row
|
|
||||||
function renderEventRow(event: any) {
|
|
||||||
return `
|
|
||||||
<tr class="hover:bg-base-200">
|
|
||||||
<!-- Mobile View -->
|
|
||||||
<td class="block lg:table-cell">
|
|
||||||
<div class="lg:hidden space-y-2">
|
|
||||||
<div class="font-medium text-center">${event.event_name || "N/A"}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">${columns.event_id}: ${event.event_id || "N/A"}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">${columns.event_code}: ${event.event_code || "N/A"}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">${columns.start_date}: ${formatDate(event.start_date)}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">${columns.end_date}: ${formatDate(event.end_date)}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">${columns.points_to_reward}: ${event.points_to_reward || "0"}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">${columns.location}: ${event.location || "N/A"}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">Files: ${event.files?.length || 0}</div>
|
|
||||||
<div class="text-sm opacity-70 text-center">Attendees: ${event.attendees ? JSON.parse(event.attendees).length : 0}</div>
|
|
||||||
<div class="flex items-center justify-center gap-2 mt-2">
|
|
||||||
<button class="btn btn-ghost btn-xs view-attendees" data-event-id="${event.id}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
|
||||||
</svg>
|
|
||||||
View Attendees
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
|
||||||
</svg>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-xs text-error delete-event" data-event-id="${event.id}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop View -->
|
|
||||||
<span class="hidden lg:block text-center">${event.event_name || "N/A"}</span>
|
|
||||||
</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">${event.event_id || "N/A"}</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">${event.event_code || "N/A"}</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">${formatDate(event.start_date)}</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">${formatDate(event.end_date)}</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">${event.points_to_reward || "0"}</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">${event.location || "N/A"}</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">
|
|
||||||
${
|
|
||||||
event.files && event.files.length > 0
|
|
||||||
? `<button class="btn btn-ghost btn-xs view-files" data-event-id="${event.id}">
|
|
||||||
${event.files.length} File${event.files.length > 1 ? "s" : ""}
|
|
||||||
</button>`
|
|
||||||
: '<span class="text-sm opacity-50">No files</span>'
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">
|
|
||||||
<button class="btn btn-ghost btn-xs view-attendees" data-event-id="${event.id}">
|
|
||||||
<span class="mr-2">${event.attendees ? JSON.parse(event.attendees).length : 0}</span>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td class="hidden lg:table-cell text-center">
|
|
||||||
<div class="flex justify-center gap-2">
|
|
||||||
<button class="btn btn-ghost btn-xs edit-event" data-event-id="${event.id}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
|
||||||
</svg>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-xs text-error delete-event" data-event-id="${event.id}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to load events
|
|
||||||
async function loadEvents(searchQuery = "") {
|
|
||||||
if (!eventsList) return;
|
|
||||||
|
|
||||||
// Show loading state immediately
|
|
||||||
eventsList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="10" class="text-center py-8">
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
|
||||||
<span class="text-sm opacity-70">Loading events...</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch events with filter if search query exists
|
|
||||||
const filter = searchQuery
|
|
||||||
? `event_name ~ "${searchQuery}" || event_id ~ "${searchQuery}" || event_code ~ "${searchQuery}"`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const events = await get.getList(
|
|
||||||
"events",
|
|
||||||
1,
|
|
||||||
50,
|
|
||||||
filter,
|
|
||||||
"-created"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update table
|
|
||||||
if (events.items.length === 0) {
|
|
||||||
eventsList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="10" class="text-center py-4">
|
|
||||||
${searchQuery ? "No events found matching your search." : "No events found."}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventsList.innerHTML = events.items.map(renderEventRow).join("");
|
|
||||||
|
|
||||||
// Setup event listeners for buttons
|
|
||||||
setupEventListeners(events.items);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load events:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event management",
|
|
||||||
`Failed to load events: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
eventsList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="10" class="text-center py-4 text-error">
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
||||||
</svg>
|
|
||||||
<span>Failed to load events. Please try again.</span>
|
|
||||||
<button onclick="loadEvents()" class="btn btn-sm btn-ghost mt-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
||||||
</svg>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to setup event listeners
|
|
||||||
function setupEventListeners(events: any[]) {
|
|
||||||
const editButtons = eventsList.querySelectorAll(".edit-event");
|
|
||||||
editButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const eventId = (button as HTMLButtonElement).dataset.eventId;
|
|
||||||
if (eventId) {
|
|
||||||
handleEventEdit(eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteButtons = eventsList.querySelectorAll(".delete-event");
|
|
||||||
deleteButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const eventId = (button as HTMLButtonElement).dataset.eventId;
|
|
||||||
if (eventId) {
|
|
||||||
handleEventDelete(eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const viewAttendeesButtons =
|
|
||||||
eventsList.querySelectorAll(".view-attendees");
|
|
||||||
viewAttendeesButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const eventId = (button as HTMLButtonElement).dataset.eventId;
|
|
||||||
if (eventId) {
|
|
||||||
handleViewAttendees(eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const viewFilesButtons = eventsList.querySelectorAll(".view-files");
|
|
||||||
viewFilesButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", async () => {
|
|
||||||
const eventId = (button as HTMLButtonElement).dataset.eventId;
|
|
||||||
if (eventId) {
|
|
||||||
await handleViewFiles(eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle event edit
|
|
||||||
async function handleEventEdit(eventId: string) {
|
|
||||||
try {
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
const {
|
|
||||||
eventEditor,
|
|
||||||
editorEventId,
|
|
||||||
editorEventName,
|
|
||||||
editorEventCode,
|
|
||||||
editorStartDate,
|
|
||||||
editorStartTime,
|
|
||||||
editorEndDate,
|
|
||||||
editorEndTime,
|
|
||||||
editorPointsToReward,
|
|
||||||
editorLocation,
|
|
||||||
saveEventButton,
|
|
||||||
} = getElements();
|
|
||||||
|
|
||||||
// Split start and end dates into separate date and time
|
|
||||||
const startDateTime = splitDateTime(event.start_date);
|
|
||||||
const endDateTime = splitDateTime(event.end_date);
|
|
||||||
|
|
||||||
// Populate the form
|
|
||||||
editorEventId.value = event.event_id || "";
|
|
||||||
editorEventName.value = event.event_name || "";
|
|
||||||
editorEventCode.value = event.event_code || "";
|
|
||||||
editorStartDate.value = startDateTime.date;
|
|
||||||
editorStartTime.value = startDateTime.time;
|
|
||||||
editorEndDate.value = endDateTime.date;
|
|
||||||
editorEndTime.value = endDateTime.time;
|
|
||||||
editorPointsToReward.value =
|
|
||||||
event.points_to_reward?.toString() || "0";
|
|
||||||
editorLocation.value = event.location || "";
|
|
||||||
|
|
||||||
// Store the event ID for saving
|
|
||||||
saveEventButton.dataset.eventId = eventId;
|
|
||||||
|
|
||||||
// Show the dialog
|
|
||||||
eventEditor.showModal();
|
|
||||||
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"event management",
|
|
||||||
`Opened event editor for event: ${event.event_name} (${event.event_id})`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load event for editing:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event management",
|
|
||||||
`Failed to load event for editing: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle event delete
|
|
||||||
async function handleEventDelete(eventId: string) {
|
|
||||||
if (confirm("Are you sure you want to delete this event?")) {
|
|
||||||
try {
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
await update.deleteRecord("events", eventId);
|
|
||||||
await loadEvents();
|
|
||||||
|
|
||||||
await logger.send(
|
|
||||||
"delete",
|
|
||||||
"event management",
|
|
||||||
`Deleted event: ${event.event_name} (${event.event_id})`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to delete event:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event management",
|
|
||||||
`Failed to delete event: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle viewing attendees
|
|
||||||
async function handleViewAttendees(eventId: string) {
|
|
||||||
try {
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
const attendees = JSON.parse(event.attendees || "[]");
|
|
||||||
|
|
||||||
// Fetch user details for each attendee
|
|
||||||
const userDetails = await Promise.all(
|
|
||||||
attendees.map(async (userId: string) => {
|
|
||||||
try {
|
|
||||||
const user = await get.getOne("users", userId);
|
|
||||||
return {
|
|
||||||
name: user.name || "N/A",
|
|
||||||
email: user.email || "N/A",
|
|
||||||
member_id: user.member_id || "N/A",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`Failed to fetch user ${userId}:`, err);
|
|
||||||
return {
|
|
||||||
name: "Unknown User",
|
|
||||||
email: "N/A",
|
|
||||||
member_id: "N/A",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create and show modal
|
|
||||||
showAttendeesModal(event, userDetails);
|
|
||||||
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"event management",
|
|
||||||
`Viewed attendees for event: ${event.event_name} (${event.event_id})`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to view attendees:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event management",
|
|
||||||
`Failed to view attendees: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle viewing files
|
|
||||||
async function handleViewFiles(eventId: string) {
|
|
||||||
try {
|
|
||||||
const event = await get.getOne("events", eventId);
|
|
||||||
showFilesModal(event);
|
|
||||||
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"event management",
|
|
||||||
`Viewed files for event: ${event.event_name} (${event.event_id})`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to view files:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"event management",
|
|
||||||
`Failed to view files: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"eventSearch"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const searchButton = document.getElementById("searchEvents");
|
|
||||||
const refreshButton = document.getElementById("refreshEvents");
|
|
||||||
const addEventButton = document.getElementById("addEvent");
|
|
||||||
|
|
||||||
if (searchButton && searchInput) {
|
|
||||||
searchButton.addEventListener("click", () => {
|
|
||||||
loadEvents(searchInput.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
searchInput.addEventListener("keypress", (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
loadEvents(searchInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshButton) {
|
|
||||||
refreshButton.addEventListener("click", () => {
|
|
||||||
if (searchInput) searchInput.value = "";
|
|
||||||
loadEvents();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addEventButton) {
|
|
||||||
addEventButton.addEventListener("click", () => {
|
|
||||||
const dialog = document.getElementById(
|
|
||||||
"eventEditor"
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
dialog?.showModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
loadEvents();
|
|
||||||
|
|
||||||
// File preview handling
|
|
||||||
const fileViewer = document.getElementById(
|
|
||||||
"fileViewer"
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
const fileFrame = document.getElementById("fileFrame") as HTMLIFrameElement;
|
|
||||||
const imagePreview = document.getElementById(
|
|
||||||
"imagePreview"
|
|
||||||
) as HTMLImageElement;
|
|
||||||
const textPreview = document.getElementById(
|
|
||||||
"textPreview"
|
|
||||||
) as HTMLDivElement;
|
|
||||||
const unsupportedPreview = document.getElementById(
|
|
||||||
"unsupportedPreview"
|
|
||||||
) as HTMLDivElement;
|
|
||||||
const fileTitle = document.getElementById(
|
|
||||||
"fileTitle"
|
|
||||||
) as HTMLHeadingElement;
|
|
||||||
const fileExternalLink = document.getElementById(
|
|
||||||
"fileExternalLink"
|
|
||||||
) as HTMLAnchorElement;
|
|
||||||
|
|
||||||
// Function to show file preview
|
|
||||||
async function showFilePreview(url: string, fileName: string) {
|
|
||||||
// Reset all preview elements
|
|
||||||
fileFrame.classList.add("hidden");
|
|
||||||
imagePreview.classList.add("hidden");
|
|
||||||
textPreview.classList.add("hidden");
|
|
||||||
unsupportedPreview.classList.add("hidden");
|
|
||||||
|
|
||||||
// Update title and external link
|
|
||||||
fileTitle.textContent = fileName;
|
|
||||||
fileExternalLink.href = url;
|
|
||||||
|
|
||||||
// Get file extension
|
|
||||||
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
||||||
|
|
||||||
// Handle different file types
|
|
||||||
if (["jpg", "jpeg", "png", "gif"].includes(ext)) {
|
|
||||||
imagePreview.src = url;
|
|
||||||
imagePreview.classList.remove("hidden");
|
|
||||||
} else if (ext === "pdf") {
|
|
||||||
fileFrame.src = url;
|
|
||||||
fileFrame.classList.remove("hidden");
|
|
||||||
} else if (["txt", "md", "json", "yaml", "yml"].includes(ext)) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
const text = await response.text();
|
|
||||||
textPreview.textContent = text;
|
|
||||||
textPreview.classList.remove("hidden");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load text file:", err);
|
|
||||||
unsupportedPreview.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsupportedPreview.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
fileViewer.showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add global event listener for file preview
|
|
||||||
document.addEventListener("showFilePreview", ((e: CustomEvent) => {
|
|
||||||
showFilePreview(e.detail.url, e.detail.fileName);
|
|
||||||
}) as EventListener);
|
|
||||||
</script>
|
|
|
@ -1,502 +0,0 @@
|
||||||
---
|
|
||||||
// Import the majors list
|
|
||||||
import allMajors from "../../data/allUCSDMajors.txt?raw";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import textConfig from "../../config/text.yml?raw";
|
|
||||||
import profileConfig from "../../config/profileConfig.yaml?raw";
|
|
||||||
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
|
|
||||||
|
|
||||||
const majorsList: string[] = allMajors
|
|
||||||
.split("\n")
|
|
||||||
.filter((major: string) => major.trim())
|
|
||||||
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
|
|
||||||
|
|
||||||
// Parse configurations
|
|
||||||
const text = yaml.load(textConfig) as any;
|
|
||||||
const profile = yaml.load(profileConfig) as any;
|
|
||||||
const pbConfig = yaml.load(pocketbaseConfig) as any;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 lg:gap-0 mb-4"
|
|
||||||
>
|
|
||||||
<h2 class="text-2xl font-bold">Member Management</h2>
|
|
||||||
<div class="flex flex-col lg:flex-row gap-2">
|
|
||||||
<div class="form-control w-full">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="resumeSearch"
|
|
||||||
placeholder="Search users..."
|
|
||||||
class="input input-bordered input-sm w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 w-full lg:w-auto">
|
|
||||||
<button id="searchResumes" class="btn btn-sm flex-1 lg:flex-none">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="lg:hidden ml-2">Search</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="refreshResumes"
|
|
||||||
class="btn btn-ghost btn-sm flex-1 lg:flex-none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="lg:hidden ml-2">Refresh</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table
|
|
||||||
class="table table-zebra w-full [&_tr]:border-b [&_tr]:border-base-200 text-center"
|
|
||||||
>
|
|
||||||
<thead class="hidden lg:table-header-group">
|
|
||||||
<tr>
|
|
||||||
<th class="text-center">Name</th>
|
|
||||||
<th class="text-center">Email</th>
|
|
||||||
<th class="text-center">Member ID</th>
|
|
||||||
<th class="text-center">Major</th>
|
|
||||||
<th class="text-center">Grad Year</th>
|
|
||||||
<th class="text-center">Points</th>
|
|
||||||
<th class="text-center">Resume</th>
|
|
||||||
<th class="text-center">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="resumeList" class="divide-y divide-base-200">
|
|
||||||
<!-- Resume entries will be populated here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add Profile Editor Dialog -->
|
|
||||||
<dialog id="profileEditor" class="modal">
|
|
||||||
<div class="modal-box">
|
|
||||||
<h3 class="font-bold text-lg mb-4">Edit User Profile</h3>
|
|
||||||
<form method="dialog" class="space-y-4">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Name</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="editorName"
|
|
||||||
class="input input-bordered"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Email</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="editorEmail"
|
|
||||||
class="input input-bordered"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">IEEE Member ID</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="editorMemberId"
|
|
||||||
class="input input-bordered"
|
|
||||||
placeholder="00000000"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Major</span>
|
|
||||||
</label>
|
|
||||||
<select id="editorMajor" class="select select-bordered w-full">
|
|
||||||
<option value="">Select major</option>
|
|
||||||
{
|
|
||||||
majorsList.map((major) => (
|
|
||||||
<option value={major.trim()}>{major.trim()}</option>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Graduation Year</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="editorGradYear"
|
|
||||||
class="select select-bordered w-full"
|
|
||||||
>
|
|
||||||
<option value="">Select graduation year</option>
|
|
||||||
{
|
|
||||||
Array.from({ length: 6 }, (_, i) => {
|
|
||||||
const year = new Date().getFullYear() + i;
|
|
||||||
return <option value={year}>{year}</option>;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Points</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="editorPoints"
|
|
||||||
class="input input-bordered"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Events Attended</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="editorEvents"
|
|
||||||
class="textarea textarea-bordered"
|
|
||||||
readonly></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="modal-action">
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
onclick="document.getElementById('profileEditor').close()"
|
|
||||||
>Cancel</button
|
|
||||||
>
|
|
||||||
<button id="saveProfileButton" class="btn btn-primary"
|
|
||||||
>Save Changes</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<script is:inline define:vars={{ majorsList }}>
|
|
||||||
// Make majorsList available globally
|
|
||||||
window.majorsList = majorsList;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import PocketBase from "pocketbase";
|
|
||||||
import type { RecordModel } from "pocketbase";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import textConfig from "../../config/text.yml?raw";
|
|
||||||
import profileConfig from "../../config/profileConfig.yaml?raw";
|
|
||||||
import pocketbaseConfig from "../../config/pocketbaseConfig.yml?raw";
|
|
||||||
|
|
||||||
// Define interface for user data structure
|
|
||||||
interface UserData {
|
|
||||||
id: string;
|
|
||||||
avatar: string;
|
|
||||||
collectionId: string;
|
|
||||||
collectionName: string;
|
|
||||||
created: string;
|
|
||||||
email: string;
|
|
||||||
emailVisibility: boolean;
|
|
||||||
events_attended: string[];
|
|
||||||
graduation_year: number;
|
|
||||||
last_login: string;
|
|
||||||
major: string;
|
|
||||||
member_id: string;
|
|
||||||
member_type: string;
|
|
||||||
name: string;
|
|
||||||
points: number;
|
|
||||||
resume: string[];
|
|
||||||
updated: string;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse YAML configuration
|
|
||||||
const text = yaml.load(textConfig) as any;
|
|
||||||
const profile = yaml.load(profileConfig) as any;
|
|
||||||
const pbConfig = yaml.load(pocketbaseConfig) as any;
|
|
||||||
const pb = new PocketBase(pbConfig.api.baseUrl);
|
|
||||||
|
|
||||||
// Get DOM elements
|
|
||||||
const resumeList = document.getElementById("resumeList");
|
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"resumeSearch"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const searchButton = document.getElementById("searchResumes");
|
|
||||||
const refreshButton = document.getElementById("refreshResumes");
|
|
||||||
|
|
||||||
// Function to format date
|
|
||||||
function formatDate(dateStr: string): string {
|
|
||||||
return new Date(dateStr).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make handleEditUser globally available
|
|
||||||
window.handleEditUser = function (user: UserData) {
|
|
||||||
console.log("Edit user data:", user);
|
|
||||||
const profileEditor = document.getElementById(
|
|
||||||
"profileEditor"
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
const editorName = document.getElementById(
|
|
||||||
"editorName"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const editorEmail = document.getElementById(
|
|
||||||
"editorEmail"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const editorMemberId = document.getElementById(
|
|
||||||
"editorMemberId"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const editorMajor = document.getElementById(
|
|
||||||
"editorMajor"
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const editorGradYear = document.getElementById(
|
|
||||||
"editorGradYear"
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const editorPoints = document.getElementById(
|
|
||||||
"editorPoints"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const editorEvents = document.getElementById(
|
|
||||||
"editorEvents"
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
const saveProfileButton = document.getElementById(
|
|
||||||
"saveProfileButton"
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
|
|
||||||
// Check if all required elements exist
|
|
||||||
if (
|
|
||||||
!profileEditor ||
|
|
||||||
!editorName ||
|
|
||||||
!editorEmail ||
|
|
||||||
!editorMemberId ||
|
|
||||||
!editorMajor ||
|
|
||||||
!editorGradYear ||
|
|
||||||
!editorPoints ||
|
|
||||||
!editorEvents ||
|
|
||||||
!saveProfileButton
|
|
||||||
) {
|
|
||||||
console.error("Missing form elements:", {
|
|
||||||
profileEditor: !!profileEditor,
|
|
||||||
editorName: !!editorName,
|
|
||||||
editorEmail: !!editorEmail,
|
|
||||||
editorMemberId: !!editorMemberId,
|
|
||||||
editorMajor: !!editorMajor,
|
|
||||||
editorGradYear: !!editorGradYear,
|
|
||||||
editorPoints: !!editorPoints,
|
|
||||||
editorEvents: !!editorEvents,
|
|
||||||
saveProfileButton: !!saveProfileButton,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Populate the form with user data
|
|
||||||
editorName.value = user.name || "";
|
|
||||||
editorEmail.value = user.email || "";
|
|
||||||
editorMemberId.value = user.member_id || "";
|
|
||||||
editorMajor.value = user.major?.trim() || ""; // Trim to remove any newlines
|
|
||||||
editorGradYear.value = user.graduation_year?.toString() || "";
|
|
||||||
editorPoints.value = user.points?.toString() || "0";
|
|
||||||
editorEvents.value = Array.isArray(user.events_attended)
|
|
||||||
? user.events_attended.join(", ")
|
|
||||||
: "No events attended";
|
|
||||||
saveProfileButton.dataset.userId = user.id;
|
|
||||||
|
|
||||||
// Show the dialog
|
|
||||||
profileEditor.showModal();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error populating form:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to render user row
|
|
||||||
function renderUserRow(user: RecordModel) {
|
|
||||||
const userData = user as unknown as UserData;
|
|
||||||
const escapedUser = JSON.stringify(userData)
|
|
||||||
.replace(/'/g, "\\'")
|
|
||||||
.replace(/"/g, """);
|
|
||||||
return `
|
|
||||||
<tr class="hover:bg-base-200">
|
|
||||||
<td class="text-center">
|
|
||||||
<div class="font-medium">${userData.name || "N/A"}</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">${userData.email || "N/A"}</td>
|
|
||||||
<td class="text-center">${userData.member_id || "N/A"}</td>
|
|
||||||
<td class="text-center">${userData.major?.trim() || "N/A"}</td>
|
|
||||||
<td class="text-center">${userData.graduation_year || "N/A"}</td>
|
|
||||||
<td class="text-center">${userData.points || "0"}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
${
|
|
||||||
userData.resume && userData.resume.length > 0
|
|
||||||
? `
|
|
||||||
<button class="btn btn-ghost btn-xs" onclick="window.open('${pb.files.getUrl(userData, userData.resume[0])}', '_blank')">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: "No Resume"
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<button class="btn btn-ghost btn-xs" onclick="handleEditUser(${escapedUser})">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
|
||||||
</svg>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle saving profile changes
|
|
||||||
async function handleProfileSave() {
|
|
||||||
const profileEditor = document.getElementById(
|
|
||||||
"profileEditor"
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
const editorName = document.getElementById(
|
|
||||||
"editorName"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const editorMemberId = document.getElementById(
|
|
||||||
"editorMemberId"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const editorMajor = document.getElementById(
|
|
||||||
"editorMajor"
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const editorGradYear = document.getElementById(
|
|
||||||
"editorGradYear"
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const editorPoints = document.getElementById(
|
|
||||||
"editorPoints"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const saveProfileButton = document.getElementById(
|
|
||||||
"saveProfileButton"
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const userId = saveProfileButton.dataset.userId;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
console.error("No user ID found for saving");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("name", editorName.value);
|
|
||||||
formData.append("member_id", editorMemberId.value);
|
|
||||||
formData.append("major", editorMajor.value);
|
|
||||||
formData.append("graduation_year", editorGradYear.value);
|
|
||||||
formData.append("points", editorPoints.value);
|
|
||||||
|
|
||||||
await pb.collection("users").update(userId, formData);
|
|
||||||
profileEditor.close();
|
|
||||||
// Refresh the user list
|
|
||||||
loadUsers();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to save user profile:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to load users
|
|
||||||
async function loadUsers(searchQuery = "") {
|
|
||||||
if (!resumeList) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Show loading state
|
|
||||||
resumeList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center py-4">
|
|
||||||
<span class="loading loading-spinner loading-md"></span>
|
|
||||||
Loading users...
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Fetch users with filter if search query exists
|
|
||||||
const filter = searchQuery
|
|
||||||
? `name ~ "${searchQuery}" || email ~ "${searchQuery}" || member_id ~ "${searchQuery}"`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const users = await pb.collection("users").getList(1, 50, {
|
|
||||||
filter,
|
|
||||||
sort: "-created",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update table
|
|
||||||
if (users.items.length === 0) {
|
|
||||||
resumeList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center py-4">
|
|
||||||
No users found
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeList.innerHTML = users.items.map(renderUserRow).join("");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load users:", error);
|
|
||||||
resumeList.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center py-4 text-error">
|
|
||||||
Failed to load users. Please try again.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
if (searchButton && searchInput) {
|
|
||||||
searchButton.addEventListener("click", () => {
|
|
||||||
loadUsers(searchInput.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
searchInput.addEventListener("keypress", (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
loadUsers(searchInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshButton) {
|
|
||||||
refreshButton.addEventListener("click", () => {
|
|
||||||
if (searchInput) searchInput.value = "";
|
|
||||||
loadUsers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listener for save button
|
|
||||||
const saveProfileButton = document.getElementById("saveProfileButton");
|
|
||||||
if (saveProfileButton) {
|
|
||||||
saveProfileButton.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleProfileSave();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
loadUsers();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
handleEditUser: (user: any) => void;
|
|
||||||
majorsList: string[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,48 +0,0 @@
|
||||||
---
|
|
||||||
import MemberManagement from "./MemberManagement.astro";
|
|
||||||
import EventManagement from "./EventManagement.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<div id="officerContent" class="hidden">
|
|
||||||
<div class="tabs tabs-boxed mb-6" id="officerViewTabs">
|
|
||||||
<button class="tab tab-active" data-officer-tab="members"
|
|
||||||
>Member Management</button
|
|
||||||
>
|
|
||||||
<button class="tab" data-officer-tab="events">Event Management</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-contents">
|
|
||||||
<div id="officerMembersTab" style="display: block;">
|
|
||||||
<MemberManagement />
|
|
||||||
</div>
|
|
||||||
<div id="officerEventsTab" style="display: none;">
|
|
||||||
<EventManagement />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const officerTabs = document.querySelectorAll("[data-officer-tab]");
|
|
||||||
const officerMembersTab = document.getElementById("officerMembersTab");
|
|
||||||
const officerEventsTab = document.getElementById("officerEventsTab");
|
|
||||||
|
|
||||||
if (officerTabs && officerMembersTab && officerEventsTab) {
|
|
||||||
officerTabs.forEach((tab) => {
|
|
||||||
tab.addEventListener("click", () => {
|
|
||||||
// Update tab styles
|
|
||||||
officerTabs.forEach((t) => t.classList.remove("tab-active"));
|
|
||||||
tab.classList.add("tab-active");
|
|
||||||
|
|
||||||
// Update content visibility
|
|
||||||
const tabId = (tab as HTMLElement).dataset.officerTab;
|
|
||||||
if (tabId === "members") {
|
|
||||||
officerMembersTab.style.display = "block";
|
|
||||||
officerEventsTab.style.display = "none";
|
|
||||||
} else {
|
|
||||||
officerMembersTab.style.display = "none";
|
|
||||||
officerEventsTab.style.display = "block";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,555 +0,0 @@
|
||||||
---
|
|
||||||
// Import the majors list
|
|
||||||
import allMajors from "../../data/allUCSDMajors.txt?raw";
|
|
||||||
|
|
||||||
const majorsList: string[] = allMajors
|
|
||||||
.split("\n")
|
|
||||||
.filter((major: string) => major.trim())
|
|
||||||
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center gap-4 mb-8">
|
|
||||||
<div class="flex-1">
|
|
||||||
<h2 class="card-title text-2xl">Profile Settings</h2>
|
|
||||||
<p class="text-base-content/70 mt-1">
|
|
||||||
Manage your academic information and preferences
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex-none">
|
|
||||||
<button class="btn btn-primary" id="saveSettings">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5 mr-2"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings Sections -->
|
|
||||||
<div class="space-y-8">
|
|
||||||
<!-- Academic Information Section -->
|
|
||||||
<div class="bg-base-200 rounded-box p-6">
|
|
||||||
<div class="flex items-center gap-2 mb-6">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<h3 class="text-xl font-medium">Academic Information</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-6">
|
|
||||||
<!-- Major Selection -->
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-medium">Major</span>
|
|
||||||
<span class="label-text-alt text-base-content/70"
|
|
||||||
>Select your current major</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<select id="majorSelect" class="select select-bordered w-full">
|
|
||||||
<option value="">Select your major</option>
|
|
||||||
{
|
|
||||||
majorsList.map((major: string) => (
|
|
||||||
<option value={major}>{major}</option>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Graduation Year -->
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-medium">Expected Graduation</span>
|
|
||||||
<span class="label-text-alt text-base-content/70"
|
|
||||||
>When do you plan to graduate?</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<select id="gradYearSelect" class="select select-bordered w-full">
|
|
||||||
<option value="">Select graduation year</option>
|
|
||||||
{
|
|
||||||
Array.from({ length: 6 }, (_, i) => {
|
|
||||||
const year = new Date().getFullYear() + i;
|
|
||||||
return <option value={year}>{year}</option>;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- IEEE Information Section -->
|
|
||||||
<div class="bg-base-200 rounded-box p-6">
|
|
||||||
<div class="flex items-center gap-2 mb-6">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<h3 class="text-xl font-medium">IEEE Membership</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-6">
|
|
||||||
<!-- IEEE Member ID -->
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-medium">IEEE Member ID</span>
|
|
||||||
<span class="label-text-alt text-base-content/70"
|
|
||||||
>Your IEEE membership number</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="memberIdInput"
|
|
||||||
placeholder="Enter your IEEE Member ID"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resume Upload -->
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-medium">Resume</span>
|
|
||||||
<span class="label-text-alt text-base-content/70"
|
|
||||||
>Upload your latest resume</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- Current Resume Display -->
|
|
||||||
<div id="resumeDisplay" class="hidden">
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 p-3 bg-base-100 rounded-lg border border-base-300"
|
|
||||||
>
|
|
||||||
<div class="flex-1 flex items-center gap-3">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-8 w-8 text-primary"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p
|
|
||||||
id="currentResume"
|
|
||||||
class="text-sm font-medium truncate"
|
|
||||||
>
|
|
||||||
No resume uploaded
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-base-content/70">
|
|
||||||
PDF, DOC, or DOCX
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-none flex gap-2">
|
|
||||||
<button id="previewResume" class="btn btn-sm btn-ghost">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
Preview
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Upload Input -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="resumeUpload"
|
|
||||||
accept=".pdf,.doc,.docx"
|
|
||||||
class="file-input file-input-bordered file-input-primary w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Upload Status -->
|
|
||||||
<div class="text-sm">
|
|
||||||
<span id="uploadStatus" class="label-text-alt"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// TypeScript declarations
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
showEventFiles: (eventId: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import { Authentication } from "../pocketbase/Authentication";
|
|
||||||
import { Update } from "../pocketbase/Update";
|
|
||||||
import { SendLog } from "../pocketbase/SendLog";
|
|
||||||
import { FileManager } from "../pocketbase/FileManager";
|
|
||||||
import { Get } from "../pocketbase/Get";
|
|
||||||
|
|
||||||
const auth = Authentication.getInstance();
|
|
||||||
const update = Update.getInstance();
|
|
||||||
const logger = SendLog.getInstance();
|
|
||||||
const fileManager = FileManager.getInstance();
|
|
||||||
const get = Get.getInstance();
|
|
||||||
|
|
||||||
// Get form elements
|
|
||||||
const memberIdInput = document.getElementById(
|
|
||||||
"memberIdInput",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const majorSelect = document.getElementById(
|
|
||||||
"majorSelect",
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const gradYearSelect = document.getElementById(
|
|
||||||
"gradYearSelect",
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const resumeUpload = document.getElementById(
|
|
||||||
"resumeUpload",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const currentResume = document.getElementById("currentResume");
|
|
||||||
const uploadStatus = document.getElementById("uploadStatus");
|
|
||||||
const saveSettings = document.getElementById("saveSettings");
|
|
||||||
|
|
||||||
// Add resume preview functionality
|
|
||||||
const resumeDisplay = document.getElementById("resumeDisplay");
|
|
||||||
const previewResume = document.getElementById("previewResume");
|
|
||||||
|
|
||||||
// Helper function to get file type
|
|
||||||
const getFileType = (fileName: string): string => {
|
|
||||||
const extension = fileName.split(".").pop()?.toLowerCase() || "";
|
|
||||||
const mimeTypes: Record<string, string> = {
|
|
||||||
pdf: "application/pdf",
|
|
||||||
doc: "application/msword",
|
|
||||||
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
};
|
|
||||||
return mimeTypes[extension] || "application/octet-stream";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load current user data
|
|
||||||
const loadUserData = async () => {
|
|
||||||
if (auth.isAuthenticated()) {
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
if (user) {
|
|
||||||
try {
|
|
||||||
// Get full user data with auto-cancellation disabled
|
|
||||||
const userData = await get.getOne("users", user.id, {
|
|
||||||
disableAutoCancellation: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("User major from server:", userData.major);
|
|
||||||
|
|
||||||
if (memberIdInput) memberIdInput.value = userData.member_id || "";
|
|
||||||
if (majorSelect) {
|
|
||||||
const serverMajor = userData.major?.trim();
|
|
||||||
console.log("Setting major select to:", serverMajor);
|
|
||||||
|
|
||||||
// Get all available options with their normalized values
|
|
||||||
const availableOptions = Array.from(majorSelect.options).map(
|
|
||||||
(opt) => ({
|
|
||||||
element: opt,
|
|
||||||
normalizedValue: opt.value.trim(),
|
|
||||||
displayValue: opt.textContent?.trim() || opt.value.trim(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Available options:",
|
|
||||||
availableOptions.map((o) => o.normalizedValue),
|
|
||||||
);
|
|
||||||
|
|
||||||
// First try exact match
|
|
||||||
let matchingOption = availableOptions.find(
|
|
||||||
(opt) => opt.normalizedValue === serverMajor,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If no exact match, try case-insensitive match
|
|
||||||
if (!matchingOption && serverMajor) {
|
|
||||||
const serverMajorLower = serverMajor.toLowerCase();
|
|
||||||
matchingOption = availableOptions.find(
|
|
||||||
(opt) => opt.normalizedValue.toLowerCase() === serverMajorLower,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still no match, try partial match
|
|
||||||
if (!matchingOption && serverMajor) {
|
|
||||||
const serverMajorLower = serverMajor.toLowerCase();
|
|
||||||
matchingOption = availableOptions.find(
|
|
||||||
(opt) =>
|
|
||||||
opt.normalizedValue
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(serverMajorLower) ||
|
|
||||||
serverMajorLower.includes(opt.normalizedValue.toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingOption) {
|
|
||||||
console.log(
|
|
||||||
"Found matching major:",
|
|
||||||
matchingOption.normalizedValue,
|
|
||||||
);
|
|
||||||
majorSelect.value = matchingOption.normalizedValue;
|
|
||||||
} else {
|
|
||||||
console.log("No matching major found for:", serverMajor);
|
|
||||||
majorSelect.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (gradYearSelect)
|
|
||||||
gradYearSelect.value = userData.graduation_year || "";
|
|
||||||
|
|
||||||
// Update resume display
|
|
||||||
if (currentResume && resumeDisplay) {
|
|
||||||
if (userData.resume) {
|
|
||||||
const fileName = userData.resume.toString();
|
|
||||||
currentResume.textContent = fileName || "Resume uploaded";
|
|
||||||
resumeDisplay.classList.remove("hidden");
|
|
||||||
|
|
||||||
// Get the file URL from PocketBase
|
|
||||||
const resumeUrl = fileManager.getFileUrl(
|
|
||||||
"users",
|
|
||||||
userData.id,
|
|
||||||
fileName,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update preview button to use new modal
|
|
||||||
if (previewResume) {
|
|
||||||
previewResume.onclick = () => {
|
|
||||||
// Dispatch custom event for FileViewerModal
|
|
||||||
const showFileViewerEvent = new CustomEvent(
|
|
||||||
"showFileViewer",
|
|
||||||
{
|
|
||||||
detail: {
|
|
||||||
files: {
|
|
||||||
url: resumeUrl,
|
|
||||||
name: fileName,
|
|
||||||
type: getFileType(fileName),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
window.dispatchEvent(showFileViewerEvent);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentResume.textContent = "No resume uploaded";
|
|
||||||
resumeDisplay.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load user data:", err);
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"profile settings",
|
|
||||||
`Failed to load user data: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle resume upload
|
|
||||||
if (resumeUpload && uploadStatus) {
|
|
||||||
resumeUpload.addEventListener("change", async (e) => {
|
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
|
||||||
if (file) {
|
|
||||||
uploadStatus.textContent = "Uploading...";
|
|
||||||
try {
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
if (!user) throw new Error("User not authenticated");
|
|
||||||
|
|
||||||
await fileManager.uploadFile("users", user.id, "resume", file);
|
|
||||||
uploadStatus.textContent = "Resume uploaded successfully";
|
|
||||||
|
|
||||||
// Refresh the user data to show the new resume
|
|
||||||
await loadUserData();
|
|
||||||
|
|
||||||
// Log successful resume upload
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"resume upload",
|
|
||||||
`Successfully uploaded resume: ${file.name}`,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Resume upload error:", err);
|
|
||||||
uploadStatus.textContent = "Failed to upload resume";
|
|
||||||
|
|
||||||
// Log resume upload error
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"resume upload",
|
|
||||||
`Failed to upload resume: ${file.name}. Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle save settings
|
|
||||||
if (saveSettings) {
|
|
||||||
saveSettings.addEventListener("click", async () => {
|
|
||||||
const button = saveSettings as HTMLButtonElement;
|
|
||||||
button.classList.add("loading");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
if (!user) throw new Error("User not authenticated");
|
|
||||||
|
|
||||||
// Get current user data for comparison
|
|
||||||
const currentUserData = await get.getOne("users", user.id);
|
|
||||||
|
|
||||||
const oldData = {
|
|
||||||
major: currentUserData.major || null,
|
|
||||||
graduation_year: currentUserData.graduation_year || null,
|
|
||||||
member_id: currentUserData.member_id || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newData = {
|
|
||||||
major: majorSelect?.value || null,
|
|
||||||
graduation_year: gradYearSelect?.value
|
|
||||||
? parseInt(gradYearSelect.value)
|
|
||||||
: null,
|
|
||||||
member_id: memberIdInput?.value || null,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
await update.updateFields("users", user.id, newData);
|
|
||||||
|
|
||||||
// Create detailed log message of changes
|
|
||||||
const changes = [];
|
|
||||||
if (oldData.major !== newData.major) {
|
|
||||||
changes.push(
|
|
||||||
`Major: "${oldData.major || "none"}" → "${newData.major || "none"}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (oldData.graduation_year !== newData.graduation_year) {
|
|
||||||
changes.push(
|
|
||||||
`Graduation Year: "${oldData.graduation_year || "none"}" → "${newData.graduation_year || "none"}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (oldData.member_id !== newData.member_id) {
|
|
||||||
changes.push(
|
|
||||||
`IEEE Member ID: "${oldData.member_id || "none"}" → "${newData.member_id || "none"}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.length > 0) {
|
|
||||||
await logger.send(
|
|
||||||
"update",
|
|
||||||
"profile settings",
|
|
||||||
`Updated profile settings:\n${changes.join("\n")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show success toast
|
|
||||||
const toast = document.createElement("div");
|
|
||||||
toast.className =
|
|
||||||
"alert alert-success fixed bottom-4 right-4 w-auto z-50";
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
<span>Settings saved successfully!</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
setTimeout(() => toast.remove(), 3000);
|
|
||||||
|
|
||||||
// Refresh the form data
|
|
||||||
await loadUserData();
|
|
||||||
|
|
||||||
// Ensure settings view stays visible
|
|
||||||
const defaultView = document.getElementById("defaultView");
|
|
||||||
const settingsView = document.getElementById("settingsView");
|
|
||||||
if (defaultView && settingsView) {
|
|
||||||
defaultView.classList.add("hidden");
|
|
||||||
settingsView.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to save settings:", err);
|
|
||||||
|
|
||||||
// Log the settings update error with details
|
|
||||||
const errorDetails = {
|
|
||||||
attempted_major: majorSelect?.value || "none",
|
|
||||||
attempted_graduation_year: gradYearSelect?.value || "none",
|
|
||||||
attempted_member_id: memberIdInput?.value || "none",
|
|
||||||
error_message: err instanceof Error ? err.message : "Unknown error",
|
|
||||||
};
|
|
||||||
|
|
||||||
await logger.send(
|
|
||||||
"error",
|
|
||||||
"profile settings",
|
|
||||||
`Failed to update profile settings:\nAttempted changes:\n` +
|
|
||||||
`Major: ${errorDetails.attempted_major}\n` +
|
|
||||||
`Graduation Year: ${errorDetails.attempted_graduation_year}\n` +
|
|
||||||
`IEEE Member ID: ${errorDetails.attempted_member_id}\n` +
|
|
||||||
`Error: ${errorDetails.error_message}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show error toast
|
|
||||||
const toast = document.createElement("div");
|
|
||||||
toast.className =
|
|
||||||
"alert alert-error fixed bottom-4 right-4 w-auto z-50";
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
<span>Failed to save settings. Please try again.</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
setTimeout(() => toast.remove(), 3000);
|
|
||||||
} finally {
|
|
||||||
button.classList.remove("loading");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
await loadUserData();
|
|
||||||
|
|
||||||
// Update when auth state changes
|
|
||||||
auth.onAuthStateChange(async () => {
|
|
||||||
await loadUserData();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in a new issue