mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-23 19:27:25 +02:00
First public commit
This commit is contained in:
commit
e05cd62e53
228 changed files with 17717 additions and 0 deletions
1
components/time-zone-picker/index.ts
Normal file
1
components/time-zone-picker/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./time-zone-picker";
|
200
components/time-zone-picker/time-zone-picker.tsx
Normal file
200
components/time-zone-picker/time-zone-picker.tsx
Normal file
|
@ -0,0 +1,200 @@
|
|||
import { Combobox } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import spacetime from "spacetime";
|
||||
import soft from "timezone-soft";
|
||||
|
||||
import ChevronDown from "../../components/icons/chevron-down.svg";
|
||||
import { styleMenuItem } from "../menu-styles";
|
||||
import timeZones from "./time-zones.json";
|
||||
interface TimeZoneOption {
|
||||
value: string;
|
||||
label: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
const useTimeZones = () => {
|
||||
const options = React.useMemo(() => {
|
||||
return Object.entries(timeZones)
|
||||
.reduce<TimeZoneOption[]>((selectOptions, zone) => {
|
||||
const now = spacetime.now(zone[0]);
|
||||
const tz = now.timezone();
|
||||
|
||||
let label = "";
|
||||
|
||||
const min = tz.current.offset * 60;
|
||||
const hr =
|
||||
`${(min / 60) ^ 0}:` + (min % 60 === 0 ? "00" : Math.abs(min % 60));
|
||||
const prefix = `(GMT${hr.includes("-") ? hr : `+${hr}`}) ${zone[1]}`;
|
||||
|
||||
label = prefix;
|
||||
|
||||
selectOptions.push({
|
||||
value: tz.name,
|
||||
label: label,
|
||||
offset: tz.current.offset,
|
||||
});
|
||||
|
||||
return selectOptions;
|
||||
}, [])
|
||||
.sort((a: TimeZoneOption, b: TimeZoneOption) => a.offset - b.offset);
|
||||
}, []);
|
||||
|
||||
const findFuzzyTz = React.useCallback(
|
||||
(zone: string): TimeZoneOption => {
|
||||
let currentTime = spacetime.now("GMT");
|
||||
try {
|
||||
currentTime = spacetime.now(zone);
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid time zone: zone`);
|
||||
}
|
||||
return options
|
||||
.filter(
|
||||
(tz: TimeZoneOption) =>
|
||||
tz.offset === currentTime.timezone().current.offset,
|
||||
)
|
||||
.map((tz: TimeZoneOption) => {
|
||||
let score = 0;
|
||||
if (
|
||||
currentTime.timezones[tz.value.toLowerCase()] &&
|
||||
!!currentTime.timezones[tz.value.toLowerCase()].dst ===
|
||||
currentTime.timezone().hasDst
|
||||
) {
|
||||
if (
|
||||
tz.value
|
||||
.toLowerCase()
|
||||
.indexOf(
|
||||
currentTime.tz.substring(currentTime.tz.indexOf("/") + 1),
|
||||
) !== -1
|
||||
) {
|
||||
score += 8;
|
||||
}
|
||||
if (
|
||||
tz.label
|
||||
.toLowerCase()
|
||||
.indexOf(
|
||||
currentTime.tz.substring(currentTime.tz.indexOf("/") + 1),
|
||||
) !== -1
|
||||
) {
|
||||
score += 4;
|
||||
}
|
||||
if (
|
||||
tz.value
|
||||
.toLowerCase()
|
||||
.indexOf(
|
||||
currentTime.tz.substring(0, currentTime.tz.indexOf("/")),
|
||||
)
|
||||
) {
|
||||
score += 2;
|
||||
}
|
||||
score += 1;
|
||||
} else if (tz.value === "GMT") {
|
||||
score += 1;
|
||||
}
|
||||
return { tz, score };
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(({ tz }) => tz)[0];
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
options,
|
||||
findFuzzyTz,
|
||||
}),
|
||||
[findFuzzyTz, options],
|
||||
);
|
||||
};
|
||||
|
||||
const TimeZonePicker: React.VoidFunctionComponent<{
|
||||
value: string;
|
||||
onChange: (tz: string) => void;
|
||||
onBlur?: () => void;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}> = ({ value, onChange, onBlur, className, style, disabled }) => {
|
||||
const { options, findFuzzyTz } = useTimeZones();
|
||||
|
||||
const timeZoneOptions = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
value: "",
|
||||
label: "Ignore time zone",
|
||||
offset: 0,
|
||||
},
|
||||
...options,
|
||||
],
|
||||
[options],
|
||||
);
|
||||
|
||||
const selectedTimeZone = React.useMemo(
|
||||
() =>
|
||||
value
|
||||
? timeZoneOptions.find(
|
||||
(timeZoneOption) => timeZoneOption.value === value,
|
||||
) ?? findFuzzyTz(value)
|
||||
: timeZoneOptions[0],
|
||||
[findFuzzyTz, timeZoneOptions, value],
|
||||
);
|
||||
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
const filteredTimeZones = React.useMemo(() => {
|
||||
return query
|
||||
? timeZoneOptions.filter((tz) => {
|
||||
if (tz.label.toLowerCase().includes(query.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
const tzStrings = soft(query);
|
||||
return tzStrings.some((tzString) => tzString.iana === tz.value);
|
||||
})
|
||||
: timeZoneOptions;
|
||||
}, [timeZoneOptions, query]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
value={selectedTimeZone}
|
||||
onChange={(newTimeZone) => {
|
||||
setQuery("");
|
||||
onChange(newTimeZone.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className={clsx("relative", className)} style={style}>
|
||||
{/* Remove generic params once Combobox.Input can infer the types */}
|
||||
<Combobox.Input<"input", TimeZoneOption>
|
||||
className="input w-full pr-8"
|
||||
displayValue={() => ""}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-0 w-full flex items-center cursor-default px-2 h-9 text-left">
|
||||
<span className="grow truncate">
|
||||
{!query ? selectedTimeZone.label : null}
|
||||
</span>
|
||||
<span className="flex pointer-events-none">
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</span>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="absolute w-full z-50 py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg max-h-72 ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{filteredTimeZones.map((timeZone) => (
|
||||
<Combobox.Option
|
||||
key={timeZone.value}
|
||||
className={styleMenuItem}
|
||||
value={timeZone}
|
||||
>
|
||||
{timeZone.label}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeZonePicker;
|
81
components/time-zone-picker/time-zones.json
Normal file
81
components/time-zone-picker/time-zones.json
Normal file
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"Pacific/Midway": "Midway Island, Samoa",
|
||||
"Pacific/Honolulu": "Hawaii",
|
||||
"America/Juneau": "Alaska",
|
||||
"America/Boise": "Mountain Time",
|
||||
"America/Dawson": "Dawson, Yukon",
|
||||
"America/Chihuahua": "Chihuahua, La Paz, Mazatlan",
|
||||
"America/Phoenix": "Arizona",
|
||||
"America/Chicago": "Central Time",
|
||||
"America/Regina": "Saskatchewan",
|
||||
"America/Mexico_City": "Guadalajara, Mexico City, Monterrey",
|
||||
"America/Belize": "Central America",
|
||||
"America/Detroit": "Eastern Time",
|
||||
"America/Bogota": "Bogota, Lima, Quito",
|
||||
"America/Caracas": "Caracas, La Paz",
|
||||
"America/Santiago": "Santiago",
|
||||
"America/St_Johns": "Newfoundland and Labrador",
|
||||
"America/Sao_Paulo": "Brasilia",
|
||||
"America/Tijuana": "Tijuana",
|
||||
"America/Montevideo": "Montevideo",
|
||||
"America/Argentina/Buenos_Aires": "Buenos Aires, Georgetown",
|
||||
"America/Godthab": "Greenland",
|
||||
"America/Los_Angeles": "Pacific Time",
|
||||
"Atlantic/Azores": "Azores",
|
||||
"Atlantic/Cape_Verde": "Cape Verde Islands",
|
||||
"GMT": "UTC",
|
||||
"Europe/London": "Edinburgh, London",
|
||||
"Europe/Dublin": "Dublin",
|
||||
"Europe/Lisbon": "Lisbon",
|
||||
"Africa/Casablanca": "Casablanca, Monrovia",
|
||||
"Atlantic/Canary": "Canary Islands",
|
||||
"Europe/Belgrade": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
|
||||
"Europe/Sarajevo": "Sarajevo, Skopje, Warsaw, Zagreb",
|
||||
"Europe/Brussels": "Brussels, Copenhagen, Madrid, Paris",
|
||||
"Europe/Amsterdam": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
|
||||
"Africa/Algiers": "West Central Africa",
|
||||
"Europe/Bucharest": "Bucharest",
|
||||
"Africa/Cairo": "Cairo",
|
||||
"Europe/Helsinki": "Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius",
|
||||
"Europe/Athens": "Athens, Minsk",
|
||||
"Asia/Jerusalem": "Jerusalem",
|
||||
"Africa/Harare": "Harare, Pretoria",
|
||||
"Europe/Moscow": "Istanbul, Moscow, St. Petersburg, Volgograd",
|
||||
"Asia/Kuwait": "Kuwait, Riyadh",
|
||||
"Africa/Nairobi": "Nairobi",
|
||||
"Asia/Baghdad": "Baghdad",
|
||||
"Asia/Tehran": "Tehran",
|
||||
"Asia/Dubai": "Abu Dhabi, Muscat",
|
||||
"Asia/Baku": "Baku, Tbilisi, Yerevan",
|
||||
"Asia/Kabul": "Kabul",
|
||||
"Asia/Yekaterinburg": "Ekaterinburg",
|
||||
"Asia/Karachi": "Islamabad, Karachi, Tashkent",
|
||||
"Asia/Kolkata": "Chennai, Kolkata, Mumbai, New Delhi",
|
||||
"Asia/Kathmandu": "Kathmandu",
|
||||
"Asia/Dhaka": "Astana, Dhaka",
|
||||
"Asia/Colombo": "Sri Jayawardenepura",
|
||||
"Asia/Almaty": "Almaty, Novosibirsk",
|
||||
"Asia/Rangoon": "Yangon Rangoon",
|
||||
"Asia/Bangkok": "Bangkok, Hanoi, Jakarta",
|
||||
"Asia/Krasnoyarsk": "Krasnoyarsk",
|
||||
"Asia/Shanghai": "Beijing, Chongqing, Hong Kong SAR, Urumqi",
|
||||
"Asia/Kuala_Lumpur": "Kuala Lumpur, Singapore",
|
||||
"Asia/Taipei": "Taipei",
|
||||
"Australia/Perth": "Perth",
|
||||
"Asia/Irkutsk": "Irkutsk, Ulaanbaatar",
|
||||
"Asia/Seoul": "Seoul",
|
||||
"Asia/Tokyo": "Osaka, Sapporo, Tokyo",
|
||||
"Asia/Yakutsk": "Yakutsk",
|
||||
"Australia/Darwin": "Darwin",
|
||||
"Australia/Adelaide": "Adelaide",
|
||||
"Australia/Sydney": "Canberra, Melbourne, Sydney",
|
||||
"Australia/Brisbane": "Brisbane",
|
||||
"Australia/Hobart": "Hobart",
|
||||
"Asia/Vladivostok": "Vladivostok",
|
||||
"Pacific/Guam": "Guam, Port Moresby",
|
||||
"Asia/Magadan": "Magadan, Solomon Islands, New Caledonia",
|
||||
"Asia/Kamchatka": "Kamchatka, Marshall Islands",
|
||||
"Pacific/Fiji": "Fiji Islands",
|
||||
"Pacific/Auckland": "Auckland, Wellington",
|
||||
"Pacific/Tongatapu": "Nuku'alofa"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue