diff --git a/backend/package-lock.json b/backend/package-lock.json index d43c787..f8fd2de 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,7 +14,483 @@ "multer": "^2.0.2", "mysql2": "^3.6.0", "passport": "^0.6.0", - "passport-discord": "^0.1.4" + "passport-discord": "^0.1.4", + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/accepts": { @@ -240,6 +716,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -976,6 +1461,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -1032,6 +1529,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1148,6 +1689,13 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/backend/package.json b/backend/package.json index ca183bc..1b5f7d5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,8 @@ "multer": "^2.0.2", "mysql2": "^3.6.0", "passport": "^0.6.0", - "passport-discord": "^0.1.4" + "passport-discord": "^0.1.4", + "sharp": "^0.34.5", + "express-mysql-session": "^3.0.0" } } diff --git a/backend/server.js b/backend/server.js index 35d4d6c..26759d9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,5 +1,6 @@ const express = require('express'); const session = require('express-session'); +const MySQLStore = require('express-mysql-session')(session); const passport = require('passport'); const DiscordStrategy = require('passport-discord').Strategy; const cors = require('cors'); @@ -58,14 +59,41 @@ const upload = multer({ init(); // Initialize DB +// Configure MySQL session store +const sessionStore = new MySQLStore({ + host: process.env.DB_HOST || 'db', + port: 3306, + user: process.env.DB_USER || 'obsidian_user', + password: process.env.DB_PASS || 'obsidian_pass', + database: process.env.DB_NAME || 'obsidian_db', + clearExpired: true, + checkExpirationInterval: 900000, // Clean expired sessions every 15 minutes + expiration: 86400000 * 30, // 30 days default + createDatabaseTable: true, // Auto-create sessions table + schema: { + tableName: 'sessions', + columnNames: { + session_id: 'session_id', + expires: 'expires', + data: 'data' + } + } +}); + // Middleware app.use(express.json()); app.use(cors({ origin: corsOrigins, credentials: true })); app.use(session({ secret: process.env.SESSION_SECRET || 'dev_secret', + store: sessionStore, resave: false, saveUninitialized: false, - cookie: { secure: false } // Set true if using https + cookie: { + secure: false, // Set true if using https + maxAge: 24 * 60 * 60 * 1000, // 24 hours default + httpOnly: true, + sameSite: 'lax' + } })); app.use(passport.initialize()); app.use(passport.session()); @@ -129,11 +157,34 @@ app.get('/api/status', (req, res) => { // --- ROUTES --- // Auth -app.get('/auth/discord', passport.authenticate('discord')); +app.get('/auth/discord', (req, res, next) => { + // Check if user wants to be remembered (longer session) + const rememberMe = req.query.remember_me === 'true'; + req.session.rememberMe = rememberMe; + + // Set session maxAge based on remember me preference + if (rememberMe) { + req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days + req.session.cookie.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); + } else { + req.session.cookie.maxAge = 24 * 60 * 60 * 1000; // 24 hours + req.session.cookie.expires = new Date(Date.now() + 24 * 60 * 60 * 1000); + } + + passport.authenticate('discord')(req, res, next); +}); + app.get('/auth/discord/callback', passport.authenticate('discord', { failureRedirect: FRONTEND_URL + '?error=login_failed' }), (req, res) => { - res.redirect(FRONTEND_URL); + // Ensure session is saved with correct maxAge + if (req.session.rememberMe) { + req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days + req.session.cookie.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); + } + req.session.save(() => { + res.redirect(FRONTEND_URL); + }); }); app.get('/auth/me', (req, res) => { @@ -494,6 +545,282 @@ app.post('/api/admin/npc-company', (req, res) => { ); }); +// Upload NPC Company Banner +app.post('/api/admin/npc-companies/:projectId/banner/upload', upload.single('banner'), async (req, res) => { + if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); + + // Check if user is admin + if (!req.user.isAdmin) { + return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + } + + const { projectId } = req.params; + + // Verify this is an NPC company + if (!projectId.startsWith('npc_proj_')) { + return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'}); + } + + // Check if NPC company exists + db.get("SELECT title FROM projects WHERE id = ?", [projectId], (err, row) => { + if (err) return res.status(500).json({error: err.message}); + if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'}); + + if (!req.file) { + return res.status(400).json({error: 'Keine Datei hochgeladen'}); + } + + // Generate unique image ID + const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + // Convert to WebP + const originalPath = path.join(UPLOAD_DIR, req.file.filename); + const webpFilename = imageId + '.webp'; + const webpPath = path.join(UPLOAD_DIR, webpFilename); + + convertToWebP(originalPath, webpPath).then((webpSize) => { + // Clean up original file + fs.unlinkSync(originalPath); + + // Create image record with WebP data + const imageData = { + id: imageId, + filename: webpFilename, + originalName: req.file.originalname, + mimeType: 'image/webp', + size: webpSize, + uploadDate: new Date().toISOString(), + altText: req.body.altText || `${row.title} Banner`, + entityType: 'project', + entityId: projectId, + imageType: 'banner' + }; + + db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size, + imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType], + function(err) { + if (err) { + console.error('Error creating NPC company banner image record:', err); + return res.status(500).json({error: 'Fehler beim Speichern des Banners'}); + } + + // Update NPC company banner + db.run("UPDATE projects SET bannerImageId = ? WHERE id = ?", [imageId, projectId], function(err) { + if (err) { + console.error('Error updating NPC company banner:', err); + return res.status(500).json({error: 'Fehler beim Aktualisieren des Banners'}); + } + res.json({ + success: true, + imageId: imageId, + imageUrl: `${BACKEND_URL}/api/images/${imageId}`, + message: 'Banner erfolgreich hochgeladen' + }); + }); + }); + }).catch((error) => { + console.error('Error converting NPC company banner to WebP:', error); + // Clean up files on error + try { + if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath); + if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath); + } catch (cleanupErr) { + console.error('Error cleaning up files:', cleanupErr); + } + res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'}); + }); + }); +}); + +// Upload NPC Company Logo +app.post('/api/admin/npc-companies/:projectId/logo/upload', upload.single('logo'), async (req, res) => { + if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); + + // Check if user is admin + if (!req.user.isAdmin) { + return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + } + + const { projectId } = req.params; + + // Verify this is an NPC company + if (!projectId.startsWith('npc_proj_')) { + return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'}); + } + + // Check if NPC company exists + db.get("SELECT title FROM projects WHERE id = ?", [projectId], (err, row) => { + if (err) return res.status(500).json({error: err.message}); + if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'}); + + if (!req.file) { + return res.status(400).json({error: 'Keine Datei hochgeladen'}); + } + + // Generate unique image ID + const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + // Convert to WebP + const originalPath = path.join(UPLOAD_DIR, req.file.filename); + const webpFilename = imageId + '.webp'; + const webpPath = path.join(UPLOAD_DIR, webpFilename); + + convertToWebP(originalPath, webpPath).then((webpSize) => { + // Clean up original file + fs.unlinkSync(originalPath); + + // Create image record with WebP data + const imageData = { + id: imageId, + filename: webpFilename, + originalName: req.file.originalname, + mimeType: 'image/webp', + size: webpSize, + uploadDate: new Date().toISOString(), + altText: req.body.altText || `${row.title} Logo`, + entityType: 'project', + entityId: projectId, + imageType: 'logo' + }; + + db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size, + imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType], + function(err) { + if (err) { + console.error('Error creating NPC company logo image record:', err); + return res.status(500).json({error: 'Fehler beim Speichern des Logos'}); + } + + // Update NPC company logo + db.run("UPDATE projects SET logoImageId = ? WHERE id = ?", [imageId, projectId], function(err) { + if (err) { + console.error('Error updating NPC company logo:', err); + return res.status(500).json({error: 'Fehler beim Aktualisieren des Logos'}); + } + res.json({ + success: true, + imageId: imageId, + logoUrl: `${BACKEND_URL}/api/images/${imageId}`, + message: 'Logo erfolgreich hochgeladen' + }); + }); + }); + }).catch((error) => { + console.error('Error converting NPC company logo to WebP:', error); + // Clean up files on error + try { + if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath); + if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath); + } catch (cleanupErr) { + console.error('Error cleaning up files:', cleanupErr); + } + res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'}); + }); + }); +}); + +// Upload NPC Company Gallery Image +app.post('/api/admin/npc-companies/:projectId/gallery/upload', upload.single('gallery'), async (req, res) => { + if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); + + // Check if user is admin + if (!req.user.isAdmin) { + return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + } + + const { projectId } = req.params; + + // Verify this is an NPC company + if (!projectId.startsWith('npc_proj_')) { + return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'}); + } + + // Check if NPC company exists + db.get("SELECT title, gallery FROM projects WHERE id = ?", [projectId], (err, row) => { + if (err) return res.status(500).json({error: err.message}); + if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'}); + + if (!req.file) { + return res.status(400).json({error: 'Keine Datei hochgeladen'}); + } + + // Generate unique image ID + const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + // Convert to WebP + const originalPath = path.join(UPLOAD_DIR, req.file.filename); + const webpFilename = imageId + '.webp'; + const webpPath = path.join(UPLOAD_DIR, webpFilename); + + convertToWebP(originalPath, webpPath).then((webpSize) => { + // Clean up original file + fs.unlinkSync(originalPath); + + // Create image record with WebP data + const imageData = { + id: imageId, + filename: webpFilename, + originalName: req.file.originalname, + mimeType: 'image/webp', + size: webpSize, + uploadDate: new Date().toISOString(), + altText: req.body.altText || `${row.title} Portfolio`, + entityType: 'project', + entityId: projectId, + imageType: 'gallery' + }; + + db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size, + imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType], + function(err) { + if (err) { + console.error('Error creating NPC company gallery image record:', err); + return res.status(500).json({error: 'Fehler beim Speichern des Bildes'}); + } + + try { + // Add image ID to gallery array + const gallery = JSON.parse(row.gallery || '[]'); + gallery.push(imageId); + + db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) { + if (err) { + console.error('Error updating NPC company gallery:', err); + return res.status(500).json({error: 'Fehler beim Aktualisieren der Galerie'}); + } + res.json({ + success: true, + imageId: imageId, + imageUrl: `${BACKEND_URL}/api/images/${imageId}`, + gallery: gallery, + message: 'Bild erfolgreich zum Portfolio hinzugefügt' + }); + }); + } catch (e) { + console.error('Error parsing NPC company gallery:', e); + res.status(500).json({error: 'Fehler beim Verarbeiten der Galerie-Daten'}); + } + }); + }).catch((error) => { + console.error('Error converting NPC company gallery image to WebP:', error); + // Clean up files on error + try { + if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath); + if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath); + } catch (cleanupErr) { + console.error('Error cleaning up files:', cleanupErr); + } + res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'}); + }); + }); +}); + // Grant admin rights to a player (Admin only) app.post('/api/admin/grant-admin/:uuid', (req, res) => { if (!req.isAuthenticated()) return res.status(401).send(); @@ -1019,7 +1346,7 @@ app.get('/api/images/:imageId/meta', (req, res) => { }); // Upload banner image -app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req, res) => { +app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); const { projectId } = req.params; @@ -1081,7 +1408,7 @@ app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req }); // Upload gallery image -app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (req, res) => { +app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); const { projectId } = req.params; @@ -1152,7 +1479,7 @@ app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (r }); // Upload logo image -app.post('/api/projects/:projectId/logo/upload', upload.single('logo'), (req, res) => { +app.post('/api/projects/:projectId/logo/upload', upload.single('logo'), async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); const { projectId } = req.params; @@ -1344,7 +1671,7 @@ app.delete('/api/admin/cities/:cityId', (req, res) => { }); // Upload city banner -app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (req, res) => { +app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); // Check if user is admin @@ -1408,7 +1735,7 @@ app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (re }); // Upload city logo -app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), (req, res) => { +app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); // Check if user is admin @@ -1472,7 +1799,7 @@ app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), (req, r }); // Upload city gallery image -app.post('/api/admin/cities/:cityId/gallery/upload', upload.single('gallery'), (req, res) => { +app.post('/api/admin/cities/:cityId/gallery/upload', upload.single('gallery'), async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'}); // Check if user is admin diff --git a/components/Layout.tsx b/components/Layout.tsx index b25e7fe..a04db23 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -34,6 +34,10 @@ const NavItem = ({ const Layout: React.FC = ({ children, activeTab, onNavigate }) => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [user, setUser] = useState(null); + const [rememberMe, setRememberMe] = useState(() => { + // Load remember me preference from localStorage + return localStorage.getItem('rememberMe') === 'true'; + }); useEffect(() => { // Subscribe to auth changes @@ -151,13 +155,28 @@ const Layout: React.FC = ({ children, activeTab, onNavigate }) => { ) : ( - +
+ + +
)} diff --git a/components/NpcBannerManagementModal.tsx b/components/NpcBannerManagementModal.tsx new file mode 100644 index 0000000..8c7da6c --- /dev/null +++ b/components/NpcBannerManagementModal.tsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from 'react'; +import { Icons } from './IconSet'; + +interface NpcBannerManagementModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + currentBannerUrl: string; + onUpdate: () => void; +} + +const NpcBannerManagementModal: React.FC = ({ + isOpen, + onClose, + projectId, + currentBannerUrl, + onUpdate +}) => { + const [bannerUrl, setBannerUrl] = useState(currentBannerUrl); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + setBannerUrl(currentBannerUrl); + setError(null); + } + }, [isOpen, currentBannerUrl]); + + const updateBanner = async () => { + if (!bannerUrl.trim()) { + setError('Banner-URL ist erforderlich'); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ bannerUrl: bannerUrl.trim() }) + }); + + if (response.ok) { + onUpdate(); + onClose(); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Fehler beim Aktualisieren des Banners'); + } + } catch (err) { + console.error('Error updating banner:', err); + setError('Netzwerkfehler'); + } finally { + setLoading(false); + } + }; + + const handleImageLoad = () => { + setPreviewLoading(false); + }; + + const handleImageError = () => { + setPreviewLoading(false); + setError('Bild konnte nicht geladen werden'); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ + NPC-Banner bearbeiten +

+ +
+ +
+ {error && ( +
+

{error}

+
+ )} + + {/* Current Banner Preview */} +
+

Aktuelles Banner

+
+ {currentBannerUrl ? ( + <> + {previewLoading && ( +
+
+
+ )} + Current banner + + ) : ( +
+ +
+ )} +
+
+ + {/* Banner URL Input */} +
+

Neue Banner-URL

+ setBannerUrl(e.target.value)} + placeholder="https://example.com/banner-image.jpg" + className="w-full bg-[#0b0b0d] border border-border rounded p-3 text-sm" + /> +

