feat: web UI chat render, panels, presence + analytics
This commit is contained in:
1 parent
75425d1bee
commit
ed469ba8dd
83 files changed
+6034
-136
No files matched your search
@@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import type { PresenceHourly } from "$lib/api/types";
|
||||
|
||||
interface Props {
|
||||
hourly: PresenceHourly[];
|
||||
}
|
||||
|
||||
const { hourly }: Props = $props();
|
||||
|
||||
const DAYS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||||
const HOURS = Array.from({ length: 24 }, (_, hour) => hour);
|
||||
const HOUR_TICKS = new Set([0, 6, 12, 18]);
|
||||
|
||||
interface Cell {
|
||||
online: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const grid = $derived.by(() => {
|
||||
const cells: Cell[][] = DAYS.map(() =>
|
||||
HOURS.map(() => ({ online: 0, total: 0 }))
|
||||
);
|
||||
for (const bucket of hourly) {
|
||||
const date = new Date(bucket.bucket);
|
||||
const day = (date.getDay() + 6) % 7;
|
||||
const cell = cells[day]?.[date.getHours()];
|
||||
if (cell) {
|
||||
cell.online += bucket.online_samples;
|
||||
cell.total += bucket.samples;
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
});
|
||||
|
||||
function intensity(cell: Cell): number {
|
||||
return cell.total > 0 ? cell.online / cell.total : 0;
|
||||
}
|
||||
|
||||
function cellTitle(day: number, hour: number, cell: Cell): string {
|
||||
const pct = cell.total > 0 ? Math.round(intensity(cell) * 100) : 0;
|
||||
return `${DAYS[day]} ${hour}:00 — ${pct}% online`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="heatmap">
|
||||
{#each grid as row, day (day)}
|
||||
<div class="heat-row">
|
||||
<span class="day-label">{DAYS[day]}</span>
|
||||
<div class="cells">
|
||||
{#each row as cell, hour (hour)}
|
||||
<span
|
||||
class="cell"
|
||||
class:empty={cell.total === 0}
|
||||
style:--i={intensity(cell)}
|
||||
title={cellTitle(day, hour, cell)}
|
||||
></span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="heat-row axis">
|
||||
<span class="day-label"></span>
|
||||
<div class="cells">
|
||||
{#each HOURS as hour (hour)}
|
||||
<span class="tick">{HOUR_TICKS.has(hour) ? hour : ""}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.heatmap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.heat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
width: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cells {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
grid-template-columns: repeat(24, 1fr);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 2px;
|
||||
background-color: var(--color-primary);
|
||||
opacity: calc(0.12 + 0.88 * var(--i));
|
||||
|
||||
&.empty {
|
||||
background-color: var(--color-text-secondary);
|
||||
opacity: 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
.tick {
|
||||
font-size: 0.625rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user