refactor(cabinet): migrate inline SVG icons to the central react-icons set

Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77
files with the central Phosphor (react-icons/pi) components from
@/components/icons, preserving each icon's size classes and colour (dynamic
stroke colours via the parent's currentColor, RefreshIcon's spinning state,
conditional rotate-180 chevrons).

Verified: tsc + vite build + eslint clean; an adversarial diff review of all
changed files found the replacements correct (70 files clean, 0 blockers).
Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate
non-icons (brand/provider logos, loading spinners & framer-motion animations,
charts/sparklines, background decoration) plus a few ambiguous glyphs.
This commit is contained in:
c0mrade
2026-05-31 23:37:12 +03:00
parent 08f12daa40
commit b5088c70a1
76 changed files with 331 additions and 1985 deletions

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { brandingApi } from '../../api/branding';
import { CheckIcon, CloseIcon } from './icons';
import { CheckIcon, CloseIcon, PencilIcon } from './icons';
export function AnalyticsTab() {
const { t } = useTranslation();
@@ -143,19 +143,7 @@ export function AnalyticsTab() {
}}
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
<PencilIcon className="h-4 w-4" />
</button>
</div>
)}
@@ -294,19 +282,7 @@ export function AnalyticsTab() {
}}
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
<PencilIcon className="h-4 w-4" />
</button>
</div>
)}
@@ -360,19 +336,7 @@ export function AnalyticsTab() {
}}
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
<PencilIcon className="h-4 w-4" />
</button>
</div>
)}

View File