+ Geben Sie eine direkte URL zu einem Bild ein. Empfohlene Größe: 1200x400 Pixel oder größer. +

+
+ + {/* File Upload */} +
+

Oder Bild hochladen

+
+ { + const file = e.target.files?.[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('banner', file); + + try { + setLoading(true); + setError(null); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/npc-companies/${projectId}/banner/upload`, { + method: 'POST', + credentials: 'include', + body: formData + }); + + if (response.ok) { + const data = await response.json(); + setBannerUrl(data.bannerUrl); + onUpdate(); + onClose(); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Fehler beim Hochladen'); + } + } catch (err) { + console.error('Error uploading banner:', err); + setError('Netzwerkfehler beim Hochladen'); + } finally { + setLoading(false); + } + }} + className="hidden" + id="npc-banner-upload" + /> + +
+
+ + {/* Preview */} + {bannerUrl && bannerUrl !== currentBannerUrl && ( +
+

Vorschau

+
+ Banner preview setError('Vorschau-Bild konnte nicht geladen werden')} + /> +
+
+ )} + + {/* Common Banner Suggestions */} +
+

Beispiele

+
+ + + + +
+
+ + {/* Info */} +
+
+ +
+

Banner-Empfehlungen

+
    +
  • • Verwenden Sie hochwertige Bilder mit 16:9 Seitenverhältnis
  • +
  • • Stellen Sie sicher, dass die Bilder öffentlich zugänglich sind
  • +
  • • Dunklere Bilder funktionieren oft besser mit dem Text-Overlay
  • +
+
+
+
+
+ +
+ + +
+
+
+ ); +}; + +export default NpcBannerManagementModal; diff --git a/components/NpcGalleryManagementModal.tsx b/components/NpcGalleryManagementModal.tsx new file mode 100644 index 0000000..9444e01 --- /dev/null +++ b/components/NpcGalleryManagementModal.tsx @@ -0,0 +1,271 @@ +import React, { useState, useEffect } from 'react'; +import { Icons } from './IconSet'; + +interface GalleryImage { + id: string; + url: string; +} + +interface NpcGalleryManagementModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + onUpdate: () => void; +} + +const NpcGalleryManagementModal: React.FC = ({ + isOpen, + onClose, + projectId, + onUpdate +}) => { + const [images, setImages] = useState([]); + const [loading, setLoading] = useState(false); + const [imageUrl, setImageUrl] = useState(''); + const [error, setError] = useState(null); + + useEffect(() => { + if (isOpen && projectId) { + loadGallery(); + } + }, [isOpen, projectId]); + + const loadGallery = async () => { + try { + setLoading(true); + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery`); + if (response.ok) { + const data = await response.json(); + setImages(data); + } else { + setError('Fehler beim Laden der Galerie'); + } + } catch (err) { + console.error('Error loading gallery:', err); + setError('Netzwerkfehler'); + } finally { + setLoading(false); + } + }; + + const addImage = async () => { + if (!imageUrl.trim()) return; + + try { + setLoading(true); + setError(null); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ imageUrl: imageUrl.trim() }) + }); + + if (response.ok) { + await loadGallery(); + setImageUrl(''); + onUpdate(); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Fehler beim Hinzufügen'); + } + } catch (err) { + console.error('Error adding image:', err); + setError('Netzwerkfehler'); + } finally { + setLoading(false); + } + }; + + const removeImage = async (imageId: string) => { + try { + setLoading(true); + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery/${imageId}`, { + method: 'DELETE', + credentials: 'include' + }); + + if (response.ok) { + await loadGallery(); + onUpdate(); + } else { + setError('Fehler beim Löschen'); + } + } catch (err) { + console.error('Error removing image:', err); + setError('Netzwerkfehler'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ + NPC-Portfolio verwalten +

+ +
+ +
+ {error && ( +
+

{error}

+
+ )} + + {/* Add Image */} +
+

Bild hinzufügen

+ + {/* URL Input */} +
+ setImageUrl(e.target.value)} + placeholder="Bild-URL eingeben..." + className="flex-1 bg-[#0b0b0d] border border-border rounded p-2 text-sm" + /> + +
+

