feat(web): стандартизированный справочник стран и городов в @cfdm/shared/geo
- 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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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({
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="start" className="w-[--anchor-width] p-0">
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[--anchor-width] p-0"
|
||||
initialFocus={false}
|
||||
>
|
||||
<Command shouldFilter={false} loop>
|
||||
<div className="flex items-center gap-2 border-b px-3">
|
||||
<SearchIcon className="size-4 text-muted-foreground" />
|
||||
<CommandInput
|
||||
placeholder={searchPlaceholder}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
className="h-9"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground truncate flex-1">
|
||||
{value.trim() || 'Введите для поиска'}
|
||||
</span>
|
||||
</div>
|
||||
<CommandList>
|
||||
<CommandEmpty>{emptyText}</CommandEmpty>
|
||||
@@ -115,7 +110,6 @@ export function AutoCompleteInput({
|
||||
value={opt.value}
|
||||
onSelect={() => {
|
||||
onChange(opt.value)
|
||||
setQuery('')
|
||||
setOpen(false)
|
||||
}}
|
||||
className="gap-2"
|
||||
|
||||
@@ -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<string, string> = {
|
||||
direct_vps_payment: 'Прямой платеж за VPS',
|
||||
provider_balance_topup: 'Пополнение баланса хостера',
|
||||
|
||||
@@ -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<string>()
|
||||
// Из существующих 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<string>()
|
||||
// Из существующих 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) {
|
||||
|
||||
@@ -115,6 +115,10 @@ export interface ActiveTariff {
|
||||
diskType?: string
|
||||
monthlyRate?: number
|
||||
currency?: string
|
||||
datacenterKey?: string
|
||||
datacenterName?: string
|
||||
location?: string
|
||||
country?: string
|
||||
}
|
||||
|
||||
export interface SyncLogRow {
|
||||
|
||||
@@ -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/*"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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<string, CountryRef> = Object.fromEntries(
|
||||
COUNTRIES.map((c) => [c.code, c]),
|
||||
)
|
||||
|
||||
export const COUNTRY_BY_NAME_RU: Record<string, CountryRef> = Object.fromEntries(
|
||||
COUNTRIES.map((c) => [c.name.toLowerCase(), c]),
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './countries.js'
|
||||
export * from './cities.js'
|
||||
Reference in New Issue
Block a user