mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
Refactor CityProfile and PlayerProfile components for improved data fetching and error handling; add NPC management modals for banner, gallery, and logo with enhanced user experience and error feedback.
This commit is contained in:
550
backend/package-lock.json
generated
550
backend/package-lock.json
generated
@@ -14,7 +14,483 @@
|
|||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.6.0",
|
"mysql2": "^3.6.0",
|
||||||
"passport": "^0.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": {
|
"node_modules/accepts": {
|
||||||
@@ -240,6 +716,15 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -976,6 +1461,18 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/send": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||||
@@ -1032,6 +1529,50 @@
|
|||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
@@ -1148,6 +1689,13 @@
|
|||||||
"node": ">=0.6"
|
"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": {
|
"node_modules/type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.6.0",
|
"mysql2": "^3.6.0",
|
||||||
"passport": "^0.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
|
const MySQLStore = require('express-mysql-session')(session);
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const DiscordStrategy = require('passport-discord').Strategy;
|
const DiscordStrategy = require('passport-discord').Strategy;
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
@@ -58,14 +59,41 @@ const upload = multer({
|
|||||||
|
|
||||||
init(); // Initialize DB
|
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
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors({ origin: corsOrigins, credentials: true }));
|
app.use(cors({ origin: corsOrigins, credentials: true }));
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: process.env.SESSION_SECRET || 'dev_secret',
|
secret: process.env.SESSION_SECRET || 'dev_secret',
|
||||||
|
store: sessionStore,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: 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.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
@@ -129,11 +157,34 @@ app.get('/api/status', (req, res) => {
|
|||||||
// --- ROUTES ---
|
// --- ROUTES ---
|
||||||
|
|
||||||
// Auth
|
// 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', {
|
app.get('/auth/discord/callback', passport.authenticate('discord', {
|
||||||
failureRedirect: FRONTEND_URL + '?error=login_failed'
|
failureRedirect: FRONTEND_URL + '?error=login_failed'
|
||||||
}), (req, res) => {
|
}), (req, res) => {
|
||||||
|
// 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);
|
res.redirect(FRONTEND_URL);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/auth/me', (req, res) => {
|
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)
|
// Grant admin rights to a player (Admin only)
|
||||||
app.post('/api/admin/grant-admin/:uuid', (req, res) => {
|
app.post('/api/admin/grant-admin/:uuid', (req, res) => {
|
||||||
if (!req.isAuthenticated()) return res.status(401).send();
|
if (!req.isAuthenticated()) return res.status(401).send();
|
||||||
@@ -1019,7 +1346,7 @@ app.get('/api/images/:imageId/meta', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload banner image
|
// 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'});
|
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
|
||||||
|
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
@@ -1081,7 +1408,7 @@ app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload gallery image
|
// 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'});
|
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
|
||||||
|
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
@@ -1152,7 +1479,7 @@ app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (r
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload logo image
|
// 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'});
|
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
|
||||||
|
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
@@ -1344,7 +1671,7 @@ app.delete('/api/admin/cities/:cityId', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload city banner
|
// 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'});
|
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
@@ -1408,7 +1735,7 @@ app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (re
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload city logo
|
// 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'});
|
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
|
||||||
|
|
||||||
// Check if user is admin
|
// 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
|
// 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'});
|
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ const NavItem = ({
|
|||||||
const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
|
const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const [user, setUser] = useState<DiscordUser | null>(null);
|
const [user, setUser] = useState<DiscordUser | null>(null);
|
||||||
|
const [rememberMe, setRememberMe] = useState(() => {
|
||||||
|
// Load remember me preference from localStorage
|
||||||
|
return localStorage.getItem('rememberMe') === 'true';
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Subscribe to auth changes
|
// Subscribe to auth changes
|
||||||
@@ -151,13 +155,28 @@ const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => authService.login()}
|
onClick={() => authService.login(rememberMe)}
|
||||||
className="flex items-center gap-2 text-textMain hover:text-accentInfo transition-colors font-medium"
|
className="flex items-center gap-2 text-textMain hover:text-accentInfo transition-colors font-medium text-sm"
|
||||||
>
|
>
|
||||||
<Icons.Users className="w-4 h-4" />
|
<Icons.Users className="w-4 h-4" />
|
||||||
<span>Discord Login</span>
|
<span>Discord Login</span>
|
||||||
</button>
|
</button>
|
||||||
|
<label className="flex items-center gap-1 text-xs text-textMuted cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRememberMe(e.target.checked);
|
||||||
|
// Store preference for next login
|
||||||
|
localStorage.setItem('rememberMe', e.target.checked.toString());
|
||||||
|
}}
|
||||||
|
className="w-3 h-3 text-accentInfo bg-surface border-border rounded focus:ring-accentInfo"
|
||||||
|
/>
|
||||||
|
<span>Remember me (30 days)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
285
components/NpcBannerManagementModal.tsx
Normal file
285
components/NpcBannerManagementModal.tsx
Normal file
@@ -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<NpcBannerManagementModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projectId,
|
||||||
|
currentBannerUrl,
|
||||||
|
onUpdate
|
||||||
|
}) => {
|
||||||
|
const [bannerUrl, setBannerUrl] = useState(currentBannerUrl);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
|
<div className="bg-surface border border-border rounded-xl w-full max-w-2xl shadow-2xl flex flex-col max-h-[90vh]">
|
||||||
|
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
|
||||||
|
<h3 className="font-bold text-textMain flex items-center gap-2">
|
||||||
|
<Icons.Layers className="w-5 h-5" />
|
||||||
|
NPC-Banner bearbeiten
|
||||||
|
</h3>
|
||||||
|
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 flex-1 overflow-y-auto">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current Banner Preview */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Aktuelles Banner</h4>
|
||||||
|
<div className="relative h-32 rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
|
||||||
|
{currentBannerUrl ? (
|
||||||
|
<>
|
||||||
|
{previewLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accentInfo"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={currentBannerUrl}
|
||||||
|
alt="Current banner"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-textMuted">
|
||||||
|
<Icons.Layers className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Banner URL Input */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Neue Banner-URL</h4>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={bannerUrl}
|
||||||
|
onChange={(e) => setBannerUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com/banner-image.jpg"
|
||||||
|
className="w-full bg-[#0b0b0d] border border-border rounded p-3 text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-textMuted mt-2">
|
||||||
|
Geben Sie eine direkte URL zu einem Bild ein. Empfohlene Größe: 1200x400 Pixel oder größer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Oder Bild hochladen</h4>
|
||||||
|
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center hover:border-accentInfo/50 transition-colors">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={async (e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<label htmlFor="npc-banner-upload" className="cursor-pointer">
|
||||||
|
<div className="w-12 h-12 mx-auto mb-4 bg-surfaceHighlight rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-accentInfo" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-textMain font-medium">Bild auswählen</p>
|
||||||
|
<p className="text-xs text-textMuted mt-1">Max. 10MB, nur Bilddateien</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
{bannerUrl && bannerUrl !== currentBannerUrl && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Vorschau</h4>
|
||||||
|
<div className="relative h-32 rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
|
||||||
|
<img
|
||||||
|
src={bannerUrl}
|
||||||
|
alt="Banner preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={() => setError('Vorschau-Bild konnte nicht geladen werden')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Common Banner Suggestions */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Beispiele</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<button
|
||||||
|
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1449824913935-59a10b8d2000?q=80&w=1200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Berglandschaft</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=1200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Stadt bei Nacht</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?q=80&w=1200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Architektur</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=1200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Abstrakt</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Icons.Terminal className="w-5 h-5 text-blue-400 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-blue-400 mb-1">Banner-Empfehlungen</h4>
|
||||||
|
<ul className="text-xs text-blue-300 space-y-1">
|
||||||
|
<li>• Verwenden Sie hochwertige Bilder mit 16:9 Seitenverhältnis</li>
|
||||||
|
<li>• Stellen Sie sicher, dass die Bilder öffentlich zugänglich sind</li>
|
||||||
|
<li>• Dunklere Bilder funktionieren oft besser mit dem Text-Overlay</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-border flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={updateBanner}
|
||||||
|
disabled={loading || !bannerUrl.trim() || bannerUrl === currentBannerUrl}
|
||||||
|
className="px-6 py-2 text-sm font-medium bg-orange-500 hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
|
Aktualisiere...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icons.Layers className="w-4 h-4" />
|
||||||
|
Banner aktualisieren
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NpcBannerManagementModal;
|
||||||
271
components/NpcGalleryManagementModal.tsx
Normal file
271
components/NpcGalleryManagementModal.tsx
Normal file
@@ -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<NpcGalleryManagementModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projectId,
|
||||||
|
onUpdate
|
||||||
|
}) => {
|
||||||
|
const [images, setImages] = useState<GalleryImage[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [imageUrl, setImageUrl] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
|
<div className="bg-surface border border-border rounded-xl w-full max-w-4xl shadow-2xl flex flex-col max-h-[90vh]">
|
||||||
|
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
|
||||||
|
<h3 className="font-bold text-textMain flex items-center gap-2">
|
||||||
|
<Icons.Layers className="w-5 h-5" />
|
||||||
|
NPC-Portfolio verwalten
|
||||||
|
</h3>
|
||||||
|
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 flex-1 overflow-y-auto">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Image */}
|
||||||
|
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-4">Bild hinzufügen</h4>
|
||||||
|
|
||||||
|
{/* URL Input */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={imageUrl}
|
||||||
|
onChange={(e) => setImageUrl(e.target.value)}
|
||||||
|
placeholder="Bild-URL eingeben..."
|
||||||
|
className="flex-1 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addImage}
|
||||||
|
disabled={!imageUrl.trim() || loading}
|
||||||
|
className="bg-accentInfo hover:bg-accentInfo/90 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium"
|
||||||
|
>
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-textMuted mb-4">
|
||||||
|
Geben Sie eine direkte URL zu einem Bild ein (z.B. von Imgur, Discord, etc.)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<div className="border-t border-border pt-4">
|
||||||
|
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
onChange={async (e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<label htmlFor="npc-gallery-upload" className="cursor-pointer">
|
||||||
|
<div className="w-10 h-10 mx-auto mb-3 bg-surfaceHighlight rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-accentInfo" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-textMain font-medium">Bilder auswählen</p>
|
||||||
|
<p className="text-xs text-textMuted mt-1">Max. 10MB pro Bild, Mehrfachauswahl möglich</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gallery Grid */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="font-semibold text-textMain mb-4">
|
||||||
|
Portfolio-Bilder ({images.length})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accentInfo"></div>
|
||||||
|
</div>
|
||||||
|
) : images.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-textMuted">
|
||||||
|
<p>Noch keine Bilder im Portfolio.</p>
|
||||||
|
<p className="text-sm mt-2">Fügen Sie Bilder hinzu, um das Portfolio Ihrer NPC-Firma zu füllen.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<div key={image.id} className="relative group">
|
||||||
|
<div className="aspect-square rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
|
||||||
|
<img
|
||||||
|
src={image.url}
|
||||||
|
alt={`Portfolio ${index + 1}`}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.src = 'https://via.placeholder.com/200x200/374151/6b7280?text=Bild+fehlerhaft';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => removeImage(image.id)}
|
||||||
|
disabled={loading}
|
||||||
|
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Bild entfernen"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Overlay on hover */}
|
||||||
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors rounded-lg pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-border flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NpcGalleryManagementModal;
|
||||||
285
components/NpcLogoManagementModal.tsx
Normal file
285
components/NpcLogoManagementModal.tsx
Normal file
@@ -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<NpcLogoManagementModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projectId,
|
||||||
|
currentLogoUrl,
|
||||||
|
onUpdate
|
||||||
|
}) => {
|
||||||
|
const [logoUrl, setLogoUrl] = useState(currentLogoUrl);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
|
<div className="bg-surface border border-border rounded-xl w-full max-w-2xl shadow-2xl flex flex-col max-h-[90vh]">
|
||||||
|
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
|
||||||
|
<h3 className="font-bold text-textMain flex items-center gap-2">
|
||||||
|
<Icons.Layers className="w-5 h-5" />
|
||||||
|
NPC-Logo bearbeiten
|
||||||
|
</h3>
|
||||||
|
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 flex-1 overflow-y-auto">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current Logo Preview */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Aktuelles Logo</h4>
|
||||||
|
<div className="relative w-32 h-32 mx-auto rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
|
||||||
|
{currentLogoUrl ? (
|
||||||
|
<>
|
||||||
|
{previewLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accentInfo"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={currentLogoUrl}
|
||||||
|
alt="Current logo"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-textMuted">
|
||||||
|
<Icons.Layers className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo URL Input */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Neue Logo-URL</h4>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={logoUrl}
|
||||||
|
onChange={(e) => setLogoUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com/logo-image.png"
|
||||||
|
className="w-full bg-[#0b0b0d] border border-border rounded p-3 text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-textMuted mt-2">
|
||||||
|
Geben Sie eine direkte URL zu einem Bild ein. Empfohlene Größe: 200x200 Pixel oder größer, quadratisch.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Oder Bild hochladen</h4>
|
||||||
|
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center hover:border-accentInfo/50 transition-colors">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={async (e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<label htmlFor="npc-logo-upload" className="cursor-pointer">
|
||||||
|
<div className="w-12 h-12 mx-auto mb-4 bg-surfaceHighlight rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-accentInfo" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-textMain font-medium">Bild auswählen</p>
|
||||||
|
<p className="text-xs text-textMuted mt-1">Max. 10MB, nur Bilddateien</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
{logoUrl && logoUrl !== currentLogoUrl && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Vorschau</h4>
|
||||||
|
<div className="relative w-32 h-32 mx-auto rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
|
||||||
|
<img
|
||||||
|
src={logoUrl}
|
||||||
|
alt="Logo preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={() => setError('Vorschau-Bild konnte nicht geladen werden')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Common Logo Suggestions */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-semibold text-textMain mb-3">Beispiele</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<button
|
||||||
|
onClick={() => setLogoUrl('https://images.unsplash.com/photo-1560472354-b33ff0c44a43?q=80&w=200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Firmenlogo</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLogoUrl('https://images.unsplash.com/photo-1559136555-9303baea8ebd?q=80&w=200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Wappen</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLogoUrl('https://images.unsplash.com/photo-1611224923853-80b023f02d71?q=80&w=200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Schild</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLogoUrl('https://images.unsplash.com/photo-1581291518857-4e27b48ff24e?q=80&w=200&auto=format&fit=crop')}
|
||||||
|
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="font-medium">Emblem</div>
|
||||||
|
<div className="text-textMuted truncate">Unsplash</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Icons.Terminal className="w-5 h-5 text-blue-400 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-blue-400 mb-1">Logo-Empfehlungen</h4>
|
||||||
|
<ul className="text-xs text-blue-300 space-y-1">
|
||||||
|
<li>• Verwenden Sie quadratische Bilder für beste Ergebnisse</li>
|
||||||
|
<li>• Stellen Sie sicher, dass das Logo auf dunklen Hintergründen gut sichtbar ist</li>
|
||||||
|
<li>• Vermeiden Sie zu komplexe Designs - Einfachheit ist besser</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-border flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={updateLogo}
|
||||||
|
disabled={loading || !logoUrl.trim() || logoUrl === currentLogoUrl}
|
||||||
|
className="px-6 py-2 text-sm font-medium bg-orange-500 hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
|
Aktualisiere...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icons.Layers className="w-4 h-4" />
|
||||||
|
Logo aktualisieren
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NpcLogoManagementModal;
|
||||||
550
package-lock.json
generated
550
package-lock.json
generated
@@ -10,7 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
@@ -263,6 +264,16 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
@@ -278,6 +289,471 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"dev": true,
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.267",
|
"version": "1.5.267",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -812,6 +1297,62 @@
|
|||||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -835,6 +1376,13 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Icons } from '../components/IconSet';
|
import { Icons } from '../components/IconSet';
|
||||||
import { authService } from '../services/AuthService';
|
import { authService } from '../services/AuthService';
|
||||||
|
import NpcBannerManagementModal from '../components/NpcBannerManagementModal';
|
||||||
|
import NpcLogoManagementModal from '../components/NpcLogoManagementModal';
|
||||||
|
import NpcGalleryManagementModal from '../components/NpcGalleryManagementModal';
|
||||||
|
|
||||||
interface AdminPageProps {
|
interface AdminPageProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@@ -453,6 +456,9 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
|
|||||||
const [isEditingShop, setIsEditingShop] = useState(false);
|
const [isEditingShop, setIsEditingShop] = useState(false);
|
||||||
const [isManaging, setIsManaging] = useState(false);
|
const [isManaging, setIsManaging] = useState(false);
|
||||||
const [shopItems, setShopItems] = useState<any[]>(company.shopCatalog || []);
|
const [shopItems, setShopItems] = useState<any[]>(company.shopCatalog || []);
|
||||||
|
const [bannerModalOpen, setBannerModalOpen] = useState(false);
|
||||||
|
const [logoModalOpen, setLogoModalOpen] = useState(false);
|
||||||
|
const [galleryModalOpen, setGalleryModalOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: company.title,
|
title: company.title,
|
||||||
description: company.description || '',
|
description: company.description || '',
|
||||||
@@ -606,6 +612,7 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -618,6 +625,27 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setBannerModalOpen(true)}
|
||||||
|
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||||
|
title="Banner bearbeiten"
|
||||||
|
>
|
||||||
|
<Icons.Layers className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLogoModalOpen(true)}
|
||||||
|
className="text-green-400 hover:text-green-300 text-sm"
|
||||||
|
title="Logo bearbeiten"
|
||||||
|
>
|
||||||
|
<Icons.Shield className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setGalleryModalOpen(true)}
|
||||||
|
className="text-orange-400 hover:text-orange-300 text-sm"
|
||||||
|
title="Portfolio verwalten"
|
||||||
|
>
|
||||||
|
<Icons.Box className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onOpenShopModal(company.id, company.shopCatalog || [])}
|
onClick={() => onOpenShopModal(company.id, company.shopCatalog || [])}
|
||||||
className="text-accentInfo hover:text-accentInfo/80 text-sm"
|
className="text-accentInfo hover:text-accentInfo/80 text-sm"
|
||||||
@@ -642,6 +670,31 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Image Management Modals */}
|
||||||
|
<NpcBannerManagementModal
|
||||||
|
isOpen={bannerModalOpen}
|
||||||
|
onClose={() => setBannerModalOpen(false)}
|
||||||
|
projectId={company.id}
|
||||||
|
currentBannerUrl={company.bannerUrl}
|
||||||
|
onUpdate={() => onUpdate()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NpcLogoManagementModal
|
||||||
|
isOpen={logoModalOpen}
|
||||||
|
onClose={() => setLogoModalOpen(false)}
|
||||||
|
projectId={company.id}
|
||||||
|
currentLogoUrl={company.logoUrl}
|
||||||
|
onUpdate={() => onUpdate()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NpcGalleryManagementModal
|
||||||
|
isOpen={galleryModalOpen}
|
||||||
|
onClose={() => setGalleryModalOpen(false)}
|
||||||
|
projectId={company.id}
|
||||||
|
onUpdate={() => onUpdate()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -667,12 +720,23 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'npcs' && isAdmin) {
|
|
||||||
loadNpcs();
|
loadNpcs();
|
||||||
}
|
loadCities();
|
||||||
if (activeTab === 'cities' && isAdmin) {
|
}, [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();
|
loadCities();
|
||||||
}
|
}
|
||||||
|
}, 30000); // 30 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, [activeTab, isAdmin]);
|
}, [activeTab, isAdmin]);
|
||||||
|
|
||||||
const loadNpcs = async () => {
|
const loadNpcs = async () => {
|
||||||
@@ -1677,7 +1741,6 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert('Banner erfolgreich aktualisiert!');
|
|
||||||
loadCities(); // Reload to show updated image
|
loadCities(); // Reload to show updated image
|
||||||
} else {
|
} else {
|
||||||
setError('Fehler beim Hochladen des Banners');
|
setError('Fehler beim Hochladen des Banners');
|
||||||
@@ -1727,7 +1790,6 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert('Logo erfolgreich aktualisiert!');
|
|
||||||
loadCities(); // Reload to show updated image
|
loadCities(); // Reload to show updated image
|
||||||
} else {
|
} else {
|
||||||
setError('Fehler beim Hochladen des Logos');
|
setError('Fehler beim Hochladen des Logos');
|
||||||
@@ -1781,7 +1843,6 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert('Stadt erfolgreich aktualisiert!');
|
|
||||||
setEditingCity(null);
|
setEditingCity(null);
|
||||||
loadCities();
|
loadCities();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ const CityProfile: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load city data
|
// Fetch fresh data from database on mount/reload
|
||||||
|
const loadCityData = async () => {
|
||||||
|
try {
|
||||||
|
await dbService.fetchAll();
|
||||||
const cityData = dbService.getOrg(id);
|
const cityData = dbService.getOrg(id);
|
||||||
if (cityData) {
|
if (cityData) {
|
||||||
setCity(cityData);
|
setCity(cityData);
|
||||||
@@ -32,8 +35,21 @@ const CityProfile: React.FC = () => {
|
|||||||
navigate('/cities');
|
navigate('/cities');
|
||||||
return;
|
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]);
|
}, [id, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,7 +58,7 @@ const CityProfile: React.FC = () => {
|
|||||||
const loadCityData = () => {
|
const loadCityData = () => {
|
||||||
// Load residents (players in this city)
|
// Load residents (players in this city)
|
||||||
const allPlayers = dbService.getPlayers();
|
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);
|
setResidents(cityResidents);
|
||||||
|
|
||||||
// Load ventures (projects in this city)
|
// Load ventures (projects in this city)
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ const PlayerProfile: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeTab, setActiveTab] = useState<'story' | 'stats' | 'projects'>('story');
|
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?
|
// Is this the logged-in user's profile?
|
||||||
const isOwner = currentUser?.linkedPlayerUuid === player?.uuid;
|
const isOwner = currentUser?.linkedPlayerUuid === player?.uuid;
|
||||||
const playerOrg = player ? dbService.getOrg(player.organizationId || '') : null;
|
const playerOrg = player ? dbService.getOrg(player.organizationId || '') : null;
|
||||||
@@ -36,17 +49,37 @@ const PlayerProfile: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load player data
|
// Load player data - try to fetch from API first, fallback to cache
|
||||||
const playerData = dbService.getPlayer(id);
|
const loadPlayerData = async () => {
|
||||||
|
try {
|
||||||
|
const playerData = await dbService.fetchPlayer(id);
|
||||||
if (playerData) {
|
if (playerData) {
|
||||||
setPlayer(playerData);
|
setPlayer(playerData);
|
||||||
|
} else {
|
||||||
|
// Fallback to cache if API fails
|
||||||
|
const cachedPlayer = dbService.getPlayer(id);
|
||||||
|
if (cachedPlayer) {
|
||||||
|
setPlayer(cachedPlayer);
|
||||||
} else {
|
} else {
|
||||||
navigate('/players');
|
navigate('/players');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
console.log(playerData);
|
} 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);
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPlayerData();
|
||||||
}, [id, navigate]);
|
}, [id, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -252,29 +285,40 @@ const PlayerProfile: React.FC = () => {
|
|||||||
|
|
||||||
<div className="bg-surface border border-border rounded-xl p-4 shadow-card">
|
<div className="bg-surface border border-border rounded-xl p-4 shadow-card">
|
||||||
<h3 className="text-xs font-bold uppercase tracking-wider text-textMuted mb-3">Zugehörigkeit</h3>
|
<h3 className="text-xs font-bold uppercase tracking-wider text-textMuted mb-3">Zugehörigkeit</h3>
|
||||||
<div className="flex items-center gap-3">
|
{playerOrg ? (
|
||||||
|
<div
|
||||||
|
onClick={() => handleNavigateToOrg(playerOrg.id)}
|
||||||
|
className="flex items-center gap-3 cursor-pointer hover:bg-surfaceHighlight/50 -m-1 p-1 rounded transition-colors group"
|
||||||
|
>
|
||||||
<div className={`w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 ${
|
<div className={`w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 ${
|
||||||
playerOrg
|
playerOrg.type === 'City' ? 'bg-blue-500/10 text-blue-400' :
|
||||||
? (playerOrg.type === 'City' ? 'bg-blue-500/10 text-blue-400' :
|
|
||||||
playerOrg.type === 'Guild' ? 'bg-amber-500/10 text-amber-400' :
|
playerOrg.type === 'Guild' ? 'bg-amber-500/10 text-amber-400' :
|
||||||
'bg-purple-500/10 text-purple-400')
|
'bg-purple-500/10 text-purple-400'
|
||||||
: 'bg-surfaceHighlight text-textMuted'
|
|
||||||
}`}>
|
}`}>
|
||||||
{playerOrg ? playerOrg.name.charAt(0) : <Icons.Map className="w-5 h-5 opacity-50" />}
|
{playerOrg.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-textMain">{player.minecraftStats?.role || 'Unbekannt'}</div>
|
<div className="text-sm font-medium text-textMain group-hover:text-accentInfo transition-colors">
|
||||||
|
{playerOrg.name}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-textMuted">
|
<div className="text-xs text-textMuted">
|
||||||
{playerOrg ? (
|
{player.minecraftStats?.role || 'Unbekannt'} • Klick zum Anzeigen
|
||||||
<span className="group-hover:text-accentInfo transition-colors">{playerOrg.name}</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
'Freiberufler / Keine Zugehörigkeit'
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 bg-surfaceHighlight text-textMuted">
|
||||||
|
<Icons.Map className="w-5 h-5 opacity-50" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-textMain">Freiberufler</div>
|
||||||
|
<div className="text-xs text-textMuted">Keine Zugehörigkeit</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Col: Tabs */}
|
{/* Right Col: Tabs */}
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
@@ -437,9 +481,15 @@ const PlayerProfile: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{ownedProjects.map((project) => (
|
{ownedProjects.map((project) => (
|
||||||
<div key={project.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 hover:border-accentInfo/50 transition-all">
|
<div
|
||||||
|
key={project.id}
|
||||||
|
onClick={() => handleNavigateToProject(project.id)}
|
||||||
|
className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 hover:border-accentInfo/50 transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<h4 className="font-medium text-textMain">{project.title}</h4>
|
<h4 className="font-medium text-textMain group-hover:text-accentInfo transition-colors">
|
||||||
|
{project.title}
|
||||||
|
</h4>
|
||||||
<span className={`text-xs px-2 py-1 rounded border ${
|
<span className={`text-xs px-2 py-1 rounded border ${
|
||||||
project.status === 'active' ? 'bg-green-500/10 text-green-400 border-green-500/20' :
|
project.status === 'active' ? 'bg-green-500/10 text-green-400 border-green-500/20' :
|
||||||
project.status === 'recruiting' ? 'bg-blue-500/10 text-blue-400 border-blue-500/20' :
|
project.status === 'recruiting' ? 'bg-blue-500/10 text-blue-400 border-blue-500/20' :
|
||||||
@@ -468,6 +518,9 @@ const PlayerProfile: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-accentInfo mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
Klick zum Anzeigen →
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirects to Discord OAuth
|
// Redirects to Discord OAuth
|
||||||
async login(): Promise<void> {
|
async login(rememberMe: boolean = false): Promise<void> {
|
||||||
window.location.href = `${API_URL}/auth/discord`;
|
const rememberParam = rememberMe ? '?remember_me=true' : '';
|
||||||
|
window.location.href = `${API_URL}/auth/discord${rememberParam}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
|||||||
@@ -78,6 +78,26 @@ class DatabaseService {
|
|||||||
return this.players.find(p => p.uuid === uuid);
|
return this.players.find(p => p.uuid === uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch individual player data directly from API (for profile pages)
|
||||||
|
async fetchPlayer(uuid: string): Promise<Player | null> {
|
||||||
|
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 {
|
getOrg(id: string): Organization | undefined {
|
||||||
return this.orgs.find(o => o.id === id);
|
return this.orgs.find(o => o.id === id);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user