Browse Source

refactor: split main file to pages

C4illin 1 month ago
parent
commit
9e759a75de

+ 7 - 1
.dockerignore

@@ -2,18 +2,24 @@
 .editorconfig
 .env
 .git
+.gitignore
+.github
 .idea
 .vscode
+biome.json
 CHANGELOG.md
+compose.yaml
 coverage*
 data
 docker-compose*
 Dockerfile*
 eslint.config.js
 helm-charts
+images
 LICENSE
 Makefile
 node_modules
 prettier.config.js
 README.md
-renovate.json
+renovate.json
+SECURITY.md

+ 6 - 399
bun.lock

@@ -19,19 +19,14 @@
         "@tailwindcss/postcss": "^4.1.7",
         "@total-typescript/ts-reset": "^0.6.1",
         "@types/bun": "^1.2.14",
-        "@types/eslint-plugin-tailwindcss": "^3.17.0",
         "@types/node": "^22.15.21",
-        "autoprefixer": "^10.4.21",
-        "cssnano": "^7.0.7",
         "eslint": "^9.27.0",
-        "eslint-plugin-readable-tailwind": "^2.1.2",
+        "eslint-plugin-better-tailwindcss": "^3.0.0",
         "eslint-plugin-simple-import-sort": "^12.1.1",
-        "eslint-plugin-tailwindcss": "4.0.0-alpha.0",
         "globals": "^16.1.0",
         "knip": "^5.57.2",
         "npm-run-all2": "^8.0.3",
         "postcss": "^8.5.3",
-        "postcss-cli": "^11.0.1",
         "prettier": "^3.5.3",
         "tailwind-scrollbar": "^4.0.2",
         "tailwindcss": "^4.1.7",
@@ -95,12 +90,8 @@
 
     "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
 
-    "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
-
     "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 
-    "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
-
     "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
 
     "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.4.1", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@vue/compiler-sfc": "2.7.x || 3.x", "prettier": "2 || 3" }, "optionalPeers": ["@vue/compiler-sfc"] }, "sha512-F0/Hrcfpy8WuxlQyAWJTEren/uxKhYonOGY4OyWmwRdeTvkh9mMSCxowZLjNkhwi/2ipqCgtXwwOk7tW0mWXkA=="],
@@ -225,16 +216,10 @@
 
     "@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="],
 
-    "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="],
-
     "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 
     "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
 
-    "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
-
-    "@types/eslint-plugin-tailwindcss": ["@types/eslint-plugin-tailwindcss@3.17.0", "", { "dependencies": { "@types/eslint": "*" } }, "sha512-ucQGf2YIdTcndYcxRU3UdZgmhUHsOlbIF4BaRtl0op+7k2JmqM2i3aXZ6XIcfZgVq1ZKov7VM5c/BR81ukmkyg=="],
-
     "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 
     "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -247,20 +232,18 @@
 
     "@typescript-eslint/parser": ["@typescript-eslint/parser@8.32.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg=="],
 
-    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA=="],
+    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="],
 
     "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.32.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA=="],
 
-    "@typescript-eslint/types": ["@typescript-eslint/types@7.18.0", "", {}, "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ=="],
+    "@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
 
-    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" } }, "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA=="],
+    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="],
 
-    "@typescript-eslint/utils": ["@typescript-eslint/utils@7.18.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw=="],
+    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="],
 
     "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w=="],
 
-    "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
-
     "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 
     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -271,38 +254,20 @@
 
     "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
 
-    "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
-
     "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 
-    "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
-
-    "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
-
     "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
-    "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
-
-    "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
-
     "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
 
     "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 
-    "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
-
     "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
 
     "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 
-    "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="],
-
-    "caniuse-lite": ["caniuse-lite@1.0.30001707", "", {}, "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw=="],
-
     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 
-    "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
-
     "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
 
     "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@@ -315,78 +280,36 @@
 
     "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
 
-    "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="],
-
-    "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
-
     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
 
     "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
 
     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 
-    "css-declaration-sorter": ["css-declaration-sorter@7.2.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow=="],
-
-    "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
-
-    "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
-
-    "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
-
-    "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
-
-    "cssnano": ["cssnano@7.0.7", "", { "dependencies": { "cssnano-preset-default": "^7.0.7", "lilconfig": "^3.1.3" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-evKu7yiDIF7oS+EIpwFlMF730ijRyLFaM2o5cTxRGJR9OKHKkc+qP443ZEVR9kZG0syaAJJCPJyfv5pbrxlSng=="],
-
-    "cssnano-preset-default": ["cssnano-preset-default@7.0.7", "", { "dependencies": { "browserslist": "^4.24.5", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.1", "postcss-calc": "^10.1.1", "postcss-colormin": "^7.0.3", "postcss-convert-values": "^7.0.5", "postcss-discard-comments": "^7.0.4", "postcss-discard-duplicates": "^7.0.2", "postcss-discard-empty": "^7.0.1", "postcss-discard-overridden": "^7.0.1", "postcss-merge-longhand": "^7.0.5", "postcss-merge-rules": "^7.0.5", "postcss-minify-font-values": "^7.0.1", "postcss-minify-gradients": "^7.0.1", "postcss-minify-params": "^7.0.3", "postcss-minify-selectors": "^7.0.5", "postcss-normalize-charset": "^7.0.1", "postcss-normalize-display-values": "^7.0.1", "postcss-normalize-positions": "^7.0.1", "postcss-normalize-repeat-style": "^7.0.1", "postcss-normalize-string": "^7.0.1", "postcss-normalize-timing-functions": "^7.0.1", "postcss-normalize-unicode": "^7.0.3", "postcss-normalize-url": "^7.0.1", "postcss-normalize-whitespace": "^7.0.1", "postcss-ordered-values": "^7.0.2", "postcss-reduce-initial": "^7.0.3", "postcss-reduce-transforms": "^7.0.1", "postcss-svgo": "^7.0.2", "postcss-unique-selectors": "^7.0.4" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-jW6CG/7PNB6MufOrlovs1TvBTEVmhY45yz+bd0h6nw3h6d+1e+/TX+0fflZ+LzvZombbT5f+KC063w9VoHeHow=="],
-
-    "cssnano-utils": ["cssnano-utils@5.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg=="],
-
-    "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
-
     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 
     "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
 
     "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
 
-    "dependency-graph": ["dependency-graph@1.0.0", "", {}, "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg=="],
-
     "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
 
-    "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
-
-    "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
-
-    "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
-
-    "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
-
-    "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
-
-    "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
-
-    "electron-to-chromium": ["electron-to-chromium@1.5.90", "", {}, "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug=="],
-
     "elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="],
 
     "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 
     "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
 
-    "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
-
     "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 
     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 
     "eslint": ["eslint@9.27.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.27.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q=="],
 
-    "eslint-plugin-readable-tailwind": ["eslint-plugin-readable-tailwind@2.1.2", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "postcss": "^8.5.3", "postcss-import": "^16.1.0", "synckit": "0.9.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.0.0" } }, "sha512-JTRxIxZ5CsjLZZGax/Mao+Q//0XetHohijExRpjyCrZCs2Ns3UPkyZGH52KLCb6m4k15Wrt2UpmQXs9xACIQlg=="],
+    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.0.0", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "postcss": "^8.5.3", "postcss-import": "^16.1.0", "synckit": "0.9.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.0.0" } }, "sha512-MJ1kV0OEkzpej8dkcCAbnA9yw9qpJWV3kFE1UqdKcgt7LpiGrNd1/8nIhzol4NG6qfM7T3oer/vih30KyErVtA=="],
 
     "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="],
 
-    "eslint-plugin-tailwindcss": ["eslint-plugin-tailwindcss@4.0.0-alpha.0", "", { "dependencies": { "@typescript-eslint/types": "^7.13.0", "@typescript-eslint/utils": "^7.13.0", "eslint": "^8.56.0" }, "peerDependencies": { "tailwindcss": "next" } }, "sha512-0903c/da8CP5AlHWsmHRFq/YxGdcRgSg4AzvLKXHhnPN/f7wP0/RHYaaIVcJcoEF6NUZ2gTeui7Qhi34wn01Og=="],
-
     "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
 
     "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
@@ -417,8 +340,6 @@
 
     "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="],
 
-    "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
-
     "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
 
     "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -435,26 +356,14 @@
 
     "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="],
 
-    "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
-
-    "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
-
-    "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
-
-    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
-
     "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 
     "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
 
-    "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
-
     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 
     "globals": ["globals@16.1.0", "", {}, "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g=="],
 
-    "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
-
     "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 
     "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
@@ -471,12 +380,6 @@
 
     "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
 
-    "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
-
-    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
-
-    "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
-
     "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
 
     "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -487,8 +390,6 @@
 
     "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
 
-    "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
-
     "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
 
     "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
@@ -509,8 +410,6 @@
 
     "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 
-    "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
-
     "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
 
     "knip": ["knip@5.57.2", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^9.0.2", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-6a5h4MLfn71DxfwkLaA447+4SSFtEs/LVX1241yqX/B1ARGiCQTstA1adF949JESIMwUUJP/JLCH9TSn+aRFMg=="],
@@ -539,20 +438,12 @@
 
     "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
 
-    "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
-
     "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
 
-    "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="],
-
     "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
 
-    "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
-
     "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
 
-    "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
-
     "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
 
     "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@@ -581,20 +472,10 @@
 
     "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="],
 
-    "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
-
-    "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
-
-    "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
-
     "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="],
 
     "npm-run-all2": ["npm-run-all2@8.0.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "minimatch": "^10.0.1", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-0mAycidMUMThrLt8AT3LGtOMgfLaMg6/4oUKHTKMU0jDSIsdKBsKp98H8zBFcJylQC4CtOB140UUFbOlFyE9gA=="],
 
-    "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
-
-    "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
-
     "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
 
     "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
@@ -609,14 +490,10 @@
 
     "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
 
-    "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
-
     "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
 
     "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
 
-    "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
-
     "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
 
     "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -629,78 +506,14 @@
 
     "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
 
