hackatime/app/javascript/pages/Home/signedIn/Dashboard.svelte
Mahad Kalam ef3f36c829
Inertia migration/UI3 (#911)
* Inertia p1?

* Inertia'fied signed out homepage?

* Split up signed in page

* WIP signed in v2?

* Better signed in?

* Clean up extensions page!

* Fix currently hacking

* Better docs page?

* Docs update 2

* Clean up "What is Hackatime?" + get rid of that godawful green dev mode

* Better nav?

* Cleaner settings?

* Fix commit times

* Fix flashes + OS improv

* Setup v2

* Readd some of the syncers?

* Remove stray emdash

* Clean up Step 3

* Oops, remove .vite

* bye bye, /inertia-example

* bin/rubocop -A

* Fix docs vuln
2026-02-09 11:26:30 +00:00

183 lines
5.1 KiB
Svelte

<script lang="ts">
import { router } from "@inertiajs/svelte";
import { secondsToDisplay } from "./utils";
import StatCard from "./StatCard.svelte";
import HorizontalBarList from "./HorizontalBarList.svelte";
import PieChart from "./PieChart.svelte";
import ProjectTimelineChart from "./ProjectTimelineChart.svelte";
import IntervalSelect from "./IntervalSelect.svelte";
import MultiSelect from "./MultiSelect.svelte";
let {
data,
}: {
data: Record<string, any>;
} = $props();
let loading = $state(false);
const langStats = $derived(
(data.language_stats || {}) as Record<string, number>,
);
const editorStats = $derived(
(data.editor_stats || {}) as Record<string, number>,
);
const osStats = $derived(
(data.operating_system_stats || {}) as Record<string, number>,
);
const projectEntries = $derived(
Object.entries(data.project_durations || {}) as [string, number][],
);
const weeklyStats = $derived(
(data.weekly_project_stats || {}) as Record<string, Record<string, number>>,
);
const capitalize = (s: string) =>
s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
function applyFilters(overrides: Record<string, string>) {
const current = new URL(window.location.href);
for (const [k, v] of Object.entries(overrides)) {
if (v) {
current.searchParams.set(k, v);
} else {
current.searchParams.delete(k);
}
}
loading = true;
router.get(
current.pathname + current.search,
{},
{
preserveState: true,
preserveScroll: true,
only: ["filterable_dashboard_data"],
onFinish: () => (loading = false),
},
);
}
function onIntervalChange(interval: string, from: string, to: string) {
if (from || to) {
applyFilters({ interval: "custom", from, to });
} else {
applyFilters({ interval, from: "", to: "" });
}
}
function onFilterChange(param: string, selected: string[]) {
applyFilters({ [param]: selected.join(",") });
}
</script>
<div class="flex flex-col gap-6 w-full" class:opacity-60={loading}>
<!-- Filters -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-2">
<IntervalSelect
selected={data.selected_interval || ""}
from={data.selected_from || ""}
to={data.selected_to || ""}
onchange={onIntervalChange}
/>
<MultiSelect
label="Project"
param="project"
values={data.project || []}
selected={data.selected_project || []}
onchange={(s) => onFilterChange("project", s)}
/>
<MultiSelect
label="Language"
param="language"
values={data.language || []}
selected={data.selected_language || []}
onchange={(s) => onFilterChange("language", s)}
/>
<MultiSelect
label="OS"
param="operating_system"
values={data.operating_system || []}
selected={data.selected_operating_system || []}
onchange={(s) => onFilterChange("operating_system", s)}
/>
<MultiSelect
label="Editor"
param="editor"
values={data.editor || []}
selected={data.selected_editor || []}
onchange={(s) => onFilterChange("editor", s)}
/>
<MultiSelect
label="Category"
param="category"
values={data.category || []}
selected={data.selected_category || []}
onchange={(s) => onFilterChange("category", s)}
/>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<StatCard
label="Total Time"
value={secondsToDisplay(data.total_time)}
highlight
/>
<StatCard
label="Top Project"
value={data.top_project || "None"}
subtitle={data.singular_project ? "obviously" : ""}
/>
<StatCard
label="Top Language"
value={data.top_language || "Unknown"}
subtitle={data.singular_language ? "obviously" : ""}
/>
<StatCard
label="Top OS"
value={data.top_operating_system || "Unknown"}
subtitle={data.singular_operating_system ? "obviously" : ""}
/>
<StatCard
label="Top Editor"
value={data.top_editor || "Unknown"}
subtitle={data.singular_editor ? "obviously" : ""}
/>
<StatCard
label="Top Category"
value={capitalize(data.top_category) || "Unknown"}
subtitle={data.singular_category ? "obviously" : ""}
/>
</div>
<!-- Charts Layout -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
{#if projectEntries.length > 1}
<div class="lg:col-span-1">
<HorizontalBarList
title="Project Durations"
entries={projectEntries}
empty_message="No data yet."
useLogScale
/>
</div>
{/if}
{#if Object.keys(langStats).length > 0}
<PieChart title="Languages" stats={langStats} />
{/if}
{#if Object.keys(editorStats).length > 0}
<PieChart title="Editors" stats={editorStats} />
{/if}
{#if Object.keys(osStats).length > 0}
<PieChart title="Operating Systems" stats={osStats} />
{/if}
<div class="lg:col-span-2">
<ProjectTimelineChart {weeklyStats} />
</div>
</div>
</div>