diff --git a/package.json b/package.json index 1419dc6..8931b76 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@heroicons/react": "^2.1.1", "@vvo/tzdb": "^6.125.0", + "array-utils-ts": "^0.1.2", "clsx": "^2.1.0", "fuse.js": "^7.0.0", "jotai": "^2.6.2", @@ -18,17 +19,17 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "timezones.json": "^1.7.1", - "countries-and-timezones": "^3.6.0", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.17", + "countries-and-timezones": "^3.6.0", "postcss": "^8.4.33", "prettier": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.1", + "timezones.json": "^1.7.1", "typescript": "^5.2.2", "vite": "^5.0.8", "vite-plugin-bundlesize": "^0.0.7" diff --git a/src/app.tsx b/src/app.tsx index 315c3e4..c36f48b 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,4 +1,5 @@ import { MoonIcon, SunIcon } from "@heroicons/react/16/solid" +import { filterNullable } from "array-utils-ts" import clsx from "clsx" import { Provider, useAtom } from "jotai" import { FC, useState } from "react" @@ -70,7 +71,15 @@ const Head: FC = () => { const Main: FC = () => { const [tzs, setTzs] = useAtom(TzListState) - const places = tzs.map((tz) => getPlaceByTzName(tz)) + const places = filterNullable( + tzs.map((tz) => { + try { + return getPlaceByTzName(tz) + } catch (e) { + return null + } + }), + ) return (
diff --git a/src/components/SelectPlace.tsx b/src/components/SelectPlace.tsx index 584e276..c0bdf06 100644 --- a/src/components/SelectPlace.tsx +++ b/src/components/SelectPlace.tsx @@ -1,8 +1,8 @@ -import { TimeZone, getTimeZones } from "@vvo/tzdb" +import { TimeZone } from "@vvo/tzdb" import clsx from "clsx" import Fuse from "fuse.js" import { FC, useEffect, useState } from "react" -import { Place, tzToPlace } from "../utils/places" +import { Place, TimeZones, tzToPlace } from "../utils/places" type SelectPlaceProps = { values: Place[] @@ -12,8 +12,10 @@ type SelectPlaceProps = { export const SelectPlace: FC = ({ values, onChange }) => { const [value, setValue] = useState("") - const timezones = getTimeZones() - const fuse = new Fuse(timezones, { threshold: 0.1, keys: ["name", "mainCities", "countryName"] }) + const fuse = new Fuse(Object.values(TimeZones), { + threshold: 0.1, + keys: ["name", "mainCities", "countryName"], + }) const options = (value.length > 0 ? fuse.search(value).map((x) => x.item) : []) .filter((x) => !values.find((y) => y.tzName === x.name)) diff --git a/src/state.ts b/src/state.ts index 21edb1f..0c5d65f 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,11 +1,13 @@ import { atom, useAtomValue } from "jotai" import { atomWithStorageSync } from "./utils/atomWithStorageSync" -import { getPlaceByTzName } from "./utils/places" +import { getPlaceByTzName, getSystemPlace } from "./utils/places" type TzView = "12" | "24" | "MX" -export const TzListState = atomWithStorageSync("tzList", []) -export const TzHomeState = atomWithStorageSync("tzHome", "Europe/London") +const systemTz = getSystemPlace().tzName + +export const TzListState = atomWithStorageSync("tzList", [systemTz]) +export const TzHomeState = atomWithStorageSync("tzHome", systemTz) export const TzModeState = atomWithStorageSync("tzMode", "24") const HomePlace = atom((get) => getPlaceByTzName(get(TzHomeState))) diff --git a/src/utils/places.ts b/src/utils/places.ts index dec7d1b..c7e64ee 100644 --- a/src/utils/places.ts +++ b/src/utils/places.ts @@ -1,5 +1,37 @@ import { TimeZone, getTimeZones } from "@vvo/tzdb" +export const TimeZones: Record = {} + +for (const tz of getTimeZones()) { + const names = [tz.name, ...tz.group] + for (const name of names) { + if (TimeZones[name]) { + const lOff = TimeZones[name].rawOffsetInMinutes + const rOff = tz.rawOffsetInMinutes + if (lOff === rOff) continue + + console.warn("Duplicate timezone", { name, lOff, rOff, tz }) + continue + } + TimeZones[name] = { ...tz, name } + } +} + +export const getPlaceByTzName = (tzName: string): Place => { + const tz = TimeZones[tzName] + if (!tz) throw new Error(`No timezone found for ${tzName}`) + return tzToPlace(tz) +} + +export const getSystemPlace = () => { + try { + const tzName = Intl.DateTimeFormat().resolvedOptions().timeZone + return getPlaceByTzName(tzName) + } catch (e) { + return getPlaceByTzName("Europe/London") + } +} + export type Place = { tzName: string tzAbbr: string @@ -17,9 +49,3 @@ export const tzToPlace = (tz: TimeZone): Place => { city: tz.mainCities[0], } } - -export const getPlaceByTzName = (tzName: string): Place => { - const tz = getTimeZones().find((x) => x.name === tzName) - if (!tz) throw new Error(`No timezone found for ${tzName}`) - return tzToPlace(tz) -} diff --git a/yarn.lock b/yarn.lock index 44bb1f4..75215d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,13 +7,6 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== -"@babel/runtime@^7.23.5": - version "7.23.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" - integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== - dependencies: - regenerator-runtime "^0.14.0" - "@esbuild/aix-ppc64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" @@ -431,6 +424,11 @@ arg@^5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== +array-utils-ts@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/array-utils-ts/-/array-utils-ts-0.1.2.tgz#232a416b0e6794b64880372e297d9702cf8877ef" + integrity sha512-AVp/ybvqELxWd7ZtSC9HGwPPv4FOoWlJWtOaQY1lZuPKmRmJKXA80f+CAyMByH6yCF7H5wDupA67c7N0SGTiTQ== + autoprefixer@^10.4.17: version "10.4.17" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be" @@ -993,11 +991,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - resolve@^1.1.7, resolve@^1.22.2: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" @@ -1122,13 +1115,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tailwind-merge@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.0.tgz#b6bb1c63ef26283c9e6675ba237df83bbd554688" - integrity sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ== - dependencies: - "@babel/runtime" "^7.23.5" - tailwindcss@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.1.tgz#f512ca5d1dd4c9503c7d3d28a968f1ad8f5c839d"