fix file handling and previews

This commit is contained in:
chark1es 2025-02-13 05:30:07 -08:00
parent ba52ca0b91
commit 8eb7fdd90f
7 changed files with 1321 additions and 780 deletions

View file

@ -8,23 +8,28 @@
"@astrojs/react": "^4.2.0",
"@astrojs/tailwind": "5.1.4",
"@iconify-json/heroicons": "^1.2.2",
"@types/highlight.js": "^10.1.0",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"astro": "5.1.1",
"astro-expressive-code": "^0.38.3",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"highlight.js": "^11.11.1",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"motion": "^11.15.0",
"next": "^15.1.2",
"pocketbase": "^0.25.1",
"prismjs": "^1.29.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"rehype-expressive-code": "^0.40.2",
"tailwindcss": "^3.4.16",
},
"devDependencies": {
"@types/prismjs": "^1.26.5",
"daisyui": "^4.12.23",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
@ -150,13 +155,13 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@expressive-code/core": ["@expressive-code/core@0.38.3", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-s0/OtdRpBONwcn23O8nVwDNQqpBGKscysejkeBkwlIeHRLZWgiTVrusT5Idrdz1d8cW5wRk9iGsAIQmwDPXgJg=="],
"@expressive-code/core": ["@expressive-code/core@0.40.2", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-gXY3v7jbgz6nWKvRpoDxK4AHUPkZRuJsM79vHX/5uhV9/qX6Qnctp/U/dMHog/LCVXcuOps+5nRmf1uxQVPb3w=="],
"@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.38.3", "", { "dependencies": { "@expressive-code/core": "^0.38.3" } }, "sha512-qL2oC6FplmHNQfZ8ZkTR64/wKo9x0c8uP2WDftR/ydwN/yhe1ed7ZWYb8r3dezxsls+tDokCnN4zYR594jbpvg=="],
"@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.40.2", "", { "dependencies": { "@expressive-code/core": "^0.40.2" } }, "sha512-aLw5IlDlZWb10Jo/TTDCVsmJhKfZ7FJI83Zo9VDrV0OBlmHAg7klZqw68VDz7FlftIBVAmMby53/MNXPnMjTSQ=="],
"@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.38.3", "", { "dependencies": { "@expressive-code/core": "^0.38.3", "shiki": "^1.22.2" } }, "sha512-kqHnglZeesqG3UKrb6e9Fq5W36AZ05Y9tCREmSN2lw8LVTqENIeCIkLDdWtQ5VoHlKqwUEQFTVlRehdwoY7Gmw=="],
"@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.40.2", "", { "dependencies": { "@expressive-code/core": "^0.40.2", "shiki": "^1.26.1" } }, "sha512-t2HMR5BO6GdDW1c1ISBTk66xO503e/Z8ecZdNcr6E4NpUfvY+MRje+LtrcvbBqMwWBBO8RpVKcam/Uy+1GxwKQ=="],
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.38.3", "", { "dependencies": { "@expressive-code/core": "^0.38.3" } }, "sha512-dPK3+BVGTbTmGQGU3Fkj3jZ3OltWUAlxetMHI6limUGCWBCucZiwoZeFM/WmqQa71GyKRzhBT+iEov6kkz2xVA=="],
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.40.2", "", { "dependencies": { "@expressive-code/core": "^0.40.2" } }, "sha512-/XoLjD67K9nfM4TgDlXAExzMJp6ewFKxNpfUw4F7q5Ecy+IU3/9zQQG/O70Zy+RxYTwKGw2MA9kd7yelsxnSmw=="],
"@iconify-json/heroicons": ["@iconify-json/heroicons@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qoW4pXr5kTTL6juEjgTs83OJIwpePu7q1tdtKVEdj+i0zyyVHgg/dd9grsXJQnpTpBt6/VwNjrXBvFjRsKPENg=="],
@ -320,6 +325,10 @@
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.24.0", "", { "dependencies": { "@shikijs/types": "1.24.0", "@shikijs/vscode-textmate": "^9.3.0" } }, "sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg=="],
"@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="],
"@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="],
"@shikijs/types": ["@shikijs/types@1.24.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } }, "sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@9.3.0", "", {}, "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA=="],
@ -350,6 +359,8 @@
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/highlight.js": ["@types/highlight.js@10.1.0", "", { "dependencies": { "highlight.js": "*" } }, "sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
@ -362,6 +373,8 @@
"@types/node": ["@types/node@22.10.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ=="],
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
"@types/react": ["@types/react@19.0.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="],
"@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="],
@ -402,7 +415,7 @@
"astro": ["astro@5.1.1", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/internal-helpers": "0.4.2", "@astrojs/markdown-remark": "6.0.1", "@astrojs/telemetry": "3.2.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.3", "@types/cookie": "^0.6.0", "acorn": "^8.14.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.1.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^0.7.2", "cssesc": "^3.0.0", "debug": "^4.3.7", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.5.4", "esbuild": "^0.21.5", "estree-walker": "^3.0.3", "fast-glob": "^3.3.2", "flattie": "^1.1.1", "github-slugger": "^2.0.0", "html-escaper": "^3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.14", "magicast": "^0.3.5", "micromatch": "^4.0.8", "mrmime": "^2.0.0", "neotraverse": "^0.6.18", "p-limit": "^6.1.0", "p-queue": "^8.0.1", "preferred-pm": "^4.0.0", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.6.3", "shiki": "^1.23.1", "tinyexec": "^0.3.1", "tsconfck": "^3.1.4", "ultrahtml": "^1.5.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.14.0", "vfile": "^6.0.3", "vite": "^6.0.5", "vitefu": "^1.0.4", "which-pm": "^3.0.0", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.1.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-prpWC2PRs4P3FKQg6gZaU+VNMqbZi5pDvORGB2nrjfRjkrvF6/l4BqhvkJ6YQ0Ohm5rIMVz8ljgaRI77mLHbwg=="],
"astro-expressive-code": ["astro-expressive-code@0.38.3", "", { "dependencies": { "rehype-expressive-code": "^0.38.3" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-Tvdc7RV0G92BbtyEOsfJtXU35w41CkM94fOAzxbQP67Wj5jArfserJ321FO4XA7WG9QMV0GIBmQq77NBIRDzpQ=="],
"astro-expressive-code": ["astro-expressive-code@0.40.2", "", { "dependencies": { "rehype-expressive-code": "^0.40.2" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-yJMQId0yXSAbW9I6yqvJ3FcjKzJ8zRL7elbJbllkv1ZJPlsI0NI83Pxn1YL1IapEM347EvOOkSW2GL+2+NO61w=="],
"astro-icon": ["astro-icon@1.1.5", "", { "dependencies": { "@iconify/tools": "^4.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^2.1.30" } }, "sha512-CJYS5nWOw9jz4RpGWmzNQY7D0y2ZZacH7atL2K9DeJXJVaz7/5WrxeyIxO8KASk1jCM96Q4LjRx/F3R+InjJrw=="],
@ -626,7 +639,7 @@
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
"expressive-code": ["expressive-code@0.38.3", "", { "dependencies": { "@expressive-code/core": "^0.38.3", "@expressive-code/plugin-frames": "^0.38.3", "@expressive-code/plugin-shiki": "^0.38.3", "@expressive-code/plugin-text-markers": "^0.38.3" } }, "sha512-COM04AiUotHCKJgWdn7NtW2lqu8OW8owAidMpkXt1qxrZ9Q2iC7+tok/1qIn2ocGnczvr9paIySgGnEwFeEQ8Q=="],
"expressive-code": ["expressive-code@0.40.2", "", { "dependencies": { "@expressive-code/core": "^0.40.2", "@expressive-code/plugin-frames": "^0.40.2", "@expressive-code/plugin-shiki": "^0.40.2", "@expressive-code/plugin-text-markers": "^0.40.2" } }, "sha512-1zIda2rB0qiDZACawzw2rbdBQiWHBT56uBctS+ezFe5XMAaFaHLnnSYND/Kd+dVzO9HfCXRDpzH3d+3fvOWRcw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@ -720,6 +733,8 @@
"hastscript": ["hastscript@9.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw=="],
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
@ -1132,7 +1147,7 @@
"rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
"rehype-expressive-code": ["rehype-expressive-code@0.38.3", "", { "dependencies": { "expressive-code": "^0.38.3" } }, "sha512-RYSSDkMBikoTbycZPkcWp6ELneANT4eTpND1DSRJ6nI2eVFUwTBDCvE2vO6jOOTaavwnPiydi4i/87NRyjpdOA=="],
"rehype-expressive-code": ["rehype-expressive-code@0.40.2", "", { "dependencies": { "expressive-code": "^0.40.2" } }, "sha512-+kn+AMGCrGzvtH8Q5lC6Y5lnmTV/r33fdmi5QU/IH1KPHKobKr5UnLwJuqHv5jBTSN/0v2wLDS7RTM73FVzqmQ=="],
"rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
@ -1384,6 +1399,8 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@expressive-code/plugin-shiki/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
@ -1392,6 +1409,10 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@shikijs/langs/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@shikijs/themes/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"ansi-align/string-width": ["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=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -1456,8 +1477,22 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.1", "", {}, "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@shikijs/langs/@shikijs/types/@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.1", "", {}, "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg=="],
"@shikijs/themes/@shikijs/types/@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.1", "", {}, "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -1522,6 +1557,10 @@
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"astro/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw=="],
@ -1571,5 +1610,9 @@
"astro/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw=="],
"astro/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
}
}

208
package-lock.json generated
View file

@ -12,23 +12,26 @@
"@astrojs/node": "^9.0.0",
"@astrojs/react": "^4.2.0",
"@astrojs/tailwind": "5.1.4",
"@iconify-json/heroicons": "^1.2.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"astro": "5.1.1",
"astro-expressive-code": "^0.38.3",
"astro-icon": "^1.1.4",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"motion": "^11.15.0",
"next": "^15.1.2",
"pocketbase": "^0.25.1",
"prismjs": "^1.29.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"tailwindcss": "^3.4.16"
},
"devDependencies": {
"@types/prismjs": "^1.26.5",
"daisyui": "^4.12.23",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
@ -484,7 +487,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
"integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==",
"license": "MIT",
"engines": {
"node": ">=14"
}
@ -900,10 +902,9 @@
}
},
"node_modules/@expressive-code/core": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.38.3.tgz",
"integrity": "sha512-s0/OtdRpBONwcn23O8nVwDNQqpBGKscysejkeBkwlIeHRLZWgiTVrusT5Idrdz1d8cW5wRk9iGsAIQmwDPXgJg==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.40.2.tgz",
"integrity": "sha512-gXY3v7jbgz6nWKvRpoDxK4AHUPkZRuJsM79vHX/5uhV9/qX6Qnctp/U/dMHog/LCVXcuOps+5nRmf1uxQVPb3w==",
"dependencies": {
"@ctrl/tinycolor": "^4.0.4",
"hast-util-select": "^6.0.2",
@ -917,31 +918,36 @@
}
},
"node_modules/@expressive-code/plugin-frames": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.38.3.tgz",
"integrity": "sha512-qL2oC6FplmHNQfZ8ZkTR64/wKo9x0c8uP2WDftR/ydwN/yhe1ed7ZWYb8r3dezxsls+tDokCnN4zYR594jbpvg==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.40.2.tgz",
"integrity": "sha512-aLw5IlDlZWb10Jo/TTDCVsmJhKfZ7FJI83Zo9VDrV0OBlmHAg7klZqw68VDz7FlftIBVAmMby53/MNXPnMjTSQ==",
"dependencies": {
"@expressive-code/core": "^0.38.3"
"@expressive-code/core": "^0.40.2"
}
},
"node_modules/@expressive-code/plugin-shiki": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.38.3.tgz",
"integrity": "sha512-kqHnglZeesqG3UKrb6e9Fq5W36AZ05Y9tCREmSN2lw8LVTqENIeCIkLDdWtQ5VoHlKqwUEQFTVlRehdwoY7Gmw==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.40.2.tgz",
"integrity": "sha512-t2HMR5BO6GdDW1c1ISBTk66xO503e/Z8ecZdNcr6E4NpUfvY+MRje+LtrcvbBqMwWBBO8RpVKcam/Uy+1GxwKQ==",
"dependencies": {
"@expressive-code/core": "^0.38.3",
"shiki": "^1.22.2"
"@expressive-code/core": "^0.40.2",
"shiki": "^1.26.1"
}
},
"node_modules/@expressive-code/plugin-text-markers": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.38.3.tgz",
"integrity": "sha512-dPK3+BVGTbTmGQGU3Fkj3jZ3OltWUAlxetMHI6limUGCWBCucZiwoZeFM/WmqQa71GyKRzhBT+iEov6kkz2xVA==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.40.2.tgz",
"integrity": "sha512-/XoLjD67K9nfM4TgDlXAExzMJp6ewFKxNpfUw4F7q5Ecy+IU3/9zQQG/O70Zy+RxYTwKGw2MA9kd7yelsxnSmw==",
"dependencies": {
"@expressive-code/core": "^0.38.3"
"@expressive-code/core": "^0.40.2"
}
},
"node_modules/@iconify-json/heroicons": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@iconify-json/heroicons/-/heroicons-1.2.2.tgz",
"integrity": "sha512-qoW4pXr5kTTL6juEjgTs83OJIwpePu7q1tdtKVEdj+i0zyyVHgg/dd9grsXJQnpTpBt6/VwNjrXBvFjRsKPENg==",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify/tools": {
@ -1960,73 +1966,66 @@
]
},
"node_modules/@shikijs/core": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.25.1.tgz",
"integrity": "sha512-0j5k3ZkLTQViOuNzPVyWGoW1zgH3kiFdUT/JOCkTm7TU74mz+dF+NID+YoiCBzHQxgsDpcGYPjKDJRcuVLSt4A==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz",
"integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==",
"dependencies": {
"@shikijs/engine-javascript": "1.25.1",
"@shikijs/engine-oniguruma": "1.25.1",
"@shikijs/types": "1.25.1",
"@shikijs/vscode-textmate": "^9.3.1",
"@shikijs/engine-javascript": "1.29.2",
"@shikijs/engine-oniguruma": "1.29.2",
"@shikijs/types": "1.29.2",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.4"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.25.1.tgz",
"integrity": "sha512-zQ7UWKnRCfD/Q1M+XOSyjsbhpE0qv8LUnmn82HYCeOsgAHgUZGEDIQ63bbuK3kU5sQg+2CtI+dPfOqD/mjSY9w==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz",
"integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==",
"dependencies": {
"@shikijs/types": "1.25.1",
"@shikijs/vscode-textmate": "^9.3.1",
"oniguruma-to-es": "0.10.0"
"@shikijs/types": "1.29.2",
"@shikijs/vscode-textmate": "^10.0.1",
"oniguruma-to-es": "^2.2.0"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.25.1.tgz",
"integrity": "sha512-iKPMh3H+0USHtWfZ1irfMTH6tGmIUFSnqt3E2K8BgI1VEsqiPh0RYkG2WTwzNiM1/WHN4FzYx/nrKR7PDHiRyw==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz",
"integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==",
"dependencies": {
"@shikijs/types": "1.25.1",
"@shikijs/vscode-textmate": "^9.3.1"
"@shikijs/types": "1.29.2",
"@shikijs/vscode-textmate": "^10.0.1"
}
},
"node_modules/@shikijs/langs": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.25.1.tgz",
"integrity": "sha512-hdYjq9aRJplAzGe2qF51PR9IDgEoyGb4IkXvr3Ts6lEdg4Z8M/kdknKRo2EIuv3IR/aKkJXTlBQRM+wr3t20Ew==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz",
"integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==",
"dependencies": {
"@shikijs/types": "1.25.1"
"@shikijs/types": "1.29.2"
}
},
"node_modules/@shikijs/themes": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.25.1.tgz",
"integrity": "sha512-JO0lDn4LgGqg5QKvgich5ScUmC2okK+LxM9a3iLUH7YMeI2c8UGXThuJv6sZduS7pdJbYQHPrvWq9t/V4GhpbQ==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz",
"integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==",
"dependencies": {
"@shikijs/types": "1.25.1"
"@shikijs/types": "1.29.2"
}
},
"node_modules/@shikijs/types": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.25.1.tgz",
"integrity": "sha512-dceqFUoO95eY4tpOj3OGq8wE8EgJ4ey6Me1HQEu5UbwIYszFndEll/bjlB8Kp9wl4fx3uM7n4+y9XCYuDBmcXA==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz",
"integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==",
"dependencies": {
"@shikijs/vscode-textmate": "^9.3.1",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.1.tgz",
"integrity": "sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==",
"license": "MIT"
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz",
"integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg=="
},
"node_modules/@swc/counter": {
"version": "0.1.3",
@ -2185,6 +2184,12 @@
"undici-types": "~6.20.0"
}
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
"dev": true
},
"node_modules/@types/react": {
"version": "19.0.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
@ -2497,12 +2502,11 @@
}
},
"node_modules/astro-expressive-code": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.38.3.tgz",
"integrity": "sha512-Tvdc7RV0G92BbtyEOsfJtXU35w41CkM94fOAzxbQP67Wj5jArfserJ321FO4XA7WG9QMV0GIBmQq77NBIRDzpQ==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.40.2.tgz",
"integrity": "sha512-yJMQId0yXSAbW9I6yqvJ3FcjKzJ8zRL7elbJbllkv1ZJPlsI0NI83Pxn1YL1IapEM347EvOOkSW2GL+2+NO61w==",
"dependencies": {
"rehype-expressive-code": "^0.38.3"
"rehype-expressive-code": "^0.40.2"
},
"peerDependencies": {
"astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0"
@ -2620,7 +2624,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
"integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@ -3142,8 +3145,7 @@
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
]
},
"node_modules/css-selector-tokenizer": {
"version": "0.8.0",
@ -3398,7 +3400,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz",
"integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==",
"license": "MIT",
"bin": {
"direction": "cli.js"
},
@ -3504,8 +3505,7 @@
"node_modules/emoji-regex-xs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
"license": "MIT"
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="
},
"node_modules/encodeurl": {
"version": "2.0.0",
@ -3773,15 +3773,14 @@
"license": "MIT"
},
"node_modules/expressive-code": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.38.3.tgz",
"integrity": "sha512-COM04AiUotHCKJgWdn7NtW2lqu8OW8owAidMpkXt1qxrZ9Q2iC7+tok/1qIn2ocGnczvr9paIySgGnEwFeEQ8Q==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.40.2.tgz",
"integrity": "sha512-1zIda2rB0qiDZACawzw2rbdBQiWHBT56uBctS+ezFe5XMAaFaHLnnSYND/Kd+dVzO9HfCXRDpzH3d+3fvOWRcw==",
"dependencies": {
"@expressive-code/core": "^0.38.3",
"@expressive-code/plugin-frames": "^0.38.3",
"@expressive-code/plugin-shiki": "^0.38.3",
"@expressive-code/plugin-text-markers": "^0.38.3"
"@expressive-code/core": "^0.40.2",
"@expressive-code/plugin-frames": "^0.40.2",
"@expressive-code/plugin-shiki": "^0.40.2",
"@expressive-code/plugin-text-markers": "^0.40.2"
}
},
"node_modules/extend": {
@ -4227,7 +4226,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz",
"integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
@ -4291,7 +4289,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.3.tgz",
"integrity": "sha512-OVRQlQ1XuuLP8aFVLYmC2atrfWHS5UD3shonxpnyrjcCkwtvmt/+N6kYJdcY4mkMJhxp4kj2EFIxQ9kvkkt/eQ==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
@ -4430,7 +4427,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
"integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
@ -6490,10 +6486,9 @@
}
},
"node_modules/oniguruma-to-es": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.10.0.tgz",
"integrity": "sha512-zapyOUOCJxt+xhiNRPPMtfJkHGsZ98HHB9qJEkdT8BGytO/+kpe4m1Ngf0MzbzTmhacn11w9yGeDP6tzDhnCdg==",
"license": "MIT",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz",
"integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==",
"dependencies": {
"emoji-regex-xs": "^1.0.0",
"regex": "^5.1.1",
@ -7244,7 +7239,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz",
"integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
@ -7253,7 +7247,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz",
"integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==",
"license": "MIT",
"dependencies": {
"regex": "^5.1.1",
"regex-utilities": "^2.3.0"
@ -7262,8 +7255,7 @@
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="
},
"node_modules/rehype": {
"version": "13.0.2",
@ -7282,12 +7274,11 @@
}
},
"node_modules/rehype-expressive-code": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.38.3.tgz",
"integrity": "sha512-RYSSDkMBikoTbycZPkcWp6ELneANT4eTpND1DSRJ6nI2eVFUwTBDCvE2vO6jOOTaavwnPiydi4i/87NRyjpdOA==",
"license": "MIT",
"version": "0.40.2",
"resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.40.2.tgz",
"integrity": "sha512-+kn+AMGCrGzvtH8Q5lC6Y5lnmTV/r33fdmi5QU/IH1KPHKobKr5UnLwJuqHv5jBTSN/0v2wLDS7RTM73FVzqmQ==",
"dependencies": {
"expressive-code": "^0.38.3"
"expressive-code": "^0.40.2"
}
},
"node_modules/rehype-parse": {
@ -7754,18 +7745,17 @@
}
},
"node_modules/shiki": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.25.1.tgz",
"integrity": "sha512-/1boRvNYwRW3GLG9Y6dXdnZ/Ha+J5T/5y3hV7TGQUcDSBM185D3FCbXlz2eTGNKG2iWCbWqo+P0yhGKZ4/CUrw==",
"license": "MIT",
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz",
"integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==",
"dependencies": {
"@shikijs/core": "1.25.1",
"@shikijs/engine-javascript": "1.25.1",
"@shikijs/engine-oniguruma": "1.25.1",
"@shikijs/langs": "1.25.1",
"@shikijs/themes": "1.25.1",
"@shikijs/types": "1.25.1",
"@shikijs/vscode-textmate": "^9.3.1",
"@shikijs/core": "1.29.2",
"@shikijs/engine-javascript": "1.29.2",
"@shikijs/engine-oniguruma": "1.29.2",
"@shikijs/langs": "1.29.2",
"@shikijs/themes": "1.29.2",
"@shikijs/types": "1.29.2",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4"
}
},