+ Geben Sie eine direkte URL zu einem Bild ein (z.B. von Imgur, Discord, etc.) +

+ + {/* File Upload */} +
+
+ { + const files = e.target.files as FileList; + if (!files || files.length === 0) return; + + try { + setLoading(true); + setError(null); + + // Upload each file + const fileArray = Array.from(files) as File[]; + for (const file of fileArray) { + const formData = new FormData(); + formData.append('gallery', file); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/npc-companies/${projectId}/gallery/upload`, { + method: 'POST', + credentials: 'include', + body: formData + }); + + if (!response.ok) { + const errorData = await response.json(); + setError(errorData.error || `Fehler beim Hochladen von ${file.name}`); + break; + } + } + + // Reload gallery after all uploads + await loadGallery(); + + } catch (err) { + console.error('Error uploading images:', err); + setError('Netzwerkfehler beim Hochladen'); + } finally { + setLoading(false); + } + }} + className="hidden" + id="npc-gallery-upload" + /> + +
+
+
+ + {/* Gallery Grid */} +
+

+ Portfolio-Bilder ({images.length}) +

+ + {loading ? ( +
+
+
+ ) : images.length === 0 ? ( +
+

Noch keine Bilder im Portfolio.

+

Fügen Sie Bilder hinzu, um das Portfolio Ihrer NPC-Firma zu füllen.

+
+ ) : ( +
+ {images.map((image, index) => ( +
+
+ {`Portfolio { + const target = e.target as HTMLImageElement; + target.src = 'https://via.placeholder.com/200x200/374151/6b7280?text=Bild+fehlerhaft'; + }} + /> +
+ + {/* Delete Button */} + + + {/* Overlay on hover */} +
+
+ ))} +
+ )} +
+
+ +
+ +
+
+
+ ); +}; + +export default NpcGalleryManagementModal; diff --git a/components/NpcLogoManagementModal.tsx b/components/NpcLogoManagementModal.tsx new file mode 100644 index 0000000..de02ea0 --- /dev/null +++ b/components/NpcLogoManagementModal.tsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from 'react'; +import { Icons } from './IconSet'; + +interface NpcLogoManagementModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + currentLogoUrl: string; + onUpdate: () => void; +} + +const NpcLogoManagementModal: React.FC = ({ + isOpen, + onClose, + projectId, + currentLogoUrl, + onUpdate +}) => { + const [logoUrl, setLogoUrl] = useState(currentLogoUrl); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + setLogoUrl(currentLogoUrl); + setError(null); + } + }, [isOpen, currentLogoUrl]); + + const updateLogo = async () => { + if (!logoUrl.trim()) { + setError('Logo-URL ist erforderlich'); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ logoUrl: logoUrl.trim() }) + }); + + if (response.ok) { + onUpdate(); + onClose(); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Fehler beim Aktualisieren des Logos'); + } + } catch (err) { + console.error('Error updating logo:', err); + setError('Netzwerkfehler'); + } finally { + setLoading(false); + } + }; + + const handleImageLoad = () => { + setPreviewLoading(false); + }; + + const handleImageError = () => { + setPreviewLoading(false); + setError('Bild konnte nicht geladen werden'); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ + NPC-Logo bearbeiten +

+ +
+ +
+ {error && ( +
+

{error}

+
+ )} + + {/* Current Logo Preview */} +
+

Aktuelles Logo

+
+ {currentLogoUrl ? ( + <> + {previewLoading && ( +
+
+
+ )} + Current logo + + ) : ( +
+ +
+ )} +
+
+ + {/* Logo URL Input */} +
+

Neue Logo-URL

+ setLogoUrl(e.target.value)} + placeholder="https://example.com/logo-image.png" + className="w-full bg-[#0b0b0d] border border-border rounded p-3 text-sm" + /> +

+ Geben Sie eine direkte URL zu einem Bild ein. Empfohlene Größe: 200x200 Pixel oder größer, quadratisch. +

+
+ + {/* File Upload */} +
+

Oder Bild hochladen

+
+ { + const file = e.target.files?.[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('logo', file); + + try { + setLoading(true); + setError(null); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/npc-companies/${projectId}/logo/upload`, { + method: 'POST', + credentials: 'include', + body: formData + }); + + if (response.ok) { + const data = await response.json(); + setLogoUrl(data.logoUrl); + onUpdate(); + onClose(); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Fehler beim Hochladen'); + } + } catch (err) { + console.error('Error uploading logo:', err); + setError('Netzwerkfehler beim Hochladen'); + } finally { + setLoading(false); + } + }} + className="hidden" + id="npc-logo-upload" + /> + +
+
+ + {/* Preview */} + {logoUrl && logoUrl !== currentLogoUrl && ( +
+

Vorschau

+
+ Logo preview setError('Vorschau-Bild konnte nicht geladen werden')} + /> +
+
+ )} + + {/* Common Logo Suggestions */} +
+

Beispiele

+
+ + + + +
+
+ + {/* Info */} +
+
+ +
+

Logo-Empfehlungen

+
    +
  • • Verwenden Sie quadratische Bilder für beste Ergebnisse
  • +
  • • Stellen Sie sicher, dass das Logo auf dunklen Hintergründen gut sichtbar ist
  • +
  • • Vermeiden Sie zu komplexe Designs - Einfachheit ist besser
  • +
+
+
+
+
+ +
+ + +
+
+
+ ); +}; + +export default NpcLogoManagementModal; diff --git a/package-lock.json b/package-lock.json index f163f0d..d3acba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "react": "^19.2.3", "react-dom": "^19.2.3", - "react-router-dom": "^7.11.0" + "react-router-dom": "^7.11.0", + "sharp": "^0.34.5" }, "devDependencies": { "@types/node": "^22.14.0", @@ -263,6 +264,16 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/win32-x64": { "version": "0.25.12", "cpu": [ @@ -278,6 +289,471 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "dev": true, @@ -509,6 +985,15 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "dev": true, @@ -812,6 +1297,62 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -835,6 +1376,13 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.8.3", "dev": true, diff --git a/package.json b/package.json index fa776a4..aeb0345 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "dependencies": { "react": "^19.2.3", "react-dom": "^19.2.3", - "react-router-dom": "^7.11.0" + "react-router-dom": "^7.11.0", + "sharp": "^0.34.5" }, "devDependencies": { "@types/node": "^22.14.0", diff --git a/pages/Admin.tsx b/pages/Admin.tsx index e602ccb..61aff23 100644 --- a/pages/Admin.tsx +++ b/pages/Admin.tsx @@ -1,6 +1,9 @@ import React, { useState, useEffect } from 'react'; import { Icons } from '../components/IconSet'; import { authService } from '../services/AuthService'; +import NpcBannerManagementModal from '../components/NpcBannerManagementModal'; +import NpcLogoManagementModal from '../components/NpcLogoManagementModal'; +import NpcGalleryManagementModal from '../components/NpcGalleryManagementModal'; interface AdminPageProps { onBack: () => void; @@ -453,6 +456,9 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate: const [isEditingShop, setIsEditingShop] = useState(false); const [isManaging, setIsManaging] = useState(false); const [shopItems, setShopItems] = useState(company.shopCatalog || []); + const [bannerModalOpen, setBannerModalOpen] = useState(false); + const [logoModalOpen, setLogoModalOpen] = useState(false); + const [galleryModalOpen, setGalleryModalOpen] = useState(false); const [formData, setFormData] = useState({ title: company.title, description: company.description || '', @@ -606,42 +612,89 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate: } return ( -
-
-
-
- NPC + <> +
+
+
+
+ NPC +
+
+

{company.title}

+

{company.category}

+
-
-

{company.title}

-

{company.category}

+
+ + + + +
-
- - -
+
+ Eigentümer: {company.owner}
-
- Eigentümer: {company.owner} + {company.shopCatalog && company.shopCatalog.length > 0 && ( +
+ Shop: {company.shopCatalog.length} Artikel +
+ )}
- {company.shopCatalog && company.shopCatalog.length > 0 && ( -
- Shop: {company.shopCatalog.length} Artikel -
- )} -
+ + {/* Image Management Modals */} + setBannerModalOpen(false)} + projectId={company.id} + currentBannerUrl={company.bannerUrl} + onUpdate={() => onUpdate()} + /> + + setLogoModalOpen(false)} + projectId={company.id} + currentLogoUrl={company.logoUrl} + onUpdate={() => onUpdate()} + /> + + setGalleryModalOpen(false)} + projectId={company.id} + onUpdate={() => onUpdate()} + /> + ); }; @@ -666,13 +719,24 @@ const AdminPage: React.FC = ({ onBack }) => { return unsub; }, []); - useEffect(() => { - if (activeTab === 'npcs' && isAdmin) { + useEffect(() => { loadNpcs(); - } - if (activeTab === 'cities' && isAdmin) { loadCities(); - } + }, [isAdmin]); + + // Auto-refresh data every 30 seconds when on relevant tabs + useEffect(() => { + if (!isAdmin) return; + + const interval = setInterval(() => { + if (activeTab === 'edit-npcs' || activeTab === 'create-npc') { + loadNpcs(); + } else if (activeTab === 'cities' || activeTab === 'create-city') { + loadCities(); + } + }, 30000); // 30 seconds + + return () => clearInterval(interval); }, [activeTab, isAdmin]); const loadNpcs = async () => { @@ -1677,7 +1741,6 @@ const AdminPage: React.FC = ({ onBack }) => { }); if (response.ok) { - alert('Banner erfolgreich aktualisiert!'); loadCities(); // Reload to show updated image } else { setError('Fehler beim Hochladen des Banners'); @@ -1727,7 +1790,6 @@ const AdminPage: React.FC = ({ onBack }) => { }); if (response.ok) { - alert('Logo erfolgreich aktualisiert!'); loadCities(); // Reload to show updated image } else { setError('Fehler beim Hochladen des Logos'); @@ -1781,7 +1843,6 @@ const AdminPage: React.FC = ({ onBack }) => { }); if (response.ok) { - alert('Stadt erfolgreich aktualisiert!'); setEditingCity(null); loadCities(); } else { diff --git a/pages/CityProfile.tsx b/pages/CityProfile.tsx index 5a12118..84a78e1 100644 --- a/pages/CityProfile.tsx +++ b/pages/CityProfile.tsx @@ -24,16 +24,32 @@ const CityProfile: React.FC = () => { return; } - // Load city data - const cityData = dbService.getOrg(id); - if (cityData) { - setCity(cityData); - } else { - navigate('/cities'); - return; - } + // Fetch fresh data from database on mount/reload + const loadCityData = async () => { + try { + await dbService.fetchAll(); + const cityData = dbService.getOrg(id); + if (cityData) { + setCity(cityData); + } else { + navigate('/cities'); + return; + } + } catch (error) { + console.warn('Failed to fetch fresh city data:', error); + // Fallback to cached data + const cityData = dbService.getOrg(id); + if (cityData) { + setCity(cityData); + } else { + navigate('/cities'); + return; + } + } + setLoading(false); + }; - setLoading(false); + loadCityData(); }, [id, navigate]); useEffect(() => { @@ -42,7 +58,7 @@ const CityProfile: React.FC = () => { const loadCityData = () => { // Load residents (players in this city) const allPlayers = dbService.getPlayers(); - const cityResidents = allPlayers.filter(p => p.stats.organizationId === city.id); + const cityResidents = allPlayers.filter(p => p.organizationId === city.id); setResidents(cityResidents); // Load ventures (projects in this city) diff --git a/pages/PlayerProfile.tsx b/pages/PlayerProfile.tsx index c18e0a7..7367af6 100644 --- a/pages/PlayerProfile.tsx +++ b/pages/PlayerProfile.tsx @@ -21,6 +21,19 @@ const PlayerProfile: React.FC = () => { const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<'story' | 'stats' | 'projects'>('story'); + const handleNavigateToOrg = (orgId: string) => { + const org = dbService.getOrg(orgId); + if (org?.type === 'City') { + navigate(`/cities/${orgId}`); + } else { + navigate(`/organizations/${orgId}`); + } + }; + + const handleNavigateToProject = (projectId: string) => { + navigate(`/projects/${projectId}`); + }; + // Is this the logged-in user's profile? const isOwner = currentUser?.linkedPlayerUuid === player?.uuid; const playerOrg = player ? dbService.getOrg(player.organizationId || '') : null; @@ -36,17 +49,37 @@ const PlayerProfile: React.FC = () => { return; } - // Load player data - const playerData = dbService.getPlayer(id); - if (playerData) { - setPlayer(playerData); - } else { - navigate('/players'); - return; - } + // Load player data - try to fetch from API first, fallback to cache + const loadPlayerData = async () => { + try { + const playerData = await dbService.fetchPlayer(id); + if (playerData) { + setPlayer(playerData); + } else { + // Fallback to cache if API fails + const cachedPlayer = dbService.getPlayer(id); + if (cachedPlayer) { + setPlayer(cachedPlayer); + } else { + navigate('/players'); + return; + } + } + } catch (error) { + console.error('Error loading player data:', error); + // Fallback to cache + const cachedPlayer = dbService.getPlayer(id); + if (cachedPlayer) { + setPlayer(cachedPlayer); + } else { + navigate('/players'); + return; + } + } + setLoading(false); + }; - console.log(playerData); - setLoading(false); + loadPlayerData(); }, [id, navigate]); useEffect(() => { @@ -252,27 +285,38 @@ const PlayerProfile: React.FC = () => {

Zugehörigkeit

-
-
- {playerOrg ? playerOrg.name.charAt(0) : } -
-
-
{player.minecraftStats?.role || 'Unbekannt'}
-
- {playerOrg ? ( - {playerOrg.name} - ) : ( - 'Freiberufler / Keine Zugehörigkeit' - )} + {playerOrg ? ( +
handleNavigateToOrg(playerOrg.id)} + className="flex items-center gap-3 cursor-pointer hover:bg-surfaceHighlight/50 -m-1 p-1 rounded transition-colors group" + > +
+ {playerOrg.name.charAt(0)} +
+
+
+ {playerOrg.name} +
+
+ {player.minecraftStats?.role || 'Unbekannt'} • Klick zum Anzeigen +
-
+ ) : ( +
+
+ +
+
+
Freiberufler
+
Keine Zugehörigkeit
+
+
+ )}
@@ -437,9 +481,15 @@ const PlayerProfile: React.FC = () => {
{ownedProjects.map((project) => ( -
+
handleNavigateToProject(project.id)} + className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 hover:border-accentInfo/50 transition-all cursor-pointer group" + >
-

{project.title}

+

+ {project.title} +

{
+
+ Klick zum Anzeigen → +
))}
diff --git a/services/AuthService.ts b/services/AuthService.ts index 2bf8f0c..659d912 100644 --- a/services/AuthService.ts +++ b/services/AuthService.ts @@ -30,8 +30,9 @@ class AuthService { } // Redirects to Discord OAuth - async login(): Promise { - window.location.href = `${API_URL}/auth/discord`; + async login(rememberMe: boolean = false): Promise { + const rememberParam = rememberMe ? '?remember_me=true' : ''; + window.location.href = `${API_URL}/auth/discord${rememberParam}`; } logout() { diff --git a/services/DatabaseService.ts b/services/DatabaseService.ts index 61ce3ee..eeae540 100644 --- a/services/DatabaseService.ts +++ b/services/DatabaseService.ts @@ -78,6 +78,26 @@ class DatabaseService { return this.players.find(p => p.uuid === uuid); } + // Fetch individual player data directly from API (for profile pages) + async fetchPlayer(uuid: string): Promise { + try { + const response = await fetch(`${API_URL}/players/${uuid}`); + if (response.ok) { + const playerData = await response.json(); + // Update local cache + this.players = this.players.map(p => p.uuid === uuid ? playerData : p); + if (!this.players.find(p => p.uuid === uuid)) { + this.players.push(playerData); + } + this.notify(); + return playerData; + } + } catch (e) { + console.warn("Failed to fetch individual player data", e); + } + return null; + } + getOrg(id: string): Organization | undefined { return this.orgs.find(o => o.id === id); }