b
This commit is contained in:
Generated
+496
-125
@@ -8,7 +8,10 @@
|
||||
"name": "FurryPlace-frontend",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4"
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.52.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"maplibre-gl": "^4.0.0",
|
||||
"minidenticons": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
@@ -25,7 +28,7 @@
|
||||
"vite": "^5.0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.52.3"
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.52.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -503,6 +506,89 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-rewind": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"get-stream": "^6.0.1",
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"geojson-rewind": "geojson-rewind"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
|
||||
"integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "20.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
|
||||
"integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^2.0.0",
|
||||
"rw": "^1.3.3",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
|
||||
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -560,9 +646,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
|
||||
"integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
|
||||
"integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -574,9 +660,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz",
|
||||
"integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -588,9 +674,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz",
|
||||
"integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -602,9 +688,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz",
|
||||
"integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
|
||||
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -616,9 +702,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz",
|
||||
"integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -630,9 +716,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz",
|
||||
"integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
|
||||
"integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -644,9 +730,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz",
|
||||
"integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
|
||||
"integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -658,9 +744,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz",
|
||||
"integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
|
||||
"integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -672,9 +758,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -686,9 +772,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz",
|
||||
"integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -700,9 +786,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -714,9 +800,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -728,9 +814,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -742,9 +828,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz",
|
||||
"integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -756,9 +842,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -770,9 +856,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -784,9 +870,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz",
|
||||
"integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -798,9 +884,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz",
|
||||
"integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -812,9 +898,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz",
|
||||
"integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -826,9 +912,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz",
|
||||
"integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -840,9 +926,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
|
||||
"integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -854,9 +940,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz",
|
||||
"integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -884,9 +970,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-static": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.9.tgz",
|
||||
"integrity": "sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==",
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
|
||||
"integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -894,9 +980,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.43.7",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.43.7.tgz",
|
||||
"integrity": "sha512-6trpyltB9XZNkM8cfVHG9U2urAH4NPD7UeO0wiBvZjD8gHj6w9bVeWnBQgnO8LPNpzOhSlwnZDk355OOAa/9Zw==",
|
||||
"version": "2.43.8",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.43.8.tgz",
|
||||
"integrity": "sha512-z21dG8W4g6XtAnK8bMpaSahtPOV6JVhghhco1+GR4H39XEgIxrjIpRoT1Js84c7TmhBzbTkVpZVVPFNNPFsXkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -991,9 +1077,17 @@
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson-vt": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
|
||||
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz",
|
||||
@@ -1004,6 +1098,29 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mapbox__point-geometry": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mapbox__vector-tile": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
|
||||
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*",
|
||||
"@types/mapbox__point-geometry": "*",
|
||||
"@types/pbf": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pbf": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/pug": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
||||
@@ -1011,6 +1128,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -1144,9 +1270,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.10",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
|
||||
"integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==",
|
||||
"version": "2.8.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.11.tgz",
|
||||
"integrity": "sha512-i+sRXGhz4+QW8aACZ3+r1GAKMt0wlFpeA8M5rOQd0HEYw9zhDrlx9Wc8uQ0IdXakjJRthzglEwfB/yqIjO6iDg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1245,9 +1371,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001746",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz",
|
||||
"integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==",
|
||||
"version": "1.0.30001747",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz",
|
||||
"integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1366,6 +1492,29 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn/node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cross-spawn/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-selector-tokenizer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
|
||||
@@ -1493,6 +1642,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -1501,9 +1656,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.228",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz",
|
||||
"integrity": "sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==",
|
||||
"version": "1.5.230",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz",
|
||||
"integrity": "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1697,6 +1852,30 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/geojson-vt": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -1732,6 +1911,20 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
|
||||
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ini": "^4.1.3",
|
||||
"kind-of": "^6.0.3",
|
||||
"which": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -1752,6 +1945,26 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@@ -1771,6 +1984,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
|
||||
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@@ -1854,11 +2076,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
@@ -1886,6 +2110,27 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
@@ -1946,6 +2191,47 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
|
||||
"integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.6",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^20.3.1",
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/geojson-vt": "3.2.5",
|
||||
"@types/mapbox__point-geometry": "^0.1.4",
|
||||
"@types/mapbox__vector-tile": "^1.3.4",
|
||||
"@types/pbf": "^3.0.5",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"earcut": "^3.0.0",
|
||||
"geojson-vt": "^4.0.2",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"global-prefix": "^4.0.0",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^3.3.0",
|
||||
"potpack": "^2.0.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0",
|
||||
"vt-pbf": "^3.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
@@ -1987,6 +2273,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minidenticons": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minidenticons/-/minidenticons-4.2.1.tgz",
|
||||
"integrity": "sha512-oWfFivA0lOx/V/bO/YIJbthB26lV8JXYvhnv9zM2hNd3fzsHTXQ6c6bWZPcvhD3nnOB+lQk/D9lF43BXixrN8g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=15.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -2004,7 +2299,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -2060,6 +2354,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@@ -2092,9 +2392,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.21",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
||||
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
|
||||
"version": "2.0.23",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
|
||||
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2199,6 +2499,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ieee754": "^1.1.12",
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/periscopic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||
@@ -2411,6 +2724,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -2432,6 +2757,12 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -2476,6 +2807,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -2502,9 +2842,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz",
|
||||
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2518,28 +2858,28 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.3",
|
||||
"@rollup/rollup-android-arm64": "4.52.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.52.3",
|
||||
"@rollup/rollup-darwin-x64": "4.52.3",
|
||||
"@rollup/rollup-freebsd-arm64": "4.52.3",
|
||||
"@rollup/rollup-freebsd-x64": "4.52.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.52.3",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.52.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.52.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.52.3",
|
||||
"@rollup/rollup-openharmony-arm64": "4.52.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.52.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.52.3",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.52.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.3",
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.4",
|
||||
"@rollup/rollup-android-arm64": "4.52.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.52.4",
|
||||
"@rollup/rollup-darwin-x64": "4.52.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.52.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.52.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.52.4",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.52.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.52.4",
|
||||
"@rollup/rollup-openharmony-arm64": "4.52.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.52.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.52.4",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.52.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -2567,6 +2907,12 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
@@ -2864,6 +3210,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
@@ -3074,6 +3429,12 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -3231,20 +3592,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vt-pbf": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "0.1.0",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"pbf": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
|
||||
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
"isexe": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
"node": "^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
"vite": "^5.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4"
|
||||
"leaflet": "^1.9.4",
|
||||
"maplibre-gl": "^4.0.0",
|
||||
"minidenticons": "^4.2.1"
|
||||
},
|
||||
"type": "module",
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.52.3"
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.52.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.font-flag {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
}
|
||||
|
||||
/* Leaflet overrides */
|
||||
.leaflet-container {
|
||||
background: #f8f4f0;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { API_URL, ENABLE_TURNSTILE } from '$lib/constants/config';
|
||||
import { captcha } from '$lib/stores/global';
|
||||
import { t } from '$lib/i18n';
|
||||
import Turnstile from './Turnstile.svelte';
|
||||
import Logo from '../layout/Logo.svelte';
|
||||
import Turnstile from './Turnstile.svelte';
|
||||
|
||||
export let redirect: string | undefined = undefined;
|
||||
|
||||
@@ -34,13 +34,11 @@
|
||||
<Logo size="lg" hasText={true} />
|
||||
</div>
|
||||
|
||||
<form class="w-full">
|
||||
{#if ENABLE_TURNSTILE}
|
||||
<div class="mt-6 flex flex-col items-center gap-2">
|
||||
<Turnstile />
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
{#if ENABLE_TURNSTILE}
|
||||
<div class="mt-6 flex flex-col items-center gap-2">
|
||||
<Turnstile />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 flex flex-col items-center gap-2 w-full">
|
||||
<a
|
||||
|
||||
@@ -1,119 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { MAP_URL } from '../../constants/config';
|
||||
import { api } from '../../api/client';
|
||||
import { BACKEND_URL } from '../../constants/config';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
|
||||
const TILE_ZOOM = 11;
|
||||
const TILE_PIXELS = 1024;
|
||||
const SEASON = 's0';
|
||||
|
||||
import type { Map as LeafletMap, GridLayer, CircleMarker, LatLng, Coords } from 'leaflet';
|
||||
type LeafletModule = typeof import('leaflet');
|
||||
|
||||
type PixelEventDetail = { tileX: number; tileY: number; x: number; y: number; latlng: LatLng };
|
||||
type PixelEventDetail = { tileX: number; tileY: number; x: number; y: number; latlng: { lat: number; lng: number } };
|
||||
|
||||
const dispatch = createEventDispatcher<{ pixel: PixelEventDetail }>();
|
||||
|
||||
let mapContainer: HTMLDivElement;
|
||||
let map: LeafletMap | null = null;
|
||||
let tiles: GridLayer | null = null;
|
||||
let marker: CircleMarker | null = null;
|
||||
let L: LeafletModule | null = null;
|
||||
let map: maplibregl.Map | null = null;
|
||||
let marker: maplibregl.Marker | null = null;
|
||||
|
||||
export let center: [number, number] = [0, 0];
|
||||
|
||||
onMount(async () => {
|
||||
L = await import('leaflet');
|
||||
await import('leaflet/dist/leaflet.css');
|
||||
// Load the liberty style from static files
|
||||
const styleResponse = await fetch('/maps/styles/liberty');
|
||||
const baseStyle = await styleResponse.json();
|
||||
|
||||
map = L.map(mapContainer, {
|
||||
center,
|
||||
zoom: TILE_ZOOM,
|
||||
minZoom: TILE_ZOOM,
|
||||
maxZoom: TILE_ZOOM,
|
||||
zoomControl: false,
|
||||
maxBounds: [
|
||||
[-85, -180],
|
||||
[85, 180]
|
||||
],
|
||||
maxBoundsViscosity: 1.0
|
||||
});
|
||||
|
||||
L.tileLayer(MAP_URL + '/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: TILE_ZOOM,
|
||||
minZoom: TILE_ZOOM
|
||||
}).addTo(map);
|
||||
|
||||
const gridLayer = L.gridLayer({
|
||||
minZoom: TILE_ZOOM,
|
||||
maxZoom: TILE_ZOOM,
|
||||
// Add pixel tiles source and layer to the style
|
||||
baseStyle.sources['pixel-tiles'] = {
|
||||
type: 'raster',
|
||||
tiles: [`${BACKEND_URL}/files/${SEASON}/tiles/{x}/{y}.png`],
|
||||
tileSize: 256,
|
||||
className: 'pixelated'
|
||||
}) as GridLayer & { createTile: (coords: Coords) => HTMLImageElement };
|
||||
|
||||
gridLayer.createTile = (coords) => {
|
||||
const img = document.createElement('img');
|
||||
img.width = 256;
|
||||
img.height = 256;
|
||||
img.decoding = 'async';
|
||||
img.loading = 'lazy';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = api.getTileImageUrl(SEASON, coords.x, coords.y);
|
||||
return img;
|
||||
minzoom: TILE_ZOOM,
|
||||
maxzoom: TILE_ZOOM
|
||||
};
|
||||
|
||||
tiles = gridLayer;
|
||||
tiles.addTo(map);
|
||||
baseStyle.layers.push({
|
||||
id: 'pixel-layer',
|
||||
type: 'raster',
|
||||
source: 'pixel-tiles',
|
||||
minzoom: TILE_ZOOM,
|
||||
maxzoom: TILE_ZOOM,
|
||||
paint: {
|
||||
'raster-opacity': 0.8
|
||||
}
|
||||
});
|
||||
|
||||
map = new maplibregl.Map({
|
||||
container: mapContainer,
|
||||
style: baseStyle,
|
||||
center: center as [number, number],
|
||||
zoom: 2,
|
||||
minZoom: 0,
|
||||
maxZoom: 22
|
||||
});
|
||||
|
||||
map.on('click', (event) => {
|
||||
if (!L) return;
|
||||
const latlng = event.latlng;
|
||||
const info = latLngToTile(latlng.lat, latlng.lng);
|
||||
if (!map) return;
|
||||
const { lng, lat } = event.lngLat;
|
||||
const info = latLngToTile(lat, lng);
|
||||
if (!info) return;
|
||||
|
||||
if (!marker) {
|
||||
marker = L.circleMarker(latlng, {
|
||||
radius: 4,
|
||||
color: '#ffffff',
|
||||
weight: 2,
|
||||
fillColor: '#ed1c24',
|
||||
fillOpacity: 1
|
||||
}).addTo(map!);
|
||||
const el = document.createElement('div');
|
||||
el.className = 'w-3 h-3 bg-red-500 border-2 border-white rounded-full';
|
||||
marker = new maplibregl.Marker({ element: el })
|
||||
.setLngLat([lng, lat])
|
||||
.addTo(map!);
|
||||
} else {
|
||||
marker.setLatLng(latlng);
|
||||
marker.setLngLat([lng, lat]);
|
||||
}
|
||||
|
||||
dispatch('pixel', { ...info, latlng });
|
||||
dispatch('pixel', { ...info, latlng: { lat, lng } });
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
marker?.remove();
|
||||
map?.remove();
|
||||
map = null;
|
||||
tiles = null;
|
||||
marker = null;
|
||||
});
|
||||
|
||||
export function focusPixel(tileX: number, tileY: number, x: number, y: number) {
|
||||
if (!map || !L) return;
|
||||
const latlng = tileToLatLng(tileX, tileY, x, y);
|
||||
map.panTo(latlng, { animate: true });
|
||||
if (!map) return;
|
||||
const { lat, lng } = tileToLatLng(tileX, tileY, x, y);
|
||||
map.flyTo({ center: [lng, lat], essential: true });
|
||||
|
||||
if (!marker) {
|
||||
marker = L.circleMarker(latlng, {
|
||||
radius: 4,
|
||||
color: '#ffffff',
|
||||
weight: 2,
|
||||
fillColor: '#ed1c24',
|
||||
fillOpacity: 1
|
||||
}).addTo(map);
|
||||
const el = document.createElement('div');
|
||||
el.className = 'w-3 h-3 bg-red-500 border-2 border-white rounded-full';
|
||||
marker = new maplibregl.Marker({ element: el })
|
||||
.setLngLat([lng, lat])
|
||||
.addTo(map);
|
||||
} else {
|
||||
marker.setLatLng(latlng);
|
||||
marker.setLngLat([lng, lat]);
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshTiles() {
|
||||
tiles?.redraw();
|
||||
if (!map) return;
|
||||
const source = map.getSource('pixel-tiles') as maplibregl.RasterTileSource;
|
||||
if (source) {
|
||||
// Force tile refresh by clearing cache
|
||||
map.style.sourceCaches['pixel-tiles']?.clearTiles();
|
||||
map.triggerRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
export function getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
function latLngToTile(lat: number, lng: number) {
|
||||
@@ -138,15 +131,8 @@ function tileToLatLng(tileX: number, tileY: number, x: number, y: number) {
|
||||
const lng = (fx / n) * 360 - 180;
|
||||
const latRad = Math.atan(Math.sinh(Math.PI * (1 - (2 * fy) / n)));
|
||||
const lat = (latRad * 180) / Math.PI;
|
||||
return L!.latLng(lat, lng);
|
||||
return { lat, lng };
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(.leaflet-container) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div bind:this={mapContainer} class="w-full h-full rounded-box overflow-hidden shadow-lg border border-base-300"></div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import ColorPalette from './ColorPalette.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Bottom sheet drawer -->
|
||||
<div class="fixed inset-0 z-50 flex items-end justify-center" on:click={close}>
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/20"></div>
|
||||
|
||||
<!-- Drawer -->
|
||||
<div
|
||||
class="relative w-full bg-base-100 rounded-t-2xl shadow-xl p-4 pb-6"
|
||||
on:click|stopPropagation
|
||||
>
|
||||
<button
|
||||
on:click={close}
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M240-120q-45 0-89-22t-71-58q26 0 53-20.5t27-59.5q0-50 35-85t85-35q50 0 85 35t35 85q0 66-47 113t-113 47Zm230-240L360-470l358-358q11-11 27.5-11.5T774-828l54 54q12 12 12 28t-12 28L470-360Z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-bold text-lg">Paint pixel (#)</h3>
|
||||
</div>
|
||||
|
||||
<ColorPalette />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { currentUser, currentCharges } from '$lib/stores/user';
|
||||
import { selectedColor } from '$lib/stores/canvas';
|
||||
import { COLORS } from '$lib/constants/colors';
|
||||
|
||||
export let disabled = false;
|
||||
export let painting = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let chargeCanvas: HTMLCanvasElement;
|
||||
|
||||
$: selectedColorData = COLORS.find((c) => c.id === $selectedColor);
|
||||
$: chargePercentage = $currentUser ? ($currentCharges / $currentUser.maxCharges) * 100 : 0;
|
||||
|
||||
// Draw charge bar whenever charges change
|
||||
$: if (chargeCanvas && $currentUser) {
|
||||
drawChargeBar(chargeCanvas, $currentCharges, $currentUser.maxCharges);
|
||||
}
|
||||
|
||||
function drawChargeBar(canvas: HTMLCanvasElement, charges: number, maxCharges: number) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const width = 112;
|
||||
const height = 20;
|
||||
const barHeight = 12;
|
||||
const barY = (height - barHeight) / 2;
|
||||
const filledWidth = (charges / maxCharges) * width;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Draw background (empty bar)
|
||||
ctx.fillStyle = '#444';
|
||||
ctx.fillRect(0, barY, width, barHeight);
|
||||
|
||||
// Draw filled portion
|
||||
ctx.fillStyle = '#4ade80'; // Green color
|
||||
ctx.fillRect(0, barY, filledWidth, barHeight);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(1, barY + 1, width - 2, barHeight - 2);
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (!disabled) {
|
||||
dispatch('click');
|
||||
}
|
||||
}
|
||||
|
||||
function openPalette() {
|
||||
dispatch('openPalette');
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-lg sm:btn-xl relative z-30"
|
||||
on:click={handleClick}
|
||||
{disabled}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
d="M240-120q-45 0-89-22t-71-58q26 0 53-20.5t27-59.5q0-50 35-85t85-35q50 0 85 35t35 85q0 66-47 113t-113 47Zm230-240L360-470l358-358q11-11 27.5-11.5T774-828l54 54q12 12 12 28t-12 28L470-360Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
Paint
|
||||
{#if $currentUser}
|
||||
<span class="font-semibold">{Math.floor($currentCharges)}/{$currentUser.maxCharges}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Color preview - click to open palette -->
|
||||
{#if selectedColorData}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute -right-2 -top-2 size-8 rounded-full border-2 border-white shadow-md"
|
||||
style="background: {selectedColorData.hex}"
|
||||
on:click|stopPropagation={openPalette}
|
||||
title="Change color"
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { COLORS } from '$lib/constants/colors';
|
||||
|
||||
export let pixelInfo: any;
|
||||
export let coords: { tileX: number; tileY: number; x: number; y: number };
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: color = pixelInfo?.colorId ? COLORS.find((c) => c.id === pixelInfo.colorId) : null;
|
||||
|
||||
function handlePaint() {
|
||||
dispatch('paint');
|
||||
}
|
||||
|
||||
function handleFavorite() {
|
||||
dispatch('favorite');
|
||||
}
|
||||
|
||||
function handleShare() {
|
||||
dispatch('share');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Popup card -->
|
||||
<div class="bg-base-100 rounded-2xl shadow-xl p-4 min-w-[280px]">
|
||||
<!-- Pixel coordinates -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5 text-primary"
|
||||
>
|
||||
<path
|
||||
d="M480-480q33 0 56.5-23.5T560-560q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560q0 33 23.5 56.5T480-480Zm0 294q122-112 181-203.5T720-552q0-109-69.5-178.5T480-800q-101 0-170.5 69.5T240-552q0 71 59 162.5T480-186Zm0 106Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Zm0-480Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-semibold">
|
||||
Pixel: {coords.x},{coords.y}
|
||||
</span>
|
||||
{#if color}
|
||||
<span
|
||||
class="ml-auto size-6 rounded border-2 border-base-300"
|
||||
style="background: {color.hex}"
|
||||
title={color.name}
|
||||
></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Painted by -->
|
||||
{#if pixelInfo?.paintedBy}
|
||||
<div class="text-sm mb-4">
|
||||
Painted by:
|
||||
<button class="link link-primary font-semibold">
|
||||
{pixelInfo.paintedBy.name} #{pixelInfo.paintedBy.id}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary flex-1" on:click={handlePaint}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M240-120q-45 0-89-22t-71-58q26 0 53-20.5t27-59.5q0-50 35-85t85-35q50 0 85 35t35 85q0 66-47 113t-113 47Zm230-240L360-470l358-358q11-11 27.5-11.5T774-828l54 54q12 12 12 28t-12 28L470-360Z"
|
||||
/>
|
||||
</svg>
|
||||
Paint
|
||||
</button>
|
||||
<button class="btn btn-ghost" on:click={handleFavorite} title="Favorite">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-ghost" on:click={handleShare} title="Share">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M720-80q-50 0-85-35t-35-85q0-7 1-14.5t3-13.5L322-392q-17 15-38 23.5t-44 8.5q-50 0-85-35t-35-85q0-50 35-85t85-35q23 0 44 8.5t38 23.5l282-164q-2-6-3-13.5t-1-14.5q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35q-23 0-44-8.5T638-672L356-508q2 6 3 13.5t1 14.5q0 7-1 14.5t-3 13.5l282 164q17-15 38-23.5t44-8.5q50 0 85 35t35 85q0 50-35 85t-85 35Zm0-640q17 0 28.5-11.5T760-760q0-17-11.5-28.5T720-800q-17 0-28.5 11.5T680-760q0 17 11.5 28.5T720-720ZM240-440q17 0 28.5-11.5T280-480q0-17-11.5-28.5T240-520q-17 0-28.5 11.5T200-480q0 17 11.5 28.5T240-440Zm480 280q17 0 28.5-11.5T760-200q0-17-11.5-28.5T720-240q-17 0-28.5 11.5T680-200q0 17 11.5 28.5T720-160Zm0-600ZM240-480Zm480 280Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
type Props = {
|
||||
open?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export let open = false;
|
||||
export let title: string = "Sample Modal";
|
||||
export let description: string = "Use this dialog as a starting point for your own content.";
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: void }>();
|
||||
|
||||
let dialogElement: HTMLDialogElement | null = null;
|
||||
|
||||
onMount(() => {
|
||||
if (open) {
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (dialogElement) {
|
||||
open ? dialogElement.showModal() : dialogElement.close();
|
||||
}
|
||||
|
||||
function show() {
|
||||
dialogElement?.showModal();
|
||||
open = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
dialogElement?.close();
|
||||
open = false;
|
||||
dispatch("close");
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialogElement} class="modal" on:cancel|preventDefault={hide}>
|
||||
<div class="modal-box sm:max-w-xl">
|
||||
<header class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold">{title}</h2>
|
||||
<p class="text-base-content/70 mt-2 text-sm">{description}</p>
|
||||
</div>
|
||||
<button class="btn btn-circle btn-ghost" type="button" on:click={hide}>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<slot />
|
||||
|
||||
<footer class="mt-6 flex justify-end gap-3">
|
||||
<button class="btn btn-ghost" type="button" on:click={hide}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" on:click={hide}>
|
||||
Okay
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<form method="dialog" class="modal-backdrop" aria-label="Close modal">
|
||||
<button type="submit">Close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
:global(.modal.is-open) {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
// Action buttons for Store, Alliance, Explore, Leaderboard
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<!-- Store -->
|
||||
<a href="/store" class="btn btn-square shadow-md" title="Store">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M280-80q-33 0-56.5-23.5T200-160q0-33 23.5-56.5T280-240q33 0 56.5 23.5T360-160q0 33-23.5 56.5T280-80Zm400 0q-33 0-56.5-23.5T600-160q0-33 23.5-56.5T680-240q33 0 56.5 23.5T760-160q0 33-23.5 56.5T680-80ZM246-720l96 200h280l110-200H246Zm-38-80h590q23 0 35 20.5t1 41.5L692-482q-11 20-29.5 31T622-440H324l-44 80h480v80H280q-45 0-68-39.5t-2-78.5l54-98-144-304H40v-80h130l38 80Zm134 280h280-280Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Alliance -->
|
||||
<a href="/alliance" class="btn btn-square shadow-md" title="Alliance">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M40-160v-160q0-34 23.5-57t56.5-23h131q20 0 38 10t29 27q29 39 71.5 61t90.5 22q49 0 91.5-22t70.5-61q13-17 30.5-27t36.5-10h131q34 0 57 23t23 57v160H640v-91q-35 25-75.5 38T480-200q-43 0-84-13.5T320-252v92H40Zm440-160q-38 0-72-17.5T351-386q-17-25-42.5-39.5T253-440q22-37 93-58.5T480-520q63 0 134 21.5t93 58.5q-29 0-55 14.5T609-386q-22 32-56 49t-73 17ZM160-440q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T280-560q0 50-34.5 85T160-440Zm640 0q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T920-560q0 50-34.5 85T800-440ZM480-560q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-680q0 50-34.5 85T480-560Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Explore -->
|
||||
<a href="/explore" class="btn btn-square shadow-md" title="Explore">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q146 0 255.5 91.5T872-559h-82q-19-73-68.5-130.5T600-776v16q0 33-23.5 56.5T520-680h-80v80q0 17-11.5 28.5T400-560h-80v80h80v120h-40L168-552q-3 18-5.5 36t-2.5 36q0 131 92 225t228 95v80Zm364-20L716-228q-21 12-45 20t-51 8q-75 0-127.5-52.5T440-380q0-75 52.5-127.5T620-560q75 0 127.5 52.5T800-380q0 27-8 51t-20 45l128 128-56 56ZM620-280q42 0 71-29t29-71q0-42-29-71t-71-29q-42 0-71 29t-29 71q0 42 29 71t71 29Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<a href="/leaderboard" class="btn btn-square shadow-md" title="Leaderboard">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M160-200h160v-320H160v320Zm240 0h160v-560H400v560Zm240 0h160v-240H640v240ZM80-120v-480h240v-240h320v320h240v400H80Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function zoomIn() {
|
||||
dispatch('zoom', { direction: 'in' });
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
dispatch('zoom', { direction: 'out' });
|
||||
}
|
||||
|
||||
function showInfo() {
|
||||
dispatch('info');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<!-- Info button -->
|
||||
<button title="Info" class="btn btn-sm btn-circle" on:click={showInfo}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
d="M480-680q-33 0-56.5-23.5T400-760q0-33 23.5-56.5T480-840q33 0 56.5 23.5T560-760q0 33-23.5 56.5T480-680Zm-60 560v-480h120v480H420Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Zoom controls -->
|
||||
<div class="flex flex-col gap-1 max-sm:hidden">
|
||||
<button title="Zoom in" class="btn btn-sm btn-circle" on:click={zoomIn}>+</button>
|
||||
<button title="Zoom out" class="btn btn-sm btn-circle" on:click={zoomOut}>-</button>
|
||||
</div>
|
||||
|
||||
<!-- Livestreams link -->
|
||||
<div class="max-sm:hidden">
|
||||
<a
|
||||
href="https://www.twitch.tv/directory/category/wplace"
|
||||
class="btn btn-sm btn-circle"
|
||||
target="_blank"
|
||||
title="Livestreams"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 2400 2800"
|
||||
class="size-4"
|
||||
>
|
||||
<path fill="#fff" d="m2200 1300-400 400h-400l-350 350v-350H600V200h1600z" />
|
||||
<g fill="#9146ff">
|
||||
<path
|
||||
d="M500 0 0 500v1800h600v500l500-500h400l900-900V0H500zm1700 1300-400 400h-400l-350 350v-350H600V200h1600v1100z"
|
||||
/>
|
||||
<path d="M1700 550h200v600h-200zm-550 0h200v600h-200z" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { currentUser } from '$lib/stores/user';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
$: alliance = $currentUser?.alliance;
|
||||
$: role = $currentUser?.allianceRole;
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Alliance</h2>
|
||||
|
||||
{#if alliance}
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Alliance Name</p>
|
||||
<p class="text-lg font-semibold">{alliance.name}</p>
|
||||
</div>
|
||||
|
||||
{#if alliance.description}
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Description</p>
|
||||
<p class="text-sm">{alliance.description}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Your Role</p>
|
||||
<p>
|
||||
<span class="badge badge-primary">
|
||||
{role === 'owner' ? 'Owner' : role === 'admin' ? 'Admin' : 'Member'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Alliance Pixels</p>
|
||||
<p class="text-lg font-semibold">{alliance.pixelsPainted.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<a href="/alliance" class="btn btn-primary btn-sm">
|
||||
Manage Alliance
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-base-content/60">
|
||||
You are not part of an alliance. Join one to collaborate with other players!
|
||||
</p>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<a href="/alliance" class="btn btn-primary btn-sm">
|
||||
Create or Join Alliance
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,209 @@
|
||||
<script lang="ts">
|
||||
import { currentUser } from '$lib/stores/user';
|
||||
import { api } from '$lib/api/client';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface FavoriteLocation {
|
||||
id: number;
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
let locations: FavoriteLocation[] = [];
|
||||
let loading = true;
|
||||
let error = '';
|
||||
let addingNew = false;
|
||||
let editingId: number | null = null;
|
||||
|
||||
let newName = '';
|
||||
let newLat = 0;
|
||||
let newLng = 0;
|
||||
|
||||
$: if ($currentUser) {
|
||||
loadLocations();
|
||||
}
|
||||
|
||||
async function loadLocations() {
|
||||
if (!$currentUser) return;
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const userData = await api.getMe();
|
||||
// Assuming favorite locations are part of user data
|
||||
// The API might need to be extended if not available
|
||||
locations = (userData as any).favoriteLocations || [];
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load locations';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startAdd() {
|
||||
addingNew = true;
|
||||
newName = '';
|
||||
newLat = 0;
|
||||
newLng = 0;
|
||||
error = '';
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
addingNew = false;
|
||||
editingId = null;
|
||||
}
|
||||
|
||||
async function saveLocation() {
|
||||
if (!newName.trim()) return;
|
||||
|
||||
error = '';
|
||||
try {
|
||||
if (editingId) {
|
||||
await api.updateFavoriteLocation(editingId, {
|
||||
name: newName.trim(),
|
||||
latitude: newLat,
|
||||
longitude: newLng
|
||||
});
|
||||
} else {
|
||||
await api.addFavoriteLocation({
|
||||
name: newName.trim(),
|
||||
latitude: newLat,
|
||||
longitude: newLng
|
||||
});
|
||||
}
|
||||
await loadLocations();
|
||||
addingNew = false;
|
||||
editingId = null;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to save location';
|
||||
}
|
||||
}
|
||||
|
||||
function editLocation(location: FavoriteLocation) {
|
||||
editingId = location.id;
|
||||
addingNew = true;
|
||||
newName = location.name;
|
||||
newLat = location.latitude;
|
||||
newLng = location.longitude;
|
||||
}
|
||||
|
||||
function goToLocation(lat: number, lng: number) {
|
||||
// This would need to communicate with the map component
|
||||
// For now, just show coordinates
|
||||
console.log('Navigate to:', lat, lng);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Favorite Locations</h2>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if error && !addingNew}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each locations as location (location.id)}
|
||||
<div class="flex items-center justify-between p-3 rounded-box bg-base-200 hover:bg-base-300 transition">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{location.name}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
{location.latitude.toFixed(4)}, {location.longitude.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-xs"
|
||||
on:click={() => goToLocation(location.latitude, location.longitude)}
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-xs" on:click={() => editLocation(location)}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-base-content/60 py-4 text-center">
|
||||
No favorite locations yet. Add one to save your favorite spots!
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if addingNew}
|
||||
<div class="divider"></div>
|
||||
<div class="space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label" for="loc-name">
|
||||
<span class="label-text">Location Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="loc-name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
bind:value={newName}
|
||||
placeholder="My favorite spot"
|
||||
maxlength="50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="form-control">
|
||||
<label class="label" for="loc-lat">
|
||||
<span class="label-text text-xs">Latitude</span>
|
||||
</label>
|
||||
<input
|
||||
id="loc-lat"
|
||||
type="number"
|
||||
class="input input-bordered input-sm"
|
||||
bind:value={newLat}
|
||||
step="0.0001"
|
||||
min="-90"
|
||||
max="90"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="loc-lng">
|
||||
<span class="label-text text-xs">Longitude</span>
|
||||
</label>
|
||||
<input
|
||||
id="loc-lng"
|
||||
type="number"
|
||||
class="input input-bordered input-sm"
|
||||
bind:value={newLng}
|
||||
step="0.0001"
|
||||
min="-180"
|
||||
max="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error alert-sm">
|
||||
<span class="text-xs">{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button class="btn btn-ghost btn-sm" on:click={cancelAdd}>
|
||||
{$t('cancel')}
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" on:click={saveLocation} disabled={!newName.trim()}>
|
||||
{$t('save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="btn btn-primary btn-sm btn-block mt-4" on:click={startAdd}>
|
||||
Add Location
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script lang="ts">
|
||||
import { currentUser, userLevel } from '$lib/stores/user';
|
||||
import { api } from '$lib/api/client';
|
||||
import { t } from '$lib/i18n';
|
||||
import { getLevelProgress } from '$lib/utils/level';
|
||||
|
||||
let editing = false;
|
||||
let name = '';
|
||||
let discord = '';
|
||||
let showLastPixel = false;
|
||||
let saving = false;
|
||||
let error = '';
|
||||
|
||||
$: if ($currentUser && !editing) {
|
||||
name = $currentUser.name;
|
||||
discord = $currentUser.discord || '';
|
||||
showLastPixel = $currentUser.showLastPixel;
|
||||
}
|
||||
|
||||
$: levelProgress = $currentUser ? getLevelProgress($currentUser.pixelsPainted) : 0;
|
||||
|
||||
function startEdit() {
|
||||
editing = true;
|
||||
error = '';
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing = false;
|
||||
error = '';
|
||||
if ($currentUser) {
|
||||
name = $currentUser.name;
|
||||
discord = $currentUser.discord || '';
|
||||
showLastPixel = $currentUser.showLastPixel;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
if (!$currentUser) return;
|
||||
|
||||
saving = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
await currentUser.updateProfile({
|
||||
name: name.trim(),
|
||||
discord: discord.trim() || undefined,
|
||||
showLastPixel
|
||||
});
|
||||
editing = false;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update profile';
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $currentUser}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">User Profile</h2>
|
||||
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-16 h-16">
|
||||
{#if $currentUser.picture}
|
||||
<img src={$currentUser.picture} alt={$currentUser.name} />
|
||||
{:else}
|
||||
<span class="text-2xl">{$currentUser.name[0].toUpperCase()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">{$currentUser.name}</h3>
|
||||
<p class="text-sm text-base-content/60">Level {$userLevel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !editing}
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Username</p>
|
||||
<p>{$currentUser.name}</p>
|
||||
</div>
|
||||
|
||||
{#if $currentUser.discord}
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Discord</p>
|
||||
<p>{$currentUser.discord}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Pixels Painted</p>
|
||||
<p>{$currentUser.pixelsPainted.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Level Progress</p>
|
||||
<progress class="progress progress-primary w-full" value={levelProgress} max="100"></progress>
|
||||
<p class="text-xs text-base-content/60 mt-1">{levelProgress.toFixed(1)}% to Level {$userLevel + 1}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Droplets</p>
|
||||
<p class="text-lg font-semibold">{$currentUser.droplets.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={$currentUser.showLastPixel} disabled />
|
||||
<span class="label-text">Show last painted pixel</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button class="btn btn-primary btn-sm" on:click={startEdit}>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label" for="name">
|
||||
<span class="label-text">Username</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={name}
|
||||
maxlength="32"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="discord">
|
||||
<span class="label-text">Discord (optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="discord"
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={discord}
|
||||
maxlength="32"
|
||||
placeholder="username#0000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox" bind:checked={showLastPixel} />
|
||||
<span class="label-text">Show last painted pixel</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4 gap-2">
|
||||
<button class="btn btn-ghost btn-sm" on:click={cancelEdit} disabled={saving}>
|
||||
{$t('cancel')}
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" on:click={saveProfile} disabled={saving || !name.trim()}>
|
||||
{#if saving}Saving...{:else}{$t('save')}{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,408 @@
|
||||
<script lang="ts">
|
||||
import { currentUser, userLevel } from '$lib/stores/user';
|
||||
import { getLevelProgress } from '$lib/utils/level';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
|
||||
let dropdownOpen = false;
|
||||
let showEditProfile = false;
|
||||
let isMuted = false;
|
||||
|
||||
$: levelProgress = $currentUser ? getLevelProgress($currentUser.pixelsPainted) : 0;
|
||||
$: svgURI = $currentUser
|
||||
? 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon($currentUser.name, 90, 50))
|
||||
: '';
|
||||
|
||||
function toggleDropdown() {
|
||||
dropdownOpen = !dropdownOpen;
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
dropdownOpen = false;
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $currentUser}
|
||||
<div class="relative dropdown dropdown-end">
|
||||
<!-- Profile Button -->
|
||||
<button
|
||||
on:click={toggleDropdown}
|
||||
class="btn size-12 p-0 shadow-md bg-primary"
|
||||
title="Show profile"
|
||||
>
|
||||
<div class="relative w-max">
|
||||
<!-- Background circle -->
|
||||
<div class="bg-base-content/20 size-12 rounded-full"></div>
|
||||
|
||||
<!-- Level progress ring -->
|
||||
<div
|
||||
class="level-fill center-absolute absolute size-12 rotate-[215deg] rounded-full"
|
||||
style="--angle: {(levelProgress / 100) * 360}deg; --color: var(--color-secondary);"
|
||||
></div>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="avatar center-absolute absolute">
|
||||
<div class="size-10 rounded-full">
|
||||
<div class="bg-base-200 minidenticon">
|
||||
{@html minidenticon($currentUser.name, 90, 50)}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit profile button -->
|
||||
<button
|
||||
class="btn btn-circle btn-sm absolute -bottom-1 -right-1"
|
||||
on:click|stopPropagation={() => (showEditProfile = true)}
|
||||
title="Edit profile"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Level badge -->
|
||||
<div
|
||||
class="text-primary-content bg-secondary absolute bottom-0 left-0 -left-1 flex items-center justify-center rounded-full px-[5px] py-0 text-xs font-bold"
|
||||
>
|
||||
{$userLevel}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
{#if dropdownOpen}
|
||||
<div
|
||||
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-50 relative right-1 w-[min(100vw-24px,400px)] translate-y-2 border p-4 shadow-md"
|
||||
>
|
||||
<!-- Close button -->
|
||||
<button on:click={closeDropdown} class="btn btn-ghost btn-circle absolute right-2 top-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Profile Info -->
|
||||
<section class="flex gap-2">
|
||||
<div class="relative">
|
||||
<div class="avatar relative rounded-full border-3 border-primary">
|
||||
<div class="border-base-300 size-20 rounded-full border-2">
|
||||
<div class="bg-base-200 minidenticon">
|
||||
{@html minidenticon($currentUser.name, 90, 50)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-1.5 pr-8 text-lg font-medium">
|
||||
<h3 class="line-clamp-1 text-ellipsis text-lg" title={$currentUser.name}>
|
||||
{$currentUser.name}
|
||||
</h3>
|
||||
<span class="text-rose-500">#{$currentUser.id}</span>
|
||||
{#if $currentUser.country}
|
||||
<span class="tooltip font-flag ml-0.5" data-tip={$currentUser.country}>
|
||||
{$currentUser.country === 'US' ? '🇺🇸' : '🇧🇷'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $currentUser.discord}
|
||||
<div class="mt-1">
|
||||
<span class="tooltip h-4">
|
||||
<div class="tooltip-content">
|
||||
<span>Discord: {$currentUser.discord}</span>
|
||||
</div>
|
||||
<button class="flex items-center gap-1 text-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 127.14 96.36"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="inline size-4"
|
||||
>
|
||||
<path
|
||||
d="M240-120q-45 0-89-22t-71-58q26 0 53-20.5t27-59.5q0-50 35-85t85-35q50 0 85 35t35 85q0 66-47 113t-113 47Zm230-240L360-470l358-358q11-11 27.5-11.5T774-828l54 54q12 12 12 28t-12 28L470-360Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
>Pixels painted: <span class="text-primary font-semibold"
|
||||
>{$currentUser.pixelsPainted.toLocaleString()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="inline size-4"
|
||||
>
|
||||
<path
|
||||
d="M440-160v-487L216-423l-56-57 320-320 320 320-56 57-224-224v487h-80Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-secondary">
|
||||
<span class="font-semibold">Level {$userLevel}</span> ({levelProgress.toFixed(0)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<section class="mt-3 flex flex-col gap-2">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Menu</h3>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Language selector -->
|
||||
<div class="dropdown dropdown-end">
|
||||
<button class="btn btn-sm btn-circle" tabindex="0" title="Language">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 border border-base-300 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<button class="font-flag">🇺🇸 English</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="font-flag">🇧🇷 Português</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Mute button -->
|
||||
<button
|
||||
class="btn btn-sm btn-circle"
|
||||
on:click={toggleMute}
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{#if isMuted}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Zm-80 238v-94l-72-72H200v80h114l86 86Zm-36-130Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="btn w-full" href="/profile">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"
|
||||
/>
|
||||
</svg>
|
||||
Profile
|
||||
</a>
|
||||
|
||||
<a class="btn w-full" href="/alliance">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M40-160v-160q0-34 23.5-57t56.5-23h131q20 0 38 10t29 27q29 39 71.5 61t90.5 22q49 0 91.5-22t70.5-61q13-17 30.5-27t36.5-10h131q34 0 57 23t23 57v160H640v-91q-35 25-75.5 38T480-200q-43 0-84-13.5T320-252v92H40Zm440-160q-38 0-72-17.5T351-386q-17-25-42.5-39.5T253-440q22-37 93-58.5T480-520q63 0 134 21.5t93 58.5q-29 0-55 14.5T609-386q-22 32-56 49t-73 17Z"
|
||||
/>
|
||||
</svg>
|
||||
Alliance
|
||||
</a>
|
||||
|
||||
<a class="btn w-full" href="/store">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M280-80q-33 0-56.5-23.5T200-160q0-33 23.5-56.5T280-240q33 0 56.5 23.5T360-160q0 33-23.5 56.5T280-80Zm400 0q-33 0-56.5-23.5T600-160q0-33 23.5-56.5T680-240q33 0 56.5 23.5T760-160q0 33-23.5 56.5T680-80ZM246-720l96 200h280l110-200H246Zm-38-80h590q23 0 35 20.5t1 41.5L692-482q-11 20-29.5 31T622-440H324l-44 80h480v80H280q-45 0-68-39.5t-2-78.5l54-98-144-304H40v-80h130l38 80Z"
|
||||
/>
|
||||
</svg>
|
||||
Store
|
||||
</a>
|
||||
|
||||
<a class="btn w-full" href="/leaderboard">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M160-200h160v-320H160v320Zm240 0h160v-560H400v560Zm240 0h160v-240H640v240ZM80-120v-480h240v-240h320v320h240v400H80Z"
|
||||
/>
|
||||
</svg>
|
||||
Leaderboard
|
||||
</a>
|
||||
|
||||
<!-- Social Media Buttons -->
|
||||
<a
|
||||
class="btn w-full"
|
||||
href="https://www.twitch.tv/directory/category/wplace"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 268"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M17.458 0L0 46.556v186.201h63.983v34.934h34.931l34.898-34.934h52.36L256 162.954V0H17.458zm23.259 23.263H232.73v128.029l-40.739 40.741H128L93.113 226.92v-34.886H40.717V23.263zm64.008 116.405H128V69.844h-23.275v69.824zm63.997 0h23.27V69.844h-23.27v69.824z"
|
||||
/>
|
||||
</svg>
|
||||
Livestreams
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="btn w-full"
|
||||
href="https://discord.gg/wplace"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 127.14 96.36"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
|
||||
/>
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="btn w-full"
|
||||
href="https://www.reddit.com/r/wplace_unofficial"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0zm4.388 3.199c.605 0 1.126.21 1.551.628.426.418.64.948.64 1.59 0 .613-.213 1.128-.64 1.546-.425.418-.946.628-1.551.628-.578 0-1.075-.21-1.486-.628-.412-.418-.619-.933-.619-1.546 0-.642.207-1.172.619-1.59.411-.418.908-.628 1.486-.628zM5.423 9.636c.853 0 1.573.3 2.16.902.586.6.879 1.334.879 2.201 0 .866-.293 1.6-.879 2.2-.587.6-1.307.9-2.16.9-.852 0-1.573-.3-2.159-.9-.587-.6-.88-1.334-.88-2.2 0-.867.293-1.601.88-2.2.586-.602 1.307-.903 2.159-.903zm13.154 0c.853 0 1.574.3 2.161.902.586.6.879 1.334.879 2.201 0 .866-.293 1.6-.879 2.2-.587.6-1.308.9-2.161.9-.852 0-1.573-.3-2.159-.9-.587-.6-.88-1.334-.88-2.2 0-.867.293-1.601.88-2.2.586-.602 1.307-.903 2.159-.903zM12 15.314c1.777 0 3.309.48 4.597 1.44.964.72 1.446 1.584 1.446 2.592H5.957c0-1.008.482-1.872 1.445-2.592 1.289-.96 2.821-1.44 4.598-1.44z"
|
||||
/>
|
||||
</svg>
|
||||
Reddit
|
||||
</a>
|
||||
|
||||
<button class="btn w-full" on:click={() => currentUser.logout()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h280v80H200Zm440-160-55-58 102-102H360v-80h327L585-622l55-58 200 200-200 200Z"
|
||||
/>
|
||||
</svg>
|
||||
Log Out
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<a href="/join" class="btn btn-primary shadow-md">Login</a>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.level-fill {
|
||||
background: conic-gradient(
|
||||
var(--color) 0deg,
|
||||
var(--color) var(--angle),
|
||||
transparent var(--angle)
|
||||
);
|
||||
-webkit-mask: radial-gradient(
|
||||
farthest-side,
|
||||
transparent calc(100% - 3px),
|
||||
white calc(100% - 2px)
|
||||
);
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 3px), white calc(100% - 2px));
|
||||
}
|
||||
|
||||
.center-absolute {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.minidenticon :global(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,210 +1,263 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import Header from '../lib/components/layout/Header.svelte';
|
||||
import CanvasMap from '../lib/components/canvas/CanvasMap.svelte';
|
||||
import ColorPalette from '../lib/components/canvas/ColorPalette.svelte';
|
||||
import { api, ApiError } from '../lib/api/client';
|
||||
import { currentUser, currentCharges } from '../lib/stores/user';
|
||||
import { selectedColor } from '../lib/stores/canvas';
|
||||
import { online } from '../lib/stores/global';
|
||||
import { calculateLevel } from '../lib/utils/level';
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import CanvasMap from '../lib/components/canvas/CanvasMap.svelte';
|
||||
import UserProfileButton from '../lib/components/user/UserProfileButton.svelte';
|
||||
import PaintButton from '../lib/components/canvas/PaintButton.svelte';
|
||||
import ActionButtons from '../lib/components/layout/ActionButtons.svelte';
|
||||
import MapControls from '../lib/components/layout/MapControls.svelte';
|
||||
import ColorPaletteModal from '../lib/components/canvas/ColorPaletteModal.svelte';
|
||||
import PixelInfoPopup from '../lib/components/canvas/PixelInfoPopup.svelte';
|
||||
import { api, ApiError } from '../lib/api/client';
|
||||
import { currentUser, currentCharges } from '../lib/stores/user';
|
||||
import { selectedColor } from '../lib/stores/canvas';
|
||||
import { online } from '../lib/stores/global';
|
||||
|
||||
interface SelectedPixel {
|
||||
tileX: number;
|
||||
tileY: number;
|
||||
x: number;
|
||||
y: number;
|
||||
latlng: { lat: number; lng: number };
|
||||
}
|
||||
interface SelectedPixel {
|
||||
tileX: number;
|
||||
tileY: number;
|
||||
x: number;
|
||||
y: number;
|
||||
latlng: { lat: number; lng: number };
|
||||
}
|
||||
|
||||
interface PixelInfo {
|
||||
paintedBy: {
|
||||
id: number;
|
||||
name: string;
|
||||
allianceId: number;
|
||||
allianceName: string;
|
||||
equippedFlag: number;
|
||||
};
|
||||
region: Record<string, unknown> | null;
|
||||
}
|
||||
const SEASON = 's0';
|
||||
|
||||
const SEASON = 's0';
|
||||
let mapRef: CanvasMap | null = null;
|
||||
let selected: SelectedPixel | null = null;
|
||||
let pixelInfo: any = null;
|
||||
let loadingPixel = false;
|
||||
let painting = false;
|
||||
let errorMessage = '';
|
||||
let successMessage = '';
|
||||
let showColorPalette = false;
|
||||
|
||||
let mapRef: CanvasMap | null = null;
|
||||
let selected: SelectedPixel | null = null;
|
||||
let pixelInfo: PixelInfo | null = null;
|
||||
let loadingPixel = false;
|
||||
let painting = false;
|
||||
let errorMessage = '';
|
||||
let successMessage = '';
|
||||
onMount(() => {
|
||||
currentUser.fetch();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
currentUser.fetch();
|
||||
});
|
||||
async function handlePixel(event: CustomEvent<SelectedPixel>) {
|
||||
selected = event.detail;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
pixelInfo = null;
|
||||
await loadPixelInfo(event.detail);
|
||||
}
|
||||
|
||||
async function handlePixel(event: CustomEvent<SelectedPixel>) {
|
||||
selected = event.detail;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
pixelInfo = null;
|
||||
await loadPixelInfo(event.detail);
|
||||
}
|
||||
async function loadPixelInfo(selection: SelectedPixel) {
|
||||
loadingPixel = true;
|
||||
try {
|
||||
pixelInfo = await api.getPixelInfo(
|
||||
SEASON,
|
||||
selection.tileX,
|
||||
selection.tileY,
|
||||
selection.x,
|
||||
selection.y
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to load pixel info:', error);
|
||||
} finally {
|
||||
loadingPixel = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPixelInfo(selection: SelectedPixel) {
|
||||
loadingPixel = true;
|
||||
try {
|
||||
pixelInfo = await api.getPixelInfo(SEASON, selection.tileX, selection.tileY, selection.x, selection.y);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
errorMessage = error.message;
|
||||
} else {
|
||||
errorMessage = 'Failed to load pixel info.';
|
||||
}
|
||||
} finally {
|
||||
loadingPixel = false;
|
||||
}
|
||||
}
|
||||
function canPaint(): boolean {
|
||||
const user = get(currentUser);
|
||||
if (!user || !selected) return false;
|
||||
const color = get(selectedColor);
|
||||
if (color === 0) return false;
|
||||
const charges = get(currentCharges);
|
||||
return charges > 0 && get(online);
|
||||
}
|
||||
|
||||
function canPaint(): boolean {
|
||||
const user = get(currentUser);
|
||||
if (!user || !selected) return false;
|
||||
const color = get(selectedColor);
|
||||
if (color === 0) return false;
|
||||
const charges = get(currentCharges);
|
||||
return charges > 0;
|
||||
}
|
||||
async function paint() {
|
||||
if (!selected) return;
|
||||
const user = get(currentUser);
|
||||
if (!user) {
|
||||
errorMessage = 'Login required to paint.';
|
||||
return;
|
||||
}
|
||||
const color = get(selectedColor);
|
||||
if (color === 0) {
|
||||
errorMessage = 'Select a color to paint.';
|
||||
return;
|
||||
}
|
||||
const charges = get(currentCharges);
|
||||
if (charges <= 0) {
|
||||
errorMessage = 'Not enough charges.';
|
||||
return;
|
||||
}
|
||||
|
||||
async function paint() {
|
||||
if (!selected) return;
|
||||
const user = get(currentUser);
|
||||
if (!user) {
|
||||
errorMessage = 'Login required to paint.';
|
||||
return;
|
||||
}
|
||||
const color = get(selectedColor);
|
||||
if (color === 0) {
|
||||
errorMessage = 'Select a color to paint.';
|
||||
return;
|
||||
}
|
||||
const charges = get(currentCharges);
|
||||
if (charges <= 0) {
|
||||
errorMessage = 'Not enough charges.';
|
||||
return;
|
||||
}
|
||||
painting = true;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
|
||||
painting = true;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
|
||||
try {
|
||||
await api.paintPixels(SEASON, selected.tileX, selected.tileY, {
|
||||
colors: [color],
|
||||
coords: [selected.x, selected.y]
|
||||
});
|
||||
await currentUser.fetch();
|
||||
await loadPixelInfo(selected);
|
||||
mapRef?.refreshTiles();
|
||||
successMessage = 'Pixel painted!';
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
errorMessage = error.message;
|
||||
} else {
|
||||
errorMessage = 'Failed to paint pixel.';
|
||||
}
|
||||
} finally {
|
||||
painting = false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await api.paintPixels(SEASON, selected.tileX, selected.tileY, {
|
||||
colors: [color],
|
||||
coords: [selected.x, selected.y]
|
||||
});
|
||||
await currentUser.fetch();
|
||||
await loadPixelInfo(selected);
|
||||
mapRef?.refreshTiles();
|
||||
successMessage = 'Pixel painted!';
|
||||
setTimeout(() => (successMessage = ''), 2000);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
errorMessage = error.message;
|
||||
} else {
|
||||
errorMessage = 'Failed to paint pixel.';
|
||||
}
|
||||
} finally {
|
||||
painting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Header />
|
||||
<svelte:head>
|
||||
<title>wplace - Paint the World</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto w-full max-w-7xl px-4 py-6 gap-6 flex flex-col lg:flex-row">
|
||||
<section class="flex-1 h-[70vh] lg:h-[calc(100vh-8rem)]">
|
||||
<CanvasMap bind:this={mapRef} on:pixel={handlePixel} />
|
||||
</section>
|
||||
<aside class="w-full lg:max-w-md flex flex-col gap-6">
|
||||
<section class="rounded-box border border-base-300 bg-base-100 p-4 shadow">
|
||||
{#if $currentUser}
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Hello, {$currentUser.name}</h2>
|
||||
<p class="text-sm text-base-content/60">Level {calculateLevel($currentUser.pixelsPainted)} · {$currentUser.pixelsPainted.toLocaleString()} pixels</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="rounded-box bg-base-200 p-3">
|
||||
<p class="text-xs uppercase text-base-content/60">Charges</p>
|
||||
<p class="text-lg font-semibold">{$currentCharges}/{$currentUser.maxCharges}</p>
|
||||
</div>
|
||||
<div class="rounded-box bg-base-200 p-3">
|
||||
<p class="text-xs uppercase text-base-content/60">Droplets</p>
|
||||
<p class="text-lg font-semibold">{$currentUser.droplets.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-base-content/70">
|
||||
Login to start painting the world.
|
||||
<a href="/join" class="btn btn-sm btn-primary mt-3">Login</a>
|
||||
</p>
|
||||
{/if}
|
||||
{#if !$online}
|
||||
<p class="mt-4 rounded-box bg-warning/20 px-3 py-2 text-sm text-warning">You are offline. Painting is disabled.</p>
|
||||
{/if}
|
||||
</section>
|
||||
<!-- Full-screen map container -->
|
||||
<div class="disable-pinch-zoom relative h-screen w-screen overflow-hidden">
|
||||
<!-- Map -->
|
||||
<div class="h-screen w-screen">
|
||||
<CanvasMap bind:this={mapRef} on:pixel={handlePixel} />
|
||||
</div>
|
||||
|
||||
<section class="rounded-box border border-base-300 bg-base-100 p-4 shadow">
|
||||
<h2 class="text-base font-semibold">Palette</h2>
|
||||
<ColorPalette />
|
||||
<button
|
||||
class="btn btn-primary btn-block mt-4"
|
||||
on:click={paint}
|
||||
disabled={!canPaint() || painting || !$online}
|
||||
>
|
||||
{#if painting}Painting...{:else}Paint Pixel{/if}
|
||||
</button>
|
||||
{#if errorMessage}
|
||||
<p class="mt-3 rounded-box bg-error/10 px-3 py-2 text-sm text-error">{errorMessage}</p>
|
||||
{/if}
|
||||
{#if successMessage}
|
||||
<p class="mt-3 rounded-box bg-success/10 px-3 py-2 text-sm text-success">{successMessage}</p>
|
||||
{/if}
|
||||
</section>
|
||||
<!-- Top-right: User profile & actions -->
|
||||
<div class="absolute right-2 top-2 z-30">
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<UserProfileButton />
|
||||
<ActionButtons />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-box border border-base-300 bg-base-100 p-4 shadow">
|
||||
<h2 class="text-base font-semibold">Pixel details</h2>
|
||||
{#if !selected}
|
||||
<p class="mt-2 text-sm text-base-content/60">Click the map to inspect a pixel.</p>
|
||||
{:else if loadingPixel}
|
||||
<p class="mt-2 text-sm text-base-content/60">Loading...</p>
|
||||
{:else if pixelInfo}
|
||||
<div class="mt-3 space-y-3 text-sm">
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Coordinates</p>
|
||||
<p>{selected.tileX}, {selected.tileY} · {selected.x}, {selected.y}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Painter</p>
|
||||
{#if pixelInfo.paintedBy.id !== 0}
|
||||
<p>{pixelInfo.paintedBy.name}</p>
|
||||
{#if pixelInfo.paintedBy.allianceName}
|
||||
<p class="text-base-content/60">Alliance: {pixelInfo.paintedBy.allianceName}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Unpainted</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if pixelInfo.region}
|
||||
<div>
|
||||
<p class="text-xs uppercase text-base-content/50">Region</p>
|
||||
<p>{pixelInfo.region?.name ?? 'Unknown'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="mt-2 text-sm text-base-content/60">No data for this pixel.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
<!-- Top-left: Map controls -->
|
||||
<div class="absolute left-2 top-2 z-30">
|
||||
<MapControls
|
||||
on:zoom={(e) => {
|
||||
if (e.detail.direction === 'in') mapRef?.getMap()?.zoomIn();
|
||||
else mapRef?.getMap()?.zoomOut();
|
||||
}}
|
||||
on:info={() => alert('wplace - Collaborative pixel art canvas')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-center: Paint button and tooltip -->
|
||||
<div class="absolute bottom-3 left-1/2 z-30 -translate-x-1/2 flex flex-col items-center gap-2">
|
||||
<!-- Tooltip message -->
|
||||
{#if selected && !painting}
|
||||
<div class="bg-base-100 rounded-full px-4 py-2 shadow-md flex items-center gap-2 text-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M240-120q-45 0-89-22t-71-58q26 0 53-20.5t27-59.5q0-50 35-85t85-35q50 0 85 35t35 85q0 66-47 113t-113 47Zm230-240L360-470l358-358q11-11 27.5-11.5T774-828l54 54q12 12 12 28t-12 28L470-360Z"
|
||||
/>
|
||||
</svg>
|
||||
Click or hold <kbd class="kbd kbd-sm">SPACE</kbd> to paint.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<PaintButton
|
||||
disabled={!canPaint() || painting}
|
||||
{painting}
|
||||
on:click={paint}
|
||||
on:openPalette={() => (showColorPalette = true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-left: Toggle art opacity -->
|
||||
<div class="absolute bottom-3 left-3 z-30">
|
||||
<button
|
||||
title="Toggle art opacity"
|
||||
class="btn btn-lg btn-square sm:btn-xl shadow-md text-base-content/80"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M440-440v-80h80v80h-80Zm-80 80v-80h80v80h-80Zm160 0v-80h80v80h-80Zm80-80v-80h80v80h-80Zm-320 0v-80h80v80h-80Zm-80 320q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm80-80h80v-80h-80v80Zm160 0h80v-80h-80v80Zm320 0v-80 80Zm-560-80h80v-80h80v80h80v-80h80v80h80v-80h80v80h80v-80h-80v-80h80v-320H200v320h80v80h-80v80Zm0 80v-560 560Zm560-240v80-80ZM600-280v80h80v-80h-80Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-right: My location -->
|
||||
<div class="absolute bottom-3 right-3 z-30">
|
||||
<button title="My location" class="btn btn-lg btn-square sm:btn-xl shadow-md">
|
||||
<div class="relative">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="size-5.5 fill-red-400"
|
||||
>
|
||||
<path
|
||||
d="M440-40v-80q-125-14-214.5-103.5T122-438H42v-80h80q14-125 103.5-214.5T440-836v-80h80v80q125 14 214.5 103.5T838-518h80v80h-80q-14 125-103.5 214.5T520-120v80h-80Zm40-158q116 0 198-82t82-198q0-116-82-198t-198-82q-116 0-198 82t-82 198q0 116 82 198t198 82Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="center-absolute absolute text-[10px] text-red-400">?</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pixel Info Popup -->
|
||||
{#if selected && pixelInfo && !showColorPalette}
|
||||
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-40">
|
||||
<PixelInfoPopup
|
||||
{pixelInfo}
|
||||
coords={{ tileX: selected.tileX, tileY: selected.tileY, x: selected.x, y: selected.y }}
|
||||
on:paint={paint}
|
||||
on:favorite={() => console.log('Favorite')}
|
||||
on:share={() => console.log('Share')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Color Palette Modal -->
|
||||
{#if showColorPalette}
|
||||
<ColorPaletteModal on:close={() => (showColorPalette = false)} />
|
||||
{/if}
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
{#if successMessage}
|
||||
<div class="absolute top-20 left-1/2 -translate-x-1/2 z-40">
|
||||
<div class="alert alert-success shadow-lg">
|
||||
<span>{successMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="absolute top-20 left-1/2 -translate-x-1/2 z-40">
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.disable-pinch-zoom {
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
.center-absolute {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
:global(.size-5\.5) {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { currentUser } from '$lib/stores/user';
|
||||
import { api } from '$lib/api/client';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
let loading = true;
|
||||
let error = '';
|
||||
let success = '';
|
||||
|
||||
// Alliance creation
|
||||
let creatingAlliance = false;
|
||||
let newAllianceName = '';
|
||||
|
||||
// Join alliance
|
||||
let joiningAlliance = false;
|
||||
let inviteCode = '';
|
||||
|
||||
// Alliance details
|
||||
let allianceDetails: any = null;
|
||||
let members: any[] = [];
|
||||
let invites: any[] = [];
|
||||
|
||||
// Editing
|
||||
let editingDescription = false;
|
||||
let newDescription = '';
|
||||
|
||||
$: hasAlliance = $currentUser?.allianceId !== null;
|
||||
$: isOwnerOrAdmin = $currentUser?.allianceRole === 'owner' || $currentUser?.allianceRole === 'admin';
|
||||
|
||||
onMount(async () => {
|
||||
await loadAllianceData();
|
||||
});
|
||||
|
||||
async function loadAllianceData() {
|
||||
if (!$currentUser) return;
|
||||
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
if (hasAlliance) {
|
||||
allianceDetails = await api.getAlliance();
|
||||
const membersResult = await api.getAllianceMembers(1);
|
||||
members = membersResult?.members || [];
|
||||
|
||||
if (isOwnerOrAdmin) {
|
||||
const invitesResult = await api.getAllianceInvites();
|
||||
invites = invitesResult?.invites || [];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load alliance data';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createAlliance() {
|
||||
if (!newAllianceName.trim()) return;
|
||||
|
||||
creatingAlliance = true;
|
||||
error = '';
|
||||
success = '';
|
||||
|
||||
try {
|
||||
await api.createAlliance(newAllianceName.trim());
|
||||
await currentUser.fetch();
|
||||
await loadAllianceData();
|
||||
success = 'Alliance created successfully!';
|
||||
newAllianceName = '';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create alliance';
|
||||
} finally {
|
||||
creatingAlliance = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function joinAlliance() {
|
||||
if (!inviteCode.trim()) return;
|
||||
|
||||
joiningAlliance = true;
|
||||
error = '';
|
||||
success = '';
|
||||
|
||||
try {
|
||||
await api.joinAlliance(inviteCode.trim());
|
||||
await currentUser.fetch();
|
||||
await loadAllianceData();
|
||||
success = 'Joined alliance successfully!';
|
||||
inviteCode = '';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to join alliance';
|
||||
} finally {
|
||||
joiningAlliance = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateDescription() {
|
||||
if (!newDescription.trim()) return;
|
||||
|
||||
error = '';
|
||||
success = '';
|
||||
|
||||
try {
|
||||
await api.updateAllianceDescription(newDescription.trim());
|
||||
await loadAllianceData();
|
||||
editingDescription = false;
|
||||
success = 'Description updated successfully!';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update description';
|
||||
}
|
||||
}
|
||||
|
||||
function startEditDescription() {
|
||||
newDescription = allianceDetails?.description || '';
|
||||
editingDescription = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Alliance - FurryPlace</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-6">
|
||||
<h1 class="text-3xl font-bold mb-6">Alliance</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-6">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="alert alert-success mb-6">
|
||||
<span>{success}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if !hasAlliance}
|
||||
<!-- Create or Join Alliance -->
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<!-- Create Alliance -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Create Alliance</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Start your own alliance and invite other players to join you.
|
||||
</p>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label" for="alliance-name">
|
||||
<span class="label-text">Alliance Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="alliance-name"
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={newAllianceName}
|
||||
placeholder="My Awesome Alliance"
|
||||
maxlength="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={createAlliance}
|
||||
disabled={creatingAlliance || !newAllianceName.trim()}
|
||||
>
|
||||
{creatingAlliance ? 'Creating...' : 'Create Alliance'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Join Alliance -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Join Alliance</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Have an invite code? Join an existing alliance here.
|
||||
</p>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label" for="invite-code">
|
||||
<span class="label-text">Invite Code</span>
|
||||
</label>
|
||||
<input
|
||||
id="invite-code"
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={inviteCode}
|
||||
placeholder="ABC123"
|
||||
maxlength="20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={joinAlliance}
|
||||
disabled={joiningAlliance || !inviteCode.trim()}
|
||||
>
|
||||
{joiningAlliance ? 'Joining...' : 'Join Alliance'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if allianceDetails}
|
||||
<!-- Alliance Management -->
|
||||
<div class="space-y-6">
|
||||
<!-- Alliance Info -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="card-title text-2xl">{allianceDetails.name}</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
{members.length} member{members.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-xs text-base-content/50">Total Pixels</p>
|
||||
<p class="text-2xl font-bold text-primary">{allianceDetails.pixelsPainted.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{#if !editingDescription}
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<p class="text-xs uppercase text-base-content/50">Description</p>
|
||||
{#if isOwnerOrAdmin}
|
||||
<button class="btn btn-ghost btn-xs" on:click={startEditDescription}>Edit</button>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-sm">{allianceDetails.description || 'No description yet.'}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
bind:value={newDescription}
|
||||
placeholder="Alliance description..."
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
<div class="flex gap-2 justify-end mt-2">
|
||||
<button class="btn btn-ghost btn-sm" on:click={() => (editingDescription = false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" on:click={updateDescription}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Members</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Pixels</th>
|
||||
{#if isOwnerOrAdmin}
|
||||
<th>Actions</th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each members as member}
|
||||
<tr>
|
||||
<td>{member.name}</td>
|
||||
<td>
|
||||
<span class="badge" class:badge-primary={member.role === 'owner'}>
|
||||
{member.role}
|
||||
</span>
|
||||
</td>
|
||||
<td>{member.pixelsPainted?.toLocaleString() || '0'}</td>
|
||||
{#if isOwnerOrAdmin && member.id !== $currentUser?.id}
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-xs">Manage</button>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invites (Owner/Admin only) -->
|
||||
{#if isOwnerOrAdmin && invites.length > 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Pending Invites</h3>
|
||||
<div class="space-y-2">
|
||||
{#each invites as invite}
|
||||
<div class="flex justify-between items-center p-3 rounded-box bg-base-200">
|
||||
<div>
|
||||
<p class="font-medium">Code: {invite.code}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
Created {new Date(invite.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,216 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api/client';
|
||||
import { t } from '$lib/i18n';
|
||||
import { calculateLevel } from '$lib/utils/level';
|
||||
import type { LeaderboardMode } from '$lib/constants/config';
|
||||
|
||||
type TabType = 'player' | 'alliance' | 'country';
|
||||
|
||||
let activeTab: TabType = 'player';
|
||||
let activeMode: LeaderboardMode = 'all-time';
|
||||
let loading = true;
|
||||
let error = '';
|
||||
let playerData: any[] = [];
|
||||
let allianceData: any[] = [];
|
||||
let countryData: any[] = [];
|
||||
|
||||
const modes: LeaderboardMode[] = ['today', 'week', 'month', 'all-time'];
|
||||
|
||||
onMount(() => {
|
||||
loadLeaderboard();
|
||||
});
|
||||
|
||||
$: {
|
||||
activeTab;
|
||||
activeMode;
|
||||
loadLeaderboard();
|
||||
}
|
||||
|
||||
async function loadLeaderboard() {
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
if (activeTab === 'player') {
|
||||
const result = await api.getPlayerLeaderboard(activeMode);
|
||||
playerData = result?.players || [];
|
||||
} else if (activeTab === 'alliance') {
|
||||
const result = await api.getAllianceLeaderboardGlobal(activeMode);
|
||||
allianceData = result?.alliances || [];
|
||||
} else if (activeTab === 'country') {
|
||||
const result = await api.getCountryLeaderboard(activeMode);
|
||||
countryData = result?.countries || [];
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load leaderboard';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getModeLabel(mode: LeaderboardMode): string {
|
||||
const labels: Record<LeaderboardMode, string> = {
|
||||
'today': 'Today',
|
||||
'week': 'This Week',
|
||||
'month': 'This Month',
|
||||
'all-time': 'All Time'
|
||||
};
|
||||
return labels[mode];
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Leaderboard - FurryPlace</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-6">
|
||||
<h1 class="text-3xl font-bold mb-6">Leaderboard</h1>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-boxed mb-6">
|
||||
<button class="tab" class:tab-active={activeTab === 'player'} on:click={() => (activeTab = 'player')}>
|
||||
Players
|
||||
</button>
|
||||
<button class="tab" class:tab-active={activeTab === 'alliance'} on:click={() => (activeTab = 'alliance')}>
|
||||
Alliances
|
||||
</button>
|
||||
<button class="tab" class:tab-active={activeTab === 'country'} on:click={() => (activeTab = 'country')}>
|
||||
Countries
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div class="flex gap-2 mb-6 flex-wrap">
|
||||
{#each modes as mode}
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-primary={activeMode === mode}
|
||||
class:btn-ghost={activeMode !== mode}
|
||||
on:click={() => (activeMode = mode)}
|
||||
>
|
||||
{getModeLabel(mode)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{:else if loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body p-0">
|
||||
{#if activeTab === 'player'}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Player</th>
|
||||
<th>Level</th>
|
||||
<th>Pixels</th>
|
||||
{#if playerData[0]?.alliance}
|
||||
<th>Alliance</th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each playerData as player, i}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="font-bold">#{i + 1}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-8">
|
||||
<span class="text-xs">{player.name[0].toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>{player.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{calculateLevel(player.pixelsPainted)}</td>
|
||||
<td>{player.pixelsPainted.toLocaleString()}</td>
|
||||
{#if player.alliance}
|
||||
<td>{player.alliance.name || '-'}</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-base-content/60">No data available</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if activeTab === 'alliance'}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Alliance</th>
|
||||
<th>Members</th>
|
||||
<th>Pixels</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each allianceData as alliance, i}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="font-bold">#{i + 1}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-semibold">{alliance.name}</span>
|
||||
</td>
|
||||
<td>{alliance.memberCount || alliance._count?.members || '-'}</td>
|
||||
<td>{alliance.pixelsPainted.toLocaleString()}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-base-content/60">No data available</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if activeTab === 'country'}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Country</th>
|
||||
<th>Pixels</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each countryData as country, i}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="font-bold">#{i + 1}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-semibold">{country.country}</span>
|
||||
</td>
|
||||
<td>{country.pixelsPainted.toLocaleString()}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-base-content/60">No data available</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$lib/components/layout/Logo.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Offline - FurryPlace</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-base-200 flex items-center justify-center p-4">
|
||||
<div class="text-center max-w-md">
|
||||
<div class="flex justify-center mb-6">
|
||||
<Logo size="lg" hasText={true} />
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-box shadow-xl p-8">
|
||||
<svg
|
||||
class="mx-auto mb-4 h-24 w-24 text-base-content/30"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h1 class="text-2xl font-bold mb-2">You're Offline</h1>
|
||||
<p class="text-base-content/70 mb-6">
|
||||
It looks like you've lost your internet connection. Please check your network and try again.
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-block"
|
||||
on:click={() => window.location.reload()}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
|
||||
<p class="text-sm text-base-content/50 mt-4">
|
||||
Once you're back online, you'll be able to paint pixels again!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { currentUser } from '$lib/stores/user';
|
||||
import { goto } from '$app/navigation';
|
||||
import UserProfile from '$lib/components/user/UserProfile.svelte';
|
||||
import FavoriteLocations from '$lib/components/user/FavoriteLocations.svelte';
|
||||
import AllianceCard from '$lib/components/user/AllianceCard.svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (!$currentUser) {
|
||||
goto('/join?r=/profile');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Profile - FurryPlace</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $currentUser}
|
||||
<div class="mx-auto max-w-6xl px-4 py-6">
|
||||
<h1 class="text-3xl font-bold mb-6">Profile</h1>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<div class="space-y-6">
|
||||
<UserProfile />
|
||||
<AllianceCard />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FavoriteLocations />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<p class="text-lg">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,179 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { currentUser } from '$lib/stores/user';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api/client';
|
||||
import { t } from '$lib/i18n';
|
||||
import { STORE_ITEMS } from '$lib/constants/config';
|
||||
|
||||
let purchasing = false;
|
||||
let error = '';
|
||||
let success = '';
|
||||
|
||||
onMount(() => {
|
||||
if (!$currentUser) {
|
||||
goto('/join?r=/store');
|
||||
}
|
||||
});
|
||||
|
||||
async function purchase(itemId: number, amount?: number, variant?: number) {
|
||||
if (!$currentUser) return;
|
||||
|
||||
purchasing = true;
|
||||
error = '';
|
||||
success = '';
|
||||
|
||||
try {
|
||||
await api.purchase({ id: itemId, amount, variant });
|
||||
await currentUser.fetch();
|
||||
success = 'Purchase successful!';
|
||||
setTimeout(() => (success = ''), 3000);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Purchase failed';
|
||||
} finally {
|
||||
purchasing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function canAfford(price: number): boolean {
|
||||
return $currentUser ? $currentUser.droplets >= price : false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Store - FurryPlace</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $currentUser}
|
||||
<div class="mx-auto max-w-5xl px-4 py-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold">Store</h1>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
You have <span class="font-semibold text-primary">{$currentUser.droplets.toLocaleString()}</span> droplets
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-6">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="alert alert-success mb-6">
|
||||
<span>{success}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Max Charges -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">+5 Max Charges</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Increase your maximum charge capacity by 5. Stack multiple purchases.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<p class="text-xs text-base-content/50">Current: {$currentUser.maxCharges}</p>
|
||||
</div>
|
||||
<div class="card-actions justify-between items-center mt-4">
|
||||
<span class="text-lg font-bold text-primary">500 droplets</span>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
on:click={() => purchase(70)}
|
||||
disabled={purchasing || !canAfford(500)}
|
||||
>
|
||||
{purchasing ? 'Processing...' : 'Purchase'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paint Charges -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">+30 Charges</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Instantly refill 30 paint charges. Great for quick painting sessions!
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<p class="text-xs text-base-content/50">
|
||||
Current: {$currentUser.currentCharges}/{$currentUser.maxCharges}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions justify-between items-center mt-4">
|
||||
<span class="text-lg font-bold text-primary">500 droplets</span>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
on:click={() => purchase(80)}
|
||||
disabled={purchasing || !canAfford(500)}
|
||||
>
|
||||
{purchasing ? 'Processing...' : 'Purchase'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paid Colors -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Unlock Paid Colors</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Unlock all premium colors permanently. Expand your artistic palette!
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<p class="text-xs text-base-content/50">32 additional colors</p>
|
||||
</div>
|
||||
<div class="card-actions justify-between items-center mt-4">
|
||||
<span class="text-lg font-bold text-primary">2,000 droplets</span>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
on:click={() => purchase(100)}
|
||||
disabled={purchasing || !canAfford(2000)}
|
||||
>
|
||||
{purchasing ? 'Processing...' : 'Purchase'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flags -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Unlock Country Flag</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Unlock a country flag to represent your nation. Get 10% charge discount in your region!
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<p class="text-xs text-base-content/50">Choose any country flag</p>
|
||||
</div>
|
||||
<div class="card-actions justify-between items-center mt-4">
|
||||
<span class="text-lg font-bold text-primary">20,000 droplets</span>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
on:click={() => purchase(110)}
|
||||
disabled={purchasing || !canAfford(20000)}
|
||||
>
|
||||
{purchasing ? 'Processing...' : 'Purchase'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 rounded-box bg-base-200">
|
||||
<h3 class="font-semibold mb-2">How to earn droplets?</h3>
|
||||
<ul class="text-sm text-base-content/70 space-y-1 list-disc list-inside">
|
||||
<li>Paint pixels to earn droplets</li>
|
||||
<li>Participate in community events</li>
|
||||
<li>Complete daily challenges</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<p class="text-lg">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user