update eslint and refactor backend

This commit is contained in:
2026-01-04 12:37:46 +01:00
parent 43824ce284
commit 090b3c10d1
13 changed files with 438 additions and 152 deletions

View File

@@ -9,24 +9,19 @@
"@fontsource/roboto": "5.2.8", "@fontsource/roboto": "5.2.8",
"@mui/icons-material": "7.3.5", "@mui/icons-material": "7.3.5",
"@mui/material": "7.3.5", "@mui/material": "7.3.5",
"cookie": "^1.0.2",
"mobx-react-lite": "^4.1.1",
"react": "19", "react": "19",
"react-dom": "19", "react-dom": "19",
"react-router-dom": "^7.9.5", "react-router-dom": "^7.9.5",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.1", "@sebastianbrenner/eslint-config": "git+https://gitea.sebastianbrenner.dev/sebastianbrenner/eslint-config.git/#1.2.0",
"@stylistic/eslint-plugin": "5.5.0",
"@types/bun": "1.3.1", "@types/bun": "1.3.1",
"@types/react": "19", "@types/react": "19",
"@types/react-dom": "19", "@types/react-dom": "19",
"eslint": "9.39.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-unused-imports": "^4.3.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.46.3",
}, },
}, },
}, },
@@ -153,6 +148,10 @@
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@sebastianbrenner/eslint-config": ["@sebastianbrenner/eslint-config@git+https://gitea.sebastianbrenner.dev/sebastianbrenner/eslint-config.git/#98e16522c315347de4203ffcbe0fff554f22c370", { "peerDependencies": { "@stylistic/eslint-plugin": "1.7.0", "@typescript-eslint/parser": "8.48.1", "eslint": "^9.0.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-unused-imports": "4.3.0", "typescript": "5" } }, "98e16522c315347de4203ffcbe0fff554f22c370"],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.46.1", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw=="], "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.46.1", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
@@ -161,6 +160,8 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
@@ -211,6 +212,8 @@
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
@@ -263,7 +266,7 @@
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
@@ -323,6 +326,12 @@
"eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "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.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.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-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "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.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.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-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
@@ -495,7 +504,7 @@
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
@@ -525,6 +534,12 @@
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mobx": ["mobx@6.15.0", "", {}, "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g=="],
"mobx-react-lite": ["mobx-react-lite@4.1.1", "", { "dependencies": { "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "mobx": "^6.9.0", "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -543,6 +558,8 @@
"object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
"object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"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=="], "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=="],
@@ -651,6 +668,8 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
@@ -663,6 +682,8 @@
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
@@ -675,8 +696,6 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.46.3", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.3", "@typescript-eslint/parser": "8.46.3", "@typescript-eslint/typescript-estree": "8.46.3", "@typescript-eslint/utils": "8.46.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
@@ -685,6 +704,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@@ -707,7 +728,9 @@
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -719,6 +742,14 @@
"babel-plugin-macros/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "babel-plugin-macros/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-import-resolver-node/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],

BIN
data.sqlite Normal file

Binary file not shown.

View File

@@ -1,54 +1,22 @@
// eslint.config.js // eslint.config.js
import js from '@eslint/js'; import config from '@sebastianbrenner/eslint-config/src/index.js';
import stylistic from '@stylistic/eslint-plugin';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
export default [ export default [
js.configs.recommended, ...config,
...tseslint.configs.recommended,
{
plugins: {
react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y,
'@stylistic': stylistic,
'unused-imports': unusedImports,
},
rules: {
// React 19 JSX transform requires no React in scope
'react/react-in-jsx-scope': 'off',
// Hooks rules
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Accessibility
'jsx-a11y/alt-text': 'warn',
// Style rules (using @stylistic)
'@stylistic/indent': ['error', 4],
'@stylistic/semi': ['error', 'always'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/no-multiple-empty-lines': 'error',
'unused-imports/no-unused-imports': 'error',
},
settings: {
react: {
version: 'detect',
},
},
},
{ {
ignores: [ ignores: [
'dist', 'dist',
'build', 'build',
'node_modules', 'node_modules',
], ],
rules: {
"react/react-in-jsx-scope": "off",
"react/jsx-newline": ["error", { "prevent": true }],
"@typescript-eslint/no-unsafe-assignment": "off",
"indent": ["error", 4]
},
settings: {
'import/core-modules': ['bun' ]
}
}, },
]; ];

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "bun --hot src/index.ts", "dev": "bun --hot src/index.ts",
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
"start": "NODE_ENV=production bun src/index.ts" "start": "NODE_ENV=production bun src/index.ts",
"eslint-quiet": "eslint 'src/**/*.{ts,tsx}' --quiet"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
@@ -14,23 +15,18 @@
"@fontsource/roboto": "5.2.8", "@fontsource/roboto": "5.2.8",
"@mui/icons-material": "7.3.5", "@mui/icons-material": "7.3.5",
"@mui/material": "7.3.5", "@mui/material": "7.3.5",
"cookie": "^1.0.2",
"mobx-react-lite": "^4.1.1",
"react": "19", "react": "19",
"react-dom": "19", "react-dom": "19",
"react-router-dom": "^7.9.5" "react-router-dom": "^7.9.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.1",
"@stylistic/eslint-plugin": "5.5.0",
"@types/bun": "1.3.1", "@types/bun": "1.3.1",
"@types/react": "19", "@types/react": "19",
"@types/react-dom": "19", "@types/react-dom": "19",
"eslint": "9.39.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-unused-imports": "^4.3.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.46.3" "@sebastianbrenner/eslint-config": "git+https://gitea.sebastianbrenner.dev/sebastianbrenner/eslint-config.git/#1.2.0"
} }
} }

