b
@@ -33,3 +33,5 @@ You will be required to configure an SSL certificate if you plan to use this in
|
||||
|
||||
#### Updating your database
|
||||
In the event that the database schematic changes, you simply need to run `npm run db:push` to update your database schema.
|
||||
## Frontend backup monkey patches
|
||||
If you need to tweak the rules that ship with the compiled backup frontend, edit `scripts/monkey-patch/rules.json` and run `npm run patch:frontend-rules`. You can reorder, add, or remove entries from the per-locale `rules` arrays, adjust the headings, and translate the footer text. The script will regenerate the monkey-patch block inside `frontend-backup/_app/immutable/nodes/4.CrDfIbdR.js`, so you can re-run it safely after pulling new builds.
|
||||
|
||||
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 984 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 318 KiB |
@@ -26,5 +26,5 @@
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759353996237</span> <!--[!--><!----><div class="flex h-full flex-col items-center justify-center gap-6"><a href="/"><div class="flex items-center gap-1.5 "><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">FurryPlace</span><!--]--></div><!----></a> <p class="max-w-3xl text-center font-medium sm:text-xl">Not found</p> <a class="btn btn-primary btn-lg" href="/">Go to map</a></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759353996237</span> <!--[!--><!----><div class="flex h-full flex-col items-center justify-center gap-6"><a href="/"><div class="flex items-center gap-1.5 "><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">openplace</span><!--]--></div><!----></a> <p class="max-w-3xl text-center font-medium sm:text-xl">Not found</p> <a class="btn btn-primary btn-lg" href="/">Go to map</a></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
</html>
|
||||
|
||||
@@ -48,8 +48,8 @@ try {
|
||||
} catch {}
|
||||
const B =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC";
|
||||
var L = c("<span>FurryPlace</span>"),
|
||||
R = c('<div><img alt="FurryPlace logo"/> <!></div>');
|
||||
var L = c("<span>openplace</span>"),
|
||||
R = c('<div><img alt="Wplace logo"/> <!></div>');
|
||||
function z(e, a) {
|
||||
m(a, !0);
|
||||
let t = w(a, "size", 3, "default"),
|
||||
|
||||
@@ -29,7 +29,7 @@ const __vite__mapDeps = (
|
||||
"../chunks/D3yDgRbd.js",
|
||||
"../chunks/wZ7b5CwQ.js",
|
||||
"../nodes/3.DOMAwJeg.js",
|
||||
"../nodes/4.CrDfIbdR.js",
|
||||
"../nodes/4.DB4WphWP.js",
|
||||
"../chunks/DueIxFLX.js",
|
||||
"../chunks/CgCA7Awo.js",
|
||||
"../chunks/Dpga8uG-.js",
|
||||
@@ -6133,7 +6133,7 @@ const Ll = ai(gl),
|
||||
),
|
||||
() =>
|
||||
L(
|
||||
() => import("../nodes/4.CrDfIbdR.js"),
|
||||
() => import("../nodes/4.DB4WphWP.js"),
|
||||
__vite__mapDeps([
|
||||
26, 1, 2, 3, 4, 5, 10, 12, 22, 11, 20, 19, 6, 7, 8, 9, 27, 13, 28, 29,
|
||||
30, 31, 32, 33, 34, 24, 35, 36, 37, 38, 39, 40, 15, 18, 23, 14, 41,
|
||||
|
||||
@@ -262,7 +262,7 @@ function Oa(u, v) {
|
||||
}
|
||||
var P = fa();
|
||||
Ce((r) => {
|
||||
Oe.title = "FurryPlace - Admin - Mods - Leaderboard";
|
||||
Oe.title = "openplace - Admin - Mods - Leaderboard";
|
||||
});
|
||||
var F = t(P),
|
||||
V = t(F),
|
||||
|
||||
@@ -266,7 +266,7 @@ function Or(v, u) {
|
||||
}
|
||||
var F = xr();
|
||||
Ce((a) => {
|
||||
Oe.title = "FurryPlace - Admin - Reports - Leaderboard";
|
||||
Oe.title = "openplace - Admin - Reports - Leaderboard";
|
||||
});
|
||||
var P = t(F),
|
||||
V = t(P),
|
||||
|
||||
@@ -565,7 +565,7 @@ function Oi(u, o) {
|
||||
}
|
||||
var Kt = bi();
|
||||
js((n) => {
|
||||
Ss.title = "FurryPlace - Admin - Users";
|
||||
Ss.title = "openplace - Admin - Users";
|
||||
});
|
||||
var Qt = a(Kt),
|
||||
Wt = a(Qt),
|
||||
|
||||
@@ -146,7 +146,7 @@ function aa(a, e) {
|
||||
var I = Me();
|
||||
fe((r) => {
|
||||
var d = Ve();
|
||||
ue(4), w((L) => (ce.title = `FurryPlace - ${L ?? ""}`), [() => ke()]), l(r, d);
|
||||
ue(4), w((L) => (ce.title = `openplace - ${L ?? ""}`), [() => ke()]), l(r, d);
|
||||
});
|
||||
var W = s(I);
|
||||
{
|
||||
|
||||
@@ -400,7 +400,7 @@ function Vr(p, c) {
|
||||
Z = B(void 0);
|
||||
var va = Ur();
|
||||
vs((n) => {
|
||||
ns.title = "FurryPlace - Moderation";
|
||||
ns.title = "openplace - Moderation";
|
||||
});
|
||||
var Ie = K(va),
|
||||
ze = t(Ie),
|
||||
|
||||
@@ -68,7 +68,7 @@ function M(e, t) {
|
||||
y(t, !1), E();
|
||||
var n = j();
|
||||
$((p) => {
|
||||
u((f) => (T.title = `FurryPlace - ${f ?? ""}`), [() => b()]);
|
||||
u((f) => (T.title = `openplace - ${f ?? ""}`), [() => b()]);
|
||||
});
|
||||
var s = a(n),
|
||||
_ = a(s);
|
||||
|
||||
@@ -98,7 +98,7 @@ function ge(e, t) {
|
||||
});
|
||||
var l = ae();
|
||||
O((r) => {
|
||||
w((s) => (N.title = `FurryPlace - ${s ?? ""}`), [() => T()]);
|
||||
w((s) => (N.title = `openplace - ${s ?? ""}`), [() => T()]);
|
||||
});
|
||||
var c = a(l),
|
||||
q = a(c);
|
||||
|
||||
@@ -1086,7 +1086,7 @@ var r =
|
||||
function g(s) {
|
||||
var e = r();
|
||||
d((b) => {
|
||||
c.title = "FurryPlace - Privacy Policy";
|
||||
c.title = "openplace - Privacy Policy";
|
||||
});
|
||||
var l = t(e),
|
||||
n = t(l);
|
||||
|
||||
@@ -55,7 +55,7 @@ var h =
|
||||
function w(e) {
|
||||
var a = h();
|
||||
u((f) => {
|
||||
n.title = "FurryPlace - Refund Policy";
|
||||
n.title = "openplace - Refund Policy";
|
||||
});
|
||||
var i = s(a),
|
||||
r = s(i);
|
||||
|
||||
@@ -76,7 +76,7 @@ function re(e, d) {
|
||||
}
|
||||
var n = H();
|
||||
U((r) => {
|
||||
S.title = "FurryPlace - Admin";
|
||||
S.title = "openplace - Admin";
|
||||
});
|
||||
var l = t(n),
|
||||
c = t(l),
|
||||
|
||||
@@ -50,7 +50,7 @@ var u =
|
||||
function y(o) {
|
||||
var e = u();
|
||||
c((p) => {
|
||||
t.title = "FurryPlace - Política de Reembolso";
|
||||
t.title = "openplace - Política de Reembolso";
|
||||
});
|
||||
var a = s(e),
|
||||
i = s(a);
|
||||
|
||||
@@ -472,7 +472,7 @@ Calibri;color:#595959;mso-themecolor:text1;mso-themetint:166;" class="svelte-11v
|
||||
function u(t) {
|
||||
var e = p();
|
||||
v((b) => {
|
||||
r.title = "FurryPlace - Terms of Service";
|
||||
r.title = "openplace - Terms of Service";
|
||||
});
|
||||
var s = l(e),
|
||||
o = l(s);
|
||||
|
||||
@@ -80,7 +80,7 @@ function X(e, d) {
|
||||
}
|
||||
var n = B();
|
||||
j((t) => {
|
||||
D.title = "FurryPlace - Admin - Mods";
|
||||
D.title = "openplace - Admin - Mods";
|
||||
});
|
||||
var l = o(n),
|
||||
m = v(o(l), 2),
|
||||
|
||||
@@ -70094,7 +70094,7 @@ function R9(m, a) {
|
||||
return Math.round(m * p) / p;
|
||||
}
|
||||
var B9 = Te(
|
||||
'<meta property="og:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!"/> <meta name="twitter:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!"/> <meta name="robots" content="index, follow, max-image-preview:large"/> <meta name="color-scheme" content="light only"/>',
|
||||
'<meta property="og:title" content="openplace - A massive real-time pixel art canvas on the world map!"/> <meta name="twitter:title" content="openplace - A massive real-time pixel art canvas on the world map!"/> <meta name="robots" content="index, follow, max-image-preview:large"/> <meta name="color-scheme" content="light only"/>',
|
||||
1
|
||||
),
|
||||
F9 = (m, a) => {
|
||||
@@ -70700,7 +70700,7 @@ function uF(m, a) {
|
||||
var qr = wB();
|
||||
nx((bt) => {
|
||||
var Xt = B9();
|
||||
(rx.title = "FurryPlace - Paint the world"), yn(6), $(bt, Xt);
|
||||
(rx.title = "openplace - Paint the world"), yn(6), $(bt, Xt);
|
||||
});
|
||||
var ue = Ct(qr);
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ function M(e, o) {
|
||||
y();
|
||||
var t = E();
|
||||
g((n) => {
|
||||
m.title = "FurryPlace - Admin Dashboard";
|
||||
m.title = "openplace - Admin Dashboard";
|
||||
});
|
||||
var a = u(s(t), 2),
|
||||
r = s(a, !0);
|
||||
|
||||
@@ -484,7 +484,7 @@ function Ro(d, r) {
|
||||
}
|
||||
var Se = no();
|
||||
tr((n) => {
|
||||
Za.title = "FurryPlace - Admin - Alliances";
|
||||
Za.title = "openplace - Admin - Alliances";
|
||||
});
|
||||
var De = a(Se),
|
||||
Re = a(De),
|
||||
|
||||
@@ -170,7 +170,7 @@ function Et(f, p) {
|
||||
}
|
||||
var P = vt();
|
||||
Ge((a) => {
|
||||
Me.title = "FurryPlace - Admin Dashboard";
|
||||
Me.title = "openplace - Admin Dashboard";
|
||||
});
|
||||
var Q = t(P),
|
||||
U = t(Q),
|
||||
|
||||
@@ -41,7 +41,7 @@ try {
|
||||
} catch {}
|
||||
function s(e) {
|
||||
o((d) => {
|
||||
n.title = "FurryPlace - Admin - Mods Dashboard";
|
||||
n.title = "openplace - Admin - Mods Dashboard";
|
||||
});
|
||||
}
|
||||
export { s as component };
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/D3yDgRbd.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/wZ7b5CwQ.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/nodes/6.WPRvZASS.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/Z_72d8Vp.js"><!--[--><!--]--><!--[--><!--]--><title>FurryPlace - Admin Dashboard</title>
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/Z_72d8Vp.js"><!--[--><!--]--><!--[--><!--]--><title>openplace - Admin Dashboard</title>
|
||||
|
||||
<meta property="og:image" content="/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -41,24 +41,24 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta name="twitter:image" content="/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="FurryPlace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="FurryPlace" />
|
||||
<meta name="keywords" content="openplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="openplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
@@ -70,8 +70,8 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "FurryPlace",
|
||||
"url": "https://github.com/FurryPlaceteam/FurryPlace"
|
||||
"name": "openplace",
|
||||
"url": "https://github.com/openplaceteam/openplace"
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/B6ZK_HZO.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/Bn0Xcwmn.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/D3yDgRbd.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BSXXHLQ0.js"><!--[--><meta property="og:title" content="Join the alliance"/> <meta name="twitter:title" content="Join the allince"/> <meta name="robots" content="noindex"/><!--]--><title>FurryPlace - Alliance invite</title>
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BSXXHLQ0.js"><!--[--><meta property="og:title" content="Join the alliance"/> <meta name="twitter:title" content="Join the allince"/> <meta name="robots" content="noindex"/><!--]--><title>openplace - Alliance invite</title>
|
||||
|
||||
<meta property="og:image" content="/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -40,24 +40,24 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta name="twitter:image" content="/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="FurryPlace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="FurryPlace" />
|
||||
<meta name="keywords" content="openplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="openplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
@@ -69,8 +69,8 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "FurryPlace",
|
||||
"url": "https://github.com/FurryPlaceteam/FurryPlace"
|
||||
"name": "openplace",
|
||||
"url": "https://github.com/openplaceteam/openplace"
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/C3E1P42D.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DBSOMMI_.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BpoSU4rb.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/lE0oaQc5.js"><!--[--><!--]--><title>FurryPlace - Moderation</title>
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/lE0oaQc5.js"><!--[--><!--]--><title>openplace - Moderation</title>
|
||||
|
||||
<meta property="og:image" content="/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -48,24 +48,24 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta name="twitter:image" content="/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="FurryPlace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="FurryPlace" />
|
||||
<meta name="keywords" content="openplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="openplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
@@ -77,8 +77,8 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "FurryPlace",
|
||||
"url": "https://github.com/FurryPlaceteam/FurryPlace"
|
||||
"name": "openplace",
|
||||
"url": "https://github.com/openplaceteam/openplace"
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/D3yDgRbd.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/m3o6lEf1.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DCynssDD.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/C3E1P42D.js"><!--[--><!--]--><title>FurryPlace - No internet connection</title>
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/C3E1P42D.js"><!--[--><!--]--><title>openplace - No internet connection</title>
|
||||
|
||||
<meta property="og:image" content="/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -40,24 +40,24 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta name="twitter:image" content="/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="FurryPlace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="FurryPlace" />
|
||||
<meta name="keywords" content="openplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="openplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
@@ -69,8 +69,8 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "FurryPlace",
|
||||
"url": "https://github.com/FurryPlaceteam/FurryPlace"
|
||||
"name": "openplace",
|
||||
"url": "https://github.com/openplaceteam/openplace"
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759353996237</span> <!--[!--><!----><div class="relative flex h-full flex-col items-center justify-center gap-2"><a href="/"><div class="flex items-center gap-1.5 absolute left-1/2 top-10 -translate-x-1/2"><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">FurryPlace</span><!--]--></div><!----></a> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="text-base-content/80 w-40"><path d="M790-56 414-434q-47 11-87.5 33T254-346l-84-86q32-32 69-56t79-42l-90-90q-41 21-76.5 46.5T84-516L0-602q32-32 66.5-57.5T140-708l-84-84 56-56 736 736-58 56Zm-310-64q-42 0-71-29.5T380-220q0-42 29-71t71-29q42 0 71 29t29 71q0 41-29 70.5T480-120Zm236-238-29-29-29-29-144-144q81 8 151.5 41T790-432l-74 74Zm160-158q-77-77-178.5-120.5T480-680q-21 0-40.5 1.5T400-674L298-776q44-12 89.5-18t92.5-6q142 0 265 53t215 145l-84 86Z"></path></svg><!----> <p class="text-lg">No internet connection</p> <button class="btn btn-lg mt-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"></path></svg><!----> Refresh</button></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759353996237</span> <!--[!--><!----><div class="relative flex h-full flex-col items-center justify-center gap-2"><a href="/"><div class="flex items-center gap-1.5 absolute left-1/2 top-10 -translate-x-1/2"><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">openplace</span><!--]--></div><!----></a> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="text-base-content/80 w-40"><path d="M790-56 414-434q-47 11-87.5 33T254-346l-84-86q32-32 69-56t79-42l-90-90q-41 21-76.5 46.5T84-516L0-602q32-32 66.5-57.5T140-708l-84-84 56-56 736 736-58 56Zm-310-64q-42 0-71-29.5T380-220q0-42 29-71t71-29q42 0 71 29t29 71q0 41-29 70.5T480-120Zm236-238-29-29-29-29-144-144q81 8 151.5 41T790-432l-74 74Zm160-158q-77-77-178.5-120.5T480-680q-21 0-40.5 1.5T400-674L298-776q44-12 89.5-18t92.5-6q142 0 265 53t215 145l-84 86Z"></path></svg><!----> <p class="text-lg">No internet connection</p> <button class="btn btn-lg mt-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"></path></svg><!----> Refresh</button></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
|
||||
<script>
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DueIxFLX.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/B6ZK_HZO.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/D3yDgRbd.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BSXXHLQ0.js"><!--[--><!--]--><title>FurryPlace - Payment succeeded</title>
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BSXXHLQ0.js"><!--[--><!--]--><title>openplace - Payment succeeded</title>
|
||||
|
||||
<meta property="og:image" content="/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -39,24 +39,24 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta name="twitter:image" content="/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="FurryPlace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="FurryPlace" />
|
||||
<meta name="keywords" content="openplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="openplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
@@ -68,8 +68,8 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "FurryPlace",
|
||||
"url": "https://github.com/FurryPlaceteam/FurryPlace"
|
||||
"name": "openplace",
|
||||
"url": "https://github.com/openplaceteam/openplace"
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759353996237</span> <!--[!--><!----><div class="relative flex h-full flex-col items-center justify-center px-4"><div class="absolute top-8"><div class="flex items-center gap-1.5 "><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">FurryPlace</span><!--]--></div><!----></div> <div class="card border-base-content/10 w-full max-w-xl border shadow-sm"><div class="card-body gap-3"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-16 text-emerald-500"><path d="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-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-80Z"></path></svg><!----> <h2 class="text-4xl font-medium sm:text-5xl">Payment succeeded!</h2> <p class="text-lg"><!--[!--><!--]--> Thank you for your support!</p> <a class="btn btn-primary btn-lg mt-2 w-max" href="/">Go to map</a></div></div></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759353996237</span> <!--[!--><!----><div class="relative flex h-full flex-col items-center justify-center px-4"><div class="absolute top-8"><div class="flex items-center gap-1.5 "><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">openplace</span><!--]--></div><!----></div> <div class="card border-base-content/10 w-full max-w-xl border shadow-sm"><div class="card-body gap-3"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-16 text-emerald-500"><path d="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-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-80Z"></path></svg><!----> <h2 class="text-4xl font-medium sm:text-5xl">Payment succeeded!</h2> <p class="text-lg"><!--[!--><!--]--> Thank you for your support!</p> <a class="btn btn-primary btn-lg mt-2 w-max" href="/">Go to map</a></div></div></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
|
||||
<script>
|
||||
{
|
||||
|
||||
@@ -46,24 +46,24 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="FurryPlace is a free unofficial open source backend for wplace."
|
||||
content="openplace is a free unofficial open source backend for wplace."
|
||||
/>
|
||||
<meta name="twitter:image" content="/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="FurryPlace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="FurryPlace" />
|
||||
<meta name="keywords" content="openplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="openplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
@@ -75,8 +75,8 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "FurryPlace",
|
||||
"url": "https://github.com/FurryPlaceteam/FurryPlace"
|
||||
"name": "openplace",
|
||||
"url": "https://github.com/openplaceteam/openplace"
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const ae = "/files",
|
||||
e + "/_app/immutable/nodes/20.LCTNv26D.js",
|
||||
e + "/_app/immutable/nodes/21.zScYLJw9.js",
|
||||
e + "/_app/immutable/nodes/3.DOMAwJeg.js",
|
||||
e + "/_app/immutable/nodes/4.CrDfIbdR.js",
|
||||
e + "/_app/immutable/nodes/4.DB4WphWP.js",
|
||||
e + "/_app/immutable/assets/4.BtKF873c.css",
|
||||
e + "/_app/immutable/nodes/5.cZCL4YVE.js",
|
||||
e + "/_app/immutable/nodes/6.WPRvZASS.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "FurryPlace",
|
||||
"short_name": "FurryPlace",
|
||||
"description": "FurryPlace is a free unofficial open source backend for wplace.",
|
||||
"name": "openplace",
|
||||
"short_name": "openplace",
|
||||
"description": "openplace is a free unofficial open source backend for wplace.",
|
||||
"start_url": "/",
|
||||
"theme_color": "#f8f4f0",
|
||||
"background_color": "#ffffff",
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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,14 +1,17 @@
|
||||
<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 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';
|
||||
import { calculateLevel } from '../lib/utils/level';
|
||||
|
||||
interface SelectedPixel {
|
||||
tileX: number;
|
||||
@@ -18,26 +21,16 @@ interface SelectedPixel {
|
||||
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';
|
||||
|
||||
let mapRef: CanvasMap | null = null;
|
||||
let selected: SelectedPixel | null = null;
|
||||
let pixelInfo: PixelInfo | null = null;
|
||||
let pixelInfo: any = null;
|
||||
let loadingPixel = false;
|
||||
let painting = false;
|
||||
let errorMessage = '';
|
||||
let successMessage = '';
|
||||
let showColorPalette = false;
|
||||
|
||||
onMount(() => {
|
||||
currentUser.fetch();
|
||||
@@ -54,13 +47,15 @@ async function handlePixel(event: CustomEvent<SelectedPixel>) {
|
||||
async function loadPixelInfo(selection: SelectedPixel) {
|
||||
loadingPixel = true;
|
||||
try {
|
||||
pixelInfo = await api.getPixelInfo(SEASON, selection.tileX, selection.tileY, selection.x, selection.y);
|
||||
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.';
|
||||
}
|
||||
console.error('Failed to load pixel info:', error);
|
||||
} finally {
|
||||
loadingPixel = false;
|
||||
}
|
||||
@@ -72,7 +67,7 @@ function canPaint(): boolean {
|
||||
const color = get(selectedColor);
|
||||
if (color === 0) return false;
|
||||
const charges = get(currentCharges);
|
||||
return charges > 0;
|
||||
return charges > 0 && get(online);
|
||||
}
|
||||
|
||||
async function paint() {
|
||||
@@ -106,6 +101,7 @@ async function paint() {
|
||||
await loadPixelInfo(selected);
|
||||
mapRef?.refreshTiles();
|
||||
successMessage = 'Pixel painted!';
|
||||
setTimeout(() => (successMessage = ''), 2000);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
errorMessage = error.message;
|
||||
@@ -118,93 +114,150 @@ async function paint() {
|
||||
}
|
||||
</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)]">
|
||||
<!-- 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} />
|
||||
</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>
|
||||
|
||||
<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}
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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"
|
||||
>
|
||||
{#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>
|
||||
<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}
|
||||
{#if successMessage}
|
||||
<p class="mt-3 rounded-box bg-success/10 px-3 py-2 text-sm text-success">{successMessage}</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<PaintButton
|
||||
disabled={!canPaint() || painting}
|
||||
{painting}
|
||||
on:click={paint}
|
||||
on:openPalette={() => (showColorPalette = true)}
|
||||
/>
|
||||
</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}
|
||||
|
||||
<!-- 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>
|
||||
{#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>
|
||||
|
||||
<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}
|
||||
@@ -1,92 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>FurryPlace - Paint the world</title>
|
||||
|
||||
<meta property="og:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta name="twitter:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content="https://wplace.live/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta itemprop="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta property="og:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="robots" content="index, follow, max-image-preview:large" />
|
||||
<meta name="color-scheme" content="light only" />
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="Wplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Wplace",
|
||||
"url": "https://wplace.live"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add favicon files to static/img/ -->
|
||||
<!-- <link rel="icon" type="image/png" href="/img/favicon-96x96.png" sizes="96x96" /> -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" /> -->
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
|
||||
<link rel="modulepreload" href="/_app/immutable/entry/start.CGEerVGH.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BPIWuEio.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DfpL3vsM.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DgYqO0BT.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/entry/app.D8VNzzNO.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/C1FmrZbK.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/IHki7fMi.js">
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_dgvam6 = {
|
||||
base: ""
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("/_app/immutable/entry/start.CGEerVGH.js"),
|
||||
import("/_app/immutable/entry/app.D8VNzzNO.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
window.pwaInstallPrompt = event;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add service worker if needed -->
|
||||
<!-- <script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
}
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,532 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Site Content Editor - Admin Panel</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.locale-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.locale-selector label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.content-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.content-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content-key {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #1976D2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-item input, .content-item textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.content-item textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.new-item-form {
|
||||
background: #e3f2fd;
|
||||
border: 2px dashed #2196F3;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-item-form.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-message.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎨 Site Content Editor</h1>
|
||||
<p class="subtitle">Manage modal content, rules, and site text</p>
|
||||
|
||||
<div class="success-message" id="successMessage"></div>
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Items</span>
|
||||
<span class="stat-value" id="totalItems">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Current Locale</span>
|
||||
<span class="stat-value" id="currentLocale">en</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="locale-selector">
|
||||
<label for="localeSelect">Locale:</label>
|
||||
<select id="localeSelect">
|
||||
<option value="en">English (en)</option>
|
||||
<option value="zh">Chinese (zh)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="loadContent()">🔄 Reload</button>
|
||||
<button class="btn-secondary" onclick="toggleNewItemForm()">➕ Add New Item</button>
|
||||
<button class="btn-warning" onclick="initializeDefaults()">🔧 Initialize Defaults</button>
|
||||
<button class="btn-primary" onclick="saveAllChanges()">💾 Save All Changes</button>
|
||||
</div>
|
||||
|
||||
<div class="new-item-form" id="newItemForm">
|
||||
<h3>Add New Content Item</h3>
|
||||
<div class="form-row">
|
||||
<input type="text" id="newKey" placeholder="Key (e.g., modal.rules.item.5)">
|
||||
<textarea id="newValue" placeholder="Value"></textarea>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="addNewItem()">Add Item</button>
|
||||
<button class="btn-secondary" onclick="toggleNewItemForm()">Cancel</button>
|
||||
</div>
|
||||
|
||||
<div id="contentContainer" class="loading">
|
||||
Loading content...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentContent = [];
|
||||
let currentLocale = 'en';
|
||||
let changedItems = new Set();
|
||||
|
||||
// Get JWT token from cookie
|
||||
function getAuthToken() {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'j') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
function showSuccess(message) {
|
||||
const el = document.getElementById('successMessage');
|
||||
el.textContent = message;
|
||||
el.classList.add('active');
|
||||
setTimeout(() => el.classList.remove('active'), 3000);
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showError(message) {
|
||||
const el = document.getElementById('errorMessage');
|
||||
el.textContent = message;
|
||||
el.classList.add('active');
|
||||
setTimeout(() => el.classList.remove('active'), 5000);
|
||||
}
|
||||
|
||||
// Toggle new item form
|
||||
function toggleNewItemForm() {
|
||||
const form = document.getElementById('newItemForm');
|
||||
form.classList.toggle('active');
|
||||
}
|
||||
|
||||
// Load content from API
|
||||
async function loadContent() {
|
||||
const locale = document.getElementById('localeSelect').value;
|
||||
currentLocale = locale;
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content', {
|
||||
headers: {
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load content');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
currentContent = data.content.filter(item => item.locale === locale);
|
||||
|
||||
renderContent();
|
||||
updateStats();
|
||||
showSuccess(`Loaded ${currentContent.length} items for locale: ${locale}`);
|
||||
} catch (error) {
|
||||
showError('Error loading content: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render content items
|
||||
function renderContent() {
|
||||
const container = document.getElementById('contentContainer');
|
||||
|
||||
if (currentContent.length === 0) {
|
||||
container.innerHTML = '<div class="loading">No content found. Click "Initialize Defaults" to create default content.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
container.className = 'content-grid';
|
||||
|
||||
currentContent.forEach((item, index) => {
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = 'content-item';
|
||||
itemEl.innerHTML = `
|
||||
<div class="content-item-header">
|
||||
<span class="content-key">${escapeHtml(item.key)}</span>
|
||||
<button class="btn-delete" onclick="deleteItem('${escapeHtml(item.key)}')">🗑 Delete</button>
|
||||
</div>
|
||||
${item.value.length > 100
|
||||
? `<textarea data-key="${escapeHtml(item.key)}" onchange="markChanged('${escapeHtml(item.key)}')">${escapeHtml(item.value)}</textarea>`
|
||||
: `<input type="text" data-key="${escapeHtml(item.key)}" value="${escapeHtml(item.value)}" onchange="markChanged('${escapeHtml(item.key)}')">`
|
||||
}
|
||||
`;
|
||||
container.appendChild(itemEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Mark item as changed
|
||||
function markChanged(key) {
|
||||
changedItems.add(key);
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
function updateStats() {
|
||||
document.getElementById('totalItems').textContent = currentContent.length;
|
||||
document.getElementById('currentLocale').textContent = currentLocale;
|
||||
}
|
||||
|
||||
// Add new item
|
||||
async function addNewItem() {
|
||||
const key = document.getElementById('newKey').value.trim();
|
||||
const value = document.getElementById('newValue').value.trim();
|
||||
|
||||
if (!key || !value) {
|
||||
showError('Both key and value are required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ key, value, locale: currentLocale })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add item');
|
||||
}
|
||||
|
||||
document.getElementById('newKey').value = '';
|
||||
document.getElementById('newValue').value = '';
|
||||
toggleNewItemForm();
|
||||
loadContent();
|
||||
showSuccess('Item added successfully');
|
||||
} catch (error) {
|
||||
showError('Error adding item: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete item
|
||||
async function deleteItem(key) {
|
||||
if (!confirm(`Delete "${key}"?`)) return;
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch(`/api/admin/site-content/${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete item');
|
||||
}
|
||||
|
||||
loadContent();
|
||||
showSuccess('Item deleted successfully');
|
||||
} catch (error) {
|
||||
showError('Error deleting item: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Save all changes
|
||||
async function saveAllChanges() {
|
||||
const items = [];
|
||||
const inputs = document.querySelectorAll('[data-key]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const key = input.getAttribute('data-key');
|
||||
if (changedItems.has(key)) {
|
||||
items.push({
|
||||
key: key,
|
||||
value: input.value,
|
||||
locale: currentLocale
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
showError('No changes to save');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content/bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ items })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save changes');
|
||||
}
|
||||
|
||||
changedItems.clear();
|
||||
loadContent();
|
||||
showSuccess(`Saved ${items.length} items successfully`);
|
||||
} catch (error) {
|
||||
showError('Error saving changes: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize default content
|
||||
async function initializeDefaults() {
|
||||
if (!confirm('Initialize default content? This will create/update default modal content.')) return;
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content/initialize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to initialize defaults');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
loadContent();
|
||||
showSuccess(`Initialized ${data.initialized} default items`);
|
||||
} catch (error) {
|
||||
showError('Error initializing defaults: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('localeSelect').addEventListener('change', loadContent);
|
||||
|
||||
// Initial load
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
export const env={"PUBLIC_BACKEND_URL":"https://backend.wplace.live","PUBLIC_FILES_URL":"https://backend.wplace.live/files","PUBLIC_MAP_URL":"https://maps.wplace.live/styles/liberty","PUBLIC_ENV":"prod","PUBLIC_MAINTENANCE":"false","PUBLIC_TURNSTILE_ENABLED":"true","PUBLIC_TURNSTILE_SITE_KEY_LOGIN":"0x4AAAAAABpHqZ-6i7uL0nmG","PUBLIC_TURNSTILE_SITE_KEY_PAINT":"0x4AAAAAABpqJe8FO0N84q0F"}
|
||||
@@ -1 +0,0 @@
|
||||
.leaflet-container{width:100%;height:100%}
|
||||
@@ -1 +0,0 @@
|
||||
const E="modulepreload",y=function(a,l){return new URL(a,l).href},m={},g=function(l,c,u){let f=Promise.resolve();if(c&&c.length>0){const r=document.getElementsByTagName("link"),e=document.querySelector("meta[property=csp-nonce]"),h=(e==null?void 0:e.nonce)||(e==null?void 0:e.getAttribute("nonce"));f=Promise.allSettled(c.map(t=>{if(t=y(t,u),t in m)return;m[t]=!0;const o=t.endsWith(".css"),v=o?'[rel="stylesheet"]':"";if(!!u)for(let s=r.length-1;s>=0;s--){const i=r[s];if(i.href===t&&(!o||i.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${t}"]${v}`))return;const n=document.createElement("link");if(n.rel=o?"stylesheet":E,o||(n.as="script"),n.crossOrigin="",n.href=t,h&&n.setAttribute("nonce",h),document.head.appendChild(n),o)return new Promise((s,i)=>{n.addEventListener("load",s),n.addEventListener("error",()=>i(new Error(`Unable to preload CSS for ${t}`)))})}))}function d(r){const e=new Event("vite:preloadError",{cancelable:!0});if(e.payload=r,window.dispatchEvent(e),!e.defaultPrevented)throw r}return f.then(r=>{for(const e of r||[])e.status==="rejected"&&d(e.reason);return l().catch(d)})};export{g as _};
|
||||
@@ -1 +0,0 @@
|
||||
import{w as n}from"./DgYqO0BT.js";const e="http://localhost:3000",l="/maps/styles/liberty",o=!1,u=e,c=o;function s(){if(typeof navigator>"u")return"en";if(navigator.languages&&navigator.languages.length>0){const t=navigator.languages.find(a=>a.length===2);if(t)return t==="pt"?"pt":"en"}return(navigator.language||navigator.userLanguage||navigator.browserLanguage||"en").substring(0,2)==="pt"?"pt":"en"}const p=n(s()),f=n(void 0),g=n(Date.now());typeof window<"u"&&setInterval(()=>{g.set(Date.now())},500);const L=n(!0);export{u as A,c as E,l as M,f as c,p as l,g as n,L as o};
|
||||
@@ -1 +0,0 @@
|
||||
import{S,i as B,s as v,n as h,d as o,z as f,r,b as _,c as x,A as C,e as u,f as k,h as L,j as m,k as Q,B as z,C as d,w as T}from"./DfpL3vsM.js";import"./IHki7fMi.js";function g(l){let e,t="wplace";return{c(){e=m("span"),e.textContent=t,this.h()},l(i){e=u(i,"SPAN",{class:!0,"data-svelte-h":!0}),T(e)!=="svelte-6klpyz"&&(e.textContent=t),this.h()},h(){r(e,"class","text-base-content font-pixel"),f(e,"text-4xl",l[1]["text-4xl"]),f(e,"text-5xl",l[1]["text-5xl"])},m(i,c){_(i,e,c)},p(i,c){c&2&&f(e,"text-4xl",i[1]["text-4xl"]),c&2&&f(e,"text-5xl",i[1]["text-5xl"])},d(i){i&&o(e)}}}function E(l){let e,t,i,c,A,s=l[0]&&g(l);return{c(){e=m("div"),t=m("img"),c=Q(),s&&s.c(),this.h()},l(a){e=u(a,"DIV",{class:!0});var n=k(e);t=u(n,"IMG",{src:!0,alt:!0,class:!0}),c=L(n),s&&s.l(n),n.forEach(o),this.h()},h(){C(t.src,i=R)||r(t,"src",i),r(t,"alt","Wplace logo"),r(t,"class","pixelated"),f(t,"size-10",l[2]["size-10"]),f(t,"size-16",l[2]["size-16"]),f(t,"size-20",l[2]["size-20"]),r(e,"class",A="flex items-center gap-1.5 "+(l[3].class||""))},m(a,n){_(a,e,n),x(e,t),x(e,c),s&&s.m(e,null)},p(a,[n]){n&4&&f(t,"size-10",a[2]["size-10"]),n&4&&f(t,"size-16",a[2]["size-16"]),n&4&&f(t,"size-20",a[2]["size-20"]),a[0]?s?s.p(a,n):(s=g(a),s.c(),s.m(e,null)):s&&(s.d(1),s=null),n&8&&A!==(A="flex items-center gap-1.5 "+(a[3].class||""))&&r(e,"class",A)},i:h,o:h,d(a){a&&o(e),s&&s.d()}}}const R="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC";function b(l,e,t){let i,c,{size:A="default"}=e,{hasText:s=!1}=e;return l.$$set=a=>{t(3,e=z(z({},e),d(a))),"size"in a&&t(4,A=a.size),"hasText"in a&&t(0,s=a.hasText)},l.$$.update=()=>{l.$$.dirty&16&&t(2,i={"size-10":A==="default","size-16":A==="medium","size-20":A==="lg"}),l.$$.dirty&16&&t(1,c={"text-4xl":A==="default","text-5xl":A==="lg"||A==="medium"})},e=d(e),[s,c,i,e,A]}class F extends S{constructor(e){super(),B(this,e,b,E,v,{size:4,hasText:0})}}export{F as L};
|
||||
@@ -1 +0,0 @@
|
||||
import{s as e}from"./BPIWuEio.js";const r=()=>{const s=e;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},b={subscribe(s){return r().page.subscribe(s)}};export{b as p};
|
||||
@@ -1 +0,0 @@
|
||||
import{n as f,s as w,D as m,E as q,F as x}from"./DfpL3vsM.js";const a=[];function z(e,i){return{subscribe:A(e,i).subscribe}}function A(e,i=f){let r;const n=new Set;function u(t){if(w(e,t)&&(e=t,r)){const o=!a.length;for(const s of n)s[1](),a.push(s,e);if(o){for(let s=0;s<a.length;s+=2)a[s][0](a[s+1]);a.length=0}}}function l(t){u(t(e))}function b(t,o=f){const s=[t,o];return n.add(s),n.size===1&&(r=i(u,l)||f),t(e),()=>{n.delete(s),n.size===0&&r&&(r(),r=null)}}return{set:u,update:l,subscribe:b}}function B(e,i,r){const n=!Array.isArray(e),u=n?[e]:e;if(!u.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const l=i.length<2;return z(r,(b,t)=>{let o=!1;const s=[];let d=0,p=f;const h=()=>{if(d)return;p();const c=i(n?s[0]:s,b,t);l?b(c):p=x(c)?c:f},y=u.map((c,g)=>m(c,_=>{s[g]=_,d&=~(1<<g),o&&h()},()=>{d|=1<<g}));return o=!0,h(),function(){q(y),p(),o=!1}})}export{B as d,A as w};
|
||||
@@ -1 +0,0 @@
|
||||
const e="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(e);
|
||||
@@ -1 +0,0 @@
|
||||
import{l as o,a as r}from"../chunks/BPIWuEio.js";export{o as load_css,r as start};
|
||||
@@ -1 +0,0 @@
|
||||
import{S as l,i as r,s as i,Q as u,o as _,p as f,R as c,T as p,U as m}from"../chunks/DfpL3vsM.js";import"../chunks/IHki7fMi.js";const d=!0,$=!1,v=Object.freeze(Object.defineProperty({__proto__:null,prerender:d,ssr:$},Symbol.toStringTag,{value:"Module"}));function g(n){let s;const a=n[1].default,t=u(a,n,n[0],null);return{c(){t&&t.c()},l(e){t&&t.l(e)},m(e,o){t&&t.m(e,o),s=!0},p(e,[o]){t&&t.p&&(!s||o&1)&&c(t,a,e,e[0],s?m(a,e[0],o,null):p(e[0]),null)},i(e){s||(f(t,e),s=!0)},o(e){_(t,e),s=!1},d(e){t&&t.d(e)}}}function b(n,s,a){let{$$slots:t={},$$scope:e}=s;return n.$$set=o=>{"$$scope"in o&&a(0,e=o.$$scope)},[e,t]}class h extends l{constructor(s){super(),r(this,s,b,g,i,{})}}export{h as component,v as universal};
|
||||
@@ -1 +0,0 @@
|
||||
import{S as x,i as S,s as j,n as u,d as c,a as h,b as _,c as d,e as v,f as g,g as b,h as k,j as E,t as $,k as q,l as y}from"../chunks/DfpL3vsM.js";import"../chunks/IHki7fMi.js";import{p as C}from"../chunks/DIeP6ySR.js";function H(p){var f;let a,s=p[0].status+"",r,n,o,i=((f=p[0].error)==null?void 0:f.message)+"",m;return{c(){a=E("h1"),r=$(s),n=q(),o=E("p"),m=$(i)},l(e){a=v(e,"H1",{});var t=g(a);r=b(t,s),t.forEach(c),n=k(e),o=v(e,"P",{});var l=g(o);m=b(l,i),l.forEach(c)},m(e,t){_(e,a,t),d(a,r),_(e,n,t),_(e,o,t),d(o,m)},p(e,[t]){var l;t&1&&s!==(s=e[0].status+"")&&h(r,s),t&1&&i!==(i=((l=e[0].error)==null?void 0:l.message)+"")&&h(m,i)},i:u,o:u,d(e){e&&(c(a),c(n),c(o))}}}function P(p,a,s){let r;return y(p,C,n=>s(0,r=n)),[r]}class B extends x{constructor(a){super(),S(this,a,P,H,j,{})}}export{B as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{S as D,i as H,s as I,n as L,d,m as S,o as T,p as V,b as P,c,q as k,r as m,u as z,h as p,e as u,f as q,v as A,w as C,k as x,j as h,x as F}from"../chunks/DfpL3vsM.js";import"../chunks/IHki7fMi.js";import{L as G}from"../chunks/DBKVvboF.js";function N(w){let i,n,t,o,_,l,$="404",v,r,y="Oops! This page doesn't exist.",b,s,j="Go Home",f;return o=new G({props:{size:"lg",hasText:!0,class:"justify-center mb-8"}}),{c(){i=x(),n=h("div"),t=h("div"),F(o.$$.fragment),_=x(),l=h("h1"),l.textContent=$,v=x(),r=h("p"),r.textContent=y,b=x(),s=h("a"),s.textContent=j,this.h()},l(e){z("svelte-18fu4an",document.head).forEach(d),i=p(e),n=u(e,"DIV",{class:!0});var E=q(n);t=u(E,"DIV",{class:!0});var a=q(t);A(o.$$.fragment,a),_=p(a),l=u(a,"H1",{class:!0,"data-svelte-h":!0}),C(l)!=="svelte-84luby"&&(l.textContent=$),v=p(a),r=u(a,"P",{class:!0,"data-svelte-h":!0}),C(r)!=="svelte-os11l"&&(r.textContent=y),b=p(a),s=u(a,"A",{href:!0,class:!0,"data-svelte-h":!0}),C(s)!=="svelte-1cbbj8a"&&(s.textContent=j),a.forEach(d),E.forEach(d),this.h()},h(){document.title="404 - Page Not Found",m(l,"class","text-6xl font-bold mb-4"),m(r,"class","text-xl text-base-content/70 mb-8"),m(s,"href","/"),m(s,"class","btn btn-primary"),m(t,"class","text-center"),m(n,"class","min-h-screen bg-base-200 flex items-center justify-center p-4")},m(e,g){P(e,i,g),P(e,n,g),c(n,t),k(o,t,null),c(t,_),c(t,l),c(t,v),c(t,r),c(t,b),c(t,s),f=!0},p:L,i(e){f||(V(o.$$.fragment,e),f=!0)},o(e){T(o.$$.fragment,e),f=!1},d(e){e&&(d(i),d(n)),S(o)}}}class K extends D{constructor(i){super(),H(this,i,null,N,I,{})}}export{K as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{S as c,i as d,s as m,n as i,d as r,b as l,r as b,u as p,h,e as f,w as x,k as u,j as v,l as g,y as _}from"../chunks/DfpL3vsM.js";import"../chunks/IHki7fMi.js";import{c as w}from"../chunks/C4PhwnwB.js";import"../chunks/CmyxTY1z.js";import{g as y}from"../chunks/BPIWuEio.js";function k(n){let t,e,s='<header class="bg-base-100 border-base-300 sticky top-0 z-20 border-b"><div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3"><a class="flex items-center gap-2" href="/"><div class="flex items-center gap-1.5 h-7 w-auto"><span class="text-base-content/80 text-lg font-semibold">Admin</span></div></a> <div></div></div> <nav class="mx-auto max-w-7xl px-4"><div class="tabs tabs-border flex items-center gap-2 overflow-auto"><a href="/admin" data-sveltekit-prefetch="" class="tab font-semibold tab-active">Dashboard</a> <a href="/admin/users" data-sveltekit-prefetch="" class="tab font-semibold">Users</a> <a href="/admin/alliances" data-sveltekit-prefetch="" class="tab font-semibold">Alliances</a></div></nav></header> <main class="mx-auto max-w-7xl px-4 py-4"><div><section class="rounded-box bg-base-100 border-base-300 border p-4 shadow"><h2 class="text-xl font-semibold">Dashboard</h2> <p class="text-base-content/80 mt-1">Admin dashboard content coming soon...</p></section></div></main>';return{c(){t=u(),e=v("div"),e.innerHTML=s,this.h()},l(a){p("svelte-1l26xoh",document.head).forEach(r),t=h(a),e=f(a,"DIV",{class:!0,"data-svelte-h":!0}),x(e)!=="svelte-38hood"&&(e.innerHTML=s),this.h()},h(){document.title="Admin Dashboard - FurryPlace",b(e,"class","bg-base-200 min-h-screen")},m(a,o){l(a,t,o),l(a,e,o)},p:i,i,o:i,d(a){a&&(r(t),r(e))}}}function A(n,t,e){let s;return g(n,w,a=>e(0,s=a)),_(()=>{(!s||s.allianceRole!=="admin")&&y("/")}),[]}class C extends c{constructor(t){super(),d(this,t,A,k,m,{})}}export{C as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{S as r,i as p,s as d,n as l,d as c,b as i,r as m,u as f,h,e as u,w as v,k as x,j as b,l as g,y as _}from"../chunks/DfpL3vsM.js";import"../chunks/IHki7fMi.js";import{c as w}from"../chunks/C4PhwnwB.js";import{g as y}from"../chunks/BPIWuEio.js";function k(o){let s,e,a='<div class="grid h-full grid-cols-[350px_1fr] gap-4"><section class="flex flex-col gap-3 overflow-auto px-1 pb-1"><div class="mb-4 flex items-center justify-between"><div class="flex items-center gap-1"><a class="btn btn-ghost btn-circle" href="/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"></path></svg></a> <span class="relative flex flex-col items-start"><h2 class="text-xl font-semibold">Reported users</h2> <span class="text-base-content/80 text-sm">Open tickets: 0</span></span></div></div> <div class="mt-10 flex flex-col items-center justify-center gap-2"><p class="text-base-content/60">No open tickets</p></div></section> <section class="rounded-box bg-base-100 overflow-auto shadow p-4"><p class="text-base-content/60">Select a ticket to view details</p></section></div>';return{c(){s=x(),e=b("div"),e.innerHTML=a,this.h()},l(t){f("svelte-h8o4mo",document.head).forEach(c),s=h(t),e=u(t,"DIV",{class:!0,"data-svelte-h":!0}),v(e)!=="svelte-1u2xloy"&&(e.innerHTML=a),this.h()},h(){document.title="Moderation - FurryPlace",m(e,"class","bg-base-200 min-h-screen p-4")},m(t,n){i(t,s,n),i(t,e,n)},p:l,i:l,o:l,d(t){t&&(c(s),c(e))}}}function M(o,s,e){let a;return g(o,w,t=>e(0,a=t)),_(()=>{(!a||a.allianceRole==="member")&&y("/")}),[]}class $ extends r{constructor(s){super(),p(this,s,M,k,d,{})}}export{$ as component};
|
||||
@@ -1,302 +0,0 @@
|
||||
// Modal content configuration
|
||||
// This file fetches content from the backend API and patches modals dynamically
|
||||
window.WPLACE_INFO = {
|
||||
// Default fallback content (used if API fails)
|
||||
modal: {
|
||||
overview: {
|
||||
title_en: "Overview",
|
||||
title_zh: "概述",
|
||||
videoUrl: "https://www.youtube.com/embed/xOXtd-WzRxA?si=fHz8Z6ecXGYrDhkN"
|
||||
},
|
||||
paintFaster: {
|
||||
title_en: "How to paint faster",
|
||||
title_zh: "如何画得更快?",
|
||||
mobile_en: "When painting, click on the button on the top right corner of the screen. This will lock the screen but it'll also enable painting by moving your finger over the map.",
|
||||
mobile_zh: "在绘制时候按住按钮屏幕右上角。这将锁定屏幕,但也可以通过在地图上移动手指来绘画.",
|
||||
desktop_en: "Hold SPACE and move your cursor over the map.",
|
||||
desktop_zh: "按住空格并且移动鼠标."
|
||||
},
|
||||
mapLagging: {
|
||||
title_en: "My map is lagging",
|
||||
title_zh: "地图卡顿",
|
||||
text_en: "Follow the instructions to enable hardware acceleration:",
|
||||
text_zh: "请按照说明启用硬件加速:",
|
||||
link: "https://help.constructiononline.com/en/scheduling-webgl-and-hardware-acceleration"
|
||||
},
|
||||
rules: {
|
||||
title_en: "Rules",
|
||||
title_zh: "规则",
|
||||
badge_en: "Important",
|
||||
badge_zh: "重要",
|
||||
items: [
|
||||
{ text_en: "📜 All users are responsible for the content they post. The platform reserves the right of final interpretation.", text_zh: "📜 所有用户对其发布的内容负责。平台保留最终解释权。" },
|
||||
{ text_en: "🛑 Any violation may result in immediate removal of content and permanent ban of the account", text_zh: "🛑 任何违规行为可能导致内容立即删除和账户永久封禁" },
|
||||
{ text_en: "😈 Do not paint over other artworks using random colors or patterns just to mess things up", text_zh: "😈 请勿使用随机颜色或图案覆盖其他作品" },
|
||||
{ text_en: "🙅 Disclosing other's personal information is not allowed", text_zh: "🙅 不允许泄露他人个人信息" }
|
||||
],
|
||||
footer_en: "Violations of these rules may result in suspension of your account.",
|
||||
footer_zh: "违反会导致你被封禁。"
|
||||
},
|
||||
footer: {
|
||||
email: "contact@wplace.live",
|
||||
discord: { url: "https://discord.gg/ZRC4DnP9Z2", text_en: "Feedback and bugs", text_zh: "反馈和错误报告" },
|
||||
github: { url: "https://github.com/FurryPlaceteam", text_en: "Github", text_zh: "Github" },
|
||||
instagram: { url: "https://www.instagram.com/wplace.live/", text_en: "Instagram", text_zh: "Instagram" },
|
||||
terms: { url: "https://wplace.live/terms/terms-of-service", text_en: "Terms", text_zh: "条款" },
|
||||
privacy: { url: "https://wplace.live/terms/privacy", text_en: "Privacy", text_zh: "隐私" }
|
||||
}
|
||||
},
|
||||
|
||||
// Loaded content from API
|
||||
loadedContent: {},
|
||||
contentLoaded: false
|
||||
};
|
||||
|
||||
// Function to get current locale
|
||||
function getCurrentLocale() {
|
||||
return localStorage.getItem('locale') || 'en';
|
||||
}
|
||||
|
||||
// Function to load content from API
|
||||
async function loadContentFromAPI() {
|
||||
try {
|
||||
const locale = getCurrentLocale();
|
||||
const response = await fetch(`/api/site-content?locale=${locale}`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WPLACE_INFO] Failed to load content from API, using defaults');
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
window.WPLACE_INFO.loadedContent = data.content || {};
|
||||
window.WPLACE_INFO.contentLoaded = true;
|
||||
|
||||
console.log('[WPLACE_INFO] Content loaded from API:', Object.keys(window.WPLACE_INFO.loadedContent).length, 'items');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[WPLACE_INFO] Error loading content from API:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get value from API content or fallback
|
||||
function getContent(key, locale = 'en') {
|
||||
const apiKey = key;
|
||||
|
||||
// Try to get from loaded API content first
|
||||
if (window.WPLACE_INFO.contentLoaded && window.WPLACE_INFO.loadedContent[apiKey]) {
|
||||
return window.WPLACE_INFO.loadedContent[apiKey];
|
||||
}
|
||||
|
||||
// Fallback to hardcoded defaults
|
||||
const keys = key.split('.');
|
||||
let obj = window.WPLACE_INFO.modal;
|
||||
|
||||
for (let i = 1; i < keys.length; i++) { // Skip 'modal' prefix
|
||||
obj = obj[keys[i]];
|
||||
if (!obj) return '';
|
||||
}
|
||||
|
||||
// Try localized version first
|
||||
const localizedKey = keys[keys.length - 1] + '_' + locale;
|
||||
if (obj && obj[localizedKey]) return obj[localizedKey];
|
||||
|
||||
return obj || '';
|
||||
}
|
||||
|
||||
// Function to parse rule items from API content
|
||||
function getRuleItems(locale = 'en') {
|
||||
const items = [];
|
||||
let index = 0;
|
||||
|
||||
while (true) {
|
||||
const item = getContent(`modal.rules.item.${index}`, locale);
|
||||
if (!item) break;
|
||||
items.push(item);
|
||||
index++;
|
||||
}
|
||||
|
||||
// Fallback to default if no items found
|
||||
if (items.length === 0 && window.WPLACE_INFO.modal.rules.items) {
|
||||
return window.WPLACE_INFO.modal.rules.items.map(i => i['text_' + locale] || i.text_en);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// Function to patch the info modal
|
||||
function patchInfoModal(modal) {
|
||||
const locale = getCurrentLocale();
|
||||
|
||||
// Find the modal content container
|
||||
const modalBox = modal.querySelector('.modal-box.sm\\:max-w-5xl');
|
||||
if (!modalBox) return;
|
||||
|
||||
// Check if this is the info/welcome modal
|
||||
const hasLogo = modalBox.querySelector('img[alt*="logo" i]');
|
||||
const hasOverview = modalBox.textContent.includes('Overview') || modalBox.textContent.includes('概述');
|
||||
|
||||
if (!hasLogo && !hasOverview) return;
|
||||
|
||||
console.log('[WPLACE_INFO] Patching info modal with custom content');
|
||||
|
||||
// Find all sections and update them
|
||||
const sections = modalBox.querySelectorAll('section');
|
||||
|
||||
sections.forEach(section => {
|
||||
// Patch "How to paint faster" section
|
||||
const paintFasterTitle = section.querySelector('h3');
|
||||
if (paintFasterTitle && (paintFasterTitle.textContent.includes('paint faster') || paintFasterTitle.textContent.includes('画得更快'))) {
|
||||
paintFasterTitle.textContent = getContent('modal.paintFaster.title', locale);
|
||||
|
||||
const mobileText = section.querySelector('.not-touchscreen\\:hidden');
|
||||
if (mobileText) {
|
||||
const textNodes = Array.from(mobileText.childNodes).filter(n => n.nodeType === 3);
|
||||
textNodes.forEach(node => {
|
||||
if (node.textContent.trim()) {
|
||||
node.textContent = getContent('modal.paintFaster.mobile', locale);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const desktopText = section.querySelector('.touchscreen\\:hidden');
|
||||
if (desktopText) {
|
||||
const textNodes = Array.from(desktopText.childNodes).filter(n => n.nodeType === 3);
|
||||
textNodes.forEach(node => {
|
||||
if (node.textContent.trim()) {
|
||||
node.textContent = getContent('modal.paintFaster.desktop', locale);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Patch "My map is lagging" section
|
||||
const laggingTitle = section.querySelector('h3');
|
||||
if (laggingTitle && (laggingTitle.textContent.includes('lagging') || laggingTitle.textContent.includes('卡顿'))) {
|
||||
laggingTitle.textContent = getContent('modal.mapLagging.title', locale);
|
||||
|
||||
const text = section.querySelector('p');
|
||||
if (text) {
|
||||
const link = text.querySelector('a');
|
||||
if (link) {
|
||||
link.href = getContent('modal.mapLagging.link', locale);
|
||||
text.childNodes.forEach(node => {
|
||||
if (node.nodeType === 3 && node.textContent.trim()) {
|
||||
node.textContent = getContent('modal.mapLagging.text', locale) + ' ';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Patch Rules section
|
||||
const rulesHeader = section.querySelector('h3');
|
||||
if (rulesHeader && (rulesHeader.textContent.includes('Rules') || rulesHeader.textContent.includes('规则'))) {
|
||||
rulesHeader.textContent = getContent('modal.rules.title', locale);
|
||||
|
||||
const badge = rulesHeader.querySelector('.badge');
|
||||
if (badge) badge.textContent = getContent('modal.rules.badge', locale);
|
||||
|
||||
const rulesList = section.querySelector('ul');
|
||||
if (rulesList) {
|
||||
const ruleItems = rulesList.querySelectorAll('li');
|
||||
const contentItems = getRuleItems(locale);
|
||||
|
||||
ruleItems.forEach((item, index) => {
|
||||
if (contentItems[index]) {
|
||||
item.textContent = contentItems[index];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const footer = section.querySelector('p.text-base-content\\/80');
|
||||
if (footer) {
|
||||
footer.textContent = getContent('modal.rules.footer', locale);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Patch footer links
|
||||
const footerSection = modalBox.querySelector('section.text-center:last-of-type');
|
||||
if (footerSection) {
|
||||
const emailLink = footerSection.querySelector('a[href^="mailto:"]');
|
||||
if (emailLink) {
|
||||
const email = getContent('modal.footer.email', locale);
|
||||
emailLink.href = 'mailto:' + email;
|
||||
}
|
||||
|
||||
const termsLink = footerSection.querySelector('a[href*="terms-of-service"], a[href*="terms"]');
|
||||
if (termsLink) {
|
||||
termsLink.href = getContent('modal.footer.terms.url', locale);
|
||||
const termsText = getContent('modal.footer.terms.text', locale);
|
||||
if (termsText) termsLink.textContent = termsText;
|
||||
}
|
||||
|
||||
const privacyLink = footerSection.querySelector('a[href*="privacy"]');
|
||||
if (privacyLink) {
|
||||
privacyLink.href = getContent('modal.footer.privacy.url', locale);
|
||||
const privacyText = getContent('modal.footer.privacy.text', locale);
|
||||
if (privacyText) privacyLink.textContent = privacyText;
|
||||
}
|
||||
|
||||
const discordLinks = footerSection.querySelectorAll('a[href*="discord"]');
|
||||
discordLinks.forEach(link => {
|
||||
link.href = getContent('modal.footer.discord.url', locale);
|
||||
});
|
||||
|
||||
const githubLink = footerSection.querySelector('a[href*="github"]');
|
||||
if (githubLink) {
|
||||
githubLink.href = getContent('modal.footer.github.url', locale);
|
||||
}
|
||||
|
||||
const instagramLink = footerSection.querySelector('a[href*="instagram"]');
|
||||
if (instagramLink) {
|
||||
instagramLink.href = getContent('modal.footer.instagram.url', locale);
|
||||
}
|
||||
}
|
||||
|
||||
// Patch video URL
|
||||
const iframe = modalBox.querySelector('iframe[src*="youtube"]');
|
||||
if (iframe) {
|
||||
const videoUrl = getContent('modal.overview.videoUrl', locale);
|
||||
if (videoUrl) iframe.src = videoUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Use MutationObserver to watch for modal being added to DOM
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === 1) {
|
||||
if (node.matches && node.matches('dialog.modal')) {
|
||||
patchInfoModal(node);
|
||||
} else if (node.querySelectorAll) {
|
||||
const modals = node.querySelectorAll('dialog.modal');
|
||||
modals.forEach(modal => patchInfoModal(modal));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize on page load
|
||||
(async function() {
|
||||
'use strict';
|
||||
|
||||
// Load content from API
|
||||
await loadContentFromAPI();
|
||||
|
||||
// Start observing when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
document.querySelectorAll('dialog.modal').forEach(modal => patchInfoModal(modal));
|
||||
});
|
||||
} else {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
document.querySelectorAll('dialog.modal').forEach(modal => patchInfoModal(modal));
|
||||
}
|
||||
|
||||
console.log('[WPLACE_INFO] Monkey patch initialized');
|
||||
})();
|
||||
@@ -1 +0,0 @@
|
||||
{"version":"1759390668366"}
|
||||
@@ -1,93 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>FurryPlace - Paint the world</title>
|
||||
|
||||
<meta property="og:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta name="twitter:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content="https://wplace.live/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta itemprop="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta property="og:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="robots" content="index, follow, max-image-preview:large" />
|
||||
<meta name="color-scheme" content="light only" />
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="Wplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Wplace",
|
||||
"url": "https://wplace.live"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add favicon files to static/img/ -->
|
||||
<!-- <link rel="icon" type="image/png" href="/img/favicon-96x96.png" sizes="96x96" /> -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" /> -->
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/start.CGEerVGH.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/BPIWuEio.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DfpL3vsM.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DgYqO0BT.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/app.D8VNzzNO.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/C1FmrZbK.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/IHki7fMi.js">
|
||||
<script src="./_app/info.js"></script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_dgvam6 = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.CGEerVGH.js"),
|
||||
import("./_app/immutable/entry/app.D8VNzzNO.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
window.pwaInstallPrompt = event;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add service worker if needed -->
|
||||
<!-- <script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
}
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,532 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Site Content Editor - Admin Panel</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.locale-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.locale-selector label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.content-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.content-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content-key {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #1976D2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-item input, .content-item textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.content-item textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.new-item-form {
|
||||
background: #e3f2fd;
|
||||
border: 2px dashed #2196F3;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-item-form.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-message.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎨 Site Content Editor</h1>
|
||||
<p class="subtitle">Manage modal content, rules, and site text</p>
|
||||
|
||||
<div class="success-message" id="successMessage"></div>
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Items</span>
|
||||
<span class="stat-value" id="totalItems">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Current Locale</span>
|
||||
<span class="stat-value" id="currentLocale">en</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="locale-selector">
|
||||
<label for="localeSelect">Locale:</label>
|
||||
<select id="localeSelect">
|
||||
<option value="en">English (en)</option>
|
||||
<option value="zh">Chinese (zh)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="loadContent()">🔄 Reload</button>
|
||||
<button class="btn-secondary" onclick="toggleNewItemForm()">➕ Add New Item</button>
|
||||
<button class="btn-warning" onclick="initializeDefaults()">🔧 Initialize Defaults</button>
|
||||
<button class="btn-primary" onclick="saveAllChanges()">💾 Save All Changes</button>
|
||||
</div>
|
||||
|
||||
<div class="new-item-form" id="newItemForm">
|
||||
<h3>Add New Content Item</h3>
|
||||
<div class="form-row">
|
||||
<input type="text" id="newKey" placeholder="Key (e.g., modal.rules.item.5)">
|
||||
<textarea id="newValue" placeholder="Value"></textarea>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="addNewItem()">Add Item</button>
|
||||
<button class="btn-secondary" onclick="toggleNewItemForm()">Cancel</button>
|
||||
</div>
|
||||
|
||||
<div id="contentContainer" class="loading">
|
||||
Loading content...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentContent = [];
|
||||
let currentLocale = 'en';
|
||||
let changedItems = new Set();
|
||||
|
||||
// Get JWT token from cookie
|
||||
function getAuthToken() {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'j') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
function showSuccess(message) {
|
||||
const el = document.getElementById('successMessage');
|
||||
el.textContent = message;
|
||||
el.classList.add('active');
|
||||
setTimeout(() => el.classList.remove('active'), 3000);
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showError(message) {
|
||||
const el = document.getElementById('errorMessage');
|
||||
el.textContent = message;
|
||||
el.classList.add('active');
|
||||
setTimeout(() => el.classList.remove('active'), 5000);
|
||||
}
|
||||
|
||||
// Toggle new item form
|
||||
function toggleNewItemForm() {
|
||||
const form = document.getElementById('newItemForm');
|
||||
form.classList.toggle('active');
|
||||
}
|
||||
|
||||
// Load content from API
|
||||
async function loadContent() {
|
||||
const locale = document.getElementById('localeSelect').value;
|
||||
currentLocale = locale;
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content', {
|
||||
headers: {
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load content');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
currentContent = data.content.filter(item => item.locale === locale);
|
||||
|
||||
renderContent();
|
||||
updateStats();
|
||||
showSuccess(`Loaded ${currentContent.length} items for locale: ${locale}`);
|
||||
} catch (error) {
|
||||
showError('Error loading content: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render content items
|
||||
function renderContent() {
|
||||
const container = document.getElementById('contentContainer');
|
||||
|
||||
if (currentContent.length === 0) {
|
||||
container.innerHTML = '<div class="loading">No content found. Click "Initialize Defaults" to create default content.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
container.className = 'content-grid';
|
||||
|
||||
currentContent.forEach((item, index) => {
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = 'content-item';
|
||||
itemEl.innerHTML = `
|
||||
<div class="content-item-header">
|
||||
<span class="content-key">${escapeHtml(item.key)}</span>
|
||||
<button class="btn-delete" onclick="deleteItem('${escapeHtml(item.key)}')">🗑 Delete</button>
|
||||
</div>
|
||||
${item.value.length > 100
|
||||
? `<textarea data-key="${escapeHtml(item.key)}" onchange="markChanged('${escapeHtml(item.key)}')">${escapeHtml(item.value)}</textarea>`
|
||||
: `<input type="text" data-key="${escapeHtml(item.key)}" value="${escapeHtml(item.value)}" onchange="markChanged('${escapeHtml(item.key)}')">`
|
||||
}
|
||||
`;
|
||||
container.appendChild(itemEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Mark item as changed
|
||||
function markChanged(key) {
|
||||
changedItems.add(key);
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
function updateStats() {
|
||||
document.getElementById('totalItems').textContent = currentContent.length;
|
||||
document.getElementById('currentLocale').textContent = currentLocale;
|
||||
}
|
||||
|
||||
// Add new item
|
||||
async function addNewItem() {
|
||||
const key = document.getElementById('newKey').value.trim();
|
||||
const value = document.getElementById('newValue').value.trim();
|
||||
|
||||
if (!key || !value) {
|
||||
showError('Both key and value are required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ key, value, locale: currentLocale })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add item');
|
||||
}
|
||||
|
||||
document.getElementById('newKey').value = '';
|
||||
document.getElementById('newValue').value = '';
|
||||
toggleNewItemForm();
|
||||
loadContent();
|
||||
showSuccess('Item added successfully');
|
||||
} catch (error) {
|
||||
showError('Error adding item: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete item
|
||||
async function deleteItem(key) {
|
||||
if (!confirm(`Delete "${key}"?`)) return;
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch(`/api/admin/site-content/${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete item');
|
||||
}
|
||||
|
||||
loadContent();
|
||||
showSuccess('Item deleted successfully');
|
||||
} catch (error) {
|
||||
showError('Error deleting item: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Save all changes
|
||||
async function saveAllChanges() {
|
||||
const items = [];
|
||||
const inputs = document.querySelectorAll('[data-key]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const key = input.getAttribute('data-key');
|
||||
if (changedItems.has(key)) {
|
||||
items.push({
|
||||
key: key,
|
||||
value: input.value,
|
||||
locale: currentLocale
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
showError('No changes to save');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content/bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ items })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save changes');
|
||||
}
|
||||
|
||||
changedItems.clear();
|
||||
loadContent();
|
||||
showSuccess(`Saved ${items.length} items successfully`);
|
||||
} catch (error) {
|
||||
showError('Error saving changes: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize default content
|
||||
async function initializeDefaults() {
|
||||
if (!confirm('Initialize default content? This will create/update default modal content.')) return;
|
||||
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('/api/admin/site-content/initialize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Cookie': `j=${token}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to initialize defaults');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
loadContent();
|
||||
showSuccess(`Initialized ${data.initialized} default items`);
|
||||
} catch (error) {
|
||||
showError('Error initializing defaults: ' + error.message);
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('localeSelect').addEventListener('change', loadContent);
|
||||
|
||||
// Initial load
|
||||
loadContent();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,108 +0,0 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWpCBC10HFw.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWpCBC10HFw.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWpCBC10HFw.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWpCBC10HFw.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWpCBC10HFw.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWpCBC10.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-cXbKDO1w.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-cXbKDO1w.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-cXbKDO1w.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-cXbKDO1w.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-cXbKDO1w.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 189 B |
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path d="M0 0 C1.9375 1.6875 1.9375 1.6875 3 4 C2.92897215 7.4803645 2.61415498 9.29485909 0.3125 11.9375 C-2.89943934 13.41325591 -4.66474802 13.06444212 -8 12 C-9.9375 10.3125 -9.9375 10.3125 -11 8 C-10.92897215 4.5196355 -10.61415498 2.70514091 -8.3125 0.0625 C-5.10056066 -1.41325591 -3.33525198 -1.06444212 0 0 Z " fill="#184AA8" transform="translate(12,2)"/>
|
||||
<path d="M0 0 C0.6875 2.8125 0.6875 2.8125 1 6 C-0.4375 7.875 -0.4375 7.875 -2 9 C-2 8.34 -2 7.68 -2 7 C-2.66 7 -3.32 7 -4 7 C-5.5 5.625 -5.5 5.625 -7 4 C-7.66 3.34 -8.32 2.68 -9 2 C-8.34 2 -7.68 2 -7 2 C-7 1.34 -7 0.68 -7 0 C-3.86650268 -1.04449911 -3.01031744 -0.93423645 0 0 Z " fill="#1D53BB" transform="translate(14,4)"/>
|
||||
<path d="M0 0 C-0.66 0.66 -1.32 1.32 -2 2 C-2.99 2 -3.98 2 -5 2 C-5 3.32 -5 4.64 -5 6 C-6.65 5.67 -8.3 5.34 -10 5 C-10.33 5.99 -10.66 6.98 -11 8 C-10.51970669 2.59670027 -10.51970669 2.59670027 -8.3125 0.0625 C-5.21524075 -1.36056506 -3.24533993 -0.81133498 0 0 Z " fill="#2D8437" transform="translate(12,2)"/>
|
||||
<path d="M0 0 C0.99 0.33 1.98 0.66 3 1 C3 1.66 3 2.32 3 3 C3.66 3 4.32 3 5 3 C5 3.99 5 4.98 5 6 C3.68 6 2.36 6 1 6 C0.34 4.35 -0.32 2.7 -1 1 C-0.67 0.67 -0.34 0.34 0 0 Z " fill="#3FBC4E" transform="translate(7,8)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,93 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>FurryPlace - Paint the world</title>
|
||||
|
||||
<meta property="og:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta name="twitter:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content="https://wplace.live/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta itemprop="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta property="og:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="robots" content="index, follow, max-image-preview:large" />
|
||||
<meta name="color-scheme" content="light only" />
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="Wplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Wplace",
|
||||
"url": "https://wplace.live"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add favicon files to static/img/ -->
|
||||
<!-- <link rel="icon" type="image/png" href="/img/favicon-96x96.png" sizes="96x96" /> -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" /> -->
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/start.CGEerVGH.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/BPIWuEio.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DfpL3vsM.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DgYqO0BT.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/app.D8VNzzNO.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/C1FmrZbK.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/IHki7fMi.js">
|
||||
<script src="./_app/info.js"></script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_dgvam6 = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.CGEerVGH.js"),
|
||||
import("./_app/immutable/entry/app.D8VNzzNO.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
window.pwaInstallPrompt = event;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add service worker if needed -->
|
||||
<!-- <script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
}
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,93 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>FurryPlace - Paint the world</title>
|
||||
|
||||
<meta property="og:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta name="twitter:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content="https://wplace.live/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta itemprop="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta property="og:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="robots" content="index, follow, max-image-preview:large" />
|
||||
<meta name="color-scheme" content="light only" />
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="Wplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Wplace",
|
||||
"url": "https://wplace.live"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add favicon files to static/img/ -->
|
||||
<!-- <link rel="icon" type="image/png" href="/img/favicon-96x96.png" sizes="96x96" /> -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" /> -->
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/start.CGEerVGH.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/BPIWuEio.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DfpL3vsM.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DgYqO0BT.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/app.D8VNzzNO.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/C1FmrZbK.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/IHki7fMi.js">
|
||||
<script src="./_app/info.js"></script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_dgvam6 = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.CGEerVGH.js"),
|
||||
import("./_app/immutable/entry/app.D8VNzzNO.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
window.pwaInstallPrompt = event;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add service worker if needed -->
|
||||
<!-- <script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
}
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,92 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>FurryPlace - Paint the world</title>
|
||||
|
||||
<meta property="og:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta name="twitter:title" content="FurryPlace - A massive real-time pixel art canvas on the world map!" />
|
||||
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content="https://wplace.live/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta itemprop="description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta property="og:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:description" content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together." />
|
||||
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="robots" content="index, follow, max-image-preview:large" />
|
||||
<meta name="color-scheme" content="light only" />
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="Wplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Wplace",
|
||||
"url": "https://wplace.live"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add favicon files to static/img/ -->
|
||||
<!-- <link rel="icon" type="image/png" href="/img/favicon-96x96.png" sizes="96x96" /> -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" /> -->
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/start.CGEerVGH.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/BPIWuEio.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DfpL3vsM.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DgYqO0BT.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/app.D8VNzzNO.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/C1FmrZbK.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/IHki7fMi.js">
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_dgvam6 = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.CGEerVGH.js"),
|
||||
import("./_app/immutable/entry/app.D8VNzzNO.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
window.pwaInstallPrompt = event;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add service worker if needed -->
|
||||
<!-- <script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
}
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
||||