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-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=="],
|
||||||
|
|
|
@ -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
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 } {
|
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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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>
|
</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>
|
|
@ -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!
|
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
|
||||||
|
|
|
@ -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
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