-    "postcss-calc": ["postcss-calc@10.1.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.38" } }, "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw=="],
-
-    "postcss-cli": ["postcss-cli@11.0.1", "", { "dependencies": { "chokidar": "^3.3.0", "dependency-graph": "^1.0.0", "fs-extra": "^11.0.0", "picocolors": "^1.0.0", "postcss-load-config": "^5.0.0", "postcss-reporter": "^7.0.0", "pretty-hrtime": "^1.0.3", "read-cache": "^1.0.0", "slash": "^5.0.0", "tinyglobby": "^0.2.12", "yargs": "^17.0.0" }, "peerDependencies": { "postcss": "^8.0.0" }, "bin": { "postcss": "index.js" } }, "sha512-0UnkNPSayHKRe/tc2YGW6XnSqqOA9eqpiRMgRlV1S6HdGi16vwJBx7lviARzbV1HpQHqLLRH3o8vTcB0cLc+5g=="],
-
-    "postcss-colormin": ["postcss-colormin@7.0.3", "", { "dependencies": { "browserslist": "^4.24.5", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-xZxQcSyIVZbSsl1vjoqZAcMYYdnJsIyG8OvqShuuqf12S88qQboxxEy0ohNCOLwVPXTU+hFHvJPACRL2B5ohTA=="],
-
-    "postcss-convert-values": ["postcss-convert-values@7.0.5", "", { "dependencies": { "browserslist": "^4.24.5", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-0VFhH8nElpIs3uXKnVtotDJJNX0OGYSZmdt4XfSfvOMrFw1jKfpwpZxfC4iN73CTM/MWakDEmsHQXkISYj4BXw=="],
-
-    "postcss-discard-comments": ["postcss-discard-comments@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg=="],
-
-    "postcss-discard-duplicates": ["postcss-discard-duplicates@7.0.2", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w=="],
-
-    "postcss-discard-empty": ["postcss-discard-empty@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg=="],
-
-    "postcss-discard-overridden": ["postcss-discard-overridden@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg=="],
-
     "postcss-import": ["postcss-import@16.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg=="],
 
-    "postcss-load-config": ["postcss-load-config@5.1.0", "", { "dependencies": { "lilconfig": "^3.1.1", "yaml": "^2.4.2" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1" }, "optionalPeers": ["jiti", "postcss", "tsx"] }, "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA=="],
-
-    "postcss-merge-longhand": ["postcss-merge-longhand@7.0.5", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^7.0.5" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw=="],
-
-    "postcss-merge-rules": ["postcss-merge-rules@7.0.5", "", { "dependencies": { "browserslist": "^4.24.5", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.1", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ZonhuSwEaWA3+xYbOdJoEReKIBs5eDiBVLAGpYZpNFPzXZcEE5VKR7/qBEQvTZpiwjqhhqEQ+ax5O3VShBj9Wg=="],
-
-    "postcss-minify-font-values": ["postcss-minify-font-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ=="],
-
-    "postcss-minify-gradients": ["postcss-minify-gradients@7.0.1", "", { "dependencies": { "colord": "^2.9.3", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A=="],
-
-    "postcss-minify-params": ["postcss-minify-params@7.0.3", "", { "dependencies": { "browserslist": "^4.24.5", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-vUKV2+f5mtjewYieanLX0xemxIp1t0W0H/D11u+kQV/MWdygOO7xPMkbK+r9P6Lhms8MgzKARF/g5OPXhb8tgg=="],
-
-    "postcss-minify-selectors": ["postcss-minify-selectors@7.0.5", "", { "dependencies": { "cssesc": "^3.0.0", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug=="],
-
-    "postcss-normalize-charset": ["postcss-normalize-charset@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ=="],
-
-    "postcss-normalize-display-values": ["postcss-normalize-display-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ=="],
-
-    "postcss-normalize-positions": ["postcss-normalize-positions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ=="],
-
-    "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ=="],
-
-    "postcss-normalize-string": ["postcss-normalize-string@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ=="],
-
-    "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg=="],
-
-    "postcss-normalize-unicode": ["postcss-normalize-unicode@7.0.3", "", { "dependencies": { "browserslist": "^4.24.5", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-EcoA29LvG3F+EpOh03iqu+tJY3uYYKzArqKJHxDhUYLa2u58aqGq16K6/AOsXD9yqLN8O6y9mmePKN5cx6krOw=="],
-
-    "postcss-normalize-url": ["postcss-normalize-url@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ=="],
-
-    "postcss-normalize-whitespace": ["postcss-normalize-whitespace@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA=="],
-
-    "postcss-ordered-values": ["postcss-ordered-values@7.0.2", "", { "dependencies": { "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw=="],
-
-    "postcss-reduce-initial": ["postcss-reduce-initial@7.0.3", "", { "dependencies": { "browserslist": "^4.24.5", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-RFvkZaqiWtGMlVjlUHpaxGqEL27lgt+Q2Ixjf83CRAzqdo+TsDyGPtJUbPx2MuYIJ+sCQc2TrOvRnhcXQfgIVA=="],
-
-    "postcss-reduce-transforms": ["postcss-reduce-transforms@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g=="],
-
-    "postcss-reporter": ["postcss-reporter@7.1.0", "", { "dependencies": { "picocolors": "^1.0.0", "thenby": "^1.3.4" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA=="],
-
-    "postcss-selector-parser": ["postcss-selector-parser@7.0.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ=="],
-
-    "postcss-svgo": ["postcss-svgo@7.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^3.3.2" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-5Dzy66JlnRM6pkdOTF8+cGsB1fnERTE8Nc+Eed++fOWo1hdsBptCsbG8UuJkgtZt75bRtMJIrPeZmtfANixdFA=="],
-
-    "postcss-unique-selectors": ["postcss-unique-selectors@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ=="],
-
     "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
 
     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 
     "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
 
-    "pretty-hrtime": ["pretty-hrtime@1.0.3", "", {}, "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A=="],
-
     "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
 
     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -713,8 +526,6 @@
 
     "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
 
-    "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
-
     "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
 
     "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
@@ -723,8 +534,6 @@
 
     "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
 
-    "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
-
     "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
 
     "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="],
@@ -737,8 +546,6 @@
 
     "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
 
-    "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
-
     "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="],
 
     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -751,14 +558,10 @@
 
     "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
 
-    "stylehacks": ["stylehacks@7.0.5", "", { "dependencies": { "browserslist": "^4.24.5", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-5kNb7V37BNf0Q3w+1pxfa+oiNPS++/b4Jil9e/kPDgrk1zjEd6uR7SZeJiYaLYH6RRSC1XX2/37OTeU/4FvuIA=="],
-
     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 
     "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
 
-    "svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="],
-
     "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="],
 
     "tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="],
@@ -769,12 +572,6 @@
 
     "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
 
-    "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
-
-    "thenby": ["thenby@1.3.4", "", {}, "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ=="],
-
-    "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
-
     "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
 
     "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
@@ -787,8 +584,6 @@
 
     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
 
-    "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
-
     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
 
     "typescript-eslint": ["typescript-eslint@8.32.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/parser": "8.32.1", "@typescript-eslint/utils": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg=="],
@@ -797,16 +592,10 @@
 
     "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
 
-    "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
-    "update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="],
-
     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 
     "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
 
-    "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
-
     "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="],
 
     "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
@@ -815,14 +604,10 @@
 
     "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
 
-    "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
-
     "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
 
     "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
 
-    "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
-
     "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
 
     "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
@@ -857,206 +642,28 @@
 
     "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="],
-
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="],
-
     "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
 
-    "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="],
-
-    "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="],
-
-    "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" } }, "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="],
-
-    "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" } }, "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg=="],
-
     "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
 
-    "@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
-
-    "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
-
-    "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
-    "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001696", "", {}, "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ=="],
-
-    "caniuse-api/caniuse-lite": ["caniuse-lite@1.0.30001696", "", {}, "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ=="],
-
     "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 
-    "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
-
     "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 
-    "cssnano-preset-default/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
-
-    "eslint-plugin-tailwindcss/eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
-
     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
-    "globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
-
     "lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 
     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 
     "npm-run-all2/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
 
-    "postcss-colormin/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "postcss-convert-values/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "postcss-discard-comments/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
-
-    "postcss-merge-rules/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "postcss-merge-rules/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
-
-    "postcss-minify-params/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "postcss-minify-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
-
-    "postcss-normalize-unicode/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "postcss-reduce-initial/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "postcss-unique-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
-
-    "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
-    "stylehacks/browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
-
-    "stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
-
-    "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="],
-
     "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="],
-
-    "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
-    "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
-
     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 
-    "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
-
     "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 
-    "cssnano-preset-default/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "cssnano-preset-default/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "cssnano-preset-default/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
-
-    "eslint-plugin-tailwindcss/eslint/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
-
-    "eslint-plugin-tailwindcss/eslint/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
-
-    "eslint-plugin-tailwindcss/eslint/@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
-
-    "eslint-plugin-tailwindcss/eslint/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
-
-    "eslint-plugin-tailwindcss/eslint/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
-
-    "eslint-plugin-tailwindcss/eslint/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
-
-    "eslint-plugin-tailwindcss/eslint/file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
-
-    "eslint-plugin-tailwindcss/eslint/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
-
     "npm-run-all2/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
-
-    "postcss-colormin/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "postcss-colormin/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "postcss-colormin/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "postcss-convert-values/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "postcss-convert-values/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "postcss-convert-values/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "postcss-merge-rules/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "postcss-merge-rules/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "postcss-merge-rules/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "postcss-minify-params/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "postcss-minify-params/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "postcss-minify-params/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "postcss-normalize-unicode/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "postcss-normalize-unicode/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "postcss-normalize-unicode/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "postcss-reduce-initial/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "postcss-reduce-initial/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "postcss-reduce-initial/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "stylehacks/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="],
-
-    "stylehacks/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
-
-    "stylehacks/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
-
-    "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="],
-
-    "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
-
-    "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="],
-
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
-    "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
-
-    "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
-
-    "eslint-plugin-tailwindcss/eslint/@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
-
-    "eslint-plugin-tailwindcss/eslint/file-entry-cache/flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
-
-    "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
-    "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
-
-    "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
   }
 }

+ 2 - 2
compose.yaml

@@ -8,8 +8,8 @@ services:
     environment: # Defaults are listed below. All are optional.
       - ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
       - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
-      - HTTP_ALLOWED=true # setting this to true is unsafe, only set this to true locally
-      - ALLOW_UNAUTHENTICATED=true # allows anyone to use the service without logging in, only set this to true locally
+      - HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
+      - ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
       - AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
       # - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
       # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable

+ 25 - 16
eslint.config.ts

@@ -1,7 +1,7 @@
 import js from "@eslint/js";
 import eslintParserTypeScript from "@typescript-eslint/parser";
 import type { Linter } from "eslint";
-import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind";
+import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
 import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
 import globals from "globals";
 import tseslint from "typescript-eslint";
@@ -13,7 +13,7 @@ export default [
   {
     plugins: {
       "simple-import-sort": simpleImportSortPlugin,
-      "readable-tailwind": eslintPluginReadableTailwind,
+      "better-tailwindcss": eslintPluginBetterTailwindcss,
     },
     ignores: ["**/node_modules/**"],
     languageOptions: {
@@ -30,28 +30,37 @@ export default [
       },
     },
     files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"],
+    settings: {
+      "better-tailwindcss": {
+        entryPoint: "src/main.css",
+      },
+    },
     rules: {
-      ...eslintPluginReadableTailwind.configs.warning.rules,
+      ...eslintPluginBetterTailwindcss.configs["recommended-warn"]?.rules,
+      ...eslintPluginBetterTailwindcss.configs["stylistic-warn"]?.rules,
       // "tailwindcss/classnames-order": "off",
-      "readable-tailwind/multiline": [
+      "better-tailwindcss/multiline": [
         "warn",
         {
           group: "newLine",
           printWidth: 100,
         },
       ],
-      // "tailwindcss/no-custom-classname": [
-      //   "warn",
-      //   {
-      //     whitelist: [
-      //       "select_container",
-      //       "convert_to_popup",
-      //       "convert_to_group",
-      //       "target",
-      //       "convert_to_target",
-      //     ],
-      //   },
-      // ],
+      "better-tailwindcss/no-unregistered-classes": [
+        "warn",
+        {
+          ignore: [
+            "^group(?:\\/(\\S*))?$",
+            "^peer(?:\\/(\\S*))?$",
+            "select_container",
+            "convert_to_popup",
+            "convert_to_group",
+            "target",
+            "convert_to_target",
+            "job-details-toggle",
+          ],
+        },
+      ],
     },
   },
 ] as Linter.Config[];

+ 2 - 7
package.json

@@ -32,23 +32,18 @@
     "@tailwindcss/postcss": "^4.1.7",
     "@total-typescript/ts-reset": "^0.6.1",
     "@types/bun": "^1.2.14",
-    "@types/eslint-plugin-tailwindcss": "^3.17.0",
     "@types/node": "^22.15.21",
-    "autoprefixer": "^10.4.21",
-    "cssnano": "^7.0.7",
     "eslint": "^9.27.0",
-    "eslint-plugin-readable-tailwind": "^2.1.2",
+    "eslint-plugin-better-tailwindcss": "^3.0.0",
     "eslint-plugin-simple-import-sort": "^12.1.1",
-    "eslint-plugin-tailwindcss": "4.0.0-alpha.0",
     "globals": "^16.1.0",
     "knip": "^5.57.2",
     "npm-run-all2": "^8.0.3",
     "postcss": "^8.5.3",
-    "postcss-cli": "^11.0.1",
     "prettier": "^3.5.3",
     "tailwind-scrollbar": "^4.0.2",
     "tailwindcss": "^4.1.7",
     "typescript": "^5.8.3",
     "typescript-eslint": "^8.32.1"
   }
-}
+}

+ 1 - 1
src/components/base.tsx

@@ -44,8 +44,8 @@ export const BaseHtml = ({
           <a
             href="https://github.com/C4illin/ConvertX"
             class={`
-              hover:text-accent-500
               text-neutral-400
+              hover:text-accent-500
             `}
           >
             ConvertX{" "}

+ 44 - 0
src/db/db.ts

@@ -0,0 +1,44 @@
+import { Database } from "bun:sqlite";
+const db = new Database("./data/mydb.sqlite", { create: true });
+
+if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
+  db.exec(`
+CREATE TABLE IF NOT EXISTS users (
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	email TEXT NOT NULL,
+	password TEXT NOT NULL
+);
+CREATE TABLE IF NOT EXISTS file_names (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  job_id INTEGER NOT NULL,
+  file_name TEXT NOT NULL,
+  output_file_name TEXT NOT NULL,
+  status TEXT DEFAULT 'not started',
+  FOREIGN KEY (job_id) REFERENCES jobs(id)
+);
+CREATE TABLE IF NOT EXISTS jobs (
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	user_id INTEGER NOT NULL,
+	date_created TEXT NOT NULL,
+  status TEXT DEFAULT 'not started',
+  num_files INTEGER DEFAULT 0,
+  FOREIGN KEY (user_id) REFERENCES users(id)
+);
+PRAGMA user_version = 1;`);
+}
+
+const dbVersion = (
+  db.query("PRAGMA user_version").get() as { user_version?: number }
+).user_version;
+if (dbVersion === 0) {
+  db.exec(
+    "ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';",
+  );
+  db.exec("PRAGMA user_version = 1;");
+  console.log("Updated database to version 1.");
+}
+
+// enable WAL mode
+db.exec("PRAGMA journal_mode = WAL;");
+
+export default db;

+ 17 - 0
src/helpers/env.ts

@@ -0,0 +1,17 @@
+export const ACCOUNT_REGISTRATION =
+  process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
+
+export const HTTP_ALLOWED =
+  process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
+
+export const ALLOW_UNAUTHENTICATED =
+  process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
+
+export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
+  ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
+  : 24;
+
+export const HIDE_HISTORY =
+  process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
+
+export const WEBROOT = process.env.WEBROOT ?? "";

+ 30 - 1745
src/index.tsx

@@ -1,98 +1,32 @@
-import { randomInt, randomUUID } from "node:crypto";
 import { rmSync } from "node:fs";
-import { mkdir, unlink } from "node:fs/promises";
-import { html, Html } from "@elysiajs/html";
-import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
+import { mkdir } from "node:fs/promises";
+import { html } from "@elysiajs/html";
 import { staticPlugin } from "@elysiajs/static";
-import { Database } from "bun:sqlite";
-import { Elysia, t } from "elysia";
-import sanitize from "sanitize-filename";
-import { BaseHtml } from "./components/base";
-import { Header } from "./components/header";
-import {
-  getAllInputs,
-  getAllTargets,
-  getPossibleTargets,
-  mainConverter,
-} from "./converters/main";
-import {
-  normalizeFiletype,
-  normalizeOutputFiletype,
-} from "./helpers/normalizeFiletype";
+
+import { Elysia } from "elysia";
 import "./helpers/printVersions";
+import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env";
+import { user } from "./pages/user";
+import { root } from "./pages/root"
+import { upload } from "./pages/upload"
+import { history } from "./pages/history";
+import { convert } from "./pages/convert"
+import { download } from "./pages/download"
+import { results } from "./pages/results";
+import { deleteFile } from "./pages/deleteFile";
+import { listConverters } from "./pages/listConverters";
+import { chooseConverter } from "./pages/chooseConverter";
+import db from "./db/db";
 
 mkdir("./data", { recursive: true }).catch(console.error);
-const db = new Database("./data/mydb.sqlite", { create: true });
-const uploadsDir = "./data/uploads/";
-const outputDir = "./data/output/";
-
-const ACCOUNT_REGISTRATION =
-  process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
-
-const HTTP_ALLOWED =
-  process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
-const ALLOW_UNAUTHENTICATED =
-  process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
-const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
-  ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
-  : 24;
-const HIDE_HISTORY =
-  process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
 
-const WEBROOT = process.env.WEBROOT ?? "";
-
-// fileNames: fileNames,
-// filesToConvert: fileNames.length,
-// convertedFiles : 0,
-// outputFiles: [],
+export const uploadsDir = "./data/uploads/";
+export const outputDir = "./data/output/";
 
 // init db if not exists
-if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
-  db.exec(`
-CREATE TABLE IF NOT EXISTS users (
-	id INTEGER PRIMARY KEY AUTOINCREMENT,
-	email TEXT NOT NULL,
-	password TEXT NOT NULL
-);
-CREATE TABLE IF NOT EXISTS file_names (
-  id INTEGER PRIMARY KEY AUTOINCREMENT,
-  job_id INTEGER NOT NULL,
-  file_name TEXT NOT NULL,
-  output_file_name TEXT NOT NULL,
-  status TEXT DEFAULT 'not started',
-  FOREIGN KEY (job_id) REFERENCES jobs(id)
-);
-CREATE TABLE IF NOT EXISTS jobs (
-	id INTEGER PRIMARY KEY AUTOINCREMENT,
-	user_id INTEGER NOT NULL,
-	date_created TEXT NOT NULL,
-  status TEXT DEFAULT 'not started',
-  num_files INTEGER DEFAULT 0,
-  FOREIGN KEY (user_id) REFERENCES users(id)
-);
-PRAGMA user_version = 1;`);
-}
-
-const dbVersion = (
-  db.query("PRAGMA user_version").get() as { user_version?: number }
-).user_version;
-if (dbVersion === 0) {
-  db.exec(
-    "ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';",
-  );
-  db.exec("PRAGMA user_version = 1;");
-  console.log("Updated database to version 1.");
-}
 
-let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
 
-class User {
-  id!: number;
-  email!: string;
-  password!: string;
-}
-
-class Filename {
+export class Filename {
   id!: number;
   job_id!: number;
   file_name!: string;
@@ -100,7 +34,7 @@ class Filename {
   status!: string;
 }
 
-class Jobs {
+export class Jobs {
   finished_files!: number;
   id!: number;
   user_id!: number;
@@ -110,9 +44,6 @@ class Jobs {
   files_detailed!: Filename[];
 }
 
-// enable WAL mode
-db.exec("PRAGMA journal_mode = WAL;");
-
 const app = new Elysia({
   serve: {
     maxRequestBodySize: Number.MAX_SAFE_INTEGER,
@@ -120,1669 +51,23 @@ const app = new Elysia({
   prefix: WEBROOT,
 })
   .use(html())
-  .use(
-    jwt({
-      name: "jwt",
-      schema: t.Object({
-        id: t.String(),
-      }),
-      secret: process.env.JWT_SECRET ?? randomUUID(),
-      exp: "7d",
-    }),
-  )
   .use(
     staticPlugin({
       assets: "public",
       prefix: "",
     }),
   )
-  .get("/setup", ({ redirect }) => {
-    if (!FIRST_RUN) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-
-    return (
-      <BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
-        <main
-          class={`
-            mx-auto w-full max-w-4xl flex-1 px-2
-            sm:px-4
-          `}
-        >
-          <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
-          <article class="article p-0">
-            <header class="w-full bg-neutral-800 p-4">
-              Create your account
-            </header>
-            <form method="post" action={`${WEBROOT}/register`} class="p-4">
-              <fieldset class="mb-4 flex flex-col gap-4">
-                <label class="flex flex-col gap-1">
-                  Email
-                  <input
-                    type="email"
-                    name="email"
-                    class="rounded-sm bg-neutral-800 p-3"
-                    placeholder="Email"
-                    autocomplete="email"
-                    required
-                  />
-                </label>
-                <label class="flex flex-col gap-1">
-                  Password
-                  <input
-                    type="password"
-                    name="password"
-                    class="rounded-sm bg-neutral-800 p-3"
-                    placeholder="Password"
-                    autocomplete="current-password"
-                    required
-                  />
-                </label>
-              </fieldset>
-              <input type="submit" value="Create account" class="btn-primary" />
-            </form>
-            <footer class="p-4">
-              Report any issues on{" "}
-              <a
-                class={`
-                  text-accent-500 underline
-                  hover:text-accent-400
-                `}
-                href="https://github.com/C4illin/ConvertX"
-              >
-                GitHub
-              </a>
-              .
-            </footer>
-          </article>
-        </main>
-      </BaseHtml>
-    );
-  })
-  .get("/register", ({ redirect }) => {
-    if (!ACCOUNT_REGISTRATION) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-
-    return (
-      <BaseHtml webroot={WEBROOT} title="ConvertX | Register">
-        <>
-          <Header
-            webroot={WEBROOT}
-            accountRegistration={ACCOUNT_REGISTRATION}
-            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-            hideHistory={HIDE_HISTORY}
-          />
-          <main
-            class={`
-              w-full flex-1 px-2
-              sm:px-4
-            `}
-          >
-            <article class="article">
-              <form method="post" class="flex flex-col gap-4">
-                <fieldset class="mb-4 flex flex-col gap-4">
-                  <label class="flex flex-col gap-1">
-                    Email
-                    <input
-                      type="email"
-                      name="email"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Email"
-                      autocomplete="email"
-                      required
-                    />
-                  </label>
-                  <label class="flex flex-col gap-1">
-                    Password
-                    <input
-                      type="password"
-                      name="password"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Password"
-                      autocomplete="current-password"
-                      required
-                    />
-                  </label>
-                </fieldset>
-                <input
-                  type="submit"
-                  value="Register"
-                  class="btn-primary w-full"
-                />
-              </form>
-            </article>
-          </main>
-        </>
-      </BaseHtml>
-    );
-  })
-  .post(
-    "/register",
-    async ({ body, set, redirect, jwt, cookie: { auth } }) => {
-      if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      if (FIRST_RUN) {
-        FIRST_RUN = false;
-      }
-
-      const existingUser = await db
-        .query("SELECT * FROM users WHERE email = ?")
-        .get(body.email);
-      if (existingUser) {
-        set.status = 400;
-        return {
-          message: "Email already in use.",
-        };
-      }
-      const savedPassword = await Bun.password.hash(body.password);
-
-      db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(
-        body.email,
-        savedPassword,
-      );
-
-      const user = db
-        .query("SELECT * FROM users WHERE email = ?")
-        .as(User)
-        .get(body.email);
-
-      if (!user) {
-        set.status = 500;
-        return {
-          message: "Failed to create user.",
-        };
-      }
-
-      const accessToken = await jwt.sign({
-        id: String(user.id),
-      });
-
-      if (!auth) {
-        set.status = 500;
-        return {
-          message: "No auth cookie, perhaps your browser is blocking cookies.",
-        };
-      }
-
-      // set cookie
-      auth.set({
-        value: accessToken,
-        httpOnly: true,
-        secure: !HTTP_ALLOWED,
-        maxAge: 60 * 60 * 24 * 7,
-        sameSite: "strict",
-      });
-
-      return redirect(`${WEBROOT}/`, 302);
-    },
-    { body: t.Object({ email: t.String(), password: t.String() }) },
-  )
-  .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
-    if (FIRST_RUN) {
-      return redirect(`${WEBROOT}/setup`, 302);
-    }
-
-    // if already logged in, redirect to home
-    if (auth?.value) {
-      const user = await jwt.verify(auth.value);
-
-      if (user) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      auth.remove();
-    }
-
-    return (
-      <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
-        <>
-          <Header
-            webroot={WEBROOT}
-            accountRegistration={ACCOUNT_REGISTRATION}
-            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-            hideHistory={HIDE_HISTORY}
-          />
-          <main
-            class={`
-              w-full flex-1 px-2
-              sm:px-4
-            `}
-          >
-            <article class="article">
-              <form method="post" class="flex flex-col gap-4">
-                <fieldset class="mb-4 flex flex-col gap-4">
-                  <label class="flex flex-col gap-1">
-                    Email
-                    <input
-                      type="email"
-                      name="email"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Email"
-                      autocomplete="email"
-                      required
-                    />
-                  </label>
-                  <label class="flex flex-col gap-1">
-                    Password
-                    <input
-                      type="password"
-                      name="password"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Password"
-                      autocomplete="current-password"
-                      required
-                    />
-                  </label>
-                </fieldset>
-                <div class="flex flex-row gap-4">
-                  {ACCOUNT_REGISTRATION ? (
-                    <a
-                      href={`${WEBROOT}/register`}
-                      role="button"
-                      class="btn-secondary w-full text-center"
-                    >
-                      Register
-                    </a>
-                  ) : null}
-                  <input
-                    type="submit"
-                    value="Login"
-                    class="btn-primary w-full"
-                  />
-                </div>
-              </form>
-            </article>
-          </main>
-        </>
-      </BaseHtml>
-    );
-  })
-  .post(
-    "/login",
-    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
-      const existingUser = db
-        .query("SELECT * FROM users WHERE email = ?")
-        .as(User)
-        .get(body.email);
-
-      if (!existingUser) {
-        set.status = 403;
-        return {
-          message: "Invalid credentials.",
-        };
-      }
-
-      const validPassword = await Bun.password.verify(
-        body.password,
-        existingUser.password,
-      );
-
-      if (!validPassword) {
-        set.status = 403;
-        return {
-          message: "Invalid credentials.",
-        };
-      }
-
-      const accessToken = await jwt.sign({
-        id: String(existingUser.id),
-      });
-
-      if (!auth) {
-        set.status = 500;
-        return {
-          message: "No auth cookie, perhaps your browser is blocking cookies.",
-        };
-      }
-
-      // set cookie
-      auth.set({
-        value: accessToken,
-        httpOnly: true,
-        secure: !HTTP_ALLOWED,
-        maxAge: 60 * 60 * 24 * 7,
-        sameSite: "strict",
-      });
-
-      return redirect(`${WEBROOT}/`, 302);
-    },
-    { body: t.Object({ email: t.String(), password: t.String() }) },
-  )
-  .get("/logoff", ({ redirect, cookie: { auth } }) => {
-    if (auth?.value) {
-      auth.remove();
-    }
-
-    return redirect(`${WEBROOT}/login`, 302);
-  })
-  .post("/logoff", ({ redirect, cookie: { auth } }) => {
-    if (auth?.value) {
-      auth.remove();
-    }
-
-    return redirect(`${WEBROOT}/login`, 302);
-  })
-  .get("/account", async ({ jwt, redirect, cookie: { auth } }) => {
-    if (!auth?.value) {
-      return redirect(`${WEBROOT}/`);
-    }
-    const user = await jwt.verify(auth.value);
-
-    if (!user) {
-      return redirect(`${WEBROOT}/`, 302);
-    }
-
-    const userData = db
-      .query("SELECT * FROM users WHERE id = ?")
-      .as(User)
-      .get(user.id);
-
-    if (!userData) {
-      return redirect(`${WEBROOT}/`, 302);
-    }
-
-    return (
-      <BaseHtml webroot={WEBROOT} title="ConvertX | Account">
-        <>
-          <Header
-            webroot={WEBROOT}
-            accountRegistration={ACCOUNT_REGISTRATION}
-            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-            hideHistory={HIDE_HISTORY}
-            loggedIn
-          />
-          <main
-            class={`
-              w-full flex-1 px-2
-              sm:px-4
-            `}
-          >
-            <article class="article">
-              <form method="post" class="flex flex-col gap-4">
-                <fieldset class="mb-4 flex flex-col gap-4">
-                  <label class="flex flex-col gap-1">
-                    Email
-                    <input
-                      type="email"
-                      name="email"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Email"
-                      autocomplete="email"
-                      value={userData.email}
-                      required
-                    />
-                  </label>
-                  <label class="flex flex-col gap-1">
-                    Password (leave blank for unchanged)
-                    <input
-                      type="password"
-                      name="newPassword"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Password"
-                      autocomplete="new-password"
-                    />
-                  </label>
-                  <label class="flex flex-col gap-1">
-                    Current Password
-                    <input
-                      type="password"
-                      name="password"
-                      class="rounded-sm bg-neutral-800 p-3"
-                      placeholder="Password"
-                      autocomplete="current-password"
-                      required
-                    />
-                  </label>
-                </fieldset>
-                <div role="group">
-                  <input
-                    type="submit"
-                    value="Update"
-                    class="btn-primary w-full"
-                  />
-                </div>
-              </form>
-            </article>
-          </main>
-        </>
-      </BaseHtml>
-    );
-  })
-  .post(
-    "/account",
-    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-      const existingUser = db
-        .query("SELECT * FROM users WHERE id = ?")
-        .as(User)
-        .get(user.id);
-
-      if (!existingUser) {
-        if (auth?.value) {
-          auth.remove();
-        }
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const validPassword = await Bun.password.verify(
-        body.password,
-        existingUser.password,
-      );
-
-      if (!validPassword) {
-        set.status = 403;
-        return {
-          message: "Invalid credentials.",
-        };
-      }
-
-      const fields = [];
-      const values = [];
-
-      if (body.email) {
-        const existingUser = await db
-          .query("SELECT id FROM users WHERE email = ?")
-          .as(User)
-          .get(body.email);
-        if (existingUser && existingUser.id.toString() !== user.id) {
-          set.status = 409;
-          return { message: "Email already in use." };
-        }
-        fields.push("email");
-        values.push(body.email);
-      }
-      if (body.newPassword) {
-        fields.push("password");
-        values.push(await Bun.password.hash(body.newPassword));
-      }
-
-      if (fields.length > 0) {
-        db.query(
-          `UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`,
-        ).run(...values, user.id);
-      }
-
-      return redirect(`${WEBROOT}/`, 302);
-    },
-    {
-      body: t.Object({
-        email: t.MaybeEmpty(t.String()),
-        newPassword: t.MaybeEmpty(t.String()),
-        password: t.String(),
-      }),
-    },
-  )
-
-  .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
-    if (!ALLOW_UNAUTHENTICATED) {
-      if (FIRST_RUN) {
-        return redirect(`${WEBROOT}/setup`, 302);
-      }
-
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-    }
-
-    // validate jwt
-    let user: ({ id: string } & JWTPayloadSpec) | false = false;
-    if (ALLOW_UNAUTHENTICATED) {
-      const newUserId = String(
-        randomInt(
-          2 ** 24,
-          Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER),
-        ),
-      );
-      const accessToken = await jwt.sign({
-        id: newUserId,
-      });
-
-      user = { id: newUserId };
-      if (!auth) {
-        return {
-          message: "No auth cookie, perhaps your browser is blocking cookies.",
-        };
-      }
-
-      // set cookie
-      auth.set({
-        value: accessToken,
-        httpOnly: true,
-        secure: !HTTP_ALLOWED,
-        maxAge: 24 * 60 * 60,
-        sameSite: "strict",
-      });
-    } else if (auth?.value) {
-      user = await jwt.verify(auth.value);
-
-      if (user !== false && user.id) {
-        if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
-          // make sure user exists in db
-          const existingUser = db
-            .query("SELECT * FROM users WHERE id = ?")
-            .as(User)
-            .get(user.id);
-
-          if (!existingUser) {
-            if (auth?.value) {
-              auth.remove();
-            }
-            return redirect(`${WEBROOT}/login`, 302);
-          }
-        }
-      }
-    }
-
-    if (!user) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-
-    // create a new job
-    db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
-      user.id,
-      new Date().toISOString(),
-    );
-
-    const id = (
-      db
-        .query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
-        .get(user.id) as { id: number }
-    ).id;
-
-    if (!jobId) {
-      return { message: "Cookies should be enabled to use this app." };
-    }
-
-    jobId.set({
-      value: id,
-      httpOnly: true,
-      secure: !HTTP_ALLOWED,
-      maxAge: 24 * 60 * 60,
-      sameSite: "strict",
-    });
-
-    console.log("jobId set to:", id);
-
-    return (
-      <BaseHtml webroot={WEBROOT}>
-        <>
-          <Header
-            webroot={WEBROOT}
-            accountRegistration={ACCOUNT_REGISTRATION}
-            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-            hideHistory={HIDE_HISTORY}
-            loggedIn
-          />
-          <main
-            class={`
-              w-full flex-1 px-2
-              sm:px-4
-            `}
-          >
-            <article class="article">
-              <h1 class="mb-4 text-xl">Convert</h1>
-              <div class="scrollbar-thin mb-4 max-h-[50vh] overflow-y-auto">
-                <table
-                  id="file-list"
-                  class={`
-                    w-full table-auto rounded bg-neutral-900
-                    [&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
-                    [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
-                  `}
-                />
-              </div>
-              <div
-                id="dropzone"
-                class={`
-                  relative flex h-48 w-full items-center justify-center rounded border border-dashed
-                  border-neutral-700 transition-all
-                  hover:border-neutral-600
-                  [&.dragover]:border-4 [&.dragover]:border-neutral-500
-                `}
-              >
-                <span>
-                  <b>Choose a file</b> or drag it here
-                </span>
-                <input
-                  type="file"
-                  name="file"
-                  multiple
-                  class="absolute inset-0 size-full cursor-pointer opacity-0"
-                />
-              </div>
-            </article>
-            <form
-              method="post"
-              action={`${WEBROOT}/convert`}
-              class="relative mx-auto mb-[35vh] w-full max-w-4xl"
-            >
-              <input type="hidden" name="file_names" id="file_names" />
-              <article class="article w-full">
-                <input
-                  type="search"
-                  name="convert_to_search"
-                  placeholder="Search for conversions"
-                  autocomplete="off"
-                  class="w-full rounded-sm bg-neutral-800 p-4"
-                />
-                <div class="select_container relative">
-                  <article
-                    class={`
-                      convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
-                      overflow-x-hidden overflow-y-auto rounded bg-neutral-800
-                      sm:h-[30vh]
-                    `}
-                  >
-                    {Object.entries(getAllTargets()).map(
-                      ([converter, targets]) => (
-                        <article
-                          class={`
-                            convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
-                          `}
-                          data-converter={converter}
-                        >
-                          <header class="mb-2 w-full text-xl font-bold" safe>
-                            {converter}
-                          </header>
-                          <ul class="convert_to_target flex flex-row flex-wrap gap-1">
-                            {targets.map((target) => (
-                              <button
-                                // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
-                                tabindex={0}
-                                class={`
-                                  target rounded bg-neutral-700 p-1 text-base
-                                  hover:bg-neutral-600
-                                `}
-                                data-value={`${target},${converter}`}
-                                data-target={target}
-                                data-converter={converter}
-                                type="button"
-                                safe
-                              >
-                                {target}
-                              </button>
-                            ))}
-                          </ul>
-                        </article>
-                      ),
-                    )}
-                  </article>
-
-                  {/* Hidden element which determines the format to convert the file too and the converter to use */}
-                  <select
-                    name="convert_to"
-                    aria-label="Convert to"
-                    required
-                    hidden
-                  >
-                    <option selected disabled value="">
-                      Convert to
-                    </option>
-                    {Object.entries(getAllTargets()).map(
-                      ([converter, targets]) => (
-                        <optgroup label={converter}>
-                          {targets.map((target) => (
-                            <option value={`${target},${converter}`} safe>
-                              {target}
-                            </option>
-                          ))}
-                        </optgroup>
-                      ),
-                    )}
-                  </select>
-                </div>
-              </article>
-              <input
-                class={`
-                  btn-primary w-full opacity-100
-                  disabled:cursor-not-allowed disabled:opacity-50
-                `}
-                type="submit"
-                value="Convert"
-                disabled
-              />
-            </form>
-          </main>
-          <script src="script.js" defer />
-        </>
-      </BaseHtml>
-    );
-  })
-  .post(
-    "/conversions",
-    ({ body }) => {
-      return (
-        <>
-          <article
-            class={`
-              convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
-              overflow-x-hidden overflow-y-auto rounded bg-neutral-800
-              sm:h-[30vh]
-            `}
-          >
-            {Object.entries(getPossibleTargets(body.fileType)).map(
-              ([converter, targets]) => (
-                <article
-                  class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
-                  data-converter={converter}
-                >
-                  <header class="mb-2 w-full text-xl font-bold" safe>
-                    {converter}
-                  </header>
-                  <ul class="convert_to_target flex flex-row flex-wrap gap-1">
-                    {targets.map((target) => (
-                      <button
-                        // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
-                        tabindex={0}
-                        class={`
-                          target rounded bg-neutral-700 p-1 text-base
-                          hover:bg-neutral-600
-                        `}
-                        data-value={`${target},${converter}`}
-                        data-target={target}
-                        data-converter={converter}
-                        type="button"
-                        safe
-                      >
-                        {target}
-                      </button>
-                    ))}
-                  </ul>
-                </article>
-              ),
-            )}
-          </article>
-
-          <select name="convert_to" aria-label="Convert to" required hidden>
-            <option selected disabled value="">
-              Convert to
-            </option>
-            {Object.entries(getPossibleTargets(body.fileType)).map(
-              ([converter, targets]) => (
-                <optgroup label={converter}>
-                  {targets.map((target) => (
-                    <option value={`${target},${converter}`} safe>
-                      {target}
-                    </option>
-                  ))}
-                </optgroup>
-              ),
-            )}
-          </select>
-        </>
-      );
-    },
-    { body: t.Object({ fileType: t.String() }) },
-  )
-  .post(
-    "/upload",
-    async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      if (!jobId?.value) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      const existingJob = await db
-        .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
-        .get(jobId.value, user.id);
-
-      if (!existingJob) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
-
-      if (body?.file) {
-        if (Array.isArray(body.file)) {
-          for (const file of body.file) {
-            await Bun.write(`${userUploadsDir}${file.name}`, file);
-          }
-        } else {
-          await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
-        }
-      }
-
-      return {
-        message: "Files uploaded successfully.",
-      };
-    },
-    { body: t.Object({ file: t.Files() }) },
-  )
-  .post(
-    "/delete",
-    async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      if (!jobId?.value) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      const existingJob = await db
-        .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
-        .get(jobId.value, user.id);
-
-      if (!existingJob) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
-
-      await unlink(`${userUploadsDir}${body.filename}`);
-    },
-    { body: t.Object({ filename: t.String() }) },
-  )
-  .post(
-    "/convert",
-    async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      if (!jobId?.value) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      const existingJob = db
-        .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
-        .as(Jobs)
-        .get(jobId.value, user.id);
-
-      if (!existingJob) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
-      const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
-
-      // create the output directory
-      try {
-        await mkdir(userOutputDir, { recursive: true });
-      } catch (error) {
-        console.error(
-          `Failed to create the output directory: ${userOutputDir}.`,
-          error,
-        );
-      }
-
-      const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
-      const converterName = body.convert_to.split(",")[1];
-      const fileNames = JSON.parse(body.file_names) as string[];
-
-      for (let i = 0; i < fileNames.length; i++) {
-        fileNames[i] = sanitize(fileNames[i] || "");
-      }
-
-      if (!Array.isArray(fileNames) || fileNames.length === 0) {
-        return redirect(`${WEBROOT}/`, 302);
-      }
-
-      db.query(
-        "UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2",
-      ).run(fileNames.length, jobId.value);
-
-      const query = db.query(
-        "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
-      );
-
-      // Start the conversion process in the background
-      Promise.all(
-        fileNames.map(async (fileName) => {
-          const filePath = `${userUploadsDir}${fileName}`;
-          const fileTypeOrig = fileName.split(".").pop() ?? "";
-          const fileType = normalizeFiletype(fileTypeOrig);
-          const newFileExt = normalizeOutputFiletype(convertTo);
-          const newFileName = fileName.replace(
-            new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
-            newFileExt,
-          );
-          const targetPath = `${userOutputDir}${newFileName}`;
-
-          const result = await mainConverter(
-            filePath,
-            fileType,
-            convertTo,
-            targetPath,
-            {},
-            converterName,
-          );
-          if (jobId.value) {
-            query.run(jobId.value, fileName, newFileName, result);
-          }
-        }),
-      )
-        .then(() => {
-          // All conversions are done, update the job status to 'completed'
-          if (jobId.value) {
-            db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(
-              jobId.value,
-            );
-          }
-
-          // delete all uploaded files in userUploadsDir
-          // rmSync(userUploadsDir, { recursive: true, force: true });
-        })
-        .catch((error) => {
-          console.error("Error in conversion process:", error);
-        });
-
-      // Redirect the client immediately
-      return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
-    },
-    {
-      body: t.Object({
-        convert_to: t.String(),
-        file_names: t.String(),
-      }),
-    },
-  )
-  .get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
-    if (HIDE_HISTORY) {
-      return redirect(`${WEBROOT}/`, 302);
-    }
-
-    if (!auth?.value) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-    const user = await jwt.verify(auth.value);
-
-    if (!user) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-
-    let userJobs = db
-      .query("SELECT * FROM jobs WHERE user_id = ?")
-      .as(Jobs)
-      .all(user.id)
-      .reverse();
-
-    for (const job of userJobs) {
-      const files = db
-        .query("SELECT * FROM file_names WHERE job_id = ?")
-        .as(Filename)
-        .all(job.id);
-
-      job.finished_files = files.length;
-      job.files_detailed = files;
-    }
-
-    // filter out jobs with no files
-    userJobs = userJobs.filter((job) => job.num_files > 0);
-
-    return (
-      <BaseHtml webroot={WEBROOT} title="ConvertX | Results">
-        <>
-          <Header
-            webroot={WEBROOT}
-            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-            hideHistory={HIDE_HISTORY}
-            loggedIn
-          />
-          <main
-            class={`
-              w-full flex-1 px-2
-              sm:px-4
-            `}
-          >
-            <article class="article">
-              <h1 class="mb-4 text-xl">Results</h1>
-              <table
-                class={`
-                  w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
-                  [&_td]:p-4
-                  [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
-                `}
-              >
-                <thead>
-                  <tr>
-                    <th
-                      class={`
-                        px-2 py-2
-                        sm:px-4
-                      `}
-                    >
-                      <span class="sr-only">Expand details</span>
-                    </th>
-                    <th
-                      class={`
-                        px-2 py-2
-                        sm:px-4
-                      `}
-                    >
-                      Time
-                    </th>
-                    <th
-                      class={`
-                        px-2 py-2
-                        sm:px-4
-                      `}
-                    >
-                      Files
-                    </th>
-                    <th
-                      class={`
-                        px-2 py-2
-                        max-sm:hidden
-                        sm:px-4
-                      `}
-                    >
-                      Files Done
-                    </th>
-                    <th
-                      class={`
-                        px-2 py-2
-                        sm:px-4
-                      `}
-                    >
-                      Status
-                    </th>
-                    <th
-                      class={`
-                        px-2 py-2
-                        sm:px-4
-                      `}
-                    >
-                      View
-                    </th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {userJobs.map((job) => (
-                    <>
-                      <tr id={`job-row-${job.id}`}>
-                        <td
-                          class="job-details-toggle cursor-pointer"
-                          data-job-id={job.id}
-                        >
-                          <svg
-                            id={`arrow-${job.id}`}
-                            xmlns="http://www.w3.org/2000/svg"
-                            fill="none"
-                            viewBox="0 0 24 24"
-                            stroke-width="1.5"
-                            stroke="currentColor"
-                            class="inline-block h-4 w-4"
-                          >
-                            <path
-                              stroke-linecap="round"
-                              stroke-linejoin="round"
-                              d="M8.25 4.5l7.5 7.5-7.5 7.5"
-                            />
-                          </svg>
-                        </td>
-                        <td safe>
-                          {new Date(job.date_created).toLocaleTimeString()}
-                        </td>
-                        <td>{job.num_files}</td>
-                        <td class="max-sm:hidden">{job.finished_files}</td>
-                        <td safe>{job.status}</td>
-                        <td>
-                          <a
-                            class={`
-                              text-accent-500 underline
-                              hover:text-accent-400
-                            `}
-                            href={`${WEBROOT}/results/${job.id}`}
-                          >
-                            View
-                          </a>
-                        </td>
-                      </tr>
-                      <tr id={`details-${job.id}`} class="hidden">
-                        <td colspan="6">
-                          <div class="p-2 text-sm text-neutral-500">
-                            <div class="mb-1 font-semibold">
-                              Detailed File Information:
-                            </div>
-                            {job.files_detailed.map(
-                              (file: Filename) => (
-                                <div
-                                  class="flex items-center"
-                                >
-                                  <span
-                                    class="w-5/12 truncate"
-                                    title={file.file_name}
-                                    safe
-                                  >
-                                    {file.file_name}
-                                  </span>
-                                  <svg
-                                    xmlns="http://www.w3.org/2000/svg"
-                                    viewBox="0 0 20 20"
-                                    fill="currentColor"
-                                    class="mx-2 inline-block h-4 w-4 text-neutral-500"
-                                  >
-                                    <path
-                                      fill-rule="evenodd"
-                                      d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
-                                      clip-rule="evenodd"
-                                    />
-                                  </svg>
-                                  <span
-                                    class="w-5/12 truncate"
-                                    title={file.output_file_name}
-                                    safe
-                                  >
-                                    {file.output_file_name}
-                                  </span>
-                                </div>
-                              ),
-                            )}
-                          </div>
-                        </td>
-                      </tr>
-                    </>
-                  ))}
-                </tbody>
-              </table>
-            </article>
-          </main>
-          <script>
-            {`
-              document.addEventListener('DOMContentLoaded', () => {
-                const toggles = document.querySelectorAll('.job-details-toggle');
-                toggles.forEach(toggle => {
-                  toggle.addEventListener('click', function() {
-                    const jobId = this.dataset.jobId;
-                    const detailsRow = document.getElementById(\`details-\${jobId}\`);
-                    // The arrow SVG itself has the ID arrow-\${jobId}
-                    const arrow = document.getElementById(\`arrow-\${jobId}\`);
-
-                    if (detailsRow && arrow) {
-                      detailsRow.classList.toggle("hidden");
-                      if (detailsRow.classList.contains("hidden")) {
-                        // Right-facing arrow (collapsed)
-                        arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
-                      } else {
-                        // Down-facing arrow (expanded)
-                        arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
-                      }
-                    }
-                  });
-                });
-              });
-            `}
-          </script>
-        </>
-      </BaseHtml>
-    );
-  })
-  .get(
-    "/results/:jobId",
-    async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      if (job_id?.value) {
-        // clear the job_id cookie since we are viewing the results
-        job_id.remove();
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const job = db
-        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
-        .as(Jobs)
-        .get(user.id, params.jobId);
-
-      if (!job) {
-        set.status = 404;
-        return {
-          message: "Job not found.",
-        };
-      }
-
-      const outputPath = `${user.id}/${params.jobId}/`;
-
-      const files = db
-        .query("SELECT * FROM file_names WHERE job_id = ?")
-        .as(Filename)
-        .all(params.jobId);
-
-      return (
-        <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
-          <>
-            <Header
-              webroot={WEBROOT}
-              allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-              loggedIn
-            />
-            <main
-              class={`
-                w-full flex-1 px-2
-                sm:px-4
-              `}
-            >
-              <article class="article">
-                <div class="mb-4 flex items-center justify-between">
-                  <h1 class="text-xl">Results</h1>
-                  <div>
-                    <button
-                      type="button"
-                      class="btn-primary float-right w-40"
-                      onclick="downloadAll()"
-                      {...(files.length !== job.num_files
-                        ? { disabled: true, "aria-busy": "true" }
-                        : "")}
-                    >
-                      {files.length === job.num_files
-                        ? "Download All"
-                        : "Converting..."}
-                    </button>
-                  </div>
-                </div>
-                <progress
-                  max={job.num_files}
-                  value={files.length}
-                  class={`
-                    text-accent-500 accent-accent-500 mb-4 inline-block h-2 w-full appearance-none
-                    overflow-hidden rounded-full border-0 bg-neutral-700 bg-none
-                    [&[value]::-webkit-progress-value]:bg-accent-500
-                    [&[value]::-webkit-progress-value]:transition-[inline-size]
-                    [&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
-                    [&::-webkit-progress-value]:[background:none]
-                  `}
-                />
-                <table
-                  class={`
-                    w-full table-auto rounded bg-neutral-900 text-left
-                    [&_td]:p-4
-                    [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
-                  `}
-                >
-                  <thead>
-                    <tr>
-                      <th
-                        class={`
-                          px-2 py-2
-                          sm:px-4
-                        `}
-                      >
-                        Converted File Name
-                      </th>
-                      <th
-                        class={`
-                          px-2 py-2
-                          sm:px-4
-                        `}
-                      >
-                        Status
-                      </th>
-                      <th
-                        class={`
-                          px-2 py-2
-                          sm:px-4
-                        `}
-                      >
-                        View
-                      </th>
-                      <th
-                        class={`
-                          px-2 py-2
-                          sm:px-4
-                        `}
-                      >
-                        Download
-                      </th>
-                    </tr>
-                  </thead>
-                  <tbody>
-                    {files.map((file) => (
-                      <tr>
-                        <td safe class="max-w-[20vw] truncate">
-                          {file.output_file_name}
-                        </td>
-                        <td safe>{file.status}</td>
-                        <td>
-                          <a
-                            class={`
-                              text-accent-500 underline
-                              hover:text-accent-400
-                            `}
-                            href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
-                          >
-                            View
-                          </a>
-                        </td>
-                        <td>
-                          <a
-                            class={`
-                              text-accent-500 underline
-                              hover:text-accent-400
-                            `}
-                            href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
-                            download={file.output_file_name}
-                          >
-                            Download
-                          </a>
-                        </td>
-                      </tr>
-                    ))}
-                  </tbody>
-                </table>
-              </article>
-            </main>
-            <script src={`${WEBROOT}/results.js`} defer />
-          </>
-        </BaseHtml>
-      );
-    },
-  )
-  .post(
-    "/progress/:jobId",
-    async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      if (job_id?.value) {
-        // clear the job_id cookie since we are viewing the results
-        job_id.remove();
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const job = db
-        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
-        .as(Jobs)
-        .get(user.id, params.jobId);
-
-      if (!job) {
-        set.status = 404;
-        return {
-          message: "Job not found.",
-        };
-      }
-
-      const outputPath = `${user.id}/${params.jobId}/`;
-
-      const files = db
-        .query("SELECT * FROM file_names WHERE job_id = ?")
-        .as(Filename)
-        .all(params.jobId);
-
-      return (
-        <article class="article">
-          <div class="mb-4 flex items-center justify-between">
-            <h1 class="text-xl">Results</h1>
-            <div>
-              <button
-                type="button"
-                class="btn-primary float-right w-40"
-                onclick="downloadAll()"
-                {...(files.length !== job.num_files
-                  ? { disabled: true, "aria-busy": "true" }
-                  : "")}
-              >
-                {files.length === job.num_files
-                  ? "Download All"
-                  : "Converting..."}
-              </button>
-            </div>
-          </div>
-          <progress
-            max={job.num_files}
-            value={files.length}
-            class={`
-              text-accent-500 accent-accent-500 mb-4 inline-block h-2 w-full appearance-none
-              overflow-hidden rounded-full border-0 bg-neutral-700 bg-none
-              [&[value]::-webkit-progress-value]:bg-accent-500
-              [&[value]::-webkit-progress-value]:transition-[inline-size]
-              [&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
-              [&::-webkit-progress-value]:[background:none]
-            `}
-          />
-          <table
-            class={`
-              w-full table-auto rounded bg-neutral-900 text-left
-              [&_td]:p-4
-              [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
-            `}
-          >
-            <thead>
-              <tr>
-                <th
-                  class={`
-                    px-2 py-2
-                    sm:px-4
-                  `}
-                >
-                  Converted File Name
-                </th>
-                <th
-                  class={`
-                    px-2 py-2
-                    sm:px-4
-                  `}
-                >
-                  Status
-                </th>
-                <th
-                  class={`
-                    px-2 py-2
-                    sm:px-4
-                  `}
-                >
-                  View
-                </th>
-                <th
-                  class={`
-                    px-2 py-2
-                    sm:px-4
-                  `}
-                >
-                  Download
-                </th>
-              </tr>
-            </thead>
-            <tbody>
-              {files.map((file) => (
-                <tr>
-                  <td safe class="max-w-[20vw] truncate">
-                    {file.output_file_name}
-                  </td>
-                  <td safe>{file.status}</td>
-                  <td>
-                    <a
-                      class={`
-                        text-accent-500 underline
-                        hover:text-accent-400
-                      `}
-                      href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
-                    >
-                      View
-                    </a>
-                  </td>
-                  <td>
-                    <a
-                      class={`
-                        text-accent-500 underline
-                        hover:text-accent-400
-                      `}
-                      href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
-                      download={file.output_file_name}
-                    >
-                      Download
-                    </a>
-                  </td>
-                </tr>
-              ))}
-            </tbody>
-          </table>
-        </article>
-      );
-    },
-  )
-  .get(
-    "/download/:userId/:jobId/:fileName",
-    async ({ params, jwt, redirect, cookie: { auth } }) => {
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const job = await db
-        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
-        .get(user.id, params.jobId);
-
-      if (!job) {
-        return redirect(`${WEBROOT}/results`, 302);
-      }
-      // parse from url encoded string
-      const userId = decodeURIComponent(params.userId);
-      const jobId = decodeURIComponent(params.jobId);
-      const fileName = sanitize(decodeURIComponent(params.fileName));
-
-      const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
-      return Bun.file(filePath);
-    },
-  )
-  .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
-    if (!auth?.value) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-
-    const user = await jwt.verify(auth.value);
-    if (!user) {
-      return redirect(`${WEBROOT}/login`, 302);
-    }
-
-    return (
-      <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
-        <>
-          <Header
-            webroot={WEBROOT}
-            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
-            loggedIn
-          />
-          <main
-            class={`
-              w-full flex-1 px-2
-              sm:px-4
-            `}
-          >
-            <article class="article">
-              <h1 class="mb-4 text-xl">Converters</h1>
-              <table
-                class={`
-                  w-full table-auto rounded bg-neutral-900 text-left
-                  [&_td]:p-4
-                  [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
-                  [&_ul]:list-inside [&_ul]:list-disc
-                `}
-              >
-                <thead>
-                  <tr>
-                    <th class="mx-4 my-2">Converter</th>
-                    <th class="mx-4 my-2">From (Count)</th>
-                    <th class="mx-4 my-2">To (Count)</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {Object.entries(getAllTargets()).map(
-                    ([converter, targets]) => {
-                      const inputs = getAllInputs(converter);
-                      return (
-                        <tr>
-                          <td safe>{converter}</td>
-                          <td>
-                            Count: {inputs.length}
-                            <ul>
-                              {inputs.map((input) => (
-                                <li safe>{input}</li>
-                              ))}
-                            </ul>
-                          </td>
-                          <td>
-                            Count: {targets.length}
-                            <ul>
-                              {targets.map((target) => (
-                                <li safe>{target}</li>
-                              ))}
-                            </ul>
-                          </td>
-                        </tr>
-                      );
-                    },
-                  )}
-                </tbody>
-              </table>
-            </article>
-          </main>
-        </>
-      </BaseHtml>
-    );
-  })
-  .get(
-    "/zip/:userId/:jobId",
-    async ({ params, jwt, redirect, cookie: { auth } }) => {
-      // TODO: Implement zip download
-      if (!auth?.value) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const user = await jwt.verify(auth.value);
-      if (!user) {
-        return redirect(`${WEBROOT}/login`, 302);
-      }
-
-      const job = await db
-        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
-        .get(user.id, params.jobId);
-
-      if (!job) {
-        return redirect(`${WEBROOT}/results`, 302);
-      }
-
-      // const userId = decodeURIComponent(params.userId);
-      // const jobId = decodeURIComponent(params.jobId);
-      // const outputPath = `${outputDir}${userId}/`{jobId}/);
-
-      // return Bun.zip(outputPath);
-    },
-  )
+  .use(user)
+  .use(root)
+  .use(upload)
+  .use(history)
+  .use(convert)
+  .use(download)
+  .use(results)
+  .use(deleteFile)
+  .use(listConverters)
+  .use(chooseConverter)
   .onError(({ error }) => {
-    // log.error(` ${request.method} ${request.url}`, code, error);
     console.error(error);
   });
 

+ 0 - 18
src/main.css

@@ -18,24 +18,6 @@
   --color-accent-400: rgba(var(--accent-400));
 }
 
