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,6 +2,7 @@ import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router'; import { useNavigate, useLocation } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { InfoIcon, WalletIcon, PlusIcon } from '@/components/icons';
interface InsufficientBalancePromptProps { interface InsufficientBalancePromptProps {
/** Amount missing in kopeks */ /** 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}`} 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"> <div className="flex items-center gap-2 text-sm text-error-400">
<svg <InfoIcon className="h-4 w-4 flex-shrink-0" />
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>
<span> <span>
{message || t('balance.insufficientFunds')}:{' '} {message || t('balance.insufficientFunds')}:{' '}
<span className="font-semibold"> <span className="font-semibold">
@@ -96,19 +85,7 @@ export default function InsufficientBalancePrompt({
> >
<div className="flex items-start gap-3"> <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"> <div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-error-500/20">
<svg <WalletIcon className="h-5 w-5 text-error-400" />
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>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="mb-1 font-medium text-error-400">{t('balance.insufficientFunds')}</div> <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" /> <span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<> <>
<svg <PlusIcon className="h-5 w-5" />
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>
{t('balance.topUpBalance')} {t('balance.topUpBalance')}
</> </>
)} )}

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { infoApi, type LanguageInfo } from '@/api/info'; import { infoApi, type LanguageInfo } from '@/api/info';
import { ChevronDownIcon } from '@/components/icons';
export default function LanguageSwitcher() { export default function LanguageSwitcher() {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@@ -57,14 +58,9 @@ export default function LanguageSwitcher() {
> >
<span>{currentLang.flag}</span> <span>{currentLang.flag}</span>
<span className="font-medium text-dark-200">{currentLang.code.toUpperCase()}</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' : ''}`} 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> </button>
{isOpen && ( {isOpen && (

View File

@@ -1,4 +1,5 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { EmailIcon as CentralEmailIcon } from '@/components/icons';
import OAuthProviderIcon from './OAuthProviderIcon'; import OAuthProviderIcon from './OAuthProviderIcon';
export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) { 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 }) { export function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) {
return ( return <CentralEmailIcon className={className} />;
<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>
);
} }
export default function ProviderIcon({ export default function ProviderIcon({

View File

@@ -8,6 +8,8 @@ import {
ReactNode, ReactNode,
} from 'react'; } from 'react';
import { CheckIcon, XIcon, ExclamationIcon, InfoIcon } from '@/components/icons';
interface ToastOptions { interface ToastOptions {
type?: 'success' | 'error' | 'info' | 'warning'; type?: 'success' | 'error' | 'info' | 'warning';
message: string; message: string;
@@ -163,58 +165,10 @@ function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) {
}; };
const defaultIcons = { const defaultIcons = {
success: ( success: <CheckIcon className="h-5 w-5" />,
<svg error: <XIcon className="h-5 w-5" />,
className="h-5 w-5" warning: <ExclamationIcon className="h-5 w-5" />,
fill="none" info: <InfoIcon className="h-5 w-5" />,
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>
),
}; };
return ( return (

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { brandingApi } from '../../api/branding'; import { brandingApi } from '../../api/branding';
import { CheckIcon, CloseIcon } from './icons'; import { CheckIcon, CloseIcon, PencilIcon } from './icons';
export function AnalyticsTab() { export function AnalyticsTab() {
const { t } = useTranslation(); 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" className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
> >
<svg <PencilIcon className="h-4 w-4" />
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>
</button> </button>
</div> </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" className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
> >
<svg <PencilIcon className="h-4 w-4" />
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>
</button> </button>
</div> </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" className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
> >
<svg <PencilIcon className="h-4 w-4" />
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>
</button> </button>
</div> </div>
)} )}

View File

@@ -9,6 +9,7 @@ import {
ButtonSection, ButtonSection,
BOT_LOCALES, BOT_LOCALES,
} from '../../api/buttonStyles'; } from '../../api/buttonStyles';
import { ChevronDownIcon } from '@/components/icons';
import { Toggle } from './Toggle'; import { Toggle } from './Toggle';
import { useNotify } from '../../platform/hooks/useNotify'; import { useNotify } from '../../platform/hooks/useNotify';
import { useNativeDialog } from '../../platform/hooks/useNativeDialog'; import { useNativeDialog } from '../../platform/hooks/useNativeDialog';
@@ -284,15 +285,9 @@ export function ButtonsTab() {
{t('admin.buttons.customLabels')} {t('admin.buttons.customLabels')}
{hasCustomLabels && <span className="h-1.5 w-1.5 rounded-full bg-accent-500" />} {hasCustomLabels && <span className="h-1.5 w-1.5 rounded-full bg-accent-500" />}
</span> </span>
<svg <ChevronDownIcon
className={`h-3.5 w-3.5 transition-transform ${isExpanded ? 'rotate-180' : ''}`} 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> </button>
{isExpanded && ( {isExpanded && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">

View File

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

View File

@@ -4,6 +4,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { PiCaretDown } from 'react-icons/pi'; import { PiCaretDown } from 'react-icons/pi';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { CheckIcon } from '@/components/icons';
import { GripIcon, TrashIcon } from '../icons/LandingIcons'; import { GripIcon, TrashIcon } from '../icons/LandingIcons';
import type { AdminLandingPaymentMethod, EditableMethodField } from '../../api/landings'; import type { AdminLandingPaymentMethod, EditableMethodField } from '../../api/landings';
import type { PaymentMethodSubOptionInfo } from '../../types'; import type { PaymentMethodSubOptionInfo } from '../../types';
@@ -218,17 +219,7 @@ export function SortableSelectedMethodCard({
: 'border border-dark-600 bg-dark-700', : 'border border-dark-600 bg-dark-700',
)} )}
> >
{enabled && ( {enabled && <CheckIcon className="h-2.5 w-2.5" />}
<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>
)}
</div> </div>
{opt.name} {opt.name}
</button> </button>

View File

@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; 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 { useFocusTrap } from '@/hooks/useFocusTrap';
import { DropdownSelect } from './DropdownSelect'; import { DropdownSelect } from './DropdownSelect';
import type { UserListItem } from '../../../api/adminUsers'; 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"> <div key={idx} className="flex items-start gap-2 text-xs">
{entry.success ? ( {entry.success ? (
<span className="mt-0.5 shrink-0 text-success-400" aria-hidden="true"> <span className="mt-0.5 shrink-0 text-success-400" aria-hidden="true">
<svg <CheckIcon className="h-3 w-3" />
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>
</span> </span>
) : ( ) : (
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true"> <span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
<svg <XIcon className="h-3 w-3" />
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>
</span> </span>
)} )}
<span className="font-mono text-dark-400"> <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"> <span className="text-sm font-medium text-error-400">
{t('admin.bulkActions.errors.title', { count: result.error_count })} {t('admin.bulkActions.errors.title', { count: result.error_count })}
</span> </span>
<svg <ChevronDownIcon
className={cn( className={cn(
'h-4 w-4 text-error-400 transition-transform duration-200', 'h-4 w-4 text-error-400 transition-transform duration-200',
expanded && 'rotate-180', 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> </button>
{expanded && ( {expanded && (
<div className="max-h-48 overflow-y-auto border-t border-error-500/20 px-4 py-3"> <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) => ( {result.errors.map((err, idx) => (
<div key={idx} className="flex items-start gap-2 text-xs"> <div key={idx} className="flex items-start gap-2 text-xs">
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true"> <span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
<svg <XIcon className="h-3 w-3" />
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>
</span> </span>
<span className="shrink-0 font-mono text-dark-400"> <span className="shrink-0 font-mono text-dark-400">
{err.username ? `@${err.username}` : `#${err.user_id}`} {err.username ? `@${err.username}` : `#${err.user_id}`}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react'; import { useEffect, useRef, useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { TrashIcon } from '@/components/icons';
import { ChevronDownIcon } from './DropdownSelect'; import { ChevronDownIcon } from './DropdownSelect';
import { isSubscriptionLevelAction } from './actionTargets'; import { isSubscriptionLevelAction } from './actionTargets';
import type { BulkActionType } from '../../../api/adminBulkActions'; import type { BulkActionType } from '../../../api/adminBulkActions';
@@ -112,21 +113,7 @@ export function FloatingActionBar({
{ {
type: 'delete_subscription', type: 'delete_subscription',
labelKey: 'admin.bulkActions.actions.deleteSubscription', labelKey: 'admin.bulkActions.actions.deleteSubscription',
icon: ( icon: <TrashIcon className="h-3.5 w-3.5" />,
<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>
),
colorClass: 'text-error-400 hover:bg-error-500/10', colorClass: 'text-error-400 hover:bg-error-500/10',
}, },
{ {

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckIcon } from '@/components/icons';
import { ChevronDownIcon } from './DropdownSelect'; import { ChevronDownIcon } from './DropdownSelect';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
@@ -135,21 +136,7 @@ export function MultiSelectDropdown({
: 'border-dark-500 bg-dark-700/60', : 'border-dark-500 bg-dark-700/60',
)} )}
> >
{isChecked && ( {isChecked && <CheckIcon className="h-2.5 w-2.5 text-white" />}
<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>
)}
</div> </div>
<span className={cn('text-sm', isChecked ? 'text-dark-100' : 'text-dark-300')}> <span className={cn('text-sm', isChecked ? 'text-dark-100' : 'text-dark-300')}>
{option.label} {option.label}

View File

@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckIcon } from '@/components/icons';
import type { UserListItemSubscription } from '../../../api/adminUsers'; import type { UserListItemSubscription } from '../../../api/adminUsers';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
@@ -68,17 +69,7 @@ export function SubscriptionSubRow({
: t('admin.bulkActions.selectUser', { name: subscription.tariff_name || '' }) : t('admin.bulkActions.selectUser', { name: subscription.tariff_name || '' })
} }
> >
{isSelected && ( {isSelected && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</button> </button>
</div> </div>
)} )}

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { CheckIcon } from '@/components/icons';
import { ChevronDownIcon, GlobeIcon } from '../TrafficIcons'; import { ChevronDownIcon, GlobeIcon } from '../TrafficIcons';
import { getFlagEmoji } from '../trafficUsageHelpers'; import { getFlagEmoji } from '../trafficUsageHelpers';
@@ -71,17 +72,7 @@ export function CountryFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600' allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{allSelected && ( {allSelected && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
All All
</button> </button>
@@ -102,21 +93,7 @@ export function CountryFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600' checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{checked && ( {checked && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
{getFlagEmoji(code)} {code.toUpperCase()} {getFlagEmoji(code)} {code.toUpperCase()}
<span className="ml-auto text-dark-500">{count}</span> <span className="ml-auto text-dark-500">{count}</span>

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChevronDownIcon, ServerIcon } from '../TrafficIcons'; import { ChevronDownIcon, ServerIcon } from '../TrafficIcons';
import { CheckIcon } from '@/components/icons';
import { getFlagEmoji } from '../trafficUsageHelpers'; import { getFlagEmoji } from '../trafficUsageHelpers';
import type { TrafficNodeInfo } from '../../../../api/adminTraffic'; import type { TrafficNodeInfo } from '../../../../api/adminTraffic';
@@ -75,17 +76,7 @@ export function NodeFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600' allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{allSelected && ( {allSelected && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
{t('admin.trafficUsage.allNodes')} {t('admin.trafficUsage.allNodes')}
</button> </button>
@@ -106,21 +97,7 @@ export function NodeFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600' checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{checked && ( {checked && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
{getFlagEmoji(node.country_code)} {node.node_name} {getFlagEmoji(node.country_code)} {node.node_name}
</button> </button>

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CheckIcon } from '@/components/icons';
import { ChevronDownIcon, StatusIcon } from '../TrafficIcons'; import { ChevronDownIcon, StatusIcon } from '../TrafficIcons';
// Status colour pills shared with the StatusFilter dropdown. // 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 ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{allSelected && ( {allSelected && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
{t('admin.trafficUsage.allStatuses')} {t('admin.trafficUsage.allStatuses')}
</button> </button>
@@ -117,21 +108,7 @@ export function StatusFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600' checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{checked && ( {checked && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
<span className={`h-2 w-2 rounded-full ${STATUS_COLORS[s] || 'bg-dark-500'}`} /> <span className={`h-2 w-2 rounded-full ${STATUS_COLORS[s] || 'bg-dark-500'}`} />
{statusLabel(s)} {statusLabel(s)}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChevronDownIcon, FilterIcon } from '../TrafficIcons'; import { ChevronDownIcon, FilterIcon } from '../TrafficIcons';
import { CheckIcon } from '@/components/icons';
export function TariffFilter({ export function TariffFilter({
available, available,
@@ -73,17 +74,7 @@ export function TariffFilter({
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600' allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{allSelected && ( {allSelected && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
{t('admin.trafficUsage.allTariffs')} {t('admin.trafficUsage.allTariffs')}
</button> </button>
@@ -104,21 +95,7 @@ export function TariffFilter({
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600' checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
}`} }`}
> >
{checked && ( {checked && <CheckIcon className="h-3 w-3 text-white" />}
<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>
)}
</span> </span>
{tariff} {tariff}
</button> </button>

View File

@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SendIcon } from '@/components/icons';
import { useCurrency } from '../../../hooks/useCurrency'; import { useCurrency } from '../../../hooks/useCurrency';
import type { AdminUserGiftItem, AdminUserGiftsResponse } from '../../../api/adminUsers'; import type { AdminUserGiftItem, AdminUserGiftsResponse } from '../../../api/adminUsers';
@@ -224,19 +225,7 @@ export function GiftsTab({ giftsLoading, giftsData, locale, onNavigateToUser }:
{/* Sent Gifts */} {/* Sent Gifts */}
<div> <div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-dark-200"> <h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-dark-200">
<svg <SendIcon className="h-4 w-4 text-accent-400" />
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>
{t('admin.users.detail.gifts.sentTitle')} {t('admin.users.detail.gifts.sentTitle')}
<span className="text-dark-500">({giftsData.sent_total})</span> <span className="text-dark-500">({giftsData.sent_total})</span>
</h3> </h3>

View File

@@ -9,6 +9,7 @@ import type {
UserSubscriptionInfo, UserSubscriptionInfo,
} from '../../../api/adminUsers'; } from '../../../api/adminUsers';
import type { PromoGroup } from '../../../api/promocodes'; import type { PromoGroup } from '../../../api/promocodes';
import { ServerIcon } from '@/components/icons';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
// Local status badge (parent has its own — duplicating here to keep // 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')} {t('admin.users.detail.lastNode')}
</div> </div>
<div className="flex items-center gap-2 text-sm text-dark-100"> <div className="flex items-center gap-2 text-sm text-dark-100">
<svg <ServerIcon className="h-4 w-4 shrink-0 text-dark-400" />
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>
{panelInfo.last_connected_node_name} {panelInfo.last_connected_node_name}
</div> </div>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';
import { useCurrency } from '../../../hooks/useCurrency'; import { useCurrency } from '../../../hooks/useCurrency';
import { useNotify } from '../../../platform/hooks/useNotify'; import { useNotify } from '../../../platform/hooks/useNotify';
import { adminUsersApi, type UserDetailResponse, type UserListItem } from '../../../api/adminUsers'; import { adminUsersApi, type UserDetailResponse, type UserListItem } from '../../../api/adminUsers';
import { XIcon } from '@/components/icons';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
// Referrals tab — top-of-graph referrer + stats + referrals list, // 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" 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')} title={t('admin.users.detail.referrals.removeReferral')}
> >
<svg <XIcon className="h-4 w-4" />
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>
</button> </button>
</div> </div>
))} ))}

