feat(dashboard,accounts,charts): v1 dashboard redrawn, account lists and pages, svg chart kit

This commit is contained in:
hh committed 2026-10-16 03:50:02 +00:00
1 parent 62cd17ecde
commit 9e89829bae
36 files changed
+5678 -16

No files matched your search

+91
View File
@@ -1,5 +1,96 @@
<script lang="ts">
import { createQuery, keepPreviousData } from "@tanstack/svelte-query";
import AccountBox from "#lib/components/dashboard/AccountBox.svelte";
import AccountsChart from "#lib/components/dashboard/AccountsChart.svelte";
import BudgetsChart from "#lib/components/dashboard/BudgetsChart.svelte";
import CategoriesChart from "#lib/components/dashboard/CategoriesChart.svelte";
import DebtsBox from "#lib/components/dashboard/DebtsBox.svelte";
import {
accountsChartQuery,
accountsQuery,
seriesPoints,
} from "#lib/components/dashboard/data.js";
import InfoBoxes from "#lib/components/dashboard/InfoBoxes.svelte";
import InsightBox from "#lib/components/dashboard/InsightBox.svelte";
import PiggyBanksBox from "#lib/components/dashboard/PiggyBanksBox.svelte";
import SubscriptionsBox from "#lib/components/dashboard/SubscriptionsBox.svelte";
import PageHeader from "#lib/components/shell/PageHeader.svelte";
import { today } from "#lib/dates.js";
import { period } from "#lib/period.svelte.js";
import { session } from "#lib/session.svelte.js";
const assets = createQuery(() => ({
...accountsQuery("asset", period.start, period.end),
placeholderData: keepPreviousData,
}));
const chart = createQuery(() => ({
...accountsChartQuery(period.start, period.end),
placeholderData: keepPreviousData,
}));
const frontpage = $derived.by(() => {
const list = (assets.data ?? [])
.filter((a) => a.attributes.active !== false)
.sort((a, b) => (a.attributes.order ?? 0) - (b.attributes.order ?? 0));
if (!("frontpageAccounts" in session.prefs)) {
return list;
}
const ids = new Set(session.frontpageAccounts);
return list.filter((a) => ids.has(a.id));
});
const trends = $derived(
new Map(
(chart.data ?? []).map((set) => {
const points = seriesPoints(set, false);
return [
set.label,
{
marker: points.findIndex((p) => p.x === today()),
values: points.map((p) => p.y),
},
];
})
)
);
$effect(() => {
if (
assets.isSuccess &&
!assets.isPlaceholderData &&
assets.data.length === 0
) {
location.assign("/new-user");
}
});
</script>
<PageHeader subtitle="What's playing?" title="Dashboard" />
<InfoBoxes />
<div class="mt-3 grid gap-3 lg:grid-cols-12">
<div class="flex min-w-0 flex-col gap-3 lg:col-span-8">
<AccountsChart />
<BudgetsChart />
<CategoriesChart />
</div>
<div class="flex min-w-0 flex-col gap-3 lg:col-span-4">
{#each frontpage as account (account.id)}
{@const trend = trends.get(account.attributes.name)}
<AccountBox
{account}
marker={trend?.marker ?? -1}
trend={trend?.values ?? []}
/>
{/each}
<SubscriptionsBox />
<PiggyBanksBox />
<DebtsBox />
</div>
</div>
<div class="mt-3 flex flex-col gap-3">
<InsightBox kind="expense" />
<InsightBox kind="revenue" />
</div>
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export function load() {
redirect(307, "/accounts/asset");
}
@@ -0,0 +1,11 @@
<script lang="ts">
import type { AccountType } from "#lib/api/types.js";
import AccountsIndex from "#lib/components/accounts/AccountsIndex.svelte";
import { page } from "$app/state";
const type = $derived(page.params.type as AccountType);
</script>
{#key type}
<AccountsIndex {type} />
{/key}
@@ -0,0 +1,25 @@
<script lang="ts">
import type { AccountType } from "#lib/api/types.js";
import AccountForm from "#lib/components/accounts/AccountForm.svelte";
import {
indexUrl,
TYPE_ONE,
TYPE_TITLE,
} from "#lib/components/accounts/links.js";
import PageHeader from "#lib/components/shell/PageHeader.svelte";
import { page } from "$app/state";
const type = $derived(page.params.type as AccountType);
</script>
<PageHeader
crumbs={[
{ href: indexUrl(type), label: TYPE_TITLE[type] },
{ label: `Create a new ${TYPE_ONE[type]}` },
]}
title="Create a new {TYPE_ONE[type]}"
/>
{#key type}
<AccountForm {type} />
{/key}
@@ -0,0 +1,40 @@
<script lang="ts">
import { createQuery } from "@tanstack/svelte-query";
import { api } from "#lib/api/client.js";
import type { Account, One } from "#lib/api/types.js";
import AccountForm from "#lib/components/accounts/AccountForm.svelte";
import {
indexUrl,
showUrl,
TYPE_TITLE,
typeOf,
} from "#lib/components/accounts/links.js";
import PageHeader from "#lib/components/shell/PageHeader.svelte";
import { page } from "$app/state";
const id = $derived(page.params.id ?? "");
const account = createQuery(() => ({
queryFn: () => api<One<Account>>(`/v1/accounts/${id}`),
queryKey: ["account-edit", id],
staleTime: 0,
}));
const item = $derived(account.data?.data);
const type = $derived(item ? typeOf(item.attributes) : "asset");
</script>
<PageHeader
crumbs={[
{ href: indexUrl(type), label: TYPE_TITLE[type] },
{ href: showUrl(id), label: item?.attributes.name ?? "" },
{ label: "Edit" },
]}
title={item ? `Edit ${item.attributes.name}` : "Edit account"}
/>
{#if account.isError}
<p class="text-expense">{account.error.message}</p>
{:else if item}
{#key item.id}
<AccountForm account={item} {type} />
{/key}
{/if}
@@ -0,0 +1,13 @@
<script lang="ts">
import Reconcile from "#lib/components/accounts/Reconcile.svelte";
import { period } from "#lib/period.svelte.js";
import { page } from "$app/state";
const id = $derived(page.params.id ?? "");
const start = $derived(page.params.start ?? period.start);
const end = $derived(page.params.end ?? period.end);
</script>
{#key `${id}-${start}-${end}`}
<Reconcile {end} {id} {start} />
{/key}
@@ -0,0 +1,32 @@
<script lang="ts">
import { untrack } from "svelte";
import AccountShow from "#lib/components/accounts/AccountShow.svelte";
import { period } from "#lib/period.svelte.js";
import { goto } from "$app/navigation";
import { page } from "$app/state";
const id = $derived(page.params.id ?? "");
const start = $derived(page.params.start);
const end = $derived(page.params.end);
let applied = "";
$effect(() => {
const key = `${start}|${end}`;
if (start && end && key !== applied) {
applied = key;
period.set(start <= end ? { end, start } : { end: start, start: end });
return;
}
const moved = period.start !== start || period.end !== end;
untrack(() => {
if (start && end && moved) {
goto(`/accounts/show/${id}`, { replace: true });
}
});
});
</script>
{#key id}
<AccountShow {id} />
{/key}
@@ -0,0 +1,10 @@
<script lang="ts">
import AccountShow from "#lib/components/accounts/AccountShow.svelte";
import { page } from "$app/state";
const id = $derived(page.params.id ?? "");
</script>
{#key id}
<AccountShow everything {id} />
{/key}
@@ -0,0 +1,11 @@
<script lang="ts">
import type { AccountType } from "#lib/api/types.js";
import AccountsIndex from "#lib/components/accounts/AccountsIndex.svelte";
import { page } from "$app/state";
const type = $derived(page.params.type as AccountType);
</script>
{#key type}
<AccountsIndex active={false} {type} />
{/key}
+16 -16
View File
@@ -16,14 +16,14 @@
--ff-row-hover: oklch(0.96 0.012 240);
--ff-row-cursor: oklch(0.93 0.03 240);
--ff-row-selected: oklch(0.9 0.05 240);
--ff-series-1: oklch(0.57 0.105 240);
--ff-series-2: oklch(0.62 0.13 160);
--ff-series-3: oklch(0.7 0.14 70);
--ff-series-4: oklch(0.58 0.15 20);
--ff-series-5: oklch(0.55 0.12 300);
--ff-series-6: oklch(0.65 0.1 200);
--ff-series-7: oklch(0.6 0.13 110);
--ff-series-8: oklch(0.6 0.08 260);
--ff-series-1: #1777b8;
--ff-series-2: #cb6620;
--ff-series-3: #008f7d;
--ff-series-4: #b08000;
--ff-series-5: #bc4b87;
--ff-series-6: #3f842e;
--ff-series-7: #6b4cae;
--ff-series-8: #b63132;
}
.cladd-color-brand {
@@ -41,14 +41,14 @@
--ff-row-hover: oklch(0.25 0.012 240);
--ff-row-cursor: oklch(0.29 0.03 240);
--ff-row-selected: oklch(0.33 0.06 240);
--ff-series-1: oklch(0.7 0.1 240);
--ff-series-2: oklch(0.72 0.12 160);
--ff-series-3: oklch(0.8 0.13 75);
--ff-series-4: oklch(0.7 0.13 25);
--ff-series-5: oklch(0.7 0.11 300);
--ff-series-6: oklch(0.75 0.09 200);
--ff-series-7: oklch(0.75 0.12 115);
--ff-series-8: oklch(0.72 0.07 260);
--ff-series-1: #348dcf;
--ff-series-2: #d5753a;
--ff-series-3: #0f9b89;
--ff-series-4: #b68b16;
--ff-series-5: #cd6199;
--ff-series-6: #4e943e;
--ff-series-7: #876cca;
--ff-series-8: #c44844;
}
@theme inline {