-/*
-  The default border color has changed to `currentColor` in Tailwind CSS v4,
-  so we've added these compatibility styles to make sure everything still
-  looks the same as it did with Tailwind CSS v3.
-
-  If we ever want to remove these styles, we need to add an explicit border
-  color utility to any element that depends on these defaults.
-*/
-@layer base {
-  *,
-  ::after,
-  ::before,
-  ::backdrop,
-  ::file-selector-button {
-    border-color: var(--color-gray-200, currentColor);
-  }
-}
-
 @utility article {
   @apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm;
 }

+ 75 - 0
src/pages/chooseConverter.tsx

@@ -0,0 +1,75 @@
+import Elysia, { t } from "elysia";
+import { userService } from "./user";
+import { Html } from "@elysiajs/html";
+import {
+  getPossibleTargets,
+} from "../converters/main";
+
+export const chooseConverter = new Elysia()
+  .use(userService)
+  .post(
+    "/conversions",
+    ({ body }) => {
+      return (
+        <>
+          <article
+            class={`
+              convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
+              overflow-x-hidden overflow-y-auto rounded bg-neutral-800
+              sm:h-[30vh]
+            `}
+          >
+            {Object.entries(getPossibleTargets(body.fileType)).map(
+              ([converter, targets]) => (
+                <article
+                  class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
+                  data-converter={converter}
+                >
+                  <header class="mb-2 w-full text-xl font-bold" safe>
+                    {converter}
+                  </header>
+                  <ul class="convert_to_target flex flex-row flex-wrap gap-1">
+                    {targets.map((target) => (
+                      <button
+                        // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
+                        tabindex={0}
+                        class={`
+                          target rounded bg-neutral-700 p-1 text-base
+                          hover:bg-neutral-600
+                        `}
+                        data-value={`${target},${converter}`}
+                        data-target={target}
+                        data-converter={converter}
+                        type="button"
+                        safe
+                      >
+                        {target}
+                      </button>
+                    ))}
+                  </ul>
+                </article>
+              ),
+            )}
+          </article>
+
+          <select name="convert_to" aria-label="Convert to" required hidden>
+            <option selected disabled value="">
+              Convert to
+            </option>
+            {Object.entries(getPossibleTargets(body.fileType)).map(
+              ([converter, targets]) => (
+                <optgroup label={converter}>
+                  {targets.map((target) => (
+                    <option value={`${target},${converter}`} safe>
+                      {target}
+                    </option>
+                  ))}
+                </optgroup>
+              ),
+            )}
+          </select>
+        </>
+      );
+    },
+    { body: t.Object({ fileType: t.String() }) },
+  )

