fix: address CodeRabbit review

This commit is contained in:
ajnart committed 2026-08-20 15:43:23 +02:00
1 parent e3475d3efe
commit 4f1599dfee
17 files changed
+108 -39

No files matched your search

+18 -13
View File
@@ -20,6 +20,7 @@ import {
getAqiStandard,
getAqiValue,
getCompactAirQualityLayout,
getDailyAqiValue,
getDominantPollutant,
getStrongestPollen,
getUpcomingHours,
@@ -32,7 +33,7 @@ export default function AirQualityWidget({ options, width, height, displayMode }
latitude: options.location.latitude,
longitude: options.location.longitude,
});
const airQuality = getUsableWidgetQueryData(query) as AirQualityData | null | undefined;
const airQuality = getUsableWidgetQueryData(query);
if (isInitialWidgetQueryPending(query)) return <WidgetQueryLoadingState />;
if (!airQuality) return <WidgetEmptyState />;
@@ -118,18 +119,18 @@ const CompactAirQuality = ({
<>
<MetricLine
label={labels.pollutants.pm2_5}
value={formatConcentration(airQuality.current.pollutants.pm2_5, locale)}
value={formatConcentration(airQuality.current.pollutants.pm2_5, locale, t)}
/>
<MetricLine
label={labels.pollutants.pm10}
value={formatConcentration(airQuality.current.pollutants.pm10, locale)}
value={formatConcentration(airQuality.current.pollutants.pm10, locale, t)}
/>
</>
)}
{options.showPollen && strongestPollen && (
<MetricLine
label={t("strongestPollen")}
value={`${labels.pollen[strongestPollen.key]} · ${formatPollen(strongestPollen.value, locale)}`}
value={`${labels.pollen[strongestPollen.key]} · ${formatPollen(strongestPollen.value, locale, t)}`}
/>
)}
</Group>
@@ -169,7 +170,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
uv: hour.uvIndex ?? Number.NaN,
}));
const series = [
{ name: "aqi", label: "AQI", color: getAqiColor(category), type: "line" as const },
{ name: "aqi", label: t("aqiLabel"), color: getAqiColor(category), type: "line" as const },
...(options.showUv
? [{ name: "uv", label: t("uv.label"), color: "yellow.7", type: "line" as const, yAxisId: "right" as const }]
: []),
@@ -201,7 +202,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
</Group>
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
<MetricCard label="AQI" value={formatAqi(value, locale)} />
<MetricCard label={t("aqiLabel")} value={formatAqi(value, locale)} />
{options.showUv && (
<MetricCard label={t("uv.label")} value={formatDecimal(airQuality.current.uvIndex, locale)} />
)}
@@ -209,11 +210,11 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
<>
<MetricCard
label={labels.pollutants.pm2_5}
value={formatConcentration(airQuality.current.pollutants.pm2_5, locale)}
value={formatConcentration(airQuality.current.pollutants.pm2_5, locale, t)}
/>
<MetricCard
label={labels.pollutants.pm10}
value={formatConcentration(airQuality.current.pollutants.pm10, locale)}
value={formatConcentration(airQuality.current.pollutants.pm10, locale, t)}
/>
</>
)}
@@ -279,7 +280,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
<MetricLine
key={key}
label={labels.pollutants[key]}
value={formatConcentration(airQuality.current.pollutants[key], locale)}
value={formatConcentration(airQuality.current.pollutants[key], locale, t)}
/>
))}
</SimpleGrid>
@@ -295,7 +296,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
{pollenKeys.map((key) => {
const pollen = airQuality.current.pollen[key];
if (pollen === null) return null;
return <MetricLine key={key} label={labels.pollen[key]} value={formatPollen(pollen, locale)} />;
return <MetricLine key={key} label={labels.pollen[key]} value={formatPollen(pollen, locale, t)} />;
})}
</SimpleGrid>
</Paper>
@@ -309,7 +310,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
</Text>
<SimpleGrid cols={{ base: 2, sm: 4, md: 7 }} spacing="xs">
{airQuality.daily.map((day) => {
const dailyAqi = standard === "us" ? day.usAqiMax : day.europeanAqiMax;
const dailyAqi = getDailyAqiValue(day, standard);
return (
<Stack key={day.date} gap={2} align="center">
<Text size="xs" c="dimmed">
@@ -388,7 +389,11 @@ const formatDecimal = (value: number | null, locale: string) => {
return new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
};
const formatConcentration = (value: number | null, locale: string) => `${formatDecimal(value, locale)} µg/m³`;
const formatPollen = (value: number | null, locale: string) => `${formatDecimal(value, locale)} grains/m³`;
type ScopedAirQualityTranslator = ReturnType<typeof useScopedI18n<"widget.airQuality">>;
const formatConcentration = (value: number | null, locale: string, t: ScopedAirQualityTranslator) =>
t("unit.concentration", { value: formatDecimal(value, locale) });
const formatPollen = (value: number | null, locale: string, t: ScopedAirQualityTranslator) =>
t("unit.pollen", { value: formatDecimal(value, locale) });
const hasAvailableValue = (values: Record<string, number | null>) =>
Object.values(values).some((value) => value !== null);
@@ -65,6 +65,11 @@ export const getAqiValue = (point: AirQualityPoint, standard: AqiStandard): numb
return point.europeanAqi;
};
export const getDailyAqiValue = (day: AirQualityData["daily"][number], standard: AqiStandard): number | null => {
if (standard === "us") return day.usAqiMax;
return day.europeanAqiMax;
};
export const getAqiCategory = (value: number | null, standard: AqiStandard): AqiCategory => {
if (value === null) return "unknown";
@@ -21,12 +21,14 @@ const phaseSections = [
] as const;
const phaseLegend = [
{ key: "night", range: "00–05 · 21–24", color: "indigo.8" },
{ key: "dawn", range: "05–08", color: "orange.6" },
{ key: "day", range: "08–18", color: "cyan.6" },
{ key: "dusk", range: "18–21", color: "grape.6" },
{ key: "night", color: "indigo.8" },
{ key: "dawn", color: "orange.6" },
{ key: "day", color: "cyan.6" },
{ key: "dusk", color: "grape.6" },
] as const;
const timeTicks = ["midnight", "morning", "noon", "evening", "end"] as const;
export const TimeOfDayBar = ({
minuteOfDay,
phase,
@@ -72,9 +74,9 @@ export const TimeOfDayBar = ({
</Box>
{showTicks && (
<Group justify="space-between" wrap="nowrap">
{["00", "06", "12", "18", "24"].map((tick) => (
{timeTicks.map((tick) => (
<Text key={tick} size="xs" c="dimmed" aria-hidden>
{tick}
{t(`worldClock.tick.${tick}`)}
</Text>
))}
</Group>
@@ -88,7 +90,7 @@ export const TimeOfDayBar = ({
<Group key={item.key} gap={6} wrap="nowrap">
<Box w={9} h={9} bg={item.color} style={{ borderRadius: "var(--mantine-radius-xl)" }} />
<Text size="xs" c="dimmed">
{t(`worldClock.phase.${item.key}`)} · {item.range}
{t(`worldClock.phase.${item.key}`)} · {t(`worldClock.phaseRange.${item.key}`)}
</Text>
</Group>
))}
@@ -57,6 +57,7 @@ export const useLocalWidgetState = <T>({
const defaultValueRef = useRef(defaultValue);
const validateRef = useRef(validate);
const migrateRef = useRef(migrate);
defaultValueRef.current = defaultValue;
validateRef.current = validate;
migrateRef.current = migrate;
+7 -1
View File
@@ -9,6 +9,7 @@ dayjs.extend(timezone);
const minuteMs = 60_000;
const dayMs = 86_400_000;
const timeZoneSupportCache = new Map<string, boolean>();
export interface WallDateTime {
year: number;
@@ -28,10 +29,14 @@ export interface CountdownEventState {
}
export const isCountdownTimeZoneSupported = (timeZone: string) => {
const cached = timeZoneSupportCache.get(timeZone);
if (cached !== undefined) return cached;
try {
new Intl.DateTimeFormat("en", { timeZone }).format(0);
timeZoneSupportCache.set(timeZone, true);
return true;
} catch {
timeZoneSupportCache.set(timeZone, false);
return false;
}
};
@@ -95,7 +100,8 @@ const resolveExactWallDateTime = (value: WallDateTime, timeZone: string): Date[]
const candidate = new Date(naiveUtc - offset * minuteMs);
if (wallDateTimeEquals(value, dayjs(candidate).tz(timeZone))) matches.push(candidate);
}
return matches.toSorted((left, right) => left.getTime() - right.getTime());
// oxlint-disable-next-line unicorn/no-array-sort -- Array.toSorted is not supported in all target browsers.
return [...matches].sort((left, right) => left.getTime() - right.getTime());
};
/** Resolves a local wall-clock value, preferring the earlier repeated time and the first valid time after a DST gap. */
+4 -1
View File
@@ -202,7 +202,10 @@ export default function TimerWidget({
const nowMs = now.getTime();
const remainingMs = getRemainingMs(runtime, nowMs);
const phaseDurationMs = runtime.totalDurationMs;
const progress = Math.min(100, Math.max(0, ((phaseDurationMs - remainingMs) / phaseDurationMs) * 100));
let progress = 0;
if (phaseDurationMs > 0) {
progress = Math.min(100, Math.max(0, ((phaseDurationMs - remainingMs) / phaseDurationMs) * 100));
}
const controlSize = getTimerControlSize(width, height, displayMode);
const attentionStyle = getTimerAttentionStyle({
awaitingManualStart: runtime.awaitingManualStart,
+4 -1
View File
@@ -130,7 +130,10 @@ const AdvancedToday = ({ now, locale, options, stats }: AdvancedTodayProps) => {
label: t("advanced.dayOfYear"),
value: `${numberFormatter.format(stats.dayOfYear)} / ${numberFormatter.format(stats.daysInYear)}`,
},
options.showQuarter && { label: t("advanced.quarter"), value: `Q${stats.quarter}` },
options.showQuarter && {
label: t("advanced.quarter"),
value: t("advanced.quarterValue", { quarter: stats.quarter }),
},
{ label: t("advanced.daysRemaining"), value: numberFormatter.format(stats.daysAfterToday) },
].filter((metric): metric is { label: string; value: string } => Boolean(metric));
+1 -1
View File
@@ -36,7 +36,7 @@ export const getTodayStats = (date: Date, weekConvention: WeekConvention, locale
const weekStart = new Date(date.getFullYear(), date.getMonth(), date.getDate() - daysSinceWeekStart);
const nextWeekStart = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7);
const currentDayOfYear = value.dayOfYear();
const daysInYear = nextYearStart.diff(yearStart, "day");
const daysInYear = yearStart.endOf("year").dayOfYear();
return {
dayOfYear: currentDayOfYear,
@@ -20,7 +20,8 @@ const getAnimationClass = (code: number): string => {
};
export const AnimatedWeatherIcon = ({ animated = false, code, isDay, size = 26 }: AnimatedWeatherIconProps) => {
const animationClass = animated ? getAnimationClass(code) : "";
let animationClass = "";
if (animated && (code !== 0 || isDay !== false)) animationClass = getAnimationClass(code);
return (
<span className={`weather-anim-wrapper ${animationClass}`}>
<WeatherIcon code={code} isDay={isDay} size={size} />
+5 -1
View File
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { Group, Popover, Stack, Text, UnstyledButton } from "@mantine/core";
import { IconArrowDownRight, IconArrowUpRight, IconDroplets, IconMapPin, IconWind } from "@tabler/icons-react";
@@ -24,7 +25,10 @@ export const CompactWeather = ({ height, isEditMode, options, weather, width }:
const locale = useCurrentIntlLocale();
const t = useScopedI18n("widget.weather");
const tCommon = useScopedI18n("common");
const layout = getCompactWeatherLayout(width, height, options.hasForecast, options.forecastDayCount);
const layout = useMemo(
() => getCompactWeatherLayout(width, height, options.hasForecast, options.forecastDayCount),
[height, options.forecastDayCount, options.hasForecast, width],
);
const today = weather.daily[0];
const speedUnit = options.useImperialSpeed
? tCommon("unit.speed.milesPerHour")
@@ -13,9 +13,6 @@ import { AdvancedWeather } from "./advanced";
import classes from "./component.module.css";
import { CompactWeather } from "./compact";
export { getPreferredTime, getPreferredUnit, getPreferredWindSpeed } from "./format";
export { getCompactWeatherLayout } from "./layout";
export default function WeatherWidget({
isEditMode,
options,
+15 -4
View File
@@ -9,13 +9,25 @@ export interface CompactWeatherLayout {
showSecondary: boolean;
}
const compactWeatherBreakpoints = [
{ tier: "roomy", minWidth: 500, minHeight: 280 },
{ tier: "standard", minWidth: 280, minHeight: 180 },
{ tier: "compact", minWidth: 160, minHeight: 120 },
{ tier: "micro", minWidth: 0, minHeight: 0 },
] as const satisfies readonly { tier: CompactWeatherTier; minWidth: number; minHeight: number }[];
export const getCompactWeatherLayout = (
width: number,
height: number,
hasForecast: boolean,
configuredDays: number,
): CompactWeatherLayout => {
if (height < 120 || width < 160) {
const breakpoint = compactWeatherBreakpoints.find(
({ minHeight, minWidth }) => width >= minWidth && height >= minHeight,
);
const tier = breakpoint?.tier ?? "micro";
if (tier === "micro") {
return {
tier: "micro",
forecastDays: 0,
@@ -26,7 +38,7 @@ export const getCompactWeatherLayout = (
};
}
if (height < 180 || width < 280) {
if (tier === "compact") {
return {
tier: "compact",
forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(width / 92))) : 0,
@@ -37,10 +49,9 @@ export const getCompactWeatherLayout = (
};
}
const roomy = width >= 500 && height >= 280;
const availableForecastWidth = Math.max(0, width - 24);
return {
tier: roomy ? "roomy" : "standard",
tier,
forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(availableForecastWidth / 64))) : 0,
showCity: true,
showCondition: true,