mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
refactor(cabinet): migrate inline SVG icons to the central react-icons set
Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77 files with the central Phosphor (react-icons/pi) components from @/components/icons, preserving each icon's size classes and colour (dynamic stroke colours via the parent's currentColor, RefreshIcon's spinning state, conditional rotate-180 chevrons). Verified: tsc + vite build + eslint clean; an adversarial diff review of all changed files found the replacements correct (70 files clean, 0 blockers). Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate non-icons (brand/provider logos, loading spinners & framer-motion animations, charts/sparklines, background decoration) plus a few ambiguous glyphs.
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { InfoIcon, WalletIcon, PlusIcon } from '@/components/icons';
|
||||
|
||||
interface InsufficientBalancePromptProps {
|
||||
/** Amount missing in kopeks */
|
||||
@@ -55,19 +56,7 @@ export default function InsufficientBalancePrompt({
|
||||
className={`flex items-center justify-between gap-3 rounded-xl border border-error-500/30 bg-error-500/10 p-3 ${className}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-error-400">
|
||||
<svg
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<InfoIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
{message || t('balance.insufficientFunds')}:{' '}
|
||||
<span className="font-semibold">
|
||||
@@ -96,19 +85,7 @@ export default function InsufficientBalancePrompt({
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-error-500/20">
|
||||
<svg
|
||||
className="h-5 w-5 text-error-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z"
|
||||
/>
|
||||
</svg>
|
||||
<WalletIcon className="h-5 w-5 text-error-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 font-medium text-error-400">{t('balance.insufficientFunds')}</div>
|
||||
@@ -132,15 +109,7 @@ export default function InsufficientBalancePrompt({
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
{t('balance.topUpBalance')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { infoApi, type LanguageInfo } from '@/api/info';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n } = useTranslation();
|
||||
@@ -57,14 +58,9 @@ export default function LanguageSwitcher() {
|
||||
>
|
||||
<span>{currentLang.flag}</span>
|
||||
<span className="font-medium text-dark-200">{currentLang.code.toUpperCase()}</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-3.5 w-3.5 text-dark-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EmailIcon as CentralEmailIcon } from '@/components/icons';
|
||||
import OAuthProviderIcon from './OAuthProviderIcon';
|
||||
|
||||
export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) {
|
||||
@@ -13,22 +14,7 @@ export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string })
|
||||
}
|
||||
|
||||
export function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return <CentralEmailIcon className={className} />;
|
||||
}
|
||||
|
||||
export default function ProviderIcon({
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { CheckIcon, XIcon, ExclamationIcon, InfoIcon } from '@/components/icons';
|
||||
|
||||
interface ToastOptions {
|
||||
type?: 'success' | 'error' | 'info' | 'warning';
|
||||
message: string;
|
||||
@@ -163,58 +165,10 @@ function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) {
|
||||
};
|
||||
|
||||
const defaultIcons = {
|
||||
success: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
),
|
||||
error: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
),
|
||||
warning: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
info: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
success: <CheckIcon className="h-5 w-5" />,
|
||||
error: <XIcon className="h-5 w-5" />,
|
||||
warning: <ExclamationIcon className="h-5 w-5" />,
|
||||
info: <InfoIcon className="h-5 w-5" />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { brandingApi } from '../../api/branding';
|
||||
import { CheckIcon, CloseIcon } from './icons';
|
||||
import { CheckIcon, CloseIcon, PencilIcon } from './icons';
|
||||
|
||||
export function AnalyticsTab() {
|
||||
const { t } = useTranslation();
|
||||
@@ -143,19 +143,7 @@ export function AnalyticsTab() {
|
||||
}}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -294,19 +282,7 @@ export function AnalyticsTab() {
|
||||
}}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -360,19 +336,7 @@ export function AnalyticsTab() {
|
||||
}}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ButtonSection,
|
||||
BOT_LOCALES,
|
||||
} from '../../api/buttonStyles';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
import { Toggle } from './Toggle';
|
||||
import { useNotify } from '../../platform/hooks/useNotify';
|
||||
import { useNativeDialog } from '../../platform/hooks/useNativeDialog';
|
||||
@@ -284,15 +285,9 @@ export function ButtonsTab() {
|
||||
{t('admin.buttons.customLabels')}
|
||||
{hasCustomLabels && <span className="h-1.5 w-1.5 rounded-full bg-accent-500" />}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-3.5 w-3.5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="mt-2 space-y-2">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckIcon, ChevronDownIcon, CloseIcon, PlusIcon } from '@/components/icons';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
|
||||
@@ -209,9 +210,7 @@ export function ColoredItemCombobox({
|
||||
className="shrink-0 rounded p-0.5 text-dark-500 transition-colors hover:text-dark-300"
|
||||
aria-label={t('news.admin.combobox.clear')}
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -222,16 +221,12 @@ export function ColoredItemCombobox({
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-dark-500 transition-transform duration-200',
|
||||
isOpen && 'rotate-180',
|
||||
)}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7 10l5 5 5-5z" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
@@ -279,13 +274,7 @@ export function ColoredItemCombobox({
|
||||
/>
|
||||
<span className="flex-1 truncate">{item.name}</span>
|
||||
{value?.id === item.id && (
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 text-accent-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
<CheckIcon className="h-4 w-4 shrink-0 text-accent-400" />
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
@@ -298,9 +287,7 @@ export function ColoredItemCombobox({
|
||||
{deletingId === item.id ? (
|
||||
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-error-400 border-t-transparent" />
|
||||
) : (
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -365,9 +352,7 @@ export function ColoredItemCombobox({
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
) : (
|
||||
<>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('news.admin.combobox.create')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSortable } from '@dnd-kit/sortable';
|
||||
import { PiCaretDown } from 'react-icons/pi';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { GripIcon, TrashIcon } from '../icons/LandingIcons';
|
||||
import type { AdminLandingPaymentMethod, EditableMethodField } from '../../api/landings';
|
||||
import type { PaymentMethodSubOptionInfo } from '../../types';
|
||||
@@ -218,17 +219,7 @@ export function SortableSelectedMethodCard({
|
||||
: 'border border-dark-600 bg-dark-700',
|
||||
)}
|
||||
>
|
||||
{enabled && (
|
||||
<svg
|
||||
className="h-2.5 w-2.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{enabled && <CheckIcon className="h-2.5 w-2.5" />}
|
||||
</div>
|
||||
{opt.name}
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, XCloseIcon } from '@/components/icons';
|
||||
import { CheckIcon, ChevronDownIcon, XCloseIcon, XIcon } from '@/components/icons';
|
||||
import { useFocusTrap } from '@/hooks/useFocusTrap';
|
||||
import { DropdownSelect } from './DropdownSelect';
|
||||
import type { UserListItem } from '../../../api/adminUsers';
|
||||
@@ -98,31 +98,11 @@ function ProgressView({ progress }: { progress: ProgressState }) {
|
||||
<div key={idx} className="flex items-start gap-2 text-xs">
|
||||
{entry.success ? (
|
||||
<span className="mt-0.5 shrink-0 text-success-400" aria-hidden="true">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
<span className="font-mono text-dark-400">
|
||||
@@ -159,18 +139,12 @@ function ErrorDetails({ result }: { result: BulkActionResult }) {
|
||||
<span className="text-sm font-medium text-error-400">
|
||||
{t('admin.bulkActions.errors.title', { count: result.error_count })}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'h-4 w-4 text-error-400 transition-transform duration-200',
|
||||
expanded && 'rotate-180',
|
||||
)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="max-h-48 overflow-y-auto border-t border-error-500/20 px-4 py-3">
|
||||
@@ -178,15 +152,7 @@ function ErrorDetails({ result }: { result: BulkActionResult }) {
|
||||
{result.errors.map((err, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-xs">
|
||||
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</span>
|
||||
<span className="shrink-0 font-mono text-dark-400">
|
||||
{err.username ? `@${err.username}` : `#${err.user_id}`}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TrashIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon } from './DropdownSelect';
|
||||
import { isSubscriptionLevelAction } from './actionTargets';
|
||||
import type { BulkActionType } from '../../../api/adminBulkActions';
|
||||
@@ -112,21 +113,7 @@ export function FloatingActionBar({
|
||||
{
|
||||
type: 'delete_subscription',
|
||||
labelKey: 'admin.bulkActions.actions.deleteSubscription',
|
||||
icon: (
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
icon: <TrashIcon className="h-3.5 w-3.5" />,
|
||||
colorClass: 'text-error-400 hover:bg-error-500/10',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon } from './DropdownSelect';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -135,21 +136,7 @@ export function MultiSelectDropdown({
|
||||
: 'border-dark-500 bg-dark-700/60',
|
||||
)}
|
||||
>
|
||||
{isChecked && (
|
||||
<svg
|
||||
className="h-2.5 w-2.5 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={4}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{isChecked && <CheckIcon className="h-2.5 w-2.5 text-white" />}
|
||||
</div>
|
||||
<span className={cn('text-sm', isChecked ? 'text-dark-100' : 'text-dark-300')}>
|
||||
{option.label}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import type { UserListItemSubscription } from '../../../api/adminUsers';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -68,17 +69,7 @@ export function SubscriptionSubRow({
|
||||
: t('admin.bulkActions.selectUser', { name: subscription.tariff_name || '' })
|
||||
}
|
||||
>
|
||||
{isSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={4}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{isSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon, GlobeIcon } from '../TrafficIcons';
|
||||
import { getFlagEmoji } from '../trafficUsageHelpers';
|
||||
|
||||
@@ -71,17 +72,7 @@ export function CountryFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
All
|
||||
</button>
|
||||
@@ -102,21 +93,7 @@ export function CountryFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{getFlagEmoji(code)} {code.toUpperCase()}
|
||||
<span className="ml-auto text-dark-500">{count}</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronDownIcon, ServerIcon } from '../TrafficIcons';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { getFlagEmoji } from '../trafficUsageHelpers';
|
||||
import type { TrafficNodeInfo } from '../../../../api/adminTraffic';
|
||||
|
||||
@@ -75,17 +76,7 @@ export function NodeFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{t('admin.trafficUsage.allNodes')}
|
||||
</button>
|
||||
@@ -106,21 +97,7 @@ export function NodeFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{getFlagEmoji(node.country_code)} {node.node_name}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon, StatusIcon } from '../TrafficIcons';
|
||||
|
||||
// Status colour pills shared with the StatusFilter dropdown.
|
||||
@@ -86,17 +87,7 @@ export function StatusFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{t('admin.trafficUsage.allStatuses')}
|
||||
</button>
|
||||
@@ -117,21 +108,7 @@ export function StatusFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
<span className={`h-2 w-2 rounded-full ${STATUS_COLORS[s] || 'bg-dark-500'}`} />
|
||||
{statusLabel(s)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronDownIcon, FilterIcon } from '../TrafficIcons';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
|
||||
export function TariffFilter({
|
||||
available,
|
||||
@@ -73,17 +74,7 @@ export function TariffFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{t('admin.trafficUsage.allTariffs')}
|
||||
</button>
|
||||
@@ -104,21 +95,7 @@ export function TariffFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{tariff}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SendIcon } from '@/components/icons';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import type { AdminUserGiftItem, AdminUserGiftsResponse } from '../../../api/adminUsers';
|
||||
|
||||
@@ -224,19 +225,7 @@ export function GiftsTab({ giftsLoading, giftsData, locale, onNavigateToUser }:
|
||||
{/* Sent Gifts */}
|
||||
<div>
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-dark-200">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<SendIcon className="h-4 w-4 text-accent-400" />
|
||||
{t('admin.users.detail.gifts.sentTitle')}
|
||||
<span className="text-dark-500">({giftsData.sent_total})</span>
|
||||
</h3>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
UserSubscriptionInfo,
|
||||
} from '../../../api/adminUsers';
|
||||
import type { PromoGroup } from '../../../api/promocodes';
|
||||
import { ServerIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Local status badge (parent has its own — duplicating here to keep
|
||||
@@ -260,19 +261,7 @@ export function InfoTab(props: InfoTabProps) {
|
||||
{t('admin.users.detail.lastNode')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-dark-100">
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<ServerIcon className="h-4 w-4 shrink-0 text-dark-400" />
|
||||
{panelInfo.last_connected_node_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import { useNotify } from '../../../platform/hooks/useNotify';
|
||||
import { adminUsersApi, type UserDetailResponse, type UserListItem } from '../../../api/adminUsers';
|
||||
import { XIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Referrals tab — top-of-graph referrer + stats + referrals list,
|
||||
@@ -481,15 +482,7 @@ export function ReferralsTab({ user, userId, onUserRefresh }: ReferralsTabProps)
|
||||
className="rounded-lg p-2 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400 disabled:opacity-50"
|
||||
title={t('admin.users.detail.referrals.removeReferral')}
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MinusIcon, PlusIcon, RefreshIcon } from '@/components/icons';
|
||||
import {
|
||||
BackIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
EditIcon,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
RefreshIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
import { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices';
|
||||
import { createNumberInputHandler } from '../../../utils/inputHelpers';
|
||||
import { getFlagEmoji } from '../../../utils/subscriptionHelpers';
|
||||
@@ -217,19 +227,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
</span>
|
||||
<StatusBadge status={sub.status} />
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-4 w-4 text-dark-500" />
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-dark-400">
|
||||
<span>
|
||||
@@ -309,15 +307,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
onClick={() => onSubscriptionDetailViewChange(false)}
|
||||
className="flex items-center gap-1.5 text-sm text-dark-400 transition-colors hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<BackIcon className="h-4 w-4" />
|
||||
{t('admin.users.detail.subscription.backToList', 'Все подписки')}
|
||||
</button>
|
||||
)}
|
||||
@@ -943,19 +933,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
)}
|
||||
aria-label={t('admin.users.detail.devices.renameSave', 'Сохранить')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CheckIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -968,19 +946,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
title={t('common.cancel', 'Отмена')}
|
||||
aria-label={t('common.cancel', 'Отмена')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -995,19 +961,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
title={t('admin.users.detail.devices.rename', 'Переименовать')}
|
||||
aria-label={t('admin.users.detail.devices.rename', 'Переименовать')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
|
||||
</svg>
|
||||
<EditIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
@@ -1060,19 +1014,9 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-dark-500 transition-transform ${requestHistoryExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{requestHistoryExpanded && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { adminApi, type AdminTicket, type AdminTicketDetail } from '../../../api/admin';
|
||||
import { MessageMediaGrid } from '../../tickets/MessageMediaGrid';
|
||||
import { linkifyText } from '../../../utils/linkify';
|
||||
import { ChatIcon, BackIcon, SendIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Tickets tab — list view + chat view (selected ticket replaces list).
|
||||
@@ -155,19 +156,7 @@ function EmptyState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-dark-800/50 py-12">
|
||||
<svg
|
||||
className="mb-3 h-12 w-12 text-dark-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
||||
/>
|
||||
</svg>
|
||||
<ChatIcon className="mb-3 h-12 w-12 text-dark-600" />
|
||||
<p className="text-dark-400">{t('admin.users.detail.noTickets')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -265,15 +254,7 @@ function ChatView({
|
||||
aria-label={t('common.back', 'Back')}
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-dark-800 transition-colors hover:bg-dark-700 sm:h-8 sm:w-8"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
<BackIcon className="h-4 w-4 text-dark-400" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-dark-100">
|
||||
@@ -372,19 +353,7 @@ function ChatView({
|
||||
{replySending ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
) : (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<SendIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlatform } from '@/platform';
|
||||
import { InfoIcon } from '@/components/icons';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
|
||||
@@ -55,20 +56,7 @@ export default function AccountDeletedScreen() {
|
||||
<div className="w-full max-w-md text-center">
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-warning-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<InfoIcon className="h-12 w-12 text-warning-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { BanIcon } from '@/components/icons';
|
||||
|
||||
export default function BlacklistedScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,19 +21,7 @@ export default function BlacklistedScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-error-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
/>
|
||||
</svg>
|
||||
<BanIcon className="h-12 w-12 text-error-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useBlockingStore } from '../../store/blocking';
|
||||
import { apiClient, isChannelSubscriptionError } from '../../api/client';
|
||||
import { usePlatform } from '../../platform';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { TelegramIcon, ClockIcon, CheckIcon } from '@/components/icons';
|
||||
|
||||
const CHECK_COOLDOWN_SECONDS = 5;
|
||||
|
||||
@@ -96,9 +97,7 @@ export default function ChannelSubscriptionScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-gradient-to-br from-blue-500/20 to-cyan-500/20">
|
||||
<svg className="h-12 w-12 text-blue-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
<TelegramIcon className="h-12 w-12 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -183,31 +182,12 @@ export default function ChannelSubscriptionScreen() {
|
||||
</>
|
||||
) : cooldown > 0 ? (
|
||||
<>
|
||||
<svg
|
||||
className="h-5 w-5 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<ClockIcon className="h-5 w-5 text-dark-500" />
|
||||
{t('blocking.channel.waitSeconds', { seconds: cooldown })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
{t('blocking.channel.checkSubscription')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { AdjustmentsIcon } from '@/components/icons';
|
||||
|
||||
export default function MaintenanceScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,19 +21,7 @@ export default function MaintenanceScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-warning-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<AdjustmentsIcon className="h-12 w-12 text-warning-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
|
||||
import type { BlockRendererProps } from './blocks';
|
||||
import TvQuickConnect from './TvQuickConnect';
|
||||
import { BackIcon } from '@/components/icons';
|
||||
import { BackIcon, ChevronIcon, DocumentIcon } from '@/components/icons';
|
||||
|
||||
const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
|
||||
|
||||
@@ -259,15 +259,7 @@ export default function InstallationGuide({
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-2.5 text-dark-400">
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 9l4-4 4 4M8 15l4 4 4-4" />
|
||||
</svg>
|
||||
<ChevronIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -315,19 +307,7 @@ export default function InstallationGuide({
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary w-full justify-center"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
</svg>
|
||||
<DocumentIcon className="h-5 w-5" />
|
||||
{getBaseTranslation('tutorial', 'subscription.connection.tutorial')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
import { getColorGradient } from '@/utils/colorParser';
|
||||
import { ThemeIcon } from './ThemeIcon';
|
||||
import type { BlockRendererProps } from './types';
|
||||
@@ -52,15 +53,9 @@ export function AccordionBlock({
|
||||
<span className="min-w-0 flex-1 truncate font-semibold text-dark-100">
|
||||
{getLocalizedText(block.title)}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-[18px] w-[18px] shrink-0 text-dark-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
{/* Panel */}
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GiftIcon } from '@/components/icons';
|
||||
import type { PendingGift } from '../../api/gift';
|
||||
|
||||
interface PendingGiftCardProps {
|
||||
@@ -28,19 +29,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro
|
||||
<div className="relative flex items-start gap-4">
|
||||
{/* Gift icon */}
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-accent-500/20">
|
||||
<svg
|
||||
className="h-6 w-6 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
<GiftIcon className="h-6 w-6 text-accent-400" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
|
||||
import { formatTraffic } from '../../utils/formatTraffic';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import { RefreshIcon } from '@/components/icons';
|
||||
import { CalendarIcon, RefreshIcon } from '@/components/icons';
|
||||
import { useHaptic } from '../../platform';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
@@ -309,20 +309,14 @@ export default function SubscriptionCardActive({
|
||||
background: daysLeft <= 3 ? 'rgba(var(--color-warning-400), 0.1)' : g.hoverBg,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<span
|
||||
style={{
|
||||
color: daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-[13px] w-[13px]" />
|
||||
</span>
|
||||
</div>
|
||||
{t('dashboard.remaining')}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { getInsufficientBalanceError } from '../../utils/subscriptionHelpers';
|
||||
import { SubscriptionIcon } from '@/components/icons';
|
||||
import { ClockIcon, ExclamationIcon, PlusIcon, SubscriptionIcon } from '@/components/icons';
|
||||
|
||||
interface SubscriptionCardExpiredProps {
|
||||
subscription: Subscription;
|
||||
@@ -168,39 +168,13 @@ export default function SubscriptionCardExpired({
|
||||
style={{
|
||||
background: `rgba(${accent.r},${accent.g},${accent.b},0.1)`,
|
||||
border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.15)`,
|
||||
color: accent.hex,
|
||||
}}
|
||||
>
|
||||
{isLimited ? (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<ExclamationIcon className="h-[22px] w-[22px]" />
|
||||
) : (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
<ClockIcon className="h-[22px] w-[22px]" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||
@@ -275,19 +249,7 @@ export default function SubscriptionCardExpired({
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('subscription.buyTraffic')}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -330,19 +292,7 @@ export default function SubscriptionCardExpired({
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('dashboard.expired.topUp')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { TrialInfo } from '../../types';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { BoltIcon, SparklesIcon } from '@/components/icons';
|
||||
|
||||
interface TrialOfferCardProps {
|
||||
trialInfo: TrialInfo;
|
||||
@@ -94,37 +95,21 @@ export default function TrialOfferCard({
|
||||
}}
|
||||
>
|
||||
{isFree ? (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgb(var(--color-accent-400))"
|
||||
strokeWidth="1.5"
|
||||
<span
|
||||
className="flex"
|
||||
style={{ color: 'rgb(var(--color-accent-400))' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<SparklesIcon className="h-[26px] w-[26px]" />
|
||||
</span>
|
||||
) : (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgb(var(--color-urgent-400))"
|
||||
strokeWidth="1.5"
|
||||
<span
|
||||
className="flex"
|
||||
style={{ color: 'rgb(var(--color-urgent-400))' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<BoltIcon className="h-[26px] w-[26px]" />
|
||||
</span>
|
||||
)}
|
||||
{/* Glow effect */}
|
||||
<div
|
||||
|
||||
@@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
|
||||
import { newsApi } from '../../api/news';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { ArrowIcon } from '@/components/icons';
|
||||
import { ArrowIcon, NewsIcon } from '@/components/icons';
|
||||
import type { NewsListItem } from '../../types/news';
|
||||
|
||||
// --- Security: hex color validation to prevent CSS injection ---
|
||||
@@ -415,35 +415,7 @@ export default function NewsSection() {
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-accent-400 to-accent-600">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
fill="currentColor"
|
||||
className="text-dark-950/20"
|
||||
/>
|
||||
<path
|
||||
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
<path
|
||||
d="M7 8h4M7 11h10M7 14h10M7 17h6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
<rect
|
||||
x="14"
|
||||
y="7"
|
||||
width="4"
|
||||
height="4"
|
||||
rx="0.5"
|
||||
fill="currentColor"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
</svg>
|
||||
<NewsIcon className="h-[18px] w-[18px] text-dark-950" />
|
||||
</div>
|
||||
<span className="font-mono text-[11px] font-bold uppercase tracking-[0.18em] text-dark-500">
|
||||
{t('news.title')}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import { SubscriptionIcon } from '@/components/icons';
|
||||
import { ChevronRightIcon, SubscriptionIcon } from '@/components/icons';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
interface PurchaseCTAButtonProps {
|
||||
@@ -86,20 +86,7 @@ export default function PurchaseCTAButton({
|
||||
</div>
|
||||
|
||||
{/* Right: chevron */}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</HoverBorderGradient>
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { useHaptic } from '../../platform';
|
||||
import { CalendarIcon, CheckIcon, ChevronRightIcon, DevicesIcon } from '@/components/icons';
|
||||
import type { SubscriptionListItem } from '../../types';
|
||||
|
||||
function formatDate(iso: string | null, locale?: string): string {
|
||||
@@ -136,15 +137,7 @@ export default function SubscriptionListCard({
|
||||
</span>
|
||||
<StatusBadge status={subscription.status} isTrial={isTrial} t={t} />
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 opacity-30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-4 w-4 shrink-0 opacity-30" />
|
||||
</div>
|
||||
|
||||
{/* Traffic mini progress bar */}
|
||||
@@ -177,29 +170,11 @@ export default function SubscriptionListCard({
|
||||
style={{ color: g.textSecondary }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" />
|
||||
<path d="M12 18h.01" />
|
||||
</svg>
|
||||
<DevicesIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
{subscription.device_limit}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
{formatDate(subscription.end_date, i18n.language)}
|
||||
</span>
|
||||
{!isTrial &&
|
||||
@@ -213,19 +188,19 @@ export default function SubscriptionListCard({
|
||||
<span
|
||||
className={`flex items-center gap-1 ${enabled ? 'text-success-400/70' : 'text-error-400/50'}`}
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
{enabled ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
) : (
|
||||
{enabled ? (
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
) : (
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
)}
|
||||
</svg>
|
||||
</svg>
|
||||
)}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTheme } from '../../../hooks/useTheme';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
||||
import { getGlassColors } from '../../../utils/glassTheme';
|
||||
import { ArrowDownIcon, DevicesIcon, RestartIcon } from '@/components/icons';
|
||||
import type { Tariff, Subscription, PurchaseOptions } from '../../../types';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -169,35 +170,11 @@ export function TariffPickerGrid({
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowDownIcon className="h-4 w-4 text-accent-400" />
|
||||
<span className="font-medium text-dark-200">{tariff.traffic_limit_label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||
/>
|
||||
</svg>
|
||||
<DevicesIcon className="h-4 w-4 text-dark-400" />
|
||||
<span className="text-dark-300">
|
||||
{tariff.device_limit === 0
|
||||
? '∞'
|
||||
@@ -206,19 +183,7 @@ export function TariffPickerGrid({
|
||||
</div>
|
||||
{tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M2.985 19.644l3.181-3.182"
|
||||
/>
|
||||
</svg>
|
||||
<RestartIcon className="h-4 w-4 text-dark-400" />
|
||||
<span className="text-dark-300">
|
||||
{t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TrashIcon } from '@/components/icons';
|
||||
import { subscriptionApi } from '../../../api/subscription';
|
||||
import { usePlatform } from '../../../platform';
|
||||
import { useDestructiveConfirm } from '../../../platform/hooks/useNativeDialog';
|
||||
@@ -72,19 +73,7 @@ export function DeleteSubscriptionSheet({
|
||||
disabled={deleteLoading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-error-400/20 bg-error-400/5 p-3.5 text-sm font-medium text-error-400 transition-colors hover:bg-error-400/10 disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
{t('subscription.delete', 'Удалить подписку')}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, DocumentIcon, XIcon } from '@/components/icons';
|
||||
|
||||
import { ticketsApi } from '../../api/tickets';
|
||||
|
||||
export interface MediaItem {
|
||||
@@ -150,19 +153,7 @@ export function MessageMediaGrid({
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-dark-700 px-3 py-2 text-sm text-dark-200 transition-colors hover:bg-dark-600"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
|
||||
/>
|
||||
</svg>
|
||||
<DocumentIcon className="h-4 w-4" />
|
||||
{item.caption || `Download ${item.type}`}
|
||||
</a>
|
||||
);
|
||||
@@ -180,15 +171,7 @@ export function MessageMediaGrid({
|
||||
className="absolute right-4 top-4 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white text-black shadow-xl transition-colors hover:bg-gray-200"
|
||||
onClick={closeFullscreen}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{photoItems.length > 1 && (
|
||||
@@ -199,15 +182,7 @@ export function MessageMediaGrid({
|
||||
onClick={() => setFullscreenIndex(fullscreenIndex - 1)}
|
||||
className="absolute left-4 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-xl transition-colors hover:bg-white disabled:opacity-30"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -215,15 +190,7 @@ export function MessageMediaGrid({
|
||||
onClick={() => setFullscreenIndex(fullscreenIndex + 1)}
|
||||
className="absolute right-4 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-xl transition-colors hover:bg-white disabled:opacity-30"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 rounded-full bg-dark-950/70 px-3 py-1 text-sm text-white">
|
||||
{fullscreenIndex + 1} / {photoItems.length}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminAppsApi } from '../api/adminApps';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { BackIcon } from '@/components/icons';
|
||||
|
||||
export default function AdminApps() {
|
||||
const { t } = useTranslation();
|
||||
@@ -45,15 +46,7 @@ export default function AdminApps() {
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<BackIcon className="h-5 w-5 text-dark-400" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('admin.apps.title')}</h1>
|
||||
|
||||
@@ -34,6 +34,9 @@ import {
|
||||
TrafficIcon,
|
||||
ReportIcon,
|
||||
HealthIcon,
|
||||
ExclamationIcon,
|
||||
BackIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
type TabType =
|
||||
@@ -393,34 +396,10 @@ export default function AdminBanSystem() {
|
||||
<div className="mb-6 flex justify-center">
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-10 w-10 text-error-400" />
|
||||
</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">
|
||||
<svg
|
||||
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>
|
||||
<SettingsIcon className="h-3.5 w-3.5 text-dark-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,19 +433,7 @@ export default function AdminBanSystem() {
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<BackIcon className="h-5 w-5" />
|
||||
{t('common.back')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1444,14 +1411,7 @@ export default function AdminBanSystem() {
|
||||
aria-label={t('common.close')}
|
||||
className="text-dark-400 hover:text-dark-200"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4 p-4">
|
||||
|
||||
@@ -13,7 +13,7 @@ import { partnerApi } from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||
import Twemoji from 'react-twemoji';
|
||||
import { CampaignIcon, CheckIcon, RefreshIcon } from '@/components/icons';
|
||||
import { CampaignIcon, CheckIcon, LinkIcon, RefreshIcon } from '@/components/icons';
|
||||
|
||||
// Bonus type config
|
||||
const bonusTypeConfig: Record<
|
||||
@@ -273,19 +273,7 @@ export default function AdminCampaignCreate() {
|
||||
{partnerId && partner && (
|
||||
<div className="rounded-xl border border-accent-500/20 bg-accent-500/5 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg
|
||||
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>
|
||||
<LinkIcon className="mt-0.5 h-5 w-5 shrink-0 text-accent-400" />
|
||||
<p className="text-sm text-accent-300">
|
||||
{t('admin.campaigns.form.partnerAutoAssign', {
|
||||
name: partner.first_name || partner.username || `#${partnerId}`,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PARTNER_STATS } from '../constants/partner';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import { useHaptic } from '../platform';
|
||||
import { ChartIcon, CopyIcon, LinkIcon, UsersIcon } from '@/components/icons';
|
||||
import { ChartIcon, ChevronDownIcon, CopyIcon, LinkIcon, UsersIcon } from '@/components/icons';
|
||||
|
||||
// Bonus type config
|
||||
const bonusTypeConfig: Record<
|
||||
@@ -449,15 +449,9 @@ export default function AdminCampaignStats() {
|
||||
{t('admin.campaigns.stats.users')} ({stats.registrations})
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
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>
|
||||
|
||||
{showUsers && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { partnerApi } from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { XIcon } from '@/components/icons';
|
||||
|
||||
// Status badge config — keys must match backend PartnerStatus enum values
|
||||
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"
|
||||
title={t('admin.partnerDetail.campaigns.unassign')}
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { ChevronRightIcon, SettingsIcon } from '@/components/icons';
|
||||
|
||||
export default function AdminPartners() {
|
||||
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"
|
||||
title={t('admin.partners.settings')}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<SettingsIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -167,19 +151,7 @@ export default function AdminPartners() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
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>
|
||||
<ChevronRightIcon className="h-5 w-5 shrink-0 text-dark-500" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,13 @@ import { adminPaymentsApi, type SearchStats } from '../api/adminPayments';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import type { PendingPayment, PaginatedResponse } from '../types';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { BackIcon, SearchIcon, CalendarIcon } from '@/components/icons';
|
||||
import {
|
||||
BackIcon,
|
||||
SearchIcon,
|
||||
CalendarIcon,
|
||||
RefreshIcon,
|
||||
CheckCircleIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
@@ -210,19 +216,7 @@ export default function AdminPayments() {
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => refetch()} className="btn-secondary flex items-center gap-2">
|
||||
<svg
|
||||
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>
|
||||
<RefreshIcon className="h-4 w-4" />
|
||||
{t('common.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -574,19 +568,7 @@ export default function AdminPayments() {
|
||||
) : (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<CheckCircleIcon className="h-8 w-8 text-dark-500" />
|
||||
</div>
|
||||
<div className="text-dark-400">{t('admin.payments.noPayments')}</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { rbacApi, AccessPolicy, CreatePolicyPayload, UpdatePolicyPayload } from '@/api/rbac';
|
||||
import { AdminBackButton } from '@/components/admin';
|
||||
import { XIcon } from '@/components/icons';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -150,15 +151,7 @@ function IpTagInput({ values, onChange }: IpTagInputProps) {
|
||||
className="text-dark-400 transition-colors hover:text-dark-200"
|
||||
aria-label={t('admin.policies.conditions.removeIp', { ip })}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
UserIcon,
|
||||
SearchIcon,
|
||||
CloseIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
const getOfferTypeIcon = (offerType: string): string => {
|
||||
@@ -188,25 +189,9 @@ export default function AdminPromoOfferSend() {
|
||||
}`}
|
||||
>
|
||||
{result.isSuccess ? (
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-8 w-8 text-success-400" />
|
||||
) : (
|
||||
<svg
|
||||
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>
|
||||
<XIcon className="h-8 w-8 text-error-400" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-dark-100">{result.title}</h3>
|
||||
|
||||
@@ -105,19 +105,7 @@ function SortableTariffCard({
|
||||
)}
|
||||
{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">
|
||||
<svg
|
||||
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>
|
||||
<GiftIcon className="h-3 w-3" />
|
||||
{t('admin.tariffs.giftBadge')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { adminApi, AdminTicket, AdminTicketDetail } from '../api/admin';
|
||||
import { ticketsApi } from '../api/tickets';
|
||||
import { copyToClipboard as copyText } from '../utils/clipboard';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { BackIcon } from '@/components/icons';
|
||||
import { BackIcon, SettingsIcon, TicketIcon, XIcon } from '@/components/icons';
|
||||
|
||||
interface MediaAttachment {
|
||||
id: string;
|
||||
@@ -270,24 +270,7 @@ export default function AdminTickets() {
|
||||
onClick={() => navigate('/admin/tickets/settings')}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<SettingsIcon className="h-5 w-5" />
|
||||
{t('admin.tickets.settings')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -433,19 +416,7 @@ export default function AdminTickets() {
|
||||
{!selectedTicketId ? (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<TicketIcon className="h-8 w-8 text-dark-500" />
|
||||
</div>
|
||||
<div className="text-dark-400">{t('admin.tickets.selectTicket')}</div>
|
||||
</div>
|
||||
@@ -593,19 +564,7 @@ export default function AdminTickets() {
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { withdrawalApi } from '../api/withdrawals';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { WarningIcon } from '@/components/icons';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import {
|
||||
formatDate,
|
||||
@@ -237,19 +238,7 @@ export default function AdminWithdrawalDetail() {
|
||||
key={index}
|
||||
className="flex items-start gap-2 rounded-lg bg-error-500/10 px-3 py-2"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<WarningIcon className="mt-0.5 h-4 w-4 shrink-0 text-error-400" />
|
||||
<span className="text-sm text-error-300">{flag}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { withdrawalApi, AdminWithdrawalItem } from '../api/withdrawals';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { ChevronRightIcon } from '@/components/icons';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { formatDate, getWithdrawalStatusBadge, getRiskColor } from '../utils/withdrawalUtils';
|
||||
|
||||
@@ -150,19 +151,7 @@ export default function AdminWithdrawals() {
|
||||
</div>
|
||||
|
||||
{/* Chevron right */}
|
||||
<svg
|
||||
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>
|
||||
<ChevronRightIcon className="mt-1 h-5 w-5 shrink-0 text-dark-500" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authApi } from '../api/auth';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
import { XIcon } from '@/components/icons';
|
||||
|
||||
export default function AutoLogin() {
|
||||
const { t } = useTranslation();
|
||||
@@ -51,15 +52,7 @@ export default function AutoLogin() {
|
||||
{error ? (
|
||||
<div className="space-y-4">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-error-500/10">
|
||||
<svg
|
||||
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>
|
||||
<XIcon className="h-8 w-8 text-error-400" />
|
||||
</div>
|
||||
<p className="text-sm text-dark-300">{t('landing.autoLoginFailed')}</p>
|
||||
<button
|
||||
|
||||
@@ -6,6 +6,7 @@ import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
|
||||
import { subscriptionApi } from '../api/subscription';
|
||||
import { useTelegramSDK } from '../hooks/useTelegramSDK';
|
||||
import { useHaptic } from '@/platform';
|
||||
import { SettingsIcon } from '@/components/icons';
|
||||
import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
|
||||
import { isHappCryptolinkMode, resolveConnectionUrlForUi } from '../utils/connectionLink';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
@@ -182,24 +183,7 @@ export default function Connection() {
|
||||
</p>
|
||||
{isAdmin && (
|
||||
<Link to="/admin/apps" className="btn-primary inline-flex items-center gap-2 px-6 py-2.5">
|
||||
<svg
|
||||
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>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
{t('subscription.connection.goToApps')}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { contestsApi, ContestInfo, ContestGameData } from '../api/contests';
|
||||
import { GamepadIcon, TrophyIcon } from '@/components/icons';
|
||||
import { GamepadIcon, TrophyIcon, XIcon } from '@/components/icons';
|
||||
|
||||
export default function Contests() {
|
||||
const { t } = useTranslation();
|
||||
@@ -87,14 +87,7 @@ export default function Contests() {
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">{selectedContest.name}</h2>
|
||||
<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">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { promoApi } from '../api/promo';
|
||||
import PendingGiftCard from '../components/dashboard/PendingGiftCard';
|
||||
import SubscriptionListCard from '../components/subscription/SubscriptionListCard';
|
||||
import { API } from '../config/constants';
|
||||
import { ChevronRightIcon } from '@/components/icons';
|
||||
import { ChevronRightIcon, StarIcon } from '@/components/icons';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation();
|
||||
@@ -263,16 +263,7 @@ export default function Dashboard() {
|
||||
color: 'rgb(var(--color-accent-400))',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<StarIcon filled className="h-2.5 w-2.5 shrink-0" />
|
||||
<span className="truncate">{promoGroupData.group_name}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { brandingApi } from '../api/branding';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import {
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
ExclamationIcon,
|
||||
ExternalLinkIcon,
|
||||
LinkIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
type Status = 'countdown' | 'fallback' | 'error';
|
||||
|
||||
@@ -209,19 +216,7 @@ export default function DeepLinkRedirect() {
|
||||
onClick={openDeepLink}
|
||||
className="btn-primary flex w-full items-center justify-center gap-2 py-3"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<ExternalLinkIcon className="h-5 w-5" />
|
||||
{t('deepLink.openApp')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -243,36 +238,12 @@ export default function DeepLinkRedirect() {
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
{t('deepLink.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
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>
|
||||
<CopyIcon className="h-5 w-5" />
|
||||
{t('deepLink.copyLink')}
|
||||
</>
|
||||
)}
|
||||
@@ -283,19 +254,7 @@ export default function DeepLinkRedirect() {
|
||||
onClick={openDeepLink}
|
||||
className="btn-secondary flex w-full items-center justify-center gap-2"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<ExternalLinkIcon className="h-5 w-5" />
|
||||
{t('deepLink.tryAgain')}
|
||||
</button>
|
||||
|
||||
@@ -327,19 +286,7 @@ export default function DeepLinkRedirect() {
|
||||
{status === 'error' && (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-8 w-8 text-error-400" />
|
||||
</div>
|
||||
<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>
|
||||
@@ -351,19 +298,7 @@ export default function DeepLinkRedirect() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 flex items-center justify-center gap-2 text-dark-600">
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="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>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span className="text-xs">VPN Config Redirect</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
|
||||
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { CheckIcon, CopyIcon, InfoIcon, ExclamationIcon, ClockIcon } from '@/components/icons';
|
||||
|
||||
const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
@@ -147,32 +148,12 @@ function CodeOnlySuccessState({
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
{t('common.copied', 'Copied!')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
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>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
{t('gift.copyMessage', 'Copy message')}
|
||||
</>
|
||||
)}
|
||||
@@ -272,19 +253,7 @@ function PendingActivationState({
|
||||
>
|
||||
{/* Info icon */}
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10">
|
||||
<svg
|
||||
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>
|
||||
<InfoIcon className="h-10 w-10 text-warning-400" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -372,19 +341,7 @@ function PollErrorState() {
|
||||
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">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-10 w-10 text-warning-400" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -420,19 +377,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
|
||||
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">
|
||||
<svg
|
||||
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>
|
||||
<ClockIcon className="h-10 w-10 text-dark-400" />
|
||||
</div>
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-dark-800/50">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-10 w-10 text-dark-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-dark-50">{t('gift.noToken', 'Invalid link')}</h1>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getApiErrorMessage } from '../utils/api-error';
|
||||
import { formatPrice } from '../utils/format';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { usePlatform, useHaptic } from '@/platform';
|
||||
import { SparklesIcon } from '@/components/icons';
|
||||
|
||||
function GiftIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
@@ -686,17 +687,7 @@ function BuyTabContent({
|
||||
{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 h-8 w-8 shrink-0 items-center justify-center rounded-full bg-success-500/20">
|
||||
<svg
|
||||
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>
|
||||
<SparklesIcon className="h-4 w-4 text-success-400" />
|
||||
</div>
|
||||
<div>
|
||||
<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 && (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<SparklesIcon className="h-4 w-4 text-warning-400" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-warning-400">
|
||||
{t('promo.discountApplied')} -{config.active_discount_percent}%
|
||||
|
||||
@@ -23,6 +23,7 @@ import TelegramLoginButton from '../components/TelegramLoginButton';
|
||||
import OAuthProviderIcon from '../components/OAuthProviderIcon';
|
||||
import { saveOAuthState } from '../utils/oauth';
|
||||
import { getPendingReferralCode } from '../utils/referral';
|
||||
import { UsersIcon, EmailIcon, RefreshIcon, ChevronDownIcon } from '@/components/icons';
|
||||
|
||||
export default function Login() {
|
||||
const { t } = useTranslation();
|
||||
@@ -362,19 +363,7 @@ export default function Login() {
|
||||
{referralCode && isEmailAuthEnabled && (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<UsersIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-xs font-medium">{t('auth.referralInvite')}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,19 +374,7 @@ export default function Login() {
|
||||
{registeredEmail ? (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<EmailIcon className="h-7 w-7 text-success-400" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-bold text-dark-50">
|
||||
{t('auth.checkEmail', 'Check your email')}
|
||||
@@ -447,19 +424,7 @@ export default function Login() {
|
||||
onClick={handleRetryTelegramAuth}
|
||||
className="btn-primary mx-auto flex items-center gap-2 px-5 py-2.5"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<RefreshIcon className="h-4 w-4" />
|
||||
{t('auth.tryAgain')}
|
||||
</button>
|
||||
<p className="text-xs text-dark-500">
|
||||
@@ -516,29 +481,11 @@ export default function Login() {
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<EmailIcon className="h-3.5 w-3.5 text-dark-400" />
|
||||
<span>{t('auth.loginWithEmail')}</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
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>
|
||||
<div className="h-px flex-1 bg-dark-700" />
|
||||
</div>
|
||||
@@ -557,19 +504,7 @@ export default function Login() {
|
||||
forgotPasswordSent ? (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<EmailIcon className="h-6 w-6 text-success-400" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-dark-100">
|
||||
{t('auth.checkEmail', 'Check your email')}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getErrorDetail,
|
||||
} from '../utils/oauth';
|
||||
import type { ServerCompleteResponse } from '../types';
|
||||
import { CheckIcon, ExclamationIcon } from '@/components/icons';
|
||||
|
||||
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="card">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-success-500/20">
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-8 w-8 text-success-400" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold text-dark-50">
|
||||
{t('profile.accounts.linkSuccess')}
|
||||
@@ -209,19 +202,7 @@ export default function OAuthCallback() {
|
||||
<div className="relative w-full max-w-md text-center">
|
||||
<div className="card">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-error-500/20">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-8 w-8 text-error-400" />
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { pollsApi, PollInfo, PollQuestion } from '../api/polls';
|
||||
import { useFocusTrap } from '../hooks/useFocusTrap';
|
||||
import { ClipboardIcon, GiftIcon, CheckIcon } from '@/components/icons';
|
||||
import { ClipboardIcon, GiftIcon, CheckIcon, CloseIcon } from '@/components/icons';
|
||||
|
||||
export default function Polls() {
|
||||
const { t } = useTranslation();
|
||||
@@ -135,14 +135,7 @@ export default function Polls() {
|
||||
aria-label={t('common.close')}
|
||||
className="text-dark-400 hover:text-dark-200"
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<CloseIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -319,15 +319,7 @@ export default function Profile() {
|
||||
</h2>
|
||||
<p className="text-sm text-dark-400">{t('profile.accounts.subtitle')}</p>
|
||||
</div>
|
||||
<svg
|
||||
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>
|
||||
<ArrowRightIcon className="h-5 w-5 text-dark-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { landingApi } from '../api/landings';
|
||||
import { authApi } from '../api/auth';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import { CheckIcon, ClipboardIcon, ClockIcon, ExclamationIcon } from '@/components/icons';
|
||||
import { Spinner } from '@/components/ui/Spinner';
|
||||
import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
|
||||
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
|
||||
@@ -299,32 +300,12 @@ function SuccessState({
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
{t('landing.copied', 'Copied!')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
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>
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
{t('landing.copyLink', 'Copy link')}
|
||||
</>
|
||||
)}
|
||||
@@ -383,19 +364,7 @@ function PendingActivationState({
|
||||
>
|
||||
{/* Warning icon */}
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-warning-500/10">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-10 w-10 text-warning-400" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -560,19 +529,7 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
|
||||
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">
|
||||
<svg
|
||||
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>
|
||||
<ClockIcon className="h-10 w-10 text-dark-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-dark-50">
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
PurchaseRequest,
|
||||
} from '../api/landings';
|
||||
import { StaticBackgroundRenderer } from '../components/backgrounds/BackgroundRenderer';
|
||||
import { CheckCircleIcon, CheckIcon, DevicesIcon, DownloadIcon } from '@/components/icons';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
import { cn } from '../lib/utils';
|
||||
import { getApiErrorMessage } from '../utils/api-error';
|
||||
@@ -285,52 +286,18 @@ function TariffCard({
|
||||
isSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600',
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<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>
|
||||
)}
|
||||
{isSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info row */}
|
||||
<div className="flex items-center gap-3 text-xs text-dark-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
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>
|
||||
<DownloadIcon className="h-3.5 w-3.5" />
|
||||
{tariff.traffic_limit_gb === 0 ? '∞' : tariff.traffic_limit_gb} {t('landing.gb', 'GB')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
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>
|
||||
<DevicesIcon className="h-3.5 w-3.5" />
|
||||
{tariff.device_limit} {t('landing.devices', 'devices')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -550,15 +517,7 @@ function SummaryCard({
|
||||
{config.features.map((feature, idx) => (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<CheckCircleIcon className="h-3 w-3 text-success-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-dark-100">{feature.title}</p>
|
||||
|
||||
@@ -14,9 +14,12 @@ import {
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
ExclamationIcon,
|
||||
LinkIcon,
|
||||
PartnerIcon,
|
||||
ShareIcon,
|
||||
TelegramIcon,
|
||||
UsersIcon,
|
||||
WalletIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
@@ -212,19 +215,7 @@ export default function Referral() {
|
||||
return (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<UsersIcon className="h-12 w-12 text-dark-500" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h1 className="mb-2 text-2xl font-bold text-dark-100">{t('referral.title')}</h1>
|
||||
@@ -273,9 +264,7 @@ export default function Referral() {
|
||||
{botReferralLink && (
|
||||
<div>
|
||||
<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">
|
||||
<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>
|
||||
<TelegramIcon className="h-4 w-4 text-accent-400" />
|
||||
{t('referral.botLink')}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
@@ -302,19 +291,7 @@ export default function Referral() {
|
||||
{/* Cabinet link */}
|
||||
<div>
|
||||
<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="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>
|
||||
<LinkIcon className="h-4 w-4 text-accent-400" />
|
||||
{t('referral.cabinetLink')}
|
||||
</div>
|
||||
<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="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
|
||||
<svg
|
||||
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>
|
||||
<UsersIcon className="h-8 w-8 text-dark-500" />
|
||||
</div>
|
||||
<div className="text-dark-400">{t('referral.noReferrals')}</div>
|
||||
</div>
|
||||
@@ -519,19 +484,7 @@ export default function Referral() {
|
||||
<div className="bento-card border-error-500/20">
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-semibold text-dark-100">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { CloseIcon } from '@/components/icons';
|
||||
import { referralNetworkApi } from '@/api/referralNetwork';
|
||||
import { useReferralNetworkStore } from '@/store/referralNetwork';
|
||||
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"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<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>
|
||||
<CloseIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MinusIcon, PlusIcon } from '@/components/icons';
|
||||
import { getSigmaInstance } from '../sigmaGlobals';
|
||||
|
||||
interface NetworkControlsProps {
|
||||
@@ -62,32 +63,12 @@ export function NetworkControls({ className }: NetworkControlsProps) {
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.zoomIn'),
|
||||
onClick: handleZoomIn,
|
||||
icon: (
|
||||
<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>
|
||||
),
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.zoomOut'),
|
||||
onClick: handleZoomOut,
|
||||
icon: (
|
||||
<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>
|
||||
),
|
||||
icon: <MinusIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.resetZoom'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloseIcon, FilterIcon } from '@/components/icons';
|
||||
import { useReferralNetworkStore } from '@/store/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'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<FilterIcon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t('admin.referralNetwork.filters.title')}</span>
|
||||
{hasActiveFilters && (
|
||||
<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')}
|
||||
className="text-dark-500 transition-colors hover:text-dark-300"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{panelContent}
|
||||
@@ -182,15 +163,7 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
|
||||
aria-label={t('common.close')}
|
||||
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300"
|
||||
>
|
||||
<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>
|
||||
<CloseIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
{panelContent}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { referralNetworkApi } from '@/api/referralNetwork';
|
||||
import { CheckIcon, CloseIcon, PlusIcon, SearchIcon } from '@/components/icons';
|
||||
import { MAX_SCOPE_ITEMS } from '@/store/referralNetwork';
|
||||
import type { ScopeSelection, ScopeType } from '@/types/referralNetwork';
|
||||
|
||||
@@ -30,28 +31,6 @@ const AVATAR_LETTERS: Record<ScopeType, string> = {
|
||||
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 }) {
|
||||
return (
|
||||
<div
|
||||
@@ -245,7 +224,7 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
|
||||
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"
|
||||
>
|
||||
<CloseIcon />
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
@@ -273,15 +252,7 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
|
||||
}`}
|
||||
disabled={isMaxReached && !isDropdownOpen}
|
||||
>
|
||||
<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>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -323,19 +294,7 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
|
||||
</div>
|
||||
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<svg
|
||||
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>
|
||||
<SearchIcon className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-dark-500" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { referralNetworkApi } from '@/api/referralNetwork';
|
||||
import { CloseIcon } from '@/components/icons';
|
||||
import { useReferralNetworkStore } from '@/store/referralNetwork';
|
||||
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"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<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>
|
||||
<CloseIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSearchParams, Link, useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authApi } from '../api/auth';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
|
||||
export default function ResetPassword() {
|
||||
const { t } = useTranslation();
|
||||
@@ -97,15 +98,7 @@ export default function ResetPassword() {
|
||||
{status === 'success' ? (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-8 w-8 text-success-400" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-bold text-dark-50">
|
||||
{t('resetPassword.success', 'Password changed!')}
|
||||
|
||||
@@ -17,7 +17,16 @@ import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { useCloseOnSuccessNotification } from '../store/successNotification';
|
||||
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 { resolveConnectionUrlForUi } from '../utils/connectionLink';
|
||||
import {
|
||||
@@ -97,26 +106,17 @@ const CountdownTimer = memo(function CountdownTimer({
|
||||
: g.hoverBg,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={
|
||||
isExpired
|
||||
<span
|
||||
style={{
|
||||
color: isExpired
|
||||
? 'rgb(var(--color-critical-500))'
|
||||
: isUrgent
|
||||
? 'rgb(var(--color-urgent-400))'
|
||||
: g.textSecondary
|
||||
}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
: g.textSecondary,
|
||||
}}
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-[13px] w-[13px]" />
|
||||
</span>
|
||||
</div>
|
||||
{t('dashboard.remaining')}
|
||||
</div>
|
||||
@@ -774,20 +774,10 @@ export default function Subscription() {
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
className={`h-3 w-3 ${refreshTrafficMutation.isPending ? 'animate-spin' : ''}`}
|
||||
fill="none"
|
||||
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>
|
||||
<RefreshIcon
|
||||
className="h-3 w-3"
|
||||
spinning={refreshTrafficMutation.isPending}
|
||||
/>
|
||||
{trafficRefreshCooldown > 0
|
||||
? `${trafficRefreshCooldown}s`
|
||||
: t('common.refresh')}
|
||||
@@ -827,23 +817,9 @@ export default function Subscription() {
|
||||
>
|
||||
<div
|
||||
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
|
||||
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>
|
||||
<DevicesIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<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 h-7 w-7 items-center justify-center rounded-[8px]"
|
||||
style={{ background: `${zone.mainHex}12` }}
|
||||
style={{ background: `${zone.mainHex}12`, color: zone.mainHex }}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<DownloadIcon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-dark-50">
|
||||
{purchase.traffic_gb} {t('common.units.gb')}
|
||||
@@ -1121,23 +1085,9 @@ export default function Subscription() {
|
||||
>
|
||||
<div
|
||||
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
|
||||
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>
|
||||
<TrashIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="text-sm text-dark-50/30">{t('subscription.noSubscription')}</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTarif
|
||||
import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm';
|
||||
import { TariffPickerGrid } from '../components/subscription/purchase/TariffPickerGrid';
|
||||
import { ClassicPurchaseWizard } from '../components/subscription/purchase/ClassicPurchaseWizard';
|
||||
import { ExclamationIcon, SparklesIcon } from '@/components/icons';
|
||||
|
||||
export default function SubscriptionPurchase() {
|
||||
const { t } = useTranslation();
|
||||
@@ -177,23 +178,12 @@ export default function SubscriptionPurchase() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
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
|
||||
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>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
@@ -225,23 +215,12 @@ export default function SubscriptionPurchase() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
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
|
||||
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>
|
||||
<ExclamationIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Navigate, useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ClipboardIcon, PlusIcon } from '@/components/icons';
|
||||
import { subscriptionApi } from '../api/subscription';
|
||||
import { balanceApi } from '../api/balance';
|
||||
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"
|
||||
style={{ background: g.innerBg }}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<ClipboardIcon className="h-8 w-8 opacity-40" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold" style={{ color: g.text }}>
|
||||
{t('subscriptions.empty', 'Нет подписок')}
|
||||
@@ -128,15 +117,7 @@ export default function Subscriptions() {
|
||||
border: '1px solid rgba(var(--color-accent-400), 0.2)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('subscriptions.buyAnother', 'Новый тариф')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { TicketDetail } from '../types';
|
||||
import { Card } from '@/components/data-display/Card';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
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 { linkifyText } from '../utils/linkify';
|
||||
|
||||
@@ -266,19 +266,7 @@ export default function Support() {
|
||||
<div className="mx-auto mt-12 max-w-md">
|
||||
<Card className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
|
||||
<svg
|
||||
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>
|
||||
<ChatIcon className="h-8 w-8 text-dark-400" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold text-dark-100">{supportMessage.title}</h2>
|
||||
<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">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-dark-800">
|
||||
<svg
|
||||
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>
|
||||
<ChatIcon className="h-5 w-5 text-dark-400" />
|
||||
</div>
|
||||
<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="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
|
||||
<svg
|
||||
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>
|
||||
<ChatIcon className="h-8 w-8 text-dark-500" />
|
||||
</div>
|
||||
<div className="text-dark-400">{t('support.noTickets')}</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { brandingApi } from '../api/branding';
|
||||
import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK';
|
||||
import { tokenStorage } from '../utils/token';
|
||||
import { getSafeRedirectPath } from '../utils/safeRedirect';
|
||||
import { CheckIcon, XIcon, ExclamationIcon } from '@/components/icons';
|
||||
|
||||
const MAX_RETRY_ATTEMPTS = 3;
|
||||
const RETRY_COUNT_KEY = 'telegram_redirect_retry_count';
|
||||
@@ -158,15 +159,7 @@ export default function TelegramRedirect() {
|
||||
{status === 'success' && (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<CheckIcon className="h-8 w-8 text-success-400" />
|
||||
</div>
|
||||
<p className="text-dark-200">{t('auth.loginSuccess')}</p>
|
||||
<p className="mt-2 text-sm text-dark-500">{t('telegramRedirect.redirecting')}</p>
|
||||
@@ -177,15 +170,7 @@ export default function TelegramRedirect() {
|
||||
{status === 'error' && (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<XIcon className="h-8 w-8 text-error-400" />
|
||||
</div>
|
||||
<p className="mb-2 text-dark-200">{t('auth.loginFailed')}</p>
|
||||
<p className="mb-6 text-sm text-error-400">{errorMessage}</p>
|
||||
@@ -204,19 +189,7 @@ export default function TelegramRedirect() {
|
||||
{status === 'not-telegram' && (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-8 w-8 text-warning-400" />
|
||||
</div>
|
||||
<p className="mb-2 text-dark-200">{t('telegramRedirect.openInTelegram')}</p>
|
||||
<p className="mb-6 text-sm text-dark-400">{t('telegramRedirect.openInTelegramDesc')}</p>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CryptoIcon,
|
||||
ExclamationIcon,
|
||||
ExternalLinkIcon,
|
||||
SparklesIcon,
|
||||
StarIcon,
|
||||
@@ -520,19 +521,7 @@ export default function TopUpAmount() {
|
||||
variants={staggerItem}
|
||||
className="flex items-center gap-2 rounded-xl border border-error-500/20 bg-error-500/10 p-3"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<ExclamationIcon className="h-5 w-5 shrink-0 text-error-400" />
|
||||
<span className="text-sm text-error-400">{error}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user