+ 122 - 0
src/pages/convert.tsx

@@ -0,0 +1,122 @@
+import { mkdir } from "node:fs/promises";
+import { Elysia, t } from "elysia";
+import sanitize from "sanitize-filename";
+import { Jobs, outputDir, uploadsDir } from "..";
+import { mainConverter } from "../converters/main";
+import { WEBROOT } from "../helpers/env";
+import db from "../db/db";
+import {
+  normalizeFiletype,
+  normalizeOutputFiletype,
+} from "../helpers/normalizeFiletype";
+import { userService } from "./user";
+
+export const convert = new Elysia().use(userService).post(
+  "/convert",
+  async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
+    if (!auth?.value) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    const user = await jwt.verify(auth.value);
+    if (!user) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    if (!jobId?.value) {
+      return redirect(`${WEBROOT}/`, 302);
+    }
+
+    const existingJob = db
+      .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
+      .as(Jobs)
+      .get(jobId.value, user.id);
+
+    if (!existingJob) {
+      return redirect(`${WEBROOT}/`, 302);
+    }
+
+    const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
+    const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
+
+    // create the output directory
+    try {
+      await mkdir(userOutputDir, { recursive: true });
+    } catch (error) {
+      console.error(
+        `Failed to create the output directory: ${userOutputDir}.`,
+        error,
+      );
+    }
+
+    const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
+    const converterName = body.convert_to.split(",")[1];
+    const fileNames = JSON.parse(body.file_names) as string[];
+
+    for (let i = 0; i < fileNames.length; i++) {
+      fileNames[i] = sanitize(fileNames[i] || "");
+    }
+
+    if (!Array.isArray(fileNames) || fileNames.length === 0) {
+      return redirect(`${WEBROOT}/`, 302);
+    }
+
+    db.query(
+      "UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2",
+    ).run(fileNames.length, jobId.value);
+
+    const query = db.query(
+      "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
+    );
+
+    // Start the conversion process in the background
+    Promise.all(
+      fileNames.map(async (fileName) => {
+        const filePath = `${userUploadsDir}${fileName}`;
+        const fileTypeOrig = fileName.split(".").pop() ?? "";
+        const fileType = normalizeFiletype(fileTypeOrig);
+        const newFileExt = normalizeOutputFiletype(convertTo);
+        const newFileName = fileName.replace(
+          new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
+          newFileExt,
+        );
+        const targetPath = `${userOutputDir}${newFileName}`;
+
+        const result = await mainConverter(
+          filePath,
+          fileType,
+          convertTo,
+          targetPath,
+          {},
+          converterName,
+        );
+        if (jobId.value) {
+          query.run(jobId.value, fileName, newFileName, result);
+        }
+      }),
+    )
+      .then(() => {
+        // All conversions are done, update the job status to 'completed'
+        if (jobId.value) {
+          db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(
+            jobId.value,
+          );
+        }
+
+        // delete all uploaded files in userUploadsDir
+        // rmSync(userUploadsDir, { recursive: true, force: true });
+      })
+      .catch((error) => {
+        console.error("Error in conversion process:", error);
+      });
+
+    // Redirect the client immediately
+    return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
+  },
+  {
+    body: t.Object({
+      convert_to: t.String(),
+      file_names: t.String(),
+    }),
+  },
+);

