mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(admin): show flag for every country code, not just hardcoded 25-36
User reported the EE (Estonia) flag rendered as plain text 'EE' on the
admin servers page. Root cause: getCountryFlag() in AdminServers.tsx
and AdminServerEdit.tsx hardcoded a 25-entry codeMap that didn't
include EE; falling through to 'return code' produced raw text.
Two other admin pages (AdminRemnawaveSquadDetail, AdminUserDetail)
had bigger 35-entry maps that did include EE — so EE worked there
but MX/AR/EG/ZA and friends still wouldn't. AdminRemnawave repeated
the same 35-entry map. AdminTrafficUsage already had the correct
algorithmic ISO→regional-indicator code but as a local duplicate of
utils/subscriptionHelpers.
Unify all six on the single algorithmic helper:
- getFlagEmoji() in utils/subscriptionHelpers.ts now:
* accepts string | null | undefined (callers don't need to guard)
* trims whitespace
* validates [A-Za-z]{2} before composing regional indicators
- Each admin page now either imports getFlagEmoji directly or wraps
it with the page's preferred fallback character (e.g. '🌍' for
empty codes where the UI expects a placeholder).
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '../api/adminRemnawave';
|
} from '../api/adminRemnawave';
|
||||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
import { formatUptime } from '../utils/format';
|
import { formatUptime } from '../utils/format';
|
||||||
|
import { getFlagEmoji } from '../utils/subscriptionHelpers';
|
||||||
import Twemoji from 'react-twemoji';
|
import Twemoji from 'react-twemoji';
|
||||||
import {
|
import {
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -46,48 +47,9 @@ const formatBytes = (bytes: number): string => {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCountryFlag = (code: string | null | undefined): string => {
|
// Алгоритмический ISO 3166-1 alpha-2 → regional indicator. Глобус-fallback
|
||||||
if (!code) return '🌍';
|
// сохранён для случая пустого кода (важно для UI-плейсхолдеров).
|
||||||
const codeMap: Record<string, string> = {
|
const getCountryFlag = (code: string | null | undefined): string => getFlagEmoji(code) || '🌍';
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -5,49 +5,12 @@ import { adminRemnawaveApi, SquadWithLocalInfo } from '../api/adminRemnawave';
|
|||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons';
|
import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons';
|
||||||
import Twemoji from 'react-twemoji';
|
import Twemoji from 'react-twemoji';
|
||||||
// Country flags helper
|
import { getFlagEmoji } from '../utils/subscriptionHelpers';
|
||||||
const getCountryFlag = (code: string | null | undefined): string => {
|
|
||||||
if (!code) return '🌍';
|
// Country flag helper. Алгоритмический ISO 3166-1 alpha-2 → regional indicator,
|
||||||
const codeMap: Record<string, string> = {
|
// чтобы не плодить хардкод-словари (исторически у каждого экрана был свой
|
||||||
RU: '🇷🇺',
|
// неполный, и EE/MX/AR не покрывались). При отсутствии кода — глобус.
|
||||||
US: '🇺🇸',
|
const getCountryFlag = (code: string | null | undefined): string => getFlagEmoji(code) || '🌍';
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminRemnawaveSquadDetail() {
|
export default function AdminRemnawaveSquadDetail() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -7,39 +7,7 @@ import { AdminBackButton } from '../components/admin';
|
|||||||
import { ServerIcon } from '../components/icons';
|
import { ServerIcon } from '../components/icons';
|
||||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||||
import Twemoji from 'react-twemoji';
|
import Twemoji from 'react-twemoji';
|
||||||
|
import { getFlagEmoji as getCountryFlag } from '../utils/subscriptionHelpers';
|
||||||
// Country flags (simple emoji mapping)
|
|
||||||
const getCountryFlag = (code: string | null): string => {
|
|
||||||
if (!code) return '';
|
|
||||||
const codeMap: Record<string, string> = {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminServerEdit() {
|
export default function AdminServerEdit() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -20,37 +20,7 @@ const BackIcon = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Country flags (simple emoji mapping)
|
// Country flags (simple emoji mapping)
|
||||||
const getCountryFlag = (code: string | null): string => {
|
import { getFlagEmoji as getCountryFlag } from '../utils/subscriptionHelpers';
|
||||||
if (!code) return '';
|
|
||||||
const codeMap: Record<string, string> = {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminServers() {
|
export default function AdminServers() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
type TrafficEnrichmentData,
|
type TrafficEnrichmentData,
|
||||||
} from '../api/adminTraffic';
|
} from '../api/adminTraffic';
|
||||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
import { getFlagEmoji as _sharedGetFlagEmoji } from '../utils/subscriptionHelpers';
|
||||||
|
|
||||||
// ============ TanStack Table module augmentation ============
|
// ============ TanStack Table module augmentation ============
|
||||||
|
|
||||||
@@ -40,14 +41,9 @@ const formatBytes = (bytes: number): string => {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFlagEmoji = (countryCode: string): string => {
|
// Локальная обёртка над общим helper'ом, чтобы внутренние сигнатуры (string)
|
||||||
if (!countryCode || countryCode.length !== 2) return '';
|
// оставались как были и call-sites не меняли.
|
||||||
const codePoints = countryCode
|
const getFlagEmoji = (countryCode: string): string => _sharedGetFlagEmoji(countryCode);
|
||||||
.toUpperCase()
|
|
||||||
.split('')
|
|
||||||
.map((char) => 127397 + char.charCodeAt(0));
|
|
||||||
return String.fromCodePoint(...codePoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrency = (kopeks: number): string => {
|
const formatCurrency = (kopeks: number): string => {
|
||||||
const rubles = kopeks / 100;
|
const rubles = kopeks / 100;
|
||||||
|
|||||||
@@ -24,51 +24,14 @@ import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
|||||||
import { usePermissionStore } from '../store/permissions';
|
import { usePermissionStore } from '../store/permissions';
|
||||||
import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid';
|
import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid';
|
||||||
import { linkifyText } from '../utils/linkify';
|
import { linkifyText } from '../utils/linkify';
|
||||||
|
import { getFlagEmoji } from '../utils/subscriptionHelpers';
|
||||||
|
|
||||||
// ============ Helpers ============
|
// ============ Helpers ============
|
||||||
|
|
||||||
const getCountryFlag = (code: string | null | undefined): string => {
|
// Алгоритмический ISO 3166-1 alpha-2 → regional indicator (вместо хардкод-словаря,
|
||||||
if (!code) return '';
|
// который не покрывал все страны: например, EE раньше плыл сырым текстом в одних
|
||||||
const codeMap: Record<string, string> = {
|
// местах, MX/AR/EG до сих пор отсутствуют в других). Единая точка истины в utils.
|
||||||
RU: '\u{1F1F7}\u{1F1FA}',
|
const getCountryFlag = (code: string | null | undefined): string => getFlagEmoji(code);
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============ Icons ============
|
// ============ Icons ============
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,12 @@ export const getInsufficientBalanceError = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlagEmoji = (countryCode: string): string => {
|
export const getFlagEmoji = (countryCode: string | null | undefined): string => {
|
||||||
if (!countryCode || countryCode.length !== 2) return '';
|
// Trim + длина строго 2 буквы — иначе Unicode regional indicators не дадут флаг.
|
||||||
const codePoints = countryCode
|
// Принимаем null/undefined чтобы вызывающие коду не приходилось страховаться.
|
||||||
|
const code = (countryCode ?? '').trim();
|
||||||
|
if (code.length !== 2 || !/^[A-Za-z]{2}$/.test(code)) return '';
|
||||||
|
const codePoints = code
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.split('')
|
.split('')
|
||||||
.map((char) => 127397 + char.charCodeAt(0));
|
.map((char) => 127397 + char.charCodeAt(0));
|
||||||
|
|||||||
Reference in New Issue
Block a user