View File

@@ -1,20 +1,29 @@
import SignIn from './components/SignIn'; import SignIn from './components/SignIn';
import { createTheme, ThemeProvider } from '@mui/material'; import { createTheme, ThemeProvider } from '@mui/material';
import { useStore } from './Store';
import Group from './components/Group';
import { observer } from 'mobx-react-lite';
const App = () => { const App = observer(() => {
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
mode: 'dark', mode: 'dark',
}, },
}); });
const store = useStore();
const { loggedIn } = store;
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<div className="app"> <div className="app">
<SignIn /> {loggedIn ?
<Group /> :
<SignIn />
}
</div> </div>
</ThemeProvider> </ThemeProvider>
); );
}; });
export default App; export default App;

39
src/client/Store.ts Normal file
View File

@@ -0,0 +1,39 @@
import { createContext, useContext } from 'react';
import { makeAutoObservable } from 'mobx';
import cookie from 'cookie';
import type { Group, User } from '@/interfaces';
export type CookieData = {
user: User;
group: Group;
};
export class Store {
cookieData: CookieData | null = null;
processCookie() {
const parsed = cookie.parse(document.cookie);
this.cookieData = {
user: parsed.user ? JSON.parse(parsed.user) : null,
group: parsed.group ? JSON.parse(parsed.group) : null,
} as CookieData;
}
get loggedIn(): boolean {
if (this.cookieData === null) {
return false;
};
return !!this.cookieData.user && !!this.cookieData.group;
}
constructor() {
console.log('Initializing Store');
this.processCookie();
makeAutoObservable(this);
}
}
export const StoreContext = createContext<Store>(new Store());
export const useStore = (): Store => useContext(StoreContext);

View File

@@ -0,0 +1,64 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
import Card from './Card';
import { useStore } from '../Store';
const Group = () => {
const [loading, setLoading] = React.useState(false);
const theme = useTheme();
const store = useStore();
const { group } = store.cookieData!;
console.log('Group component rendered with group:', group.name);
return (
<div style={{
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
backgroundRepeat: 'no-repeat',
}}>
<CssBaseline enableColorScheme />
<Stack direction="column" justifyContent="space-between"
sx={{
height: 'calc((1 - var(--template-frame-height, 0)) * 100dvh)',
minHeight: '100%',
padding: theme.spacing(2),
[theme.breakpoints.up('sm')]: {
padding: theme.spacing(4),
},
}}
>
<Card variant="outlined">
<Typography
component="h1"
variant="h4"
sx={{ width: '100%', fontSize: 'clamp(2rem, 10vw, 2.15rem)' }}
>
{group.name}
</Typography>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: 2,
}}
>
<Typography>
{'Willkommen in der Gruppe!'}
</Typography>
<Typography>
{'Admin: ' + group.mail}
</Typography>
</Box>
</Card>
</Stack>
</div>
);
};
export default Group;

