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";
};