Add basic frontend structure
This commit is contained in:
parent
f0eff338ff
commit
4f5f2be68f
29 changed files with 4659 additions and 3811 deletions
7703
frontend/package-lock.json
generated
7703
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -34,7 +34,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.14",
|
"@tailwindcss/vite": "^4.0.14",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
"daisyui": "^5.0.3",
|
"daisyui": "^5.0.3",
|
||||||
|
"i18next": "^24.2.3",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
|
"i18next-chained-backend": "^4.6.2",
|
||||||
|
"i18next-fetch-backend": "^6.0.0",
|
||||||
|
"i18next-localstorage-backend": "^4.2.0",
|
||||||
"tailwindcss": "^4.0.14"
|
"tailwindcss": "^4.0.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,5 +13,17 @@
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
|
"i18n-ally.enabledFrameworks": ["i18next", "svelte"],
|
||||||
|
"i18n-ally.localesPaths": ["static/locales"],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.namespace": true,
|
||||||
|
"i18n-ally.editor.preferEditor": true,
|
||||||
|
"i18n-ally.refactor.templates": [
|
||||||
|
{
|
||||||
|
"templates": ["{{ t('{key}'{args}) }}"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"i18n-ally.sourceLanguage": "en",
|
||||||
|
"i18n-ally.displayLanguage": "en",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
42
frontend/src/app.css
Normal file
42
frontend/src/app.css
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@import 'bootstrap-icons';
|
||||||
|
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes:
|
||||||
|
emerald --default;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "darkgray";
|
||||||
|
default: false;
|
||||||
|
prefersdark: true;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(37% 0.034 259.733);
|
||||||
|
--color-base-200: oklch(27% 0.033 256.848);
|
||||||
|
--color-base-300: oklch(21% 0.006 285.885);
|
||||||
|
--color-base-content: oklch(96% 0.001 286.375);
|
||||||
|
--color-primary: oklch(70% 0.14 182.503);
|
||||||
|
--color-primary-content: oklch(98% 0.014 180.72);
|
||||||
|
--color-secondary: oklch(65% 0.241 354.308);
|
||||||
|
--color-secondary-content: oklch(94% 0.028 342.258);
|
||||||
|
--color-accent: oklch(58% 0.233 277.117);
|
||||||
|
--color-accent-content: oklch(96% 0.018 272.314);
|
||||||
|
--color-neutral: oklch(20% 0 0);
|
||||||
|
--color-neutral-content: oklch(96% 0.001 286.375);
|
||||||
|
--color-info: oklch(74% 0.16 232.661);
|
||||||
|
--color-info-content: oklch(95% 0.026 236.824);
|
||||||
|
--color-success: oklch(76% 0.177 163.223);
|
||||||
|
--color-success-content: oklch(26% 0.051 172.552);
|
||||||
|
--color-warning: oklch(82% 0.189 84.429);
|
||||||
|
--color-warning-content: oklch(27% 0.077 45.635);
|
||||||
|
--color-error: oklch(64% 0.246 16.439);
|
||||||
|
--color-error-content: oklch(96% 0.015 12.422);
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.25rem;
|
||||||
|
--radius-box: 0.5rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
49
frontend/src/lib/component/DashboardCard.svelte
Normal file
49
frontend/src/lib/component/DashboardCard.svelte
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import i18n from '$lib/i18n'
|
||||||
|
|
||||||
|
let props: {
|
||||||
|
title: string
|
||||||
|
icon: string
|
||||||
|
value: number
|
||||||
|
previousValue: number
|
||||||
|
unit?: string
|
||||||
|
} = $props()
|
||||||
|
|
||||||
|
let diff = $derived(
|
||||||
|
Math.round(((props.previousValue - props.value) / props.previousValue) * 100 * 10 * -1) / 10
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow rounded-md basis-0 grow">
|
||||||
|
<div class="card-body gap-2">
|
||||||
|
<div class="flex items-start justify-between gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-base-content/80 font-medium">
|
||||||
|
{props.title}
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 flex items-center gap-2">
|
||||||
|
<p class="text-2xl font-semibold">
|
||||||
|
{props.value}{#if props.unit}{' ' + props.unit}{/if}
|
||||||
|
</p>
|
||||||
|
{#if diff > 0}
|
||||||
|
<div class="badge badge-soft badge-success badge-sm gap-0.5 px-1 font-medium">
|
||||||
|
<i class="bi bi-arrow-up-right"></i>{diff}%
|
||||||
|
</div>
|
||||||
|
{:else if diff < 0}
|
||||||
|
<div class="badge badge-soft badge-error badge-sm gap-0.5 px-1 font-medium">
|
||||||
|
<i class="bi bi-arrow-down-right"></i>{diff}%
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="badge badge-soft badge-warning badge-sm gap-0.5 px-1 font-medium">
|
||||||
|
<i class="bi bi-arrow-right"></i>{diff}%
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="bi {props.icon} text-primary text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-base-content/60 text-sm">
|
||||||
|
{$i18n.t('dashboard:cards.lastMonth', { val: props.previousValue, unit: props.unit })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
94
frontend/src/lib/component/TransactionTable.svelte
Normal file
94
frontend/src/lib/component/TransactionTable.svelte
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import i18n from '$lib/i18n'
|
||||||
|
|
||||||
|
let props: {
|
||||||
|
transactions: {
|
||||||
|
id: string
|
||||||
|
begin: Date
|
||||||
|
end: Date
|
||||||
|
chargepoint: {
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
energyAmmount: number
|
||||||
|
cost: number
|
||||||
|
}[]
|
||||||
|
} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto mt-4 bg-base-100 rounded-md">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{$i18n.t('common:transactionTable.headerDate')}</th>
|
||||||
|
<th>{$i18n.t('common:transactionTable.headerChargepoint')}</th>
|
||||||
|
<th>{$i18n.t('common:transactionTable.headerEnergyTotal')}</th>
|
||||||
|
<th>{$i18n.t('common:transactionTable.headerCost')}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#if props.transactions.length > 0}
|
||||||
|
{#each props.transactions as transaction}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{$i18n.t('common:transactionTable.startTime', {
|
||||||
|
time: transaction.begin,
|
||||||
|
formatParams: {
|
||||||
|
time: {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{$i18n.t('common:transactionTable.endTime', {
|
||||||
|
time: transaction.end,
|
||||||
|
formatParams: {
|
||||||
|
time: {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-plug-fill text-lg"></i>
|
||||||
|
{transaction.chargepoint.name}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="font-bold">{transaction.energyAmmount} kWh</td>
|
||||||
|
<td class="font-bold">{transaction.cost} €</td>
|
||||||
|
<th>
|
||||||
|
<a href="/transaction/{transaction.id}" class="btn btn-sm btn-primary">
|
||||||
|
{$i18n.t('common:transactionTable.detailButton')}
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{#if props.transactions.length === 0}
|
||||||
|
<p class="w-full mt-15 mb-15 text-center text-xl font-bold">
|
||||||
|
{$i18n.t('common:transactionTable.noPreviousTransactions')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
55
frontend/src/lib/i18n-store.ts
Normal file
55
frontend/src/lib/i18n-store.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import type { i18n } from 'i18next'
|
||||||
|
import { writable, type Readable, type Writable } from 'svelte/store'
|
||||||
|
|
||||||
|
export interface TranslationService {
|
||||||
|
i18n: Readable<i18n>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLoading = writable(true)
|
||||||
|
|
||||||
|
export class I18NextTranslationStore implements TranslationService {
|
||||||
|
public i18n: Writable<i18n>
|
||||||
|
public isLoading: Writable<boolean>
|
||||||
|
|
||||||
|
constructor(i18n: i18n) {
|
||||||
|
this.i18n = this.createInstance(i18n)
|
||||||
|
this.isLoading = this.createLoadingInstance(i18n)
|
||||||
|
}
|
||||||
|
|
||||||
|
private createInstance(i18n: i18n): Writable<i18n> {
|
||||||
|
const i18nWritable = writable(i18n)
|
||||||
|
|
||||||
|
i18n.on('initialized', () => {
|
||||||
|
i18nWritable.set(i18n)
|
||||||
|
})
|
||||||
|
i18n.on('loaded', () => {
|
||||||
|
i18nWritable.set(i18n)
|
||||||
|
})
|
||||||
|
i18n.on('added', () => i18nWritable.set(i18n))
|
||||||
|
i18n.on('languageChanged', () => {
|
||||||
|
i18nWritable.set(i18n)
|
||||||
|
})
|
||||||
|
return i18nWritable
|
||||||
|
}
|
||||||
|
|
||||||
|
private createLoadingInstance(i18n: i18n): Writable<boolean> {
|
||||||
|
// if loaded resources are empty || {}, set loading to true
|
||||||
|
i18n.on('loaded', (resources) => {
|
||||||
|
if (Object.keys(resources).length !== 0) {
|
||||||
|
isLoading.set(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// if resources failed loading, set loading to true
|
||||||
|
i18n.on('failedLoading', () => {
|
||||||
|
isLoading.set(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return isLoading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createI18nStore = (i18n: i18n) => {
|
||||||
|
const i18nStore = new I18NextTranslationStore(i18n)
|
||||||
|
return i18nStore.i18n
|
||||||
|
}
|
32
frontend/src/lib/i18n.ts
Normal file
32
frontend/src/lib/i18n.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import Backend from 'i18next-chained-backend'
|
||||||
|
import Fetch from 'i18next-fetch-backend'
|
||||||
|
import LocalStorageBackend from 'i18next-localstorage-backend'
|
||||||
|
import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
import { createI18nStore } from './i18n-store'
|
||||||
|
|
||||||
|
i18next
|
||||||
|
.use(Backend)
|
||||||
|
.use(I18nextBrowserLanguageDetector)
|
||||||
|
.init({
|
||||||
|
supportedLngs: ['en', 'de'],
|
||||||
|
ns: ['common'],
|
||||||
|
defaultNS: 'common',
|
||||||
|
backend: {
|
||||||
|
backends: [LocalStorageBackend, Fetch],
|
||||||
|
backendOptions: [
|
||||||
|
{
|
||||||
|
expirationTime: 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const i18n = createI18nStore(i18next)
|
||||||
|
export default i18n
|
|
@ -1 +0,0 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
23
frontend/src/lib/persistent-store.ts
Normal file
23
frontend/src/lib/persistent-store.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
interface PersistedSettings {
|
||||||
|
darkmode: boolean
|
||||||
|
loggedIn: boolean
|
||||||
|
friendlyName: string
|
||||||
|
email: string
|
||||||
|
role: string
|
||||||
|
refreshToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsDefault: PersistedSettings = {
|
||||||
|
darkmode: false,
|
||||||
|
loggedIn: false,
|
||||||
|
friendlyName: "",
|
||||||
|
email: "",
|
||||||
|
role: "member",
|
||||||
|
refreshToken: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export const persistentSettings = writable<PersistedSettings>(JSON.parse(localStorage.getItem('persistentSettings') || JSON.stringify(settingsDefault)))
|
||||||
|
|
||||||
|
persistentSettings.subscribe((value) => localStorage.persistentSettings = JSON.stringify(value))
|
10
frontend/src/lib/util.ts
Normal file
10
frontend/src/lib/util.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export const currentDaytime = function(): string {
|
||||||
|
const currentHour = new Date().getHours()
|
||||||
|
|
||||||
|
if (currentHour >= 0 && currentHour < 12) {
|
||||||
|
return 'morning'
|
||||||
|
} else if (currentHour >= 12 && currentHour < 18) {
|
||||||
|
return 'day'
|
||||||
|
}
|
||||||
|
return 'evening'
|
||||||
|
}
|
97
frontend/src/routes/(navbar)/+layout.svelte
Normal file
97
frontend/src/routes/(navbar)/+layout.svelte
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
import { persistentSettings } from '$lib/persistent-store'
|
||||||
|
import i18n from '$lib/i18n'
|
||||||
|
let { children } = $props()
|
||||||
|
|
||||||
|
if (!$persistentSettings.loggedIn) {
|
||||||
|
goto('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
let drawerOpen = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full p-4 flex flex-col bg-base-200">
|
||||||
|
<div class="navbar w-auto mb-4 pr-4 bg-base-100 shadow-md rounded-md">
|
||||||
|
<div class="drawer navbar-start">
|
||||||
|
<input bind:checked={drawerOpen} id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
<div class="drawer-content">
|
||||||
|
<label for="nav-drawer" class="btn btn-ghost btn-circle drawer-button">
|
||||||
|
<i class="bi bi-list text-2xl"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="drawer-side z-10">
|
||||||
|
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
|
<ul class="menu bg-base-100 text-base min-h-full w-80 p-4">
|
||||||
|
<li class="mb-4">
|
||||||
|
<a class="btn btn-ghost text-xl" href="/">LibreCharge</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onclick={() => {
|
||||||
|
drawerOpen = !drawerOpen
|
||||||
|
}}
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i class="bi bi-graph-up text-xl"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onclick={() => {
|
||||||
|
drawerOpen = !drawerOpen
|
||||||
|
}}
|
||||||
|
href="/idtoken"
|
||||||
|
>
|
||||||
|
<i class="bi bi-credit-card-fill text-xl"></i>
|
||||||
|
<span>{$i18n.t('common:navbar.link.idtoken')}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onclick={() => {
|
||||||
|
drawerOpen = !drawerOpen
|
||||||
|
}}
|
||||||
|
href="/transaction"
|
||||||
|
>
|
||||||
|
<i class="bi bi-battery-charging text-xl"></i>
|
||||||
|
<span>{$i18n.t('common:navbar.link.transaction')}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onclick={() => {
|
||||||
|
drawerOpen = !drawerOpen
|
||||||
|
}}
|
||||||
|
href="/chargepoint"
|
||||||
|
>
|
||||||
|
<i class="bi bi-plug-fill text-xl"></i>
|
||||||
|
<span>{$i18n.t('common:navbar.link.chargepoint')}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-center">
|
||||||
|
<a class="btn btn-ghost text-xl" href="/">LibreCharge</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
||||||
|
<i class="bi bi-person-circle text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="-1"
|
||||||
|
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
|
||||||
|
>
|
||||||
|
<li><a href="/profile">{$i18n.t('common:navbar.link.profile')}</a></li>
|
||||||
|
<li><a href="/login?logout">{$i18n.t('common:navbar.link.logout')}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-full flex flex-col">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
121
frontend/src/routes/(navbar)/+page.svelte
Normal file
121
frontend/src/routes/(navbar)/+page.svelte
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { persistentSettings } from '$lib/persistent-store'
|
||||||
|
import { currentDaytime } from '$lib/util'
|
||||||
|
import i18n from '$lib/i18n'
|
||||||
|
import DashboardCard from '$lib/component/DashboardCard.svelte'
|
||||||
|
import TransactionTable from '$lib/component/TransactionTable.svelte'
|
||||||
|
|
||||||
|
$i18n.loadNamespaces('dashboard')
|
||||||
|
|
||||||
|
let hasActiveTransaction = $state(true)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full mt-10 flex flex-col">
|
||||||
|
<p class="w-full text-2xl font-bold">
|
||||||
|
{$i18n.t('dashboard:greeting.' + currentDaytime(), { name: $persistentSettings.friendlyName })}
|
||||||
|
</p>
|
||||||
|
<div class="w-full mt-5 flex gap-x-4">
|
||||||
|
<div class="card bg-base-100 shadow rounded-md basis-0 grow">
|
||||||
|
<div class="card-body gap-2">
|
||||||
|
<div class="flex items-start justify-between gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-base-content/80 font-medium">
|
||||||
|
{$i18n.t('dashboard:cards.currentTransaction')}
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 flex items-center gap-2">
|
||||||
|
{#if hasActiveTransaction}
|
||||||
|
<div class="inline-grid *:[grid-area:1/1]">
|
||||||
|
<div class="status status-success animate-ping"></div>
|
||||||
|
<div class="status status-success"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<p class="text-2xl font-semibold">
|
||||||
|
{#if hasActiveTransaction}0,25 kWh{:else}-{/if}
|
||||||
|
</p>
|
||||||
|
{#if hasActiveTransaction}
|
||||||
|
<div class="badge badge-soft badge-success badge-sm gap-0.5 px-1 font-medium">
|
||||||
|
2,30 €
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-plug-fill text-primary text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-row items-center">
|
||||||
|
<p class="text-base-content/60 text-sm">
|
||||||
|
{#if hasActiveTransaction}
|
||||||
|
{$i18n.t('dashboard:cards.chargepoint', { name: 'DE-EXMPL-0001' })}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('dashboard:cards.noCurrentTransaction')}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{#if hasActiveTransaction}
|
||||||
|
<button class="btn btn-xs btn-primary">
|
||||||
|
{$i18n.t('dashboard:cards.toCurrentTransactionButton')}
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DashboardCard
|
||||||
|
title={$i18n.t('dashboard:cards.transactionCount')}
|
||||||
|
icon={'bi-battery-charging'}
|
||||||
|
value={3}
|
||||||
|
previousValue={1}
|
||||||
|
/>
|
||||||
|
<DashboardCard
|
||||||
|
title={$i18n.t('dashboard:cards.transactionEnergyTotal')}
|
||||||
|
icon={'bi-lightning-charge-fill'}
|
||||||
|
value={180}
|
||||||
|
previousValue={50}
|
||||||
|
unit={'kWh'}
|
||||||
|
/>
|
||||||
|
<DashboardCard
|
||||||
|
title={$i18n.t('dashboard:cards.transactionCostTotal')}
|
||||||
|
icon={'bi-currency-exchange'}
|
||||||
|
value={30.56}
|
||||||
|
previousValue={30.56}
|
||||||
|
unit={'€'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col mt-10">
|
||||||
|
<p class="text-xl font-bold">{$i18n.t('dashboard:table.title')}</p>
|
||||||
|
<TransactionTable
|
||||||
|
transactions={[
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
|
||||||
|
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
|
||||||
|
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
|
||||||
|
energyAmmount: 58.65,
|
||||||
|
cost: 36.45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
|
||||||
|
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
|
||||||
|
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
|
||||||
|
energyAmmount: 58.65,
|
||||||
|
cost: 36.45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
|
||||||
|
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
|
||||||
|
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
|
||||||
|
energyAmmount: 58.65,
|
||||||
|
cost: 36.45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
|
||||||
|
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
|
||||||
|
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
|
||||||
|
energyAmmount: 58.65,
|
||||||
|
cost: 36.45,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
0
frontend/src/routes/(navbar)/chargepoint/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/chargepoint/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/idtoken/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/idtoken/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/profile/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/profile/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/transaction/+page.svelte
Normal file
0
frontend/src/routes/(navbar)/transaction/+page.svelte
Normal file
38
frontend/src/routes/+layout.svelte
Normal file
38
frontend/src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<script>
|
||||||
|
import { persistentSettings } from '$lib/persistent-store'
|
||||||
|
let { children } = $props()
|
||||||
|
import '../app.css'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-screen h-screen flex flex-col"
|
||||||
|
data-theme={$persistentSettings.darkmode ? 'darkgray' : 'emerald'}
|
||||||
|
>
|
||||||
|
{@render children()}
|
||||||
|
<footer
|
||||||
|
class="footer sm:footer-horizontal bg-neutral text-neutral-content items-center pt-3 pb-3 pl-6 pr-6"
|
||||||
|
>
|
||||||
|
<aside class="grid-flow-col">
|
||||||
|
<p>Powered by LibreCharge - Licensed under GNU AGPL v3</p>
|
||||||
|
</aside>
|
||||||
|
<nav class="grid-flow-col gap-4 md:place-self-center md:justify-self-end text-3xl">
|
||||||
|
<label class="swap swap-rotate">
|
||||||
|
<!-- this hidden checkbox controls the state -->
|
||||||
|
<input type="checkbox" bind:checked={$persistentSettings.darkmode} />
|
||||||
|
|
||||||
|
<!-- sun icon -->
|
||||||
|
<i class="bi bi-sun swap-off"></i>
|
||||||
|
|
||||||
|
<!-- moon icon -->
|
||||||
|
<i class="bi bi-moon swap-on"></i>
|
||||||
|
</label>
|
||||||
|
<a
|
||||||
|
aria-label="Source Code"
|
||||||
|
href="https://git.bluemedia.dev/Bluemedia/simple-ocpp-cs"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i class="bi bi-git"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
|
@ -1,6 +0,0 @@
|
||||||
<script>
|
|
||||||
import '../style.css'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h1>Welcome to SvelteKit</h1>
|
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
|
57
frontend/src/routes/login/+page.svelte
Normal file
57
frontend/src/routes/login/+page.svelte
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import i18n from '$lib/i18n'
|
||||||
|
$i18n.loadNamespaces('login')
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const isReauth = urlParams.has('reauth')
|
||||||
|
const isLogout = urlParams.has('logout')
|
||||||
|
|
||||||
|
let showToast = $state(true)
|
||||||
|
if (isReauth || isLogout) {
|
||||||
|
setTimeout(() => {
|
||||||
|
showToast = false
|
||||||
|
}, 6000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full flex flex-col justify-center items-center bg-base-200">
|
||||||
|
<div class="basis-0 grow flex justify-center items-center">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<i class="bi bi-ev-front-fill text-8xl text-primary"></i>
|
||||||
|
<p class="text-2xl font-bold">LibreCharge</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="basis-0 flex justify-center items-center">
|
||||||
|
<div class="card bg-base-100 w-full max-w-sm h-fit shrink-0 shadow-2xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="text-2xl font-bold">{$i18n.t('login:title')}</h1>
|
||||||
|
<p class="py-2 text-base">{$i18n.t('login:text')}</p>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<label class="fieldset-label input text-base-content">
|
||||||
|
<i class="bi bi-envelope-at"></i>
|
||||||
|
<input type="email" placeholder="me@example.com" required />
|
||||||
|
</label>
|
||||||
|
<label class="fieldset-label input text-base-content">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
<input type="password" placeholder={$i18n.t('login:passwordPlaceholder')} required />
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-primary mt-4">{$i18n.t('login:button')}</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="basis-0 grow"></span>
|
||||||
|
{#if (isReauth || isLogout) && showToast}
|
||||||
|
<div class="toast toast-top" out:fly={{ x: 200 }}>
|
||||||
|
<div class="alert text-base {isReauth ? 'alert-warning' : 'alert-success'}">
|
||||||
|
{#if isReauth}
|
||||||
|
<i class="bi bi-exclamation-diamond text-xl"></i>
|
||||||
|
{:else}
|
||||||
|
<i class="bi bi-check-circle text-xl"></i>
|
||||||
|
{/if}
|
||||||
|
<span>{$i18n.t(isReauth ? 'login:toast.reauth' : 'login:toast.logoutSuccess')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -1,6 +0,0 @@
|
||||||
@import 'tailwindcss';
|
|
||||||
@plugin "daisyui" {
|
|
||||||
themes:
|
|
||||||
light --default,
|
|
||||||
dark --prefersdark;
|
|
||||||
}
|
|
21
frontend/static/locales/de/common.json
Normal file
21
frontend/static/locales/de/common.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"navbar": {
|
||||||
|
"link": {
|
||||||
|
"idtoken": "Ladekarten",
|
||||||
|
"transaction": "Ladevorgänge",
|
||||||
|
"chargepoint": "Ladestationen",
|
||||||
|
"profile": "Profil",
|
||||||
|
"logout": "Abmelden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transactionTable": {
|
||||||
|
"noPreviousTransactions": "Du hast noch keine abgeschlossenen Ladevorgänge.",
|
||||||
|
"headerDate": "Zeitpunkt",
|
||||||
|
"headerChargepoint": "Ladestation",
|
||||||
|
"headerEnergyTotal": "Ladevolumen",
|
||||||
|
"headerCost": "Kosten",
|
||||||
|
"startTime": "Beginn: {{ time, datetime }} Uhr",
|
||||||
|
"endTime": "Ende: {{ time, datetime }} Uhr",
|
||||||
|
"detailButton": "Details"
|
||||||
|
}
|
||||||
|
}
|
28
frontend/static/locales/de/dashboard.json
Normal file
28
frontend/static/locales/de/dashboard.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"greeting": {
|
||||||
|
"morning": "Guten Morgen {{name}}!",
|
||||||
|
"day": "Guten Tag {{name}}!",
|
||||||
|
"evening": "Guten Abend {{name}}!"
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"currentTransaction": "Aktueller Ladevorgang",
|
||||||
|
"chargepoint": "Ladestation: {{name}}",
|
||||||
|
"toCurrentTransactionButton": "Zum Ladevorgang",
|
||||||
|
"noCurrentTransaction": "Kein Ladevorgang aktiv",
|
||||||
|
"transactionCount": "Ladevorgänge",
|
||||||
|
"transactionEnergyTotal": "Ladevolumen",
|
||||||
|
"transactionCostTotal": "Kosten",
|
||||||
|
"lastMonth": "{{val}} {{unit}} im letzten Monat"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"title": "Deine letzten Ladevorgänge",
|
||||||
|
"noPreviousTransactions": "Du hast noch keine abgeschlossenen Ladevorgänge.",
|
||||||
|
"headerDate": "Zeitpunkt",
|
||||||
|
"headerChargepoint": "Ladestation",
|
||||||
|
"headerEnergyTotal": "Ladevolumen",
|
||||||
|
"headerCost": "Kosten",
|
||||||
|
"startTime": "Beginn: {{ time, datetime }} Uhr",
|
||||||
|
"endTime": "Ende: {{ time, datetime }} Uhr",
|
||||||
|
"detailButton": "Details"
|
||||||
|
}
|
||||||
|
}
|
10
frontend/static/locales/de/login.json
Normal file
10
frontend/static/locales/de/login.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"title": "Anmelden",
|
||||||
|
"text": "Bitte melde dich mit deinem Account an:",
|
||||||
|
"passwordPlaceholder": "Passwort",
|
||||||
|
"button": "Anmelden",
|
||||||
|
"toast": {
|
||||||
|
"reauth": "Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.",
|
||||||
|
"logoutSuccess": "Abmeldung erfolgreich."
|
||||||
|
}
|
||||||
|
}
|
21
frontend/static/locales/en/common.json
Normal file
21
frontend/static/locales/en/common.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"navbar": {
|
||||||
|
"link": {
|
||||||
|
"idtoken": "Charging Cards",
|
||||||
|
"transaction": "Transactions",
|
||||||
|
"chargepoint": "Charging Stations",
|
||||||
|
"profile": "Profile",
|
||||||
|
"logout": "Logout"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transactionTable": {
|
||||||
|
"noPreviousTransactions": "You don't have any completed transactions yet.",
|
||||||
|
"headerDate": "Date",
|
||||||
|
"headerChargepoint": "Chargepoint",
|
||||||
|
"headerEnergyTotal": "Energy charged",
|
||||||
|
"headerCost": "Cost",
|
||||||
|
"startTime": "Start: {{ time, datetime }}",
|
||||||
|
"endTime": "End: {{ time, datetime }}",
|
||||||
|
"detailButton": "Details"
|
||||||
|
}
|
||||||
|
}
|
28
frontend/static/locales/en/dashboard.json
Normal file
28
frontend/static/locales/en/dashboard.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"greeting": {
|
||||||
|
"morning": "Good morning {{name}}!",
|
||||||
|
"day": "Hello {{name}}!",
|
||||||
|
"evening": "Good evening {{name}}!"
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"currentTransaction": "Current Transaction",
|
||||||
|
"chargepoint": "Chargepoint: {{name}}",
|
||||||
|
"toCurrentTransactionButton": "More",
|
||||||
|
"noCurrentTransaction": "No active transaction",
|
||||||
|
"transactionCount": "Transactions",
|
||||||
|
"transactionEnergyTotal": "Energy Ammount",
|
||||||
|
"transactionCostTotal": "Cost",
|
||||||
|
"lastMonth": "{{val}} {{unit}} last month"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"title": "Your recent transactions",
|
||||||
|
"noPreviousTransactions": "You don't have any completed transactions yet.",
|
||||||
|
"headerDate": "Date",
|
||||||
|
"headerChargepoint": "Chargepoint",
|
||||||
|
"headerEnergyTotal": "Energy charged",
|
||||||
|
"headerCost": "Cost",
|
||||||
|
"startTime": "Start: {{ time, datetime }}",
|
||||||
|
"endTime": "End: {{ time, datetime }}",
|
||||||
|
"detailButton": "Details"
|
||||||
|
}
|
||||||
|
}
|
10
frontend/static/locales/en/login.json
Normal file
10
frontend/static/locales/en/login.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"title": "Login",
|
||||||
|
"text": "Please login to your account:",
|
||||||
|
"passwordPlaceholder": "Password",
|
||||||
|
"button": "Login",
|
||||||
|
"toast": {
|
||||||
|
"reauth": "Your session expired. Please log in again.",
|
||||||
|
"logoutSuccess": "Successfully logged out."
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue