diff --git a/frontend/components/Profile/ProfileSettings.tsx b/frontend/components/Profile/ProfileSettings.tsx index f481312..46a62f7 100644 --- a/frontend/components/Profile/ProfileSettings.tsx +++ b/frontend/components/Profile/ProfileSettings.tsx @@ -30,6 +30,11 @@ import { dispatchTelegramStatusChange } from '../../contexts/TelegramStatusConte import LanguageDropdown from '../Shared/LanguageDropdown'; import FirstDayOfWeekDropdown from '../Shared/FirstDayOfWeekDropdown'; import { getLocaleFirstDayOfWeek } from '../../utils/profileService'; +import { + getTimezonesByRegion, + getRegionDisplayName, +} from '../../utils/timezoneUtils'; +import TimezoneDropdown from '../Shared/TimezoneDropdown'; interface ProfileSettingsProps { currentUser: { uid: string; email: string }; @@ -88,6 +93,11 @@ const ProfileSettings: React.FC = ({ const { showSuccessToast, showErrorToast } = useToast(); const [activeTab, setActiveTab] = useState('general'); + // Generate timezone list using date-fns-tz and Intl API + const timezonesByRegion = React.useMemo(() => { + return getTimezonesByRegion(); + }, []); + // Password visibility state const [showCurrentPassword, setShowCurrentPassword] = useState(false); @@ -954,258 +964,17 @@ const ProfileSettings: React.FC = ({ - + + setFormData((prev) => ({ + ...prev, + timezone, + })) + } + timezonesByRegion={timezonesByRegion} + getRegionDisplayName={getRegionDisplayName} + />
diff --git a/frontend/components/Shared/TimezoneDropdown.tsx b/frontend/components/Shared/TimezoneDropdown.tsx new file mode 100644 index 0000000..22b51fb --- /dev/null +++ b/frontend/components/Shared/TimezoneDropdown.tsx @@ -0,0 +1,197 @@ +import React, { useState, useRef, useEffect, useMemo } from 'react'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import type { TimezoneOption } from '../../utils/timezoneUtils'; + +interface TimezoneDropdownProps { + value: string; + onChange: (timezone: string) => void; + timezonesByRegion: Record; + getRegionDisplayName: (region: string) => string; +} + +const TimezoneDropdown: React.FC = ({ + value, + onChange, + timezonesByRegion, + getRegionDisplayName, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const dropdownRef = useRef(null); + const searchInputRef = useRef(null); + + // Flatten all timezones into a single searchable list + const allTimezones = useMemo(() => { + const flattened: (TimezoneOption & { regionName: string })[] = []; + Object.keys(timezonesByRegion).forEach((region) => { + timezonesByRegion[region].forEach((tz) => { + flattened.push({ + ...tz, + regionName: getRegionDisplayName(region), + }); + }); + }); + return flattened; + }, [timezonesByRegion, getRegionDisplayName]); + + // Get the current timezone display label + const selectedTimezone = useMemo(() => { + if (value === 'UTC') return 'UTC'; + const found = allTimezones.find((tz) => tz.value === value); + return found ? found.label : value; + }, [value, allTimezones]); + + // Filter timezones based on search query + const filteredTimezones = useMemo(() => { + if (!searchQuery.trim()) return allTimezones; + + const query = searchQuery.toLowerCase(); + return allTimezones.filter( + (tz) => + tz.label.toLowerCase().includes(query) || + tz.value.toLowerCase().includes(query) || + tz.regionName.toLowerCase().includes(query) + ); + }, [searchQuery, allTimezones]); + + // Group filtered timezones by region + const groupedFilteredTimezones = useMemo(() => { + const grouped: Record = {}; + filteredTimezones.forEach((tz) => { + if (!grouped[tz.regionName]) { + grouped[tz.regionName] = []; + } + grouped[tz.regionName].push(tz); + }); + return grouped; + }, [filteredTimezones]); + + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + setSearchQuery(''); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // Focus search input when dropdown opens + useEffect(() => { + if (isOpen && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, [isOpen]); + + const handleSelect = (timezone: string) => { + onChange(timezone); + setIsOpen(false); + setSearchQuery(''); + }; + + return ( +
+ {/* Dropdown trigger button */} + + + {/* Dropdown menu */} + {isOpen && ( +
+ {/* Search input */} +
+ setSearchQuery(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ + {/* Timezone list */} +
+ {/* UTC option */} + {(!searchQuery || 'utc'.includes(searchQuery.toLowerCase())) && ( + + )} + + {/* Grouped timezones */} + {Object.keys(groupedFilteredTimezones) + .sort() + .map((regionName) => ( +
+
+ {regionName} +
+ {groupedFilteredTimezones[regionName].map( + (tz) => ( + + ) + )} +
+ ))} + + {/* No results message */} + {filteredTimezones.length === 0 && + searchQuery && + 'utc'.includes(searchQuery.toLowerCase()) === + false && ( +
+ No timezones found matching " + {searchQuery}" +
+ )} +
+
+ )} +
+ ); +}; + +export default TimezoneDropdown; diff --git a/frontend/utils/timezoneUtils.ts b/frontend/utils/timezoneUtils.ts new file mode 100644 index 0000000..657f371 --- /dev/null +++ b/frontend/utils/timezoneUtils.ts @@ -0,0 +1,220 @@ +import { formatInTimeZone } from 'date-fns-tz'; + +export interface TimezoneOption { + value: string; + label: string; + offset: string; + region: string; +} + +/** + * Get all IANA timezone identifiers + * Uses Intl.supportedValuesOf which is supported in modern browsers + */ +const getAllTimezones = (): string[] => { + // Use Intl.supportedValuesOf if available (modern browsers) + if (typeof Intl !== 'undefined' && 'supportedValuesOf' in Intl) { + return (Intl as any).supportedValuesOf('timeZone'); + } + + // Fallback: Return a comprehensive list of major timezones + // This fallback ensures compatibility with older browsers + return [ + // Africa + 'Africa/Abidjan', + 'Africa/Accra', + 'Africa/Algiers', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Johannesburg', + 'Africa/Lagos', + 'Africa/Nairobi', + 'Africa/Tunis', + // America + 'America/Anchorage', + 'America/Argentina/Buenos_Aires', + 'America/Bogota', + 'America/Caracas', + 'America/Chicago', + 'America/Denver', + 'America/Edmonton', + 'America/Halifax', + 'America/Lima', + 'America/Los_Angeles', + 'America/Mexico_City', + 'America/New_York', + 'America/Phoenix', + 'America/Regina', + 'America/Santiago', + 'America/Sao_Paulo', + 'America/St_Johns', + 'America/Toronto', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Winnipeg', + // Asia + 'Asia/Bangkok', + 'Asia/Dhaka', + 'Asia/Dubai', + 'Asia/Hong_Kong', + 'Asia/Jakarta', + 'Asia/Jerusalem', + 'Asia/Karachi', + 'Asia/Kolkata', + 'Asia/Kuala_Lumpur', + 'Asia/Manila', + 'Asia/Riyadh', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Taipei', + 'Asia/Tehran', + 'Asia/Tokyo', + // Atlantic + 'Atlantic/Azores', + 'Atlantic/Reykjavik', + // Australia + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Darwin', + 'Australia/Hobart', + 'Australia/Melbourne', + 'Australia/Perth', + 'Australia/Sydney', + // Europe + 'Europe/Amsterdam', + 'Europe/Athens', + 'Europe/Berlin', + 'Europe/Brussels', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Helsinki', + 'Europe/Istanbul', + 'Europe/Lisbon', + 'Europe/London', + 'Europe/Madrid', + 'Europe/Moscow', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Prague', + 'Europe/Rome', + 'Europe/Stockholm', + 'Europe/Vienna', + 'Europe/Warsaw', + 'Europe/Zurich', + // Pacific + 'Pacific/Auckland', + 'Pacific/Fiji', + 'Pacific/Guam', + 'Pacific/Honolulu', + ]; +}; + +/** + * Get current UTC offset for a timezone + */ +const getTimezoneOffset = (tz: string): string => { + try { + const now = new Date(); + // Format the date in the target timezone and extract offset + const formatted = formatInTimeZone(now, tz, 'XXX'); // Returns like "+05:30" or "-08:00" + return formatted; + } catch { + return '+00:00'; + } +}; + +/** + * Get all available timezones grouped by region + * @returns Object with region names as keys and timezone arrays as values + */ +export const getTimezonesByRegion = (): Record< + string, + TimezoneOption[] +> => { + const timezones = getAllTimezones(); + const grouped: Record = {}; + + timezones.forEach((tz) => { + // Skip deprecated/alias timezones and special cases + if ( + tz.includes('Etc/') || + tz === 'Factory' || + tz.includes('SystemV/') + ) { + return; + } + + const parts = tz.split('/'); + if (parts.length < 2) return; + + const region = parts[0]; + const location = parts.slice(1).join('/').replace(/_/g, ' '); + + // Get current offset for the timezone + const offset = getTimezoneOffset(tz); + + const option: TimezoneOption = { + value: tz, + label: `${location} (UTC${offset})`, + offset, + region, + }; + + if (!grouped[region]) { + grouped[region] = []; + } + + grouped[region].push(option); + }); + + // Sort timezones within each region by offset, then by name + Object.keys(grouped).forEach((region) => { + grouped[region].sort((a, b) => { + // First sort by offset + const offsetA = parseOffset(a.offset); + const offsetB = parseOffset(b.offset); + if (offsetA !== offsetB) { + return offsetB - offsetA; // Descending (west to east) + } + // Then by label + return a.label.localeCompare(b.label); + }); + }); + + return grouped; +}; + +/** + * Parse offset string (e.g., "+05:30") to minutes + */ +const parseOffset = (offset: string): number => { + const match = offset.match(/([+-])(\d{2}):(\d{2})/); + if (!match) return 0; + + const sign = match[1] === '+' ? 1 : -1; + const hours = parseInt(match[2], 10); + const minutes = parseInt(match[3], 10); + + return sign * (hours * 60 + minutes); +}; + +/** + * Get region display name for grouping + */ +export const getRegionDisplayName = (region: string): string => { + const regionNames: Record = { + America: 'Americas', + Europe: 'Europe', + Asia: 'Asia', + Africa: 'Africa', + Australia: 'Australia & Oceania', + Pacific: 'Pacific Islands', + Atlantic: 'Atlantic', + Indian: 'Indian Ocean', + Arctic: 'Arctic', + Antarctica: 'Antarctica', + }; + + return regionNames[region] || region; +}; diff --git a/package-lock.json b/package-lock.json index db43ea7..d127ef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tududi", - "version": "v0.83-rc8", + "version": "v0.84", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tududi", - "version": "v0.83-rc8", + "version": "v0.84", "license": "ISC", "dependencies": { "@heroicons/react": "^2.1.5", @@ -17,6 +17,7 @@ "connect-session-sequelize": "~7.1.7", "cors": "~2.8.5", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dotenv": "~16.5.0", "eslint-plugin-react": "^7.37.5", "express": "^4.21.2", @@ -46,8 +47,6 @@ "sequelize": "~6.37.7", "slugify": "^1.6.6", "sqlite3": "~5.1.7", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", "swr": "^2.2.5", "tagify": "^0.1.1", "typescript-eslint": "^8.36.0", @@ -133,50 +132,6 @@ "node": ">=6.0.0" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "license": "MIT" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2913,12 +2868,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", @@ -3199,13 +3148,6 @@ "node": ">=14.0.0" } }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3945,6 +3887,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/mdast": { @@ -5865,12 +5808,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6947,6 +6884,15 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -12507,20 +12453,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -12534,12 +12466,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "license": "MIT" - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -14610,13 +14536,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT", - "peer": true - }, "node_modules/optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", @@ -18039,101 +17958,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "license": "MIT", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.29.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.3.tgz", - "integrity": "sha512-U99f/2YocRA2Mxqx3eUBRhQonWVtE5dIvMs0Zlsn4a4ip8awMq0JxXhU+Sidtna2WlZrHbK2Rro3RZvYUymRbA==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, "node_modules/swc-loader": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", @@ -20115,36 +19939,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/zustand": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.6.tgz", diff --git a/package.json b/package.json index 06d68c0..d198202 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "connect-session-sequelize": "~7.1.7", "cors": "~2.8.5", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dotenv": "~16.5.0", "eslint-plugin-react": "^7.37.5", "express": "^4.21.2", diff --git a/scripts/update-timezones.js b/scripts/update-timezones.js new file mode 100755 index 0000000..a06ffc8 --- /dev/null +++ b/scripts/update-timezones.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(__dirname, '../frontend/components/Profile/ProfileSettings.tsx'); +let content = fs.readFileSync(filePath, 'utf8'); + +// Step 1: Add imports after the existing imports +const importToAdd = `import { + getTimezonesByRegion, + getRegionDisplayName, +} from '../../utils/timezoneUtils';`; + +// Find the last import statement before interfaces +const lastImportMatch = content.match(/(import.*from.*';[\r\n]+)(\r\n)(interface)/); +if (lastImportMatch) { + content = content.replace( + lastImportMatch[0], + `${lastImportMatch[1]}${importToAdd}\n${lastImportMatch[2]}${lastImportMatch[3]}` + ); +} + +// Step 2: Add useMemo after const [activeTab... +const useMemoToAdd = ` + // Generate timezone list using date-fns-tz and Intl API + const timezonesByRegion = React.useMemo(() => { + return getTimezonesByRegion(); + }, []); +`; + +content = content.replace( + /(const \[activeTab, setActiveTab\] = useState\('general'\);)/, + `$1${useMemoToAdd}` +); + +// Step 3: Replace the entire hardcoded timezone section +// Find the start: followed by {/* Americas */} +// Find the end: last before + +const timezoneReplacement = ` + + {/* Dynamically generated timezone list */} + {Object.keys(timezonesByRegion) + .sort() + .map((region) => ( + + {timezonesByRegion[region].map( + (tz) => ( + + ) + )} + + ))}`; + +// Match from to the last before +const timezonePattern = /(