diff --git a/bun.lock b/bun.lock
index 4ee959a..8bdbc34 100644
--- a/bun.lock
+++ b/bun.lock
@@ -14,6 +14,7 @@
"astro-expressive-code": "^0.38.3",
"astro-icon": "^1.1.4",
"js-yaml": "^4.1.0",
+ "jszip": "^3.10.1",
"motion": "^11.15.0",
"next": "^15.1.2",
"pocketbase": "^0.25.1",
@@ -500,6 +501,8 @@
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
+ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crossws": ["crossws@0.3.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw=="],
@@ -730,6 +733,8 @@
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+ "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
+
"import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -772,6 +777,8 @@
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
+ "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
@@ -786,10 +793,14 @@
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+ "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
+
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
+ "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
+
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@@ -1014,6 +1025,8 @@
"package-manager-detector": ["package-manager-detector@0.2.6", "", {}, "sha512-9vPH3qooBlYRJdmdYP00nvjZOulm40r5dhtal8st18ctf+6S1k7pi5yIHLvI4w5D70x0Y+xdVD9qITH0QO/A8A=="],
+ "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
+
"parse-entities": ["parse-entities@4.0.1", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w=="],
"parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
@@ -1072,6 +1085,8 @@
"prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="],
+ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
+
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
@@ -1096,6 +1111,8 @@
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
+ "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
+
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
@@ -1154,6 +1171,8 @@
"s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="],
+ "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sass-formatter": ["sass-formatter@0.7.9", "", { "dependencies": { "suf-log": "^2.5.3" } }, "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw=="],
@@ -1166,6 +1185,8 @@
"server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
+ "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
@@ -1200,6 +1221,8 @@
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+ "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
+
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
diff --git a/package.json b/package.json
index 56fcc98..3fc19bd 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"astro-expressive-code": "^0.38.3",
"astro-icon": "^1.1.4",
"js-yaml": "^4.1.0",
+ "jszip": "^3.10.1",
"motion": "^11.15.0",
"next": "^15.1.2",
"pocketbase": "^0.25.1",
diff --git a/src/components/auth/EventAuth.ts b/src/components/auth/EventAuth.ts
index a86d70f..6fa72cc 100644
--- a/src/components/auth/EventAuth.ts
+++ b/src/components/auth/EventAuth.ts
@@ -1,6 +1,8 @@
import PocketBase from "pocketbase";
+import type { RecordModel } from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../data/storeConfig.yaml?raw";
+import JSZip from 'jszip';
// Configuration type definitions
interface Config {
@@ -19,6 +21,28 @@ interface Config {
messageTimeout: number;
};
};
+ tables: {
+ events: {
+ title: string;
+ columns: {
+ event_name: string;
+ event_id: string;
+ event_code: string;
+ start_date: string;
+ end_date: string;
+ points_to_reward: string;
+ location: string;
+ registered_users: string;
+ actions: string;
+ };
+ form: {
+ buttons: {
+ edit: string;
+ delete: string;
+ };
+ };
+ };
+ };
defaults: {
pageSize: number;
sortField: string;
@@ -28,16 +52,19 @@ interface Config {
// Parse YAML configuration with type
const config = yaml.load(configYaml) as Config;
+const { columns, form } = config.ui.tables.events;
interface Event {
id: string;
event_id: string;
event_name: string;
event_code: string;
- registered_users: string; // JSON string
+ attendees: string; // JSON string of attendee IDs
points_to_reward: number;
start_date: string;
end_date: string;
+ location: string;
+ files: string[]; // Array of file URLs
collectionId: string;
collectionName: string;
}
@@ -52,15 +79,32 @@ interface AuthElements {
editorEventName: HTMLInputElement;
editorEventCode: HTMLInputElement;
editorStartDate: HTMLInputElement;
+ editorStartTime: HTMLInputElement;
editorEndDate: HTMLInputElement;
+ editorEndTime: HTMLInputElement;
editorPointsToReward: HTMLInputElement;
+ editorLocation: HTMLInputElement;
+ editorFiles: HTMLInputElement;
+ currentFiles: HTMLDivElement;
saveEventButton: HTMLButtonElement;
}
+interface ValidationErrors {
+ eventId?: string;
+ eventName?: string;
+ eventCode?: string;
+ startDate?: string;
+ startTime?: string;
+ endDate?: string;
+ endTime?: string;
+ points?: string;
+}
+
export class EventAuth {
private pb: PocketBase;
private elements: AuthElements;
private cachedEvents: Event[] = [];
+ private abortController: AbortController | null = null;
constructor() {
this.pb = new PocketBase(config.api.baseUrl);
@@ -78,8 +122,13 @@ export class EventAuth {
const editorEventName = document.getElementById("editorEventName") as HTMLInputElement;
const editorEventCode = document.getElementById("editorEventCode") as HTMLInputElement;
const editorStartDate = document.getElementById("editorStartDate") as HTMLInputElement;
+ const editorStartTime = document.getElementById("editorStartTime") as HTMLInputElement;
const editorEndDate = document.getElementById("editorEndDate") as HTMLInputElement;
+ const editorEndTime = document.getElementById("editorEndTime") as HTMLInputElement;
const editorPointsToReward = document.getElementById("editorPointsToReward") as HTMLInputElement;
+ const editorLocation = document.getElementById("editorLocation") as HTMLInputElement;
+ const editorFiles = document.getElementById("editorFiles") as HTMLInputElement;
+ const currentFiles = document.getElementById("currentFiles") as HTMLDivElement;
const saveEventButton = document.getElementById("saveEventButton") as HTMLButtonElement;
if (
@@ -92,8 +141,13 @@ export class EventAuth {
!editorEventName ||
!editorEventCode ||
!editorStartDate ||
+ !editorStartTime ||
!editorEndDate ||
+ !editorEndTime ||
!editorPointsToReward ||
+ !editorLocation ||
+ !editorFiles ||
+ !currentFiles ||
!saveEventButton
) {
throw new Error("Required DOM elements not found");
@@ -109,23 +163,47 @@ export class EventAuth {
editorEventName,
editorEventCode,
editorStartDate,
+ editorStartTime,
editorEndDate,
+ editorEndTime,
editorPointsToReward,
+ editorLocation,
+ editorFiles,
+ currentFiles,
saveEventButton,
};
}
private getRegisteredUsersCount(registeredUsers: string): number {
+ // Handle different cases for registered_users field
+ if (!registeredUsers) return 0;
+
try {
- if (!registeredUsers) return 0;
+ // Try to parse if it's a JSON string
const users = JSON.parse(registeredUsers);
- return Array.isArray(users) ? users.length : 0;
+ // Ensure users is an array
+ if (!Array.isArray(users)) {
+ return 0;
+ }
+ return users.length;
} catch (err) {
- console.warn("Failed to parse registered_users:", err);
+ console.warn("Failed to parse registered_users, using 0");
return 0;
}
}
+ private parseArrayField(field: any, defaultValue: any[] = []): any[] {
+ if (!field) return defaultValue;
+ if (Array.isArray(field)) return field;
+ try {
+ const parsed = JSON.parse(field);
+ return Array.isArray(parsed) ? parsed : defaultValue;
+ } catch (err) {
+ console.warn("Failed to parse array field:", err);
+ return defaultValue;
+ }
+ }
+
private async fetchEvents(searchQuery: string = "") {
try {
// Only fetch from API if we don't have cached data
@@ -145,7 +223,8 @@ export class EventAuth {
return terms.every(term =>
(event.event_name?.toLowerCase().includes(term) ||
event.event_id?.toLowerCase().includes(term) ||
- event.event_code?.toLowerCase().includes(term))
+ event.event_code?.toLowerCase().includes(term) ||
+ event.location?.toLowerCase().includes(term))
);
});
}
@@ -157,7 +236,7 @@ export class EventAuth {
if (filteredEvents.length === 0) {
const row = document.createElement("tr");
row.innerHTML = `
-
+ |
${searchQuery ? "No events found matching your search." : "No events found."}
|
`;
@@ -167,57 +246,105 @@ export class EventAuth {
const row = document.createElement("tr");
const startDate = event.start_date ? new Date(event.start_date).toLocaleString() : "N/A";
const endDate = event.end_date ? new Date(event.end_date).toLocaleString() : "N/A";
- const registeredCount = this.getRegisteredUsersCount(event.registered_users);
+
+ // Parse attendees using the new helper method
+ const attendees = this.parseArrayField(event.attendees);
+ const attendeeCount = attendees.length;
+
+ // Handle files display
+ const filesHtml = event.files && Array.isArray(event.files) && event.files.length > 0
+ ? ``
+ : 'No files';
+
+ // Format dates for display
+ const formatDateTime = (dateStr: string) => {
+ if (!dateStr) return { dateDisplay: 'N/A', timeDisplay: 'N/A' };
+ const date = new Date(dateStr);
+ return {
+ dateDisplay: date.toLocaleDateString(),
+ timeDisplay: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ };
+ };
+
+ const startDateTime = formatDateTime(event.start_date);
+ const endDateTime = formatDateTime(event.end_date);
row.innerHTML = `
- ${event.event_name || "N/A"}
- Event ID: ${event.event_id || "N/A"}
- Code: ${event.event_code || "N/A"}
- Start: ${startDate}
- End: ${endDate}
- Points: ${event.points_to_reward || 0}
- Registered: ${registeredCount}
-
+ ${event.event_name || "N/A"}
+ ${columns.event_id}: ${event.event_id || "N/A"}
+ ${columns.event_code}: ${event.event_code || "N/A"}
+ ${columns.start_date}: ${startDateTime.dateDisplay} ${startDateTime.timeDisplay}
+ ${columns.end_date}: ${endDateTime.dateDisplay} ${endDateTime.timeDisplay}
+ ${columns.points_to_reward}: ${event.points_to_reward || 0}
+ ${columns.location}: ${event.location || "N/A"}
+ Files: ${filesHtml}
+ Attendees: ${attendeeCount}
+
+
- ${event.event_name || "N/A"}
+ ${event.event_name || "N/A"}
|
- ${event.event_id || "N/A"} |
- ${event.event_code || "N/A"} |
- ${startDate} |
- ${endDate} |
- ${event.points_to_reward || 0} |
- ${registeredCount} |
-
-
+ ${event.event_id || "N/A"} |
+ ${event.event_code || "N/A"} |
+
+
+ ${startDateTime.dateDisplay}
+ ${startDateTime.timeDisplay}
+
+ |
+
+
+ ${endDateTime.dateDisplay}
+ ${endDateTime.timeDisplay}
+
+ |
+ ${event.points_to_reward || 0} |
+ ${event.location || "N/A"} |
+ ${filesHtml} |
+
+
+ |
+
+
|
@@ -230,7 +357,7 @@ export class EventAuth {
eventList.innerHTML = "";
eventList.appendChild(fragment);
- // Setup event listeners for edit and delete buttons
+ // Setup event listeners for buttons
const editButtons = eventList.querySelectorAll(".edit-event");
editButtons.forEach((button) => {
button.addEventListener("click", () => {
@@ -250,12 +377,32 @@ export class EventAuth {
}
});
});
+
+ const viewAttendeesButtons = eventList.querySelectorAll(".view-attendees");
+ viewAttendeesButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const eventId = (button as HTMLButtonElement).dataset.eventId;
+ if (eventId) {
+ this.handleViewAttendees(eventId);
+ }
+ });
+ });
+
+ const viewFilesButtons = eventList.querySelectorAll(".view-files");
+ viewFilesButtons.forEach((button) => {
+ button.addEventListener("click", async () => {
+ const eventId = (button as HTMLButtonElement).dataset.eventId;
+ if (eventId) {
+ await this.handleViewFiles(eventId);
+ }
+ });
+ });
} catch (err) {
console.error("Failed to fetch events:", err);
const { eventList } = this.elements;
eventList.innerHTML = `
-
+ |
Failed to fetch events. Please try again.
|
@@ -263,6 +410,30 @@ export class EventAuth {
}
}
+ private splitDateTime(dateTimeStr: string): { date: string; time: string } {
+ if (!dateTimeStr) return { date: "", time: "" };
+ const dateTime = new Date(dateTimeStr);
+ const date = dateTime.toISOString().split('T')[0];
+ const time = dateTime.toTimeString().split(' ')[0].slice(0, 5);
+ return { date, time };
+ }
+
+ private combineDateTime(date: string, time: string): string {
+ if (!date || !time) return "";
+ return `${date}T${time}:00`;
+ }
+
+ private getFileNameFromUrl(url: string): string {
+ try {
+ const urlObj = new URL(url);
+ const pathParts = urlObj.pathname.split("/");
+ return decodeURIComponent(pathParts[pathParts.length - 1]);
+ } catch (e) {
+ // If URL parsing fails, try to get the filename from the path directly
+ return url.split("/").pop() || "Unknown File";
+ }
+ }
+
private async handleEventEdit(eventId: string) {
try {
const event = await this.pb.collection("events").getOne(eventId);
@@ -272,18 +443,77 @@ export class EventAuth {
editorEventName,
editorEventCode,
editorStartDate,
+ editorStartTime,
editorEndDate,
+ editorEndTime,
editorPointsToReward,
+ editorLocation,
+ currentFiles,
saveEventButton,
} = this.elements;
+ // Split start and end dates into separate date and time
+ const startDateTime = this.splitDateTime(event.start_date);
+ const endDateTime = this.splitDateTime(event.end_date);
+
// Populate the form
editorEventId.value = event.event_id || "";
editorEventName.value = event.event_name || "";
editorEventCode.value = event.event_code || "";
- editorStartDate.value = event.start_date ? new Date(event.start_date).toISOString().slice(0, 16) : "";
- editorEndDate.value = event.end_date ? new Date(event.end_date).toISOString().slice(0, 16) : "";
+ 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 || "";
+
+ // Display current files
+ this.updateFilesDisplay(event);
+
+ // Update file input to support multiple files
+ const fileInput = this.elements.editorFiles;
+ fileInput.setAttribute('multiple', 'true');
+ fileInput.setAttribute('accept', '*/*');
+
+ // Add file input change handler for automatic upload
+ fileInput.addEventListener('change', async () => {
+ const selectedFiles = fileInput.files;
+ if (selectedFiles && selectedFiles.length > 0) {
+ try {
+ // Create FormData for file upload
+ const formData = new FormData();
+
+ // Add existing files to the formData
+ if (event.files && Array.isArray(event.files)) {
+ formData.append("files", JSON.stringify(event.files));
+ }
+
+ // Add new files to the formData
+ Array.from(selectedFiles).forEach(file => {
+ formData.append("files", file);
+ });
+
+ // Update the event with new files
+ const updatedEvent = await this.pb.collection('events').update(event.id, formData);
+
+ // Update the files display without refreshing the entire editor
+ event.files = updatedEvent.files;
+ this.updateFilesDisplay(event);
+
+ // Clear the file input
+ fileInput.value = '';
+
+ // Clear the file name display
+ const fileNameDisplay = fileInput.parentElement?.querySelector('.file-name');
+ if (fileNameDisplay) {
+ fileNameDisplay.setAttribute('data-content', '');
+ }
+ } catch (err) {
+ console.error('Failed to upload files:', err);
+ alert('Failed to upload files. Please try again.');
+ }
+ }
+ });
// Store the event ID for saving
saveEventButton.dataset.eventId = eventId;
@@ -298,6 +528,152 @@ export class EventAuth {
}
}
+ private validateForm(): ValidationErrors | null {
+ const {
+ editorEventId,
+ editorEventName,
+ editorEventCode,
+ editorStartDate,
+ editorStartTime,
+ editorEndDate,
+ editorEndTime,
+ editorPointsToReward,
+ } = this.elements;
+
+ const errors: ValidationErrors = {};
+
+ // Reset all error messages
+ const errorElements = document.querySelectorAll('.label-text-alt.text-error');
+ errorElements.forEach(el => el.classList.add('hidden'));
+
+ // Event ID validation
+ if (!editorEventId.disabled) { // Only validate if it's a new event
+ if (!editorEventId.value) {
+ errors.eventId = "Event ID is required";
+ } else if (!editorEventId.value.match(/^[A-Za-z0-9_-]+$/)) {
+ errors.eventId = "Event ID can only contain letters, numbers, underscores, and hyphens";
+ } else if (editorEventId.value.length < 3) {
+ errors.eventId = "Event ID must be at least 3 characters";
+ } else if (editorEventId.value.length > 50) {
+ errors.eventId = "Event ID must be less than 50 characters";
+ }
+ }
+
+ // Event Name validation
+ if (!editorEventName.value) {
+ errors.eventName = "Event Name is required";
+ } else if (editorEventName.value.length < 3) {
+ errors.eventName = "Event Name must be at least 3 characters";
+ } else if (editorEventName.value.length > 100) {
+ errors.eventName = "Event Name must be less than 100 characters";
+ }
+
+ // Event Code validation
+ if (!editorEventCode.value) {
+ errors.eventCode = "Event Code is required";
+ } else if (!editorEventCode.value.match(/^[A-Za-z0-9_-]+$/)) {
+ errors.eventCode = "Event Code can only contain letters, numbers, underscores, and hyphens";
+ } else if (editorEventCode.value.length < 3) {
+ errors.eventCode = "Event Code must be at least 3 characters";
+ } else if (editorEventCode.value.length > 20) {
+ errors.eventCode = "Event Code must be less than 20 characters";
+ }
+
+ // Date and Time validation
+ if (!editorStartDate.value) {
+ errors.startDate = "Start Date is required";
+ }
+ if (!editorStartTime.value) {
+ errors.startTime = "Start Time is required";
+ }
+ if (!editorEndDate.value) {
+ errors.endDate = "End Date is required";
+ }
+ if (!editorEndTime.value) {
+ errors.endTime = "End Time is required";
+ }
+
+ // Validate that end date/time is after start date/time
+ if (editorStartDate.value && editorStartTime.value && editorEndDate.value && editorEndTime.value) {
+ const startDateTime = new Date(`${editorStartDate.value}T${editorStartTime.value}`);
+ const endDateTime = new Date(`${editorEndDate.value}T${editorEndTime.value}`);
+
+ if (endDateTime <= startDateTime) {
+ errors.endDate = "End date/time must be after start date/time";
+ }
+ }
+
+ // Points validation
+ const points = parseInt(editorPointsToReward.value);
+ if (!editorPointsToReward.value) {
+ errors.points = "Points are required";
+ } else if (isNaN(points) || points < 0) {
+ errors.points = "Points must be a positive number";
+ } else if (points > 1000) {
+ errors.points = "Points must be less than 1000";
+ }
+
+ // Show error messages
+ if (errors.eventId) {
+ const errorEl = document.getElementById('eventIdError');
+ if (errorEl) {
+ errorEl.textContent = errors.eventId;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.eventName) {
+ const errorEl = document.getElementById('eventNameError');
+ if (errorEl) {
+ errorEl.textContent = errors.eventName;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.eventCode) {
+ const errorEl = document.getElementById('eventCodeError');
+ if (errorEl) {
+ errorEl.textContent = errors.eventCode;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.startDate) {
+ const errorEl = document.getElementById('startDateError');
+ if (errorEl) {
+ errorEl.textContent = errors.startDate;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.startTime) {
+ const errorEl = document.getElementById('startTimeError');
+ if (errorEl) {
+ errorEl.textContent = errors.startTime;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.endDate) {
+ const errorEl = document.getElementById('endDateError');
+ if (errorEl) {
+ errorEl.textContent = errors.endDate;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.endTime) {
+ const errorEl = document.getElementById('endTimeError');
+ if (errorEl) {
+ errorEl.textContent = errors.endTime;
+ errorEl.classList.remove('hidden');
+ }
+ }
+ if (errors.points) {
+ const errorEl = document.getElementById('pointsError');
+ if (errorEl) {
+ errorEl.textContent = errors.points;
+ errorEl.classList.remove('hidden');
+ }
+ }
+
+ return Object.keys(errors).length > 0 ? errors : null;
+ }
+
private async handleEventSave() {
const {
eventEditor,
@@ -305,33 +681,65 @@ export class EventAuth {
editorEventName,
editorEventCode,
editorStartDate,
+ editorStartTime,
editorEndDate,
+ editorEndTime,
editorPointsToReward,
+ editorLocation,
+ editorFiles,
saveEventButton,
} = this.elements;
const eventId = saveEventButton.dataset.eventId;
const isNewEvent = !eventId;
- try {
- const eventData: Record = {
- event_name: editorEventName.value,
- event_code: editorEventCode.value,
- start_date: editorStartDate.value,
- end_date: editorEndDate.value,
- points_to_reward: parseInt(editorPointsToReward.value) || 0,
- };
+ // Validate form before proceeding
+ const errors = this.validateForm();
+ if (errors) {
+ return; // Stop if there are validation errors
+ }
- // Only set registered_users for new events
+ try {
+ // Combine date and time inputs
+ const startDateTime = this.combineDateTime(editorStartDate.value, editorStartTime.value);
+ const endDateTime = this.combineDateTime(editorEndDate.value, editorEndTime.value);
+
+ // Create FormData for file upload
+ const formData = new FormData();
+ formData.append("event_name", editorEventName.value);
+ formData.append("event_code", editorEventCode.value);
+ formData.append("start_date", startDateTime);
+ formData.append("end_date", endDateTime);
+ formData.append("points_to_reward", editorPointsToReward.value);
+ formData.append("location", editorLocation.value);
+
+ // For new events, set event_id and initialize attendees as empty array
if (isNewEvent) {
- eventData.event_id = editorEventId.value;
- eventData.registered_users = "[]";
+ formData.append("event_id", editorEventId.value);
+ formData.append("attendees", "[]"); // Initialize with empty array
+ } else {
+ // For existing events, preserve current attendees and files
+ const currentEvent = await this.pb.collection("events").getOne(eventId);
+ const currentAttendees = this.parseArrayField(currentEvent.attendees, []);
+ formData.append("attendees", JSON.stringify(currentAttendees));
+
+ // Preserve existing files if no new files are being uploaded
+ if (currentEvent.files && (!editorFiles.files || editorFiles.files.length === 0)) {
+ formData.append("files", JSON.stringify(currentEvent.files));
+ }
+ }
+
+ // Handle file uploads
+ if (editorFiles.files && editorFiles.files.length > 0) {
+ Array.from(editorFiles.files).forEach(file => {
+ formData.append("files", file);
+ });
}
if (isNewEvent) {
- await this.pb.collection("events").create(eventData);
+ await this.pb.collection("events").create(formData);
} else {
- await this.pb.collection("events").update(eventId, eventData);
+ await this.pb.collection("events").update(eventId, formData);
}
// Close the dialog and refresh the table
@@ -355,7 +763,289 @@ export class EventAuth {
}
}
+ private async handleViewAttendees(eventId: string) {
+ try {
+ const event = await this.pb.collection("events").getOne(eventId);
+ const attendees = this.parseArrayField(event.attendees);
+
+ // Fetch user details for each attendee
+ const userDetails = await Promise.all(
+ attendees.map(async (userId: string) => {
+ try {
+ const user = await this.pb.collection("users").getOne(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
+ const modal = document.createElement("dialog");
+ modal.className = "modal";
+ modal.innerHTML = `
+
+ Attendees for ${event.event_name}
+
+
+
+
+
+
+
+ Name |
+ Email |
+ Member ID |
+
+
+
+ ${userDetails.length === 0
+ ? 'No attendees yet | '
+ : userDetails.map(user => `
+
+ ${user.name} |
+ ${user.email} |
+ ${user.member_id} |
+
+ `).join("")
+ }
+
+
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+ modal.showModal();
+
+ // Add search functionality
+ const searchInput = modal.querySelector("#attendeeSearch") as HTMLInputElement;
+ const attendeeRows = modal.querySelectorAll(".attendee-row");
+
+ if (searchInput) {
+ searchInput.addEventListener("input", () => {
+ const searchTerm = searchInput.value.toLowerCase();
+ attendeeRows.forEach((row) => {
+ const text = row.textContent?.toLowerCase() || "";
+ if (text.includes(searchTerm)) {
+ (row as HTMLElement).style.display = "";
+ } else {
+ (row as HTMLElement).style.display = "none";
+ }
+ });
+ });
+ }
+
+ // Remove modal when closed
+ modal.addEventListener("close", () => {
+ document.body.removeChild(modal);
+ });
+ } catch (err) {
+ console.error("Failed to view attendees:", err);
+ }
+ }
+
+ private async syncAttendees() {
+ try {
+ // Cancel any existing sync operation
+ if (this.abortController) {
+ this.abortController.abort();
+ }
+
+ // Create new abort controller for this sync operation
+ this.abortController = new AbortController();
+
+ console.log("=== STARTING ATTENDEE SYNC ===");
+
+ // Fetch all events first with abort signal
+ const events = await this.pb.collection("events").getFullList({
+ sort: config.ui.defaults.sortField,
+ $cancelKey: "syncAttendees",
+ });
+
+ // Early return if aborted
+ if (this.abortController.signal.aborted) {
+ console.log("Sync operation was cancelled");
+ return;
+ }
+
+ console.log("=== EVENTS DATA ===");
+ events.forEach(event => {
+ console.log(`Event: ${event.event_name} (ID: ${event.id})`);
+ console.log("- event_id:", event.event_id);
+ console.log("- Raw attendees field:", event.attendees);
+ });
+
+ // Create a map of event_id to event record for faster lookup
+ const eventMap = new Map();
+ events.forEach(event => {
+ const currentAttendees = this.parseArrayField(event.attendees);
+ console.log(`Parsed attendees for event ${event.event_name}:`, currentAttendees);
+
+ eventMap.set(event.event_id, {
+ id: event.id,
+ event_id: event.event_id,
+ event_name: event.event_name,
+ attendees: new Set(currentAttendees)
+ });
+ console.log(`Mapped event ${event.event_name} with event_id ${event.event_id}`);
+ });
+
+ // Check if aborted before fetching users
+ if (this.abortController.signal.aborted) {
+ console.log("Sync operation was cancelled");
+ return;
+ }
+
+ // Fetch all users with abort signal
+ const users = await this.pb.collection("users").getFullList({
+ fields: "id,name,email,member_id,events_attended",
+ $cancelKey: "syncAttendees",
+ });
+
+ console.log("=== USERS DATA ===");
+ users.forEach(user => {
+ console.log(`User: ${user.name || 'Unknown'} (ID: ${user.id})`);
+ console.log("- Raw events_attended:", user.events_attended);
+ });
+
+ // Process each user's events_attended
+ for (const user of users) {
+ // Check if aborted before processing each user
+ if (this.abortController.signal.aborted) {
+ console.log("Sync operation was cancelled");
+ return;
+ }
+
+ console.log(`\nProcessing user: ${user.name || 'Unknown'} (ID: ${user.id})`);
+ const eventsAttended = this.parseArrayField(user.events_attended);
+ console.log("Parsed events_attended:", eventsAttended);
+
+ // For each event the user attended
+ for (const eventId of eventsAttended) {
+ console.log(`\nChecking event ${eventId} for user ${user.id}`);
+
+ // Find the event by event_id
+ const eventRecord = eventMap.get(eventId);
+ if (eventRecord) {
+ console.log(`Found event record:`, eventRecord);
+ // If user not already in attendees, add them
+ if (!eventRecord.attendees.has(user.id)) {
+ eventRecord.attendees.add(user.id);
+ console.log(`Added user ${user.id} to event ${eventId}`);
+ } else {
+ console.log(`User ${user.id} already in event ${eventId}`);
+ }
+ } else {
+ console.log(`Event ${eventId} not found in event map. Available event_ids:`,
+ Array.from(eventMap.keys()));
+ }
+ }
+ }
+
+ // Update all events with new attendee lists
+ console.log("\n=== UPDATING EVENTS ===");
+ for (const [eventId, record] of eventMap.entries()) {
+ // Check if aborted before each update
+ if (this.abortController.signal.aborted) {
+ console.log("Sync operation was cancelled");
+ return;
+ }
+
+ try {
+ const attendeeArray = Array.from(record.attendees);
+ console.log(`Updating event ${eventId}:`);
+ console.log("- Current attendees:", attendeeArray);
+
+ await this.pb.collection("events").update(record.id, {
+ attendees: JSON.stringify(attendeeArray)
+ }, {
+ $cancelKey: "syncAttendees",
+ });
+ console.log(`Successfully updated event ${eventId}`);
+ } catch (err: any) {
+ if (err?.name === "AbortError") {
+ console.log("Update was cancelled");
+ return;
+ }
+ console.error(`Failed to update attendees for event ${eventId}:`, err);
+ console.error("Failed record:", record);
+ }
+ }
+
+ // Clear the cache to force a refresh of the events list
+ this.cachedEvents = [];
+ console.log("\n=== SYNC COMPLETE ===");
+ await this.fetchEvents();
+ } catch (err: any) {
+ if (err?.name === "AbortError") {
+ console.log("Sync operation was cancelled");
+ return;
+ }
+ console.error("Failed to sync attendees:", err);
+ console.error("Full error:", err);
+ } finally {
+ // Clear the abort controller when done
+ this.abortController = null;
+ }
+ }
+
+ private cleanup() {
+ if (this.abortController) {
+ this.abortController.abort();
+ this.abortController = null;
+ }
+ }
+
+ private async refreshEventsAndSync() {
+ try {
+ // Clear the cache to force a fresh fetch
+ this.cachedEvents = [];
+
+ // Check if user is authorized to sync
+ const user = this.pb.authStore.model;
+ if (user && (user.member_type === "IEEE Officer" || user.member_type === "IEEE Administrator")) {
+ await this.syncAttendees().catch(console.error);
+ } else {
+ // If not authorized to sync, just refresh the events
+ await this.fetchEvents();
+ }
+ } catch (err) {
+ console.error("Failed to refresh events:", err);
+ }
+ }
+
private init() {
+ // Only sync attendees if user is an officer or administrator
+ setTimeout(async () => {
+ const user = this.pb.authStore.model;
+ if (user && (user.member_type === "IEEE Officer" || user.member_type === "IEEE Administrator")) {
+ await this.syncAttendees().catch(console.error);
+ }
+ }, 100);
+
// Initial fetch
this.fetchEvents();
@@ -371,6 +1061,14 @@ export class EventAuth {
// Search button click handler
this.elements.searchEvents.addEventListener("click", handleSearch);
+ // Refresh button click handler
+ const refreshButton = document.getElementById("refreshEvents");
+ if (refreshButton) {
+ refreshButton.addEventListener("click", () => {
+ this.refreshEventsAndSync();
+ });
+ }
+
// Add event button
this.elements.addEvent.addEventListener("click", () => {
const { eventEditor, editorEventId, saveEventButton } = this.elements;
@@ -380,7 +1078,9 @@ export class EventAuth {
this.elements.editorEventName.value = "";
this.elements.editorEventCode.value = "";
this.elements.editorStartDate.value = "";
+ this.elements.editorStartTime.value = "";
this.elements.editorEndDate.value = "";
+ this.elements.editorEndTime.value = "";
this.elements.editorPointsToReward.value = "0";
// Enable event_id field for new events
@@ -409,4 +1109,298 @@ export class EventAuth {
this.handleEventSave();
});
}
+
+ // Add this new method to handle files display update
+ private updateFilesDisplay(event: RecordModel) {
+ const { currentFiles } = this.elements;
+ currentFiles.innerHTML = "";
+
+ if (event.files && Array.isArray(event.files) && event.files.length > 0) {
+ const filesList = document.createElement("div");
+ filesList.className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4";
+
+ event.files.forEach((file: string) => {
+ const fileUrl = this.pb.files.getURL(event, file);
+ const fileName = this.getFileNameFromUrl(file);
+ const fileExt = fileName.split('.').pop()?.toLowerCase() || '';
+
+ const fileItem = document.createElement("div");
+ fileItem.className = "bg-base-200 rounded-lg overflow-hidden";
+
+ // Generate preview based on file type
+ let previewHtml = '';
+ if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) {
+ previewHtml = `
+
+ 
+
+ `;
+ } else if (fileExt === 'pdf') {
+ previewHtml = `
+
+
+
+ `;
+ } else {
+ // For other file types, show an icon based on type
+ const iconHtml = fileExt === 'txt' || fileExt === 'md'
+ ? ``
+ : ``;
+
+ previewHtml = `
+
+ ${iconHtml}
+
+ `;
+ }
+
+ fileItem.innerHTML = `
+ ${previewHtml}
+
+ `;
+
+ // Add delete handler
+ const deleteButton = fileItem.querySelector('.text-error');
+ if (deleteButton) {
+ deleteButton.addEventListener('click', async () => {
+ if (confirm('Are you sure you want to remove this file?')) {
+ try {
+ const fileToRemove = deleteButton.getAttribute('data-file');
+ if (!fileToRemove) throw new Error('File not found');
+
+ // Get the current event data to ensure we have the latest state
+ const currentEvent = await this.pb.collection('events').getOne(event.id);
+
+ // Filter out the file to be removed
+ const updatedFiles = currentEvent.files.filter((f: string) => f !== fileToRemove);
+
+ // Update the event with the new files array
+ await this.pb.collection('events').update(event.id, {
+ files: updatedFiles
+ });
+
+ // Update the event.files array in memory
+ event.files = updatedFiles;
+
+ // Remove the file item from the UI
+ fileItem.remove();
+
+ // If no files left, show the "No files" message
+ if (updatedFiles.length === 0) {
+ currentFiles.innerHTML = 'No files';
+ }
+ } catch (err) {
+ console.error('Failed to remove file:', err);
+ alert('Failed to remove file. Please try again.');
+ }
+ }
+ });
+ }
+
+ filesList.appendChild(fileItem);
+ });
+
+ currentFiles.appendChild(filesList);
+ } else {
+ currentFiles.innerHTML = 'No files';
+ }
+ }
+
+ private async handleViewFiles(eventId: string) {
+ try {
+ const event = await this.pb.collection("events").getOne(eventId);
+
+ // Create and show modal
+ const modal = document.createElement("dialog");
+ modal.className = "modal";
+ modal.innerHTML = `
+
+
+ Files for ${event.event_name}
+ ${event.files && Array.isArray(event.files) && event.files.length > 0
+ ? ` `
+ : ''
+ }
+
+
+ ${event.files && Array.isArray(event.files) && event.files.length > 0
+ ? event.files.map(file => {
+ const fileUrl = this.pb.files.getURL(event, file);
+ const fileName = this.getFileNameFromUrl(file);
+ const fileExt = fileName.split('.').pop()?.toLowerCase() || '';
+
+ let previewHtml = '';
+ if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) {
+ previewHtml = `
+
+ 
+
+ `;
+ } else {
+ // For other file types, show an icon based on type
+ const iconHtml = fileExt === 'txt' || fileExt === 'md'
+ ? ` `
+ : ` `;
+
+ previewHtml = `
+
+ ${iconHtml}
+
+ `;
+ }
+
+ return `
+
+ `;
+ }).join("")
+ : ' No files available '
+ }
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+ modal.showModal();
+
+ // Add preview functionality
+ const previewButtons = modal.querySelectorAll('.preview-file');
+ previewButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const url = (button as HTMLButtonElement).dataset.url;
+ const fileName = (button as HTMLButtonElement).dataset.filename;
+ const ext = (button as HTMLButtonElement).dataset.ext;
+ if (url && fileName) {
+ if (ext === 'pdf') {
+ window.open(url, '_blank');
+ } else {
+ document.dispatchEvent(new CustomEvent('showFilePreview', {
+ detail: { url, fileName }
+ }));
+ }
+ }
+ });
+ });
+
+ // Add download functionality
+ const downloadButton = modal.querySelector('.download-all');
+ if (downloadButton && event.files && Array.isArray(event.files)) {
+ downloadButton.addEventListener('click', async () => {
+ try {
+ if (event.files.length === 1) {
+ // For single file, download directly
+ const fileUrl = this.pb.files.getURL(event, event.files[0]);
+ const fileName = this.getFileNameFromUrl(event.files[0]);
+ const response = await fetch(fileUrl);
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ } else {
+ // For multiple files, create a zip
+ const zip = new JSZip();
+
+ // Show loading state
+ downloadButton.classList.add('loading');
+ downloadButton.setAttribute('disabled', 'true');
+
+ // Download all files and add to zip
+ await Promise.all(event.files.map(async (file: string) => {
+ const fileUrl = this.pb.files.getURL(event, file);
+ const fileName = this.getFileNameFromUrl(file);
+ const response = await fetch(fileUrl);
+ const blob = await response.blob();
+ zip.file(fileName, blob);
+ }));
+
+ // Generate and download zip file
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+ const url = window.URL.createObjectURL(zipBlob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${event.event_name || 'event'}_files.zip`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ // Reset button state
+ downloadButton.classList.remove('loading');
+ downloadButton.removeAttribute('disabled');
+ }
+ } catch (err) {
+ console.error('Failed to download files:', err);
+ alert('Failed to download files. Please try again.');
+
+ // Reset button state on error
+ if (downloadButton) {
+ downloadButton.classList.remove('loading');
+ downloadButton.removeAttribute('disabled');
+ }
+ }
+ });
+ }
+
+ // Remove modal when closed
+ modal.addEventListener("close", () => {
+ document.body.removeChild(modal);
+ });
+ } catch (err) {
+ console.error("Failed to view files:", err);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/components/auth/EventCheckIn.ts b/src/components/auth/EventCheckIn.ts
new file mode 100644
index 0000000..53f87b4
--- /dev/null
+++ b/src/components/auth/EventCheckIn.ts
@@ -0,0 +1,244 @@
+import PocketBase from "pocketbase";
+import yaml from "js-yaml";
+import configYaml from "../../data/storeConfig.yaml?raw";
+
+// Configuration type definitions
+interface Config {
+ api: {
+ baseUrl: string;
+ };
+ ui: {
+ messages: {
+ event: {
+ checkIn: {
+ checking: string;
+ success: string;
+ error: string;
+ invalid: string;
+ expired: string;
+ alreadyCheckedIn: string;
+ messageTimeout: number;
+ };
+ };
+ };
+ };
+}
+
+// Parse YAML configuration with type
+const config = yaml.load(configYaml) as Config;
+
+interface AuthElements {
+ eventCodeInput: HTMLInputElement;
+ checkInButton: HTMLButtonElement;
+ checkInStatus: HTMLParagraphElement;
+}
+
+export class EventCheckIn {
+ private pb: PocketBase;
+ private elements: AuthElements;
+
+ constructor() {
+ this.pb = new PocketBase(config.api.baseUrl);
+ this.elements = this.getElements();
+
+ // 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: "Invalid event code." };
+ }
+
+ 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,
+ message: `Event check-in is not open yet. Check-in opens at ${startDate.toLocaleString()}.`
+ };
+ }
+
+ if (now > endDate) {
+ return {
+ isValid: false,
+ message: `Event check-in has closed. Check-in closed at ${endDate.toLocaleString()}.`
+ };
+ }
+
+ return { isValid: true, event };
+ } catch (err) {
+ console.error('Failed to validate event code:', err);
+ return { isValid: false, message: "Failed to validate event code. Please try again." };
+ }
+ }
+
+ private async handleCheckIn() {
+ const { eventCodeInput, checkInStatus } = this.elements;
+ const eventCode = eventCodeInput.value.trim();
+
+ if (!eventCode) {
+ this.showStatus("Please enter an event code", "error");
+ return;
+ }
+
+ try {
+ this.showStatus(config.ui.messages.event.checkIn.checking, "info");
+
+ // Get current user
+ const user = this.pb.authStore.model;
+ if (!user) {
+ this.showStatus("Please sign in to check in to events", "error");
+ return;
+ }
+
+ // Validate event code and check time window
+ const validation = await this.validateEventCode(eventCode);
+ if (!validation.isValid) {
+ this.showStatus(validation.message || "Invalid event code.", "error");
+ 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(`You have already checked in to ${event.event_name}`, "info");
+ 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(
+ `Successfully checked in to ${event.event_name}! You earned ${pointsToAdd} points!`,
+ "success"
+ );
+ eventCodeInput.value = ""; // Clear input
+ } catch (err) {
+ console.error("Check-in error:", err);
+ this.showStatus(config.ui.messages.event.checkIn.error, "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 = "";
+ }, config.ui.messages.event.checkIn.messageTimeout);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/auth/StoreAuth.ts b/src/components/auth/StoreAuth.ts
index 6a2cf78..2cf3e20 100644
--- a/src/components/auth/StoreAuth.ts
+++ b/src/components/auth/StoreAuth.ts
@@ -132,10 +132,10 @@ export class StoreAuth {
private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } {
// Fun typescript fixes
const loginButton = document.getElementById(
- "loginButton",
+ "contentLoginButton",
) as HTMLButtonElement;
const logoutButton = document.getElementById(
- "logoutButton",
+ "contentLogoutButton",
) as HTMLButtonElement;
const userInfo = document.getElementById("userInfo") as HTMLDivElement;
const loadingSkeleton = document.getElementById(
@@ -380,14 +380,25 @@ export class StoreAuth {
sponsorViewToggle,
} = this.elements;
- // Hide buttons initially
- loginButton.style.display = "none";
- logoutButton.style.display = "none";
+ // Get all login and logout buttons using classes
+ const allLoginButtons = document.querySelectorAll('.login-button');
+ const allLogoutButtons = document.querySelectorAll('.logout-button');
+
+ // Hide all buttons initially
+ allLoginButtons.forEach(btn => btn.classList.add("hidden"));
+ allLogoutButtons.forEach(btn => btn.classList.add("hidden"));
if (this.pb.authStore.isValid && this.pb.authStore.model) {
+ // Show logout buttons for authenticated users
+ allLogoutButtons.forEach(btn => btn.classList.remove("hidden"));
+
// Update all the user information first
const user = this.pb.authStore.model;
const isSponsor = user.member_type === this.config.roles.sponsor.name;
+ const isOfficer = [
+ this.config.roles.officer.name,
+ this.config.roles.administrator.name
+ ].includes(user.member_type || "");
userName.textContent = user.name || this.config.ui.messages.auth.notProvided;
userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable;
@@ -454,12 +465,6 @@ export class StoreAuth {
}
// Handle view toggles visibility
- const isOfficer = [
- this.config.roles.officer.name,
- this.config.roles.administrator.name
- ].includes(user.member_type || "");
- const isSponsor = user.member_type === this.config.roles.sponsor.name;
-
officerViewToggle.style.display = isOfficer ? "block" : "none";
sponsorViewToggle.style.display = isSponsor ? "block" : "none";
@@ -518,8 +523,12 @@ export class StoreAuth {
userInfo.style.opacity = "1";
}, 50);
- logoutButton.style.display = "block";
+ officerViewToggle.style.display = isOfficer ? "block" : "none";
+ sponsorViewToggle.style.display = isSponsor ? "block" : "none";
} else {
+ // Show login buttons for unauthenticated users
+ allLoginButtons.forEach(btn => btn.classList.remove("hidden"));
+
// Update for logged out state
userName.textContent = this.config.ui.messages.auth.notSignedIn;
userEmail.textContent = this.config.ui.messages.auth.notSignedIn;
@@ -559,7 +568,6 @@ export class StoreAuth {
userInfo.style.opacity = "1";
}, 50);
- loginButton.style.display = "block";
officerViewToggle.style.display = "none";
sponsorViewToggle.style.display = "none";
}
diff --git a/src/components/auth/UserProfile.astro b/src/components/auth/UserProfile.astro
index 6082a52..880c0dc 100644
--- a/src/components/auth/UserProfile.astro
+++ b/src/components/auth/UserProfile.astro
@@ -47,20 +47,34 @@
-
-
-
+
+
-
+
+
+
+
+
@@ -194,10 +208,14 @@
-
-
@@ -347,3 +365,10 @@
background: rgba(0, 0, 0, 0.5);
}
+
+
diff --git a/src/components/profile/DefaultProfileView.astro b/src/components/profile/DefaultProfileView.astro
new file mode 100644
index 0000000..91cf831
--- /dev/null
+++ b/src/components/profile/DefaultProfileView.astro
@@ -0,0 +1,439 @@
+
+
+
+
+
+
+
+ Events
+
+
+
+
+
+
+
+
+ Enter your event code to check in
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ Array(3)
+ .fill(0)
+ .map(() => (
+
+ ))
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/profile/EventEditor.astro b/src/components/profile/EventEditor.astro
new file mode 100644
index 0000000..a37159f
--- /dev/null
+++ b/src/components/profile/EventEditor.astro
@@ -0,0 +1,270 @@
+---
+import yaml from "js-yaml";
+import configYaml from "../../data/storeConfig.yaml?raw";
+
+const config = yaml.load(configYaml) as any;
+const { editor_title, form } = config.ui.tables.events;
+---
+
+
+
diff --git a/src/components/profile/EventManagement.astro b/src/components/profile/EventManagement.astro
new file mode 100644
index 0000000..cad74f2
--- /dev/null
+++ b/src/components/profile/EventManagement.astro
@@ -0,0 +1,260 @@
+---
+import EventEditor from "./EventEditor.astro";
+import yaml from "js-yaml";
+import configYaml from "../../data/storeConfig.yaml?raw";
+
+const config = yaml.load(configYaml) as any;
+const { title, columns } = config.ui.tables.events;
+---
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/store/MemberManagement.astro b/src/components/profile/MemberManagement.astro
similarity index 93%
rename from src/components/store/MemberManagement.astro
rename to src/components/profile/MemberManagement.astro
index ca2a9da..1e722cc 100644
--- a/src/components/store/MemberManagement.astro
+++ b/src/components/profile/MemberManagement.astro
@@ -49,7 +49,9 @@
-
+
diff --git a/src/components/store/OfficerStoreView.astro b/src/components/profile/OfficerView.astro
similarity index 100%
rename from src/components/store/OfficerStoreView.astro
rename to src/components/profile/OfficerView.astro
diff --git a/src/components/store/EventEditor.astro b/src/components/store/EventEditor.astro
deleted file mode 100644
index 062574c..0000000
--- a/src/components/store/EventEditor.astro
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
diff --git a/src/components/store/EventManagement.astro b/src/components/store/EventManagement.astro
deleted file mode 100644
index 11428b6..0000000
--- a/src/components/store/EventManagement.astro
+++ /dev/null
@@ -1,82 +0,0 @@
----
-import EventEditor from "./EventEditor.astro";
----
-
-
- Event Management
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/data/storeConfig.yaml b/src/data/storeConfig.yaml
index 3cf86d0..a431843 100644
--- a/src/data/storeConfig.yaml
+++ b/src/data/storeConfig.yaml
@@ -61,6 +61,14 @@ ui:
deleteSuccess: Event deleted successfully!
deleteError: Failed to delete event. Please try again.
messageTimeout: 3000
+ checkIn:
+ checking: Checking event code...
+ success: Successfully checked in to event!
+ error: Failed to check in. Please try again.
+ invalid: Invalid event code. Please try again.
+ expired: This event is not currently active.
+ alreadyCheckedIn: You have already checked in to this event.
+ messageTimeout: 3000
auth:
loginError: Failed to start authentication
@@ -70,6 +78,55 @@ ui:
notAvailable: Not available
never: Never
+ tables:
+ events:
+ title: Event Management
+ editor_title: Event Details
+ columns:
+ event_name: Event Name
+ event_id: Event ID
+ event_code: Event Code
+ start_date: Start Date
+ end_date: End Date
+ points_to_reward: Points to Reward
+ location: Location
+ registered_users: Registered
+ actions: Actions
+ form:
+ event_id:
+ label: Event ID
+ placeholder: Enter unique event ID
+ event_name:
+ label: Event Name
+ placeholder: Enter event name
+ event_code:
+ label: Event Code
+ placeholder: Enter check-in code
+ start_date:
+ date_label: Start Date for check-in
+ time_label: Start Time for check-in
+ date_placeholder: Select start date for check-in
+ time_placeholder: Select start time for check-in
+ end_date:
+ date_label: End Date for check-in
+ time_label: End Time for check-in
+ date_placeholder: Select end date for check-in
+ time_placeholder: Select end time for check-in
+ points_to_reward:
+ label: Points to Reward
+ placeholder: Enter points value
+ location:
+ label: Location
+ placeholder: Enter event location
+ files:
+ label: Event Files
+ help_text: Upload event-related files (PDF, DOC, DOCX, TXT, JPG, JPEG, PNG)
+ buttons:
+ save: Save
+ cancel: Cancel
+ edit: Edit
+ delete: Delete
+
defaults:
pageSize: 50
sortField: -updated
diff --git a/src/pages/online-store.astro b/src/pages/online-store.astro
index 4d8f42f..de3db0e 100644
--- a/src/pages/online-store.astro
+++ b/src/pages/online-store.astro
@@ -2,8 +2,8 @@
import Layout from "../layouts/Layout.astro";
import UserProfile from "../components/auth/UserProfile.astro";
import DefaultStoreView from "../components/store/DefaultStoreView.astro";
-import OfficerStoreView from "../components/store/OfficerStoreView.astro";
-const title = "IEEE Store";
+import OfficerStoreView from "../components/profile/OfficerView.astro";
+const title = "IEEE Online Store";
---
diff --git a/src/pages/profile.astro b/src/pages/profile.astro
new file mode 100644
index 0000000..78b2745
--- /dev/null
+++ b/src/pages/profile.astro
@@ -0,0 +1,50 @@
+---
+import Layout from "../layouts/Layout.astro";
+import UserProfile from "../components/auth/UserProfile.astro";
+import DefaultProfileView from "../components/profile/DefaultProfileView.astro";
+import OfficerProfileView from "../components/profile/OfficerView.astro";
+const title = "User Profile";
+---
+
+
+
+ Profile Management
+
+
+
+
+
|