View File

@@ -1,5 +1,15 @@
import { useTranslation } from 'react-i18next'; 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 { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices';
import { createNumberInputHandler } from '../../../utils/inputHelpers'; import { createNumberInputHandler } from '../../../utils/inputHelpers';
import { getFlagEmoji } from '../../../utils/subscriptionHelpers'; import { getFlagEmoji } from '../../../utils/subscriptionHelpers';
@@ -217,19 +227,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
</span> </span>
<StatusBadge status={sub.status} /> <StatusBadge status={sub.status} />
</div> </div>
<svg <ChevronRightIcon className="h-4 w-4 text-dark-500" />
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>
</div> </div>
<div className="mt-2 flex items-center gap-4 text-xs text-dark-400"> <div className="mt-2 flex items-center gap-4 text-xs text-dark-400">
<span> <span>
@@ -309,15 +307,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
onClick={() => onSubscriptionDetailViewChange(false)} onClick={() => onSubscriptionDetailViewChange(false)}
className="flex items-center gap-1.5 text-sm text-dark-400 transition-colors hover:text-dark-200" className="flex items-center gap-1.5 text-sm text-dark-400 transition-colors hover:text-dark-200"
> >
<svg <BackIcon className="h-4 w-4" />
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>
{t('admin.users.detail.subscription.backToList', 'Все подписки')} {t('admin.users.detail.subscription.backToList', 'Все подписки')}
</button> </button>
)} )}
@@ -943,19 +933,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
)} )}
aria-label={t('admin.users.detail.devices.renameSave', 'Сохранить')} aria-label={t('admin.users.detail.devices.renameSave', 'Сохранить')}
> >
<svg <CheckIcon className="h-3.5 w-3.5" />
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>
</button> </button>
<button <button
type="button" type="button"
@@ -968,19 +946,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
title={t('common.cancel', 'Отмена')} title={t('common.cancel', 'Отмена')}
aria-label={t('common.cancel', 'Отмена')} aria-label={t('common.cancel', 'Отмена')}
> >
<svg <XIcon className="h-3.5 w-3.5" />
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>
</button> </button>
</> </>
) : ( ) : (
@@ -995,19 +961,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
title={t('admin.users.detail.devices.rename', 'Переименовать')} title={t('admin.users.detail.devices.rename', 'Переименовать')}
aria-label={t('admin.users.detail.devices.rename', 'Переименовать')} aria-label={t('admin.users.detail.devices.rename', 'Переименовать')}
> >
<svg <EditIcon className="h-3.5 w-3.5" />
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>
</button> </button>
<button <button
onClick={() => onClick={() =>
@@ -1060,19 +1014,9 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
</span> </span>
)} )}
</div> </div>
<svg <ChevronDownIcon
className={`h-4 w-4 text-dark-500 transition-transform ${requestHistoryExpanded ? 'rotate-180' : ''}`} 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> </button>
{requestHistoryExpanded && ( {requestHistoryExpanded && (

View File

@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import { adminApi, type AdminTicket, type AdminTicketDetail } from '../../../api/admin'; import { adminApi, type AdminTicket, type AdminTicketDetail } from '../../../api/admin';
import { MessageMediaGrid } from '../../tickets/MessageMediaGrid'; import { MessageMediaGrid } from '../../tickets/MessageMediaGrid';
import { linkifyText } from '../../../utils/linkify'; import { linkifyText } from '../../../utils/linkify';
import { ChatIcon, BackIcon, SendIcon } from '@/components/icons';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
// Tickets tab — list view + chat view (selected ticket replaces list). // Tickets tab — list view + chat view (selected ticket replaces list).
@@ -155,19 +156,7 @@ function EmptyState() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex flex-col items-center justify-center rounded-xl bg-dark-800/50 py-12"> <div className="flex flex-col items-center justify-center rounded-xl bg-dark-800/50 py-12">
<svg <ChatIcon className="mb-3 h-12 w-12 text-dark-600" />
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>
<p className="text-dark-400">{t('admin.users.detail.noTickets')}</p> <p className="text-dark-400">{t('admin.users.detail.noTickets')}</p>
</div> </div>
); );
@@ -265,15 +254,7 @@ function ChatView({
aria-label={t('common.back', 'Back')} 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" 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 <BackIcon className="h-4 w-4 text-dark-400" />
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>
</button> </button>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate font-medium text-dark-100"> <div className="truncate font-medium text-dark-100">
@@ -372,19 +353,7 @@ function ChatView({
{replySending ? ( {replySending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<svg <SendIcon className="h-5 w-5" />
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>
)} )}
</button> </button>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
import { InfoIcon } from '@/components/icons';
import { useBlockingStore } from '../../store/blocking'; import { useBlockingStore } from '../../store/blocking';
import { useFocusTrap } from '../../hooks/useFocusTrap'; import { useFocusTrap } from '../../hooks/useFocusTrap';
@@ -55,20 +56,7 @@ export default function AccountDeletedScreen() {
<div className="w-full max-w-md text-center"> <div className="w-full max-w-md text-center">
<div className="mb-8"> <div className="mb-8">
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800"> <div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
<svg <InfoIcon className="h-12 w-12 text-warning-400" />
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>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBlockingStore } from '../../store/blocking'; import { useBlockingStore } from '../../store/blocking';
import { useFocusTrap } from '../../hooks/useFocusTrap'; import { useFocusTrap } from '../../hooks/useFocusTrap';
import { BanIcon } from '@/components/icons';
export default function BlacklistedScreen() { export default function BlacklistedScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -20,19 +21,7 @@ export default function BlacklistedScreen() {
{/* Icon */} {/* Icon */}
<div className="mb-8"> <div className="mb-8">
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800"> <div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
<svg <BanIcon className="h-12 w-12 text-error-500" />
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>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useBlockingStore } from '../../store/blocking';
import { apiClient, isChannelSubscriptionError } from '../../api/client'; import { apiClient, isChannelSubscriptionError } from '../../api/client';
import { usePlatform } from '../../platform'; import { usePlatform } from '../../platform';
import { useFocusTrap } from '../../hooks/useFocusTrap'; import { useFocusTrap } from '../../hooks/useFocusTrap';
import { TelegramIcon, ClockIcon, CheckIcon } from '@/components/icons';
const CHECK_COOLDOWN_SECONDS = 5; const CHECK_COOLDOWN_SECONDS = 5;
@@ -96,9 +97,7 @@ export default function ChannelSubscriptionScreen() {
{/* Icon */} {/* Icon */}
<div className="mb-8"> <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"> <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"> <TelegramIcon className="h-12 w-12 text-blue-400" />
<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>
</div> </div>
</div> </div>
@@ -183,31 +182,12 @@ export default function ChannelSubscriptionScreen() {
</> </>
) : cooldown > 0 ? ( ) : cooldown > 0 ? (
<> <>
<svg <ClockIcon className="h-5 w-5 text-dark-500" />
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>
{t('blocking.channel.waitSeconds', { seconds: cooldown })} {t('blocking.channel.waitSeconds', { seconds: cooldown })}
</> </>
) : ( ) : (
<> <>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CheckIcon className="h-5 w-5" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
{t('blocking.channel.checkSubscription')} {t('blocking.channel.checkSubscription')}
</> </>
)} )}

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBlockingStore } from '../../store/blocking'; import { useBlockingStore } from '../../store/blocking';
import { useFocusTrap } from '../../hooks/useFocusTrap'; import { useFocusTrap } from '../../hooks/useFocusTrap';
import { AdjustmentsIcon } from '@/components/icons';
export default function MaintenanceScreen() { export default function MaintenanceScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -20,19 +21,7 @@ export default function MaintenanceScreen() {
{/* Icon */} {/* Icon */}
<div className="mb-8"> <div className="mb-8">
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800"> <div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
<svg <AdjustmentsIcon className="h-12 w-12 text-warning-500" />
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>
</div> </div>
</div> </div>

View File

@@ -12,7 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks'; import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
import type { BlockRendererProps } from './blocks'; import type { BlockRendererProps } from './blocks';
import TvQuickConnect from './TvQuickConnect'; 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']; const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
@@ -259,15 +259,7 @@ export default function InstallationGuide({
))} ))}
</select> </select>
<div className="pointer-events-none absolute right-2.5 text-dark-400"> <div className="pointer-events-none absolute right-2.5 text-dark-400">
<svg <ChevronIcon className="h-4 w-4" />
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>
</div> </div>
</div> </div>
)} )}
@@ -315,19 +307,7 @@ export default function InstallationGuide({
rel="noopener noreferrer" rel="noopener noreferrer"
className="btn-secondary w-full justify-center" className="btn-secondary w-full justify-center"
> >
<svg <DocumentIcon className="h-5 w-5" />
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>
{getBaseTranslation('tutorial', 'subscription.connection.tutorial')} {getBaseTranslation('tutorial', 'subscription.connection.tutorial')}
</a> </a>
)} )}