+ 39 - 0
src/pages/deleteFile.tsx

@@ -0,0 +1,39 @@
+import { Elysia, t } from "elysia";
+import { userService } from "./user";
+import { unlink } from "node:fs/promises";
+import { WEBROOT } from "../helpers/env";
+import { uploadsDir } from "..";
+import db from "../db/db";
+
+export const deleteFile = new Elysia()
+  .use(userService)
+  .post(
+    "/delete",
+    async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      if (!jobId?.value) {
+        return redirect(`${WEBROOT}/`, 302);
+      }
+
+      const existingJob = await db
+        .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
+        .get(jobId.value, user.id);
+
+      if (!existingJob) {
+        return redirect(`${WEBROOT}/`, 302);
+      }
+
+      const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
+
+      await unlink(`${userUploadsDir}${body.filename}`);
+    },
+    { body: t.Object({ filename: t.String() }) },
+  )

+ 66 - 0
src/pages/download.tsx

@@ -0,0 +1,66 @@
+
+import { Elysia } from "elysia";
+import { userService } from "./user";
+import { WEBROOT } from "../helpers/env";
+import sanitize from "sanitize-filename";
+import { outputDir } from "..";
+import db from "../db/db";
+
+export const download = new Elysia()
+  .use(userService)
+  .get(
+    "/download/:userId/:jobId/:fileName",
+    async ({ params, jwt, redirect, cookie: { auth } }) => {
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const job = await db
+        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
+        .get(user.id, params.jobId);
+
+      if (!job) {
+        return redirect(`${WEBROOT}/results`, 302);
+      }
+      // parse from url encoded string
+      const userId = decodeURIComponent(params.userId);
+      const jobId = decodeURIComponent(params.jobId);
+      const fileName = sanitize(decodeURIComponent(params.fileName));
+
+      const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
+      return Bun.file(filePath);
+    },
+  )
+  .get(
+    "/zip/:userId/:jobId",
+    async ({ params, jwt, redirect, cookie: { auth } }) => {
+      // TODO: Implement zip download
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const job = await db
+        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
+        .get(user.id, params.jobId);
+
+      if (!job) {
+        return redirect(`${WEBROOT}/results`, 302);
+      }
+
+      // const userId = decodeURIComponent(params.userId);
+      // const jobId = decodeURIComponent(params.jobId);
+      // const outputPath = `${outputDir}${userId}/`{jobId}/);
+
+      // return Bun.zip(outputPath);
+    },
+  )

