03568c88e3
- frontend/functions/core/format_datetime_short.md - frontend/functions/core/format_datetime_short.test.ts - frontend/functions/core/format_datetime_short.ts - frontend/functions/core/format_duration.md - frontend/functions/core/format_duration.test.ts - frontend/functions/core/format_duration.ts - frontend/functions/core/month_grid.md - frontend/functions/core/month_grid.test.ts - frontend/functions/core/month_grid.ts - frontend/functions/core/string_hash_palette.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
import { type ReactNode } from "react";
|
|
import { Box, Group, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
|
|
import { IconCheckbox, IconPlus } from "@tabler/icons-react";
|
|
import { monthGrid } from "../core/month_grid";
|
|
|
|
export interface MonthHeatmapCell {
|
|
primary?: number;
|
|
secondary?: number;
|
|
}
|
|
|
|
export interface MonthHeatmapProps {
|
|
/** Mes a mostrar (se usan year y month, el dia se ignora). */
|
|
month: Date;
|
|
/** Datos por dia, key "YYYY-MM-DD". */
|
|
cells: Map<string, MonthHeatmapCell>;
|
|
/** Icono junto al contador primary. Default: IconPlus. */
|
|
primaryIcon?: ReactNode;
|
|
/** Icono junto al contador secondary. Default: IconCheckbox. */
|
|
secondaryIcon?: ReactNode;
|
|
/** Color Mantine del counter primary. Default: "blue". */
|
|
primaryColor?: string;
|
|
/** Color Mantine del counter secondary. Default: "green". */
|
|
secondaryColor?: string;
|
|
/** Labels de cabecera, 7 elementos empezando por lunes. */
|
|
dayLabels?: string[];
|
|
/** Altura minima de cada celda en px. Default: 72. */
|
|
cellMinHeight?: number;
|
|
}
|
|
|
|
const DEFAULT_DAY_LABELS = ["Lun", "Mar", "Mie", "Jue", "Vie", "Sab", "Dom"];
|
|
|
|
/**
|
|
* Grid mensual de calor (heatmap) con inicio en lunes.
|
|
*
|
|
* Renderiza 7 columnas con header de dias de la semana.
|
|
* Cada celda muestra el numero del dia y hasta dos contadores opcionales
|
|
* (primary / secondary) con icono. El dia de hoy recibe borde azul y
|
|
* numero en negrita. El fondo de la celda se tinta segun los valores:
|
|
* secondary > 0 → verdoso, primary > 0 → azulado.
|
|
* Las celdas de relleno (fuera del mes) se renderizan vacias.
|
|
*/
|
|
export function MonthHeatmap({
|
|
month,
|
|
cells,
|
|
primaryIcon,
|
|
secondaryIcon,
|
|
primaryColor = "blue",
|
|
secondaryColor = "green",
|
|
dayLabels = DEFAULT_DAY_LABELS,
|
|
cellMinHeight = 72,
|
|
}: MonthHeatmapProps) {
|
|
const year = month.getFullYear();
|
|
const monthNum = month.getMonth() + 1; // getMonth() es 0-based
|
|
const grid = monthGrid(year, monthNum);
|
|
|
|
// Hoy como "YYYY-MM-DD" usando Date nativo
|
|
const now = new Date();
|
|
const todayStr = [
|
|
now.getFullYear(),
|
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
String(now.getDate()).padStart(2, "0"),
|
|
].join("-");
|
|
|
|
const resolvedPrimaryIcon = primaryIcon ?? <IconPlus size={10} />;
|
|
const resolvedSecondaryIcon = secondaryIcon ?? <IconCheckbox size={10} />;
|
|
|
|
return (
|
|
<Box>
|
|
{/* Header dias de la semana */}
|
|
<SimpleGrid cols={7} spacing={4} mb={4}>
|
|
{dayLabels.map((label) => (
|
|
<Text key={label} size="xs" c="dimmed" ta="center" fw={600}>
|
|
{label}
|
|
</Text>
|
|
))}
|
|
</SimpleGrid>
|
|
|
|
{/* Celdas del mes */}
|
|
<SimpleGrid cols={7} spacing={4}>
|
|
{grid.map((cell, i) => {
|
|
if (!cell.date) {
|
|
return <Box key={i} style={{ minHeight: cellMinHeight }} />;
|
|
}
|
|
|
|
const data = cells.get(cell.date) ?? {};
|
|
const primary = data.primary ?? 0;
|
|
const secondary = data.secondary ?? 0;
|
|
const dayNum = parseInt(cell.date.slice(8, 10), 10);
|
|
const isToday = cell.date === todayStr;
|
|
|
|
const background =
|
|
secondary > 0
|
|
? "rgba(81, 207, 102, 0.08)"
|
|
: primary > 0
|
|
? "rgba(34, 139, 230, 0.06)"
|
|
: undefined;
|
|
|
|
return (
|
|
<Paper
|
|
key={i}
|
|
p={6}
|
|
withBorder
|
|
radius="sm"
|
|
style={{
|
|
minHeight: cellMinHeight,
|
|
borderColor: isToday ? "var(--mantine-color-blue-5)" : undefined,
|
|
background,
|
|
}}
|
|
>
|
|
<Stack gap={2}>
|
|
<Text
|
|
size="xs"
|
|
fw={isToday ? 700 : 500}
|
|
c={isToday ? "blue" : undefined}
|
|
>
|
|
{dayNum}
|
|
</Text>
|
|
|
|
{primary > 0 && (
|
|
<Group gap={3} wrap="nowrap" c={primaryColor}>
|
|
{resolvedPrimaryIcon}
|
|
<Text size="xs" c={primaryColor}>
|
|
{primary}
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
|
|
{secondary > 0 && (
|
|
<Group gap={3} wrap="nowrap" c={secondaryColor}>
|
|
{resolvedSecondaryIcon}
|
|
<Text size="xs" c={secondaryColor}>
|
|
{secondary}
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
})}
|
|
</SimpleGrid>
|
|
</Box>
|
|
);
|
|
}
|