Status route, preparing to make Overlay/Portal component

This commit is contained in:
2025-03-25 17:55:47 +01:00
parent 9a41b5424e
commit 4af12dac40
7 changed files with 210 additions and 12 deletions

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,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,4 +1,5 @@
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
import type { User } from '$lib/types/User';
export const load: LayoutLoad = async ({ fetch }) => { export const load: LayoutLoad = async ({ fetch }) => {
const userRes = await fetch('/api/public/1/auth/myself/', { const userRes = await fetch('/api/public/1/auth/myself/', {
@@ -9,9 +10,9 @@ export const load: LayoutLoad = async ({ fetch }) => {
}); });
if (!userRes.ok) return { if (!userRes.ok) return {
user: null, user: null,
chargePermission: [], chargePermission: [] as number[],
}; };
const user = await userRes.json(); const user: User = await userRes.json();
const chargePermissionRes = await fetch(`/api/v3/users/${user['id']}/charge_permissions/`); const chargePermissionRes = await fetch(`/api/v3/users/${user['id']}/charge_permissions/`);
const chargePermission: number[] = chargePermissionRes.ok ? (await chargePermissionRes.json())['authorized_qrcode'] : []; const chargePermission: number[] = chargePermissionRes.ok ? (await chargePermissionRes.json())['authorized_qrcode'] : [];
return { return {

View File

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

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide, scale, fade } from 'svelte/transition';
import { scale } from 'svelte/transition'; import { goto } from "$app/navigation";
let { data } = $props();
let showOverlay = $state(false); let showOverlay = $state(false);
let startingCharge = $state(false); let startingCharge = $state(false);
@@ -11,6 +13,7 @@
try { try {
// TODO: StartCharge fetch request // TODO: StartCharge fetch request
console.log("nop"); console.log("nop");
await goto(`/${data.qrcode}/status`);
} finally { } finally {
startingCharge = false; startingCharge = false;
} }
@@ -20,17 +23,26 @@
if (startingCharge) return; if (startingCharge) return;
showOverlay = false; showOverlay = false;
} }
// TODO: Try to make a real component out of this "modal" using a Portal: https://stackoverflow.com/a/62733195
</script> </script>
<div class="grid grid-rows-6 h-full w-full"> <div class="grid grid-rows-6 h-full w-full">
<div class="row-start-5 row-span-1 col-start-1 flex flex-col justify-center items-center"> <div class="row-start-5 col-start-1 flex flex-col justify-center items-center">
<div class="pointer-events-auto"> <div class="pointer-events-auto">
<button class="btn btn-primary btn-lg uppercase" onclick={() => showOverlay = true}>Attivare la Ricarica</button> <button class="btn btn-primary btn-lg uppercase" onclick={() => showOverlay = true}>Attivare la Ricarica</button>
</div> </div>
</div> </div>
<div class="row-start-1 row-span-6 col-start-1 flex flex-col justify-end items-center z-10"> {#if showOverlay}
<button type="button" class="row-start-1 row-span-6 col-start-1 z-10 bg-base-100/50 pointer-events-auto"
transition:fade={{duration:200}}
onclick={closeOverlay}
aria-label="Close Overlay"
></button>
{/if}
<div class="row-start-1 row-span-6 col-start-1 flex flex-col justify-end items-center">
{#if showOverlay} {#if showOverlay}
<div class="h-fit flex flex-col items-center gap-2 rounded-t-md pointer-events-auto p-2" <div class="h-fit flex flex-col items-center gap-2 rounded-t-md pointer-events-auto p-2 z-20"
data-theme="light" data-theme="light"
transition:slide> transition:slide>
<!--suppress ALL --> <!--suppress ALL -->

View File

@@ -1,3 +1,49 @@
<div class="bg-black pointer-events-auto"> <script lang="ts">
STATUS CARD import { onMount } from "svelte";
</div> import { invalidate } from "$app/navigation";
import BatteryIcon from "$lib/icons/BatteryIcon.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);
});
// TODO: Add Modal from bottom to confirm
</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">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>
</div>