added profile

separated profile from online-store (kind of)

added event management, editor, and viewer
This commit is contained in:
chark1es 2025-01-31 06:36:46 -08:00
parent 961cf1d9c7
commit 9b49a8f088
16 changed files with 2444 additions and 249 deletions

View file

@ -14,6 +14,7 @@
"astro-expressive-code": "^0.38.3", "astro-expressive-code": "^0.38.3",
"astro-icon": "^1.1.4", "astro-icon": "^1.1.4",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"motion": "^11.15.0", "motion": "^11.15.0",
"next": "^15.1.2", "next": "^15.1.2",
"pocketbase": "^0.25.1", "pocketbase": "^0.25.1",
@ -500,6 +501,8 @@
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], "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=="], "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=="], "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=="], "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=="], "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "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=="], "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=="], "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=="], "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=="], "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=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],

View file

@ -21,6 +21,7 @@
"astro-expressive-code": "^0.38.3", "astro-expressive-code": "^0.38.3",
"astro-icon": "^1.1.4", "astro-icon": "^1.1.4",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"motion": "^11.15.0", "motion": "^11.15.0",
"next": "^15.1.2", "next": "^15.1.2",
"pocketbase": "^0.25.1", "pocketbase": "^0.25.1",

File diff suppressed because it is too large Load diff

View 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);
}
}
}

View file

@ -132,10 +132,10 @@ export class StoreAuth {
private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } { private getElements(): AuthElements & { loadingSkeleton: HTMLDivElement } {
// Fun typescript fixes // Fun typescript fixes
const loginButton = document.getElementById( const loginButton = document.getElementById(
"loginButton", "contentLoginButton",
) as HTMLButtonElement; ) as HTMLButtonElement;
const logoutButton = document.getElementById( const logoutButton = document.getElementById(
"logoutButton", "contentLogoutButton",
) as HTMLButtonElement; ) as HTMLButtonElement;
const userInfo = document.getElementById("userInfo") as HTMLDivElement; const userInfo = document.getElementById("userInfo") as HTMLDivElement;
const loadingSkeleton = document.getElementById( const loadingSkeleton = document.getElementById(
@ -380,14 +380,25 @@ export class StoreAuth {
sponsorViewToggle, sponsorViewToggle,
} = this.elements; } = this.elements;
// Hide buttons initially // Get all login and logout buttons using classes
loginButton.style.display = "none"; const allLoginButtons = document.querySelectorAll('.login-button');
logoutButton.style.display = "none"; 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) { 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 // Update all the user information first
const user = this.pb.authStore.model; const user = this.pb.authStore.model;
const isSponsor = user.member_type === this.config.roles.sponsor.name; 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; userName.textContent = user.name || this.config.ui.messages.auth.notProvided;
userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable; userEmail.textContent = user.email || this.config.ui.messages.auth.notAvailable;
@ -454,12 +465,6 @@ export class StoreAuth {
} }
// Handle view toggles visibility // 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"; officerViewToggle.style.display = isOfficer ? "block" : "none";
sponsorViewToggle.style.display = isSponsor ? "block" : "none"; sponsorViewToggle.style.display = isSponsor ? "block" : "none";
@ -518,8 +523,12 @@ export class StoreAuth {
userInfo.style.opacity = "1"; userInfo.style.opacity = "1";
}, 50); }, 50);
logoutButton.style.display = "block"; officerViewToggle.style.display = isOfficer ? "block" : "none";
sponsorViewToggle.style.display = isSponsor ? "block" : "none";
} else { } else {
// Show login buttons for unauthenticated users
allLoginButtons.forEach(btn => btn.classList.remove("hidden"));
// Update for logged out state // Update for logged out state
userName.textContent = this.config.ui.messages.auth.notSignedIn; userName.textContent = this.config.ui.messages.auth.notSignedIn;
userEmail.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"; userInfo.style.opacity = "1";
}, 50); }, 50);
loginButton.style.display = "block";
officerViewToggle.style.display = "none"; officerViewToggle.style.display = "none";
sponsorViewToggle.style.display = "none"; sponsorViewToggle.style.display = "none";
} }

View file

@ -47,20 +47,34 @@
</div> </div>
<div class="divider my-0.5"></div> <div class="divider my-0.5"></div>
<!-- Resume --> <!-- Event Check-in -->
<div class="space-y-1"> <div class="space-y-2">
<div class="skeleton h-3 w-16 opacity-70"></div> <div class="skeleton h-3 w-24 opacity-70"></div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="skeleton h-[1.25rem] flex-1"></div> <div class="skeleton h-8 flex-1"></div>
<div class="skeleton h-[1.25rem] w-24"></div> <div class="skeleton h-8 w-24"></div>
</div> </div>
<div class="skeleton h-8 w-full"></div> <div class="skeleton h-3 w-48 opacity-70"></div>
</div> </div>
</div> </div>
<div class="divider my-0.5"></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="pt-2">
<div class="skeleton h-10 w-full"></div> <div class="skeleton h-10 w-full"></div>
</div> </div>
@ -194,10 +208,14 @@
</div> </div>
<div class="divider my-0.5"></div> <div class="divider my-0.5"></div>
<div class="pt-2"> <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 >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 >Sign Out</button
> >
</div> </div>
@ -347,3 +365,10 @@
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
} }
</style> </style>
<script>
import { StoreAuth } from "./StoreAuth";
import { EventCheckIn } from "./EventCheckIn";
new StoreAuth();
new EventCheckIn();
</script>

View 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>

View 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>

View 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>

View file

@ -49,7 +49,9 @@
</div> </div>
</div> </div>
<div class="overflow-x-auto"> <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"> <thead class="hidden lg:table-header-group">
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -60,7 +62,7 @@
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="resumeList" class="divide-y"> <tbody id="resumeList" class="divide-y divide-base-200">
<!-- Resume entries will be populated here --> <!-- Resume entries will be populated here -->
</tbody> </tbody>
</table> </table>

View file

@ -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>

View file

@ -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>

View file

@ -61,6 +61,14 @@ ui:
deleteSuccess: Event deleted successfully! deleteSuccess: Event deleted successfully!
deleteError: Failed to delete event. Please try again. deleteError: Failed to delete event. Please try again.
messageTimeout: 3000 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: auth:
loginError: Failed to start authentication loginError: Failed to start authentication
@ -70,6 +78,55 @@ ui:
notAvailable: Not available notAvailable: Not available
never: Never 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: defaults:
pageSize: 50 pageSize: 50
sortField: -updated sortField: -updated

View file

@ -2,8 +2,8 @@
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import UserProfile from "../components/auth/UserProfile.astro"; import UserProfile from "../components/auth/UserProfile.astro";
import DefaultStoreView from "../components/store/DefaultStoreView.astro"; import DefaultStoreView from "../components/store/DefaultStoreView.astro";
import OfficerStoreView from "../components/store/OfficerStoreView.astro"; import OfficerStoreView from "../components/profile/OfficerView.astro";
const title = "IEEE Store"; const title = "IEEE Online Store";
--- ---
<Layout {title}> <Layout {title}>

50
src/pages/profile.astro Normal file
View 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>