From 518039023cfa8964bfbd15113688832f6e057627 Mon Sep 17 00:00:00 2001 From: leafyzito Date: Fri, 26 Dec 2025 13:40:07 -0100 Subject: [PATCH 1/3] feat(logs): add channel stats popover --- src/routes/logs/+page.svelte | 99 +++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 7205991..2b8ebe5 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -37,7 +37,7 @@ import { page } from "$app/state"; import { goto } from "$app/navigation"; - import { LoaderCircleIcon, FileTextIcon, ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon, CalendarIcon, ExternalLinkIcon, FilterIcon, SearchIcon } from "@lucide/svelte"; + import { LoaderCircleIcon, FileTextIcon, ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon, CalendarIcon, ExternalLinkIcon, FilterIcon, SearchIcon, InfoIcon } from "@lucide/svelte"; import { dateTimeFormat, type TitleContext } from "$lib/common"; @@ -196,6 +196,12 @@ let channelId = $state(""); + // Channel Stats + let statsPopoverOpen = $state(false); + let channelStats = $state<{ messageCount: number; topChatters: Array<{ userId: string; userLogin: string; messageCount: number }> } | null>(null); + let statsLoading = $state(false); + let statsError = $state(null); + // Emotes const channelEmotes = new SvelteMap(); const globalEmotes = new SvelteMap(); @@ -386,6 +392,46 @@ else return `${channelType}/${encodeURIComponent(channel)}${user ? `/${userType}/${encodeURIComponent(user)}` : ""}`; }; + const buildStatsUrl = (channelName: string) => { + const channel = channelName.trim(); + if (channel.startsWith("id:")) { + const channelId = channel.slice(3).trim(); + return `https://logs.zonian.dev/channelid/${encodeURIComponent(channelId)}/stats`; + } + return `https://logs.zonian.dev/channel/${channel.toLowerCase()}/stats`; + }; + + const fetchChannelStats = async () => { + if (!channelName) return; + + statsLoading = true; + statsError = null; + + try { + const url = buildStatsUrl(channelName); + const res = await fetch(url); + if (!res.ok) { + if (res.status === 404) statsError = "No stats found for this channel"; + else statsError = `Error from server: ${res.status} ${res.statusText}`; + statsLoading = false; + return; + } + + const data: { messageCount: number; topChatters: Array<{ userId: string; userLogin: string; messageCount: number }> } = await res.json(); + channelStats = data; + statsLoading = false; + } catch (err) { + statsError = err instanceof Error ? err.message : "Failed to fetch channel stats"; + statsLoading = false; + } + }; + + $effect(() => { + if (statsPopoverOpen && channelName) { + fetchChannelStats(); + } + }); + $effect(() => { // fetch available dates if (!channelName) return; @@ -821,6 +867,57 @@
+ + + + + + + + Channel Stats + Statistics for channel: {channelName} + + + {#if statsLoading} +
+ +
+ {:else if statsError} +
+

{statsError}

+
+ {:else if channelStats} +
+
+

Total Messages

+

{channelStats.messageCount.toLocaleString()}

+
+ +
+

Top Chatters

+
+ {#each channelStats.topChatters as chatter, index} +
+
+ {index + 1}. + {chatter.userLogin} +
+ {chatter.messageCount.toLocaleString()} +
+ {/each} +
+
+
+ {/if} +
+
+
+
{#if loading} {/if} From 1c6dc896fdd5ec5f607c5161e383c79d0a541549 Mon Sep 17 00:00:00 2001 From: leafyzito Date: Fri, 26 Dec 2025 13:50:41 -0100 Subject: [PATCH 2/3] fix(lint): make lint happy --- src/routes/logs/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 2b8ebe5..5d20159 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -901,7 +901,7 @@

Top Chatters

- {#each channelStats.topChatters as chatter, index} + {#each channelStats.topChatters as chatter, index (chatter.userId)}
{index + 1}. From dc42c367b545af0968cfd1f1ac32193be41f2b32 Mon Sep 17 00:00:00 2001 From: Supa Date: Fri, 26 Dec 2025 20:45:21 +0200 Subject: [PATCH 3/3] ux --- src/routes/logs/+page.svelte | 211 ++++++++++++++++++----------------- 1 file changed, 106 insertions(+), 105 deletions(-) diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 5d20159..0990b66 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -11,6 +11,7 @@ import { ScrollArea } from "$lib/components/ui/scroll-area/index.js"; import { Input } from "$lib/components/ui/input/index.js"; import { Label } from "$lib/components/ui/label/index.js"; + import { Skeleton } from "$lib/components/ui/skeleton/index.js"; import { cn } from "$lib/utils.js"; import { CalendarDate, DateFormatter, getLocalTimeZone, today, type DateValue } from "@internationalized/date"; @@ -37,7 +38,7 @@ import { page } from "$app/state"; import { goto } from "$app/navigation"; - import { LoaderCircleIcon, FileTextIcon, ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon, CalendarIcon, ExternalLinkIcon, FilterIcon, SearchIcon, InfoIcon } from "@lucide/svelte"; + import { LoaderCircleIcon, FileTextIcon, ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon, CalendarIcon, ExternalLinkIcon, FilterIcon, SearchIcon, ChartColumnIcon } from "@lucide/svelte"; import { dateTimeFormat, type TitleContext } from "$lib/common"; @@ -52,6 +53,11 @@ day?: string; }; + type StatsResponse = { + messageCount: number; + topChatters?: Array<{ userId: string; userLogin: string; messageCount: number }>; + }; + getContext("title").set("Logs"); const lineHeight = 20; @@ -59,7 +65,7 @@ let error: string | null = $state(null); let loading = $state(false); - let isPopoverOpen = $state(false); + let datePopoverOpen = $state(false); let selectedIndex = $state(0); // Track selected item @@ -198,8 +204,7 @@ // Channel Stats let statsPopoverOpen = $state(false); - let channelStats = $state<{ messageCount: number; topChatters: Array<{ userId: string; userLogin: string; messageCount: number }> } | null>(null); - let statsLoading = $state(false); + let channelStats = $state(null); let statsError = $state(null); // Emotes @@ -392,42 +397,27 @@ else return `${channelType}/${encodeURIComponent(channel)}${user ? `/${userType}/${encodeURIComponent(user)}` : ""}`; }; - const buildStatsUrl = (channelName: string) => { - const channel = channelName.trim(); - if (channel.startsWith("id:")) { - const channelId = channel.slice(3).trim(); - return `https://logs.zonian.dev/channelid/${encodeURIComponent(channelId)}/stats`; - } - return `https://logs.zonian.dev/channel/${channel.toLowerCase()}/stats`; - }; - const fetchChannelStats = async () => { if (!channelName) return; - statsLoading = true; statsError = null; try { - const url = buildStatsUrl(channelName); - const res = await fetch(url); + const res = await fetch(`https://logs.zonian.dev/${parseChannelUser(channelName, userName, false)}/stats`); if (!res.ok) { if (res.status === 404) statsError = "No stats found for this channel"; else statsError = `Error from server: ${res.status} ${res.statusText}`; - statsLoading = false; return; } - const data: { messageCount: number; topChatters: Array<{ userId: string; userLogin: string; messageCount: number }> } = await res.json(); - channelStats = data; - statsLoading = false; + channelStats = await res.json(); } catch (err) { statsError = err instanceof Error ? err.message : "Failed to fetch channel stats"; - statsLoading = false; } }; $effect(() => { - if (statsPopoverOpen && channelName) { + if (statsPopoverOpen && !channelStats) { fetchChannelStats(); } }); @@ -438,6 +428,7 @@ untrack(async () => { availableDates = []; chatLogs = []; + channelStats = null; loading = true; const res = await fetch(`https://logs.zonian.dev/list?${parseChannelUser(channelName, userName, true)}`); @@ -830,106 +821,116 @@ {/if}
-
-
-
-
- - - - {#if foundChannels.length && foundChannels[0].target !== inputChannelName.toLowerCase()} -
- - - - {#each foundChannels as c, index (c.target)} +
+ +
+
+
+ + + + {#if foundChannels.length && foundChannels[0].target !== inputChannelName.toLowerCase()} +
+ -
(selectedIndex = index)} - onclick={() => selectResult(index)} - > - {c.target} -
- {/each} -
-
- {/if} -
+ onmouseenter={() => (selectedIndex = index)} + onclick={() => selectResult(index)} + > + {c.target} +
+ {/each} + +
+ {/if} +
+ +
+ + +
-
- - +
+ + {#if loading} + + {/if} +
+
+ -
- - - - - - - - - Channel Stats - Statistics for channel: {channelName} - - - {#if statsLoading} -
- -
- {:else if statsError} -
-

{statsError}

-
- {:else if channelStats} -
-
-

Total Messages

-

{channelStats.messageCount.toLocaleString()}

-
+
+ + + + + + + + + {#if statsError} +

{statsError}

+ {:else} +
+
+

+ Total Messages + {#if userName} + by {userName} + {/if} +

+ {#if channelStats} +

{channelStats.messageCount.toLocaleString()}

+ {:else} + + {/if} +
-
-

Top Chatters

-
+ {#if !userName} +
+

Top Chatters

+
+ {#if channelStats?.topChatters} {#each channelStats.topChatters as chatter, index (chatter.userId)} -
+
- {index + 1}. + {index + 1}. {chatter.userLogin}
- {chatter.messageCount.toLocaleString()} + {chatter.messageCount.toLocaleString()}
{/each} -
+ {:else} + + + + + + {/if}
{/if} - - - - - {#if loading} - - {/if} -
-
- +
+ {/if} +
+
+
+
+
{#if dateContent} {#if dateContent.day} - + - + {:else}
- + {dateContent.year}-{String(dateContent.month).padStart(2, "0")}{dateContent.day ? `-${String(dateContent.day).padStart(2, "0")}` : ""} @@ -1162,7 +1163,7 @@ {/if}
-{#if isPopoverOpen} +{#if datePopoverOpen || statsPopoverOpen} {/if}