diff --git a/.prettierrc b/.prettierrc index 7b5c861b..58e54f14 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { "plugins": ["prettier-plugin-tailwindcss"], "tailwindStylesheet": "./app/globals.css" -} \ No newline at end of file +} diff --git a/README.md b/README.md index 011e0c9e..2b001613 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,32 @@ -# frontend +# plancake Frontend -This is where the frontend of tomeeto is! - -Current Tasks: - Build Mockups for all of the pages - Redesign the Create Event Page - Wait for Database man to do the funny backend thing - -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +This repository contains the frontend code for plancake. ## Getting Started -First, run the development server: +To clone and run this application, [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer: ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +npm install npm@latest -g ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +### Installation -## Learn More - -To learn more about Next.js, take a look at the following resources: +```bash +# Clone this repository +$ git clone https://github.com/tomeeto/frontend.git -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +# Install dependencies +$ npm install -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +# Run the app +$ npm run dev +``` -## Deploy on Vercel +## Credits -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +This project is built with the following: -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- [Next.js](https://nextjs.org/) +- [Tailwind.css](https://tailwindcss.com/) +- [Radix UI Elements](https://www.radix-ui.com/) diff --git a/app/_lib/classname.ts b/app/_lib/classname.ts new file mode 100644 index 00000000..d1ff4c93 --- /dev/null +++ b/app/_lib/classname.ts @@ -0,0 +1,7 @@ +// utils.ts +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: any[]) { + return twMerge(clsx(inputs)); +} diff --git a/app/_lib/grouped-timezones.ts b/app/_lib/grouped-timezones.ts new file mode 100644 index 00000000..67b3d00e --- /dev/null +++ b/app/_lib/grouped-timezones.ts @@ -0,0 +1,1721 @@ +// Auto-generated timezone list +export const groupedTimezones = [ + { + label: "Africa", + options: [ + { + label: "Abidjan (GMT0:00)", + value: "Africa/Abidjan", + }, + { + label: "Accra (GMT0:00)", + value: "Africa/Accra", + }, + { + label: "Addis Ababa (GMT+3:00)", + value: "Africa/Addis_Ababa", + }, + { + label: "Algiers (GMT+1:00)", + value: "Africa/Algiers", + }, + { + label: "Asmera (GMT+3:00)", + value: "Africa/Asmera", + }, + { + label: "Bamako (GMT0:00)", + value: "Africa/Bamako", + }, + { + label: "Bangui (GMT+1:00)", + value: "Africa/Bangui", + }, + { + label: "Banjul (GMT0:00)", + value: "Africa/Banjul", + }, + { + label: "Bissau (GMT0:00)", + value: "Africa/Bissau", + }, + { + label: "Blantyre (GMT+2:00)", + value: "Africa/Blantyre", + }, + { + label: "Brazzaville (GMT+1:00)", + value: "Africa/Brazzaville", + }, + { + label: "Bujumbura (GMT+2:00)", + value: "Africa/Bujumbura", + }, + { + label: "Cairo (GMT+3:00)", + value: "Africa/Cairo", + }, + { + label: "Casablanca (GMT+1:00)", + value: "Africa/Casablanca", + }, + { + label: "Ceuta (GMT+2:00)", + value: "Africa/Ceuta", + }, + { + label: "Conakry (GMT0:00)", + value: "Africa/Conakry", + }, + { + label: "Dakar (GMT0:00)", + value: "Africa/Dakar", + }, + { + label: "Dar es Salaam (GMT+3:00)", + value: "Africa/Dar_es_Salaam", + }, + { + label: "Djibouti (GMT+3:00)", + value: "Africa/Djibouti", + }, + { + label: "Douala (GMT+1:00)", + value: "Africa/Douala", + }, + { + label: "El Aaiun (GMT+1:00)", + value: "Africa/El_Aaiun", + }, + { + label: "Freetown (GMT0:00)", + value: "Africa/Freetown", + }, + { + label: "Gaborone (GMT+2:00)", + value: "Africa/Gaborone", + }, + { + label: "Harare (GMT+2:00)", + value: "Africa/Harare", + }, + { + label: "Johannesburg (GMT+2:00)", + value: "Africa/Johannesburg", + }, + { + label: "Juba (GMT+2:00)", + value: "Africa/Juba", + }, + { + label: "Kampala (GMT+3:00)", + value: "Africa/Kampala", + }, + { + label: "Khartoum (GMT+2:00)", + value: "Africa/Khartoum", + }, + { + label: "Kigali (GMT+2:00)", + value: "Africa/Kigali", + }, + { + label: "Kinshasa (GMT+1:00)", + value: "Africa/Kinshasa", + }, + { + label: "Lagos (GMT+1:00)", + value: "Africa/Lagos", + }, + { + label: "Libreville (GMT+1:00)", + value: "Africa/Libreville", + }, + { + label: "Lome (GMT0:00)", + value: "Africa/Lome", + }, + { + label: "Luanda (GMT+1:00)", + value: "Africa/Luanda", + }, + { + label: "Lubumbashi (GMT+2:00)", + value: "Africa/Lubumbashi", + }, + { + label: "Lusaka (GMT+2:00)", + value: "Africa/Lusaka", + }, + { + label: "Malabo (GMT+1:00)", + value: "Africa/Malabo", + }, + { + label: "Maputo (GMT+2:00)", + value: "Africa/Maputo", + }, + { + label: "Maseru (GMT+2:00)", + value: "Africa/Maseru", + }, + { + label: "Mbabane (GMT+2:00)", + value: "Africa/Mbabane", + }, + { + label: "Mogadishu (GMT+3:00)", + value: "Africa/Mogadishu", + }, + { + label: "Monrovia (GMT0:00)", + value: "Africa/Monrovia", + }, + { + label: "Nairobi (GMT+3:00)", + value: "Africa/Nairobi", + }, + { + label: "Ndjamena (GMT+1:00)", + value: "Africa/Ndjamena", + }, + { + label: "Niamey (GMT+1:00)", + value: "Africa/Niamey", + }, + { + label: "Nouakchott (GMT0:00)", + value: "Africa/Nouakchott", + }, + { + label: "Ouagadougou (GMT0:00)", + value: "Africa/Ouagadougou", + }, + { + label: "Porto-Novo (GMT+1:00)", + value: "Africa/Porto-Novo", + }, + { + label: "Sao Tome (GMT0:00)", + value: "Africa/Sao_Tome", + }, + { + label: "Tripoli (GMT+2:00)", + value: "Africa/Tripoli", + }, + { + label: "Tunis (GMT+1:00)", + value: "Africa/Tunis", + }, + { + label: "Windhoek (GMT+2:00)", + value: "Africa/Windhoek", + }, + ], + }, + { + label: "America", + options: [ + { + label: "Adak (GMT-9:00)", + value: "America/Adak", + }, + { + label: "Anchorage (GMT-8:00)", + value: "America/Anchorage", + }, + { + label: "Anguilla (GMT-4:00)", + value: "America/Anguilla", + }, + { + label: "Antigua (GMT-4:00)", + value: "America/Antigua", + }, + { + label: "Araguaina (GMT-3:00)", + value: "America/Araguaina", + }, + { + label: "Aruba (GMT-4:00)", + value: "America/Aruba", + }, + { + label: "Asuncion (GMT-3:00)", + value: "America/Asuncion", + }, + { + label: "Bahia (GMT-3:00)", + value: "America/Bahia", + }, + { + label: "Bahia Banderas (GMT-6:00)", + value: "America/Bahia_Banderas", + }, + { + label: "Barbados (GMT-4:00)", + value: "America/Barbados", + }, + { + label: "Belem (GMT-3:00)", + value: "America/Belem", + }, + { + label: "Belize (GMT-6:00)", + value: "America/Belize", + }, + { + label: "Beulah (GMT-5:00)", + value: "America/North_Dakota/Beulah", + }, + { + label: "Blanc-Sablon (GMT-4:00)", + value: "America/Blanc-Sablon", + }, + { + label: "Boa Vista (GMT-4:00)", + value: "America/Boa_Vista", + }, + { + label: "Bogota (GMT-5:00)", + value: "America/Bogota", + }, + { + label: "Boise (GMT-6:00)", + value: "America/Boise", + }, + { + label: "Buenos Aires (GMT-3:00)", + value: "America/Buenos_Aires", + }, + { + label: "Cambridge Bay (GMT-6:00)", + value: "America/Cambridge_Bay", + }, + { + label: "Campo Grande (GMT-4:00)", + value: "America/Campo_Grande", + }, + { + label: "Cancun (GMT-5:00)", + value: "America/Cancun", + }, + { + label: "Caracas (GMT-4:00)", + value: "America/Caracas", + }, + { + label: "Catamarca (GMT-3:00)", + value: "America/Catamarca", + }, + { + label: "Cayenne (GMT-3:00)", + value: "America/Cayenne", + }, + { + label: "Cayman (GMT-5:00)", + value: "America/Cayman", + }, + { + label: "Center (GMT-5:00)", + value: "America/North_Dakota/Center", + }, + { + label: "Chicago (GMT-5:00)", + value: "America/Chicago", + }, + { + label: "Chihuahua (GMT-6:00)", + value: "America/Chihuahua", + }, + { + label: "Ciudad Juarez (GMT-6:00)", + value: "America/Ciudad_Juarez", + }, + { + label: "Coral Harbour (GMT-5:00)", + value: "America/Coral_Harbour", + }, + { + label: "Cordoba (GMT-3:00)", + value: "America/Cordoba", + }, + { + label: "Costa Rica (GMT-6:00)", + value: "America/Costa_Rica", + }, + { + label: "Creston (GMT-7:00)", + value: "America/Creston", + }, + { + label: "Cuiaba (GMT-4:00)", + value: "America/Cuiaba", + }, + { + label: "Curacao (GMT-4:00)", + value: "America/Curacao", + }, + { + label: "Danmarkshavn (GMT0:00)", + value: "America/Danmarkshavn", + }, + { + label: "Dawson (GMT-7:00)", + value: "America/Dawson", + }, + { + label: "Dawson Creek (GMT-7:00)", + value: "America/Dawson_Creek", + }, + { + label: "Denver (GMT-6:00)", + value: "America/Denver", + }, + { + label: "Detroit (GMT-4:00)", + value: "America/Detroit", + }, + { + label: "Dominica (GMT-4:00)", + value: "America/Dominica", + }, + { + label: "Edmonton (GMT-6:00)", + value: "America/Edmonton", + }, + { + label: "Eirunepe (GMT-5:00)", + value: "America/Eirunepe", + }, + { + label: "El Salvador (GMT-6:00)", + value: "America/El_Salvador", + }, + { + label: "Fort Nelson (GMT-7:00)", + value: "America/Fort_Nelson", + }, + { + label: "Fortaleza (GMT-3:00)", + value: "America/Fortaleza", + }, + { + label: "Glace Bay (GMT-3:00)", + value: "America/Glace_Bay", + }, + { + label: "Godthab (GMT-1:00)", + value: "America/Godthab", + }, + { + label: "Goose Bay (GMT-3:00)", + value: "America/Goose_Bay", + }, + { + label: "Grand Turk (GMT-4:00)", + value: "America/Grand_Turk", + }, + { + label: "Grenada (GMT-4:00)", + value: "America/Grenada", + }, + { + label: "Guadeloupe (GMT-4:00)", + value: "America/Guadeloupe", + }, + { + label: "Guatemala (GMT-6:00)", + value: "America/Guatemala", + }, + { + label: "Guayaquil (GMT-5:00)", + value: "America/Guayaquil", + }, + { + label: "Guyana (GMT-4:00)", + value: "America/Guyana", + }, + { + label: "Halifax (GMT-3:00)", + value: "America/Halifax", + }, + { + label: "Havana (GMT-4:00)", + value: "America/Havana", + }, + { + label: "Hermosillo (GMT-7:00)", + value: "America/Hermosillo", + }, + { + label: "Indianapolis (GMT-4:00)", + value: "America/Indianapolis", + }, + { + label: "Inuvik (GMT-6:00)", + value: "America/Inuvik", + }, + { + label: "Iqaluit (GMT-4:00)", + value: "America/Iqaluit", + }, + { + label: "Jamaica (GMT-5:00)", + value: "America/Jamaica", + }, + { + label: "Jujuy (GMT-3:00)", + value: "America/Jujuy", + }, + { + label: "Juneau (GMT-8:00)", + value: "America/Juneau", + }, + { + label: "Knox (GMT-5:00)", + value: "America/Indiana/Knox", + }, + { + label: "Kralendijk (GMT-4:00)", + value: "America/Kralendijk", + }, + { + label: "La Paz (GMT-4:00)", + value: "America/La_Paz", + }, + { + label: "La Rioja (GMT-3:00)", + value: "America/Argentina/La_Rioja", + }, + { + label: "Lima (GMT-5:00)", + value: "America/Lima", + }, + { + label: "Los Angeles (GMT-7:00)", + value: "America/Los_Angeles", + }, + { + label: "Louisville (GMT-4:00)", + value: "America/Louisville", + }, + { + label: "Lower Princes (GMT-4:00)", + value: "America/Lower_Princes", + }, + { + label: "Maceio (GMT-3:00)", + value: "America/Maceio", + }, + { + label: "Managua (GMT-6:00)", + value: "America/Managua", + }, + { + label: "Manaus (GMT-4:00)", + value: "America/Manaus", + }, + { + label: "Marengo (GMT-4:00)", + value: "America/Indiana/Marengo", + }, + { + label: "Marigot (GMT-4:00)", + value: "America/Marigot", + }, + { + label: "Martinique (GMT-4:00)", + value: "America/Martinique", + }, + { + label: "Matamoros (GMT-5:00)", + value: "America/Matamoros", + }, + { + label: "Mazatlan (GMT-7:00)", + value: "America/Mazatlan", + }, + { + label: "Mendoza (GMT-3:00)", + value: "America/Mendoza", + }, + { + label: "Menominee (GMT-5:00)", + value: "America/Menominee", + }, + { + label: "Merida (GMT-6:00)", + value: "America/Merida", + }, + { + label: "Metlakatla (GMT-8:00)", + value: "America/Metlakatla", + }, + { + label: "Mexico City (GMT-6:00)", + value: "America/Mexico_City", + }, + { + label: "Miquelon (GMT-2:00)", + value: "America/Miquelon", + }, + { + label: "Moncton (GMT-3:00)", + value: "America/Moncton", + }, + { + label: "Monterrey (GMT-6:00)", + value: "America/Monterrey", + }, + { + label: "Montevideo (GMT-3:00)", + value: "America/Montevideo", + }, + { + label: "Monticello (GMT-4:00)", + value: "America/Kentucky/Monticello", + }, + { + label: "Montserrat (GMT-4:00)", + value: "America/Montserrat", + }, + { + label: "Nassau (GMT-4:00)", + value: "America/Nassau", + }, + { + label: "New Salem (GMT-5:00)", + value: "America/North_Dakota/New_Salem", + }, + { + label: "New York (GMT-4:00)", + value: "America/New_York", + }, + { + label: "Nome (GMT-8:00)", + value: "America/Nome", + }, + { + label: "Noronha (GMT-2:00)", + value: "America/Noronha", + }, + { + label: "Ojinaga (GMT-5:00)", + value: "America/Ojinaga", + }, + { + label: "Panama (GMT-5:00)", + value: "America/Panama", + }, + { + label: "Paramaribo (GMT-3:00)", + value: "America/Paramaribo", + }, + { + label: "Petersburg (GMT-4:00)", + value: "America/Indiana/Petersburg", + }, + { + label: "Phoenix (GMT-7:00)", + value: "America/Phoenix", + }, + { + label: "Port of Spain (GMT-4:00)", + value: "America/Port_of_Spain", + }, + { + label: "Port-au-Prince (GMT-4:00)", + value: "America/Port-au-Prince", + }, + { + label: "Porto Velho (GMT-4:00)", + value: "America/Porto_Velho", + }, + { + label: "Puerto Rico (GMT-4:00)", + value: "America/Puerto_Rico", + }, + { + label: "Punta Arenas (GMT-3:00)", + value: "America/Punta_Arenas", + }, + { + label: "Rankin Inlet (GMT-5:00)", + value: "America/Rankin_Inlet", + }, + { + label: "Recife (GMT-3:00)", + value: "America/Recife", + }, + { + label: "Regina (GMT-6:00)", + value: "America/Regina", + }, + { + label: "Resolute (GMT-5:00)", + value: "America/Resolute", + }, + { + label: "Rio Branco (GMT-5:00)", + value: "America/Rio_Branco", + }, + { + label: "Rio Gallegos (GMT-3:00)", + value: "America/Argentina/Rio_Gallegos", + }, + { + label: "Salta (GMT-3:00)", + value: "America/Argentina/Salta", + }, + { + label: "San Juan (GMT-3:00)", + value: "America/Argentina/San_Juan", + }, + { + label: "San Luis (GMT-3:00)", + value: "America/Argentina/San_Luis", + }, + { + label: "Santarem (GMT-3:00)", + value: "America/Santarem", + }, + { + label: "Santiago (GMT-4:00)", + value: "America/Santiago", + }, + { + label: "Santo Domingo (GMT-4:00)", + value: "America/Santo_Domingo", + }, + { + label: "Sao Paulo (GMT-3:00)", + value: "America/Sao_Paulo", + }, + { + label: "Scoresbysund (GMT-1:00)", + value: "America/Scoresbysund", + }, + { + label: "Sitka (GMT-8:00)", + value: "America/Sitka", + }, + { + label: "St Barthelemy (GMT-4:00)", + value: "America/St_Barthelemy", + }, + { + label: "St Johns (GMT-2:30)", + value: "America/St_Johns", + }, + { + label: "St Kitts (GMT-4:00)", + value: "America/St_Kitts", + }, + { + label: "St Lucia (GMT-4:00)", + value: "America/St_Lucia", + }, + { + label: "St Thomas (GMT-4:00)", + value: "America/St_Thomas", + }, + { + label: "St Vincent (GMT-4:00)", + value: "America/St_Vincent", + }, + { + label: "Swift Current (GMT-6:00)", + value: "America/Swift_Current", + }, + { + label: "Tegucigalpa (GMT-6:00)", + value: "America/Tegucigalpa", + }, + { + label: "Tell City (GMT-5:00)", + value: "America/Indiana/Tell_City", + }, + { + label: "Thule (GMT-3:00)", + value: "America/Thule", + }, + { + label: "Tijuana (GMT-7:00)", + value: "America/Tijuana", + }, + { + label: "Toronto (GMT-4:00)", + value: "America/Toronto", + }, + { + label: "Tortola (GMT-4:00)", + value: "America/Tortola", + }, + { + label: "Tucuman (GMT-3:00)", + value: "America/Argentina/Tucuman", + }, + { + label: "Ushuaia (GMT-3:00)", + value: "America/Argentina/Ushuaia", + }, + { + label: "Vancouver (GMT-7:00)", + value: "America/Vancouver", + }, + { + label: "Vevay (GMT-4:00)", + value: "America/Indiana/Vevay", + }, + { + label: "Vincennes (GMT-4:00)", + value: "America/Indiana/Vincennes", + }, + { + label: "Whitehorse (GMT-7:00)", + value: "America/Whitehorse", + }, + { + label: "Winamac (GMT-4:00)", + value: "America/Indiana/Winamac", + }, + { + label: "Winnipeg (GMT-5:00)", + value: "America/Winnipeg", + }, + { + label: "Yakutat (GMT-8:00)", + value: "America/Yakutat", + }, + ], + }, + { + label: "Antarctica", + options: [ + { + label: "Casey (GMT+8:00)", + value: "Antarctica/Casey", + }, + { + label: "Davis (GMT+7:00)", + value: "Antarctica/Davis", + }, + { + label: "DumontDUrville (GMT+10:00)", + value: "Antarctica/DumontDUrville", + }, + { + label: "Macquarie (GMT+10:00)", + value: "Antarctica/Macquarie", + }, + { + label: "Mawson (GMT+5:00)", + value: "Antarctica/Mawson", + }, + { + label: "McMurdo (GMT+12:00)", + value: "Antarctica/McMurdo", + }, + { + label: "Palmer (GMT-3:00)", + value: "Antarctica/Palmer", + }, + { + label: "Rothera (GMT-3:00)", + value: "Antarctica/Rothera", + }, + { + label: "Syowa (GMT+3:00)", + value: "Antarctica/Syowa", + }, + { + label: "Troll (GMT+2:00)", + value: "Antarctica/Troll", + }, + { + label: "Vostok (GMT+5:00)", + value: "Antarctica/Vostok", + }, + ], + }, + { + label: "Arctic", + options: [ + { + label: "Longyearbyen (GMT+2:00)", + value: "Arctic/Longyearbyen", + }, + ], + }, + { + label: "Asia", + options: [ + { + label: "Aden (GMT+3:00)", + value: "Asia/Aden", + }, + { + label: "Almaty (GMT+5:00)", + value: "Asia/Almaty", + }, + { + label: "Amman (GMT+3:00)", + value: "Asia/Amman", + }, + { + label: "Anadyr (GMT+12:00)", + value: "Asia/Anadyr", + }, + { + label: "Aqtau (GMT+5:00)", + value: "Asia/Aqtau", + }, + { + label: "Aqtobe (GMT+5:00)", + value: "Asia/Aqtobe", + }, + { + label: "Ashgabat (GMT+5:00)", + value: "Asia/Ashgabat", + }, + { + label: "Atyrau (GMT+5:00)", + value: "Asia/Atyrau", + }, + { + label: "Baghdad (GMT+3:00)", + value: "Asia/Baghdad", + }, + { + label: "Bahrain (GMT+3:00)", + value: "Asia/Bahrain", + }, + { + label: "Baku (GMT+4:00)", + value: "Asia/Baku", + }, + { + label: "Bangkok (GMT+7:00)", + value: "Asia/Bangkok", + }, + { + label: "Barnaul (GMT+7:00)", + value: "Asia/Barnaul", + }, + { + label: "Beirut (GMT+3:00)", + value: "Asia/Beirut", + }, + { + label: "Bishkek (GMT+6:00)", + value: "Asia/Bishkek", + }, + { + label: "Brunei (GMT+8:00)", + value: "Asia/Brunei", + }, + { + label: "Calcutta (GMT+5:30)", + value: "Asia/Calcutta", + }, + { + label: "Chita (GMT+9:00)", + value: "Asia/Chita", + }, + { + label: "Colombo (GMT+5:30)", + value: "Asia/Colombo", + }, + { + label: "Damascus (GMT+3:00)", + value: "Asia/Damascus", + }, + { + label: "Dhaka (GMT+6:00)", + value: "Asia/Dhaka", + }, + { + label: "Dili (GMT+9:00)", + value: "Asia/Dili", + }, + { + label: "Dubai (GMT+4:00)", + value: "Asia/Dubai", + }, + { + label: "Dushanbe (GMT+5:00)", + value: "Asia/Dushanbe", + }, + { + label: "Famagusta (GMT+3:00)", + value: "Asia/Famagusta", + }, + { + label: "Gaza (GMT+3:00)", + value: "Asia/Gaza", + }, + { + label: "Hebron (GMT+3:00)", + value: "Asia/Hebron", + }, + { + label: "Hong Kong (GMT+8:00)", + value: "Asia/Hong_Kong", + }, + { + label: "Hovd (GMT+7:00)", + value: "Asia/Hovd", + }, + { + label: "Irkutsk (GMT+8:00)", + value: "Asia/Irkutsk", + }, + { + label: "Jakarta (GMT+7:00)", + value: "Asia/Jakarta", + }, + { + label: "Jayapura (GMT+9:00)", + value: "Asia/Jayapura", + }, + { + label: "Jerusalem (GMT+3:00)", + value: "Asia/Jerusalem", + }, + { + label: "Kabul (GMT+4:30)", + value: "Asia/Kabul", + }, + { + label: "Kamchatka (GMT+12:00)", + value: "Asia/Kamchatka", + }, + { + label: "Karachi (GMT+5:00)", + value: "Asia/Karachi", + }, + { + label: "Katmandu (GMT+5:45)", + value: "Asia/Katmandu", + }, + { + label: "Khandyga (GMT+9:00)", + value: "Asia/Khandyga", + }, + { + label: "Krasnoyarsk (GMT+7:00)", + value: "Asia/Krasnoyarsk", + }, + { + label: "Kuala Lumpur (GMT+8:00)", + value: "Asia/Kuala_Lumpur", + }, + { + label: "Kuching (GMT+8:00)", + value: "Asia/Kuching", + }, + { + label: "Kuwait (GMT+3:00)", + value: "Asia/Kuwait", + }, + { + label: "Macau (GMT+8:00)", + value: "Asia/Macau", + }, + { + label: "Magadan (GMT+11:00)", + value: "Asia/Magadan", + }, + { + label: "Makassar (GMT+8:00)", + value: "Asia/Makassar", + }, + { + label: "Manila (GMT+8:00)", + value: "Asia/Manila", + }, + { + label: "Muscat (GMT+4:00)", + value: "Asia/Muscat", + }, + { + label: "Nicosia (GMT+3:00)", + value: "Asia/Nicosia", + }, + { + label: "Novokuznetsk (GMT+7:00)", + value: "Asia/Novokuznetsk", + }, + { + label: "Novosibirsk (GMT+7:00)", + value: "Asia/Novosibirsk", + }, + { + label: "Omsk (GMT+6:00)", + value: "Asia/Omsk", + }, + { + label: "Oral (GMT+5:00)", + value: "Asia/Oral", + }, + { + label: "Phnom Penh (GMT+7:00)", + value: "Asia/Phnom_Penh", + }, + { + label: "Pontianak (GMT+7:00)", + value: "Asia/Pontianak", + }, + { + label: "Pyongyang (GMT+9:00)", + value: "Asia/Pyongyang", + }, + { + label: "Qatar (GMT+3:00)", + value: "Asia/Qatar", + }, + { + label: "Qostanay (GMT+5:00)", + value: "Asia/Qostanay", + }, + { + label: "Qyzylorda (GMT+5:00)", + value: "Asia/Qyzylorda", + }, + { + label: "Rangoon (GMT+6:30)", + value: "Asia/Rangoon", + }, + { + label: "Riyadh (GMT+3:00)", + value: "Asia/Riyadh", + }, + { + label: "Saigon (GMT+7:00)", + value: "Asia/Saigon", + }, + { + label: "Sakhalin (GMT+11:00)", + value: "Asia/Sakhalin", + }, + { + label: "Samarkand (GMT+5:00)", + value: "Asia/Samarkand", + }, + { + label: "Seoul (GMT+9:00)", + value: "Asia/Seoul", + }, + { + label: "Shanghai (GMT+8:00)", + value: "Asia/Shanghai", + }, + { + label: "Singapore (GMT+8:00)", + value: "Asia/Singapore", + }, + { + label: "Srednekolymsk (GMT+11:00)", + value: "Asia/Srednekolymsk", + }, + { + label: "Taipei (GMT+8:00)", + value: "Asia/Taipei", + }, + { + label: "Tashkent (GMT+5:00)", + value: "Asia/Tashkent", + }, + { + label: "Tbilisi (GMT+4:00)", + value: "Asia/Tbilisi", + }, + { + label: "Tehran (GMT+3:30)", + value: "Asia/Tehran", + }, + { + label: "Thimphu (GMT+6:00)", + value: "Asia/Thimphu", + }, + { + label: "Tokyo (GMT+9:00)", + value: "Asia/Tokyo", + }, + { + label: "Tomsk (GMT+7:00)", + value: "Asia/Tomsk", + }, + { + label: "Ulaanbaatar (GMT+8:00)", + value: "Asia/Ulaanbaatar", + }, + { + label: "Urumqi (GMT+6:00)", + value: "Asia/Urumqi", + }, + { + label: "Ust-Nera (GMT+10:00)", + value: "Asia/Ust-Nera", + }, + { + label: "Vientiane (GMT+7:00)", + value: "Asia/Vientiane", + }, + { + label: "Vladivostok (GMT+10:00)", + value: "Asia/Vladivostok", + }, + { + label: "Yakutsk (GMT+9:00)", + value: "Asia/Yakutsk", + }, + { + label: "Yekaterinburg (GMT+5:00)", + value: "Asia/Yekaterinburg", + }, + { + label: "Yerevan (GMT+4:00)", + value: "Asia/Yerevan", + }, + ], + }, + { + label: "Atlantic", + options: [ + { + label: "Azores (GMT0:00)", + value: "Atlantic/Azores", + }, + { + label: "Bermuda (GMT-3:00)", + value: "Atlantic/Bermuda", + }, + { + label: "Canary (GMT+1:00)", + value: "Atlantic/Canary", + }, + { + label: "Cape Verde (GMT-1:00)", + value: "Atlantic/Cape_Verde", + }, + { + label: "Faeroe (GMT+1:00)", + value: "Atlantic/Faeroe", + }, + { + label: "Madeira (GMT+1:00)", + value: "Atlantic/Madeira", + }, + { + label: "Reykjavik (GMT0:00)", + value: "Atlantic/Reykjavik", + }, + { + label: "South Georgia (GMT-2:00)", + value: "Atlantic/South_Georgia", + }, + { + label: "St Helena (GMT0:00)", + value: "Atlantic/St_Helena", + }, + { + label: "Stanley (GMT-3:00)", + value: "Atlantic/Stanley", + }, + ], + }, + { + label: "Australia", + options: [ + { + label: "Adelaide (GMT+9:30)", + value: "Australia/Adelaide", + }, + { + label: "Brisbane (GMT+10:00)", + value: "Australia/Brisbane", + }, + { + label: "Broken Hill (GMT+9:30)", + value: "Australia/Broken_Hill", + }, + { + label: "Darwin (GMT+9:30)", + value: "Australia/Darwin", + }, + { + label: "Eucla (GMT+8:45)", + value: "Australia/Eucla", + }, + { + label: "Hobart (GMT+10:00)", + value: "Australia/Hobart", + }, + { + label: "Lindeman (GMT+10:00)", + value: "Australia/Lindeman", + }, + { + label: "Lord Howe (GMT+10:30)", + value: "Australia/Lord_Howe", + }, + { + label: "Melbourne (GMT+10:00)", + value: "Australia/Melbourne", + }, + { + label: "Perth (GMT+8:00)", + value: "Australia/Perth", + }, + { + label: "Sydney (GMT+10:00)", + value: "Australia/Sydney", + }, + ], + }, + { + label: "Europe", + options: [ + { + label: "Amsterdam (GMT+2:00)", + value: "Europe/Amsterdam", + }, + { + label: "Andorra (GMT+2:00)", + value: "Europe/Andorra", + }, + { + label: "Astrakhan (GMT+4:00)", + value: "Europe/Astrakhan", + }, + { + label: "Athens (GMT+3:00)", + value: "Europe/Athens", + }, + { + label: "Belgrade (GMT+2:00)", + value: "Europe/Belgrade", + }, + { + label: "Berlin (GMT+2:00)", + value: "Europe/Berlin", + }, + { + label: "Bratislava (GMT+2:00)", + value: "Europe/Bratislava", + }, + { + label: "Brussels (GMT+2:00)", + value: "Europe/Brussels", + }, + { + label: "Bucharest (GMT+3:00)", + value: "Europe/Bucharest", + }, + { + label: "Budapest (GMT+2:00)", + value: "Europe/Budapest", + }, + { + label: "Busingen (GMT+2:00)", + value: "Europe/Busingen", + }, + { + label: "Chisinau (GMT+3:00)", + value: "Europe/Chisinau", + }, + { + label: "Copenhagen (GMT+2:00)", + value: "Europe/Copenhagen", + }, + { + label: "Dublin (GMT+1:00)", + value: "Europe/Dublin", + }, + { + label: "Gibraltar (GMT+2:00)", + value: "Europe/Gibraltar", + }, + { + label: "Guernsey (GMT+1:00)", + value: "Europe/Guernsey", + }, + { + label: "Helsinki (GMT+3:00)", + value: "Europe/Helsinki", + }, + { + label: "Isle of Man (GMT+1:00)", + value: "Europe/Isle_of_Man", + }, + { + label: "Istanbul (GMT+3:00)", + value: "Europe/Istanbul", + }, + { + label: "Jersey (GMT+1:00)", + value: "Europe/Jersey", + }, + { + label: "Kaliningrad (GMT+2:00)", + value: "Europe/Kaliningrad", + }, + { + label: "Kiev (GMT+3:00)", + value: "Europe/Kiev", + }, + { + label: "Kirov (GMT+3:00)", + value: "Europe/Kirov", + }, + { + label: "Lisbon (GMT+1:00)", + value: "Europe/Lisbon", + }, + { + label: "Ljubljana (GMT+2:00)", + value: "Europe/Ljubljana", + }, + { + label: "London (GMT+1:00)", + value: "Europe/London", + }, + { + label: "Luxembourg (GMT+2:00)", + value: "Europe/Luxembourg", + }, + { + label: "Madrid (GMT+2:00)", + value: "Europe/Madrid", + }, + { + label: "Malta (GMT+2:00)", + value: "Europe/Malta", + }, + { + label: "Mariehamn (GMT+3:00)", + value: "Europe/Mariehamn", + }, + { + label: "Minsk (GMT+3:00)", + value: "Europe/Minsk", + }, + { + label: "Monaco (GMT+2:00)", + value: "Europe/Monaco", + }, + { + label: "Moscow (GMT+3:00)", + value: "Europe/Moscow", + }, + { + label: "Oslo (GMT+2:00)", + value: "Europe/Oslo", + }, + { + label: "Paris (GMT+2:00)", + value: "Europe/Paris", + }, + { + label: "Podgorica (GMT+2:00)", + value: "Europe/Podgorica", + }, + { + label: "Prague (GMT+2:00)", + value: "Europe/Prague", + }, + { + label: "Riga (GMT+3:00)", + value: "Europe/Riga", + }, + { + label: "Rome (GMT+2:00)", + value: "Europe/Rome", + }, + { + label: "Samara (GMT+4:00)", + value: "Europe/Samara", + }, + { + label: "San Marino (GMT+2:00)", + value: "Europe/San_Marino", + }, + { + label: "Sarajevo (GMT+2:00)", + value: "Europe/Sarajevo", + }, + { + label: "Saratov (GMT+4:00)", + value: "Europe/Saratov", + }, + { + label: "Simferopol (GMT+3:00)", + value: "Europe/Simferopol", + }, + { + label: "Skopje (GMT+2:00)", + value: "Europe/Skopje", + }, + { + label: "Sofia (GMT+3:00)", + value: "Europe/Sofia", + }, + { + label: "Stockholm (GMT+2:00)", + value: "Europe/Stockholm", + }, + { + label: "Tallinn (GMT+3:00)", + value: "Europe/Tallinn", + }, + { + label: "Tirane (GMT+2:00)", + value: "Europe/Tirane", + }, + { + label: "Ulyanovsk (GMT+4:00)", + value: "Europe/Ulyanovsk", + }, + { + label: "Vaduz (GMT+2:00)", + value: "Europe/Vaduz", + }, + { + label: "Vatican (GMT+2:00)", + value: "Europe/Vatican", + }, + { + label: "Vienna (GMT+2:00)", + value: "Europe/Vienna", + }, + { + label: "Vilnius (GMT+3:00)", + value: "Europe/Vilnius", + }, + { + label: "Volgograd (GMT+3:00)", + value: "Europe/Volgograd", + }, + { + label: "Warsaw (GMT+2:00)", + value: "Europe/Warsaw", + }, + { + label: "Zagreb (GMT+2:00)", + value: "Europe/Zagreb", + }, + { + label: "Zurich (GMT+2:00)", + value: "Europe/Zurich", + }, + ], + }, + { + label: "Indian", + options: [ + { + label: "Antananarivo (GMT+3:00)", + value: "Indian/Antananarivo", + }, + { + label: "Chagos (GMT+6:00)", + value: "Indian/Chagos", + }, + { + label: "Christmas (GMT+7:00)", + value: "Indian/Christmas", + }, + { + label: "Cocos (GMT+6:30)", + value: "Indian/Cocos", + }, + { + label: "Comoro (GMT+3:00)", + value: "Indian/Comoro", + }, + { + label: "Kerguelen (GMT+5:00)", + value: "Indian/Kerguelen", + }, + { + label: "Mahe (GMT+4:00)", + value: "Indian/Mahe", + }, + { + label: "Maldives (GMT+5:00)", + value: "Indian/Maldives", + }, + { + label: "Mauritius (GMT+4:00)", + value: "Indian/Mauritius", + }, + { + label: "Mayotte (GMT+3:00)", + value: "Indian/Mayotte", + }, + { + label: "Reunion (GMT+4:00)", + value: "Indian/Reunion", + }, + ], + }, + { + label: "Pacific", + options: [ + { + label: "Apia (GMT+13:00)", + value: "Pacific/Apia", + }, + { + label: "Auckland (GMT+12:00)", + value: "Pacific/Auckland", + }, + { + label: "Bougainville (GMT+11:00)", + value: "Pacific/Bougainville", + }, + { + label: "Chatham (GMT+12:45)", + value: "Pacific/Chatham", + }, + { + label: "Easter (GMT-6:00)", + value: "Pacific/Easter", + }, + { + label: "Efate (GMT+11:00)", + value: "Pacific/Efate", + }, + { + label: "Enderbury (GMT+13:00)", + value: "Pacific/Enderbury", + }, + { + label: "Fakaofo (GMT+13:00)", + value: "Pacific/Fakaofo", + }, + { + label: "Fiji (GMT+12:00)", + value: "Pacific/Fiji", + }, + { + label: "Funafuti (GMT+12:00)", + value: "Pacific/Funafuti", + }, + { + label: "Galapagos (GMT-6:00)", + value: "Pacific/Galapagos", + }, + { + label: "Gambier (GMT-9:00)", + value: "Pacific/Gambier", + }, + { + label: "Guadalcanal (GMT+11:00)", + value: "Pacific/Guadalcanal", + }, + { + label: "Guam (GMT+10:00)", + value: "Pacific/Guam", + }, + { + label: "Honolulu (GMT-10:00)", + value: "Pacific/Honolulu", + }, + { + label: "Kiritimati (GMT+14:00)", + value: "Pacific/Kiritimati", + }, + { + label: "Kosrae (GMT+11:00)", + value: "Pacific/Kosrae", + }, + { + label: "Kwajalein (GMT+12:00)", + value: "Pacific/Kwajalein", + }, + { + label: "Majuro (GMT+12:00)", + value: "Pacific/Majuro", + }, + { + label: "Marquesas (GMT-9:30)", + value: "Pacific/Marquesas", + }, + { + label: "Midway (GMT-11:00)", + value: "Pacific/Midway", + }, + { + label: "Nauru (GMT+12:00)", + value: "Pacific/Nauru", + }, + { + label: "Niue (GMT-11:00)", + value: "Pacific/Niue", + }, + { + label: "Norfolk (GMT+11:00)", + value: "Pacific/Norfolk", + }, + { + label: "Noumea (GMT+11:00)", + value: "Pacific/Noumea", + }, + { + label: "Pago Pago (GMT-11:00)", + value: "Pacific/Pago_Pago", + }, + { + label: "Palau (GMT+9:00)", + value: "Pacific/Palau", + }, + { + label: "Pitcairn (GMT-8:00)", + value: "Pacific/Pitcairn", + }, + { + label: "Ponape (GMT+11:00)", + value: "Pacific/Ponape", + }, + { + label: "Port Moresby (GMT+10:00)", + value: "Pacific/Port_Moresby", + }, + { + label: "Rarotonga (GMT-10:00)", + value: "Pacific/Rarotonga", + }, + { + label: "Saipan (GMT+10:00)", + value: "Pacific/Saipan", + }, + { + label: "Tahiti (GMT-10:00)", + value: "Pacific/Tahiti", + }, + { + label: "Tarawa (GMT+12:00)", + value: "Pacific/Tarawa", + }, + { + label: "Tongatapu (GMT+13:00)", + value: "Pacific/Tongatapu", + }, + { + label: "Truk (GMT+10:00)", + value: "Pacific/Truk", + }, + { + label: "Wake (GMT+12:00)", + value: "Pacific/Wake", + }, + { + label: "Wallis (GMT+12:00)", + value: "Pacific/Wallis", + }, + ], + }, +]; diff --git a/app/_types/date-range-types.tsx b/app/_types/date-range-types.tsx new file mode 100644 index 00000000..5b0ebabd --- /dev/null +++ b/app/_types/date-range-types.tsx @@ -0,0 +1,17 @@ +// _types/date-range-types.ts + +import { EventRange, WeekdayMap } from "@/app/_types/schedule-types"; + +export type DateRangeProps = { + eventRange?: EventRange; + onChangeEventRange?: (range: EventRange) => void; + displayCalendar?: boolean; + + // optional legacy support props + rangeType?: "specific" | "weekday"; + onChangeRangeType?: (type: "specific" | "weekday") => void; + specificRange?: { from: Date | null; to: Date | null }; + onChangeSpecific?: (key: "from" | "to", value: Date) => void; + weekdayRange?: WeekdayMap; + onChangeWeekday?: (map: WeekdayMap) => void; +}; diff --git a/app/_types/schedule-types.tsx b/app/_types/schedule-types.tsx new file mode 100644 index 00000000..24f73644 --- /dev/null +++ b/app/_types/schedule-types.tsx @@ -0,0 +1,115 @@ +export type TimeDateRange = { + from: Date | null; + to: Date | null; +}; + +// generic weekday mode map +export type WeekdayMap = { + [day in "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat"]: 0 | 1; +}; + +// specific date mode +export type SpecificDateRange = { + type: "specific"; + duration: number; + timezone: string; + dateRange: TimeDateRange; + timeRange: TimeDateRange; +}; + +// generic weekday mode +export type WeekdayRange = { + type: "weekday"; + duration: number; + timezone: string; + weekdays: WeekdayMap; + timeRange: TimeDateRange; +}; + +// unified type +export type EventRange = SpecificDateRange | WeekdayRange; + +export type DateTimeSlot = { + day: string; + from: Date; + to: Date; +}; + +// Combine a date and a time-only object into a full Date object +export function combineDateAndTime(date: Date, time: Date): Date { + const result = new Date(date); + result.setHours(time.getHours(), time.getMinutes(), 0, 0); + return result; +} + +// Generate all concrete weekday instances for a given reference week +export function generateConcreteInstancesForWeek( + range: WeekdayRange, + weekStart: Date, +): DateTimeSlot[] { + const slots: DateTimeSlot[] = []; + + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const baseFrom = range.timeRange.from; + const baseTo = range.timeRange.to; + + if (!baseFrom || !baseTo) return []; + + for (let i = 0; i < 7; i++) { + const day = days[i] as keyof WeekdayMap; + if (range.weekdays[day]) { + const date = new Date(weekStart); + date.setDate(weekStart.getDate() + i); + + const from = combineDateAndTime(date, baseFrom); + const to = combineDateAndTime(date, baseTo); + + slots.push({ day, from, to }); + } + } + + return slots; +} + +// Expand a specific date range into a list of dates (e.g., for previewing day-by-day) +export function expandDateRange(range: TimeDateRange): Date[] { + const { from, to } = range; + if (!from || !to) return []; + const days: Date[] = []; + + let current = new Date(from); + current.setHours(0, 0, 0, 0); + + const end = new Date(to); + end.setHours(0, 0, 0, 0); + + while (current <= end) { + days.push(new Date(current)); + current.setDate(current.getDate() + 1); + } + + return days; +} + +// Format a weekday map into a list of active days (e.g., ["Mon", "Wed"]) +export function getEnabledWeekdays(weekdays: WeekdayMap): string[] { + return Object.entries(weekdays) + .filter(([, v]) => v === 1) + .map(([k]) => k); +} + +// Create a list of visible day labels from a specific date range +export function getDateLabels({ from, to }: TimeDateRange): string[] { + const labels: string[] = []; + const range = expandDateRange({ from, to }); + for (const date of range) { + const weekday = date + .toLocaleDateString("en-US", { weekday: "short" }) + .toUpperCase(); + const monthDay = date + .toLocaleDateString("en-US", { month: "short", day: "numeric" }) + .toUpperCase(); + labels.push(`${weekday} ${monthDay}`); + } + return labels; +} diff --git a/app/_types/user-availability.tsx b/app/_types/user-availability.tsx new file mode 100644 index 00000000..7be7009c --- /dev/null +++ b/app/_types/user-availability.tsx @@ -0,0 +1,72 @@ +// keys are either weekdays or specific date strings: +// for specific days: "2025-05-10" +// for weekdays: "Mon", "Tue", etc. +// values are sets timeslots where the user is available +export type AvailabilityMap = Record>; + +export type UserAvailability = { + type: "specific" | "weekday"; + selections: AvailabilityMap; +}; + +// Initialize empty availability +export const createEmptyUserAvailability = ( + type: "specific" | "weekday" = "specific", +): UserAvailability => ({ + type, + selections: {}, +}); + +// Toggle a cell's availability (add/remove hour from Set) +export function toggleAvailability( + prev: UserAvailability, + key: string, + hour: number, +): UserAvailability { + const updated = { ...prev, selections: { ...prev.selections } }; + const hours = new Set(updated.selections[key] || []); + + if (hours.has(hour)) { + hours.delete(hour); + } else { + hours.add(hour); + } + + updated.selections[key] = hours; + return updated; +} + +// Check if a user is available at a specific key + hour +export function isAvailable( + user: UserAvailability, + key: string, + hour: number, +): boolean { + return user.selections[key]?.has(hour) ?? false; +} + +// Add multiple hours for drag selection +export function addAvailability( + prev: UserAvailability, + key: string, + hour: number, +): UserAvailability { + const updated = { ...prev, selections: { ...prev.selections } }; + const hours = new Set(updated.selections[key] || []); + hours.add(hour); + updated.selections[key] = hours; + return updated; +} + +// Remove hours for drag deselection (optional if you want toggling behavior) +export function removeAvailability( + prev: UserAvailability, + key: string, + hour: number, +): UserAvailability { + const updated = { ...prev, selections: { ...prev.selections } }; + const hours = new Set(updated.selections[key] || []); + hours.delete(hour); + updated.selections[key] = hours; + return updated; +} diff --git a/app/_utils/providers.tsx b/app/_utils/providers.tsx new file mode 100644 index 00000000..d0da32bd --- /dev/null +++ b/app/_utils/providers.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { ThemeProvider } from "next-themes"; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/_utils/timezone-file-generator.tsx b/app/_utils/timezone-file-generator.tsx new file mode 100644 index 00000000..95398cb9 --- /dev/null +++ b/app/_utils/timezone-file-generator.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "react"; + +function formatLabel(tz: string): string { + try { + const now = new Date(); + const offsetMinutes = -now.getTimezoneOffset(); + const offset = + new Intl.DateTimeFormat("en-US", { + timeZone: tz, + hour12: false, + hour: "2-digit", + minute: "2-digit", + timeZoneName: "shortOffset", + }) + .formatToParts(now) + .find((p) => p.type === "timeZoneName")?.value || ""; + + // Try to normalize to GMT+/-HH:MM + const match = offset.match(/GMT([+-]\d{1,2})(?::(\d{2}))?/); + const hours = match?.[1] ?? "0"; + const minutes = match?.[2] ?? "00"; + const fullOffset = `GMT${hours}:${minutes}`; + + const city = tz.split("/").slice(-1)[0].replaceAll("_", " "); + return `${city} (${fullOffset})`; + } catch { + return tz; + } +} + +function buildGroupedTimezones() { + const groups: Record = {}; + const timezones = Intl.supportedValuesOf("timeZone"); + + for (const tz of timezones) { + const [region = "Other"] = tz.split("/"); + const label = formatLabel(tz); + if (!groups[region]) groups[region] = []; + groups[region].push({ label, value: tz }); + } + + return Object.entries(groups) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([region, options]) => ({ + label: region, + options: options.sort((a, b) => a.label.localeCompare(b.label)), + })); +} + +export default function TimezoneFileGenerator() { + const [output, setOutput] = useState(""); + + useEffect(() => { + const grouped = buildGroupedTimezones(); + const fileContent = + "// Auto-generated timezone list\nexport const groupedTimezones = " + + JSON.stringify(grouped, null, 2) + + ";\n"; + setOutput(fileContent); + }, []); + + return ( +
+

Timezone List Generator

+