mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
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:
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { InfoIcon, WalletIcon, PlusIcon } from '@/components/icons';
|
||||
|
||||
interface InsufficientBalancePromptProps {
|
||||
/** Amount missing in kopeks */
|
||||
@@ -55,19 +56,7 @@ export default function InsufficientBalancePrompt({
|
||||
className={`flex items-center justify-between gap-3 rounded-xl border border-error-500/30 bg-error-500/10 p-3 ${className}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-error-400">
|
||||
<svg
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<InfoIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
{message || t('balance.insufficientFunds')}:{' '}
|
||||
<span className="font-semibold">
|
||||
@@ -96,19 +85,7 @@ export default function InsufficientBalancePrompt({
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-error-500/20">
|
||||
<svg
|
||||
className="h-5 w-5 text-error-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z"
|
||||
/>
|
||||
</svg>
|
||||
<WalletIcon className="h-5 w-5 text-error-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 font-medium text-error-400">{t('balance.insufficientFunds')}</div>
|
||||
@@ -132,15 +109,7 @@ export default function InsufficientBalancePrompt({
|
||||
<span 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="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
{t('balance.topUpBalance')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { infoApi, type LanguageInfo } from '@/api/info';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n } = useTranslation();
|
||||
@@ -57,14 +58,9 @@ export default function LanguageSwitcher() {
|
||||
>
|
||||
<span>{currentLang.flag}</span>
|
||||
<span className="font-medium text-dark-200">{currentLang.code.toUpperCase()}</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-3.5 w-3.5 text-dark-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EmailIcon as CentralEmailIcon } from '@/components/icons';
|
||||
import OAuthProviderIcon from './OAuthProviderIcon';
|
||||
|
||||
export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) {
|
||||
@@ -13,22 +14,7 @@ export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string })
|
||||
}
|
||||
|
||||
export function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return <CentralEmailIcon className={className} />;
|
||||
}
|
||||
|
||||
export default function ProviderIcon({
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { CheckIcon, XIcon, ExclamationIcon, InfoIcon } from '@/components/icons';
|
||||
|
||||
interface ToastOptions {
|
||||
type?: 'success' | 'error' | 'info' | 'warning';
|
||||
message: string;
|
||||
@@ -163,58 +165,10 @@ function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) {
|
||||
};
|
||||
|
||||
const defaultIcons = {
|
||||
success: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
),
|
||||
error: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
),
|
||||
warning: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
info: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
success: <CheckIcon className="h-5 w-5" />,
|
||||
error: <XIcon className="h-5 w-5" />,
|
||||
warning: <ExclamationIcon className="h-5 w-5" />,
|
||||
info: <InfoIcon className="h-5 w-5" />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlatform } from '@/platform';
|
||||
import { InfoIcon } from '@/components/icons';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
|
||||
@@ -55,20 +56,7 @@ export default function AccountDeletedScreen() {
|
||||
<div className="w-full max-w-md text-center">
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-warning-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<InfoIcon className="h-12 w-12 text-warning-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { BanIcon } from '@/components/icons';
|
||||
|
||||
export default function BlacklistedScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,19 +21,7 @@ export default function BlacklistedScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-error-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
/>
|
||||
</svg>
|
||||
<BanIcon className="h-12 w-12 text-error-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useBlockingStore } from '../../store/blocking';
|
||||
import { apiClient, isChannelSubscriptionError } from '../../api/client';
|
||||
import { usePlatform } from '../../platform';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { TelegramIcon, ClockIcon, CheckIcon } from '@/components/icons';
|
||||
|
||||
const CHECK_COOLDOWN_SECONDS = 5;
|
||||
|
||||
@@ -96,9 +97,7 @@ export default function ChannelSubscriptionScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-gradient-to-br from-blue-500/20 to-cyan-500/20">
|
||||
<svg className="h-12 w-12 text-blue-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
<TelegramIcon className="h-12 w-12 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -183,31 +182,12 @@ export default function ChannelSubscriptionScreen() {
|
||||
</>
|
||||
) : cooldown > 0 ? (
|
||||
<>
|
||||
<svg
|
||||
className="h-5 w-5 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<ClockIcon className="h-5 w-5 text-dark-500" />
|
||||
{t('blocking.channel.waitSeconds', { seconds: cooldown })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
{t('blocking.channel.checkSubscription')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { AdjustmentsIcon } from '@/components/icons';
|
||||
|
||||
export default function MaintenanceScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,19 +21,7 @@ export default function MaintenanceScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-warning-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<AdjustmentsIcon className="h-12 w-12 text-warning-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
|
||||
import type { BlockRendererProps } from './blocks';
|
||||
import TvQuickConnect from './TvQuickConnect';
|
||||
import { BackIcon } from '@/components/icons';
|
||||
import { BackIcon, ChevronIcon, DocumentIcon } from '@/components/icons';
|
||||
|
||||
const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
|
||||
|
||||
@@ -259,15 +259,7 @@ export default function InstallationGuide({
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-2.5 text-dark-400">
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 9l4-4 4 4M8 15l4 4 4-4" />
|
||||
</svg>
|
||||
<ChevronIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -315,19 +307,7 @@ export default function InstallationGuide({
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary w-full justify-center"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
</svg>
|
||||
<DocumentIcon className="h-5 w-5" />
|
||||
{getBaseTranslation('tutorial', 'subscription.connection.tutorial')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
import { getColorGradient } from '@/utils/colorParser';
|
||||
import { ThemeIcon } from './ThemeIcon';
|
||||
import type { BlockRendererProps } from './types';
|
||||
@@ -52,15 +53,9 @@ export function AccordionBlock({
|
||||
<span className="min-w-0 flex-1 truncate font-semibold text-dark-100">
|
||||
{getLocalizedText(block.title)}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-[18px] w-[18px] shrink-0 text-dark-400 transition-transform duration-200 ${isOpen ? '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>
|
||||
{/* Panel */}
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GiftIcon } from '@/components/icons';
|
||||
import type { PendingGift } from '../../api/gift';
|
||||
|
||||
interface PendingGiftCardProps {
|
||||
@@ -28,19 +29,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro
|
||||
<div className="relative flex items-start gap-4">
|
||||
{/* Gift icon */}
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-accent-500/20">
|
||||
<svg
|
||||
className="h-6 w-6 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
<GiftIcon className="h-6 w-6 text-accent-400" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
|
||||
import { formatTraffic } from '../../utils/formatTraffic';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import { RefreshIcon } from '@/components/icons';
|
||||
import { CalendarIcon, RefreshIcon } from '@/components/icons';
|
||||
import { useHaptic } from '../../platform';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
@@ -309,20 +309,14 @@ export default function SubscriptionCardActive({
|
||||
background: daysLeft <= 3 ? 'rgba(var(--color-warning-400), 0.1)' : g.hoverBg,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<span
|
||||
style={{
|
||||
color: daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-[13px] w-[13px]" />
|
||||
</span>
|
||||
</div>
|
||||
{t('dashboard.remaining')}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { getInsufficientBalanceError } from '../../utils/subscriptionHelpers';
|
||||
import { SubscriptionIcon } from '@/components/icons';
|
||||
import { ClockIcon, ExclamationIcon, PlusIcon, SubscriptionIcon } from '@/components/icons';
|
||||
|
||||
interface SubscriptionCardExpiredProps {
|
||||
subscription: Subscription;
|
||||
@@ -168,39 +168,13 @@ export default function SubscriptionCardExpired({
|
||||
style={{
|
||||
background: `rgba(${accent.r},${accent.g},${accent.b},0.1)`,
|
||||
border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.15)`,
|
||||
color: accent.hex,
|
||||
}}
|
||||
>
|
||||
{isLimited ? (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<ExclamationIcon className="h-[22px] w-[22px]" />
|
||||
) : (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
<ClockIcon className="h-[22px] w-[22px]" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||
@@ -275,19 +249,7 @@ export default function SubscriptionCardExpired({
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('subscription.buyTraffic')}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -330,19 +292,7 @@ export default function SubscriptionCardExpired({
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('dashboard.expired.topUp')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { TrialInfo } from '../../types';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { BoltIcon, SparklesIcon } from '@/components/icons';
|
||||
|
||||
interface TrialOfferCardProps {
|
||||
trialInfo: TrialInfo;
|
||||
@@ -94,37 +95,21 @@ export default function TrialOfferCard({
|
||||
}}
|
||||
>
|
||||
{isFree ? (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgb(var(--color-accent-400))"
|
||||
strokeWidth="1.5"
|
||||
<span
|
||||
className="flex"
|
||||
style={{ color: 'rgb(var(--color-accent-400))' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<SparklesIcon className="h-[26px] w-[26px]" />
|
||||
</span>
|
||||
) : (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgb(var(--color-urgent-400))"
|
||||
strokeWidth="1.5"
|
||||
<span
|
||||
className="flex"
|
||||
style={{ color: 'rgb(var(--color-urgent-400))' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<BoltIcon className="h-[26px] w-[26px]" />
|
||||
</span>
|
||||
)}
|
||||
{/* Glow effect */}
|
||||
<div
|
||||
|
||||
@@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
|
||||
import { newsApi } from '../../api/news';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { ArrowIcon } from '@/components/icons';
|
||||
import { ArrowIcon, NewsIcon } from '@/components/icons';
|
||||
import type { NewsListItem } from '../../types/news';
|
||||
|
||||
// --- Security: hex color validation to prevent CSS injection ---
|
||||
@@ -415,35 +415,7 @@ export default function NewsSection() {
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-accent-400 to-accent-600">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
fill="currentColor"
|
||||
className="text-dark-950/20"
|
||||
/>
|
||||
<path
|
||||
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
<path
|
||||
d="M7 8h4M7 11h10M7 14h10M7 17h6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
<rect
|
||||
x="14"
|
||||
y="7"
|
||||
width="4"
|
||||
height="4"
|
||||
rx="0.5"
|
||||
fill="currentColor"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
</svg>
|
||||
<NewsIcon className="h-[18px] w-[18px] text-dark-950" />
|
||||
</div>
|
||||
<span className="font-mono text-[11px] font-bold uppercase tracking-[0.18em] text-dark-500">
|
||||
{t('news.title')}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import { SubscriptionIcon } from '@/components/icons';
|
||||
import { ChevronRightIcon, SubscriptionIcon } from '@/components/icons';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
interface PurchaseCTAButtonProps {
|
||||
@@ -86,20 +86,7 @@ export default function PurchaseCTAButton({
|
||||
</div>
|
||||
|
||||
{/* Right: chevron */}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</HoverBorderGradient>
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { useHaptic } from '../../platform';
|
||||
import { CalendarIcon, CheckIcon, ChevronRightIcon, DevicesIcon } from '@/components/icons';
|
||||
import type { SubscriptionListItem } from '../../types';
|
||||
|
||||
function formatDate(iso: string | null, locale?: string): string {
|
||||
@@ -136,15 +137,7 @@ export default function SubscriptionListCard({
|
||||
</span>
|
||||
<StatusBadge status={subscription.status} isTrial={isTrial} t={t} />
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 opacity-30"
|
||||
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 shrink-0 opacity-30" />
|
||||
</div>
|
||||
|
||||
{/* Traffic mini progress bar */}
|
||||
@@ -177,29 +170,11 @@ export default function SubscriptionListCard({
|
||||
style={{ color: g.textSecondary }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" />
|
||||
<path d="M12 18h.01" />
|
||||
</svg>
|
||||
<DevicesIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
{subscription.device_limit}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
{formatDate(subscription.end_date, i18n.language)}
|
||||
</span>
|
||||
{!isTrial &&
|
||||
@@ -213,19 +188,19 @@ export default function SubscriptionListCard({
|
||||
<span
|
||||
className={`flex items-center gap-1 ${enabled ? 'text-success-400/70' : 'text-error-400/50'}`}
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
{enabled ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
) : (
|
||||
{enabled ? (
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
) : (
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
)}
|
||||
</svg>
|
||||
</svg>
|
||||
)}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTheme } from '../../../hooks/useTheme';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
||||
import { getGlassColors } from '../../../utils/glassTheme';
|
||||
import { ArrowDownIcon, DevicesIcon, RestartIcon } from '@/components/icons';
|
||||
import type { Tariff, Subscription, PurchaseOptions } from '../../../types';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -169,35 +170,11 @@ export function TariffPickerGrid({
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowDownIcon className="h-4 w-4 text-accent-400" />
|
||||
<span className="font-medium text-dark-200">{tariff.traffic_limit_label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||
/>
|
||||
</svg>
|
||||
<DevicesIcon className="h-4 w-4 text-dark-400" />
|
||||
<span className="text-dark-300">
|
||||
{tariff.device_limit === 0
|
||||
? '∞'
|
||||
@@ -206,19 +183,7 @@ export function TariffPickerGrid({
|
||||
</div>
|
||||
{tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M2.985 19.644l3.181-3.182"
|
||||
/>
|
||||
</svg>
|
||||
<RestartIcon className="h-4 w-4 text-dark-400" />
|
||||
<span className="text-dark-300">
|
||||
{t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TrashIcon } from '@/components/icons';
|
||||
import { subscriptionApi } from '../../../api/subscription';
|
||||
import { usePlatform } from '../../../platform';
|
||||
import { useDestructiveConfirm } from '../../../platform/hooks/useNativeDialog';
|
||||
@@ -72,19 +73,7 @@ export function DeleteSubscriptionSheet({
|
||||
disabled={deleteLoading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-error-400/20 bg-error-400/5 p-3.5 text-sm font-medium text-error-400 transition-colors hover:bg-error-400/10 disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
{t('subscription.delete', 'Удалить подписку')}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, DocumentIcon, XIcon } from '@/components/icons';
|
||||
|
||||
import { ticketsApi } from '../../api/tickets';
|
||||
|
||||
export interface MediaItem {
|
||||
@@ -150,19 +153,7 @@ export function MessageMediaGrid({
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-dark-700 px-3 py-2 text-sm text-dark-200 transition-colors hover:bg-dark-600"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
|
||||
/>
|
||||
</svg>
|
||||
<DocumentIcon className="h-4 w-4" />
|
||||
{item.caption || `Download ${item.type}`}
|
||||
</a>
|
||||
);
|
||||
@@ -180,15 +171,7 @@ export function MessageMediaGrid({
|
||||
className="absolute right-4 top-4 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white text-black shadow-xl transition-colors hover:bg-gray-200"
|
||||
onClick={closeFullscreen}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
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-5 w-5" />
|
||||
</button>
|
||||
|
||||
{photoItems.length > 1 && (
|
||||
@@ -199,15 +182,7 @@ export function MessageMediaGrid({
|
||||
onClick={() => setFullscreenIndex(fullscreenIndex - 1)}
|
||||
className="absolute left-4 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-xl transition-colors hover:bg-white disabled:opacity-30"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -215,15 +190,7 @@ export function MessageMediaGrid({
|
||||
onClick={() => setFullscreenIndex(fullscreenIndex + 1)}
|
||||
className="absolute right-4 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-xl transition-colors hover:bg-white disabled:opacity-30"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 rounded-full bg-dark-950/70 px-3 py-1 text-sm text-white">
|
||||
{fullscreenIndex + 1} / {photoItems.length}
|
||||
|
||||
Reference in New Issue
Block a user