mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
refactor(cabinet): migrate inline SVG icons to the central react-icons set
Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77 files with the central Phosphor (react-icons/pi) components from @/components/icons, preserving each icon's size classes and colour (dynamic stroke colours via the parent's currentColor, RefreshIcon's spinning state, conditional rotate-180 chevrons). Verified: tsc + vite build + eslint clean; an adversarial diff review of all changed files found the replacements correct (70 files clean, 0 blockers). Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate non-icons (brand/provider logos, loading spinners & framer-motion animations, charts/sparklines, background decoration) plus a few ambiguous glyphs.
This commit is contained in:
@@ -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