diff --git a/src/pages/AdminRemnawave.tsx b/src/pages/AdminRemnawave.tsx index 3c6b53d..4f040f6 100644 --- a/src/pages/AdminRemnawave.tsx +++ b/src/pages/AdminRemnawave.tsx @@ -12,6 +12,7 @@ import { } from '../api/adminRemnawave'; import { usePlatform } from '../platform/hooks/usePlatform'; import { formatUptime } from '../utils/format'; +import { getFlagEmoji } from '../utils/subscriptionHelpers'; import Twemoji from 'react-twemoji'; import { ServerIcon, @@ -46,48 +47,9 @@ const formatBytes = (bytes: number): string => { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; -const getCountryFlag = (code: string | null | undefined): string => { - if (!code) return '🌍'; - const codeMap: Record = { - RU: '🇷🇺', - US: '🇺🇸', - DE: '🇩🇪', - NL: '🇳🇱', - GB: '🇬🇧', - UK: '🇬🇧', - FR: '🇫🇷', - FI: '🇫🇮', - SE: '🇸🇪', - NO: '🇳🇴', - PL: '🇵🇱', - TR: '🇹🇷', - JP: '🇯🇵', - SG: '🇸🇬', - HK: '🇭🇰', - KR: '🇰🇷', - AU: '🇦🇺', - CA: '🇨🇦', - CH: '🇨🇭', - AT: '🇦🇹', - IT: '🇮🇹', - ES: '🇪🇸', - BR: '🇧🇷', - IN: '🇮🇳', - AE: '🇦🇪', - IL: '🇮🇱', - KZ: '🇰🇿', - UA: '🇺🇦', - CZ: '🇨🇿', - RO: '🇷🇴', - LV: '🇱🇻', - LT: '🇱🇹', - EE: '🇪🇪', - BG: '🇧🇬', - HU: '🇭🇺', - MD: '🇲🇩', - }; - return codeMap[code.toUpperCase()] || code; -}; +// Алгоритмический ISO 3166-1 alpha-2 → regional indicator. Глобус-fallback +// сохранён для случая пустого кода (важно для UI-плейсхолдеров). +const getCountryFlag = (code: string | null | undefined): string => getFlagEmoji(code) || '🌍'; interface StatCardProps { label: string; diff --git a/src/pages/AdminRemnawaveSquadDetail.tsx b/src/pages/AdminRemnawaveSquadDetail.tsx index d59ee53..c13ad9f 100644 --- a/src/pages/AdminRemnawaveSquadDetail.tsx +++ b/src/pages/AdminRemnawaveSquadDetail.tsx @@ -5,49 +5,12 @@ import { adminRemnawaveApi, SquadWithLocalInfo } from '../api/adminRemnawave'; import { AdminBackButton } from '../components/admin'; import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons'; import Twemoji from 'react-twemoji'; -// Country flags helper -const getCountryFlag = (code: string | null | undefined): string => { - if (!code) return '🌍'; - const codeMap: Record = { - RU: '🇷🇺', - US: '🇺🇸', - DE: '🇩🇪', - NL: '🇳🇱', - GB: '🇬🇧', - UK: '🇬🇧', - FR: '🇫🇷', - FI: '🇫🇮', - SE: '🇸🇪', - NO: '🇳🇴', - PL: '🇵🇱', - TR: '🇹🇷', - JP: '🇯🇵', - SG: '🇸🇬', - HK: '🇭🇰', - KR: '🇰🇷', - AU: '🇦🇺', - CA: '🇨🇦', - CH: '🇨🇭', - AT: '🇦🇹', - IT: '🇮🇹', - ES: '🇪🇸', - BR: '🇧🇷', - IN: '🇮🇳', - AE: '🇦🇪', - IL: '🇮🇱', - KZ: '🇰🇿', - UA: '🇺🇦', - CZ: '🇨🇿', - RO: '🇷🇴', - LV: '🇱🇻', - LT: '🇱🇹', - EE: '🇪🇪', - BG: '🇧🇬', - HU: '🇭🇺', - MD: '🇲🇩', - }; - return codeMap[code.toUpperCase()] || code; -}; +import { getFlagEmoji } from '../utils/subscriptionHelpers'; + +// Country flag helper. Алгоритмический ISO 3166-1 alpha-2 → regional indicator, +// чтобы не плодить хардкод-словари (исторически у каждого экрана был свой +// неполный, и EE/MX/AR не покрывались). При отсутствии кода — глобус. +const getCountryFlag = (code: string | null | undefined): string => getFlagEmoji(code) || '🌍'; export default function AdminRemnawaveSquadDetail() { const { t } = useTranslation(); diff --git a/src/pages/AdminServerEdit.tsx b/src/pages/AdminServerEdit.tsx index 8204c15..a3c7026 100644 --- a/src/pages/AdminServerEdit.tsx +++ b/src/pages/AdminServerEdit.tsx @@ -7,39 +7,7 @@ import { AdminBackButton } from '../components/admin'; import { ServerIcon } from '../components/icons'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import Twemoji from 'react-twemoji'; - -// Country flags (simple emoji mapping) -const getCountryFlag = (code: string | null): string => { - if (!code) return ''; - const codeMap: Record = { - RU: '🇷🇺', - US: '🇺🇸', - DE: '🇩🇪', - NL: '🇳🇱', - GB: '🇬🇧', - FR: '🇫🇷', - FI: '🇫🇮', - SE: '🇸🇪', - PL: '🇵🇱', - CZ: '🇨🇿', - AT: '🇦🇹', - CH: '🇨🇭', - UA: '🇺🇦', - KZ: '🇰🇿', - JP: '🇯🇵', - KR: '🇰🇷', - SG: '🇸🇬', - HK: '🇭🇰', - CA: '🇨🇦', - AU: '🇦🇺', - BR: '🇧🇷', - IN: '🇮🇳', - TR: '🇹🇷', - IL: '🇮🇱', - AE: '🇦🇪', - }; - return codeMap[code.toUpperCase()] || code; -}; +import { getFlagEmoji as getCountryFlag } from '../utils/subscriptionHelpers'; export default function AdminServerEdit() { const { t } = useTranslation(); diff --git a/src/pages/AdminServers.tsx b/src/pages/AdminServers.tsx index f04bead..9080cde 100644 --- a/src/pages/AdminServers.tsx +++ b/src/pages/AdminServers.tsx @@ -20,37 +20,7 @@ const BackIcon = () => ( ); // Country flags (simple emoji mapping) -const getCountryFlag = (code: string | null): string => { - if (!code) return ''; - const codeMap: Record = { - RU: '🇷🇺', - US: '🇺🇸', - DE: '🇩🇪', - NL: '🇳🇱', - GB: '🇬🇧', - FR: '🇫🇷', - FI: '🇫🇮', - SE: '🇸🇪', - PL: '🇵🇱', - CZ: '🇨🇿', - AT: '🇦🇹', - CH: '🇨🇭', - UA: '🇺🇦', - KZ: '🇰🇿', - JP: '🇯🇵', - KR: '🇰🇷', - SG: '🇸🇬', - HK: '🇭🇰', - CA: '🇨🇦', - AU: '🇦🇺', - BR: '🇧🇷', - IN: '🇮🇳', - TR: '🇹🇷', - IL: '🇮🇱', - AE: '🇦🇪', - }; - return codeMap[code.toUpperCase()] || code; -}; +import { getFlagEmoji as getCountryFlag } from '../utils/subscriptionHelpers'; export default function AdminServers() { const { t } = useTranslation(); diff --git a/src/pages/AdminTrafficUsage.tsx b/src/pages/AdminTrafficUsage.tsx index b28c07d..c64fe86 100644 --- a/src/pages/AdminTrafficUsage.tsx +++ b/src/pages/AdminTrafficUsage.tsx @@ -18,6 +18,7 @@ import { type TrafficEnrichmentData, } from '../api/adminTraffic'; import { usePlatform } from '../platform/hooks/usePlatform'; +import { getFlagEmoji as _sharedGetFlagEmoji } from '../utils/subscriptionHelpers'; // ============ TanStack Table module augmentation ============ @@ -40,14 +41,9 @@ const formatBytes = (bytes: number): string => { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; -const getFlagEmoji = (countryCode: string): string => { - if (!countryCode || countryCode.length !== 2) return ''; - const codePoints = countryCode - .toUpperCase() - .split('') - .map((char) => 127397 + char.charCodeAt(0)); - return String.fromCodePoint(...codePoints); -}; +// Локальная обёртка над общим helper'ом, чтобы внутренние сигнатуры (string) +// оставались как были и call-sites не меняли. +const getFlagEmoji = (countryCode: string): string => _sharedGetFlagEmoji(countryCode); const formatCurrency = (kopeks: number): string => { const rubles = kopeks / 100; diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 163beff..d1fc4f5 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -24,51 +24,14 @@ import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { usePermissionStore } from '../store/permissions'; import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid'; import { linkifyText } from '../utils/linkify'; +import { getFlagEmoji } from '../utils/subscriptionHelpers'; // ============ Helpers ============ -const getCountryFlag = (code: string | null | undefined): string => { - if (!code) return ''; - const codeMap: Record = { - RU: '\u{1F1F7}\u{1F1FA}', - US: '\u{1F1FA}\u{1F1F8}', - DE: '\u{1F1E9}\u{1F1EA}', - NL: '\u{1F1F3}\u{1F1F1}', - GB: '\u{1F1EC}\u{1F1E7}', - UK: '\u{1F1EC}\u{1F1E7}', - FR: '\u{1F1EB}\u{1F1F7}', - FI: '\u{1F1EB}\u{1F1EE}', - SE: '\u{1F1F8}\u{1F1EA}', - NO: '\u{1F1F3}\u{1F1F4}', - PL: '\u{1F1F5}\u{1F1F1}', - TR: '\u{1F1F9}\u{1F1F7}', - JP: '\u{1F1EF}\u{1F1F5}', - SG: '\u{1F1F8}\u{1F1EC}', - HK: '\u{1F1ED}\u{1F1F0}', - KR: '\u{1F1F0}\u{1F1F7}', - AU: '\u{1F1E6}\u{1F1FA}', - CA: '\u{1F1E8}\u{1F1E6}', - CH: '\u{1F1E8}\u{1F1ED}', - AT: '\u{1F1E6}\u{1F1F9}', - IT: '\u{1F1EE}\u{1F1F9}', - ES: '\u{1F1EA}\u{1F1F8}', - BR: '\u{1F1E7}\u{1F1F7}', - IN: '\u{1F1EE}\u{1F1F3}', - AE: '\u{1F1E6}\u{1F1EA}', - IL: '\u{1F1EE}\u{1F1F1}', - KZ: '\u{1F1F0}\u{1F1FF}', - UA: '\u{1F1FA}\u{1F1E6}', - CZ: '\u{1F1E8}\u{1F1FF}', - RO: '\u{1F1F7}\u{1F1F4}', - LV: '\u{1F1F1}\u{1F1FB}', - LT: '\u{1F1F1}\u{1F1F9}', - EE: '\u{1F1EA}\u{1F1EA}', - BG: '\u{1F1E7}\u{1F1EC}', - HU: '\u{1F1ED}\u{1F1FA}', - MD: '\u{1F1F2}\u{1F1E9}', - }; - return codeMap[code.toUpperCase()] || code; -}; +// Алгоритмический ISO 3166-1 alpha-2 → regional indicator (вместо хардкод-словаря, +// который не покрывал все страны: например, EE раньше плыл сырым текстом в одних +// местах, MX/AR/EG до сих пор отсутствуют в других). Единая точка истины в utils. +const getCountryFlag = (code: string | null | undefined): string => getFlagEmoji(code); // ============ Icons ============ diff --git a/src/utils/subscriptionHelpers.ts b/src/utils/subscriptionHelpers.ts index 8be7f94..944ba69 100644 --- a/src/utils/subscriptionHelpers.ts +++ b/src/utils/subscriptionHelpers.ts @@ -32,9 +32,12 @@ export const getInsufficientBalanceError = ( return null; }; -export const getFlagEmoji = (countryCode: string): string => { - if (!countryCode || countryCode.length !== 2) return ''; - const codePoints = countryCode +export const getFlagEmoji = (countryCode: string | null | undefined): string => { + // Trim + длина строго 2 буквы — иначе Unicode regional indicators не дадут флаг. + // Принимаем null/undefined чтобы вызывающие коду не приходилось страховаться. + const code = (countryCode ?? '').trim(); + if (code.length !== 2 || !/^[A-Za-z]{2}$/.test(code)) return ''; + const codePoints = code .toUpperCase() .split('') .map((char) => 127397 + char.charCodeAt(0));