View File

@@ -13,65 +13,88 @@ import { useTheme } from '@mui/material/styles';
import GroupAddIcon from '@mui/icons-material/GroupAdd'; import GroupAddIcon from '@mui/icons-material/GroupAdd';
import ForgotPassword from './ForgotPassword'; import ForgotPassword from './ForgotPassword';
import Card from './Card'; import Card from './Card';
import { createUser, fetchGroupByCode, fetchUser } from '../serverApi';
import { observer } from 'mobx-react-lite';
const SignIn = () => { const SignIn = observer(() => {
const [email, setEmail] = React.useState(''); const [email, setEmail] = React.useState('');
const [emailError, setEmailError] = React.useState(false); const [emailError, setEmailError] = React.useState('');
const [group, setGroup] = React.useState(''); const [group, setGroup] = React.useState('');
const [groupError, setGroupError] = React.useState(false); const [groupError, setGroupError] = React.useState('');
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const theme = useTheme(); const theme = useTheme();
React.useEffect(() => {
const params = new URLSearchParams(window.location.search);
const groupParam = params.get('gruppe');
if (groupParam) {
setGroup(groupParam);
}
}, []);
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true); setOpen(true);
}; };
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleEnterGroup = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if ((emailError || email === '')) { event.preventDefault();
event.preventDefault();
setEmailError(true); if (!isMailValid() || !isGroupValid()) return;
return;
} setLoading(true);
try {
const groupData = await fetchGroupByCode(group);
if (!groupData) {
setGroupError('Gruppencode existiert nicht.');
return;
}
let user = await fetchUser(email);
if (!user) {
user = await createUser(email);
if (!user) throw new Error('Error creating user');
}
await cookieStore.set('user', JSON.stringify(user));
await cookieStore.set('group', JSON.stringify(groupData));
if ((groupError || group === '')) {
event.preventDefault();
setGroupError(true);
return;
} }
catch (error) {
console.error('Error during sign-in process:', error);
} finally {
setLoading(false); }
}; };
const validateEmail = (): boolean => { const isMailValid = (): boolean => {
let isValid = true; let isValid = true;
if (email !== '' && !/\S+@\S+\.\S+/.test(email)) { if (!/\S+@\S+\.\S+/.test(email)) {
setEmailError(true);; setEmailError('Bitte gib eine gültige E-Mail Adresse ein.');
isValid = false; isValid = false;
} else { } else {
setEmailError(false); setEmailError('');
} }
return isValid; return isValid;
}; };
const validateGroup = (): boolean => { const isGroupValid = (): boolean => {
let isValid = true; let isValid = true;
if (group !== '' && !/\S+@\S+\.\S+/.test(email)) { if (!/^[A-Z0-9]{6}$/.test(group)) {
setGroupError(true); setGroupError('Bitte gib einen gültigen Gruppencode ein.');
isValid = false; isValid = false;
} else { } else {
setGroupError(false); setGroupError('');
} }
return isValid; return isValid;
}; };
const validateInputs = () => {
validateEmail();
validateGroup();
};
return ( return (
<div style={{ <div style={{
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))', backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
@@ -96,11 +119,7 @@ const SignIn = () => {
> >
{'Gruppe beitreten'} {'Gruppe beitreten'}
</Typography> </Typography>
<Box <Box
component="form"
onSubmit={handleSubmit}
noValidate
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -110,10 +129,9 @@ const SignIn = () => {
> >
<FormControl> <FormControl>
<FormLabel htmlFor="email">E-Mail</FormLabel> <FormLabel htmlFor="email">E-Mail</FormLabel>
<TextField <TextField
error={emailError} error={emailError !== ''}
helperText={emailError && 'Bitte gib eine gültige E-Mail Adresse ein.'} helperText={emailError}
id="email" id="email"
type="email" type="email"
name="email" name="email"
@@ -123,15 +141,16 @@ const SignIn = () => {
required required
fullWidth fullWidth
variant="outlined" variant="outlined"
onBlur={validateEmail} onBlur={isMailValid}
onChange={event => setEmail(event.target.value)} onChange={event => setEmail(event.target.value)}
value={email}
color={emailError ? 'error' : 'primary'} /> color={emailError ? 'error' : 'primary'} />
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel htmlFor="password">{'Gruppencode'}</FormLabel> <FormLabel htmlFor="password">{'Gruppencode'}</FormLabel>
<TextField <TextField
error={groupError} error={groupError !== ''}
helperText={groupError && 'Bitte gib einen gültigen Gruppencode ein.'} helperText={groupError}
name="groupcode" name="groupcode"
placeholder="Gruppe123" placeholder="Gruppe123"
type="text" type="text"
@@ -140,22 +159,20 @@ const SignIn = () => {
required required
fullWidth fullWidth
variant="outlined" variant="outlined"
onBlur={validateGroup} onBlur={isGroupValid}
onChange={event => setGroup(event.target.value)} onChange={event => setGroup(event.target.value)}
value={group}
color={groupError ? 'error' : 'primary'} /> color={groupError ? 'error' : 'primary'} />
</FormControl> </FormControl>
<ForgotPassword open={open} handleClose={handleClose} /> <ForgotPassword open={open} handleClose={handleClose} />
<Button <Button
type="submit"
fullWidth fullWidth
variant="contained" variant="contained"
onClick={validateInputs} onClick={event => {void handleEnterGroup(event)}}
loading={loading}
> >
{'Beitreten'} {'Beitreten'}
</Button> </Button>
<Link <Link
component="button" component="button"
type="button" type="button"
@@ -166,9 +183,7 @@ const SignIn = () => {
{'Gruppencode vergessen?'} {'Gruppencode vergessen?'}
</Link> </Link>
</Box> </Box>
<Divider>oder</Divider> <Divider>oder</Divider>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Button <Button
fullWidth fullWidth
@@ -183,6 +198,6 @@ const SignIn = () => {
</Stack> </Stack>
</div> </div>
); );
}; });
export default SignIn; export default SignIn;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/** /**
* This file is the entry point for the React app, it sets up the root * This file is the entry point for the React app, it sets up the root
* element and renders the App component to the DOM. * element and renders the App component to the DOM.
@@ -6,19 +7,25 @@
*/ */
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot, type Root } from 'react-dom/client';
import App from './App'; import App from './App';
import { Store, StoreContext } from './Store';
const elem = document.getElementById('root')!; const elem = document.getElementById('root')!;
const store = new Store();
const app = ( const app = (
<StrictMode> <StrictMode>
<App /> <StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>
</StrictMode> </StrictMode>
); );
if (import.meta.hot) { if (import.meta.hot) {
// With hot module reloading, `import.meta.hot.data` is persisted. // With hot module reloading, `import.meta.hot.data` is persisted.
const root = (import.meta.hot.data.root ??= createRoot(elem)); const root: Root = (import.meta.hot.data.root ??= createRoot(elem));
root.render(app); root.render(app);
} else { } else {
// The hot module reloading API is not available in production. // The hot module reloading API is not available in production.

View File

@@ -1,8 +1,38 @@
import type { Group } from '@/interfaces'; import type { Group, User } from '@/interfaces';
export async function fetchGroup(id: string): Promise<Group | null> { export async function fetchUser(mail: string): Promise<User | null> {
try { try {
const res = await fetch(`/api/group/${id}`); const res = await fetch(`/api/user/${mail}`);
if (!res.ok) return null;
const data: User = await res.json();
return data;
} catch (err) {
console.error('Failed to fetch user:', err);
return null;
}
}
export async function createUser(mail: string): Promise<User | null> {
try {
const res = await fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ mail }),
});
if (!res.ok) return null;
const data: User = await res.json();
return data;
} catch (err) {
console.error('Failed to create user:', err);
return null;
}
}
export async function fetchGroupByCode(code: string): Promise<Group | null> {
try {
const res = await fetch(`/api/group/${code}`);
if (!res.ok) return null; if (!res.ok) return null;
const data: Group = await res.json(); const data: Group = await res.json();
return data; return data;

View File

@@ -2,31 +2,66 @@ import { serve } from 'bun';
import index from './index.html'; import index from './index.html';
import DB from './server/db'; import DB from './server/db';
const db = new DB();
const server = serve({ const server = serve({
routes: { routes: {
// Serve index.html for all unmatched routes. // Serve index.html for all unmatched routes.
'/*': index, '/*': index,
'/api/group/:id': async req => { '/api/user/:mail': async req => {
const id = req.params.id; const mail = req.params.mail;
const group = await new DB().getGroup(id); console.log('Received request for user:', mail);
console.log('Fetching group with ID:', id, 'Result:', group); const user = await db.getUserByMail(mail);
if (!group) return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 }); console.log('Fetching user with mail:', mail, 'Result:', user);
if (!user) return new Response(JSON.stringify({ error: 'User not found' }), { status: 404 });
return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' } });
},
'/api/user': {
async POST(req) {
try {
const { mail } = await req.json() as { mail: string };
console.log('Received request to create user with mail:', mail);
const user = await db.createUser(mail);
return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' } });
} catch (err) {
console.error('Error creating user:', err);
return new Response(JSON.stringify({ error: 'Failed to create user' }), { status: 500 });
}
},
},
'/api/group/:code': async req => {
const code = req.params.code;
console.log('Received request for group:', code);
const group = await db.getGroupByCode(code);
console.log('Fetching group with ID:', code, 'Result:', group);
if (!group) return new Response(JSON.stringify({ error: 'Group not found' }), { status: 404 });
return new Response(JSON.stringify(group), { headers: { 'Content-Type': 'application/json' } }); return new Response(JSON.stringify(group), { headers: { 'Content-Type': 'application/json' } });
}, },
'/api/group': { '/api/group': {
async POST(req) { async POST(req) {
try { try {
const group = (await req.formData() as FormData).get('code') as string; const { name, mail } = await req.json() as { name: string; mail: string };
const response = await new DB().createGroup(group); console.log('Received request to create group with name:', name, 'and mail:', mail);
const response = db.createGroup({ name, mail });
return new Response(JSON.stringify(response), { headers: { 'Content-Type': 'application/json' } }); return new Response(JSON.stringify(response), { headers: { 'Content-Type': 'application/json' } });
} catch (err) { } catch (err) {
console.error('Error creating group:', err); console.error('Error creating group:', err);
return new Response(JSON.stringify({ error: 'Failed to create group' }), { status: 500 }); return new Response(JSON.stringify({ error: 'Failed to create group' }), { status: 500 });
} }
} }
} },
'/api/group_members/:groupId': async req => {
const groupId = parseInt(req.params.groupId, 10);
console.log('Received request for group members of group ID:', groupId);
const members = await db.getGroupMembers(groupId);
console.log('Fetching members for group ID:', groupId, 'Result:', members);
return new Response(JSON.stringify(members), { headers: { 'Content-Type': 'application/json' } });
},
}, },
development: process.env.NODE_ENV !== 'production' && { development: process.env.NODE_ENV !== 'production' && {

View File

@@ -1,5 +1,16 @@
export type Group = { export type Group = {
id: number; id: number;
code: string; code: string;
name: string;
mail: string;
image: Uint8Array | null;
phase: string;
created_at: string;
};
export type User = {
id: number;
mail: string;
image: Uint8Array | null;
created_at: string; created_at: string;
}; };

View File

@@ -1,14 +1,16 @@
import { Database } from 'bun:sqlite'; import type { Group, User } from '@/interfaces';
import { SQL } from 'bun';
// create tables if they don't exist // create tables if they don't exist
const createTableScript = ` const createTableScript = `
-- GROUP TRABLE -- -- GROUP TRABLE --
CREATE TABLE IF NOT EXISTS group ( CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL, code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
mail TEXT NOT NULL, mail TEXT NOT NULL,
image BLOB, image BLOB,
phase TEXT DEFAULT 'gathering',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
@@ -17,20 +19,21 @@ const createTableScript = `
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
mail TEXT NOT NULL, mail TEXT NOT NULL,
image BLOB, image BLOB,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
-- GROUP_MEMBER TABLE -- -- MEMBERSHIP TABLE --
CREATE TABLE IF NOT EXISTS group_member ( CREATE TABLE IF NOT EXISTS membership (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER, group_id INTEGER,
user_id INTEGER, user_id INTEGER,
UNIQUE(email, group_id), UNIQUE(user_id, group_id),
FOREIGN KEY(group_id) REFERENCES groups(id), FOREIGN KEY(group_id) REFERENCES groups(id),
FOREIGN KEY(user_id) REFERENCES users(id) FOREIGN KEY(user_id) REFERENCES users(id)
); );
-- BAN TABLE -- -- BAN TABLE --
CREATE TABLE IF NOT EXISTS ban ( CREATE TABLE IF NOT EXISTS bans (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER, group_id INTEGER,
user_id INTEGER, user_id INTEGER,
@@ -38,7 +41,7 @@ const createTableScript = `
FOREIGN KEY(group_id) REFERENCES groups(id), FOREIGN KEY(group_id) REFERENCES groups(id),
FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(user_id2) REFERENCES users(id) FOREIGN KEY(user_id2) REFERENCES users(id)
UNIQUE(group_id, user_id, user_id2), UNIQUE(group_id, user_id, user_id2)
); );
-- WHISHLIST TABLE -- -- WHISHLIST TABLE --
@@ -53,32 +56,110 @@ const createTableScript = `
`; `;
class DB { class DB {
private instance: Database = new Database(); private instance: SQL;
private prepareDB () { private executeScript(script: string, name: string) {
this.instance.run(createTableScript); try {
void this.instance`${script}`;
} catch (err) {
console.error(`error executing script ${name}: ${err as Error}`);
}
} }
public async getGroup(id: string) { /**
const stmt = this.instance.prepare('SELECT * FROM groups WHERE code = ?'); * Prepare the database by creating necessary tables.
const group = await stmt.get(id); */
private prepareDB () {
this.executeScript(createTableScript, 'createTableScript');
}
/* USERS */
/**
* Create a new user
* @param mail: string
* @returns created user
*/
public async createUser(mail: string): Promise<User> {
const user: User = await this.instance`
INSERT INTO users (mail) VALUES (${mail})
RETURNING *
`;
return user;
}
/**
* Get user by mail
* @param mail: string
* @returns user object or undefined
*/
public async getUserByMail(mail: string): Promise<User | undefined> {
const user: User = await this.instance`
SELECT * FROM users WHERE mail = ${mail}
`;
return user;
}
/* GROUPS */
/**
* Create a new group
* @param name: string
* @param mail: string
* @returns object with id of the created group
*/
public async createGroup( {name, mail}: {name: string, mail: string}): Promise<Group> {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
const group: Group = await this.instance`
INSERT INTO groups (code, name, mail) VALUES (${code}, ${name}, ${mail})
RETURNING *
`;
return group;
};
/**
* Get group by ID
* @param id: string
* @returns group object or undefined
*/
public async getGroupByCode(code: string): Promise<Group | undefined> {
const group: Group | undefined = await this.instance`
SELECT * FROM groups WHERE code = ${code}
`;
return group; return group;
} }
public createGroup(code: string): { id: number | bigint} { /* GROUP MEMBER */
const stmt = this.instance.prepare('INSERT INTO groups (code) VALUES (?)');
const changes = stmt.run(code);
console.log('Inserted group with ID:', changes);
return { id: changes.lastInsertRowid };
};
/**
* Add user to group
* @param userId: number
* @param groupId: number
* @returns object with id of the created group member entry
*/
public async UserEntersGroup(userId: number, groupId: number): Promise<{ id: number | bigint; }> {
const membership: { id: number | bigint} = await this.instance`
INSERT OR IGNORE INTO membership (user_id, group_id) VALUES (${userId}, ${groupId})
RETURNING *
`;
return membership;
}
public async getGroupMembers(groupId: number): Promise<User[]> {
const users: User[] = await this.instance`
SELECT *
FROM users u
JOIN membership m ON u.id = m.user_id
WHERE m.group_id = ${groupId}
`;
return users;
}
constructor(url: string = './data.sqlite') { constructor(url: string = './data.sqlite') {
if (this.instance) { this.instance = new SQL(url, {adapter: 'sqlite'});
this.instance = new Database(url); this.prepareDB();
this.prepareDB(); console.log('Database initialized at', url);
console.log('Database initialized at', url);
}
} }
} }