mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-06 13:19:31 -05:00
Strict improvement because we avoid the flash on clientside navigation. One practical bug was scroll restoration between tournament teams list and user page. When user pressed back they ended up at the bottom of the page because the placeholder (smaller height than actual content) rendered. With useHydrated this placeholder is no longer rendered for client side navigations.
159 lines
3.7 KiB
TypeScript
159 lines
3.7 KiB
TypeScript
import clsx from "clsx";
|
|
import * as React from "react";
|
|
import { type AxisOptions, Chart as ReactChart } from "react-charts";
|
|
import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Theme, useTheme } from "~/features/theme/core/provider";
|
|
import { useHydrated } from "~/hooks/useHydrated";
|
|
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
|
import styles from "./Chart.module.css";
|
|
|
|
export default function Chart({
|
|
options,
|
|
containerClassName,
|
|
headerSuffix,
|
|
valueSuffix,
|
|
xAxis,
|
|
}: {
|
|
options: [
|
|
{ label: string; data: Array<{ primary: Date; secondary: number }> },
|
|
];
|
|
containerClassName?: string;
|
|
headerSuffix?: string;
|
|
valueSuffix?: string;
|
|
xAxis: "linear" | "localTime";
|
|
}) {
|
|
const { i18n } = useTranslation();
|
|
const theme = useTheme();
|
|
const isHydrated = useHydrated();
|
|
|
|
const primaryAxis = React.useMemo<
|
|
AxisOptions<(typeof options)[number]["data"][number]>
|
|
>(
|
|
// @ts-expect-error - some weirdness here but maybe not worth fixing as the whole library needs to be replaced (it is unmaintained/deprecated)
|
|
() => ({
|
|
getValue: (datum) => datum.primary,
|
|
scaleType: xAxis,
|
|
shouldNice: false,
|
|
formatters: {
|
|
scale: (val: any) => {
|
|
if (val instanceof Date) {
|
|
return val.toLocaleDateString(i18n.language, {
|
|
day: "numeric",
|
|
month: "numeric",
|
|
});
|
|
}
|
|
|
|
return val;
|
|
},
|
|
},
|
|
}),
|
|
[i18n.language, xAxis],
|
|
);
|
|
|
|
const secondaryAxes = React.useMemo<
|
|
AxisOptions<(typeof options)[number]["data"][number]>[]
|
|
>(
|
|
() => [
|
|
{
|
|
getValue: (datum) => datum.secondary,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
if (!isHydrated) {
|
|
return <div className={clsx(styles.container, containerClassName)} />;
|
|
}
|
|
|
|
return (
|
|
<div className={clsx(styles.container, containerClassName)}>
|
|
<ReactChart
|
|
options={{
|
|
data: options,
|
|
tooltip: {
|
|
render: (props) => (
|
|
<ChartTooltip
|
|
{...props}
|
|
headerSuffix={headerSuffix}
|
|
valueSuffix={valueSuffix}
|
|
/>
|
|
),
|
|
},
|
|
primaryCursor: false,
|
|
secondaryCursor: false,
|
|
primaryAxis,
|
|
secondaryAxes,
|
|
dark: theme.htmlThemeClass === Theme.DARK,
|
|
defaultColors: [
|
|
"var(--color-text-accent)",
|
|
"var(--color-accent)",
|
|
"var(--color-info)",
|
|
],
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ChartTooltipProps extends TooltipRendererProps<any> {
|
|
headerSuffix?: string;
|
|
valueSuffix?: string;
|
|
}
|
|
|
|
function ChartTooltip({
|
|
focusedDatum,
|
|
headerSuffix = "",
|
|
valueSuffix = "",
|
|
}: ChartTooltipProps) {
|
|
const { formatDate } = useTimeFormat();
|
|
const dataPoints = focusedDatum?.interactiveGroup ?? [];
|
|
|
|
const header = () => {
|
|
const primaryValue = dataPoints[0]?.primaryValue;
|
|
if (!primaryValue) return null;
|
|
|
|
if (primaryValue instanceof Date) {
|
|
return formatDate(primaryValue, {
|
|
weekday: "short",
|
|
day: "numeric",
|
|
month: "long",
|
|
});
|
|
}
|
|
|
|
return primaryValue;
|
|
};
|
|
|
|
return (
|
|
<div className={styles.tooltip}>
|
|
<h3 className="text-center text-md">
|
|
{header()}
|
|
{headerSuffix}
|
|
</h3>
|
|
{dataPoints.map((dataPoint, index) => {
|
|
const color = dataPoint.style?.fill ?? "var(--color-accent)";
|
|
|
|
return (
|
|
<div key={index} className="stack horizontal items-center sm">
|
|
<div
|
|
className={clsx(styles.dot, {
|
|
[styles.dotFocused]:
|
|
focusedDatum?.seriesId === dataPoint.seriesId,
|
|
})}
|
|
style={{
|
|
"--dot-color": color,
|
|
"--dot-color-outline": color.replace(")", "-transparent)"),
|
|
}}
|
|
/>
|
|
<div>{dataPoint.originalSeries.label}</div>
|
|
<div className={styles.tooltipValue}>
|
|
{dataPoint.secondaryValue}
|
|
{valueSuffix}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|