@@ -9,6 +9,7 @@ import {
ButtonSection,
BOT_LOCALES,
} from '../../api/buttonStyles';
import { ChevronDownIcon } from '@/components/icons';
import { Toggle } from './Toggle';
import { useNotify } from '../../platform/hooks/useNotify';
import { useNativeDialog } from '../../platform/hooks/useNativeDialog';
@@ -284,15 +285,9 @@ export function ButtonsTab() {
{t('admin.buttons.customLabels')}
{hasCustomLabels && <span className="h-1.5 w-1.5 rounded-full bg-accent-500" />}
</span>
<svg
<ChevronDownIcon
className={`h-3.5 w-3.5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
/>
</button>
{isExpanded && (
<div className="mt-2 space-y-2">

View File

@@ -1,5 +1,6 @@
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CheckIcon, ChevronDownIcon, CloseIcon, PlusIcon } from '@/components/icons';
import { cn } from '../../lib/utils';
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
@@ -209,9 +210,7 @@ export function ColoredItemCombobox({
className="shrink-0 rounded p-0.5 text-dark-500 transition-colors hover:text-dark-300"
aria-label={t('news.admin.combobox.clear')}
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
<CloseIcon className="h-4 w-4" />
</button>
</>
) : (
@@ -222,16 +221,12 @@ export function ColoredItemCombobox({
</span>
</>
)}
<svg
<ChevronDownIcon
className={cn(
'h-4 w-4 shrink-0 text-dark-500 transition-transform duration-200',
isOpen && 'rotate-180',
)}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M7 10l5 5 5-5z" />
</svg>
/>
</button>
{/* Dropdown */}
@@ -279,13 +274,7 @@ export function ColoredItemCombobox({
/>
<span className="flex-1 truncate">{item.name}</span>
{value?.id === item.id && (
<svg
className="h-4 w-4 shrink-0 text-accent-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
<CheckIcon className="h-4 w-4 shrink-0 text-accent-400" />
)}
{onDelete && (
<button
@@ -298,9 +287,7 @@ export function ColoredItemCombobox({
{deletingId === item.id ? (
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-error-400 border-t-transparent" />
) : (
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
<CloseIcon className="h-3.5 w-3.5" />
)}
</button>
)}
@@ -365,9 +352,7 @@ export function ColoredItemCombobox({
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
) : (
<>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<PlusIcon className="h-4 w-4" />
{t('news.admin.combobox.create')}
</>
)}

View File

@@ -4,6 +4,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { PiCaretDown } from 'react-icons/pi';
import { CSS } from '@dnd-kit/utilities';
import { cn } from '../../lib/utils';
import { CheckIcon } from '@/components/icons';
import { GripIcon, TrashIcon } from '../icons/LandingIcons';
import type { AdminLandingPaymentMethod, EditableMethodField } from '../../api/landings';
import type { PaymentMethodSubOptionInfo } from '../../types';
@@ -218,17 +219,7 @@ export function SortableSelectedMethodCard({
: 'border border-dark-600 bg-dark-700',
)}
>
{enabled && (
<svg
className="h-2.5 w-2.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
{enabled && <CheckIcon className="h-2.5 w-2.5" />}
</div>
{opt.name}
</button>

View File

@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { CheckIcon, XCloseIcon } from '@/components/icons';
import { CheckIcon, ChevronDownIcon, XCloseIcon, XIcon } from '@/components/icons';
import { useFocusTrap } from '@/hooks/useFocusTrap';
import { DropdownSelect } from './DropdownSelect';
import type { UserListItem } from '../../../api/adminUsers';
@@ -98,31 +98,11 @@ function ProgressView({ progress }: { progress: ProgressState }) {
<div key={idx} className="flex items-start gap-2 text-xs">
{entry.success ? (
<span className="mt-0.5 shrink-0 text-success-400" aria-hidden="true">
<svg
className="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
<CheckIcon className="h-3 w-3" />
</span>
) : (
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
<svg
className="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<XIcon className="h-3 w-3" />
</span>
)}
<span className="font-mono text-dark-400">
@@ -159,18 +139,12 @@ function ErrorDetails({ result }: { result: BulkActionResult }) {
<span className="text-sm font-medium text-error-400">
{t('admin.bulkActions.errors.title', { count: result.error_count })}
</span>
<svg
<ChevronDownIcon
className={cn(
'h-4 w-4 text-error-400 transition-transform duration-200',
expanded && 'rotate-180',
)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
/>
</button>
{expanded && (
<div className="max-h-48 overflow-y-auto border-t border-error-500/20 px-4 py-3">
@@ -178,15 +152,7 @@ function ErrorDetails({ result }: { result: BulkActionResult }) {
{result.errors.map((err, idx) => (
<div key={idx} className="flex items-start gap-2 text-xs">
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
<svg
className="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<XIcon className="h-3 w-3" />
</span>
<span className="shrink-0 font-mono text-dark-400">
{err.username ? `@${err.username}` : `#${err.user_id}`}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { TrashIcon } from '@/components/icons';
import { ChevronDownIcon } from './DropdownSelect';
import { isSubscriptionLevelAction } from './actionTargets';
import type { BulkActionType } from '../../../api/adminBulkActions';
@@ -112,21 +113,7 @@ export function FloatingActionBar({
{
type: 'delete_subscription',
labelKey: 'admin.bulkActions.actions.deleteSubscription',
icon: (
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
),
icon: <TrashIcon className="h-3.5 w-3.5" />,
colorClass: 'text-error-400 hover:bg-error-500/10',
},
{

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { CheckIcon } from '@/components/icons';
import { ChevronDownIcon } from './DropdownSelect';
// ──────────────────────────────────────────────────────────────────
@@ -135,21 +136,7 @@ export function MultiSelectDropdown({
: 'border-dark-500 bg-dark-700/60',
)}
>
{isChecked && (
<svg
className="h-2.5 w-2.5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={4}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
)}
{isChecked && <CheckIcon className="h-2.5 w-2.5 text-white" />}
</div>
<span className={cn('text-sm', isChecked ? 'text-dark-100' : 'text-dark-300')}>
{option.label}

View File

@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { CheckIcon } from '@/components/icons';
import type { UserListItemSubscription } from '../../../api/adminUsers';
// ──────────────────────────────────────────────────────────────────
@@ -68,17 +69,7 @@ export function SubscriptionSubRow({
: t('admin.bulkActions.selectUser', { name: subscription.tariff_name || '' })
}
>
{isSelected && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={4}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)}
{isSelected && <CheckIcon className="h-3 w-3 text-white" />}
</button>
</div>
)}

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { CheckIcon } from '@/components/icons';
import { ChevronDownIcon, GlobeIcon } from '../TrafficIcons';
import { getFlagEmoji } from '../trafficUsageHelpers';
@@ -71,17 +72,7 @@ export function CountryFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{allSelected && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)}
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
</span>
All
</button>
@@ -102,21 +93,7 @@ export function CountryFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{checked && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
)}
{checked && <CheckIcon className="h-3 w-3 text-white" />}
</span>
{getFlagEmoji(code)} {code.toUpperCase()}
<span className="ml-auto text-dark-500">{count}</span>

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDownIcon, ServerIcon } from '../TrafficIcons';
import { CheckIcon } from '@/components/icons';
import { getFlagEmoji } from '../trafficUsageHelpers';
import type { TrafficNodeInfo } from '../../../../api/adminTraffic';
@@ -75,17 +76,7 @@ export function NodeFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{allSelected && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)}
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
</span>
{t('admin.trafficUsage.allNodes')}
</button>
@@ -106,21 +97,7 @@ export function NodeFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{checked && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
)}
{checked && <CheckIcon className="h-3 w-3 text-white" />}
</span>
{getFlagEmoji(node.country_code)} {node.node_name}
</button>

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CheckIcon } from '@/components/icons';
import { ChevronDownIcon, StatusIcon } from '../TrafficIcons';
// Status colour pills shared with the StatusFilter dropdown.
@@ -86,17 +87,7 @@ export function StatusFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{allSelected && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)}
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
</span>
{t('admin.trafficUsage.allStatuses')}
</button>
@@ -117,21 +108,7 @@ export function StatusFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{checked && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
)}
{checked && <CheckIcon className="h-3 w-3 text-white" />}
</span>
<span className={`h-2 w-2 rounded-full ${STATUS_COLORS[s] || 'bg-dark-500'}`} />
{statusLabel(s)}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDownIcon, FilterIcon } from '../TrafficIcons';
import { CheckIcon } from '@/components/icons';
export function TariffFilter({
available,
@@ -73,17 +74,7 @@ export function TariffFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{allSelected && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)}
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
</span>
{t('admin.trafficUsage.allTariffs')}
</button>
@@ -104,21 +95,7 @@ export function TariffFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`}
>
{checked && (
<svg
className="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
)}
{checked && <CheckIcon className="h-3 w-3 text-white" />}
</span>
{tariff}
</button>

View File

@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import { SendIcon } from '@/components/icons';
import { useCurrency } from '../../../hooks/useCurrency';
import type { AdminUserGiftItem, AdminUserGiftsResponse } from '../../../api/adminUsers';
@@ -224,19 +225,7 @@ export function GiftsTab({ giftsLoading, giftsData, locale, onNavigateToUser }:
{/* Sent Gifts */}
<div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-dark-200">
<svg
className="h-4 w-4 text-accent-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
/>
</svg>
<SendIcon className="h-4 w-4 text-accent-400" />
{t('admin.users.detail.gifts.sentTitle')}
<span className="text-dark-500">({giftsData.sent_total})</span>
</h3>

View File

@@ -9,6 +9,7 @@ import type {
UserSubscriptionInfo,
} from '../../../api/adminUsers';
import type { PromoGroup } from '../../../api/promocodes';
import { ServerIcon } from '@/components/icons';
// ──────────────────────────────────────────────────────────────────
// Local status badge (parent has its own — duplicating here to keep
@@ -260,19 +261,7 @@ export function InfoTab(props: InfoTabProps) {
{t('admin.users.detail.lastNode')}
</div>
<div className="flex items-center gap-2 text-sm text-dark-100">
<svg
className="h-4 w-4 shrink-0 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
/>
</svg>
<ServerIcon className="h-4 w-4 shrink-0 text-dark-400" />
{panelInfo.last_connected_node_name}
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';
import { useCurrency } from '../../../hooks/useCurrency';
import { useNotify } from '../../../platform/hooks/useNotify';
import { adminUsersApi, type UserDetailResponse, type UserListItem } from '../../../api/adminUsers';
import { XIcon } from '@/components/icons';
// ──────────────────────────────────────────────────────────────────
// Referrals tab — top-of-graph referrer + stats + referrals list,
@@ -481,15 +482,7 @@ export function ReferralsTab({ user, userId, onUserRefresh }: ReferralsTabProps)
className="rounded-lg p-2 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400 disabled:opacity-50"
title={t('admin.users.detail.referrals.removeReferral')}
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<XIcon className="h-4 w-4" />
</button>
</div>
))}

View File

@@ -1,5 +1,15 @@
import { useTranslation } from 'react-i18next';
import { MinusIcon, PlusIcon, RefreshIcon } from '@/components/icons';
import {
BackIcon,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
EditIcon,
MinusIcon,
PlusIcon,
RefreshIcon,
XIcon,
} from '@/components/icons';
import { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices';
import { createNumberInputHandler } from '../../../utils/inputHelpers';
import { getFlagEmoji } from '../../../utils/subscriptionHelpers';
@@ -217,19 +227,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
</span>
<StatusBadge status={sub.status} />
</div>
<svg
className="h-4 w-4 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
<ChevronRightIcon className="h-4 w-4 text-dark-500" />
</div>
<div className="mt-2 flex items-center gap-4 text-xs text-dark-400">
<span>
@@ -309,15 +307,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
onClick={() => onSubscriptionDetailViewChange(false)}
className="flex items-center gap-1.5 text-sm text-dark-400 transition-colors hover:text-dark-200"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
<BackIcon className="h-4 w-4" />
{t('admin.users.detail.subscription.backToList', 'Все подписки')}
</button>
)}
@@ -943,19 +933,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
)}
aria-label={t('admin.users.detail.devices.renameSave', 'Сохранить')}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 13l4 4L19 7" />
</svg>
<CheckIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
@@ -968,19 +946,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
title={t('common.cancel', 'Отмена')}
aria-label={t('common.cancel', 'Отмена')}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
<XIcon className="h-3.5 w-3.5" />
</button>
</>
) : (
@@ -995,19 +961,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
title={t('admin.users.detail.devices.rename', 'Переименовать')}
aria-label={t('admin.users.detail.devices.rename', 'Переименовать')}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
</svg>
<EditIcon className="h-3.5 w-3.5" />
</button>
<button
onClick={() =>
@@ -1060,19 +1014,9 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
</span>
)}
</div>
<svg
<ChevronDownIcon
className={`h-4 w-4 text-dark-500 transition-transform ${requestHistoryExpanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
/>
</button>
{requestHistoryExpanded && (

View File

@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import { adminApi, type AdminTicket, type AdminTicketDetail } from '../../../api/admin';
import { MessageMediaGrid } from '../../tickets/MessageMediaGrid';
import { linkifyText } from '../../../utils/linkify';
import { ChatIcon, BackIcon, SendIcon } from '@/components/icons';
// ──────────────────────────────────────────────────────────────────
// Tickets tab — list view + chat view (selected ticket replaces list).
@@ -155,19 +156,7 @@ function EmptyState() {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center rounded-xl bg-dark-800/50 py-12">
<svg
className="mb-3 h-12 w-12 text-dark-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
/>
</svg>
<ChatIcon className="mb-3 h-12 w-12 text-dark-600" />
<p className="text-dark-400">{t('admin.users.detail.noTickets')}</p>
</div>
);
@@ -265,15 +254,7 @@ function ChatView({
aria-label={t('common.back', 'Back')}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-dark-800 transition-colors hover:bg-dark-700 sm:h-8 sm:w-8"
>
<svg
className="h-4 w-4 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
<BackIcon className="h-4 w-4 text-dark-400" />
</button>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-dark-100">
@@ -372,19 +353,7 @@ function ChatView({
{replySending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : (
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
/>
</svg>
<SendIcon className="h-5 w-5" />
)}
</button>
</div>