+ 242 - 0
src/pages/history.tsx

@@ -0,0 +1,242 @@
+import { Elysia } from "elysia";
+import { BaseHtml } from "../components/base";
+import { Html } from "@elysiajs/html";
+import { Header } from "../components/header";
+import { userService } from "./user";
+import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env";
+import { Filename, Jobs } from "..";
+import db from "../db/db";
+
+export const history = new Elysia()
+  .use(userService)
+  .get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
+    if (HIDE_HISTORY) {
+      return redirect(`${WEBROOT}/`, 302);
+    }
+
+    if (!auth?.value) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+    const user = await jwt.verify(auth.value);
+
+    if (!user) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    let userJobs = db
+      .query("SELECT * FROM jobs WHERE user_id = ?")
+      .as(Jobs)
+      .all(user.id)
+      .reverse();
+
+    for (const job of userJobs) {
+      const files = db
+        .query("SELECT * FROM file_names WHERE job_id = ?")
+        .as(Filename)
+        .all(job.id);
+
+      job.finished_files = files.length;
+      job.files_detailed = files;
+    }
+
+    // filter out jobs with no files
+    userJobs = userJobs.filter((job) => job.num_files > 0);
+
+    return (
+      <BaseHtml webroot={WEBROOT} title="ConvertX | Results">
+        <>
+          <Header
+            webroot={WEBROOT}
+            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+            hideHistory={HIDE_HISTORY}
+            loggedIn
+          />
+          <main
+            class={`
+              w-full flex-1 px-2
+              sm:px-4
+            `}
+          >
+            <article class="article">
+              <h1 class="mb-4 text-xl">Results</h1>
+              <table
+                class={`
+                  w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
+                  [&_td]:p-4
+                  [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
+                `}
+              >
+                <thead>
+                  <tr>
+                    <th
+                      class={`
+                        px-2 py-2
+                        sm:px-4
+                      `}
+                    >
+                      <span class="sr-only">Expand details</span>
+                    </th>
+                    <th
+                      class={`
+                        px-2 py-2
+                        sm:px-4
+                      `}
+                    >
+                      Time
+                    </th>
+                    <th
+                      class={`
+                        px-2 py-2
+                        sm:px-4
+                      `}
+                    >
+                      Files
+                    </th>
+                    <th
+                      class={`
+                        px-2 py-2
+                        max-sm:hidden
+                        sm:px-4
+                      `}
+                    >
+                      Files Done
+                    </th>
+                    <th
+                      class={`
+                        px-2 py-2
+                        sm:px-4
+                      `}
+                    >
+                      Status
+                    </th>
+                    <th
+                      class={`
+                        px-2 py-2
+                        sm:px-4
+                      `}
+                    >
+                      View
+                    </th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {userJobs.map((job) => (
+                    <>
+                      <tr id={`job-row-${job.id}`}>
+                        <td
+                          class="job-details-toggle cursor-pointer"
+                          data-job-id={job.id}
+                        >
+                          <svg
+                            id={`arrow-${job.id}`}
+                            xmlns="http://www.w3.org/2000/svg"
+                            fill="none"
+                            viewBox="0 0 24 24"
+                            stroke-width="1.5"
+                            stroke="currentColor"
+                            class="inline-block h-4 w-4"
+                          >
+                            <path
+                              stroke-linecap="round"
+                              stroke-linejoin="round"
+                              d="M8.25 4.5l7.5 7.5-7.5 7.5"
+                            />
+                          </svg>
+                        </td>
+                        <td safe>
+                          {new Date(job.date_created).toLocaleTimeString()}
+                        </td>
+                        <td>{job.num_files}</td>
+                        <td class="max-sm:hidden">{job.finished_files}</td>
+                        <td safe>{job.status}</td>
+                        <td>
+                          <a
+                            class={`
+                              text-accent-500 underline
+                              hover:text-accent-400
+                            `}
+                            href={`${WEBROOT}/results/${job.id}`}
+                          >
+                            View
+                          </a>
+                        </td>
+                      </tr>
+                      <tr id={`details-${job.id}`} class="hidden">
+                        <td colspan="6">
+                          <div class="p-2 text-sm text-neutral-500">
+                            <div class="mb-1 font-semibold">
+                              Detailed File Information:
+                            </div>
+                            {job.files_detailed.map(
+                              (file: Filename) => (
+                                <div
+                                  class="flex items-center"
+                                >
+                                  <span
+                                    class="w-5/12 truncate"
+                                    title={file.file_name}
+                                    safe
+                                  >
+                                    {file.file_name}
+                                  </span>
+                                  <svg
+                                    xmlns="http://www.w3.org/2000/svg"
+                                    viewBox="0 0 20 20"
+                                    fill="currentColor"
+                                    class="mx-2 inline-block h-4 w-4 text-neutral-500"
+                                  >
+                                    <path
+                                      fill-rule="evenodd"
+                                      d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
+                                      clip-rule="evenodd"
+                                    />
+                                  </svg>
+                                  <span
+                                    class="w-5/12 truncate"
+                                    title={file.output_file_name}
+                                    safe
+                                  >
+                                    {file.output_file_name}
+                                  </span>
+                                </div>
+                              ),
+                            )}
+                          </div>
+                        </td>
+                      </tr>
+                    </>
+                  ))}
+                </tbody>
+              </table>
+            </article>
+          </main>
+          <script>
+            {`
+              document.addEventListener('DOMContentLoaded', () => {
+                const toggles = document.querySelectorAll('.job-details-toggle');
+                toggles.forEach(toggle => {
+                  toggle.addEventListener('click', function() {
+                    const jobId = this.dataset.jobId;
+                    const detailsRow = document.getElementById(\`details-\${jobId}\`);
+                    // The arrow SVG itself has the ID arrow-\${jobId}
+                    const arrow = document.getElementById(\`arrow-\${jobId}\`);
+
+                    if (detailsRow && arrow) {
+                      detailsRow.classList.toggle("hidden");
+                      if (detailsRow.classList.contains("hidden")) {
+                        // Right-facing arrow (collapsed)
+                        arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
+                      } else {
+                        // Down-facing arrow (expanded)
+                        arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
+                      }
+                    }
+                  });
+                });
+              });
+            `}
+          </script>
+        </>
+      </BaseHtml>
+    );
+  })

+ 89 - 0
src/pages/listConverters.tsx

@@ -0,0 +1,89 @@
+import Elysia from "elysia";
+import { userService } from "./user";
+import { Html } from "@elysiajs/html";
+import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
+import { BaseHtml } from "../components/base";
+import { Header } from "../components/header";
+import {
+  getAllInputs,
+  getAllTargets,
+} from "../converters/main";
+
+export const listConverters = new Elysia()
+  .use(userService)
+  .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
+    if (!auth?.value) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    const user = await jwt.verify(auth.value);
+    if (!user) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    return (
+      <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
+        <>
+          <Header
+            webroot={WEBROOT}
+            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+            loggedIn
+          />
+          <main
+            class={`
+              w-full flex-1 px-2
+              sm:px-4
+            `}
+          >
+            <article class="article">
+              <h1 class="mb-4 text-xl">Converters</h1>
+              <table
+                class={`
+                  w-full table-auto rounded bg-neutral-900 text-left
+                  [&_td]:p-4
+                  [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
+                  [&_ul]:list-inside [&_ul]:list-disc
+                `}
+              >
+                <thead>
+                  <tr>
+                    <th class="mx-4 my-2">Converter</th>
+                    <th class="mx-4 my-2">From (Count)</th>
+                    <th class="mx-4 my-2">To (Count)</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {Object.entries(getAllTargets()).map(
+                    ([converter, targets]) => {
+                      const inputs = getAllInputs(converter);
+                      return (
+                        <tr>
+                          <td safe>{converter}</td>
+                          <td>
+                            Count: {inputs.length}
+                            <ul>
+                              {inputs.map((input) => (
+                                <li safe>{input}</li>
+                              ))}
+                            </ul>
+                          </td>
+                          <td>
+                            Count: {targets.length}
+                            <ul>
+                              {targets.map((target) => (
+                                <li safe>{target}</li>
+                              ))}
+                            </ul>
+                          </td>
+                        </tr>
+                      );
+                    },
+                  )}
+                </tbody>
+              </table>
+            </article>
+          </main>
+        </>
+      </BaseHtml>
+    );
+  })

