chore: session auto-commit
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { HeatmapCell } from "../../../shared/api/client";
|
import type { HeatmapCell } from "../../../shared/api/client";
|
||||||
import { cn } from "../../../shared/lib/utils";
|
import { cn } from "../../../shared/lib/utils";
|
||||||
import {
|
import {
|
||||||
@@ -17,18 +17,22 @@ interface HeatmapProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Heatmap({ cells, loading }: HeatmapProps) {
|
export function Heatmap({ cells, loading }: HeatmapProps) {
|
||||||
|
const [tooltip, setTooltip] = useState<{
|
||||||
|
day: string;
|
||||||
|
hour: string;
|
||||||
|
total: number;
|
||||||
|
clean: number;
|
||||||
|
warned: number;
|
||||||
|
flagged: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const maxCount = useMemo(
|
const maxCount = useMemo(
|
||||||
() => Math.max(1, ...cells.map((c) => c.count)),
|
() => Math.max(1, ...cells.map((c) => c.count)),
|
||||||
[cells],
|
[cells],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading && !cells?.length) {
|
if (loading && !cells?.length) return <LoadingBox />;
|
||||||
return <LoadingBox />;
|
if (!cells?.length) return <EmptyBox />;
|
||||||
}
|
|
||||||
|
|
||||||
if (!cells?.length) {
|
|
||||||
return <EmptyBox />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellMap = new Map<string, HeatmapCell>();
|
const cellMap = new Map<string, HeatmapCell>();
|
||||||
for (const c of cells) cellMap.set(`${c.dayOfWeek}-${c.hour}`, c);
|
for (const c of cells) cellMap.set(`${c.dayOfWeek}-${c.hour}`, c);
|
||||||
@@ -38,13 +42,13 @@ export function Heatmap({ cells, loading }: HeatmapProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getHeatClass(intensity: number): string {
|
function getHeatClass(intensity: number): string {
|
||||||
if (intensity === 0) return "bg-sky-50";
|
if (intensity === 0) return "bg-muted/20";
|
||||||
if (intensity < 0.1) return "bg-sky-200/30";
|
if (intensity < 0.1) return "bg-primary/15";
|
||||||
if (intensity < 0.2) return "bg-sky-300/30";
|
if (intensity < 0.2) return "bg-primary/25";
|
||||||
if (intensity < 0.35) return "bg-sky-400/35";
|
if (intensity < 0.35) return "bg-primary/40";
|
||||||
if (intensity < 0.5) return "bg-sky-400/55";
|
if (intensity < 0.5) return "bg-primary/55";
|
||||||
if (intensity < 0.7) return "bg-primary/60";
|
if (intensity < 0.7) return "bg-primary/70";
|
||||||
return "bg-primary/80";
|
return "bg-primary/85";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,14 +58,14 @@ export function Heatmap({ cells, loading }: HeatmapProps) {
|
|||||||
Heatmap Aktivitas
|
Heatmap Aktivitas
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="text-xs">
|
||||||
Hari × jam — area biru langit = lebih ramai.
|
Hari × jam — arahkan kursor ke sel untuk detail.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="relative">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="min-w-[520px]">
|
<div className="min-w-[560px]">
|
||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<div className="mb-1 ml-8 flex gap-[3px]">
|
<div className="mb-1 flex gap-[3px] pl-8">
|
||||||
{Array.from({ length: 24 }, (_, h) => (
|
{Array.from({ length: 24 }, (_, h) => (
|
||||||
<div
|
<div
|
||||||
key={h}
|
key={h}
|
||||||
@@ -72,22 +76,38 @@ export function Heatmap({ cells, loading }: HeatmapProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
{DAYS.map((day, d) => (
|
{DAYS.map((dayLabel, d) => (
|
||||||
<div key={d} className="mb-[3px] flex items-center gap-[3px]">
|
<div key={d} className="mb-[3px] flex items-center gap-[3px]">
|
||||||
<div className="w-8 shrink-0 text-right pr-1 text-[10px] text-muted-foreground">
|
<div className="w-8 shrink-0 text-right pr-1 text-[10px] text-muted-foreground">
|
||||||
{day}
|
{dayLabel}
|
||||||
</div>
|
</div>
|
||||||
{Array.from({ length: 24 }, (_, h) => {
|
{Array.from({ length: 24 }, (_, h) => {
|
||||||
const intensity = getIntensity(d, h);
|
const intensity = getIntensity(d, h);
|
||||||
const cell = cellMap.get(`${d}-${h}`);
|
const cell = cellMap.get(`${d}-${h}`);
|
||||||
|
const count = cell?.count ?? 0;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={h}
|
key={h}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 rounded-md aspect-square border border-sky-100/50",
|
"flex-1 rounded-md aspect-square border border-muted/30 transition-all duration-150",
|
||||||
getHeatClass(intensity),
|
getHeatClass(intensity),
|
||||||
|
count > 0
|
||||||
|
? "cursor-pointer hover:ring-2 hover:ring-primary/50 hover:scale-110"
|
||||||
|
: "",
|
||||||
)}
|
)}
|
||||||
title={`${day} ${h}:00 — ${cell?.count ?? 0} pesan`}
|
onMouseEnter={() => {
|
||||||
|
if (count > 0) {
|
||||||
|
setTooltip({
|
||||||
|
day: dayLabel,
|
||||||
|
hour: `${h}:00`,
|
||||||
|
total: count,
|
||||||
|
clean: cell?.clean ?? 0,
|
||||||
|
warned: cell?.warned ?? 0,
|
||||||
|
flagged: cell?.flagged ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setTooltip(null)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -95,13 +115,51 @@ export function Heatmap({ cells, loading }: HeatmapProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
{tooltip && (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute z-10 rounded-lg border border-muted bg-white px-3 py-2 text-xs shadow-lg"
|
||||||
|
style={{
|
||||||
|
left: "50%",
|
||||||
|
top: "100%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mb-1 font-semibold text-foreground">
|
||||||
|
{tooltip.day} {tooltip.hour}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5 text-muted-foreground">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span>Total</span>
|
||||||
|
<span className="font-medium tabular-nums text-foreground">
|
||||||
|
{tooltip.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-primary">Clean</span>
|
||||||
|
<span className="tabular-nums">{tooltip.clean}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-yellow-600">Warned</span>
|
||||||
|
<span className="tabular-nums">{tooltip.warned}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-accent">Flagged</span>
|
||||||
|
<span className="tabular-nums">{tooltip.flagged}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="mt-3 flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
<div className="mt-3 flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||||
<span>Sepi</span>
|
<span>Sepi</span>
|
||||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-sky-50" />
|
<span className="inline-block h-3 w-3 rounded-sm bg-muted/20" />
|
||||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-sky-300/30" />
|
<span className="inline-block h-3 w-3 rounded-sm bg-primary/15" />
|
||||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-sky-400/55" />
|
<span className="inline-block h-3 w-3 rounded-sm bg-primary/40" />
|
||||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-primary/80" />
|
<span className="inline-block h-3 w-3 rounded-sm bg-primary/70" />
|
||||||
|
<span className="inline-block h-3 w-3 rounded-sm bg-primary/85" />
|
||||||
<span>Ramai</span>
|
<span>Ramai</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -113,7 +171,7 @@ function LoadingBox() {
|
|||||||
return (
|
return (
|
||||||
<Card className="col-span-2">
|
<Card className="col-span-2">
|
||||||
<CardContent className="flex h-[260px] items-center justify-center text-sm text-muted-foreground">
|
<CardContent className="flex h-[260px] items-center justify-center text-sm text-muted-foreground">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<span className="h-4 w-4 animate-spin rounded-sm border-2 border-current border-t-transparent" />
|
||||||
<span className="ml-2">Memuat data...</span>
|
<span className="ml-2">Memuat data...</span>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user