import { Alert, Badge, Box, Divider, Group, Paper, ScrollArea, SimpleGrid, Stack, Text, ThemeIcon, Title, } from "@mantine/core"; import { IconAlertTriangle, IconClock } from "@tabler/icons-react"; import type { Dayjs } from "dayjs"; import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client"; import { formatLocalizedDate, formatLocalizedTime } from "../common/locale"; import type { WidgetComponentProps } from "../definition"; import { clockTimeFormatShowsSeconds, clockTimeFormatUses12Hours, resolveClockTimeFormat } from "./format"; import { TimeOfDayBar } from "./time-of-day-bar"; import { ClockWeatherSummary } from "./weather-summary"; import { createWorldClockTime, formatRelativeOffset, formatUtcOffset, getTimeOfDayPhase, humanizeTimeZone, maximumWorldClockCities, } from "./world-clock"; import type { TimeOfDayPhase, WorldClockTime } from "./world-clock"; interface AdvancedClockViewProps { now: Date; options: WidgetComponentProps<"clock">["options"]; primaryTime: Dayjs; primaryTimeZone: string; primaryTimeZoneInvalid: boolean; } export const AdvancedClockView = ({ now, options, primaryTime, primaryTimeZone, primaryTimeZoneInvalid, }: AdvancedClockViewProps) => { const t = useI18n("widget.clock"); const primaryLabel = getPrimaryLabel(options, t); const primary: WorldClockTime = { id: "primary", label: primaryLabel, timeZone: primaryTimeZone, zonedTime: primaryTime, phase: getTimeOfDayPhase(primaryTime.hour()), minuteOfDay: primaryTime.hour() * 60 + primaryTime.minute() + primaryTime.second() / 60, utcOffset: primaryTime.utcOffset(), dayDifference: 0, }; const cities = options.worldClockCities .slice(0, maximumWorldClockCities) .filter(({ timeZone }) => timeZone !== primaryTimeZone) .map((city) => createWorldClockTime({ city, now, primaryTime })); return ( {primaryTimeZoneInvalid && ( } color="yellow" title={t("worldClock.unsupportedPrimaryTitle")}> {t("worldClock.unsupportedPrimary", { timeZone: options.timezone })} )} ); }; interface ModeProps { now: Date; options: WidgetComponentProps<"clock">["options"]; primary: WorldClockTime; cities: WorldClockTime[]; } const PrimaryClock = ({ now, options, primary }: Pick) => { const locale = useCurrentIntlLocale(); const t = useI18n("widget.clock"); const primaryTime = primary.zonedTime; if (!primaryTime || primary.phase === null || primary.minuteOfDay === null) return null; return ( {primary.label} {options.showDate && ( {formatLocalizedDate(now, locale, { dateStyle: "full", timeZone: primary.timeZone })} )} {primary.timeZone} {formatUtcOffset(primaryTime.utcOffset())} {options.showWeather && ( )} ); }; const OverviewMode = ({ now, options, primary, cities }: ModeProps) => { const t = useI18n("widget.clock"); if (cities.length === 0) return ; return ( {t("worldClock.cities")} {t("worldClock.cityCount", { count: cities.length })} {cities.map((city) => ( ))} ); }; interface ClockEntryProps { entry: WorldClockTime; now: Date; options: WidgetComponentProps<"clock">["options"]; primaryOffset: number; } const WorldClockCard = ({ entry, now, options, primaryOffset }: ClockEntryProps) => { if (!entry.zonedTime || entry.utcOffset === null) { return ; } return ( {entry.label} {entry.timeZone} ); }; const ClockTime = ({ entry, now, options, primary = false, compact = false, }: Omit & { primary?: boolean; compact?: boolean }) => { const locale = useCurrentIntlLocale(); const t = useI18n("widget.clock"); if (!entry.zonedTime) return null; let formattedTime = formatLocalizedTime(now, locale, { hour12: clockTimeFormatUses12Hours(options.customTimeFormat, options.is24HourFormat), includeSeconds: clockTimeFormatShowsSeconds(options.customTimeFormat), timeZone: entry.timeZone, }); if (primary) { formattedTime = entry.zonedTime.format(resolveClockTimeFormat(options.customTimeFormat, options.is24HourFormat)); } const accessibleDate = formatLocalizedDate(now, locale, { dateStyle: "medium", timeZone: entry.timeZone, }); const fontSize = compact ? { base: 30, sm: 34 } : { base: 44, sm: 56 }; return ( {formattedTime} ); }; const DayAndOffset = ({ entry, now, primaryOffset }: Pick) => { const locale = useCurrentIntlLocale(); const t = useI18n("widget.clock"); if (entry.dayDifference === null || entry.utcOffset === null) return null; const dayRelation = getDayRelation(entry.dayDifference, t); const date = formatLocalizedDate(now, locale, { weekday: "short", month: "short", day: "numeric", timeZone: entry.timeZone, }); return ( {date} · {dayRelation} {formatRelativeOffset(entry.utcOffset - primaryOffset)} · {formatUtcOffset(entry.utcOffset)} ); }; const InvalidTimeZone = ({ entry }: { entry: WorldClockTime }) => { const t = useI18n("widget.clock"); return ( {entry.label} {t("worldClock.unsupportedTimeZone", { timeZone: entry.timeZone })} ); }; const EmptyCities = () => { const t = useI18n("widget.clock"); return ( {t("worldClock.noSecondaryCities")} ); }; type ScopedClockTranslator = ReturnType>; const getDayRelation = (difference: number, t: ScopedClockTranslator) => { if (difference < 0) return t("worldClock.yesterday"); if (difference > 0) return t("worldClock.tomorrow"); return t("worldClock.today"); }; const getPrimaryLabel = (options: ModeProps["options"], t: ScopedClockTranslator) => { if (options.customTitleToggle) return options.customTitle || t("worldClock.primary"); if (options.useCustomTimezone) return humanizeTimeZone(options.timezone); return t("worldClock.local"); }; const getPhaseColor = (phase: TimeOfDayPhase) => { if (phase === "night") return "indigo"; if (phase === "dawn") return "orange"; if (phase === "day") return "cyan"; return "grape"; };