+ 324 - 0
src/pages/results.tsx

@@ -0,0 +1,324 @@
+import { Elysia } from "elysia";
+import { userService } from "./user";
+import { Html } from "@elysiajs/html";
+import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
+import { Filename, Jobs } from "..";
+import { Header } from "../components/header";
+import { BaseHtml } from "../components/base";
+import db from "../db/db";
+
+export const results = new Elysia()
+  .use(userService)
+  .get(
+    "/results/:jobId",
+    async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      if (job_id?.value) {
+        // clear the job_id cookie since we are viewing the results
+        job_id.remove();
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const job = db
+        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
+        .as(Jobs)
+        .get(user.id, params.jobId);
+
+      if (!job) {
+        set.status = 404;
+        return {
+          message: "Job not found.",
+        };
+      }
+
+      const outputPath = `${user.id}/${params.jobId}/`;
+
+      const files = db
+        .query("SELECT * FROM file_names WHERE job_id = ?")
+        .as(Filename)
+        .all(params.jobId);
+
+      return (
+        <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
+          <>
+            <Header
+              webroot={WEBROOT}
+              allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+              loggedIn
+            />
+            <main
+              class={`
+                w-full flex-1 px-2
+                sm:px-4
+              `}
+            >
+              <article class="article">
+                <div class="mb-4 flex items-center justify-between">
+                  <h1 class="text-xl">Results</h1>
+                  <div>
+                    <button
+                      type="button"
+                      class="float-right w-40 btn-primary"
+                      onclick="downloadAll()"
+                      {...(files.length !== job.num_files
+                        ? { disabled: true, "aria-busy": "true" }
+                        : "")}
+                    >
+                      {files.length === job.num_files
+                        ? "Download All"
+                        : "Converting..."}
+                    </button>
+                  </div>
+                </div>
+                <progress
+                  max={job.num_files}
+                  value={files.length}
+                  class={`
+                    mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full
+                    border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500
+                    [&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
+                    [&::-webkit-progress-value]:[background:none]
+                    [&[value]::-webkit-progress-value]:bg-accent-500
+                    [&[value]::-webkit-progress-value]:transition-[inline-size]
+                  `}
+                />
+                <table
+                  class={`
+                    w-full table-auto rounded bg-neutral-900 text-left
+                    [&_td]:p-4
+                    [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
+                  `}
+                >
+                  <thead>
+                    <tr>
+                      <th
+                        class={`
+                          px-2 py-2
+                          sm:px-4
+                        `}
+                      >
+                        Converted File Name
+                      </th>
+                      <th
+                        class={`
+                          px-2 py-2
+                          sm:px-4
+                        `}
+                      >
+                        Status
+                      </th>
+                      <th
+                        class={`
+                          px-2 py-2
+                          sm:px-4
+                        `}
+                      >
+                        View
+                      </th>
+                      <th
+                        class={`
+                          px-2 py-2
+                          sm:px-4
+                        `}
+                      >
+                        Download
+                      </th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    {files.map((file) => (
+                      <tr>
+                        <td safe class="max-w-[20vw] truncate">
+                          {file.output_file_name}
+                        </td>
+                        <td safe>{file.status}</td>
+                        <td>
+                          <a
+                            class={`
+                              text-accent-500 underline
+                              hover:text-accent-400
+                            `}
+                            href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
+                          >
+                            View
+                          </a>
+                        </td>
+                        <td>
+                          <a
+                            class={`
+                              text-accent-500 underline
+                              hover:text-accent-400
+                            `}
+                            href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
+                            download={file.output_file_name}
+                          >
+                            Download
+                          </a>
+                        </td>
+                      </tr>
+                    ))}
+                  </tbody>
+                </table>
+              </article>
+            </main>
+            <script src={`${WEBROOT}/results.js`} defer />
+          </>
+        </BaseHtml>
+      );
+    },
+  )
+  .post(
+    "/progress/:jobId",
+    async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      if (job_id?.value) {
+        // clear the job_id cookie since we are viewing the results
+        job_id.remove();
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const job = db
+        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
+        .as(Jobs)
+        .get(user.id, params.jobId);
+
+      if (!job) {
+        set.status = 404;
+        return {
+          message: "Job not found.",
+        };
+      }
+
+      const outputPath = `${user.id}/${params.jobId}/`;
+
+      const files = db
+        .query("SELECT * FROM file_names WHERE job_id = ?")
+        .as(Filename)
+        .all(params.jobId);
+
+      return (
+        <article class="article">
+          <div class="mb-4 flex items-center justify-between">
+            <h1 class="text-xl">Results</h1>
+            <div>
+              <button
+                type="button"
+                class="float-right w-40 btn-primary"
+                onclick="downloadAll()"
+                {...(files.length !== job.num_files
+                  ? { disabled: true, "aria-busy": "true" }
+                  : "")}
+              >
+                {files.length === job.num_files
+                  ? "Download All"
+                  : "Converting..."}
+              </button>
+            </div>
+          </div>
+          <progress
+            max={job.num_files}
+            value={files.length}
+            class={`
+              mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
+              bg-neutral-700 bg-none text-accent-500 accent-accent-500
+              [&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
+              [&::-webkit-progress-value]:[background:none]
+              [&[value]::-webkit-progress-value]:bg-accent-500
+              [&[value]::-webkit-progress-value]:transition-[inline-size]
+            `}
+          />
+          <table
+            class={`
+              w-full table-auto rounded bg-neutral-900 text-left
+              [&_td]:p-4
+              [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
+            `}
+          >
+            <thead>
+              <tr>
+                <th
+                  class={`
+                    px-2 py-2
+                    sm:px-4
+                  `}
+                >
+                  Converted File Name
+                </th>
+                <th
+                  class={`
+                    px-2 py-2
+                    sm:px-4
+                  `}
+                >
+                  Status
+                </th>
+                <th
+                  class={`
+                    px-2 py-2
+                    sm:px-4
+                  `}
+                >
+                  View
+                </th>
+                <th
+                  class={`
+                    px-2 py-2
+                    sm:px-4
+                  `}
+                >
+                  Download
+                </th>
+              </tr>
+            </thead>
+            <tbody>
+              {files.map((file) => (
+                <tr>
+                  <td safe class="max-w-[20vw] truncate">
+                    {file.output_file_name}
+                  </td>
+                  <td safe>{file.status}</td>
+                  <td>
+                    <a
+                      class={`
+                        text-accent-500 underline
+                        hover:text-accent-400
+                      `}
+                      href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
+                    >
+                      View
+                    </a>
+                  </td>
+                  <td>
+                    <a
+                      class={`
+                        text-accent-500 underline
+                        hover:text-accent-400
+                      `}
+                      href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
+                      download={file.output_file_name}
+                    >
+                      Download
+                    </a>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </article>
+      );
+    },
+  )

+ 248 - 0
src/pages/root.tsx

@@ -0,0 +1,248 @@
+import { Elysia } from "elysia";
+import { Html } from "@elysiajs/html";
+import { FIRST_RUN, User, userService } from "./user";
+import { ACCOUNT_REGISTRATION, ALLOW_UNAUTHENTICATED, HIDE_HISTORY, HTTP_ALLOWED, WEBROOT } from "../helpers/env";
+import { JWTPayloadSpec } from "@elysiajs/jwt";
+import { randomInt } from "node:crypto";
+import { BaseHtml } from "../components/base";
+import { Header } from "../components/header";
+import { getAllTargets } from "../converters/main";
+import db from "../db/db";
+
+export const root = new Elysia()
+  .use(userService)
+  .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
+    if (!ALLOW_UNAUTHENTICATED) {
+      if (FIRST_RUN) {
+        return redirect(`${WEBROOT}/setup`, 302);
+      }
+
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+    }
+
+    // validate jwt
+    let user: ({ id: string } & JWTPayloadSpec) | false = false;
+    if (ALLOW_UNAUTHENTICATED) {
+      const newUserId = String(
+        randomInt(
+          2 ** 24,
+          Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER),
+        ),
+      );
+      const accessToken = await jwt.sign({
+        id: newUserId,
+      });
+
+      user = { id: newUserId };
+      if (!auth) {
+        return {
+          message: "No auth cookie, perhaps your browser is blocking cookies.",
+        };
+      }
+
+      // set cookie
+      auth.set({
+        value: accessToken,
+        httpOnly: true,
+        secure: !HTTP_ALLOWED,
+        maxAge: 24 * 60 * 60,
+        sameSite: "strict",
+      });
+    } else if (auth?.value) {
+      user = await jwt.verify(auth.value);
+
+      if (user !== false && user.id) {
+        if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
+          // make sure user exists in db
+          const existingUser = db
+            .query("SELECT * FROM users WHERE id = ?")
+            .as(User)
+            .get(user.id);
+
+          if (!existingUser) {
+            if (auth?.value) {
+              auth.remove();
+            }
+            return redirect(`${WEBROOT}/login`, 302);
+          }
+        }
+      }
+    }
+
+    if (!user) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    // create a new job
+    db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
+      user.id,
+      new Date().toISOString(),
+    );
+
+    const id = (
+      db
+        .query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
+        .get(user.id) as { id: number }
+    ).id;
+
+    if (!jobId) {
+      return { message: "Cookies should be enabled to use this app." };
+    }
+
+    jobId.set({
+      value: id,
+      httpOnly: true,
+      secure: !HTTP_ALLOWED,
+      maxAge: 24 * 60 * 60,
+      sameSite: "strict",
+    });
+
+    console.log("jobId set to:", id);
+
+    return (
+      <BaseHtml webroot={WEBROOT}>
+        <>
+          <Header
+            webroot={WEBROOT}
+            accountRegistration={ACCOUNT_REGISTRATION}
+            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+            hideHistory={HIDE_HISTORY}
+            loggedIn
+          />
+          <main
+            class={`
+              w-full flex-1 px-2
+              sm:px-4
+            `}
+          >
+            <article class="article">
+              <h1 class="mb-4 text-xl">Convert</h1>
+              <div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
+                <table
+                  id="file-list"
+                  class={`
+                    w-full table-auto rounded bg-neutral-900
+                    [&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
+                    [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
+                  `}
+                />
+              </div>
+              <div
+                id="dropzone"
+                class={`
+                  relative flex h-48 w-full items-center justify-center rounded border border-dashed
+                  border-neutral-700 transition-all
+                  hover:border-neutral-600
+                  [&.dragover]:border-4 [&.dragover]:border-neutral-500
+                `}
+              >
+                <span>
+                  <b>Choose a file</b> or drag it here
+                </span>
+                <input
+                  type="file"
+                  name="file"
+                  multiple
+                  class="absolute inset-0 size-full cursor-pointer opacity-0"
+                />
+              </div>
+            </article>
+            <form
+              method="post"
+              action={`${WEBROOT}/convert`}
+              class="relative mx-auto mb-[35vh] w-full max-w-4xl"
+            >
+              <input type="hidden" name="file_names" id="file_names" />
+              <article class="article w-full">
+                <input
+                  type="search"
+                  name="convert_to_search"
+                  placeholder="Search for conversions"
+                  autocomplete="off"
+                  class="w-full rounded-sm bg-neutral-800 p-4"
+                />
+                <div class="select_container relative">
+                  <article
+                    class={`
+                      convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
+                      overflow-x-hidden overflow-y-auto rounded bg-neutral-800
+                      sm:h-[30vh]
+                    `}
+                  >
+                    {Object.entries(getAllTargets()).map(
+                      ([converter, targets]) => (
+                        <article
+                          class={`
+                            convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
+                          `}
+                          data-converter={converter}
+                        >
+                          <header class="mb-2 w-full text-xl font-bold" safe>
+                            {converter}
+                          </header>
+                          <ul class="convert_to_target flex flex-row flex-wrap gap-1">
+                            {targets.map((target) => (
+                              <button
+                                // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
+                                tabindex={0}
+                                class={`
+                                  target rounded bg-neutral-700 p-1 text-base
+                                  hover:bg-neutral-600
+                                `}
+                                data-value={`${target},${converter}`}
+                                data-target={target}
+                                data-converter={converter}
+                                type="button"
+                                safe
+                              >
+                                {target}
+                              </button>
+                            ))}
+                          </ul>
+                        </article>
+                      ),
+                    )}
+                  </article>
+
+                  {/* Hidden element which determines the format to convert the file too and the converter to use */}
+                  <select
+                    name="convert_to"
+                    aria-label="Convert to"
+                    required
+                    hidden
+                  >
+                    <option selected disabled value="">
+                      Convert to
+                    </option>
+                    {Object.entries(getAllTargets()).map(
+                      ([converter, targets]) => (
+                        <optgroup label={converter}>
+                          {targets.map((target) => (
+                            <option value={`${target},${converter}`} safe>
+                              {target}
+                            </option>
+                          ))}
+                        </optgroup>
+                      ),
+                    )}
+                  </select>
+                </div>
+              </article>
+              <input
+                class={`
+                  w-full btn-primary opacity-100
+                  disabled:cursor-not-allowed disabled:opacity-50
+                `}
+                type="submit"
+                value="Convert"
+                disabled
+              />
+            </form>
+          </main>
+          <script src="script.js" defer />
+        </>
+      </BaseHtml>
+    );
+  })

