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 { cn } from "../../../shared/lib/utils";
|
||||
import {
|
||||
@@ -17,18 +17,22 @@ interface 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(
|
||||
() => Math.max(1, ...cells.map((c) => c.count)),
|
||||
[cells],
|
||||
);
|
||||
|
||||
if (loading && !cells?.length) {
|
||||
return <LoadingBox />;
|
||||
}
|
||||
|
||||
if (!cells?.length) {
|
||||
return <EmptyBox />;
|
||||
}
|
||||
if (loading && !cells?.length) return <LoadingBox />;
|
||||
if (!cells?.length) return <EmptyBox />;
|
||||
|
||||
const cellMap = new Map<string, HeatmapCell>();
|
||||
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 {
|
||||
if (intensity === 0) return "bg-sky-50";
|
||||
if (intensity < 0.1) return "bg-sky-200/30";
|
||||
if (intensity < 0.2) return "bg-sky-300/30";
|
||||
if (intensity < 0.35) return "bg-sky-400/35";
|
||||
if (intensity < 0.5) return "bg-sky-400/55";
|
||||
if (intensity < 0.7) return "bg-primary/60";
|
||||
return "bg-primary/80";
|
||||
if (intensity === 0) return "bg-muted/20";
|
||||
if (intensity < 0.1) return "bg-primary/15";
|
||||
if (intensity < 0.2) return "bg-primary/25";
|
||||
if (intensity < 0.35) return "bg-primary/40";
|
||||
if (intensity < 0.5) return "bg-primary/55";
|
||||
if (intensity < 0.7) return "bg-primary/70";
|
||||
return "bg-primary/85";
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -54,14 +58,14 @@ export function Heatmap({ cells, loading }: HeatmapProps) {
|
||||
Heatmap Aktivitas
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Hari × jam — area biru langit = lebih ramai.
|
||||
Hari × jam — arahkan kursor ke sel untuk detail.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="relative">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-[520px]">
|
||||
<div className="min-w-[560px]">
|
||||
{/* 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) => (
|
||||
<div
|
||||
key={h}
|
||||
@@ -72,22 +76,38 @@ export function Heatmap({ cells, loading }: HeatmapProps) {
|
||||
))}
|
||||
</div>
|
||||
{/* Rows */}
|
||||
{DAYS.map((day, d) => (
|
||||
{DAYS.map((dayLabel, d) => (
|
||||
<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">
|
||||
{day}
|
||||
{dayLabel}
|
||||
</div>
|
||||
{Array.from({ length: 24 }, (_, h) => {
|
||||
const intensity = getIntensity(d, h);
|
||||
const cell = cellMap.get(`${d}-${h}`);
|
||||
const count = cell?.count ?? 0;
|
||||
return (
|
||||
<div
|
||||
key={h}
|
||||
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),
|
||||
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>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="mt-3 flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||
<span>Sepi</span>
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-sky-50" />
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-sky-300/30" />
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-sm bg-sky-400/55" />
|
||||
<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-muted/20" />
|
||||
<span className="inline-block h-3 w-3 rounded-sm bg-primary/15" />
|
||||
<span className="inline-block h-3 w-3 rounded-sm bg-primary/40" />
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -113,7 +171,7 @@ function LoadingBox() {
|
||||
return (
|
||||
<Card className="col-span-2">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user