Compare commits

...

7 Commits

27 changed files with 1091 additions and 20 deletions

View File

@@ -15,7 +15,9 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@iconify/svelte": "^4.2.0",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
@@ -40,7 +42,8 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
"esbuild",
"sharp"
]
}
}

351
pnpm-lock.yaml generated
View File

@@ -27,9 +27,15 @@ importers:
'@eslint/js':
specifier: ^9.18.0
version: 9.23.0
'@iconify/svelte':
specifier: ^4.2.0
version: 4.2.0(svelte@5.25.3)
'@sveltejs/adapter-auto':
specifier: ^4.0.0
version: 4.0.0(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))
'@sveltejs/enhanced-img':
specifier: ^0.4.4
version: 0.4.4(rollup@4.37.0)(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2))
'@sveltejs/kit':
specifier: ^2.16.0
version: 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2))
@@ -59,7 +65,7 @@ importers:
version: 5.25.3
svelte-check:
specifier: ^4.0.0
version: 4.1.5(svelte@5.25.3)(typescript@5.8.2)
version: 4.1.5(picomatch@4.0.2)(svelte@5.25.3)(typescript@5.8.2)
sveltekit-superforms:
specifier: ^2.24.0
version: 2.24.0(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))(@types/json-schema@7.0.15)(svelte@5.25.3)(typescript@5.8.2)
@@ -92,6 +98,9 @@ packages:
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
engines: {node: '>=6.9.0'}
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
'@esbuild/aix-ppc64@0.25.1':
resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==}
engines: {node: '>=18'}
@@ -322,6 +331,119 @@ packages:
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
engines: {node: '>=18.18'}
'@iconify/svelte@4.2.0':
resolution: {integrity: sha512-fEl0T7SAPonK7xk6xUlRPDmFDZVDe2Z7ZstlqeDS/sS8ve2uyU+Qa8rTWbIqzZJlRvONkK5kVXiUf9nIc+6OOQ==}
peerDependencies:
svelte: '>4.0.0'
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@inlang/paraglide-js@2.0.4':
resolution: {integrity: sha512-5p2Mia2PnwafJQtG6S2UqoHKhqUK7l0goMc5mI6AqQ2lEh14Fkl5uqYwUaI49s6Du4GX5Or1Wp+yBlOA74MKOQ==}
hasBin: true
@@ -377,6 +499,15 @@ packages:
resolution: {integrity: sha512-ct43jurbe7lsUX5eIrj4ijO3j/6zIPp7CDnFWXDs7UPAbw1Pu1iH3oAmFdP4jcskKJBURH5M9oTtyeiUXyHX8Q==}
engines: {node: '>=18.16.0'}
'@rollup/pluginutils@5.1.4':
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.37.0':
resolution: {integrity: sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==}
cpu: [arm]
@@ -509,6 +640,12 @@ packages:
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/enhanced-img@0.4.4':
resolution: {integrity: sha512-BlBTGfbLUgHa+zSVrsGLOd+noCKWfipoOjoxE26bAAX97v7zh5eiCAp1KEdpkluL05Tl3+nR14gQdPsATyZqoA==}
peerDependencies:
svelte: ^5.0.0
vite: '>= 5.0.0'
'@sveltejs/kit@2.20.2':
resolution: {integrity: sha512-Dv8TOAZC9vyfcAB9TMsvUEJsRbklRTeNfcYBPaeH6KnABJ99i3CvCB2eNx8fiiliIqe+9GIchBg4RodRH5p1BQ==}
engines: {node: '>=18.13'}
@@ -783,6 +920,13 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
@@ -941,6 +1085,9 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -1042,6 +1189,10 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
imagetools-core@7.0.2:
resolution: {integrity: sha512-nrLdKLJHHXd8MitwlXK6/h1TSwGaH3X1DZ3z6yMv/tX7dJ12ecLxZ6P5jgKetfIFh8IJwH9fCWMoTA8ixg0VVA==}
engines: {node: '>=18.0.0'}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -1056,6 +1207,9 @@ packages:
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -1283,6 +1437,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
postcss-load-config@3.1.4:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
@@ -1371,6 +1529,10 @@ packages:
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -1379,6 +1541,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
sirv@3.0.1:
resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
engines: {node: '>=18'}
@@ -1431,6 +1596,11 @@ packages:
svelte:
optional: true
svelte-parse-markup@0.1.5:
resolution: {integrity: sha512-T6mqZrySltPCDwfKXWQ6zehipVLk4GWfH1zCMGgRtLlOIFPuw58ZxVYxVvotMJgJaurKi1i14viB2GIRKXeJTQ==}
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
svelte-toolbelt@0.5.0:
resolution: {integrity: sha512-t3tenZcnfQoIeRuQf/jBU7bvTeT3TGkcEE+1EUr5orp0lR7NEpprflpuie3x9Dn0W9nOKqs3HwKGJeeN5Ok1sQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
@@ -1536,6 +1706,10 @@ packages:
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
engines: {node: '>= 0.10'}
vite-imagetools@7.0.5:
resolution: {integrity: sha512-OOvVnaBTqJJ2J7X1cM1qpH4pj9jsfTxia1VSuWeyXtf+OnP8d0YI1LHpv8y2NT47wg+n7XiTgh3BvcSffuBWrw==}
engines: {node: '>=18.0.0'}
vite@6.2.3:
resolution: {integrity: sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -1636,6 +1810,11 @@ snapshots:
regenerator-runtime: 0.14.1
optional: true
'@emnapi/runtime@1.3.1':
dependencies:
tslib: 2.4.0
optional: true
'@esbuild/aix-ppc64@0.25.1':
optional: true
@@ -1792,6 +1971,88 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {}
'@iconify/svelte@4.2.0(svelte@5.25.3)':
dependencies:
'@iconify/types': 2.0.0
svelte: 5.25.3
'@iconify/types@2.0.0': {}
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
optional: true
'@img/sharp-darwin-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
optional: true
'@img/sharp-libvips-linux-s390x@1.0.4':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
optional: true
'@img/sharp-linux-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
optional: true
'@img/sharp-linux-arm@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
optional: true
'@img/sharp-linux-s390x@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
optional: true
'@img/sharp-linux-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
optional: true
'@img/sharp-wasm32@0.33.5':
dependencies:
'@emnapi/runtime': 1.3.1
optional: true
'@img/sharp-win32-ia32@0.33.5':
optional: true
'@img/sharp-win32-x64@0.33.5':
optional: true
'@inlang/paraglide-js@2.0.4':
dependencies:
'@inlang/recommend-sherlock': 0.2.1
@@ -1866,6 +2127,14 @@ snapshots:
'@poppinss/macroable@1.0.4':
optional: true
'@rollup/pluginutils@5.1.4(rollup@4.37.0)':
dependencies:
'@types/estree': 1.0.7
estree-walker: 2.0.2
picomatch: 4.0.2
optionalDependencies:
rollup: 4.37.0
'@rollup/rollup-android-arm-eabi@4.37.0':
optional: true
@@ -1956,6 +2225,18 @@ snapshots:
'@sveltejs/kit': 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2))
import-meta-resolve: 4.1.0
'@sveltejs/enhanced-img@0.4.4(rollup@4.37.0)(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
magic-string: 0.30.17
sharp: 0.33.5
svelte: 5.25.3
svelte-parse-markup: 0.1.5(svelte@5.25.3)
vite: 6.2.3(jiti@2.4.2)(lightningcss@1.29.2)
vite-imagetools: 7.0.5(rollup@4.37.0)
zimmerframe: 1.1.2
transitivePeerDependencies:
- rollup
'@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2))
@@ -2256,6 +2537,16 @@ snapshots:
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
commander@11.1.0: {}
comment-json@4.2.5:
@@ -2450,6 +2741,8 @@ snapshots:
estraverse@5.3.0: {}
estree-walker@2.0.2: {}
esutils@2.0.3: {}
fast-check@3.23.2:
@@ -2475,7 +2768,9 @@ snapshots:
dependencies:
reusify: 1.1.0
fdir@6.4.3: {}
fdir@6.4.3(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
file-entry-cache@8.0.0:
dependencies:
@@ -2530,6 +2825,8 @@ snapshots:
ignore@5.3.2: {}
imagetools-core@7.0.2: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -2541,6 +2838,8 @@ snapshots:
inline-style-parser@0.2.4: {}
is-arrayish@0.3.2: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
@@ -2724,6 +3023,8 @@ snapshots:
picomatch@2.3.1: {}
picomatch@4.0.2: {}
postcss-load-config@3.1.4(postcss@8.5.3):
dependencies:
lilconfig: 2.1.0
@@ -2811,12 +3112,42 @@ snapshots:
set-cookie-parser@2.7.1: {}
sharp@0.33.5:
dependencies:
color: 4.2.3
detect-libc: 2.0.3
semver: 7.7.1
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-s390x': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
sirv@3.0.1:
dependencies:
'@polka/url': 1.0.0-next.28
@@ -2852,11 +3183,11 @@ snapshots:
dependencies:
has-flag: 4.0.0
svelte-check@4.1.5(svelte@5.25.3)(typescript@5.8.2):
svelte-check@4.1.5(picomatch@4.0.2)(svelte@5.25.3)(typescript@5.8.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.3
fdir: 6.4.3
fdir: 6.4.3(picomatch@4.0.2)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.25.3
@@ -2875,6 +3206,10 @@ snapshots:
optionalDependencies:
svelte: 5.25.3
svelte-parse-markup@0.1.5(svelte@5.25.3):
dependencies:
svelte: 5.25.3
svelte-toolbelt@0.5.0(svelte@5.25.3):
dependencies:
clsx: 2.1.1
@@ -2998,6 +3333,14 @@ snapshots:
validator@13.12.0:
optional: true
vite-imagetools@7.0.5(rollup@4.37.0):
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.37.0)
imagetools-core: 7.0.2
sharp: 0.33.5
transitivePeerDependencies:
- rollup
vite@6.2.3(jiti@2.4.2)(lightningcss@1.29.2):
dependencies:
esbuild: 0.25.1

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { slide, fade } from 'svelte/transition';
import type { Snippet } from "svelte";
let { show = $bindable(), closable = true, children }: {
show: boolean,
closable: boolean
children: Snippet<[{ closeOverlay: () => void }]>
} = $props();
function closeOverlay() {
if (!closable) return;
show = false;
}
</script>
{#if show}
<div class="fixed inset-0 h-screen w-screen z-20 pointer-events-auto flex flex-col justify-end items-center">
<button type="button" class="bg-base-100/50 w-full grow z-20"
transition:fade={{duration:200}}
onclick={closeOverlay}
aria-label="Close Overlay"
></button>
<div class="h-fit flex flex-col items-center gap-2 rounded-t-md pointer-events-auto p-2 z-20"
data-theme="light"
transition:slide>
{@render children({ closeOverlay })}
</div>
</div>
{/if}

35
src/lib/FormInput.svelte Normal file
View File

@@ -0,0 +1,35 @@
<script lang="ts" generics="T extends Record<string, unknown>">
import { Field, Control, FieldErrors } from "formsnap";
import type { FormPath, SuperForm } from "sveltekit-superforms";
let { form, type, label, name }: {
form: SuperForm<T>,
type: string,
label: string,
name: FormPath<T>,
} = $props();
const formData = form.form;
</script>
<div>
<Field {form} {name}>
<Control>
{#snippet children({ props })}
<label class="label floating-label w-full">
<span>{label}</span>
<input {type} class="input w-full aria-[invalid]:input-error" placeholder={label}
bind:value={$formData[name]} {...props}
/>
</label>
{/snippet}
</Control>
<FieldErrors>
{#snippet children({ errors, errorProps })}
{#each errors as err, idx (idx)}
<p class="text-error" {...errorProps}>{err}</p>
{/each}
{/snippet}
</FieldErrors>
</Field>
</div>

View File

@@ -0,0 +1,49 @@
<script lang="ts" generics="T extends Record<string, unknown>">
import { Field, Control, FieldErrors } from "formsnap";
import type { FormPath, SuperForm } from "sveltekit-superforms";
import { scale } from 'svelte/transition';
import Icon from "@iconify/svelte";
let { form, label, name }: {
form: SuperForm<T>,
label: string,
name: FormPath<T>,
} = $props();
const formData = form.form;
let show = $state(false);
let type = $derived(show ? 'text' : 'password');
</script>
<div>
<Field {form} {name}>
<Control>
{#snippet children({ props })}
<div class="flex w-full gap-1">
<label class="label floating-label w-full">
<span>{label}</span>
<input {type} class="input w-full aria-[invalid]:input-error" placeholder={label}
bind:value={$formData[name]} {...props}
/>
</label>
<button class="btn btn-circle grid" onclick={() => show = !show} type="button">
{#if show}
<span transition:scale class="col-start-1 col-end-2 row-start-1 row-end-2"><Icon
icon="mdi:eye-off-outline"/></span>
{:else }
<span transition:scale class="col-start-1 col-end-2 row-start-1 row-end-2"><Icon
icon="mdi:eye-outline"/></span>
{/if}
</button>
</div>
{/snippet}
</Control>
<FieldErrors>
{#snippet children({ errors, errorProps })}
{#each errors as err, idx (idx)}
<p class="text-error" {...errorProps}>{err}</p>
{/each}
{/snippet}
</FieldErrors>
</Field>
</div>

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import type { ToastType } from "$lib/ToastService/types";
import { onMount } from "svelte";
const { toast, destroyer }: {
toast: ToastType,
destroyer: () => void
} = $props();
const alertClass = $derived.by(() => {
switch (toast.type) {
case "success":
return 'alert-success';
case "warning":
return 'alert-warning';
case "error":
return 'alert-error';
case "info":
return 'alert-info';
default:
return '';
}
});
let timeMs = $state(0);
let expireTime = 50000;
let progressMax = 100;
let segmentTime = Math.round(expireTime / progressMax);
const progressValue = $derived(Math.round(timeMs / segmentTime));
function increment() {
timeMs += segmentTime;
if (timeMs >= expireTime) destroyer();
}
onMount(() => {
const interval = setInterval(increment, segmentTime);
return () => clearInterval(interval);
});
</script>
<div class="w-[60vw]">
<div class="absolute w-full flex flex-col px-2">
<progress class="progress h-1 w-full" value={progressValue} max="100"></progress>
</div>
<div class={["alert", alertClass]}>
<span>{toast.content}</span>
</div>
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import type { ServiceState, ToastType } from "$lib/ToastService/types";
import { setContext } from "svelte";
import Toast from "./Toast.svelte";
const { children } = $props();
let serviceState: ServiceState = $state({ toasts: {}, nextId: 0 });
function addToast(toast: ToastType) {
serviceState.toasts[serviceState.nextId++] = toast;
}
setContext('toastservice', addToast);
function destroyToast(id: string) {
delete serviceState.toasts[id];
}
</script>
{@render children()}
<div class="toast toast-top toast-center z-80">
{#each Object.entries(serviceState.toasts) as [i, toast] (i)}
<Toast {toast} destroyer={() => destroyToast(i)}/>
{/each}
</div>

View File

@@ -0,0 +1,13 @@
export type ToastType = {
type: 'info' | 'success' | 'warning' | 'error',
content: string,
}
export type InternalToasts = {
[key: string]: ToastType,
}
export type ServiceState = {
toasts: InternalToasts,
nextId: number,
}

View File

@@ -0,0 +1,8 @@
import { getContext } from "svelte";
import type { ToastType } from "$lib/ToastService/types";
export function useToast(): { fireToast: (toast: ToastType) => void } {
return {
fireToast: getContext('toastservice')
};
}

View File

@@ -0,0 +1,69 @@
<div class="flex items-center">
<div class="outer">
<div class="inner"></div>
<div class="inner"></div>
<div class="inner"></div>
</div>
<div class="pole"></div>
</div>
<style lang="css">
.outer {
width: 50px;
height: 25px;
border: 3px solid #fff;
display: flex;
padding: 2px;
border-radius: 2px;
transition: all 0.3s;
}
.inner {
height: 100%;
background-color: #fff;
flex: 1;
border-radius: 0.2px;
opacity: 0;
animation: ani1 2.5s infinite;
}
.inner:nth-child(2) {
margin: 0 1px;
animation: ani2 2.5s infinite;
}
.inner:nth-child(3) {
animation: ani3 2.5s infinite;
}
.pole {
width: 4px;
height: 10px;
border-radius: 0 1px 1px 0;
background-color: #fff;
}
@keyframes ani1 {
33%, 100% {
opacity: 1;
}
}
@keyframes ani2 {
0%, 33% {
opacity: 0;
}
66%, 100% {
opacity: 1;
}
}
@keyframes ani3 {
0%, 66% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<svg
id="Calque_1"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 101 92"
style="enable-background: new 0 0 101 92"
xml:space="preserve"
>
<path d="M15,40c0,5.5,4.5,10,10,10s10-4.5,10-10s-4.5-10-10-10S15,34.5,15,40z M20,40c0-2.8,2.2-5,5-5s5,2.2,5,5s-2.2,5-5,5
S20,42.8,20,40z M27,62c0,5.5,4.5,10,10,10s10-4.5,10-10s-4.5-10-10-10S27,56.5,27,62z M32,62c0-2.8,2.2-5,5-5s5,2.2,5,5s-2.2,5-5,5
S32,64.8,32,62z M53,62c0,5.5,4.5,10,10,10s10-4.5,10-10s-4.5-10-10-10S53,56.5,53,62z M58,62c0-2.8,2.2-5,5-5s5,2.2,5,5s-2.2,5-5,5
S58,64.8,58,62z M40,40c0,5.5,4.5,10,10,10s10-4.5,10-10s-4.5-10-10-10S40,34.5,40,40z M45,40c0-2.8,2.2-5,5-5s5,2.2,5,5s-2.2,5-5,5
S45,42.8,45,40z M65,40c0,5.5,4.5,10,10,10s10-4.5,10-10s-4.5-10-10-10S65,34.5,65,40z M70,40c0-2.8,2.2-5,5-5s5,2.2,5,5s-2.2,5-5,5
S70,42.8,70,40z M55,23c0,4.4,3.6,8,8,8s8-3.6,8-8s-3.6-8-8-8S55,18.6,55,23z M60,23c0-1.7,1.3-3,3-3s3,1.3,3,3s-1.3,3-3,3
S60,24.7,60,23z M30,23c0,4.4,3.6,8,8,8s8-3.6,8-8s-3.6-8-8-8S30,18.6,30,23z M35,23c0-1.7,1.3-3,3-3s3,1.3,3,3s-1.3,3-3,3
S35,24.7,35,23z M0,41.5C0,69.4,22.6,92,50.5,92S101,69.4,101,41.5c0-12.8-5.3-24.1-12.7-33.4C82,0.3,70.8,0,70.8,0H30.7
c-2.8,0-11.8,0.2-17.9,7.8C4.9,16.7,0,28.5,0,41.5z M9,42.4C9,32,13,22.5,19.5,15.3C24.4,9.2,31.7,9,34,9h32.5c0,0,9,0.2,14.2,6.5
C86.7,23,91,32.1,91,42.4C91,64.8,72.6,83,50,83S9,64.8,9,42.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,58 @@
export type ChargeUser = {
id: number;
username: string;
};
export type LastPower = {
chargingstation: number;
power: number;
timestamp: string; // ISO 8601 datetime string
};
export type ActiveCharge = {
id: number;
start: string; // ISO 8601 datetime string
stop: string | null; // ISO 8601 datetime string
user: ChargeUser;
last_nonzero_power_time: string | null; // ISO 8601 datetime string
energy_wh: number;
last_power: LastPower | null;
// coupon: string;
};
// custom_message_fallback_lang is guaranteed to exist
export type CustomMessage = {
en?: string,
it?: string,
de?: string,
fr?: string,
nl?: string,
}
export type ChargeController = {
qrcodeid: number;
active_charge: ActiveCharge | null;
park: number;
park_bnum: number;
evse_id: string;
latitude: number;
longitude: number;
accept_guests: boolean;
is_payment_configured: boolean;
price: number;
override_logo_url: string;
show_phrases_frontend: boolean;
logged_user: ChargeUser | null;
has_charge_permission: boolean;
show_custom_message: boolean;
custom_message: CustomMessage;
country_alpha2: string;
custom_message_fallback_lang: string;
imprint_cents: number;
google_pay: boolean;
coupon_enabled: boolean;
last_power_timestamp: string | null; // ISO 8601 datetime string
suspended: boolean;
test_mode: boolean;
mandatory_email: boolean;
};

9
src/lib/types/User.ts Normal file
View File

@@ -0,0 +1,9 @@
export type User = {
id: number;
username: string;
email: string;
is_active: boolean;
is_staff: boolean;
is_superuser: boolean;
is_readonly: boolean;
};

View File

@@ -1,7 +1,10 @@
<script lang="ts">
import '../app.css';
import '../app.css';
import ToastProvider from "$lib/ToastService/ToastProvider.svelte";
let { children } = $props();
let { children } = $props();
</script>
{@render children()}
<ToastProvider>
{@render children()}
</ToastProvider>

22
src/routes/+layout.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { LayoutLoad } from './$types';
import type { User } from '$lib/types/User';
export const load: LayoutLoad = async ({ fetch }) => {
const userRes = await fetch('/api/public/1/auth/myself/', {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
if (!userRes.ok) return {
user: null,
chargePermission: [] as number[],
};
const user: User = await userRes.json();
const chargePermissionRes = await fetch(`/api/v3/users/${user['id']}/charge_permissions/`);
const chargePermission: number[] = chargePermissionRes.ok ? (await chargePermissionRes.json())['authorized_qrcode'] : [];
return {
user: user,
chargePermission: chargePermission,
};
};

View File

@@ -1,10 +1,34 @@
<script lang="ts">
import { page } from '$app/state';
import type { LayoutProps } from './$types';
import ConnType2 from "$lib/icons/ConnType2.svelte";
import Icon from "@iconify/svelte";
import { goto, invalidateAll } from "$app/navigation";
let { data, children }: LayoutProps = $props();
const cc = $derived(data.chargecontroller);
const user = $derived(data.user);
const qrcode = $derived(page.params.qrcode);
let logouting = $state(false);
async function logout() {
logouting = true;
try {
const response = await fetch("/api/public/1/auth/logout/", {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
if (response.ok) {
await invalidateAll();
await goto(`/${qrcode}`);
}
} finally {
logouting = false;
}
}
</script>
<div class="flex flex-col h-screen">
@@ -15,21 +39,50 @@
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
<li><p>QRCODE: {qrcode}</p></li>
<li><p>
{#if user}
{user['username']}
{:else}
No User
{/if}
</p></li>
<li>
<details>
<summary>PARK: {cc['park']}</summary>
<summary>
EN
</summary>
<ul class="bg-base-100 rounded-t-none p-2">
<li><p>EN</p></li>
<li><p>IT</p></li>
</ul>
</details>
</li>
<li>
<button class={["h-full w-full", logouting && "bg-gray-300"]} onclick={logout}>
<Icon class="h-5 w-5" icon="mdi:logout"/>
</button>
</li>
</ul>
</div>
</div>
{#await import('./Map.svelte') then { default: Map }}
<Map class="grow h-10 -z-20" x={cc['latitude']} y={cc['longitude']}/>
<Map class="grow h-10 -z-20" x={cc['latitude']} y={cc['longitude']}>
<div class="flex flex-col gap-1">
<div class="inline-flex items-center gap-2">
<p class="!m-0">Informations</p>
<div class="tooltip" data-tip="Type 2">
<div class="w-5 h-5">
<ConnType2/>
</div>
</div>
</div>
<div class="w-full border-b border-b-gray-300"></div>
<div class="inline-flex items-center gap-2">
<Icon class="h-5 w-5 text-success" icon="mdi:checkbox-marked-circle"/>
<p class="!m-0">Disponibile</p>
</div>
</div>
</Map>
{/await}
</div>
<div class="z-20 mt-16 grow pointer-events-none">

View File

@@ -1,19 +1,25 @@
import type { LayoutLoad } from './$types';
import { error } from "@sveltejs/kit";
import { type } from "arktype";
import type { ChargeController } from "$lib/types/ChargeController";
const QrcodeType = type("string.integer.parse");
export const load: LayoutLoad = async ({ params, fetch }) => {
export const load: LayoutLoad = async ({ params, fetch, parent, depends }) => {
const qrcode = QrcodeType(params.qrcode);
if (qrcode instanceof type.errors) error(400, 'invalid qrcode');
const data = await fetch(`/api/v2/chargecontroller/${qrcode}/`, {
const cc = await fetch(`/api/v2/chargecontroller/${qrcode}/`, {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
depends('app:chargecontroller');
const parentData = await parent();
const chargecontroller: ChargeController = await cc.json();
return {
chargecontroller: await data.json(),
...parentData,
qrcode: qrcode,
chargecontroller: chargecontroller,
};
};

View File

@@ -1,3 +1,59 @@
<div class="bg-black pointer-events-auto">
TEST CARD
<script lang="ts">
import { scale } from 'svelte/transition';
import { goto } from "$app/navigation";
import BSlideOverlay from "$lib/BSlideOverlay.svelte";
import { useToast } from "$lib/ToastService/useToast";
let { data } = $props();
const { fireToast } = useToast();
let showOverlay = $state(false);
let startingCharge = $state(false);
async function startCharge() {
if (startingCharge) return;
startingCharge = true;
try {
// TODO: StartCharge fetch request
// const response = await fetch(`/api/public/1/chargecontroller/${data.qrcode}/start_charge/`, {
// method: 'POST',
// headers: {
// 'Accept': 'application/json',
// }
// });
console.log("nop");
await goto(`/${data.qrcode}/status`);
} finally {
startingCharge = false;
}
}
</script>
<div class="grid grid-rows-6 h-full w-full">
<div class="row-start-5 col-start-1 flex flex-col justify-center items-center">
<div class="pointer-events-auto">
<button class="btn btn-primary btn-lg uppercase" onclick={() => showOverlay = true}>Attivare la Ricarica</button>
<button class="btn btn-secondary" onclick={() => fireToast({type: 'success', content: 'Messaggio Ricevuto!'})}>
TEST
</button>
</div>
</div>
<BSlideOverlay bind:show={showOverlay} closable={!startingCharge}>
{#snippet children({ closeOverlay })}
<!--suppress ALL -->
<enhanced:img alt="Please attach the connector to the car" src="./attach_car.png"/>
<div class="font-bold uppercase text-md text-gray-700">Avete connesso il cavo alla vostra auto?</div>
<div class="flex flex-row justify-around gap-4 w-full">
<button class="btn btn-primary btn-xl w-24 grid" onclick={startCharge}>
{#if startingCharge}
<span transition:scale class="col-start-1 col-end-2 row-start-1 row-end-2 loading"></span>
{:else}
<span transition:scale class="col-start-1 col-end-2 row-start-1 row-end-2">SI</span>
{/if}
</button>
<button class="btn btn-secondary btn-xl w-24" onclick={closeOverlay}>NO</button>
</div>
{/snippet}
</BSlideOverlay>
</div>

View File

@@ -0,0 +1,13 @@
import type { PageLoad } from './$types';
import { redirect } from "@sveltejs/kit";
export const load: PageLoad = async ({ parent }) => {
const parentData = await parent();
if (parentData.user === null) return redirect(303, `${parentData.qrcode}/login`);
else {
if (!parentData.chargePermission.includes(parentData.qrcode)) {
return redirect(303, `${parentData.qrcode}/forbidden`);
}
}
return parentData;
};

View File

@@ -1,17 +1,19 @@
<script lang="ts">
import * as L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { onMount } from "svelte";
import { onMount, type Snippet } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";
type Props = {
x: number,
y: number,
children?: Snippet,
} & SvelteHTMLElements['div']
let { x, y, ...rest }: Props = $props();
let { x, y, children, ...rest }: Props = $props();
let mapDiv: HTMLDivElement;
let popupDiv: HTMLDivElement;
let map: L.Map;
onMount(() => {
@@ -20,9 +22,16 @@
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
L.marker([x, y]).addTo(map);
const marker = L.marker([x, y]).addTo(map);
if (children !== undefined)
marker.bindPopup(popupDiv, { closeOnClick: false }).openPopup();
});
</script>
<div bind:this={mapDiv} {...rest}>
</div>
<div class="hidden">
<div bind:this={popupDiv} class="h-full w-full">
{@render children?.()}
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,8 @@
<div class="pointer-events-auto bg-red-500/50 h-full w-full flex justify-center items-center">
<div class="card card-border bg-base-100">
<div class="card-body items-center">
<h2 class="card-title">Forbidden</h2>
<p>You don't have permission to use this chargepoint</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,89 @@
<script>
import { superForm, defaults, setMessage } from 'sveltekit-superforms';
import { type } from "arktype";
import { arktypeClient } from 'sveltekit-superforms/adapters';
import FormInput from "$lib/FormInput.svelte";
import PwdFormInput from "$lib/PwdFormInput.svelte";
import { goto, invalidateAll } from "$app/navigation";
let { data } = $props();
const schema = type({
username: 'string > 3',
password: 'string > 3',
});
const schemaDefaults = {
username: '',
password: '',
};
const form = superForm(defaults(schemaDefaults, arktypeClient(schema)), {
SPA: true,
validators: arktypeClient(schema),
resetForm: false,
async onUpdate({ form }) {
// Equivalent to onSubmit for this context. We can validate and then execute things.
if (form.valid) {
const payload = { ...form.data, qrcodeid: data.qrcode };
const response = await fetch("/api/public/1/auth/login/", {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
console.log(response.status);
if (response.status !== 200) {
setMessage(form, {
status: 'error',
text: `HTTP Code ${response.status}:\n${JSON.stringify(await response.json(), null, 2)}`,
});
} else {
setMessage(form, {
status: 'success',
text: 'Login succeded, redirecting...',
});
await invalidateAll();
await goto(`/${data.qrcode}`);
}
}
},
});
const { message, enhance, submitting } = form;
</script>
<div class="pointer-events-auto bg-gray-300/50 h-full w-full flex justify-center items-center">
<div class="card bg-base-100 w-fit shadow-md">
<form class="card-body" method="POST" use:enhance>
<h2 class="card-title">Accesso Utente</h2>
<div class="flex flex-col gap-2 my-2">
<FormInput {form} type="text" label="Username" name="username"/>
<PwdFormInput {form} label="Password" name="password"/>
</div>
<div class="card-actions justify-center">
<button class="btn btn-primary" type="submit">
{#if $submitting}
<span class="loading"></span>
{/if}
Login
</button>
</div>
{#if $message}
<div class="card bg-base-200 mt-4 card-border max-w-[80vw]">
<div class="card-body">
<h2 class="card-title" class:text-error={$message['status'] === 'error'}>
{$message['status'] === 'success' ? 'Success!' : 'Error'}
</h2>
<div>
{$message['text']}
</div>
</div>
</div>
{/if}
</form>
</div>
</div>

View File

@@ -0,0 +1,8 @@
import type { PageLoad } from './$types';
import { redirect } from "@sveltejs/kit";
export const load: PageLoad = async ({ parent }) => {
const parentData = await parent();
if (parentData.user !== null) return redirect(303, `/${parentData.qrcode}`);
return parentData;
};

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto, invalidate } from "$app/navigation";
import { scale } from 'svelte/transition';
import BatteryIcon from "$lib/icons/BatteryIcon.svelte";
import BSlideOverlay from "$lib/BSlideOverlay.svelte";
let { data } = $props();
onMount(() => {
const interval = setInterval(() => {
invalidate('app:chargecontroller');
}, 5000);
return () => {
clearInterval(interval);
};
});
const energykWh: string = $derived.by(() => {
const wh = data.chargecontroller.active_charge?.energy_wh ?? 0;
return (wh / 1000).toFixed(2);
});
const powerkWh: string = $derived.by(() => {
const wh = data.chargecontroller.active_charge?.last_power?.power ?? 0;
return (wh / 1000).toFixed(2);
});
let showOverlay = $state(false);
let stoppingCharge = $state(false);
async function stopCharge() {
if (stoppingCharge) return;
stoppingCharge = true;
try {
// TODO: StopCharge fetch request
console.log("nop");
await goto(`/${data.qrcode}`);
} finally {
stoppingCharge = false;
}
}
</script>
<div class="pointer-events-auto bg-green-700/80 h-full w-full flex flex-col justify-center items-center gap-12">
<div>
<BatteryIcon/>
</div>
<div class="text-center">
<p>Energia erogata a {data.user.username}:</p>
<p class="text-4xl"><span class="text-5xl">{energykWh}</span> kWh</p>
<p class="text-sm">Potenza misurata: {powerkWh} kW</p>
</div>
<div class="w-[90%]">
<button class="btn btn-warning btn-xl w-full" onclick={() => showOverlay = true}>Interrompere la ricarica</button>
</div>
<div class="text-center w-[70%] flex flex-col gap-5 text-sm">
<p>
«Ciò che sta accadendo ci pone di fronte allurgenza di procedere in una coraggiosa rivoluzione culturale.»
</p>
<p>
Papa Francesco, Enciclica Laudato Si
</p>
</div>
<BSlideOverlay bind:show={showOverlay} closable={!stoppingCharge}>
{#snippet children({ closeOverlay })}
<p class="font-bold w-[90%] text-wrap uppercase text-center">Sei sicuro di voler interrompere la ricarica?</p>
<div class="flex flex-row justify-around gap-4 w-full">
<button class="btn btn-primary btn-xl w-24 grid" onclick={stopCharge}>
{#if stoppingCharge}
<span transition:scale class="col-start-1 col-end-2 row-start-1 row-end-2 loading"></span>
{:else}
<span transition:scale class="col-start-1 col-end-2 row-start-1 row-end-2">SI</span>
{/if}
</button>
<button class="btn btn-secondary btn-xl w-24" onclick={closeOverlay}>NO</button>
</div>
{/snippet}
</BSlideOverlay>
</div>

View File

@@ -0,0 +1,8 @@
import type { PageLoad } from './$types';
import { redirect } from "@sveltejs/kit";
export const load: PageLoad = async ({ parent }) => {
const parentData = await parent();
if (parentData.user === null) return redirect(303, `${parentData.qrcode}/login`);
return parentData;
};

View File

@@ -1,11 +1,13 @@
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { enhancedImages } from '@sveltejs/enhanced-img';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tailwindcss(),
enhancedImages(),
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',