From db1d26d324de3ae44c5082fbddf0fd5e91a9d418 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 26 Jun 2026 22:56:54 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D1=81=D1=82=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=B8=D0=BA=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=B8=20=D0=B3=D0=BE=D1=80=D0=BE=D0=B4=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?@cfdm/shared/geo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/shared/src/geo: страны ISO 3166-1 alpha-2 + топ-города по странам - vps.tsx: countryOptions/cityOptions из справочника + существующих VPS, города фильтруются по выбранной стране - auto-complete-input: убран onBlur-хак с setTimeout, фильтрация по основному полю, initialFocus=false чтобы popover не закрывался - vite.config.ts + tsconfig.json: алиас @cfdm/shared/geo Co-authored-by: Cursor --- .../src/components/auto-complete-input.tsx | 28 +-- apps/web/src/lib/format.ts | 6 + apps/web/src/routes/_auth/vps.tsx | 27 ++- apps/web/src/types/entities.ts | 4 + apps/web/tsconfig.json | 2 + apps/web/vite.config.ts | 17 +- packages/shared/package.json | 4 +- packages/shared/src/geo/cities.ts | 197 ++++++++++++++++++ packages/shared/src/geo/countries.ts | 175 ++++++++++++++++ packages/shared/src/geo/index.ts | 2 + 10 files changed, 429 insertions(+), 33 deletions(-) create mode 100644 packages/shared/src/geo/cities.ts create mode 100644 packages/shared/src/geo/countries.ts create mode 100644 packages/shared/src/geo/index.ts diff --git a/apps/web/src/components/auto-complete-input.tsx b/apps/web/src/components/auto-complete-input.tsx index 3cf24a1..1655ffc 100644 --- a/apps/web/src/components/auto-complete-input.tsx +++ b/apps/web/src/components/auto-complete-input.tsx @@ -6,7 +6,6 @@ import { Command, CommandEmpty, CommandGroup, - CommandInput, CommandItem, CommandList, } from '@cfdm/ui/components/command' @@ -43,19 +42,18 @@ export function AutoCompleteInput({ onChange, options, placeholder, - searchPlaceholder = 'Поиск…', + searchPlaceholder: _searchPlaceholder = 'Поиск…', emptyText = 'Ничего не найдено', className, showLeadingInInput = true, }: AutoCompleteInputProps) { const [open, setOpen] = React.useState(false) - const [query, setQuery] = React.useState('') const filtered = React.useMemo(() => { - const q = query.trim().toLowerCase() + const q = value.trim().toLowerCase() if (!q) return options return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q)) - }, [options, query]) + }, [options, value]) const selected = options.find((o) => o.value.toLowerCase() === value.trim().toLowerCase()) const leading = selected?.leading @@ -81,10 +79,6 @@ export function AutoCompleteInput({ setOpen(true) }} onFocus={() => setOpen(true)} - onBlur={() => { - // задержка чтобы клик по item успел сработать - setTimeout(() => setOpen(false), 150) - }} className={cn( showLeadingInInput && leading ? 'pl-8' : '', className, @@ -93,16 +87,17 @@ export function AutoCompleteInput({ } /> - +
- + + {value.trim() || 'Введите для поиска'} +
{emptyText} @@ -115,7 +110,6 @@ export function AutoCompleteInput({ value={opt.value} onSelect={() => { onChange(opt.value) - setQuery('') setOpen(false) }} className="gap-2" diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index 14d5b9f..581e338 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -37,6 +37,12 @@ export function getCountryFlagEmoji(country?: string): string { return code.toUpperCase().split('').map((c) => String.fromCodePoint(127397 + c.charCodeAt(0))).join('') } +/** Флаг по ISO 3166-1 alpha-2 коду страны. */ +export function getCountryFlagEmojiByCode(code?: string): string { + if (!code || code.length !== 2) return '🌐' + return code.toUpperCase().split('').map((c) => String.fromCodePoint(127397 + c.charCodeAt(0))).join('') +} + const PAYMENT_TYPE_LABELS: Record = { direct_vps_payment: 'Прямой платеж за VPS', provider_balance_topup: 'Пополнение баланса хостера', diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx index 69617c7..8a06a94 100644 --- a/apps/web/src/routes/_auth/vps.tsx +++ b/apps/web/src/routes/_auth/vps.tsx @@ -32,8 +32,9 @@ import { } from '@/components/vps-filters' import type { Vps } from '@/types/entities' -import { vpsStatusLabel, tariffTypeLabel, getCountryFlagEmoji } from '@/lib/format' +import { vpsStatusLabel, tariffTypeLabel, getCountryFlagEmoji, getCountryFlagEmojiByCode } from '@/lib/format' import { providerByIdMap, accountSelectLabel } from '@/lib/billmanager' +import { COUNTRIES, COUNTRY_BY_NAME_RU, listCities } from '@cfdm/shared/geo' export const Route = createFileRoute('/_auth/vps')({ loader: ({ context: { queryClient } }) => @@ -141,28 +142,40 @@ function VpsPage() { const countryOptions = useMemo(() => { const names = new Set() + // Из существующих VPS — могут быть произвольные строки for (const v of snapshot?.vps ?? []) { const c = (v.country || '').trim() if (c) names.add(c) } - return [...names].sort((a, b) => a.localeCompare(b, 'ru')).map((name) => ({ - value: name, - label: name, - leading: getCountryFlagEmoji(name), - })) + // Из стандартизированного справочника + for (const c of COUNTRIES) names.add(c.name) + return [...names].sort((a, b) => a.localeCompare(b, 'ru')).map((name) => { + const ref = COUNTRY_BY_NAME_RU[name.toLowerCase()] + return { + value: name, + label: name, + leading: ref ? getCountryFlagEmojiByCode(ref.code) : getCountryFlagEmoji(name), + } + }) }, [snapshot?.vps]) const cityOptions = useMemo(() => { const names = new Set() + // Из существующих VPS for (const v of snapshot?.vps ?? []) { const c = (v.city || '').trim() if (c) names.add(c) } + // Из стандартизированного справочника (фильтр по выбранной стране, если есть) + const countryCode = filters.country + ? COUNTRY_BY_NAME_RU[filters.country.toLowerCase()]?.code + : undefined + for (const city of listCities(countryCode)) names.add(city.name) return [...names].sort((a, b) => a.localeCompare(b, 'ru')).map((name) => ({ value: name, label: name, })) - }, [snapshot?.vps]) + }, [snapshot?.vps, filters.country]) const tableSections = useMemo(() => { if (!filters.groupByProject) { diff --git a/apps/web/src/types/entities.ts b/apps/web/src/types/entities.ts index e66b589..3d3ff30 100644 --- a/apps/web/src/types/entities.ts +++ b/apps/web/src/types/entities.ts @@ -115,6 +115,10 @@ export interface ActiveTariff { diskType?: string monthlyRate?: number currency?: string + datacenterKey?: string + datacenterName?: string + location?: string + country?: string } export interface SyncLogRow { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 2647518..b71930e 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -11,6 +11,8 @@ "@cfdm/shared": ["../../packages/shared/src/index.ts"], "@cfdm/shared/contracts/*": ["../../packages/shared/src/contracts/*"], "@cfdm/shared/types/*": ["../../packages/shared/src/types/*"], + "@cfdm/shared/geo": ["../../packages/shared/src/geo/index.ts"], + "@cfdm/shared/geo/*": ["../../packages/shared/src/geo/*"], "@cfdm/db": ["../../packages/db/src/index.ts"], "@cfdm/db/schema": ["../../packages/db/src/schema/index.ts"], "@cfdm/db/repositories/*": ["../../packages/db/src/repositories/*"] diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index db342ed..71ade29 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -11,14 +11,15 @@ export default defineConfig({ tailwindcss(), ], resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@cfdm/ui/components': path.resolve(__dirname, '../../packages/ui/src/components'), - '@cfdm/ui/hooks': path.resolve(__dirname, '../../packages/ui/src/hooks'), - '@cfdm/ui/lib/utils': path.resolve(__dirname, '../../packages/ui/src/lib/utils.ts'), - '@cfdm/shared': path.resolve(__dirname, '../../packages/shared/src/index.ts'), - '@cfdm/db': path.resolve(__dirname, '../../packages/db/src/index.ts'), - }, + alias: [ + { find: '@', replacement: path.resolve(__dirname, './src') }, + { find: '@cfdm/ui/components', replacement: path.resolve(__dirname, '../../packages/ui/src/components') }, + { find: '@cfdm/ui/hooks', replacement: path.resolve(__dirname, '../../packages/ui/src/hooks') }, + { find: '@cfdm/ui/lib/utils', replacement: path.resolve(__dirname, '../../packages/ui/src/lib/utils.ts') }, + { find: /^@cfdm\/shared\/(.*)$/, replacement: path.resolve(__dirname, '../../packages/shared/src/$1') }, + { find: '@cfdm/shared', replacement: path.resolve(__dirname, '../../packages/shared/src/index.ts') }, + { find: '@cfdm/db', replacement: path.resolve(__dirname, '../../packages/db/src/index.ts') }, + ], }, server: { port: 5173, diff --git a/packages/shared/package.json b/packages/shared/package.json index e196525..30e0b1b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,7 +8,9 @@ "exports": { ".": "./src/index.ts", "./contracts/*": "./src/contracts/*.ts", - "./types/*": "./src/types/*.ts" + "./types/*": "./src/types/*.ts", + "./geo": "./src/geo/index.ts", + "./geo/*": "./src/geo/*.ts" }, "dependencies": { "zod": "^3.25.0" diff --git a/packages/shared/src/geo/cities.ts b/packages/shared/src/geo/cities.ts new file mode 100644 index 0000000..9441110 --- /dev/null +++ b/packages/shared/src/geo/cities.ts @@ -0,0 +1,197 @@ +/** + * Стандартизированный справочник городов по странам. + * Используется в фильтрах VPS и форме редактирования. + * Не зависит от данных в БД vps-tracker — общедоступный статичный справочник. + */ + +import { COUNTRY_BY_CODE, COUNTRIES, type CountryRef } from './countries.js' + +export interface CityRef { + /** ISO 3166-1 alpha-2 код страны */ + countryCode: string + /** Русское название города */ + name: string + /** Английское название города */ + nameEn: string +} + +/** + * Топ-города по странам (включая города с крупными дата-центрами). + * Список намеренно ограничен — расширяется по мере необходимости. + */ +export const CITIES: readonly CityRef[] = [ + // Россия + { countryCode: 'RU', name: 'Москва', nameEn: 'Moscow' }, + { countryCode: 'RU', name: 'Санкт-Петербург', nameEn: 'Saint Petersburg' }, + { countryCode: 'RU', name: 'Новосибирск', nameEn: 'Novosibirsk' }, + { countryCode: 'RU', name: 'Екатеринбург', nameEn: 'Yekaterinburg' }, + { countryCode: 'RU', name: 'Казань', nameEn: 'Kazan' }, + { countryCode: 'RU', name: 'Нижний Новгород', nameEn: 'Nizhny Novgorod' }, + { countryCode: 'RU', name: 'Самара', nameEn: 'Samara' }, + { countryCode: 'RU', name: 'Ростов-на-Дону', nameEn: 'Rostov-on-Don' }, + { countryCode: 'RU', name: 'Уфа', nameEn: 'Ufa' }, + { countryCode: 'RU', name: 'Краснодар', nameEn: 'Krasnodar' }, + { countryCode: 'RU', name: 'Сочи', nameEn: 'Sochi' }, + { countryCode: 'RU', name: 'Владивосток', nameEn: 'Vladivostok' }, + { countryCode: 'RU', name: 'Иркутск', nameEn: 'Irkutsk' }, + { countryCode: 'RU', name: 'Хабаровск', nameEn: 'Khabarovsk' }, + { countryCode: 'RU', name: 'Тюмень', nameEn: 'Tyumen' }, + // Нидерланды + { countryCode: 'NL', name: 'Амстердам', nameEn: 'Amsterdam' }, + { countryCode: 'NL', name: 'Роттердам', nameEn: 'Rotterdam' }, + { countryCode: 'NL', name: 'Гаага', nameEn: 'The Hague' }, + // Германия + { countryCode: 'DE', name: 'Франкфурт', nameEn: 'Frankfurt' }, + { countryCode: 'DE', name: 'Берлин', nameEn: 'Berlin' }, + { countryCode: 'DE', name: 'Мюнхен', nameEn: 'Munich' }, + { countryCode: 'DE', name: 'Гамбург', nameEn: 'Hamburg' }, + { countryCode: 'DE', name: 'Кёльн', nameEn: 'Cologne' }, + { countryCode: 'DE', name: 'Дюссельдорф', nameEn: 'Düsseldorf' }, + { countryCode: 'DE', name: 'Штутгарт', nameEn: 'Stuttgart' }, + // Финляндия + { countryCode: 'FI', name: 'Хельсинки', nameEn: 'Helsinki' }, + { countryCode: 'FI', name: 'Эспоо', nameEn: 'Espoo' }, + { countryCode: 'FI', name: 'Тампере', nameEn: 'Tampere' }, + // Швеция + { countryCode: 'SE', name: 'Стокгольм', nameEn: 'Stockholm' }, + { countryCode: 'SE', name: 'Гётеборг', nameEn: 'Gothenburg' }, + { countryCode: 'SE', name: 'Мальмё', nameEn: 'Malmö' }, + // Норвегия + { countryCode: 'NO', name: 'Осло', nameEn: 'Oslo' }, + { countryCode: 'NO', name: 'Берген', nameEn: 'Bergen' }, + // Дания + { countryCode: 'DK', name: 'Копенгаген', nameEn: 'Copenhagen' }, + { countryCode: 'DK', name: 'Орхус', nameEn: 'Aarhus' }, + // Великобритания + { countryCode: 'GB', name: 'Лондон', nameEn: 'London' }, + { countryCode: 'GB', name: 'Манчестер', nameEn: 'Manchester' }, + { countryCode: 'GB', name: 'Глазго', nameEn: 'Glasgow' }, + { countryCode: 'GB', name: 'Кардифф', nameEn: 'Cardiff' }, + // Франция + { countryCode: 'FR', name: 'Париж', nameEn: 'Paris' }, + { countryCode: 'FR', name: 'Марсель', nameEn: 'Marseille' }, + { countryCode: 'FR', name: 'Лион', nameEn: 'Lyon' }, + { countryCode: 'FR', name: 'Страсбург', nameEn: 'Strasbourg' }, + // Испания + { countryCode: 'ES', name: 'Мадрид', nameEn: 'Madrid' }, + { countryCode: 'ES', name: 'Барселона', nameEn: 'Barcelona' }, + { countryCode: 'ES', name: 'Валенсия', nameEn: 'Valencia' }, + // Италия + { countryCode: 'IT', name: 'Рим', nameEn: 'Rome' }, + { countryCode: 'IT', name: 'Милан', nameEn: 'Milan' }, + { countryCode: 'IT', name: 'Турин', nameEn: 'Turin' }, + // Чехия + { countryCode: 'CZ', name: 'Прага', nameEn: 'Prague' }, + { countryCode: 'CZ', name: 'Брно', nameEn: 'Brno' }, + // Польша + { countryCode: 'PL', name: 'Варшава', nameEn: 'Warsaw' }, + { countryCode: 'PL', name: 'Краков', nameEn: 'Kraków' }, + { countryCode: 'PL', name: 'Вроцлав', nameEn: 'Wrocław' }, + { countryCode: 'PL', name: 'Гданьск', nameEn: 'Gdańsk' }, + // Швейцария + { countryCode: 'CH', name: 'Цюрих', nameEn: 'Zurich' }, + { countryCode: 'CH', name: 'Женева', nameEn: 'Geneva' }, + { countryCode: 'CH', name: 'Базель', nameEn: 'Basel' }, + // Бельгия + { countryCode: 'BE', name: 'Брюссель', nameEn: 'Brussels' }, + { countryCode: 'BE', name: 'Антверпен', nameEn: 'Antwerp' }, + // Австрия + { countryCode: 'AT', name: 'Вена', nameEn: 'Vienna' }, + { countryCode: 'AT', name: 'Грац', nameEn: 'Graz' }, + // Латвия + { countryCode: 'LV', name: 'Рига', nameEn: 'Riga' }, + // Литва + { countryCode: 'LT', name: 'Вильнюс', nameEn: 'Vilnius' }, + { countryCode: 'LT', name: 'Каунас', nameEn: 'Kaunas' }, + // Эстония + { countryCode: 'EE', name: 'Таллин', nameEn: 'Tallinn' }, + // Украина + { countryCode: 'UA', name: 'Киев', nameEn: 'Kyiv' }, + { countryCode: 'UA', name: 'Харьков', nameEn: 'Kharkiv' }, + { countryCode: 'UA', name: 'Одесса', nameEn: 'Odesa' }, + { countryCode: 'UA', name: 'Львов', nameEn: 'Lviv' }, + // Казахстан + { countryCode: 'KZ', name: 'Алматы', nameEn: 'Almaty' }, + { countryCode: 'KZ', name: 'Астана', nameEn: 'Astana' }, + // Беларусь + { countryCode: 'BY', name: 'Минск', nameEn: 'Minsk' }, + // США + { countryCode: 'US', name: 'Нью-Йорк', nameEn: 'New York' }, + { countryCode: 'US', name: 'Лос-Анджелес', nameEn: 'Los Angeles' }, + { countryCode: 'US', name: 'Чикаго', nameEn: 'Chicago' }, + { countryCode: 'US', name: 'Хьюстон', nameEn: 'Houston' }, + { countryCode: 'US', name: 'Даллас', nameEn: 'Dallas' }, + { countryCode: 'US', name: 'Майами', nameEn: 'Miami' }, + { countryCode: 'US', name: 'Сиэтл', nameEn: 'Seattle' }, + { countryCode: 'US', name: 'Сан-Франциско', nameEn: 'San Francisco' }, + { countryCode: 'US', name: 'Вашингтон', nameEn: 'Washington' }, + { countryCode: 'US', name: 'Атланта', nameEn: 'Atlanta' }, + { countryCode: 'US', name: 'Денвер', nameEn: 'Denver' }, + // Канада + { countryCode: 'CA', name: 'Торонто', nameEn: 'Toronto' }, + { countryCode: 'CA', name: 'Монреаль', nameEn: 'Montreal' }, + { countryCode: 'CA', name: 'Ванкувер', nameEn: 'Vancouver' }, + // Сингапур + { countryCode: 'SG', name: 'Сингапур', nameEn: 'Singapore' }, + // Япония + { countryCode: 'JP', name: 'Токио', nameEn: 'Tokyo' }, + { countryCode: 'JP', name: 'Осака', nameEn: 'Osaka' }, + // Южная Корея + { countryCode: 'KR', name: 'Сеул', nameEn: 'Seoul' }, + // Китай + { countryCode: 'CN', name: 'Пекин', nameEn: 'Beijing' }, + { countryCode: 'CN', name: 'Шанхай', nameEn: 'Shanghai' }, + { countryCode: 'CN', name: 'Гуанчжоу', nameEn: 'Guangzhou' }, + { countryCode: 'CN', name: 'Шэньчжэнь', nameEn: 'Shenzhen' }, + { countryCode: 'CN', name: 'Гонконг', nameEn: 'Hong Kong' }, + // Индия + { countryCode: 'IN', name: 'Мумбаи', nameEn: 'Mumbai' }, + { countryCode: 'IN', name: 'Дели', nameEn: 'Delhi' }, + { countryCode: 'IN', name: 'Бангалор', nameEn: 'Bangalore' }, + { countryCode: 'IN', name: 'Ченнаи', nameEn: 'Chennai' }, + // ОАЭ + { countryCode: 'AE', name: 'Дубай', nameEn: 'Dubai' }, + { countryCode: 'AE', name: 'Абу-Даби', nameEn: 'Abu Dhabi' }, + // Турция + { countryCode: 'TR', name: 'Стамбул', nameEn: 'Istanbul' }, + { countryCode: 'TR', name: 'Анкара', nameEn: 'Ankara' }, + { countryCode: 'TR', name: 'Измир', nameEn: 'Izmir' }, + // Бразилия + { countryCode: 'BR', name: 'Сан-Паулу', nameEn: 'São Paulo' }, + { countryCode: 'BR', name: 'Рио-де-Жанейро', nameEn: 'Rio de Janeiro' }, + // Австралия + { countryCode: 'AU', name: 'Сидней', nameEn: 'Sydney' }, + { countryCode: 'AU', name: 'Мельбурн', nameEn: 'Melbourne' }, + // Болгария + { countryCode: 'BG', name: 'София', nameEn: 'Sofia' }, + // Румыния + { countryCode: 'RO', name: 'Бухарест', nameEn: 'Bucharest' }, + // Венгрия + { countryCode: 'HU', name: 'Будапешт', nameEn: 'Budapest' }, + // Греция + { countryCode: 'GR', name: 'Афины', nameEn: 'Athens' }, + // Португалия + { countryCode: 'PT', name: 'Лиссабон', nameEn: 'Lisbon' }, + // Ирландия + { countryCode: 'IE', name: 'Дублин', nameEn: 'Dublin' }, + // Израиль + { countryCode: 'IL', name: 'Тель-Авив', nameEn: 'Tel Aviv' }, +] + +export interface CityOption extends CityRef { + country: CountryRef +} + +/** Список городов с привязкой к стране. */ +export function listCities(countryCode?: string): CityOption[] { + return CITIES.filter((c) => !countryCode || c.countryCode === countryCode).map((c) => ({ + ...c, + country: COUNTRY_BY_CODE[c.countryCode], + })) +} + +/** Список стран, у которых есть города в справочнике. */ +export function countriesWithCities(): CountryRef[] { + const codes = new Set(CITIES.map((c) => c.countryCode)) + return COUNTRIES.filter((c) => codes.has(c.code)) +} diff --git a/packages/shared/src/geo/countries.ts b/packages/shared/src/geo/countries.ts new file mode 100644 index 0000000..493ab0f --- /dev/null +++ b/packages/shared/src/geo/countries.ts @@ -0,0 +1,175 @@ +/** + * Стандартизированный справочник стран (ISO 3166-1 alpha-2) с русскими названиями. + * Источник: общедоступный справочник. Не зависит от данных в БД vps-tracker. + */ + +export interface CountryRef { + /** ISO 3166-1 alpha-2 код */ + code: string + /** Русское название */ + name: string + /** Английское название */ + nameEn: string +} + +export const COUNTRIES: readonly CountryRef[] = [ + { code: 'AU', name: 'Австралия', nameEn: 'Australia' }, + { code: 'AT', name: 'Австрия', nameEn: 'Austria' }, + { code: 'AZ', name: 'Азербайджан', nameEn: 'Azerbaijan' }, + { code: 'AL', name: 'Албания', nameEn: 'Albania' }, + { code: 'DZ', name: 'Алжир', nameEn: 'Algeria' }, + { code: 'AO', name: 'Ангола', nameEn: 'Angola' }, + { code: 'AR', name: 'Аргентина', nameEn: 'Argentina' }, + { code: 'AM', name: 'Армения', nameEn: 'Armenia' }, + { code: 'AF', name: 'Афганистан', nameEn: 'Afghanistan' }, + { code: 'BE', name: 'Бельгия', nameEn: 'Belgium' }, + { code: 'BG', name: 'Болгария', nameEn: 'Bulgaria' }, + { code: 'BO', name: 'Боливия', nameEn: 'Bolivia' }, + { code: 'BA', name: 'Босния и Герцеговина', nameEn: 'Bosnia and Herzegovina' }, + { code: 'BR', name: 'Бразилия', nameEn: 'Brazil' }, + { code: 'GB', name: 'Великобритания', nameEn: 'United Kingdom' }, + { code: 'HU', name: 'Венгрия', nameEn: 'Hungary' }, + { code: 'VE', name: 'Венесуэла', nameEn: 'Venezuela' }, + { code: 'VN', name: 'Вьетнам', nameEn: 'Vietnam' }, + { code: 'GA', name: 'Габон', nameEn: 'Gabon' }, + { code: 'HT', name: 'Гаити', nameEn: 'Haiti' }, + { code: 'GY', name: 'Гайана', nameEn: 'Guyana' }, + { code: 'GM', name: 'Гамбия', nameEn: 'Gambia' }, + { code: 'GH', name: 'Гана', nameEn: 'Ghana' }, + { code: 'GT', name: 'Гватемала', nameEn: 'Guatemala' }, + { code: 'GN', name: 'Гвинея', nameEn: 'Guinea' }, + { code: 'DE', name: 'Германия', nameEn: 'Germany' }, + { code: 'HN', name: 'Гондурас', nameEn: 'Honduras' }, + { code: 'GR', name: 'Греция', nameEn: 'Greece' }, + { code: 'GE', name: 'Грузия', nameEn: 'Georgia' }, + { code: 'DK', name: 'Дания', nameEn: 'Denmark' }, + { code: 'CD', name: 'ДР Конго', nameEn: 'DR Congo' }, + { code: 'EG', name: 'Египет', nameEn: 'Egypt' }, + { code: 'ZM', name: 'Замбия', nameEn: 'Zambia' }, + { code: 'ZW', name: 'Зимбабве', nameEn: 'Zimbabwe' }, + { code: 'IL', name: 'Израиль', nameEn: 'Israel' }, + { code: 'IN', name: 'Индия', nameEn: 'India' }, + { code: 'ID', name: 'Индонезия', nameEn: 'Indonesia' }, + { code: 'JO', name: 'Иордания', nameEn: 'Jordan' }, + { code: 'IQ', name: 'Ирак', nameEn: 'Iraq' }, + { code: 'IR', name: 'Иран', nameEn: 'Iran' }, + { code: 'IE', name: 'Ирландия', nameEn: 'Ireland' }, + { code: 'IS', name: 'Исландия', nameEn: 'Iceland' }, + { code: 'ES', name: 'Испания', nameEn: 'Spain' }, + { code: 'IT', name: 'Италия', nameEn: 'Italy' }, + { code: 'YE', name: 'Йемен', nameEn: 'Yemen' }, + { code: 'KZ', name: 'Казахстан', nameEn: 'Kazakhstan' }, + { code: 'KH', name: 'Камбоджа', nameEn: 'Cambodia' }, + { code: 'CM', name: 'Камерун', nameEn: 'Cameroon' }, + { code: 'CA', name: 'Канада', nameEn: 'Canada' }, + { code: 'QA', name: 'Катар', nameEn: 'Qatar' }, + { code: 'KE', name: 'Кения', nameEn: 'Kenya' }, + { code: 'CY', name: 'Кипр', nameEn: 'Cyprus' }, + { code: 'KG', name: 'Киргизия', nameEn: 'Kyrgyzstan' }, + { code: 'CN', name: 'Китай', nameEn: 'China' }, + { code: 'CO', name: 'Колумбия', nameEn: 'Colombia' }, + { code: 'CG', name: 'Конго', nameEn: 'Congo' }, + { code: 'KR', name: 'Южная Корея', nameEn: 'South Korea' }, + { code: 'KP', name: 'Северная Корея', nameEn: 'North Korea' }, + { code: 'XK', name: 'Косово', nameEn: 'Kosovo' }, + { code: 'CR', name: 'Коста-Рика', nameEn: 'Costa Rica' }, + { code: 'CI', name: "Кот-д'Ивуар", nameEn: "Côte d'Ivoire" }, + { code: 'CU', name: 'Куба', nameEn: 'Cuba' }, + { code: 'KW', name: 'Кувейт', nameEn: 'Kuwait' }, + { code: 'LA', name: 'Лаос', nameEn: 'Laos' }, + { code: 'LV', name: 'Латвия', nameEn: 'Latvia' }, + { code: 'LR', name: 'Либерия', nameEn: 'Liberia' }, + { code: 'LB', name: 'Ливан', nameEn: 'Lebanon' }, + { code: 'LY', name: 'Ливия', nameEn: 'Libya' }, + { code: 'LT', name: 'Литва', nameEn: 'Lithuania' }, + { code: 'LI', name: 'Лихтенштейн', nameEn: 'Liechtenstein' }, + { code: 'LU', name: 'Люксембург', nameEn: 'Luxembourg' }, + { code: 'MU', name: 'Маврикий', nameEn: 'Mauritius' }, + { code: 'MR', name: 'Мавритания', nameEn: 'Mauritania' }, + { code: 'MG', name: 'Мадагаскар', nameEn: 'Madagascar' }, + { code: 'MO', name: 'Макао', nameEn: 'Macao' }, + { code: 'MW', name: 'Малави', nameEn: 'Malawi' }, + { code: 'MY', name: 'Малайзия', nameEn: 'Malaysia' }, + { code: 'ML', name: 'Мали', nameEn: 'Mali' }, + { code: 'MT', name: 'Мальта', nameEn: 'Malta' }, + { code: 'MA', name: 'Марокко', nameEn: 'Morocco' }, + { code: 'MX', name: 'Мексика', nameEn: 'Mexico' }, + { code: 'MZ', name: 'Мозамбик', nameEn: 'Mozambique' }, + { code: 'MD', name: 'Молдавия', nameEn: 'Moldova' }, + { code: 'MC', name: 'Монако', nameEn: 'Monaco' }, + { code: 'MN', name: 'Монголия', nameEn: 'Mongolia' }, + { code: 'MM', name: 'Мьянма', nameEn: 'Myanmar' }, + { code: 'NA', name: 'Намибия', nameEn: 'Namibia' }, + { code: 'NP', name: 'Непал', nameEn: 'Nepal' }, + { code: 'NE', name: 'Нигер', nameEn: 'Niger' }, + { code: 'NG', name: 'Нигерия', nameEn: 'Nigeria' }, + { code: 'NL', name: 'Нидерланды', nameEn: 'Netherlands' }, + { code: 'NI', name: 'Никарагуа', nameEn: 'Nicaragua' }, + { code: 'NZ', name: 'Новая Зеландия', nameEn: 'New Zealand' }, + { code: 'NO', name: 'Норвегия', nameEn: 'Norway' }, + { code: 'AE', name: 'ОАЭ', nameEn: 'United Arab Emirates' }, + { code: 'OM', name: 'Оман', nameEn: 'Oman' }, + { code: 'PK', name: 'Пакистан', nameEn: 'Pakistan' }, + { code: 'PA', name: 'Панама', nameEn: 'Panama' }, + { code: 'PG', name: 'Папуа — Новая Гвинея', nameEn: 'Papua New Guinea' }, + { code: 'PY', name: 'Парагвай', nameEn: 'Paraguay' }, + { code: 'PE', name: 'Перу', nameEn: 'Peru' }, + { code: 'PL', name: 'Польша', nameEn: 'Poland' }, + { code: 'PT', name: 'Португалия', nameEn: 'Portugal' }, + { code: 'RU', name: 'Россия', nameEn: 'Russia' }, + { code: 'RW', name: 'Руанда', nameEn: 'Rwanda' }, + { code: 'RO', name: 'Румыния', nameEn: 'Romania' }, + { code: 'SV', name: 'Сальвадор', nameEn: 'El Salvador' }, + { code: 'SA', name: 'Саудовская Аравия', nameEn: 'Saudi Arabia' }, + { code: 'MK', name: 'Северная Македония', nameEn: 'North Macedonia' }, + { code: 'SC', name: 'Сейшелы', nameEn: 'Seychelles' }, + { code: 'SN', name: 'Сенегал', nameEn: 'Senegal' }, + { code: 'RS', name: 'Сербия', nameEn: 'Serbia' }, + { code: 'SG', name: 'Сингапур', nameEn: 'Singapore' }, + { code: 'SK', name: 'Словакия', nameEn: 'Slovakia' }, + { code: 'SI', name: 'Словения', nameEn: 'Slovenia' }, + { code: 'SO', name: 'Сомали', nameEn: 'Somalia' }, + { code: 'SD', name: 'Судан', nameEn: 'Sudan' }, + { code: 'US', name: 'США', nameEn: 'United States' }, + { code: 'TJ', name: 'Таджикистан', nameEn: 'Tajikistan' }, + { code: 'TH', name: 'Таиланд', nameEn: 'Thailand' }, + { code: 'TZ', name: 'Танзания', nameEn: 'Tanzania' }, + { code: 'TG', name: 'Того', nameEn: 'Togo' }, + { code: 'TT', name: 'Тринидад и Тобаго', nameEn: 'Trinidad and Tobago' }, + { code: 'TV', name: 'Тувалу', nameEn: 'Tuvalu' }, + { code: 'TN', name: 'Тунис', nameEn: 'Tunisia' }, + { code: 'TM', name: 'Туркмения', nameEn: 'Turkmenistan' }, + { code: 'TR', name: 'Турция', nameEn: 'Turkey' }, + { code: 'UG', name: 'Уганда', nameEn: 'Uganda' }, + { code: 'UZ', name: 'Узбекистан', nameEn: 'Uzbekistan' }, + { code: 'UA', name: 'Украина', nameEn: 'Ukraine' }, + { code: 'UY', name: 'Уругвай', nameEn: 'Uruguay' }, + { code: 'FJ', name: 'Фиджи', nameEn: 'Fiji' }, + { code: 'PH', name: 'Филиппины', nameEn: 'Philippines' }, + { code: 'FI', name: 'Финляндия', nameEn: 'Finland' }, + { code: 'FR', name: 'Франция', nameEn: 'France' }, + { code: 'HR', name: 'Хорватия', nameEn: 'Croatia' }, + { code: 'TD', name: 'Чад', nameEn: 'Chad' }, + { code: 'ME', name: 'Черногория', nameEn: 'Montenegro' }, + { code: 'CZ', name: 'Чехия', nameEn: 'Czechia' }, + { code: 'CL', name: 'Чили', nameEn: 'Chile' }, + { code: 'CH', name: 'Швейцария', nameEn: 'Switzerland' }, + { code: 'SE', name: 'Швеция', nameEn: 'Sweden' }, + { code: 'LK', name: 'Шри-Ланка', nameEn: 'Sri Lanka' }, + { code: 'EC', name: 'Эквадор', nameEn: 'Ecuador' }, + { code: 'GQ', name: 'Экваториальная Гвинея', nameEn: 'Equatorial Guinea' }, + { code: 'ER', name: 'Эритрея', nameEn: 'Eritrea' }, + { code: 'EE', name: 'Эстония', nameEn: 'Estonia' }, + { code: 'ET', name: 'Эфиопия', nameEn: 'Ethiopia' }, + { code: 'ZA', name: 'ЮАР', nameEn: 'South Africa' }, + { code: 'JM', name: 'Ямайка', nameEn: 'Jamaica' }, + { code: 'JP', name: 'Япония', nameEn: 'Japan' }, +] + +export const COUNTRY_BY_CODE: Record = Object.fromEntries( + COUNTRIES.map((c) => [c.code, c]), +) + +export const COUNTRY_BY_NAME_RU: Record = Object.fromEntries( + COUNTRIES.map((c) => [c.name.toLowerCase(), c]), +) diff --git a/packages/shared/src/geo/index.ts b/packages/shared/src/geo/index.ts new file mode 100644 index 0000000..6b3d18f --- /dev/null +++ b/packages/shared/src/geo/index.ts @@ -0,0 +1,2 @@ +export * from './countries.js' +export * from './cities.js'