View File

@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { ChevronDownIcon } from '@/components/icons';
import { getColorGradient } from '@/utils/colorParser'; import { getColorGradient } from '@/utils/colorParser';
import { ThemeIcon } from './ThemeIcon'; import { ThemeIcon } from './ThemeIcon';
import type { BlockRendererProps } from './types'; 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"> <span className="min-w-0 flex-1 truncate font-semibold text-dark-100">
{getLocalizedText(block.title)} {getLocalizedText(block.title)}
</span> </span>
<svg <ChevronDownIcon
className={`h-[18px] w-[18px] shrink-0 text-dark-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} 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> </button>
{/* Panel */} {/* Panel */}
<div <div

View File

@@ -1,6 +1,7 @@
import { Link } from 'react-router'; import { Link } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { GiftIcon } from '@/components/icons';
import type { PendingGift } from '../../api/gift'; import type { PendingGift } from '../../api/gift';
interface PendingGiftCardProps { interface PendingGiftCardProps {
@@ -28,19 +29,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro
<div className="relative flex items-start gap-4"> <div className="relative flex items-start gap-4">
{/* Gift icon */} {/* Gift icon */}
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-accent-500/20"> <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-accent-500/20">
<svg <GiftIcon className="h-6 w-6 text-accent-400" />
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>
</div> </div>
{/* Content */} {/* Content */}

View File

@@ -10,7 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
import { formatTraffic } from '../../utils/formatTraffic'; import { formatTraffic } from '../../utils/formatTraffic';
import { getGlassColors } from '../../utils/glassTheme'; import { getGlassColors } from '../../utils/glassTheme';
import { HoverBorderGradient } from '../ui/hover-border-gradient'; import { HoverBorderGradient } from '../ui/hover-border-gradient';
import { RefreshIcon } from '@/components/icons'; import { CalendarIcon, RefreshIcon } from '@/components/icons';
import { useHaptic } from '../../platform'; import { useHaptic } from '../../platform';
import type { Subscription } from '../../types'; 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, background: daysLeft <= 3 ? 'rgba(var(--color-warning-400), 0.1)' : g.hoverBg,
}} }}
> >
<svg <span
width="13" style={{
height="13" color: daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary,
viewBox="0 0 24 24" }}
fill="none"
stroke={daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true" aria-hidden="true"
> >
<rect x="3" y="4" width="18" height="18" rx="2" /> <CalendarIcon className="h-[13px] w-[13px]" />
<path d="M16 2v4M8 2v4M3 10h18" /> </span>
</svg>
</div> </div>
{t('dashboard.remaining')} {t('dashboard.remaining')}
</div> </div>

View File

@@ -10,7 +10,7 @@ import { useCurrency } from '../../hooks/useCurrency';
import { useHapticFeedback } from '../../platform/hooks/useHaptic'; import { useHapticFeedback } from '../../platform/hooks/useHaptic';
import { getGlassColors } from '../../utils/glassTheme'; import { getGlassColors } from '../../utils/glassTheme';
import { getInsufficientBalanceError } from '../../utils/subscriptionHelpers'; import { getInsufficientBalanceError } from '../../utils/subscriptionHelpers';
import { SubscriptionIcon } from '@/components/icons'; import { ClockIcon, ExclamationIcon, PlusIcon, SubscriptionIcon } from '@/components/icons';
interface SubscriptionCardExpiredProps { interface SubscriptionCardExpiredProps {
subscription: Subscription; subscription: Subscription;
@@ -168,39 +168,13 @@ export default function SubscriptionCardExpired({
style={{ style={{
background: `rgba(${accent.r},${accent.g},${accent.b},0.1)`, background: `rgba(${accent.r},${accent.g},${accent.b},0.1)`,
border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.15)`, border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.15)`,
color: accent.hex,
}} }}
> >
{isLimited ? ( {isLimited ? (
<svg <ExclamationIcon className="h-[22px] w-[22px]" />
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>
) : ( ) : (
<svg <ClockIcon className="h-[22px] w-[22px]" />
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>
)} )}
</div> </div>
<h2 className="text-lg font-bold tracking-tight text-dark-50"> <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)`, boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
}} }}
> >
<svg <PlusIcon className="h-4 w-4" />
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>
{t('subscription.buyTraffic')} {t('subscription.buyTraffic')}
</Link> </Link>
) : ( ) : (
@@ -330,19 +292,7 @@ export default function SubscriptionCardExpired({
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`, boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
}} }}
> >
<svg <PlusIcon className="h-4 w-4" />
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>
{t('dashboard.expired.topUp')} {t('dashboard.expired.topUp')}
</button> </button>
)} )}

View File

@@ -5,6 +5,7 @@ import type { TrialInfo } from '../../types';
import { useCurrency } from '../../hooks/useCurrency'; import { useCurrency } from '../../hooks/useCurrency';
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import { getGlassColors } from '../../utils/glassTheme'; import { getGlassColors } from '../../utils/glassTheme';
import { BoltIcon, SparklesIcon } from '@/components/icons';
interface TrialOfferCardProps { interface TrialOfferCardProps {
trialInfo: TrialInfo; trialInfo: TrialInfo;
@@ -94,37 +95,21 @@ export default function TrialOfferCard({
}} }}
> >
{isFree ? ( {isFree ? (
<svg <span
width="26" className="flex"
height="26" style={{ color: 'rgb(var(--color-accent-400))' }}
viewBox="0 0 24 24"
fill="none"
stroke="rgb(var(--color-accent-400))"
strokeWidth="1.5"
aria-hidden="true" aria-hidden="true"
> >
<path <SparklesIcon className="h-[26px] w-[26px]" />
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" </span>
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : ( ) : (
<svg <span
width="26" className="flex"
height="26" style={{ color: 'rgb(var(--color-urgent-400))' }}
viewBox="0 0 24 24"
fill="none"
stroke="rgb(var(--color-urgent-400))"
strokeWidth="1.5"
aria-hidden="true" aria-hidden="true"
> >
<path <BoltIcon className="h-[26px] w-[26px]" />
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" </span>
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)} )}
{/* Glow effect */} {/* Glow effect */}
<div <div

View File

@@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
import { newsApi } from '../../api/news'; import { newsApi } from '../../api/news';
import { useHapticFeedback } from '../../platform/hooks/useHaptic'; import { useHapticFeedback } from '../../platform/hooks/useHaptic';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { ArrowIcon } from '@/components/icons'; import { ArrowIcon, NewsIcon } from '@/components/icons';
import type { NewsListItem } from '../../types/news'; import type { NewsListItem } from '../../types/news';
// --- Security: hex color validation to prevent CSS injection --- // --- 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="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"> <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"> <NewsIcon className="h-[18px] w-[18px] text-dark-950" />
<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>
</div> </div>
<span className="font-mono text-[11px] font-bold uppercase tracking-[0.18em] text-dark-500"> <span className="font-mono text-[11px] font-bold uppercase tracking-[0.18em] text-dark-500">
{t('news.title')} {t('news.title')}

View File

@@ -1,7 +1,7 @@
import { Link } from 'react-router'; import { Link } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { HoverBorderGradient } from '../ui/hover-border-gradient'; import { HoverBorderGradient } from '../ui/hover-border-gradient';
import { SubscriptionIcon } from '@/components/icons'; import { ChevronRightIcon, SubscriptionIcon } from '@/components/icons';
import type { Subscription } from '../../types'; import type { Subscription } from '../../types';
interface PurchaseCTAButtonProps { interface PurchaseCTAButtonProps {
@@ -86,20 +86,7 @@ export default function PurchaseCTAButton({
</div> </div>
{/* Right: chevron */} {/* Right: chevron */}
<svg <ChevronRightIcon className="h-5 w-5 flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1" />
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>
</div> </div>
</HoverBorderGradient> </HoverBorderGradient>
</Link> </Link>

View File

@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import { getGlassColors } from '../../utils/glassTheme'; import { getGlassColors } from '../../utils/glassTheme';
import { useHaptic } from '../../platform'; import { useHaptic } from '../../platform';
import { CalendarIcon, CheckIcon, ChevronRightIcon, DevicesIcon } from '@/components/icons';
import type { SubscriptionListItem } from '../../types'; import type { SubscriptionListItem } from '../../types';
function formatDate(iso: string | null, locale?: string): string { function formatDate(iso: string | null, locale?: string): string {
@@ -136,15 +137,7 @@ export default function SubscriptionListCard({
</span> </span>
<StatusBadge status={subscription.status} isTrial={isTrial} t={t} /> <StatusBadge status={subscription.status} isTrial={isTrial} t={t} />
</div> </div>
<svg <ChevronRightIcon className="h-4 w-4 shrink-0 opacity-30" />
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>
</div> </div>
{/* Traffic mini progress bar */} {/* Traffic mini progress bar */}
@@ -177,29 +170,11 @@ export default function SubscriptionListCard({
style={{ color: g.textSecondary }} style={{ color: g.textSecondary }}
> >
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg <DevicesIcon className="h-3.5 w-3.5 opacity-50" />
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>
{subscription.device_limit} {subscription.device_limit}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg <CalendarIcon className="h-3.5 w-3.5 opacity-50" />
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>
{formatDate(subscription.end_date, i18n.language)} {formatDate(subscription.end_date, i18n.language)}
</span> </span>
{!isTrial && {!isTrial &&
@@ -213,19 +188,19 @@ export default function SubscriptionListCard({
<span <span
className={`flex items-center gap-1 ${enabled ? 'text-success-400/70' : 'text-error-400/50'}`} className={`flex items-center gap-1 ${enabled ? 'text-success-400/70' : 'text-error-400/50'}`}
> >
<svg {enabled ? (
className="h-3 w-3" <CheckIcon className="h-3 w-3" />
viewBox="0 0 24 24" ) : (
fill="none" <svg
stroke="currentColor" className="h-3 w-3"
strokeWidth={2.5} viewBox="0 0 24 24"
> fill="none"
{enabled ? ( stroke="currentColor"
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" /> strokeWidth={2.5}
) : ( >
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
)} </svg>
</svg> )}
{label} {label}
</span> </span>
); );

View File

@@ -4,6 +4,7 @@ import { useTheme } from '../../../hooks/useTheme';
import { useCurrency } from '../../../hooks/useCurrency'; import { useCurrency } from '../../../hooks/useCurrency';
import { usePromoDiscount } from '../../../hooks/usePromoDiscount'; import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
import { getGlassColors } from '../../../utils/glassTheme'; import { getGlassColors } from '../../../utils/glassTheme';
import { ArrowDownIcon, DevicesIcon, RestartIcon } from '@/components/icons';
import type { Tariff, Subscription, PurchaseOptions } from '../../../types'; import type { Tariff, Subscription, PurchaseOptions } from '../../../types';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
@@ -169,35 +170,11 @@ export function TariffPickerGrid({
</div> </div>
<div className="flex flex-wrap gap-4 text-sm"> <div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<svg <ArrowDownIcon className="h-4 w-4 text-accent-400" />
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>
<span className="font-medium text-dark-200">{tariff.traffic_limit_label}</span> <span className="font-medium text-dark-200">{tariff.traffic_limit_label}</span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<svg <DevicesIcon className="h-4 w-4 text-dark-400" />
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>
<span className="text-dark-300"> <span className="text-dark-300">
{tariff.device_limit === 0 {tariff.device_limit === 0
? '∞' ? '∞'
@@ -206,19 +183,7 @@ export function TariffPickerGrid({
</div> </div>
{tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && ( {tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<svg <RestartIcon className="h-4 w-4 text-dark-400" />
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>
<span className="text-dark-300"> <span className="text-dark-300">
{t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)} {t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
</span> </span>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TrashIcon } from '@/components/icons';
import { subscriptionApi } from '../../../api/subscription'; import { subscriptionApi } from '../../../api/subscription';
import { usePlatform } from '../../../platform'; import { usePlatform } from '../../../platform';
import { useDestructiveConfirm } from '../../../platform/hooks/useNativeDialog'; import { useDestructiveConfirm } from '../../../platform/hooks/useNativeDialog';
@@ -72,19 +73,7 @@ export function DeleteSubscriptionSheet({
disabled={deleteLoading} 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" 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 <TrashIcon className="h-4 w-4" />
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>
{t('subscription.delete', 'Удалить подписку')} {t('subscription.delete', 'Удалить подписку')}
</button> </button>
); );

View File

@@ -1,5 +1,8 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ChevronLeftIcon, ChevronRightIcon, DocumentIcon, XIcon } from '@/components/icons';
import { ticketsApi } from '../../api/tickets'; import { ticketsApi } from '../../api/tickets';
export interface MediaItem { export interface MediaItem {
@@ -150,19 +153,7 @@ export function MessageMediaGrid({
rel="noopener noreferrer" 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" 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 <DocumentIcon className="h-4 w-4" />
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>
{item.caption || `Download ${item.type}`} {item.caption || `Download ${item.type}`}
</a> </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" 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} onClick={closeFullscreen}
> >
<svg <XIcon className="h-5 w-5" />
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>
</button> </button>
{photoItems.length > 1 && ( {photoItems.length > 1 && (
@@ -199,15 +182,7 @@ export function MessageMediaGrid({
onClick={() => setFullscreenIndex(fullscreenIndex - 1)} 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" 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 <ChevronLeftIcon className="h-5 w-5" />
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>
</button> </button>
<button <button
type="button" type="button"
@@ -215,15 +190,7 @@ export function MessageMediaGrid({
onClick={() => setFullscreenIndex(fullscreenIndex + 1)} 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" 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 <ChevronRightIcon className="h-5 w-5" />
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>
</button> </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"> <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} {fullscreenIndex + 1} / {photoItems.length}

View File

@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { adminAppsApi } from '../api/adminApps'; import { adminAppsApi } from '../api/adminApps';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon } from '@/components/icons';
export default function AdminApps() { export default function AdminApps() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -45,15 +46,7 @@ export default function AdminApps() {
onClick={() => navigate('/admin')} onClick={() => navigate('/admin')}
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600" className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
> >
<svg <BackIcon className="h-5 w-5 text-dark-400" />
className="h-5 w-5 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>
</button> </button>
)} )}
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('admin.apps.title')}</h1> <h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('admin.apps.title')}</h1>

View File

@@ -34,6 +34,9 @@ import {
TrafficIcon, TrafficIcon,
ReportIcon, ReportIcon,
HealthIcon, HealthIcon,
ExclamationIcon,
BackIcon,
XIcon,
} from '@/components/icons'; } from '@/components/icons';
type TabType = type TabType =
@@ -393,34 +396,10 @@ export default function AdminBanSystem() {
<div className="mb-6 flex justify-center"> <div className="mb-6 flex justify-center">
<div className="relative"> <div className="relative">
<div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br from-error-500/20 to-warning-500/20"> <div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br from-error-500/20 to-warning-500/20">
<svg <ExclamationIcon className="h-10 w-10 text-error-400" />
className="h-10 w-10 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
</div> </div>
<div className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full border border-dark-600 bg-dark-800"> <div className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full border border-dark-600 bg-dark-800">
<svg <SettingsIcon className="h-3.5 w-3.5 text-dark-400" />
className="h-3.5 w-3.5 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
</svg>
</div> </div>
</div> </div>
</div> </div>
@@ -454,19 +433,7 @@ export default function AdminBanSystem() {
onClick={() => window.history.back()} onClick={() => window.history.back()}
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dark-600 bg-dark-700 px-4 py-2 text-sm font-medium text-dark-200 transition-all duration-200 hover:border-dark-500 hover:bg-dark-600 hover:text-dark-100" className="flex w-full items-center justify-center gap-2 rounded-lg border border-dark-600 bg-dark-700 px-4 py-2 text-sm font-medium text-dark-200 transition-all duration-200 hover:border-dark-500 hover:bg-dark-600 hover:text-dark-100"
> >
<svg <BackIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
{t('common.back')} {t('common.back')}
</button> </button>
</div> </div>
@@ -1444,14 +1411,7 @@ export default function AdminBanSystem() {
aria-label={t('common.close')} aria-label={t('common.close')}
className="text-dark-400 hover:text-dark-200" className="text-dark-400 hover:text-dark-200"
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <XIcon className="h-5 w-5" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
<div className="space-y-4 p-4"> <div className="space-y-4 p-4">

View File

@@ -13,7 +13,7 @@ import { partnerApi } from '../api/partners';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import Twemoji from 'react-twemoji'; import Twemoji from 'react-twemoji';
import { CampaignIcon, CheckIcon, RefreshIcon } from '@/components/icons'; import { CampaignIcon, CheckIcon, LinkIcon, RefreshIcon } from '@/components/icons';
// Bonus type config // Bonus type config
const bonusTypeConfig: Record< const bonusTypeConfig: Record<
@@ -273,19 +273,7 @@ export default function AdminCampaignCreate() {
{partnerId && partner && ( {partnerId && partner && (
<div className="rounded-xl border border-accent-500/20 bg-accent-500/5 p-4"> <div className="rounded-xl border border-accent-500/20 bg-accent-500/5 p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<svg <LinkIcon className="mt-0.5 h-5 w-5 shrink-0 text-accent-400" />
className="mt-0.5 h-5 w-5 shrink-0 text-accent-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
/>
</svg>
<p className="text-sm text-accent-300"> <p className="text-sm text-accent-300">
{t('admin.campaigns.form.partnerAutoAssign', { {t('admin.campaigns.form.partnerAutoAssign', {
name: partner.first_name || partner.username || `#${partnerId}`, name: partner.first_name || partner.username || `#${partnerId}`,

View File

@@ -10,7 +10,7 @@ import { PARTNER_STATS } from '../constants/partner';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { copyToClipboard } from '../utils/clipboard'; import { copyToClipboard } from '../utils/clipboard';
import { useHaptic } from '../platform'; import { useHaptic } from '../platform';
import { ChartIcon, CopyIcon, LinkIcon, UsersIcon } from '@/components/icons'; import { ChartIcon, ChevronDownIcon, CopyIcon, LinkIcon, UsersIcon } from '@/components/icons';
// Bonus type config // Bonus type config
const bonusTypeConfig: Record< const bonusTypeConfig: Record<
@@ -449,15 +449,9 @@ export default function AdminCampaignStats() {
{t('admin.campaigns.stats.users')} ({stats.registrations}) {t('admin.campaigns.stats.users')} ({stats.registrations})
</span> </span>
</div> </div>
<svg <ChevronDownIcon
className={`h-5 w-5 text-dark-400 transition-transform ${showUsers ? 'rotate-180' : ''}`} className={`h-5 w-5 text-dark-400 transition-transform ${showUsers ? '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> </button>
{showUsers && ( {showUsers && (

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { partnerApi } from '../api/partners'; import { partnerApi } from '../api/partners';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { XIcon } from '@/components/icons';
// Status badge config — keys must match backend PartnerStatus enum values // Status badge config — keys must match backend PartnerStatus enum values
const statusConfig: Record<string, { labelKey: string; color: string; bgColor: string }> = { const statusConfig: Record<string, { labelKey: string; color: string; bgColor: string }> = {
@@ -259,19 +260,7 @@ export default function AdminPartnerDetail() {
className="rounded p-1 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400" className="rounded p-1 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400"
title={t('admin.partnerDetail.campaigns.unassign')} title={t('admin.partnerDetail.campaigns.unassign')}
> >
<svg <XIcon className="h-4 w-4" />
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>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -9,6 +9,7 @@ import {
} from '../api/partners'; } from '../api/partners';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { ChevronRightIcon, SettingsIcon } from '@/components/icons';
export default function AdminPartners() { export default function AdminPartners() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -50,24 +51,7 @@ export default function AdminPartners() {
className="rounded-lg bg-dark-800 p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200" className="rounded-lg bg-dark-800 p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
title={t('admin.partners.settings')} title={t('admin.partners.settings')}
> >
<svg <SettingsIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button> </button>
</div> </div>
@@ -167,19 +151,7 @@ export default function AdminPartners() {
</span> </span>
</div> </div>
</div> </div>
<svg <ChevronRightIcon className="h-5 w-5 shrink-0 text-dark-500" />
className="h-5 w-5 shrink-0 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</div> </div>
</button> </button>
))} ))}

View File

@@ -6,7 +6,13 @@ import { adminPaymentsApi, type SearchStats } from '../api/adminPayments';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import type { PendingPayment, PaginatedResponse } from '../types'; import type { PendingPayment, PaginatedResponse } from '../types';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon, SearchIcon, CalendarIcon } from '@/components/icons'; import {
BackIcon,
SearchIcon,
CalendarIcon,
RefreshIcon,
CheckCircleIcon,
} from '@/components/icons';
interface StatusBadgeProps { interface StatusBadgeProps {
status: string; status: string;
@@ -210,19 +216,7 @@ export default function AdminPayments() {
</div> </div>
</div> </div>
<button onClick={() => refetch()} className="btn-secondary flex items-center gap-2"> <button onClick={() => refetch()} className="btn-secondary flex items-center gap-2">
<svg <RefreshIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<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.182m0-4.991v4.99"
/>
</svg>
{t('common.refresh')} {t('common.refresh')}
</button> </button>
</div> </div>
@@ -574,19 +568,7 @@ export default function AdminPayments() {
) : ( ) : (
<div className="py-12 text-center"> <div className="py-12 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
<svg <CheckCircleIcon className="h-8 w-8 text-dark-500" />
className="h-8 w-8 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
<div className="text-dark-400">{t('admin.payments.noPayments')}</div> <div className="text-dark-400">{t('admin.payments.noPayments')}</div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { rbacApi, AccessPolicy, CreatePolicyPayload, UpdatePolicyPayload } from '@/api/rbac'; import { rbacApi, AccessPolicy, CreatePolicyPayload, UpdatePolicyPayload } from '@/api/rbac';
import { AdminBackButton } from '@/components/admin'; import { AdminBackButton } from '@/components/admin';
import { XIcon } from '@/components/icons';
// === Types === // === Types ===
@@ -150,15 +151,7 @@ function IpTagInput({ values, onChange }: IpTagInputProps) {
className="text-dark-400 transition-colors hover:text-dark-200" className="text-dark-400 transition-colors hover:text-dark-200"
aria-label={t('admin.policies.conditions.removeIp', { ip })} aria-label={t('admin.policies.conditions.removeIp', { ip })}
> >
<svg <XIcon className="h-3 w-3" />
className="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</span> </span>
))} ))}

View File

@@ -19,6 +19,7 @@ import {
UserIcon, UserIcon,
SearchIcon, SearchIcon,
CloseIcon, CloseIcon,
XIcon,
} from '@/components/icons'; } from '@/components/icons';
const getOfferTypeIcon = (offerType: string): string => { const getOfferTypeIcon = (offerType: string): string => {
@@ -188,25 +189,9 @@ export default function AdminPromoOfferSend() {
}`} }`}
> >
{result.isSuccess ? ( {result.isSuccess ? (
<svg <CheckIcon className="h-8 w-8 text-success-400" />
className="h-8 w-8 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
) : ( ) : (
<svg <XIcon className="h-8 w-8 text-error-400" />
className="h-8 w-8 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
)} )}
</div> </div>
<h3 className="mb-2 text-lg font-semibold text-dark-100">{result.title}</h3> <h3 className="mb-2 text-lg font-semibold text-dark-100">{result.title}</h3>

View File

@@ -105,19 +105,7 @@ function SortableTariffCard({
)} )}
{tariff.show_in_gift && ( {tariff.show_in_gift && (
<span className="inline-flex items-center gap-1 rounded bg-purple-500/20 px-2 py-0.5 text-xs text-purple-400"> <span className="inline-flex items-center gap-1 rounded bg-purple-500/20 px-2 py-0.5 text-xs text-purple-400">
<svg <GiftIcon className="h-3 w-3" />
className="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<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>
{t('admin.tariffs.giftBadge')} {t('admin.tariffs.giftBadge')}
</span> </span>
)} )}

View File

@@ -9,7 +9,7 @@ import { adminApi, AdminTicket, AdminTicketDetail } from '../api/admin';
import { ticketsApi } from '../api/tickets'; import { ticketsApi } from '../api/tickets';
import { copyToClipboard as copyText } from '../utils/clipboard'; import { copyToClipboard as copyText } from '../utils/clipboard';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon } from '@/components/icons'; import { BackIcon, SettingsIcon, TicketIcon, XIcon } from '@/components/icons';
interface MediaAttachment { interface MediaAttachment {
id: string; id: string;
@@ -270,24 +270,7 @@ export default function AdminTickets() {
onClick={() => navigate('/admin/tickets/settings')} onClick={() => navigate('/admin/tickets/settings')}
className="btn-secondary flex items-center gap-2" className="btn-secondary flex items-center gap-2"
> >
<svg <SettingsIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{t('admin.tickets.settings')} {t('admin.tickets.settings')}
</button> </button>
</div> </div>
@@ -433,19 +416,7 @@ export default function AdminTickets() {
{!selectedTicketId ? ( {!selectedTicketId ? (
<div className="flex h-64 flex-col items-center justify-center"> <div className="flex h-64 flex-col items-center justify-center">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800"> <div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
<svg <TicketIcon className="h-8 w-8 text-dark-500" />
className="h-8 w-8 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z"
/>
</svg>
</div> </div>
<div className="text-dark-400">{t('admin.tickets.selectTicket')}</div> <div className="text-dark-400">{t('admin.tickets.selectTicket')}</div>
</div> </div>
@@ -593,19 +564,7 @@ export default function AdminTickets() {
onClick={() => removeAttachment(idx)} onClick={() => removeAttachment(idx)}
className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-dark-600 text-dark-300 hover:bg-error-500 hover:text-white" className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-dark-600 text-dark-300 hover:bg-error-500 hover:text-white"
> >
<svg <XIcon className="h-3 w-3" />
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>
</button> </button>
</div> </div>
))} ))}

View File

@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { withdrawalApi } from '../api/withdrawals'; import { withdrawalApi } from '../api/withdrawals';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { WarningIcon } from '@/components/icons';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { import {
formatDate, formatDate,
@@ -237,19 +238,7 @@ export default function AdminWithdrawalDetail() {
key={index} key={index}
className="flex items-start gap-2 rounded-lg bg-error-500/10 px-3 py-2" className="flex items-start gap-2 rounded-lg bg-error-500/10 px-3 py-2"
> >
<svg <WarningIcon className="mt-0.5 h-4 w-4 shrink-0 text-error-400" />
className="mt-0.5 h-4 w-4 shrink-0 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
<span className="text-sm text-error-300">{flag}</span> <span className="text-sm text-error-300">{flag}</span>
</div> </div>
))} ))}

View File

@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { withdrawalApi, AdminWithdrawalItem } from '../api/withdrawals'; import { withdrawalApi, AdminWithdrawalItem } from '../api/withdrawals';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { ChevronRightIcon } from '@/components/icons';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { formatDate, getWithdrawalStatusBadge, getRiskColor } from '../utils/withdrawalUtils'; import { formatDate, getWithdrawalStatusBadge, getRiskColor } from '../utils/withdrawalUtils';
@@ -150,19 +151,7 @@ export default function AdminWithdrawals() {
</div> </div>
{/* Chevron right */} {/* Chevron right */}
<svg <ChevronRightIcon className="mt-1 h-5 w-5 shrink-0 text-dark-500" />
className="mt-1 h-5 w-5 shrink-0 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</div> </div>
</button> </button>
); );

View File

@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { authApi } from '../api/auth'; import { authApi } from '../api/auth';
import { useAuthStore } from '../store/auth'; import { useAuthStore } from '../store/auth';
import { XIcon } from '@/components/icons';
export default function AutoLogin() { export default function AutoLogin() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -51,15 +52,7 @@ export default function AutoLogin() {
{error ? ( {error ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-error-500/10"> <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-error-500/10">
<svg <XIcon className="h-8 w-8 text-error-400" />
className="h-8 w-8 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div> </div>
<p className="text-sm text-dark-300">{t('landing.autoLoginFailed')}</p> <p className="text-sm text-dark-300">{t('landing.autoLoginFailed')}</p>
<button <button

View File

@@ -6,6 +6,7 @@ import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
import { subscriptionApi } from '../api/subscription'; import { subscriptionApi } from '../api/subscription';
import { useTelegramSDK } from '../hooks/useTelegramSDK'; import { useTelegramSDK } from '../hooks/useTelegramSDK';
import { useHaptic } from '@/platform'; import { useHaptic } from '@/platform';
import { SettingsIcon } from '@/components/icons';
import { resolveTemplate, hasTemplates } from '../utils/templateEngine'; import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
import { isHappCryptolinkMode, resolveConnectionUrlForUi } from '../utils/connectionLink'; import { isHappCryptolinkMode, resolveConnectionUrlForUi } from '../utils/connectionLink';
import { useAuthStore } from '../store/auth'; import { useAuthStore } from '../store/auth';
@@ -182,24 +183,7 @@ export default function Connection() {
</p> </p>
{isAdmin && ( {isAdmin && (
<Link to="/admin/apps" className="btn-primary inline-flex items-center gap-2 px-6 py-2.5"> <Link to="/admin/apps" className="btn-primary inline-flex items-center gap-2 px-6 py-2.5">
<svg <SettingsIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{t('subscription.connection.goToApps')} {t('subscription.connection.goToApps')}
</Link> </Link>
)} )}

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { contestsApi, ContestInfo, ContestGameData } from '../api/contests'; import { contestsApi, ContestInfo, ContestGameData } from '../api/contests';
import { GamepadIcon, TrophyIcon } from '@/components/icons'; import { GamepadIcon, TrophyIcon, XIcon } from '@/components/icons';
export default function Contests() { export default function Contests() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -87,14 +87,7 @@ export default function Contests() {
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-bold">{selectedContest.name}</h2> <h2 className="text-xl font-bold">{selectedContest.name}</h2>
<button onClick={handleCloseGame} className="text-dark-400 hover:text-dark-200"> <button onClick={handleCloseGame} className="text-dark-400 hover:text-dark-200">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <XIcon className="h-6 w-6" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>

View File

@@ -21,7 +21,7 @@ import { promoApi } from '../api/promo';
import PendingGiftCard from '../components/dashboard/PendingGiftCard'; import PendingGiftCard from '../components/dashboard/PendingGiftCard';
import SubscriptionListCard from '../components/subscription/SubscriptionListCard'; import SubscriptionListCard from '../components/subscription/SubscriptionListCard';
import { API } from '../config/constants'; import { API } from '../config/constants';
import { ChevronRightIcon } from '@/components/icons'; import { ChevronRightIcon, StarIcon } from '@/components/icons';
export default function Dashboard() { export default function Dashboard() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -263,16 +263,7 @@ export default function Dashboard() {
color: 'rgb(var(--color-accent-400))', color: 'rgb(var(--color-accent-400))',
}} }}
> >
<svg <StarIcon filled className="h-2.5 w-2.5 shrink-0" />
className="shrink-0"
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span className="truncate">{promoGroupData.group_name}</span> <span className="truncate">{promoGroupData.group_name}</span>
</span> </span>
)} )}

View File

@@ -4,6 +4,13 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { brandingApi } from '../api/branding'; import { brandingApi } from '../api/branding';
import { copyToClipboard } from '../utils/clipboard'; import { copyToClipboard } from '../utils/clipboard';
import {
CheckIcon,
CopyIcon,
ExclamationIcon,
ExternalLinkIcon,
LinkIcon,
} from '@/components/icons';
type Status = 'countdown' | 'fallback' | 'error'; type Status = 'countdown' | 'fallback' | 'error';
@@ -209,19 +216,7 @@ export default function DeepLinkRedirect() {
onClick={openDeepLink} onClick={openDeepLink}
className="btn-primary flex w-full items-center justify-center gap-2 py-3" className="btn-primary flex w-full items-center justify-center gap-2 py-3"
> >
<svg <ExternalLinkIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
/>
</svg>
{t('deepLink.openApp')} {t('deepLink.openApp')}
</button> </button>
</div> </div>
@@ -243,36 +238,12 @@ export default function DeepLinkRedirect() {
> >
{copied ? ( {copied ? (
<> <>
<svg <CheckIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
{t('deepLink.copied')} {t('deepLink.copied')}
</> </>
) : ( ) : (
<> <>
<svg <CopyIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
{t('deepLink.copyLink')} {t('deepLink.copyLink')}
</> </>
)} )}
@@ -283,19 +254,7 @@ export default function DeepLinkRedirect() {
onClick={openDeepLink} onClick={openDeepLink}
className="btn-secondary flex w-full items-center justify-center gap-2" className="btn-secondary flex w-full items-center justify-center gap-2"
> >
<svg <ExternalLinkIcon className="h-5 w-5" />
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
/>
</svg>
{t('deepLink.tryAgain')} {t('deepLink.tryAgain')}
</button> </button>
@@ -327,19 +286,7 @@ export default function DeepLinkRedirect() {
{status === 'error' && ( {status === 'error' && (
<div className="card !bg-dark-800/80 p-6 backdrop-blur-sm"> <div className="card !bg-dark-800/80 p-6 backdrop-blur-sm">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-error-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-error-500/20">
<svg <ExclamationIcon className="h-8 w-8 text-error-400" />
className="h-8 w-8 text-error-400"
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>
</div> </div>
<p className="mb-2 font-medium text-dark-200">{t('deepLink.errorTitle')}</p> <p className="mb-2 font-medium text-dark-200">{t('deepLink.errorTitle')}</p>
<p className="mb-6 text-sm text-dark-400">{t('deepLink.errorDesc')}</p> <p className="mb-6 text-sm text-dark-400">{t('deepLink.errorDesc')}</p>
@@ -351,19 +298,7 @@ export default function DeepLinkRedirect() {
{/* Footer */} {/* Footer */}
<div className="mt-8 flex items-center justify-center gap-2 text-dark-600"> <div className="mt-8 flex items-center justify-center gap-2 text-dark-600">
<svg <LinkIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
/>
</svg>
<span className="text-xs">VPN Config Redirect</span> <span className="text-xs">VPN Config Redirect</span>
</div> </div>
</div> </div>

View File

@@ -9,6 +9,7 @@ import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark'; import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { copyToClipboard } from '@/utils/clipboard'; import { copyToClipboard } from '@/utils/clipboard';
import { CheckIcon, CopyIcon, InfoIcon, ExclamationIcon, ClockIcon } from '@/components/icons';
const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes
@@ -147,32 +148,12 @@ function CodeOnlySuccessState({
> >
{copied ? ( {copied ? (
<> <>
<svg <CheckIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
{t('common.copied', 'Copied!')} {t('common.copied', 'Copied!')}
</> </>
) : ( ) : (
<> <>
<svg <CopyIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
{t('gift.copyMessage', 'Copy message')} {t('gift.copyMessage', 'Copy message')}
</> </>
)} )}
@@ -272,19 +253,7 @@ function PendingActivationState({
> >
{/* Info icon */} {/* Info icon */}
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10">
<svg <InfoIcon className="h-10 w-10 text-warning-400" />
className="h-10 w-10 text-warning-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
</div> </div>
<div> <div>
@@ -372,19 +341,7 @@ function PollErrorState() {
className="flex flex-col items-center gap-6 text-center" className="flex flex-col items-center gap-6 text-center"
> >
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10">
<svg <ExclamationIcon className="h-10 w-10 text-warning-400" />
className="h-10 w-10 text-warning-400"
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>
</div> </div>
<div> <div>
@@ -420,19 +377,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
className="flex flex-col items-center gap-6 text-center" className="flex flex-col items-center gap-6 text-center"
> >
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50">
<svg <ClockIcon className="h-10 w-10 text-dark-400" />
className="h-10 w-10 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-50"> <h1 className="text-xl font-bold text-dark-50">
@@ -467,19 +412,7 @@ function NoTokenState() {
className="flex flex-col items-center gap-6 text-center" className="flex flex-col items-center gap-6 text-center"
> >
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50">
<svg <ExclamationIcon className="h-10 w-10 text-dark-400" />
className="h-10 w-10 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-50">{t('gift.noToken', 'Invalid link')}</h1> <h1 className="text-xl font-bold text-dark-50">{t('gift.noToken', 'Invalid link')}</h1>

View File

@@ -21,6 +21,7 @@ import { getApiErrorMessage } from '../utils/api-error';
import { formatPrice } from '../utils/format'; import { formatPrice } from '../utils/format';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { usePlatform, useHaptic } from '@/platform'; import { usePlatform, useHaptic } from '@/platform';
import { SparklesIcon } from '@/components/icons';
function GiftIcon({ className }: { className?: string }) { function GiftIcon({ className }: { className?: string }) {
return ( return (
@@ -686,17 +687,7 @@ function BuyTabContent({
{config.promo_group_name && ( {config.promo_group_name && (
<div className="flex items-center gap-3 rounded-xl border border-success-500/30 bg-success-500/10 p-3"> <div className="flex items-center gap-3 rounded-xl border border-success-500/30 bg-success-500/10 p-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-success-500/20"> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-success-500/20">
<svg <SparklesIcon className="h-4 w-4 text-success-400" />
className="h-4 w-4 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<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.09z" />
</svg>
</div> </div>
<div> <div>
<div className="text-sm font-medium text-success-400"> <div className="text-sm font-medium text-success-400">
@@ -713,17 +704,7 @@ function BuyTabContent({
{config.active_discount_percent != null && config.active_discount_percent > 0 && ( {config.active_discount_percent != null && config.active_discount_percent > 0 && (
<div className="flex items-center gap-3 rounded-xl border border-warning-500/30 bg-warning-500/10 p-3"> <div className="flex items-center gap-3 rounded-xl border border-warning-500/30 bg-warning-500/10 p-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-warning-500/20"> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-warning-500/20">
<svg <SparklesIcon className="h-4 w-4 text-warning-400" />
className="h-4 w-4 text-warning-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<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.09z" />
</svg>
</div> </div>
<div className="text-sm font-medium text-warning-400"> <div className="text-sm font-medium text-warning-400">
{t('promo.discountApplied')} -{config.active_discount_percent}% {t('promo.discountApplied')} -{config.active_discount_percent}%

View File

@@ -23,6 +23,7 @@ import TelegramLoginButton from '../components/TelegramLoginButton';
import OAuthProviderIcon from '../components/OAuthProviderIcon'; import OAuthProviderIcon from '../components/OAuthProviderIcon';
import { saveOAuthState } from '../utils/oauth'; import { saveOAuthState } from '../utils/oauth';
import { getPendingReferralCode } from '../utils/referral'; import { getPendingReferralCode } from '../utils/referral';
import { UsersIcon, EmailIcon, RefreshIcon, ChevronDownIcon } from '@/components/icons';
export default function Login() { export default function Login() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -362,19 +363,7 @@ export default function Login() {
{referralCode && isEmailAuthEnabled && ( {referralCode && isEmailAuthEnabled && (
<div className="mt-3 rounded-xl border border-accent-500/30 bg-accent-500/10 p-2.5"> <div className="mt-3 rounded-xl border border-accent-500/30 bg-accent-500/10 p-2.5">
<div className="flex items-center justify-center gap-2 text-accent-400"> <div className="flex items-center justify-center gap-2 text-accent-400">
<svg <UsersIcon className="h-4 w-4 flex-shrink-0" />
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
<span className="text-xs font-medium">{t('auth.referralInvite')}</span> <span className="text-xs font-medium">{t('auth.referralInvite')}</span>
</div> </div>
</div> </div>
@@ -385,19 +374,7 @@ export default function Login() {
{registeredEmail ? ( {registeredEmail ? (
<div className="card text-center"> <div className="card text-center">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-success-500/20"> <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-success-500/20">
<svg <EmailIcon className="h-7 w-7 text-success-400" />
className="h-7 w-7 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
</div> </div>
<h2 className="mb-2 text-lg font-bold text-dark-50"> <h2 className="mb-2 text-lg font-bold text-dark-50">
{t('auth.checkEmail', 'Check your email')} {t('auth.checkEmail', 'Check your email')}
@@ -447,19 +424,7 @@ export default function Login() {
onClick={handleRetryTelegramAuth} onClick={handleRetryTelegramAuth}
className="btn-primary mx-auto flex items-center gap-2 px-5 py-2.5" className="btn-primary mx-auto flex items-center gap-2 px-5 py-2.5"
> >
<svg <RefreshIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<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.182m0-4.991v4.99"
/>
</svg>
{t('auth.tryAgain')} {t('auth.tryAgain')}
</button> </button>
<p className="text-xs text-dark-500"> <p className="text-xs text-dark-500">
@@ -516,29 +481,11 @@ export default function Login() {
onClick={() => setShowEmailForm(!showEmailForm)} onClick={() => setShowEmailForm(!showEmailForm)}
className="flex items-center gap-1.5 rounded-full border border-dark-700 bg-dark-800/60 px-3.5 py-1.5 text-xs font-medium text-dark-300 transition-all hover:border-dark-600 hover:bg-dark-700 hover:text-dark-200" className="flex items-center gap-1.5 rounded-full border border-dark-700 bg-dark-800/60 px-3.5 py-1.5 text-xs font-medium text-dark-300 transition-all hover:border-dark-600 hover:bg-dark-700 hover:text-dark-200"
> >
<svg <EmailIcon className="h-3.5 w-3.5 text-dark-400" />
className="h-3.5 w-3.5 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
<span>{t('auth.loginWithEmail')}</span> <span>{t('auth.loginWithEmail')}</span>
<svg <ChevronDownIcon
className={`h-3 w-3 text-dark-400 transition-transform duration-300 ${showEmailForm ? 'rotate-180' : ''}`} className={`h-3 w-3 text-dark-400 transition-transform duration-300 ${showEmailForm ? 'rotate-180' : ''}`}
fill="none" />
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button> </button>
<div className="h-px flex-1 bg-dark-700" /> <div className="h-px flex-1 bg-dark-700" />
</div> </div>
@@ -557,19 +504,7 @@ export default function Login() {
forgotPasswordSent ? ( forgotPasswordSent ? (
<div className="space-y-4 text-center"> <div className="space-y-4 text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-success-500/20"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-success-500/20">
<svg <EmailIcon className="h-6 w-6 text-success-400" />
className="h-6 w-6 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
</div> </div>
<p className="text-sm font-medium text-dark-100"> <p className="text-sm font-medium text-dark-100">
{t('auth.checkEmail', 'Check your email')} {t('auth.checkEmail', 'Check your email')}

View File

@@ -11,6 +11,7 @@ import {
getErrorDetail, getErrorDetail,
} from '../utils/oauth'; } from '../utils/oauth';
import type { ServerCompleteResponse } from '../types'; import type { ServerCompleteResponse } from '../types';
import { CheckIcon, ExclamationIcon } from '@/components/icons';
type CallbackMode = 'login' | 'link-browser' | 'link-server'; type CallbackMode = 'login' | 'link-browser' | 'link-server';
@@ -145,15 +146,7 @@ export default function OAuthCallback() {
<div className="relative w-full max-w-md text-center"> <div className="relative w-full max-w-md text-center">
<div className="card"> <div className="card">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-success-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-success-500/20">
<svg <CheckIcon className="h-8 w-8 text-success-400" />
className="h-8 w-8 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div> </div>
<h2 className="mb-2 text-lg font-semibold text-dark-50"> <h2 className="mb-2 text-lg font-semibold text-dark-50">
{t('profile.accounts.linkSuccess')} {t('profile.accounts.linkSuccess')}
@@ -209,19 +202,7 @@ export default function OAuthCallback() {
<div className="relative w-full max-w-md text-center"> <div className="relative w-full max-w-md text-center">
<div className="card"> <div className="card">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-error-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-error-500/20">
<svg <ExclamationIcon className="h-8 w-8 text-error-400" />
className="h-8 w-8 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
</div> </div>
<h2 className="mb-2 text-lg font-semibold text-dark-50">{t('auth.loginFailed')}</h2> <h2 className="mb-2 text-lg font-semibold text-dark-50">{t('auth.loginFailed')}</h2>
<p className="mb-6 text-sm text-dark-400">{error}</p> <p className="mb-6 text-sm text-dark-400">{error}</p>

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { pollsApi, PollInfo, PollQuestion } from '../api/polls'; import { pollsApi, PollInfo, PollQuestion } from '../api/polls';
import { useFocusTrap } from '../hooks/useFocusTrap'; import { useFocusTrap } from '../hooks/useFocusTrap';
import { ClipboardIcon, GiftIcon, CheckIcon } from '@/components/icons'; import { ClipboardIcon, GiftIcon, CheckIcon, CloseIcon } from '@/components/icons';
export default function Polls() { export default function Polls() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -135,14 +135,7 @@ export default function Polls() {
aria-label={t('common.close')} aria-label={t('common.close')}
className="text-dark-400 hover:text-dark-200" className="text-dark-400 hover:text-dark-200"
> >
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-6 w-6" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>

View File

@@ -319,15 +319,7 @@ export default function Profile() {
</h2> </h2>
<p className="text-sm text-dark-400">{t('profile.accounts.subtitle')}</p> <p className="text-sm text-dark-400">{t('profile.accounts.subtitle')}</p>
</div> </div>
<svg <ArrowRightIcon className="h-5 w-5 text-dark-400" />
className="h-5 w-5 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div> </div>
</Card> </Card>
</motion.div> </motion.div>

View File

@@ -8,6 +8,7 @@ import { landingApi } from '../api/landings';
import { authApi } from '../api/auth'; import { authApi } from '../api/auth';
import { useAuthStore } from '../store/auth'; import { useAuthStore } from '../store/auth';
import { copyToClipboard } from '../utils/clipboard'; import { copyToClipboard } from '../utils/clipboard';
import { CheckIcon, ClipboardIcon, ClockIcon, ExclamationIcon } from '@/components/icons';
import { Spinner } from '@/components/ui/Spinner'; import { Spinner } from '@/components/ui/Spinner';
import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark'; import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark'; import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
@@ -299,32 +300,12 @@ function SuccessState({
> >
{copied ? ( {copied ? (
<> <>
<svg <CheckIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
{t('landing.copied', 'Copied!')} {t('landing.copied', 'Copied!')}
</> </>
) : ( ) : (
<> <>
<svg <ClipboardIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
{t('landing.copyLink', 'Copy link')} {t('landing.copyLink', 'Copy link')}
</> </>
)} )}
@@ -383,19 +364,7 @@ function PendingActivationState({
> >
{/* Warning icon */} {/* Warning icon */}
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10">
<svg <ExclamationIcon className="h-10 w-10 text-warning-400" />
className="h-10 w-10 text-warning-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
</div> </div>
<div> <div>
@@ -560,19 +529,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
className="flex flex-col items-center gap-6 text-center" className="flex flex-col items-center gap-6 text-center"
> >
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50">
<svg <ClockIcon className="h-10 w-10 text-dark-400" />
className="h-10 w-10 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-50"> <h1 className="text-xl font-bold text-dark-50">

View File

@@ -15,6 +15,7 @@ import type {
PurchaseRequest, PurchaseRequest,
} from '../api/landings'; } from '../api/landings';
import { StaticBackgroundRenderer } from '../components/backgrounds/BackgroundRenderer'; import { StaticBackgroundRenderer } from '../components/backgrounds/BackgroundRenderer';
import { CheckCircleIcon, CheckIcon, DevicesIcon, DownloadIcon } from '@/components/icons';
import LanguageSwitcher from '../components/LanguageSwitcher'; import LanguageSwitcher from '../components/LanguageSwitcher';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { getApiErrorMessage } from '../utils/api-error'; import { getApiErrorMessage } from '../utils/api-error';
@@ -285,52 +286,18 @@ function TariffCard({
isSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600', isSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600',
)} )}
> >
{isSelected && ( {isSelected && <CheckIcon className="h-3 w-3 text-white" />}
<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="M5 13l4 4L19 7" />
</svg>
)}
</div> </div>
</div> </div>
{/* Info row */} {/* Info row */}
<div className="flex items-center gap-3 text-xs text-dark-400"> <div className="flex items-center gap-3 text-xs text-dark-400">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg <DownloadIcon className="h-3.5 w-3.5" />
className="h-3.5 w-3.5"
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.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
{tariff.traffic_limit_gb === 0 ? '∞' : tariff.traffic_limit_gb} {t('landing.gb', 'GB')} {tariff.traffic_limit_gb === 0 ? '∞' : tariff.traffic_limit_gb} {t('landing.gb', 'GB')}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg <DevicesIcon className="h-3.5 w-3.5" />
className="h-3.5 w-3.5"
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>
{tariff.device_limit} {t('landing.devices', 'devices')} {tariff.device_limit} {t('landing.devices', 'devices')}
</span> </span>
</div> </div>
@@ -550,15 +517,7 @@ function SummaryCard({
{config.features.map((feature, idx) => ( {config.features.map((feature, idx) => (
<div key={idx} className="flex gap-3"> <div key={idx} className="flex gap-3">
<div className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-success-500/10"> <div className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-success-500/10">
<svg <CheckCircleIcon className="h-3 w-3 text-success-500" />
className="h-3 w-3 text-success-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div> </div>
<div> <div>
<p className="text-sm font-medium text-dark-100">{feature.title}</p> <p className="text-sm font-medium text-dark-100">{feature.title}</p>

View File

@@ -14,9 +14,12 @@ import {
CheckIcon, CheckIcon,
ClockIcon, ClockIcon,
CopyIcon, CopyIcon,
ExclamationIcon,
LinkIcon, LinkIcon,
PartnerIcon, PartnerIcon,
ShareIcon, ShareIcon,
TelegramIcon,
UsersIcon,
WalletIcon, WalletIcon,
} from '@/components/icons'; } from '@/components/icons';
@@ -212,19 +215,7 @@ export default function Referral() {
return ( return (
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-6"> <div className="flex min-h-[60vh] flex-col items-center justify-center gap-6">
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-dark-800"> <div className="flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
<svg <UsersIcon className="h-12 w-12 text-dark-500" />
className="h-12 w-12 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
</div> </div>
<div className="text-center"> <div className="text-center">
<h1 className="mb-2 text-2xl font-bold text-dark-100">{t('referral.title')}</h1> <h1 className="mb-2 text-2xl font-bold text-dark-100">{t('referral.title')}</h1>
@@ -273,9 +264,7 @@ export default function Referral() {
{botReferralLink && ( {botReferralLink && (
<div> <div>
<div className="mb-1.5 flex items-center gap-2 text-sm font-medium text-dark-300"> <div className="mb-1.5 flex items-center gap-2 text-sm font-medium text-dark-300">
<svg className="h-4 w-4 text-accent-400" viewBox="0 0 24 24" fill="currentColor"> <TelegramIcon className="h-4 w-4 text-accent-400" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.74-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z" />
</svg>
{t('referral.botLink')} {t('referral.botLink')}
</div> </div>
<div className="flex flex-col gap-2 sm:flex-row"> <div className="flex flex-col gap-2 sm:flex-row">
@@ -302,19 +291,7 @@ export default function Referral() {
{/* Cabinet link */} {/* Cabinet link */}
<div> <div>
<div className="mb-1.5 flex items-center gap-2 text-sm font-medium text-dark-300"> <div className="mb-1.5 flex items-center gap-2 text-sm font-medium text-dark-300">
<svg <LinkIcon className="h-4 w-4 text-accent-400" />
className="h-4 w-4 text-accent-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
{t('referral.cabinetLink')} {t('referral.cabinetLink')}
</div> </div>
<div className="flex flex-col gap-2 sm:flex-row"> <div className="flex flex-col gap-2 sm:flex-row">
@@ -383,19 +360,7 @@ export default function Referral() {
) : ( ) : (
<div className="py-12 text-center"> <div className="py-12 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
<svg <UsersIcon className="h-8 w-8 text-dark-500" />
className="h-8 w-8 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
</div> </div>
<div className="text-dark-400">{t('referral.noReferrals')}</div> <div className="text-dark-400">{t('referral.noReferrals')}</div>
</div> </div>
@@ -519,19 +484,7 @@ export default function Referral() {
<div className="bento-card border-error-500/20"> <div className="bento-card border-error-500/20">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-error-500/10 text-error-400"> <div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-error-500/10 text-error-400">
<svg <ExclamationIcon className="h-8 w-8" />
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h2 className="text-lg font-semibold text-dark-100"> <h2 className="text-lg font-semibold text-dark-100">

View File

@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { CloseIcon } from '@/components/icons';
import { referralNetworkApi } from '@/api/referralNetwork'; import { referralNetworkApi } from '@/api/referralNetwork';
import { useReferralNetworkStore } from '@/store/referralNetwork'; import { useReferralNetworkStore } from '@/store/referralNetwork';
import { formatKopeksToRubles } from '../utils'; import { formatKopeksToRubles } from '../utils';
@@ -37,15 +38,7 @@ export function CampaignDetailPanel({ campaignId, className }: CampaignDetailPan
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300" className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300"
aria-label={t('common.close')} aria-label={t('common.close')}
> >
<svg <CloseIcon className="h-5 w-5" />
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>
</button> </button>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MinusIcon, PlusIcon } from '@/components/icons';
import { getSigmaInstance } from '../sigmaGlobals'; import { getSigmaInstance } from '../sigmaGlobals';
interface NetworkControlsProps { interface NetworkControlsProps {
@@ -62,32 +63,12 @@ export function NetworkControls({ className }: NetworkControlsProps) {
{ {
label: t('admin.referralNetwork.controls.zoomIn'), label: t('admin.referralNetwork.controls.zoomIn'),
onClick: handleZoomIn, onClick: handleZoomIn,
icon: ( icon: <PlusIcon className="h-4 w-4" />,
<svg
className="h-4 w-4"
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>
),
}, },
{ {
label: t('admin.referralNetwork.controls.zoomOut'), label: t('admin.referralNetwork.controls.zoomOut'),
onClick: handleZoomOut, onClick: handleZoomOut,
icon: ( icon: <MinusIcon className="h-4 w-4" />,
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12h-15" />
</svg>
),
}, },
{ {
label: t('admin.referralNetwork.controls.resetZoom'), label: t('admin.referralNetwork.controls.resetZoom'),

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CloseIcon, FilterIcon } from '@/components/icons';
import { useReferralNetworkStore } from '@/store/referralNetwork'; import { useReferralNetworkStore } from '@/store/referralNetwork';
import type { NetworkGraphData } from '@/types/referralNetwork'; import type { NetworkGraphData } from '@/types/referralNetwork';
@@ -120,19 +121,7 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
: 'border-dark-700/50 bg-dark-800/80 text-dark-300 hover:border-dark-600 hover:text-dark-100' : 'border-dark-700/50 bg-dark-800/80 text-dark-300 hover:border-dark-600 hover:text-dark-100'
}`} }`}
> >
<svg <FilterIcon className="h-4 w-4" />
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"
/>
</svg>
<span className="hidden sm:inline">{t('admin.referralNetwork.filters.title')}</span> <span className="hidden sm:inline">{t('admin.referralNetwork.filters.title')}</span>
{hasActiveFilters && ( {hasActiveFilters && (
<span className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-accent-500" /> <span className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-accent-500" />
@@ -152,15 +141,7 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
aria-label={t('common.close')} aria-label={t('common.close')}
className="text-dark-500 transition-colors hover:text-dark-300" className="text-dark-500 transition-colors hover:text-dark-300"
> >
<svg <CloseIcon className="h-4 w-4" />
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>
</button> </button>
</div> </div>
{panelContent} {panelContent}
@@ -182,15 +163,7 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
aria-label={t('common.close')} aria-label={t('common.close')}
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300" className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300"
> >
<svg <CloseIcon className="h-5 w-5" />
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>
</button> </button>
</div> </div>
{panelContent} {panelContent}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { referralNetworkApi } from '@/api/referralNetwork'; import { referralNetworkApi } from '@/api/referralNetwork';
import { CheckIcon, CloseIcon, PlusIcon, SearchIcon } from '@/components/icons';
import { MAX_SCOPE_ITEMS } from '@/store/referralNetwork'; import { MAX_SCOPE_ITEMS } from '@/store/referralNetwork';
import type { ScopeSelection, ScopeType } from '@/types/referralNetwork'; import type { ScopeSelection, ScopeType } from '@/types/referralNetwork';
@@ -30,28 +31,6 @@ const AVATAR_LETTERS: Record<ScopeType, string> = {
user: 'U', user: 'U',
}; };
function CheckIcon() {
return (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
);
}
function CloseIcon({ className = 'h-3 w-3' }: { className?: string }) {
return (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
}
function Spinner({ size = 'h-5 w-5' }: { size?: string }) { function Spinner({ size = 'h-5 w-5' }: { size?: string }) {
return ( return (
<div <div
@@ -245,7 +224,7 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
aria-label={t('admin.referralNetwork.scope.removeItem', { label: item.label })} aria-label={t('admin.referralNetwork.scope.removeItem', { label: item.label })}
className="ml-0.5 rounded-sm p-0.5 transition-colors hover:bg-white/10" className="ml-0.5 rounded-sm p-0.5 transition-colors hover:bg-white/10"
> >
<CloseIcon /> <CloseIcon className="h-3 w-3" />
</button> </button>
</span> </span>
))} ))}
@@ -273,15 +252,7 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
}`} }`}
disabled={isMaxReached && !isDropdownOpen} disabled={isMaxReached && !isDropdownOpen}
> >
<svg <PlusIcon className="h-4 w-4" />
className="h-4 w-4"
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>
</button> </button>
</div> </div>
@@ -323,19 +294,7 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
</div> </div>
<div className="relative min-w-0 flex-1"> <div className="relative min-w-0 flex-1">
<svg <SearchIcon className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-dark-500" />
className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { referralNetworkApi } from '@/api/referralNetwork'; import { referralNetworkApi } from '@/api/referralNetwork';
import { CloseIcon } from '@/components/icons';
import { useReferralNetworkStore } from '@/store/referralNetwork'; import { useReferralNetworkStore } from '@/store/referralNetwork';
import { formatKopeksToRubles, getSubscriptionStatusColor } from '../utils'; import { formatKopeksToRubles, getSubscriptionStatusColor } from '../utils';
@@ -37,15 +38,7 @@ export function UserDetailPanel({ userId, className }: UserDetailPanelProps) {
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300" className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300"
aria-label={t('common.close')} aria-label={t('common.close')}
> >
<svg <CloseIcon className="h-5 w-5" />
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>
</button> </button>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { useSearchParams, Link, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { authApi } from '../api/auth'; import { authApi } from '../api/auth';
import LanguageSwitcher from '../components/LanguageSwitcher'; import LanguageSwitcher from '../components/LanguageSwitcher';
import { CheckIcon } from '@/components/icons';
export default function ResetPassword() { export default function ResetPassword() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -97,15 +98,7 @@ export default function ResetPassword() {
{status === 'success' ? ( {status === 'success' ? (
<div className="text-center"> <div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-success-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-success-500/20">
<svg <CheckIcon className="h-8 w-8 text-success-400" />
className="h-8 w-8 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div> </div>
<h2 className="mb-2 text-xl font-bold text-dark-50"> <h2 className="mb-2 text-xl font-bold text-dark-50">
{t('resetPassword.success', 'Password changed!')} {t('resetPassword.success', 'Password changed!')}

View File

@@ -17,7 +17,16 @@ import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { useCloseOnSuccessNotification } from '../store/successNotification'; import { useCloseOnSuccessNotification } from '../store/successNotification';
import PurchaseCTAButton from '../components/subscription/PurchaseCTAButton'; import PurchaseCTAButton from '../components/subscription/PurchaseCTAButton';
import { CopyIcon, CheckIcon, PauseIcon } from '../components/icons'; import {
CopyIcon,
CheckIcon,
PauseIcon,
CalendarIcon,
RefreshIcon,
DevicesIcon,
DownloadIcon,
TrashIcon,
} from '../components/icons';
import { useHaptic } from '../platform'; import { useHaptic } from '../platform';
import { resolveConnectionUrlForUi } from '../utils/connectionLink'; import { resolveConnectionUrlForUi } from '../utils/connectionLink';
import { import {
@@ -97,26 +106,17 @@ const CountdownTimer = memo(function CountdownTimer({
: g.hoverBg, : g.hoverBg,
}} }}
> >
<svg <span
width="13" style={{
height="13" color: isExpired
viewBox="0 0 24 24"
fill="none"
stroke={
isExpired
? 'rgb(var(--color-critical-500))' ? 'rgb(var(--color-critical-500))'
: isUrgent : isUrgent
? 'rgb(var(--color-urgent-400))' ? 'rgb(var(--color-urgent-400))'
: g.textSecondary : g.textSecondary,
} }}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
> >
<rect x="3" y="4" width="18" height="18" rx="2" /> <CalendarIcon className="h-[13px] w-[13px]" />
<path d="M16 2v4M8 2v4M3 10h18" /> </span>
</svg>
</div> </div>
{t('dashboard.remaining')} {t('dashboard.remaining')}
</div> </div>
@@ -774,20 +774,10 @@ export default function Subscription() {
disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0} disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0}
className="flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium text-dark-50/30 transition-colors hover:bg-dark-50/[0.05] hover:text-dark-50/50 disabled:cursor-not-allowed disabled:opacity-50" className="flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium text-dark-50/30 transition-colors hover:bg-dark-50/[0.05] hover:text-dark-50/50 disabled:cursor-not-allowed disabled:opacity-50"
> >
<svg <RefreshIcon
className={`h-3 w-3 ${refreshTrafficMutation.isPending ? 'animate-spin' : ''}`} className="h-3 w-3"
fill="none" spinning={refreshTrafficMutation.isPending}
viewBox="0 0 24 24" />
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<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.182m0-4.991v4.99"
/>
</svg>
{trafficRefreshCooldown > 0 {trafficRefreshCooldown > 0
? `${trafficRefreshCooldown}s` ? `${trafficRefreshCooldown}s`
: t('common.refresh')} : t('common.refresh')}
@@ -827,23 +817,9 @@ export default function Subscription() {
> >
<div <div
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] transition-colors duration-500" className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] transition-colors duration-500"
style={{ background: `${zone.mainHex}12` }} style={{ background: `${zone.mainHex}12`, color: zone.mainHex }}
> >
<svg <DevicesIcon className="h-4 w-4" />
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke={zone.mainHex}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M12 17v4M8 21h8" />
<path d="M12 8v4M10 10h4" opacity="0.7" />
</svg>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-sm font-semibold tracking-tight text-dark-50"> <div className="text-sm font-semibold tracking-tight text-dark-50">
@@ -1000,21 +976,9 @@ export default function Subscription() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="flex h-7 w-7 items-center justify-center rounded-[8px]" className="flex h-7 w-7 items-center justify-center rounded-[8px]"
style={{ background: `${zone.mainHex}12` }} style={{ background: `${zone.mainHex}12`, color: zone.mainHex }}
> >
<svg <DownloadIcon className="h-3.5 w-3.5" />
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke={zone.mainHex}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div> </div>
<span className="text-sm font-semibold text-dark-50"> <span className="text-sm font-semibold text-dark-50">
{purchase.traffic_gb} {t('common.units.gb')} {purchase.traffic_gb} {t('common.units.gb')}
@@ -1121,23 +1085,9 @@ export default function Subscription() {
> >
<div <div
className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl" className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl"
style={{ background: g.hoverBg }} style={{ background: g.hoverBg, color: g.textFaint }}
> >
<svg <TrashIcon className="h-8 w-8" />
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke={g.textFaint}
strokeWidth="1.5"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
</div> </div>
<div className="text-sm text-dark-50/30">{t('subscription.noSubscription')}</div> <div className="text-sm text-dark-50/30">{t('subscription.noSubscription')}</div>
</div> </div>

View File

@@ -12,6 +12,7 @@ import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTarif
import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm'; import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm';
import { TariffPickerGrid } from '../components/subscription/purchase/TariffPickerGrid'; import { TariffPickerGrid } from '../components/subscription/purchase/TariffPickerGrid';
import { ClassicPurchaseWizard } from '../components/subscription/purchase/ClassicPurchaseWizard'; import { ClassicPurchaseWizard } from '../components/subscription/purchase/ClassicPurchaseWizard';
import { ExclamationIcon, SparklesIcon } from '@/components/icons';
export default function SubscriptionPurchase() { export default function SubscriptionPurchase() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -177,23 +178,12 @@ export default function SubscriptionPurchase() {
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div <div
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px]" className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px]"
style={{ background: 'rgba(255,184,0,0.12)' }} style={{
background: 'rgba(255,184,0,0.12)',
color: 'rgb(var(--color-urgent-400))',
}}
> >
<svg <SparklesIcon className="h-4 w-4" />
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="rgb(var(--color-urgent-400))"
strokeWidth="1.5"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
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.09z"
/>
</svg>
</div> </div>
<div> <div>
<div <div
@@ -225,23 +215,12 @@ export default function SubscriptionPurchase() {
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div <div
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px]" className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px]"
style={{ background: 'rgba(255,59,92,0.12)' }} style={{
background: 'rgba(255,59,92,0.12)',
color: 'rgb(var(--color-critical-500))',
}}
> >
<svg <ExclamationIcon className="h-4 w-4" />
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="rgb(var(--color-critical-500))"
strokeWidth="1.5"
aria-hidden="true"
>
<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>
</div> </div>
<div> <div>
<div <div

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Navigate, useNavigate } from 'react-router'; import { Navigate, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ClipboardIcon, PlusIcon } from '@/components/icons';
import { subscriptionApi } from '../api/subscription'; import { subscriptionApi } from '../api/subscription';
import { balanceApi } from '../api/balance'; import { balanceApi } from '../api/balance';
import { useTheme } from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
@@ -24,19 +25,7 @@ function EmptyState({ onBuy }: { onBuy: () => void }) {
className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl" className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl"
style={{ background: g.innerBg }} style={{ background: g.innerBg }}
> >
<svg <ClipboardIcon className="h-8 w-8 opacity-40" />
className="h-8 w-8 opacity-40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V19.5a2.25 2.25 0 002.25 2.25h.75"
/>
</svg>
</div> </div>
<h3 className="mb-2 text-xl font-semibold" style={{ color: g.text }}> <h3 className="mb-2 text-xl font-semibold" style={{ color: g.text }}>
{t('subscriptions.empty', 'Нет подписок')} {t('subscriptions.empty', 'Нет подписок')}
@@ -128,15 +117,7 @@ export default function Subscriptions() {
border: '1px solid rgba(var(--color-accent-400), 0.2)', border: '1px solid rgba(var(--color-accent-400), 0.2)',
}} }}
> >
<svg <PlusIcon className="h-4 w-4" />
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{t('subscriptions.buyAnother', 'Новый тариф')} {t('subscriptions.buyAnother', 'Новый тариф')}
</button> </button>
)} )}

View File

@@ -12,7 +12,7 @@ import type { TicketDetail } from '../types';
import { Card } from '@/components/data-display/Card'; import { Card } from '@/components/data-display/Card';
import { Button } from '@/components/primitives/Button'; import { Button } from '@/components/primitives/Button';
import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import { CloseIcon, ImageIcon, PlusIcon, SendIcon } from '@/components/icons'; import { ChatIcon, CloseIcon, ImageIcon, PlusIcon, SendIcon } from '@/components/icons';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
import { linkifyText } from '../utils/linkify'; import { linkifyText } from '../utils/linkify';
@@ -266,19 +266,7 @@ export default function Support() {
<div className="mx-auto mt-12 max-w-md"> <div className="mx-auto mt-12 max-w-md">
<Card className="text-center"> <Card className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
<svg <ChatIcon className="h-8 w-8 text-dark-400" />
className="h-8 w-8 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
</div> </div>
<h2 className="mb-2 text-xl font-semibold text-dark-100">{supportMessage.title}</h2> <h2 className="mb-2 text-xl font-semibold text-dark-100">{supportMessage.title}</h2>
<p className="mb-6 text-dark-400">{supportMessage.message}</p> <p className="mb-6 text-dark-400">{supportMessage.message}</p>
@@ -366,19 +354,7 @@ export default function Support() {
<Card className="flex items-center justify-between"> <Card className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-dark-800"> <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-dark-800">
<svg <ChatIcon className="h-5 w-5 text-dark-400" />
className="h-5 w-5 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
</div> </div>
<div> <div>
<div className="text-sm font-medium text-dark-100">{t('support.contactUs')}</div> <div className="text-sm font-medium text-dark-100">{t('support.contactUs')}</div>
@@ -440,19 +416,7 @@ export default function Support() {
) : ( ) : (
<div className="py-12 text-center"> <div className="py-12 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
<svg <ChatIcon className="h-8 w-8 text-dark-500" />
className="h-8 w-8 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
</div> </div>
<div className="text-dark-400">{t('support.noTickets')}</div> <div className="text-dark-400">{t('support.noTickets')}</div>
</div> </div>

View File

@@ -8,6 +8,7 @@ import { brandingApi } from '../api/branding';
import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK'; import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK';
import { tokenStorage } from '../utils/token'; import { tokenStorage } from '../utils/token';
import { getSafeRedirectPath } from '../utils/safeRedirect'; import { getSafeRedirectPath } from '../utils/safeRedirect';
import { CheckIcon, XIcon, ExclamationIcon } from '@/components/icons';
const MAX_RETRY_ATTEMPTS = 3; const MAX_RETRY_ATTEMPTS = 3;
const RETRY_COUNT_KEY = 'telegram_redirect_retry_count'; const RETRY_COUNT_KEY = 'telegram_redirect_retry_count';
@@ -158,15 +159,7 @@ export default function TelegramRedirect() {
{status === 'success' && ( {status === 'success' && (
<div className="mt-8"> <div className="mt-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-success-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-success-500/20">
<svg <CheckIcon className="h-8 w-8 text-success-400" />
className="h-8 w-8 text-success-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div> </div>
<p className="text-dark-200">{t('auth.loginSuccess')}</p> <p className="text-dark-200">{t('auth.loginSuccess')}</p>
<p className="mt-2 text-sm text-dark-500">{t('telegramRedirect.redirecting')}</p> <p className="mt-2 text-sm text-dark-500">{t('telegramRedirect.redirecting')}</p>
@@ -177,15 +170,7 @@ export default function TelegramRedirect() {
{status === 'error' && ( {status === 'error' && (
<div className="mt-8"> <div className="mt-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-error-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-error-500/20">
<svg <XIcon className="h-8 w-8 text-error-400" />
className="h-8 w-8 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div> </div>
<p className="mb-2 text-dark-200">{t('auth.loginFailed')}</p> <p className="mb-2 text-dark-200">{t('auth.loginFailed')}</p>
<p className="mb-6 text-sm text-error-400">{errorMessage}</p> <p className="mb-6 text-sm text-error-400">{errorMessage}</p>
@@ -204,19 +189,7 @@ export default function TelegramRedirect() {
{status === 'not-telegram' && ( {status === 'not-telegram' && (
<div className="mt-8"> <div className="mt-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-warning-500/20"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-warning-500/20">
<svg <ExclamationIcon className="h-8 w-8 text-warning-400" />
className="h-8 w-8 text-warning-400"
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>
</div> </div>
<p className="mb-2 text-dark-200">{t('telegramRedirect.openInTelegram')}</p> <p className="mb-2 text-dark-200">{t('telegramRedirect.openInTelegram')}</p>
<p className="mb-6 text-sm text-dark-400">{t('telegramRedirect.openInTelegramDesc')}</p> <p className="mb-6 text-sm text-dark-400">{t('telegramRedirect.openInTelegramDesc')}</p>

View File

@@ -20,6 +20,7 @@ import {
CheckIcon, CheckIcon,
CopyIcon, CopyIcon,
CryptoIcon, CryptoIcon,
ExclamationIcon,
ExternalLinkIcon, ExternalLinkIcon,
SparklesIcon, SparklesIcon,
StarIcon, StarIcon,
@@ -520,19 +521,7 @@ export default function TopUpAmount() {
variants={staggerItem} variants={staggerItem}
className="flex items-center gap-2 rounded-xl border border-error-500/20 bg-error-500/10 p-3" className="flex items-center gap-2 rounded-xl border border-error-500/20 bg-error-500/10 p-3"
> >
<svg <ExclamationIcon className="h-5 w-5 shrink-0 text-error-400" />
className="h-5 w-5 shrink-0 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="text-sm text-error-400">{error}</span> <span className="text-sm text-error-400">{error}</span>
</motion.div> </motion.div>
)} )}