Compare commits

...

3 Commits

15 changed files with 329 additions and 10 deletions

View File

@@ -15,6 +15,7 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@iconify/svelte": "^4.2.0",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",

18
pnpm-lock.yaml generated
View File

@@ -27,6 +27,9 @@ 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)))
@@ -322,6 +325,14 @@ 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==}
'@inlang/paraglide-js@2.0.4':
resolution: {integrity: sha512-5p2Mia2PnwafJQtG6S2UqoHKhqUK7l0goMc5mI6AqQ2lEh14Fkl5uqYwUaI49s6Du4GX5Or1Wp+yBlOA74MKOQ==}
hasBin: true
@@ -1792,6 +1803,13 @@ 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': {}
'@inlang/paraglide-js@2.0.4':
dependencies:
'@inlang/recommend-sherlock': 0.2.1

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,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

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

@@ -0,0 +1,13 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ fetch }) => {
const user = await fetch('/api/public/1/auth/myself/', {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
return {
user: user.ok ? await user.json() : null
};
};

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

@@ -4,16 +4,19 @@ import { type } from "arktype";
const QrcodeType = type("string.integer.parse");
export const load: LayoutLoad = async ({ params, fetch }) => {
export const load: LayoutLoad = async ({ params, fetch, parent }) => {
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',
}
});
const parentData = await parent();
return {
chargecontroller: await data.json(),
...parentData,
qrcode: qrcode,
chargecontroller: await cc.json(),
};
};

View File

@@ -1,3 +1,3 @@
<div class="bg-black pointer-events-auto">
TEST CARD
START CARD
</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,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>

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) {
// Call an external API with form.data, await the result and update form
const response = await fetch("/api/public/1/auth/login/", {
method: 'POST',
body: JSON.stringify(form.data),
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,3 @@
<div class="bg-black pointer-events-auto">
STATUS CARD
</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;
};