added profile
separated profile from online-store (kind of) added event management, editor, and viewer
This commit is contained in:
parent
961cf1d9c7
commit
9b49a8f088
16 changed files with 2444 additions and 249 deletions
23
bun.lock
23
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=="],
|
||||
|
|
|
@ -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",
|
||||
|
|
File diff suppressed because it is too large
Load diff
244
src/components/auth/EventCheckIn.ts
Normal file
244
src/components/auth/EventCheckIn.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -47,20 +47,34 @@
|
|||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
|
||||
<!-- Resume -->
|
||||
<div class="space-y-1">
|
||||
<div class="skeleton h-3 w-16 opacity-70"></div>
|
||||
<!-- Event Check-in -->
|
||||
<div class="space-y-2">
|
||||
<div class="skeleton h-3 w-24 opacity-70"></div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="skeleton h-[1.25rem] flex-1"></div>
|
||||
<div class="skeleton h-[1.25rem] w-24"></div>
|
||||
<div class="skeleton h-8 flex-1"></div>
|
||||
<div class="skeleton h-8 w-24"></div>
|
||||
</div>
|
||||
<div class="skeleton h-8 w-full"></div>
|
||||
<div class="skeleton h-3 w-48 opacity-70"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
|
||||
<!-- Auth Button -->
|
||||
<!-- Resume -->
|
||||
<div class="space-y-2">
|
||||
<div class="skeleton h-3 w-16 opacity-70"></div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="skeleton h-5 flex-1"></div>
|
||||
<div class="skeleton h-5 w-24"></div>
|
||||
</div>
|
||||
<div class="skeleton h-8 w-full"></div>
|
||||
<div class="skeleton h-3 w-48 opacity-70"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
|
||||
<!-- Auth Buttons -->
|
||||
<div class="pt-2">
|
||||
<div class="skeleton h-10 w-full"></div>
|
||||
</div>
|
||||
|
@ -194,10 +208,14 @@
|
|||
</div>
|
||||
<div class="divider my-0.5"></div>
|
||||
<div class="pt-2">
|
||||
<button id="loginButton" class="btn btn-primary w-full"
|
||||
<button
|
||||
id="contentLoginButton"
|
||||
class="login-button btn btn-primary w-full"
|
||||
>Sign in with IEEEUCSD SSO</button
|
||||
>
|
||||
<button id="logoutButton" class="btn btn-error w-full"
|
||||
<button
|
||||
id="contentLogoutButton"
|
||||
class="logout-button btn btn-error w-full"
|
||||
>Sign Out</button
|
||||
>
|
||||
</div>
|
||||
|
@ -347,3 +365,10 @@
|
|||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { StoreAuth } from "./StoreAuth";
|
||||
import { EventCheckIn } from "./EventCheckIn";
|
||||
new StoreAuth();
|
||||
new EventCheckIn();
|
||||
</script>
|
||||
|
|
439
src/components/profile/DefaultProfileView.astro
Normal file
439
src/components/profile/DefaultProfileView.astro
Normal file
|
@ -0,0 +1,439 @@
|
|||
<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 -->
|
||||
<div
|
||||
id="eventCheckInSkeleton"
|
||||
class="card bg-base-100 shadow-sm mb-4 animate-pulse"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="eventCheckInContent"
|
||||
class="card bg-base-100 shadow-sm mb-4 hidden"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<h3 class="font-medium text-lg mb-2">
|
||||
Enter your event code to check in
|
||||
</h3>
|
||||
<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 input-sm flex-1"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
id="checkInButton"
|
||||
class="btn btn-sm btn-primary"
|
||||
>Check In</button
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
id="checkInStatus"
|
||||
class="text-xs mt-1 opacity-70"
|
||||
>
|
||||
</p>
|
||||
</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">Something here</h2>
|
||||
<div class="space-y-4">
|
||||
<!-- Loading Skeletons -->
|
||||
<div class="space-y-2 animate-pulse">
|
||||
<div class="h-6 bg-base-300 rounded w-1/3"></div>
|
||||
<div class="h-4 bg-base-300 rounded w-2/3 opacity-70"></div>
|
||||
</div>
|
||||
<div class="space-y-2 animate-pulse">
|
||||
<div class="h-6 bg-base-300 rounded w-1/2"></div>
|
||||
<div class="h-4 bg-base-300 rounded w-3/4 opacity-70"></div>
|
||||
</div>
|
||||
<div class="space-y-2 animate-pulse">
|
||||
<div class="h-6 bg-base-300 rounded w-2/5"></div>
|
||||
<div class="h-4 bg-base-300 rounded w-1/2 opacity-70"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF Viewer Modal -->
|
||||
<dialog id="pdfViewer" 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="pdfTitle">Resume</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
id="pdfExternalLink"
|
||||
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)]">
|
||||
<iframe
|
||||
id="pdfFrame"
|
||||
class="w-full h-full rounded-lg border-2 border-base-300"
|
||||
src=""></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
import { StoreAuth } from "../auth/StoreAuth";
|
||||
import { EventCheckIn } from "../auth/EventCheckIn";
|
||||
import PocketBase from "pocketbase";
|
||||
import yaml from "js-yaml";
|
||||
import configYaml from "../../data/storeConfig.yaml?raw";
|
||||
import type { RecordModel } from "pocketbase";
|
||||
|
||||
// Parse YAML configuration with type assertion
|
||||
interface Config {
|
||||
api: {
|
||||
baseUrl: string;
|
||||
};
|
||||
}
|
||||
const config = yaml.load(configYaml) as Config;
|
||||
const pb = new PocketBase(config.api.baseUrl);
|
||||
|
||||
// Initialize auth and check-in
|
||||
new StoreAuth();
|
||||
new EventCheckIn();
|
||||
|
||||
// Handle loading states
|
||||
const eventCheckInSkeleton = document.getElementById(
|
||||
"eventCheckInSkeleton"
|
||||
);
|
||||
const eventCheckInContent = document.getElementById("eventCheckInContent");
|
||||
|
||||
// Function to show content and hide skeleton
|
||||
function showEventCheckIn() {
|
||||
if (eventCheckInSkeleton && eventCheckInContent) {
|
||||
eventCheckInSkeleton.classList.add("hidden");
|
||||
eventCheckInContent.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Show content when auth state changes
|
||||
pb.authStore.onChange(() => {
|
||||
showEventCheckIn();
|
||||
renderEvents();
|
||||
});
|
||||
|
||||
// Show content on initial load if already authenticated
|
||||
if (pb.authStore.isValid) {
|
||||
showEventCheckIn();
|
||||
}
|
||||
|
||||
// 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
interface Event extends RecordModel {
|
||||
event_id: string;
|
||||
event_name: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
// Function to render events
|
||||
async function renderEvents() {
|
||||
const eventsList = document.getElementById("eventsList");
|
||||
if (!eventsList) return;
|
||||
|
||||
try {
|
||||
// Get current user's attended events with safe parsing
|
||||
const user = pb.authStore.model;
|
||||
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
|
||||
: [];
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse events_attended:", e);
|
||||
attendedEvents = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all events
|
||||
const events = await pb.collection("events").getList(1, 50, {
|
||||
sort: "start_date", // Sort by start date ascending for upcoming events
|
||||
});
|
||||
|
||||
// Clear loading skeletons
|
||||
eventsList.innerHTML = "";
|
||||
|
||||
// Categorize events
|
||||
const now = new Date();
|
||||
const currentEvents: Event[] = [];
|
||||
const upcomingEvents: Event[] = [];
|
||||
const pastEvents: Event[] = [];
|
||||
|
||||
events.items.forEach((event) => {
|
||||
const typedEvent = event as Event;
|
||||
const startDate = new Date(typedEvent.start_date);
|
||||
const endDate = new Date(typedEvent.end_date);
|
||||
|
||||
if (startDate > now) {
|
||||
upcomingEvents.push(typedEvent);
|
||||
} else if (endDate >= now && startDate <= now) {
|
||||
currentEvents.push(typedEvent);
|
||||
} else {
|
||||
pastEvents.push(typedEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort upcoming events by start date and limit to 2
|
||||
const nextTwoEvents = upcomingEvents
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.start_date).getTime() -
|
||||
new Date(b.start_date).getTime()
|
||||
)
|
||||
.slice(0, 2);
|
||||
|
||||
// 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()
|
||||
);
|
||||
|
||||
// Function to render event card
|
||||
function renderEventCard(event: Event): string {
|
||||
const isAttended =
|
||||
Array.isArray(attendedEvents) &&
|
||||
attendedEvents.includes(event.event_id);
|
||||
|
||||
return `
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-medium text-lg">${event.event_name}</h3>
|
||||
<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="text-xs">📍 ${event.location}</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
isAttended
|
||||
? `
|
||||
<div class="badge badge-success 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="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>
|
||||
Attended
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Function to render section
|
||||
function renderSection(
|
||||
title: string,
|
||||
events: Event[],
|
||||
showDivider: boolean = true
|
||||
): string {
|
||||
if (events.length === 0) return "";
|
||||
return `
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-medium text-base-content/70">${title}</h3>
|
||||
<div class="space-y-4">
|
||||
${events.map((event) => renderEventCard(event)).join("")}
|
||||
</div>
|
||||
${showDivider ? '<div class="divider"></div>' : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Render all sections
|
||||
eventsList.innerHTML = `
|
||||
${renderSection("Upcoming Events", nextTwoEvents, nextTwoEvents.length > 0)}
|
||||
${renderSection("Currently Happening", currentEvents, currentEvents.length > 0)}
|
||||
${renderSection("Past Events", sortedPastEvents, false)}
|
||||
`;
|
||||
|
||||
// If no events at all
|
||||
if (events.items.length === 0) {
|
||||
eventsList.innerHTML = `
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No events found</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to render events:", err);
|
||||
eventsList.innerHTML = `
|
||||
<div class="text-center py-8 text-error">
|
||||
<p>Failed to load events. Please try again later.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderEvents();
|
||||
</script>
|
270
src/components/profile/EventEditor.astro
Normal file
270
src/components/profile/EventEditor.astro
Normal file
|
@ -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;
|
||||
---
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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="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>
|
260
src/components/profile/EventManagement.astro
Normal file
260
src/components/profile/EventManagement.astro
Normal file
|
@ -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;
|
||||
---
|
||||
|
||||
<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>{columns.event_name}</th>
|
||||
<th>{columns.event_id}</th>
|
||||
<th>{columns.event_code}</th>
|
||||
<th>{columns.start_date}</th>
|
||||
<th>{columns.end_date}</th>
|
||||
<th>{columns.points_to_reward}</th>
|
||||
<th>{columns.location}</th>
|
||||
<th>Files</th>
|
||||
<th>Attendees</th>
|
||||
<th>{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 { EventAuth } from "../auth/EventAuth";
|
||||
new EventAuth();
|
||||
|
||||
// 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>
|
|
@ -49,7 +49,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<table
|
||||
class="table table-zebra w-full [&_tr]:border-b [&_tr]:border-base-200"
|
||||
>
|
||||
<thead class="hidden lg:table-header-group">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -60,7 +62,7 @@
|
|||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="resumeList" class="divide-y">
|
||||
<tbody id="resumeList" class="divide-y divide-base-200">
|
||||
<!-- Resume entries will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
|
@ -1,96 +0,0 @@
|
|||
<!-- Event Editor Dialog -->
|
||||
<dialog id="eventEditor" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Event Details</h3>
|
||||
<form class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEventId">
|
||||
<span class="label-text">Event ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editorEventId"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEventName">
|
||||
<span class="label-text">Event Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editorEventName"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEventCode">
|
||||
<span class="label-text">Event Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editorEventCode"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorStartDate">
|
||||
<span class="label-text">Start Date & Time</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="editorStartDate"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorEndDate">
|
||||
<span class="label-text">End Date & Time</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="editorEndDate"
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="editorPointsToReward">
|
||||
<span class="label-text">Points to Reward</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="editorPointsToReward"
|
||||
class="input input-bordered w-full"
|
||||
min="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick="eventEditor.close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
id="saveEventButton"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
|
@ -1,82 +0,0 @@
|
|||
---
|
||||
import EventEditor from "./EventEditor.astro";
|
||||
---
|
||||
|
||||
<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">Event Management</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="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">
|
||||
<thead class="hidden lg:table-header-group">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Event ID</th>
|
||||
<th>Event Code</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Points</th>
|
||||
<th>Registered</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="eventList" class="divide-y">
|
||||
<!-- Event entries will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<EventEditor />
|
||||
|
||||
<script>
|
||||
import { EventAuth } from "../auth/EventAuth";
|
||||
new EventAuth();
|
||||
</script>
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
---
|
||||
|
||||
<Layout {title}>
|
||||
|
|
50
src/pages/profile.astro
Normal file
50
src/pages/profile.astro
Normal file
|
@ -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";
|
||||
---
|
||||
|
||||
<Layout {title}>
|
||||
<main class="mx-auto pb-12 md:pt-[5vh] pt-[5vw] min-h-screen">
|
||||
<h1 class="text-4xl font-bold mb-12">Profile Management</h1>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Left Column - User Info -->
|
||||
<div class="lg:col-span-2 2xl:col-span-1 h-fit">
|
||||
<UserProfile />
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Store Items -->
|
||||
<div id="storeContent" class="lg:col-span-2 2xl:col-span-3">
|
||||
<div id="defaultView">
|
||||
<DefaultProfileView />
|
||||
</div>
|
||||
<div id="officerView" class="hidden">
|
||||
<OfficerProfileView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { StoreAuth } from "../components/auth/StoreAuth";
|
||||
new StoreAuth();
|
||||
|
||||
// Handle view toggling
|
||||
const officerViewToggle = document.getElementById("officerViewToggle");
|
||||
const officerViewCheckbox = officerViewToggle?.querySelector(
|
||||
'input[type="checkbox"]'
|
||||
);
|
||||
const defaultView = document.getElementById("defaultView");
|
||||
const officerView = document.getElementById("officerView");
|
||||
|
||||
if (officerViewCheckbox && defaultView && officerView) {
|
||||
officerViewCheckbox.addEventListener("change", (e) => {
|
||||
const isChecked = (e.target as HTMLInputElement).checked;
|
||||
defaultView.style.display = isChecked ? "none" : "block";
|
||||
officerView.style.display = isChecked ? "block" : "none";
|
||||
});
|
||||
}
|
||||
</script>
|
Loading…
Reference in a new issue