+ 51 - 0
src/pages/upload.tsx

@@ -0,0 +1,51 @@
+import { Elysia, t } from "elysia";
+import { userService } from "./user";
+import { WEBROOT } from "../helpers/env";
+import { uploadsDir } from "../index";
+import db from "../db/db";
+
+
+export const upload = new Elysia()
+  .use(userService)
+  .post(
+    "/upload",
+    async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      if (!jobId?.value) {
+        return redirect(`${WEBROOT}/`, 302);
+      }
+
+      const existingJob = await db
+        .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
+        .get(jobId.value, user.id);
+
+      if (!existingJob) {
+        return redirect(`${WEBROOT}/`, 302);
+      }
+
+      const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
+
+      if (body?.file) {
+        if (Array.isArray(body.file)) {
+          for (const file of body.file) {
+            await Bun.write(`${userUploadsDir}${file.name}`, file);
+          }
+        } else {
+          await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
+        }
+      }
+
+      return {
+        message: "Files uploaded successfully.",
+      };
+    },
+    { body: t.Object({ file: t.Files() }) },
+  )

+ 550 - 0
src/pages/user.tsx

@@ -0,0 +1,550 @@
+import { Elysia, t } from "elysia";
+import { Html } from "@elysiajs/html";
+import { BaseHtml } from "../components/base";
+import { Header } from "../components/header";
+import { jwt } from "@elysiajs/jwt";
+import { randomUUID } from "node:crypto";
+
+import { ACCOUNT_REGISTRATION, WEBROOT, HIDE_HISTORY, ALLOW_UNAUTHENTICATED, HTTP_ALLOWED } from "../helpers/env";
+import db from "../db/db";
+
+export class User {
+  id!: number;
+  email!: string;
+  password!: string;
+}
+
+export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
+
+export const userService = new Elysia({ name: 'user/service' })
+  .use(
+    jwt({
+      name: "jwt",
+      schema: t.Object({
+        id: t.String(),
+      }),
+      secret: process.env.JWT_SECRET ?? randomUUID(),
+      exp: "7d",
+    }),
+  )
+  .model({
+    signIn: t.Object({
+      email: t.String(),
+      password: t.String()
+    }),
+  })
+  .macro({
+    isSignIn(enabled: boolean) {
+      if (!enabled) return
+
+      return {
+        async beforeHandle({
+          status,
+          jwt,
+          cookie: { auth },
+        }) {
+          if (auth?.value) {
+            const user = await jwt.verify(auth.value);
+            return {
+              success: true,
+              user
+            }
+          }
+
+          return status(401, {
+            success: false,
+            message: 'Unauthorized'
+          })
+        }
+      }
+    }
+  })
+
+export const user = new Elysia()
+  .use(userService)
+  .get("/setup", ({ redirect }) => {
+    if (!FIRST_RUN) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    return (
+      <BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
+        <main
+          class={`
+            mx-auto w-full max-w-4xl flex-1 px-2
+            sm:px-4
+          `}
+        >
+          <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
+          <article class="article p-0">
+            <header class="w-full bg-neutral-800 p-4">
+              Create your account
+            </header>
+            <form method="post" action={`${WEBROOT}/register`} class="p-4">
+              <fieldset class="mb-4 flex flex-col gap-4">
+                <label class="flex flex-col gap-1">
+                  Email
+                  <input
+                    type="email"
+                    name="email"
+                    class="rounded-sm bg-neutral-800 p-3"
+                    placeholder="Email"
+                    autocomplete="email"
+                    required
+                  />
+                </label>
+                <label class="flex flex-col gap-1">
+                  Password
+                  <input
+                    type="password"
+                    name="password"
+                    class="rounded-sm bg-neutral-800 p-3"
+                    placeholder="Password"
+                    autocomplete="current-password"
+                    required
+                  />
+                </label>
+              </fieldset>
+              <input type="submit" value="Create account" class="btn-primary" />
+            </form>
+            <footer class="p-4">
+              Report any issues on{" "}
+              <a
+                class={`
+                  text-accent-500 underline
+                  hover:text-accent-400
+                `}
+                href="https://github.com/C4illin/ConvertX"
+              >
+                GitHub
+              </a>
+              .
+            </footer>
+          </article>
+        </main>
+      </BaseHtml>
+    );
+  })
+  .get("/register", ({ redirect }) => {
+    if (!ACCOUNT_REGISTRATION) {
+      return redirect(`${WEBROOT}/login`, 302);
+    }
+
+    return (
+      <BaseHtml webroot={WEBROOT} title="ConvertX | Register">
+        <>
+          <Header
+            webroot={WEBROOT}
+            accountRegistration={ACCOUNT_REGISTRATION}
+            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+            hideHistory={HIDE_HISTORY}
+          />
+          <main
+            class={`
+              w-full flex-1 px-2
+              sm:px-4
+            `}
+          >
+            <article class="article">
+              <form method="post" class="flex flex-col gap-4">
+                <fieldset class="mb-4 flex flex-col gap-4">
+                  <label class="flex flex-col gap-1">
+                    Email
+                    <input
+                      type="email"
+                      name="email"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Email"
+                      autocomplete="email"
+                      required
+                    />
+                  </label>
+                  <label class="flex flex-col gap-1">
+                    Password
+                    <input
+                      type="password"
+                      name="password"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Password"
+                      autocomplete="current-password"
+                      required
+                    />
+                  </label>
+                </fieldset>
+                <input
+                  type="submit"
+                  value="Register"
+                  class="w-full btn-primary"
+                />
+              </form>
+            </article>
+          </main>
+        </>
+      </BaseHtml>
+    );
+  })
+  .post(
+    "/register",
+    async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => {
+      if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      if (FIRST_RUN) {
+        FIRST_RUN = false;
+      }
+
+      const existingUser = await db
+        .query("SELECT * FROM users WHERE email = ?")
+        .get(email);
+      if (existingUser) {
+        set.status = 400;
+        return {
+          message: "Email already in use.",
+        };
+      }
+      const savedPassword = await Bun.password.hash(password);
+
+      db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(
+        email,
+        savedPassword,
+      );
+
+      const user = db
+        .query("SELECT * FROM users WHERE email = ?")
+        .as(User)
+        .get(email);
+
+      if (!user) {
+        set.status = 500;
+        return {
+          message: "Failed to create user.",
+        };
+      }
+
+      const accessToken = await jwt.sign({
+        id: String(user.id),
+      });
+
+      if (!auth) {
+        set.status = 500;
+        return {
+          message: "No auth cookie, perhaps your browser is blocking cookies.",
+        };
+      }
+
+      // set cookie
+      auth.set({
+        value: accessToken,
+        httpOnly: true,
+        secure: !HTTP_ALLOWED,
+        maxAge: 60 * 60 * 24 * 7,
+        sameSite: "strict",
+      });
+
+      return redirect(`${WEBROOT}/`, 302);
+    },
+    { body: 'signIn' },
+  )
+  .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
+    if (FIRST_RUN) {
+      return redirect(`${WEBROOT}/setup`, 302);
+    }
+
+    // if already logged in, redirect to home
+    if (auth?.value) {
+      const user = await jwt.verify(auth.value);
+
+      if (user) {
+        return redirect(`${WEBROOT}/`, 302);
+      }
+
+      auth.remove();
+    }
+
+    return (
+      <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
+        <>
+          <Header
+            webroot={WEBROOT}
+            accountRegistration={ACCOUNT_REGISTRATION}
+            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+            hideHistory={HIDE_HISTORY}
+          />
+          <main
+            class={`
+              w-full flex-1 px-2
+              sm:px-4
+            `}
+          >
+            <article class="article">
+              <form method="post" class="flex flex-col gap-4">
+                <fieldset class="mb-4 flex flex-col gap-4">
+                  <label class="flex flex-col gap-1">
+                    Email
+                    <input
+                      type="email"
+                      name="email"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Email"
+                      autocomplete="email"
+                      required
+                    />
+                  </label>
+                  <label class="flex flex-col gap-1">
+                    Password
+                    <input
+                      type="password"
+                      name="password"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Password"
+                      autocomplete="current-password"
+                      required
+                    />
+                  </label>
+                </fieldset>
+                <div class="flex flex-row gap-4">
+                  {ACCOUNT_REGISTRATION ? (
+                    <a
+                      href={`${WEBROOT}/register`}
+                      role="button"
+                      class="w-full btn-secondary text-center"
+                    >
+                      Register
+                    </a>
+                  ) : null}
+                  <input
+                    type="submit"
+                    value="Login"
+                    class="w-full btn-primary"
+                  />
+                </div>
+              </form>
+            </article>
+          </main>
+        </>
+      </BaseHtml>
+    );
+  })
+  .post(
+    "/login",
+    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
+      const existingUser = db
+        .query("SELECT * FROM users WHERE email = ?")
+        .as(User)
+        .get(body.email);
+
+      if (!existingUser) {
+        set.status = 403;
+        return {
+          message: "Invalid credentials.",
+        };
+      }
+
+      const validPassword = await Bun.password.verify(
+        body.password,
+        existingUser.password,
+      );
+
+      if (!validPassword) {
+        set.status = 403;
+        return {
+          message: "Invalid credentials.",
+        };
+      }
+
+      const accessToken = await jwt.sign({
+        id: String(existingUser.id),
+      });
+
+      if (!auth) {
+        set.status = 500;
+        return {
+          message: "No auth cookie, perhaps your browser is blocking cookies.",
+        };
+      }
+
+      // set cookie
+      auth.set({
+        value: accessToken,
+        httpOnly: true,
+        secure: !HTTP_ALLOWED,
+        maxAge: 60 * 60 * 24 * 7,
+        sameSite: "strict",
+      });
+
+      return redirect(`${WEBROOT}/`, 302);
+    },
+    { body: 'signIn' },
+  )
+  .get("/logoff", ({ redirect, cookie: { auth } }) => {
+    if (auth?.value) {
+      auth.remove();
+    }
+
+    return redirect(`${WEBROOT}/login`, 302);
+  })
+  .post("/logoff", ({ redirect, cookie: { auth } }) => {
+    if (auth?.value) {
+      auth.remove();
+    }
+
+    return redirect(`${WEBROOT}/login`, 302);
+  })
+  .get("/account", async ({ jwt, redirect, cookie: { auth } }) => {
+    if (!auth?.value) {
+      return redirect(`${WEBROOT}/`);
+    }
+    const user = await jwt.verify(auth.value);
+
+    if (!user) {
+      return redirect(`${WEBROOT}/`, 302);
+    }
+
+    const userData = db
+      .query("SELECT * FROM users WHERE id = ?")
+      .as(User)
+      .get(user.id);
+
+    if (!userData) {
+      return redirect(`${WEBROOT}/`, 302);
+    }
+
+    return (
+      <BaseHtml webroot={WEBROOT} title="ConvertX | Account">
+        <>
+          <Header
+            webroot={WEBROOT}
+            accountRegistration={ACCOUNT_REGISTRATION}
+            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
+            hideHistory={HIDE_HISTORY}
+            loggedIn
+          />
+          <main
+            class={`
+              w-full flex-1 px-2
+              sm:px-4
+            `}
+          >
+            <article class="article">
+              <form method="post" class="flex flex-col gap-4">
+                <fieldset class="mb-4 flex flex-col gap-4">
+                  <label class="flex flex-col gap-1">
+                    Email
+                    <input
+                      type="email"
+                      name="email"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Email"
+                      autocomplete="email"
+                      value={userData.email}
+                      required
+                    />
+                  </label>
+                  <label class="flex flex-col gap-1">
+                    Password (leave blank for unchanged)
+                    <input
+                      type="password"
+                      name="newPassword"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Password"
+                      autocomplete="new-password"
+                    />
+                  </label>
+                  <label class="flex flex-col gap-1">
+                    Current Password
+                    <input
+                      type="password"
+                      name="password"
+                      class="rounded-sm bg-neutral-800 p-3"
+                      placeholder="Password"
+                      autocomplete="current-password"
+                      required
+                    />
+                  </label>
+                </fieldset>
+                <div role="group">
+                  <input
+                    type="submit"
+                    value="Update"
+                    class="w-full btn-primary"
+                  />
+                </div>
+              </form>
+            </article>
+          </main>
+        </>
+      </BaseHtml>
+    );
+  })
+  .post(
+    "/account",
+    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
+      if (!auth?.value) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const user = await jwt.verify(auth.value);
+      if (!user) {
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+      const existingUser = db
+        .query("SELECT * FROM users WHERE id = ?")
+        .as(User)
+        .get(user.id);
+
+      if (!existingUser) {
+        if (auth?.value) {
+          auth.remove();
+        }
+        return redirect(`${WEBROOT}/login`, 302);
+      }
+
+      const validPassword = await Bun.password.verify(
+        body.password,
+        existingUser.password,
+      );
+
+      if (!validPassword) {
+        set.status = 403;
+        return {
+          message: "Invalid credentials.",
+        };
+      }
+
+      const fields = [];
+      const values = [];
+
+      if (body.email) {
+        const existingUser = await db
+          .query("SELECT id FROM users WHERE email = ?")
+          .as(User)
+          .get(body.email);
+        if (existingUser && existingUser.id.toString() !== user.id) {
+          set.status = 409;
+          return { message: "Email already in use." };
+        }
+        fields.push("email");
+        values.push(body.email);
+      }
+      if (body.newPassword) {
+        fields.push("password");
+        values.push(await Bun.password.hash(body.newPassword));
+      }
+
+      if (fields.length > 0) {
+        db.query(
+          `UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`,
+        ).run(...values, user.id);
+      }
+
+      return redirect(`${WEBROOT}/`, 302);
+    },
+    {
+      body: t.Object({
+        email: t.MaybeEmpty(t.String()),
+        newPassword: t.MaybeEmpty(t.String()),
+        password: t.String(),
+      }),
+    },
+  )