View file

@ -15,23 +15,28 @@
"@astrojs/react": "^4.2.0",
"@astrojs/tailwind": "5.1.4",
"@iconify-json/heroicons": "^1.2.2",
"@types/highlight.js": "^10.1.0",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"astro": "5.1.1",
"astro-expressive-code": "^0.38.3",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"highlight.js": "^11.11.1",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"motion": "^11.15.0",
"next": "^15.1.2",
"pocketbase": "^0.25.1",
"prismjs": "^1.29.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"rehype-expressive-code": "^0.40.2",
"tailwindcss": "^3.4.16"
},
"devDependencies": {
"@types/prismjs": "^1.26.5",
"daisyui": "^4.12.23",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",

View file

@ -5,6 +5,7 @@ import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { FileManager } from "../pocketbase/FileManager";
import { SendLog } from "../pocketbase/SendLog";
import FilePreview from "../modals/FilePreview";
// Get instances
const get = Get.getInstance();
@ -92,6 +93,7 @@ declare global {
hideLoading: () => void;
deleteEvent: (eventId: string, eventName: string) => Promise<void>;
resetAndCloseModal: () => void;
previewFileInEditModal: (url: string, filename: string) => void;
}
}
---
@ -621,187 +623,190 @@ declare global {
<!-- Edit Event Modal -->
<dialog id="editEventModal" class="modal">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4" id="editModalTitle">Edit Event</h3>
<form id="editEventForm" class="space-y-4">
<input type="hidden" id="editEventId" />
<!-- Main Edit Form Section -->
<div id="editFormSection">
<h3 class="font-bold text-lg mb-4" id="editModalTitle">
Edit Event
</h3>
<form id="editEventForm" class="space-y-4">
<input type="hidden" id="editEventId" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Event Name -->
<div class="form-control">
<label class="label">
<span class="label-text">Event Name</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventName"
name="editEventName"
class="input input-bordered"
required
/>
</div>
<!-- Event Code -->
<div class="form-control">
<label class="label">
<span class="label-text">Event Code</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventCode"
name="editEventCode"
class="input input-bordered"
required
/>
</div>
<!-- Location -->
<div class="form-control">
<label class="label">
<span class="label-text">Location</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventLocation"
name="editEventLocation"
class="input input-bordered"
required
/>
</div>
<!-- Points to Reward -->
<div class="form-control">
<label class="label">
<span class="label-text">Points to Reward</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="number"
id="editEventPoints"
name="editEventPoints"
class="input input-bordered"
min="0"
required
/>
</div>
<!-- Start Date -->
<div class="form-control">
<label class="label">
<span class="label-text">Start Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
id="editEventStartDate"
name="editEventStartDate"
class="input input-bordered"
required
/>
</div>
<!-- End Date -->
<div class="form-control">
<label class="label">
<span class="label-text">End Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
id="editEventEndDate"
name="editEventEndDate"
class="input input-bordered"
required
/>
</div>
</div>
<!-- Description -->
<div class="form-control">
<label class="label">
<span class="label-text">Description</span>
<span class="label-text-alt text-error">*</span>
</label>
<textarea
id="editEventDescription"
name="editEventDescription"
class="textarea textarea-bordered"
rows="3"
required></textarea>
</div>
<!-- Files -->
<div class="form-control">
<label class="label">
<span class="label-text">Upload Files</span>
</label>
<input
type="file"
id="editEventFiles"
class="file-input file-input-bordered"
multiple
/>
<div class="mt-4 space-y-2">
<div id="newFiles" class="space-y-2">
<!-- New files will be listed here -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Event Name -->
<div class="form-control">
<label class="label">
<span class="label-text">Event Name</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventName"
name="editEventName"
class="input input-bordered"
required
/>
</div>
<div class="divider">Current Files</div>
<div id="currentFiles" class="space-y-2">
<!-- Current files will be listed here -->
<!-- Event Code -->
<div class="form-control">
<label class="label">
<span class="label-text">Event Code</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventCode"
name="editEventCode"
class="input input-bordered"
required
/>
</div>
<!-- Location -->
<div class="form-control">
<label class="label">
<span class="label-text">Location</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventLocation"
name="editEventLocation"
class="input input-bordered"
required
/>
</div>
<!-- Points to Reward -->
<div class="form-control">
<label class="label">
<span class="label-text">Points to Reward</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="number"
id="editEventPoints"
name="editEventPoints"
class="input input-bordered"
min="0"
required
/>
</div>
<!-- Start Date -->
<div class="form-control">
<label class="label">
<span class="label-text">Start Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
id="editEventStartDate"
name="editEventStartDate"
class="input input-bordered"
required
/>
</div>
<!-- End Date -->
<div class="form-control">
<label class="label">
<span class="label-text">End Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
id="editEventEndDate"
name="editEventEndDate"
class="input input-bordered"
required
/>
</div>
</div>
</div>
<!-- Published -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<!-- Description -->
<div class="form-control">
<label class="label">
<span class="label-text">Description</span>
<span class="label-text-alt text-error">*</span>
</label>
<textarea
id="editEventDescription"
name="editEventDescription"
class="textarea textarea-bordered"
rows="3"
required></textarea>
</div>
<!-- Files -->
<div class="form-control">
<label class="label">
<span class="label-text">Upload Files</span>
</label>
<input
type="checkbox"
id="editEventPublished"
name="editEventPublished"
class="toggle"
type="file"
id="editEventFiles"
class="file-input file-input-bordered"
multiple
/>
<span class="label-text">Publish Event</span>
</label>
<label class="label">
<span class="label-text-alt text-info"
>This has to be clicked if you want to make this event
available to the public</span
>
</label>
</div>
<div class="mt-4 space-y-2">
<div id="newFiles" class="space-y-2">
<!-- New files will be listed here -->
</div>
<div class="divider">Current Files</div>
<div id="currentFiles" class="space-y-2">
<!-- Current files will be listed here -->
</div>
</div>
</div>
<!-- Has Food -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
id="editEventHasFood"
name="editEventHasFood"
class="toggle"
/>
<span class="label-text">Has Food</span>
</label>
<label class="label">
<span class="label-text-alt text-info"
>Check this if food will be provided at the event</span
>
</label>
</div>
<!-- Published -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
id="editEventPublished"
name="editEventPublished"
class="toggle"
/>
<span class="label-text">Publish Event</span>
</label>
<label class="label">
<span class="label-text-alt text-info"
>This has to be clicked if you want to make this
event available to the public</span
>
</label>
</div>
<div class="modal-action">
<button type="submit" class="btn btn-primary"
>Save Changes</button
>
<button
type="button"
class="btn"
onclick="editEventModal.close()">Cancel</button
>
</div>
</form>
<!-- Has Food -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
id="editEventHasFood"
name="editEventHasFood"
class="toggle"
/>
<span class="label-text">Has Food</span>
</label>
<label class="label">
<span class="label-text-alt text-info"
>Check this if food will be provided at the event</span
>
</label>
</div>
</form>
</div>
<div class="modal-action">
<button type="submit" class="btn btn-primary" form="editEventForm"
>Save Changes</button
>
<button type="button" class="btn" onclick="editEventModal.close()"
>Cancel</button
>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
@ -812,9 +817,7 @@ declare global {
<dialog id="eventDetailsModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<h3 class="font-bold text-lg" id="modalTitle">Event Details</h3>
</div>
<h3 class="font-bold text-lg">Event Details</h3>
<button
class="btn btn-circle btn-ghost"
onclick="eventDetailsModal.close()"
@ -867,29 +870,41 @@ declare global {
</div>
<!-- Attendees list will be populated here -->
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- File Preview Section -->
<div id="filePreviewSection" class="hidden">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<button
class="btn btn-ghost btn-sm"
onclick="backToFileList()"
>
← Back
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName">
</h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="loadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
<!-- Universal File Preview Modal -->
<dialog id="filePreviewModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<button
class="btn btn-ghost btn-sm"
onclick="window.closeFilePreview()"
>
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full"></div>
← Back
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName">
</h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="previewLoadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
>
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full">
<FilePreview
client:load
url=""
filename=""
id="universalFilePreview"
/>
</div>
</div>
</div>
@ -904,6 +919,10 @@ declare global {
import { Update } from "../pocketbase/Update";
import { FileManager } from "../pocketbase/FileManager";
import { SendLog } from "../pocketbase/SendLog";
import FilePreview from "../modals/FilePreview";
// Add file storage
const selectedFileStorage = new Map<string, File>();
interface AttendeeEntry {
user_id: string;
@ -1839,28 +1858,10 @@ declare global {
freshEventData.files &&
freshEventData.files.length > 0
) {
currentFiles.innerHTML = freshEventData.files
.map(
(filename) => `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span class="truncate">${filename}</span>
<div class="flex gap-2">
<button type="button" class="btn btn-ghost btn-xs" onclick="window.previewFile('${fileManager.getFileUrl("events", freshEventData.id, filename)}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
</svg>
</button>
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="window.deleteFile('${freshEventData.id}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
`
)
.join("");
currentFiles.innerHTML = updateFilePreviewButtons(
freshEventData.files,
freshEventData.id
);
}
}
@ -1871,4 +1872,492 @@ declare global {
// Show error toast or alert
}
};
// Update the previewFileInEditModal function
window.previewFileInEditModal = async function (
url: string,
filename: string
) {
const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName");
const loadingSpinner = document.getElementById("editLoadingSpinner");
if (
!editFormSection ||
!previewSection ||
!editFilePreview ||
!previewFileName ||
!loadingSpinner
)
return;
// Hide form and show preview section
editFormSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = filename;
// Show loading spinner
loadingSpinner.classList.remove("hidden");
try {
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename },
});
editFilePreview.dispatchEvent(event);
} finally {
// Hide loading spinner
loadingSpinner.classList.add("hidden");
}
};
// Update the showFilePreview function
window.showFilePreview = function (file: { url: string; name: string }) {
const fileListSection = document.getElementById("filesContent");
const previewSection = document.getElementById("filePreviewSection");
const mainFilePreview = document.getElementById("mainFilePreview");
const previewFileName = document.getElementById("previewFileName");
if (
!fileListSection ||
!previewSection ||
!mainFilePreview ||
!previewFileName
)
return;
// Hide file list and show preview section
fileListSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = file.name;
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url: file.url, filename: file.name },
});
mainFilePreview.dispatchEvent(event);
};
// Add backToEditForm function
window.backToEditForm = function () {
const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName");
if (
editFormSection &&
previewSection &&
editFilePreview &&
previewFileName
) {
editFormSection.classList.remove("hidden");
previewSection.classList.add("hidden");
// Reset the preview
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" },
});
editFilePreview.dispatchEvent(event);
previewFileName.textContent = "";
}
};
// Universal file preview function
window.previewFile = function (url: string, filename: string) {
const modal = document.getElementById(
"filePreviewModal"
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const filePreview = document.getElementById("universalFilePreview");
const loadingSpinner = document.getElementById("previewLoadingSpinner");
if (!modal || !previewFileName || !filePreview || !loadingSpinner)
return;
// Show modal and update filename
modal.showModal();
previewFileName.textContent = filename;
// Show loading spinner
loadingSpinner.classList.remove("hidden");
try {
// Update the FilePreview component
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename },
});
filePreview.dispatchEvent(event);
} finally {
// Hide loading spinner
loadingSpinner.classList.add("hidden");
}
};
// Close file preview
window.closeFilePreview = function () {
const modal = document.getElementById(
"filePreviewModal"
) as HTMLDialogElement;
const filePreview = document.getElementById("universalFilePreview");
if (modal && filePreview) {
// Reset the preview
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" },
});
filePreview.dispatchEvent(event);
modal.close();
}
};
// Update all file preview buttons to use the universal preview
function updateFilePreviewButtons(files: string[], eventId: string) {
return files
.map(
(filename) => `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span class="truncate">${filename}</span>
<div class="flex gap-2">
<button type="button" class="btn btn-ghost btn-xs" onclick="window.previewFile('${fileManager.getFileUrl("events", eventId, filename)}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
</svg>
</button>
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="window.deleteFile('${eventId}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
`
)
.join("");
}
// Update the openDetailsModal function to use the universal preview
window.openDetailsModal = function (event: Event) {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
const attendeesContent = document.getElementById("attendeesContent");
if (!modal || !filesContent || !attendeesContent) return;
// Show modal
modal.showModal();
// Update files list
if (event.files && event.files.length > 0) {
filesContent.innerHTML = `
<div class="space-y-2">
${updateFilePreviewButtons(event.files, event.id)}
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<p>No files attached to this event</p>
</div>
`;
}
};
// Add file input change handler to show selected files
document
.getElementById("editEventFiles")
?.addEventListener("change", function (e) {
const fileInput = e.target as HTMLInputElement;
const newFiles = document.getElementById("newFiles");
if (newFiles && fileInput.files) {
// Get existing files if any
const existingFiles = newFiles.querySelectorAll(".file-item");
const existingFilesArray = Array.from(existingFiles).map(
(item) => {
const nameSpan = item.querySelector(".file-name");
return nameSpan ? nameSpan.textContent : "";
}
);
// Store new files in the storage and update UI
Array.from(fileInput.files)
.filter((file) => !existingFilesArray.includes(file.name))
.forEach((file) => {
selectedFileStorage.set(file.name, file);
const fileDiv = document.createElement("div");
fileDiv.className =
"flex items-center justify-between p-2 bg-base-200 rounded-lg file-item";
fileDiv.innerHTML = `
<span class="truncate file-name">${file.name}</span>
<div class="flex gap-2">
<div class="badge badge-primary">New</div>
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="this.closest('.file-item').remove(); selectedFileStorage.delete('${file.name}');">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>
</div>
`;
newFiles.appendChild(fileDiv);
});
}
// Clear the file input
fileInput.value = "";
});
// Modify form submission handler to use selectedFileStorage
document
.getElementById("editEventForm")
?.addEventListener("submit", async function (e) {
e.preventDefault();
const form = e.target as HTMLFormElement;
// Get the submit and cancel buttons from the modal-action div
const modalAction = document.querySelector(".modal-action");
const submitButton = modalAction?.querySelector(
".btn-primary"
) as HTMLButtonElement;
const cancelButton = modalAction?.querySelector(
".btn:not(.btn-primary)"
) as HTMLButtonElement;
if (!submitButton || !cancelButton) {
console.error("Could not find submit or cancel buttons");
return;
}
const originalText = submitButton.innerHTML;
try {
const formData = new FormData(form);
const eventId = (
document.getElementById("editEventId") as HTMLInputElement
)?.value;
// Get files from storage
const selectedFiles = Array.from(selectedFileStorage.values());
// Disable buttons and show loading state
submitButton.disabled = true;
cancelButton.disabled = true;
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Saving...</span>
</div>
`;
window.showLoading?.();
// Prepare event data
const eventData = {
event_name: formData.get("editEventName"),
event_code: formData.get("editEventCode"),
event_description: formData.get("editEventDescription"),
location: formData.get("editEventLocation"),
points_to_reward: Number(formData.get("editEventPoints")),
start_date: new Date(
formData.get("editEventStartDate") as string
).toISOString(),
end_date: new Date(
formData.get("editEventEndDate") as string
).toISOString(),
published: formData.get("editEventPublished") === "on",
has_food: formData.get("editEventHasFood") === "on",
};
let updatedEvent;
try {
if (eventId) {
// Update existing event
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Updating event...</span>
</div>
`;
updatedEvent = await update.updateFields(
"events",
eventId,
eventData
);
// Handle file uploads if any
if (selectedFiles.length > 0) {
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Uploading files (0/${selectedFiles.length})...</span>
</div>
`;
await fileManager.appendFiles(
"events",
eventId,
"files",
selectedFiles
);
}
await sendLog.send(
"update",
"event",
`Updated event: ${eventData.event_name}`
);
} else {
// Create new event
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Creating event...</span>
</div>
`;
const pb = auth.getPocketBase();
const newEvent = await pb
.collection("events")
.create(eventData);
// Handle file uploads if any
if (selectedFiles.length > 0) {
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Uploading files (0/${selectedFiles.length})...</span>
</div>
`;
await fileManager.uploadFiles(
"events",
newEvent.id,
"files",
selectedFiles
);
}
await sendLog.send(
"create",
"event",
`Created event: ${eventData.event_name}`
);
}
// Show success state briefly
submitButton.innerHTML = `
<div class="flex items-center gap-2 text-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="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>
<span>Saved!</span>
</div>
`;
await new Promise((resolve) => setTimeout(resolve, 1000));
// Close modal and refresh events list
const modal = document.getElementById(
"editEventModal"
) as HTMLDialogElement;
modal?.close();
// Force cache refresh and update events list
lastCacheUpdate = 0; // Reset cache timestamp to force refresh
await refreshCache(); // Refresh the cache
await fetchEvents(); // Update the UI
// Clear form inputs
const formFileInput = document.getElementById(
"editEventFiles"
) as HTMLInputElement;
const newFiles = document.getElementById("newFiles");
if (formFileInput) formFileInput.value = "";
if (newFiles) newFiles.innerHTML = "";
// Clear storage after successful upload
selectedFileStorage.clear();
} catch (error) {
throw error; // Re-throw to be caught by outer try-catch
}
} catch (error) {
console.error("Failed to save event:", error);
// Show error message in the button with icon
submitButton.innerHTML = `
<div class="flex items-center gap-2 text-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<span>Failed</span>
</div>
`;
await new Promise((resolve) => setTimeout(resolve, 2000));
// Show detailed error to user
alert("Failed to save event. Please try again.");
} finally {
// Reset button state
submitButton.disabled = false;
cancelButton.disabled = false;
submitButton.innerHTML = originalText;
window.hideLoading?.();
}
});
// Clear storage when modal is closed
document.getElementById("editEventModal")?.addEventListener("close", () => {
selectedFileStorage.clear();
});
// Add delete file handler
window.deleteFile = async function (eventId: string, filename: string) {
if (!confirm("Are you sure you want to delete this file?")) return;
try {
window.showLoading?.();
const pb = auth.getPocketBase();
// Get current event data
const event = await pb.collection("events").getOne(eventId);
// Filter out the file to delete
const updatedFiles = event.files.filter(
(f: string) => f !== filename
);
// Update the event with the new files array
await pb.collection("events").update(eventId, {
files: updatedFiles,
});
await sendLog.send(
"delete",
"event_file",
`Deleted file ${filename} from event ${event.event_name}`
);
// Refresh the current files display
const currentFiles = document.getElementById("currentFiles");
if (currentFiles && updatedFiles.length > 0) {
currentFiles.innerHTML = updateFilePreviewButtons(
updatedFiles,
eventId
);
} else if (currentFiles) {
currentFiles.innerHTML = `
<div class="text-center py-4 text-base-content/70">
<p>No files attached</p>
</div>
`;
}
} catch (error) {
console.error("Failed to delete file:", error);
alert("Failed to delete file. Please try again.");
} finally {
window.hideLoading?.();
}
};
</script>

View file

@ -0,0 +1,415 @@
import React from 'react';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css';
interface FilePreviewProps {
url: string;
filename: string;
id?: string;
}
type JSXElement = React.ReactElement;
const FilePreview: React.FC<FilePreviewProps> = ({ url: initialUrl, filename: initialFilename, id }) => {
const [url, setUrl] = React.useState(initialUrl);
const [filename, setFilename] = React.useState(initialFilename);
const [visibleLines, setVisibleLines] = React.useState(20);
const elementRef = React.useRef<HTMLDivElement>(null);
// Constants for text preview
const INITIAL_LINES = 20;
const INCREMENT_LINES = 50;
const MAX_CHARS_PER_LINE = 120;
const TRUNCATION_MESSAGE = '...';
// Determine file type from extension
const fileExtension = filename.split('.').pop()?.toLowerCase() || '';
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(fileExtension);
const isPDF = fileExtension === 'pdf';
const isCode = [
'py', 'js', 'jsx', 'ts', 'tsx', 'html', 'htm', 'css', 'scss',
'java', 'c', 'cpp', 'cs', 'go', 'rs', 'sql', 'php', 'rb',
'swift', 'kt', 'sh', 'bash', 'yaml', 'yml', 'json', 'md',
'astro', 'vue', 'svelte', 'xml', 'toml', 'ini', 'env',
'graphql', 'prisma', 'dockerfile', 'nginx'
].includes(fileExtension);
const isText = isCode || ['txt', 'log', 'csv'].includes(fileExtension);
const isVideo = ['mp4', 'webm', 'mov', 'avi', 'mkv'].includes(fileExtension);
const isAudio = ['mp3', 'wav', 'm4a', 'ogg'].includes(fileExtension);
// Function to highlight code using highlight.js
const highlightCode = (code: string, language?: string): string => {
if (!language) return code;
try {
return hljs.highlight(code, { language }).value;
} catch (error) {
console.warn(`Failed to highlight code for language ${language}:`, error);
return code;
}
};
// Function to get the appropriate language for highlight.js
const getHighlightLanguage = (ext: string): string | undefined => {
// Map file extensions to highlight.js languages
const languageMap: { [key: string]: string } = {
'py': 'python',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'sql': 'sql',
'php': 'php',
'rb': 'ruby',
'swift': 'swift',
'kt': 'kotlin',
'sh': 'bash',
'bash': 'bash',
'yaml': 'yaml',
'yml': 'yaml',
'json': 'json',
'md': 'markdown',
'xml': 'xml',
'toml': 'ini',
'ini': 'ini',
'dockerfile': 'dockerfile',
'prisma': 'prisma',
'graphql': 'graphql'
};
return languageMap[ext];
};
// Function to truncate text content
const truncateContent = (text: string, maxLines: number): string => {
const lines = text.split('\n');
if (lines.length <= maxLines) return text;
const truncatedLines = lines.slice(0, maxLines).map(line =>
line.length > MAX_CHARS_PER_LINE
? line.slice(0, MAX_CHARS_PER_LINE) + '...'
: line
);
return truncatedLines.join('\n') + '\n' + TRUNCATION_MESSAGE;
};
// Reset visible lines when file changes
React.useEffect(() => {
setVisibleLines(INITIAL_LINES);
}, [url]);
// Function to show more lines
const showMoreLines = () => {
setVisibleLines(prev => prev + INCREMENT_LINES);
};
// Function to reset to initial view
const resetView = () => {
setVisibleLines(INITIAL_LINES);
};
// Listen for custom events to update the preview
React.useEffect(() => {
const element = elementRef.current;
if (!element) return;
const handleUpdatePreview = (e: CustomEvent<{ url: string; filename: string }>) => {
setUrl(e.detail.url);
setFilename(e.detail.filename);
};
element.addEventListener('updateFilePreview', handleUpdatePreview as EventListener);
return () => {
element.removeEventListener('updateFilePreview', handleUpdatePreview as EventListener);
};
}, []);
// Update state when props change
React.useEffect(() => {
setUrl(initialUrl);
setFilename(initialFilename);
}, [initialUrl, initialFilename]);
// For text files, we need to fetch and display the content
const [textContent, setTextContent] = React.useState<string>('');
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
async function fetchTextContent() {
if (!isText) return;
setIsLoading(true);
setError(null);
try {
const response = await fetch(url);
const text = await response.text();
setTextContent(text);
} catch (err) {
setError('Failed to load text content');
console.error('Error fetching text content:', err);
} finally {
setIsLoading(false);
}
}
if (isText) {
fetchTextContent();
}
}, [url, isText]);
// Function to parse CSV text into array
const parseCSV = (text: string): string[][] => {
const rows = text.split(/\r?\n/).filter(row => row.trim());
return rows.map(row => {
// Handle both quoted and unquoted CSV
const matches = row.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
return matches.map(cell => cell.replace(/^"|"$/g, '').trim());
});
};
// Function to format JSON with syntax highlighting
const formatJSON = (text: string): string => {
try {
const parsed = JSON.parse(text);
return highlightCode(JSON.stringify(parsed, null, 2), 'json');
} catch {
return text; // Return original text if not valid JSON
}
};
// Function to render CSV as table
const renderCSVTable = (csvData: string[][]): JSXElement => {
if (csvData.length === 0) return <p>No data</p>;
const headers = csvData[0];
const allRows = csvData.slice(1);
const rows = allRows.slice(0, visibleLines);
const remainingRows = allRows.length - visibleLines;
const hasMore = remainingRows > 0;
return (
<div className="space-y-4">
<div className="overflow-x-auto max-h-[60vh] overflow-y-auto">
<table className="table table-zebra w-full">
<thead className="sticky top-0 z-10">
<tr>
{headers.map((header, i) => (
<th key={i} className="bg-base-200">{header}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="text-center space-y-2">
{hasMore && (
<button
className="btn btn-ghost btn-sm"
onClick={showMoreLines}
>
Show More ({Math.min(remainingRows, INCREMENT_LINES)} of {remainingRows} rows)
</button>
)}
{visibleLines > INITIAL_LINES && (
<button
className="btn btn-ghost btn-sm"
onClick={resetView}
>
Reset View
</button>
)}
</div>
</div>
);
};
// Function to render text content based on file type
const renderTextContent = (): JSXElement => {
if (!textContent) return <p>No content</p>;
if (fileExtension === 'csv') {
const csvData = parseCSV(textContent);
return renderCSVTable(csvData);
}
const lines = textContent.split('\n');
const content = truncateContent(textContent, visibleLines);
const remainingLines = lines.length - visibleLines;
const hasMore = remainingLines > 0;
const renderContent = () => {
if (isCode) {
const language = getHighlightLanguage(fileExtension);
const highlightedCode = highlightCode(content, language);
return (
<code
className="text-sm font-mono"
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
);
}
return <code className="text-sm font-mono">{content}</code>;
};
return (
<div className="space-y-4">
<div className="max-h-[60vh] overflow-y-auto">
<pre className="whitespace-pre-wrap bg-base-200 p-4 rounded-lg overflow-x-auto">
{renderContent()}
</pre>
</div>
<div className="text-center space-y-2">
{hasMore && (
<button
className="btn btn-ghost btn-sm"
onClick={showMoreLines}
>
Show More ({Math.min(remainingLines, INCREMENT_LINES)} of {remainingLines} lines)
</button>
)}
{visibleLines > INITIAL_LINES && (
<button
className="btn btn-ghost btn-sm"
onClick={resetView}
>
Reset View
</button>
)}
</div>
</div>
);
};
if (isLoading) {
return (
<div ref={elementRef} id={id} className="flex justify-center items-center p-8">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
}
if (error) {
return (
<div ref={elementRef} id={id} className="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
);
}
if (isImage) {
return (
<div ref={elementRef} id={id} className="relative w-full max-h-[60vh] overflow-y-auto">
<img
src={url}
alt={filename}
className="max-w-full h-auto rounded-lg"
onError={() => setError('Failed to load image')}
/>
</div>
);
}
if (isPDF) {
return (
<div ref={elementRef} id={id} className="relative w-full h-[60vh]">
<iframe
src={url}
className="w-full h-full rounded-lg"
title={filename}
/>
</div>
);
}
if (isVideo) {
return (
<div ref={elementRef} id={id} className="relative w-full max-h-[60vh]">
<video
controls
className="w-full rounded-lg"
onError={() => setError('Failed to load video')}
>
<source src={url} type={`video/${fileExtension}`} />
Your browser does not support the video tag.
</video>
</div>
);
}
if (isAudio) {
return (
<div ref={elementRef} id={id} className="relative w-full">
<audio
controls
className="w-full"
onError={() => setError('Failed to load audio')}
>
<source src={url} type={`audio/${fileExtension}`} />
Your browser does not support the audio tag.
</audio>
</div>
);
}
if (isText) {
return (
<div ref={elementRef} id={id} className="relative w-full">
{renderTextContent()}
</div>
);
}
// Default case for unsupported file types
return (
<div ref={elementRef} id={id} className="text-center py-8">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12 mx-auto mb-4 opacity-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
<p className="text-base-content/70">Preview not available for this file type</p>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary btn-sm mt-4"
>
Download File
</a>
</div>
);
};
export default FilePreview;

View file

@ -1,444 +0,0 @@
import React, { useState, useEffect } from 'react';
import JSZip from 'jszip';
interface FileType {
url: string;
type: string;
name: string;
}
interface FileViewerModalProps {
isOpen: boolean;
onClose: () => void;
files: FileType | FileType[];
modalId?: string;
}
// Create a wrapper component that listens to custom events
export const FileViewerModalWrapper: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [files, setFiles] = useState<FileType[]>([]);
useEffect(() => {
let mounted = true;
// Listen for custom events to open/close modal and set files
const handleShowFiles = (event: CustomEvent) => {
if (mounted) {
const { files } = event.detail;
setFiles(Array.isArray(files) ? files : [files]);
setIsOpen(true);
}
};
// Add event listeners
window.addEventListener('showFileViewer' as any, handleShowFiles);
// Cleanup
return () => {
mounted = false;
window.removeEventListener('showFileViewer' as any, handleShowFiles);
// Reset state on unmount
setIsOpen(false);
setFiles([]);
};
}, []);
// Handle tab visibility changes
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
setIsOpen(false);
setFiles([]);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
const handleClose = () => {
setIsOpen(false);
setFiles([]);
};
// Only render the modal if we have files and it should be open
if (!isOpen || files.length === 0) return null;
return (
<FileViewerModal
isOpen={isOpen}
onClose={handleClose}
files={files}
modalId="file-viewer"
/>
);
};
const FileViewerModal: React.FC<FileViewerModalProps> = ({ isOpen, onClose, files, modalId = 'file-viewer' }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedFile, setSelectedFile] = useState<FileType | null>(null);
const [showPreview, setShowPreview] = useState(false);
const [downloadingAll, setDownloadingAll] = useState(false);
const fileArray = Array.isArray(files) ? files : [files];
// Reset state when modal closes
useEffect(() => {
if (!isOpen) {
setLoading(true);
setError(null);
setSelectedFile(null);
setShowPreview(false);
}
}, [isOpen]);
// Helper function to check if file type is previewable
const isPreviewableType = (fileType: string): boolean => {
return (
fileType.startsWith('image/') ||
fileType.startsWith('video/') ||
fileType.startsWith('audio/') ||
fileType === 'application/pdf' ||
fileType.startsWith('text/') ||
fileType === 'application/json'
);
};
useEffect(() => {
if (isOpen) {
// Only show file directly if there's exactly one file
if (fileArray.length === 1) {
const fileToShow = fileArray[0];
setSelectedFile(fileToShow);
setShowPreview(true);
setLoading(isPreviewableType(fileToShow.type));
} else {
// For multiple files, show the file browser
setShowPreview(false);
setSelectedFile(null);
setLoading(false);
}
setError(null);
}
}, [isOpen, files]);
const handleLoadSuccess = () => {
setLoading(false);
};
const handleLoadError = () => {
setLoading(false);
setError('Failed to load file');
};
const handleFileSelect = (file: FileType) => {
setSelectedFile(file);
setShowPreview(true);
setLoading(isPreviewableType(file.type));
setError(null);
};
const handleBackToList = () => {
setShowPreview(false);
setSelectedFile(null);
};
// Function to download all files as zip
const downloadAllFiles = async () => {
if (fileArray.length === 0) return;
setDownloadingAll(true);
const zip = new JSZip();
try {
// Download all files
const filePromises = fileArray.map(async (file) => {
const response = await fetch(file.url);
const blob = await response.blob();
zip.file(file.name, blob);
});
await Promise.all(filePromises);
// Generate and download zip
const content = await zip.generateAsync({ type: "blob" });
const zipUrl = URL.createObjectURL(content);
const link = document.createElement('a');
link.href = zipUrl;
link.download = 'files.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(zipUrl);
} catch (err) {
console.error('Failed to download files:', err);
setError('Failed to download files');
} finally {
setDownloadingAll(false);
}
};
const renderFileContent = (file: FileType) => {
const fileType = file.type.toLowerCase();
// If not a previewable type, don't show loading state
if (!isPreviewableType(fileType)) {
return (
<div className="flex flex-col items-center justify-center p-8">
<div className="text-4xl mb-4">📄</div>
<p className="text-center">
This file type ({file.type}) cannot be previewed.
<br />
<a
href={file.url}
download={file.name}
className="btn btn-primary mt-4"
target="_blank"
rel="noopener noreferrer"
>
Open in New Tab
</a>
</p>
</div>
);
}
if (fileType.startsWith('image/')) {
return (
<img
src={file.url}
alt={file.name}
className="max-w-full max-h-[70vh] object-contain"
onLoad={handleLoadSuccess}
onError={handleLoadError}
/>
);
}
if (fileType.startsWith('video/')) {
return (
<video
controls
className="max-w-full max-h-[70vh]"
onLoadedData={handleLoadSuccess}
onError={handleLoadError}
>
<source src={file.url} type={file.type} />
Your browser does not support the video tag.
</video>
);
}
if (fileType === 'application/pdf') {
return (
<iframe
src={file.url}
className="w-full h-[70vh]"
onLoad={handleLoadSuccess}
onError={handleLoadError}
/>
);
}
if (fileType.startsWith('text/') || fileType === 'application/json') {
return (
<iframe
src={file.url}
className="w-full h-[70vh] font-mono"
onLoad={handleLoadSuccess}
onError={handleLoadError}
/>
);
}
if (fileType.startsWith('audio/')) {
return (
<audio
controls
className="w-full"
onLoadedData={handleLoadSuccess}
onError={handleLoadError}
>
<source src={file.url} type={file.type} />
Your browser does not support the audio element.
</audio>
);
}
return (
<div className="flex flex-col items-center justify-center p-8">
<div className="text-4xl mb-4">📄</div>
<p className="text-center">
This file type ({file.type}) cannot be previewed.
<br />
<a
href={file.url}
download={file.name}
className="btn btn-primary mt-4"
target="_blank"
rel="noopener noreferrer"
>
Open in New Tab
</a>
</p>
</div>
);
};
const renderFileList = () => {
return (
<div className="w-full">
<div className="flex justify-between items-center mb-4">
<h3 className="font-bold text-lg">Files ({fileArray.length})</h3>
{fileArray.length > 1 && (
<button
className={`btn btn-primary btn-sm ${downloadingAll ? 'loading' : ''}`}
onClick={downloadAllFiles}
disabled={downloadingAll}
>
{!downloadingAll && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
/>
</svg>
)}
{downloadingAll ? 'Preparing Download...' : 'Download All'}
</button>
)}
</div>
<div className="overflow-y-auto max-h-[60vh]">
{fileArray.map((file, index) => (
<div
key={`${file.name}-${index}`}
className="flex items-center justify-between p-4 hover:bg-base-200 rounded-lg cursor-pointer mb-2"
onClick={() => handleFileSelect(file)}
>
<div className="flex items-center gap-3">
<div className="text-2xl">
{file.type.startsWith('image/') ? '🖼️' :
file.type.startsWith('video/') ? '🎥' :
file.type.startsWith('audio/') ? '🎵' :
file.type === 'application/pdf' ? '📄' :
file.type.startsWith('text/') ? '📝' : '📎'}
</div>
<div>
<div className="font-semibold">{file.name}</div>
<div className="text-sm opacity-70">{file.type}</div>
</div>
</div>
<button className="btn btn-ghost btn-sm">
Preview
</button>
</div>
))}
</div>
</div>
);
};
if (!isOpen) return null;
return (
<>
<input
type="checkbox"
id={modalId}
className="modal-toggle"
checked={isOpen}
onChange={onClose}
/>
<div className="modal ">
<div className="modal-box max-w-4xl">
<div className="flex justify-between items-center mb-4">
{showPreview && selectedFile ? (
<>
<div className="flex items-center gap-3">
{fileArray.length > 1 && (
<button
className="btn btn-ghost btn-sm"
onClick={handleBackToList}
>
Back
</button>
)}
<h3 className="font-bold text-lg truncate">{selectedFile.name}</h3>
</div>
</>
) : (
<h3 className="font-bold text-lg">File Browser</h3>
)}
<div className="flex gap-2">
<button
className="btn btn-circle btn-ghost"
onClick={onClose}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div className="relative">
{loading && showPreview && (
<div className="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50">
<span className="loading loading-spinner loading-lg"></span>
</div>
)}
{error ? (
<div className="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
className="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{error}</span>
</div>
) : showPreview && selectedFile ? (
renderFileContent(selectedFile)
) : (
renderFileList()
)}
</div>
</div>
<label className="modal-backdrop" htmlFor={modalId}>
Close
</label>
</div>
</>
);
};
export default FileViewerModalWrapper;

View file

@ -3,3 +3,46 @@ officer:
- "IEEE Officer"
- "IEEE Executive"
- "IEEE Administrator"
supported_file_types:
- "image"
- "video"
- "audio"
- "pdf"
- "doc"
- "docx"
- "ppt"
- "pptx"
- "txt"
- "csv"
- "mp4"
- "mov"
- "avi"
- "mkv"
- "webm"
- "m4a"
- "mp3"
- "wav"
- "m4v"
- "m4b"
- "m4p"
- "m4v"
- "m4b"
- "png"
- "jpg"
- "jpeg"
- "gif"
- "bmp"
- "tiff"
- "ico"
- "webp"
- "heic"
- "heif"
- "hevc"
- "json"
- "md"
- "csv"
- "txt"
- "xml"
- "yaml"
- "yml"