feat(cabinet): migrate all icons to the panel's Phosphor set

Replace the cabinet's hand-written heroicons-style SVG icon components with
the Remnawave panel's own icon family — Phosphor via react-icons/pi (Duotone).
All icons now live in a single central barrel (src/components/icons:
index + extended-icons + editor-icons, 144 icons); every feature file imports
from it instead of redefining inline SVGs (~440 icon defs removed).

- Add react-icons dependency
- Desktop nav (AppShell) now uses the central Phosphor icons, removing a
  stroke-vs-duotone inconsistency the partial migration introduced
- Icons with custom props (SortIcon's direction, expandable chevrons) kept as
  thin Phosphor wrappers; the RemnawaveIcon brand logo and animated SVGs
  (checkmarks, spinners) are intentionally left as-is
- Restore the original per-call-site icon sizes wherever the centralized
  default differed (211 call sites)
This commit is contained in:
c0mrade
2026-05-31 18:50:50 +03:00
parent a62d689fd3
commit d0e0b6b7e3
94 changed files with 1368 additions and 5059 deletions

10
package-lock.json generated
View File

@@ -55,6 +55,7 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
"react-icons": "^5.6.0",
"react-router": "^7.13.0", "react-router": "^7.13.0",
"react-twemoji": "^0.7.2", "react-twemoji": "^0.7.2",
"recharts": "^3.7.0", "recharts": "^3.7.0",
@@ -6908,6 +6909,15 @@
} }
} }
}, },
"node_modules/react-icons": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
"integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.2.4", "version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",

View File

@@ -63,6 +63,7 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
"react-icons": "^5.6.0",
"react-router": "^7.13.0", "react-router": "^7.13.0",
"react-twemoji": "^0.7.2", "react-twemoji": "^0.7.2",
"recharts": "^3.7.0", "recharts": "^3.7.0",

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { promoApi, PromoOffer } from '../api/promo'; import { promoApi, PromoOffer } from '../api/promo';
import { ClockIcon, CheckIcon } from './icons'; import { ClockIcon, CheckIcon, XCircleIcon } from './icons';
import { useDestructiveConfirm } from '@/platform/hooks/useNativeDialog'; import { useDestructiveConfirm } from '@/platform/hooks/useNativeDialog';
// Helper functions // Helper functions
@@ -69,17 +69,6 @@ const getOfferDescription = (
return t('promo.offers.activateDiscountHint'); return t('promo.offers.activateDiscountHint');
}; };
// Icons for deactivation
const XCircleIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
interface PromoOffersSectionProps { interface PromoOffersSectionProps {
className?: string; className?: string;
} }
@@ -237,7 +226,7 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
onClick={handleDeactivateClick} onClick={handleDeactivateClick}
className="flex items-center justify-center gap-1.5 rounded-xl border border-dark-600/50 bg-dark-900/50 px-4 py-2.5 text-sm text-dark-400 transition-colors hover:border-error-500/30 hover:bg-error-500/10 hover:text-error-400" className="flex items-center justify-center gap-1.5 rounded-xl border border-dark-600/50 bg-dark-900/50 px-4 py-2.5 text-sm text-dark-400 transition-colors hover:border-error-500/30 hover:bg-error-500/10 hover:text-error-400"
> >
<XCircleIcon /> <XCircleIcon className="h-4 w-4" />
<span>{t('promo.deactivate.button')}</span> <span>{t('promo.deactivate.button')}</span>
</button> </button>
</div> </div>

View File

@@ -12,69 +12,14 @@ import { useCurrency } from '../hooks/useCurrency';
import { useTelegramSDK } from '../hooks/useTelegramSDK'; import { useTelegramSDK } from '../hooks/useTelegramSDK';
import { useFocusTrap } from '../hooks/useFocusTrap'; import { useFocusTrap } from '../hooks/useFocusTrap';
import { useHaptic } from '@/platform'; import { useHaptic } from '@/platform';
import {
// Icons CheckCircleIcon,
const CheckCircleIcon = () => ( CloseIcon,
<svg DevicesIcon,
className="h-16 w-16" RocketIcon,
fill="none" TrafficIcon,
viewBox="0 0 24 24" WalletIcon,
stroke="currentColor" } from '@/components/icons';
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>
);
const WalletIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"
/>
</svg>
);
const RocketIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"
/>
</svg>
);
const DevicesIcon = () => (
<svg className="h-8 w-8" 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>
);
const TrafficIcon = () => (
<svg className="h-8 w-8" 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>
);
const CloseIcon = () => (
<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>
);
export default function SuccessNotificationModal() { export default function SuccessNotificationModal() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -161,33 +106,33 @@ export default function SuccessNotificationModal() {
// Determine title and message // Determine title and message
let title = data.title; let title = data.title;
const message = data.message; const message = data.message;
let icon = <CheckCircleIcon />; let icon = <CheckCircleIcon className="h-16 w-16" />;
let gradientClass = 'from-success-500 to-success-600'; let gradientClass = 'from-success-500 to-success-600';
if (!title) { if (!title) {
if (isBalanceTopup) { if (isBalanceTopup) {
title = t('successNotification.balanceTopup.title', 'Balance topped up!'); title = t('successNotification.balanceTopup.title', 'Balance topped up!');
icon = <WalletIcon />; icon = <WalletIcon className="h-8 w-8" />;
gradientClass = 'from-success-500 to-success-600'; gradientClass = 'from-success-500 to-success-600';
} else if (data.type === 'subscription_activated') { } else if (data.type === 'subscription_activated') {
title = t('successNotification.subscriptionActivated.title', 'Subscription activated!'); title = t('successNotification.subscriptionActivated.title', 'Subscription activated!');
icon = <RocketIcon />; icon = <RocketIcon className="h-8 w-8" />;
gradientClass = 'from-accent-500 to-purple-600'; gradientClass = 'from-accent-500 to-purple-600';
} else if (data.type === 'subscription_renewed') { } else if (data.type === 'subscription_renewed') {
title = t('successNotification.subscriptionRenewed.title', 'Subscription renewed!'); title = t('successNotification.subscriptionRenewed.title', 'Subscription renewed!');
icon = <RocketIcon />; icon = <RocketIcon className="h-8 w-8" />;
gradientClass = 'from-accent-500 to-purple-600'; gradientClass = 'from-accent-500 to-purple-600';
} else if (data.type === 'subscription_purchased') { } else if (data.type === 'subscription_purchased') {
title = t('successNotification.subscriptionPurchased.title', 'Subscription purchased!'); title = t('successNotification.subscriptionPurchased.title', 'Subscription purchased!');
icon = <RocketIcon />; icon = <RocketIcon className="h-8 w-8" />;
gradientClass = 'from-accent-500 to-purple-600'; gradientClass = 'from-accent-500 to-purple-600';
} else if (data.type === 'devices_purchased') { } else if (data.type === 'devices_purchased') {
title = t('successNotification.devicesPurchased.title', 'Devices added!'); title = t('successNotification.devicesPurchased.title', 'Devices added!');
icon = <DevicesIcon />; icon = <DevicesIcon className="h-8 w-8" />;
gradientClass = 'from-blue-500 to-cyan-600'; gradientClass = 'from-blue-500 to-cyan-600';
} else if (data.type === 'traffic_purchased') { } else if (data.type === 'traffic_purchased') {
title = t('successNotification.trafficPurchased.title', 'Traffic added!'); title = t('successNotification.trafficPurchased.title', 'Traffic added!');
icon = <TrafficIcon />; icon = <TrafficIcon className="h-8 w-8" />;
gradientClass = 'from-success-500 to-success-600'; gradientClass = 'from-success-500 to-success-600';
} }
} }
@@ -334,7 +279,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToSubscription} onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600" className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
> >
<RocketIcon /> <RocketIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span> <span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button> </button>
)} )}
@@ -344,7 +289,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToBalance} onClick={handleGoToBalance}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-success-500 py-3.5 font-bold text-white shadow-lg shadow-success-500/25 transition-colors hover:bg-success-400 active:bg-success-600" className="flex w-full items-center justify-center gap-2 rounded-xl bg-success-500 py-3.5 font-bold text-white shadow-lg shadow-success-500/25 transition-colors hover:bg-success-400 active:bg-success-600"
> >
<WalletIcon /> <WalletIcon className="h-8 w-8" />
<span>{t('successNotification.goToBalance', 'Go to Balance')}</span> <span>{t('successNotification.goToBalance', 'Go to Balance')}</span>
</button> </button>
)} )}
@@ -354,7 +299,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToSubscription} onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600" className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 py-3.5 font-bold text-white shadow-lg shadow-accent-500/25 transition-colors hover:bg-accent-400 active:bg-accent-600"
> >
<DevicesIcon /> <DevicesIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span> <span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button> </button>
)} )}
@@ -364,7 +309,7 @@ export default function SuccessNotificationModal() {
onClick={handleGoToSubscription} onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-success-500 py-3.5 font-bold text-white shadow-lg shadow-success-500/25 transition-colors hover:bg-success-400 active:bg-success-600" className="flex w-full items-center justify-center gap-2 rounded-xl bg-success-500 py-3.5 font-bold text-white shadow-lg shadow-success-500/25 transition-colors hover:bg-success-400 active:bg-success-600"
> >
<TrafficIcon /> <TrafficIcon className="h-8 w-8" />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span> <span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button> </button>
)} )}

View File

@@ -8,22 +8,7 @@ import { useToast } from './Toast';
import { useWebSocket, WSMessage } from '../hooks/useWebSocket'; import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
import { useHeaderHeight } from '../hooks/useHeaderHeight'; import { useHeaderHeight } from '../hooks/useHeaderHeight';
import type { TicketNotification } from '../types'; import type { TicketNotification } from '../types';
import { BellIcon, CheckIcon } from '@/components/icons';
const BellIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
);
const CheckIcon = () => (
<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>
);
interface TicketNotificationBellProps { interface TicketNotificationBellProps {
isAdmin?: boolean; isAdmin?: boolean;

View File

@@ -18,6 +18,15 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { PiCaretDownDuotone } from 'react-icons/pi';
import {
GripIcon,
TrashIcon,
PlusIcon,
LinkIcon,
ArrowUpIcon,
ArrowDownIcon,
} from '@/components/icons';
import { import {
menuLayoutApi, menuLayoutApi,
type MenuConfig, type MenuConfig,
@@ -31,82 +40,10 @@ import { Toggle } from './Toggle';
import { useNotify } from '../../platform/hooks/useNotify'; import { useNotify } from '../../platform/hooks/useNotify';
import { useNativeDialog } from '../../platform/hooks/useNativeDialog'; import { useNativeDialog } from '../../platform/hooks/useNativeDialog';
const GripIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const PlusIcon = () => (
<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>
);
const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg <PiCaretDownDuotone
className={`h-3.5 w-3.5 transition-transform ${expanded ? 'rotate-180' : ''}`} className={`h-3.5 w-3.5 transition-transform ${expanded ? '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>
);
const LinkIcon = () => (
<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="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.757m9.86-2.54a4.5 4.5 0 00-1.242-7.244l-4.5-4.5a4.5 4.5 0 00-6.364 6.364L4.03 8.591"
/>
</svg>
);
const ArrowUpIcon = () => (
<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="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg>
);
const ArrowDownIcon = () => (
<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="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
); );
function generateId(): string { function generateId(): string {
@@ -200,7 +137,7 @@ function ButtonChip({
: 'cursor-default text-dark-700' : 'cursor-default text-dark-700'
}`} }`}
> >
<ArrowUpIcon /> <ArrowUpIcon className="h-3.5 w-3.5" />
</button> </button>
<button <button
onClick={onMoveDown ?? undefined} onClick={onMoveDown ?? undefined}
@@ -212,7 +149,7 @@ function ButtonChip({
: 'cursor-default text-dark-700' : 'cursor-default text-dark-700'
}`} }`}
> >
<ArrowDownIcon /> <ArrowDownIcon className="h-3.5 w-3.5" />
</button> </button>
</div> </div>
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${colorDotClass}`} /> <span className={`h-2.5 w-2.5 shrink-0 rounded-full ${colorDotClass}`} />
@@ -221,7 +158,7 @@ function ButtonChip({
</span> </span>
{!isBuiltin && ( {!isBuiltin && (
<span className="text-dark-500" title="URL"> <span className="text-dark-500" title="URL">
<LinkIcon /> <LinkIcon className="h-3.5 w-3.5" />
</span> </span>
)} )}
<Toggle checked={button.enabled} onChange={() => onUpdate({ enabled: !button.enabled })} /> <Toggle checked={button.enabled} onChange={() => onUpdate({ enabled: !button.enabled })} />
@@ -236,7 +173,7 @@ function ButtonChip({
onClick={onRemove} onClick={onRemove}
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400" className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
)} )}
</div> </div>
@@ -425,7 +362,7 @@ function SortableRow({
onClick={() => onRemoveRow(row.id)} onClick={() => onRemoveRow(row.id)}
className="rounded-lg p-1.5 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400" className="rounded-lg p-1.5 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
)} )}
</div> </div>
@@ -481,7 +418,7 @@ function InlineAddPanel({ rowId, usedBuiltinIds, onAddBuiltin, onAddCustom }: In
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
className="flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-dark-700/50 py-2.5 text-sm text-dark-500 transition-colors hover:border-dark-600 hover:text-dark-400" className="flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-dark-700/50 py-2.5 text-sm text-dark-500 transition-colors hover:border-dark-600 hover:text-dark-400"
> >
<PlusIcon /> <PlusIcon className="h-4 w-4" />
{t('admin.menuEditor.addButton')} {t('admin.menuEditor.addButton')}
</button> </button>
); );
@@ -516,7 +453,7 @@ function InlineAddPanel({ rowId, usedBuiltinIds, onAddBuiltin, onAddCustom }: In
}} }}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-dark-200 transition-colors hover:bg-dark-700/50" className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-dark-200 transition-colors hover:bg-dark-700/50"
> >
<LinkIcon /> <LinkIcon className="h-3.5 w-3.5" />
{t('admin.menuEditor.addUrlButton')} {t('admin.menuEditor.addUrlButton')}
</button> </button>
<button <button
@@ -843,7 +780,7 @@ export function MenuEditorTab() {
onClick={addRow} onClick={addRow}
className="flex w-full items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-dark-700/50 py-4 text-sm font-medium text-dark-500 transition-colors hover:border-dark-600 hover:text-dark-400" className="flex w-full items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-dark-700/50 py-4 text-sm font-medium text-dark-500 transition-colors hover:border-dark-600 hover:text-dark-400"
> >
<PlusIcon /> <PlusIcon className="h-4 w-4" />
{t('admin.menuEditor.addRow')} {t('admin.menuEditor.addRow')}
</button> </button>

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSortable } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { GripIcon, TrashIcon } from '../icons/LandingIcons'; import { GripIcon, TrashIcon } from '../icons/LandingIcons';
@@ -10,15 +11,7 @@ import type { PaymentMethodSubOptionInfo } from '../../types';
export type MethodWithId = AdminLandingPaymentMethod & { _id: string }; export type MethodWithId = AdminLandingPaymentMethod & { _id: string };
const ChevronDownIcon = ({ open }: { open: boolean }) => ( const ChevronDownIcon = ({ open }: { open: boolean }) => (
<svg <PiCaretDownDuotone className={cn('h-5 w-5 transition-transform', open && 'rotate-180')} />
className={cn('h-5 w-5 transition-transform', open && '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>
); );
interface SortableSelectedMethodProps { interface SortableSelectedMethodProps {

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckIcon, XCloseIcon } from '@/components/icons';
import { useFocusTrap } from '@/hooks/useFocusTrap'; import { useFocusTrap } from '@/hooks/useFocusTrap';
import { DropdownSelect } from './DropdownSelect'; import { DropdownSelect } from './DropdownSelect';
import type { UserListItem } from '../../../api/adminUsers'; import type { UserListItem } from '../../../api/adminUsers';
@@ -48,24 +49,6 @@ export interface ModalState {
progress: ProgressState | null; progress: ProgressState | null;
} }
const CheckIcon = () => (
<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>
);
const XCloseIcon = () => (
<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>
);
function ProgressView({ progress }: { progress: ProgressState }) { function ProgressView({ progress }: { progress: ProgressState }) {
const { t } = useTranslation(); const { t } = useTranslation();
const logEndRef = useRef<HTMLDivElement>(null); const logEndRef = useRef<HTMLDivElement>(null);
@@ -520,7 +503,7 @@ export function ActionModal({
)} )}
aria-pressed={forceDeleteActivePaid} aria-pressed={forceDeleteActivePaid}
> >
{forceDeleteActivePaid && <CheckIcon />} {forceDeleteActivePaid && <CheckIcon className="h-3 w-3" />}
</button> </button>
<span <span
className={cn( className={cn(
@@ -558,7 +541,7 @@ export function ActionModal({
)} )}
aria-pressed={deleteFromPanel} aria-pressed={deleteFromPanel}
> >
{deleteFromPanel && <CheckIcon />} {deleteFromPanel && <CheckIcon className="h-3 w-3" />}
</button> </button>
<span <span
className={cn( className={cn(

View File

@@ -1,3 +1,4 @@
import { ChevronDownIcon } from '@/components/icons';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
@@ -7,11 +8,10 @@ import { cn } from '@/lib/utils';
// chrome (border, focus ring, chevron). // chrome (border, focus ring, chevron).
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
export const ChevronDownIcon = () => ( // Re-exported so the sibling FloatingActionBar / MultiSelectDropdown keep
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> // importing the chevron from this module while the glyph itself now comes
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /> // from the central Phosphor barrel instead of a hand-written SVG.
</svg> export { ChevronDownIcon };
);
export interface DropdownOption { export interface DropdownOption {
value: string; value: string;
@@ -40,7 +40,7 @@ export function DropdownSelect({ value, options, onChange, className }: Dropdown
))} ))}
</select> </select>
<div className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-dark-500"> <div className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-dark-500">
<ChevronDownIcon /> <ChevronDownIcon className="h-4 w-4" />
</div> </div>
</div> </div>
); );

View File

@@ -1,164 +1,35 @@
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
// TrafficIcons — the inline SVG set used across AdminTrafficUsage // TrafficIcons — the icon set used across AdminTrafficUsage (header
// (header controls, filter buttons, sort indicator, etc.). Page-local // controls, filter buttons, sort indicator, etc.). The glyphs now come
// in spirit; co-located here so the parent page imports a single // from the central Phosphor barrel (`@/components/icons`); only the
// flat barrel rather than redefining 15 inline icons. // sort indicator keeps its custom `direction` prop and is therefore a
// thin local wrapper over the panel's Phosphor caret icons.
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
export const SearchIcon = () => ( import { PiCaretDownDuotone, PiCaretUpDownDuotone, PiCaretUpDuotone } from 'react-icons/pi';
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
export const ChevronLeftIcon = () => ( export {
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> CalendarIcon,
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" /> ChevronDownIcon,
</svg> ChevronLeftIcon,
); ChevronRightIcon,
DownloadIcon,
FilterIcon,
GlobeIcon,
RefreshIcon,
SearchIcon,
ServerIcon,
ServerSmallIcon,
ShieldIcon,
StatusIcon,
XIcon,
} from '@/components/icons';
export const ChevronRightIcon = () => ( export const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) =>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> direction === 'asc' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /> <PiCaretUpDuotone className="ml-1 inline h-3 w-3" />
</svg> ) : direction === 'desc' ? (
); <PiCaretDownDuotone className="ml-1 inline h-3 w-3" />
) : (
export const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( <PiCaretUpDownDuotone className="ml-1 inline h-3 w-3" />
<svg className={className} 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>
);
export const DownloadIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
export const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) => (
<svg
className="ml-1 inline h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
{direction === 'asc' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
) : direction === 'desc' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
/>
)}
</svg>
);
export const FilterIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
);
export const ChevronDownIcon = () => (
<svg className="h-3 w-3" 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>
);
export const ServerIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 00-.12-1.03l-2.268-9.64a3.375 3.375 0 00-3.285-2.602H7.923a3.375 3.375 0 00-3.285 2.602l-2.268 9.64a4.5 4.5 0 00-.12 1.03v.228m19.5 0a3 3 0 01-3 3H5.25a3 3 0 01-3-3m19.5 0a3 3 0 00-3-3H5.25a3 3 0 00-3 3m16.5 0h.008v.008h-.008v-.008zm-3 0h.008v.008h-.008v-.008z"
/>
</svg>
);
export const CalendarIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
export const XIcon = () => (
<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>
);
export const StatusIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
export const GlobeIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
);
export const ShieldIcon = () => (
<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="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
export const ServerSmallIcon = () => (
<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="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>
);

View File

@@ -49,13 +49,13 @@ export function CountryFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700' : 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`} }`}
> >
<GlobeIcon /> <GlobeIcon className="h-4 w-4" />
{activeCount > 0 && ( {activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white"> <span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount} {activeCount}
</span> </span>
)} )}
<ChevronDownIcon /> <ChevronDownIcon className="h-3 w-3" />
</button> </button>
{open && ( {open && (

View File

@@ -52,14 +52,14 @@ export function NodeFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700' : 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`} }`}
> >
<ServerIcon /> <ServerIcon className="h-4 w-4" />
{t('admin.trafficUsage.nodes')} {t('admin.trafficUsage.nodes')}
{activeCount > 0 && ( {activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white"> <span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount} {activeCount}
</span> </span>
)} )}
<ChevronDownIcon /> <ChevronDownIcon className="h-3 w-3" />
</button> </button>
{open && ( {open && (

View File

@@ -42,7 +42,7 @@ export function PeriodSelector({
if (dateMode) { if (dateMode) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarIcon /> <CalendarIcon className="h-4 w-4" />
<span className="text-xs text-dark-400">{t('admin.trafficUsage.dateFrom')}</span> <span className="text-xs text-dark-400">{t('admin.trafficUsage.dateFrom')}</span>
<input <input
type="date" type="date"
@@ -66,7 +66,7 @@ export function PeriodSelector({
className="rounded-lg p-1 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200" className="rounded-lg p-1 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
title={t('admin.trafficUsage.period')} title={t('admin.trafficUsage.period')}
> >
<XIcon /> <XIcon className="h-3 w-3" />
</button> </button>
</div> </div>
); );
@@ -96,7 +96,7 @@ export function PeriodSelector({
className="rounded-lg border border-dark-700 bg-dark-800 p-1.5 text-dark-400 transition-colors hover:border-dark-600 hover:bg-dark-700 hover:text-dark-200" className="rounded-lg border border-dark-700 bg-dark-800 p-1.5 text-dark-400 transition-colors hover:border-dark-600 hover:bg-dark-700 hover:text-dark-200"
title={t('admin.trafficUsage.customDates')} title={t('admin.trafficUsage.customDates')}
> >
<CalendarIcon /> <CalendarIcon className="h-4 w-4" />
</button> </button>
</div> </div>
); );

View File

@@ -63,14 +63,14 @@ export function StatusFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700' : 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`} }`}
> >
<StatusIcon /> <StatusIcon className="h-4 w-4" />
{t('admin.trafficUsage.status')} {t('admin.trafficUsage.status')}
{activeCount > 0 && ( {activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white"> <span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount} {activeCount}
</span> </span>
)} )}
<ChevronDownIcon /> <ChevronDownIcon className="h-3 w-3" />
</button> </button>
{open && ( {open && (

View File

@@ -50,14 +50,14 @@ export function TariffFilter({
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700' : 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600 hover:bg-dark-700'
}`} }`}
> >
<FilterIcon /> <FilterIcon className="h-4 w-4" />
{t('admin.trafficUsage.tariff')} {t('admin.trafficUsage.tariff')}
{activeCount > 0 && ( {activeCount > 0 && (
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white"> <span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
{activeCount} {activeCount}
</span> </span>
)} )}
<ChevronDownIcon /> <ChevronDownIcon className="h-3 w-3" />
</button> </button>
{open && ( {open && (

View File

@@ -6,22 +6,7 @@ import { adminUsersApi, type UserDetailResponse } from '../../../api/adminUsers'
import { promocodesApi } from '../../../api/promocodes'; import { promocodesApi } from '../../../api/promocodes';
import { promoOffersApi } from '../../../api/promoOffers'; import { promoOffersApi } from '../../../api/promoOffers';
import { createNumberInputHandler, toNumber } from '../../../utils/inputHelpers'; import { createNumberInputHandler, toNumber } from '../../../utils/inputHelpers';
import { PlusIcon, MinusIcon } from '@/components/icons';
// ──────────────────────────────────────────────────────────────────
// Icons — local; balance is the only consumer.
// ──────────────────────────────────────────────────────────────────
const PlusIcon = () => (
<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>
);
const MinusIcon = () => (
<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>
);
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
// Balance tab — current balance, add/subtract form, active promo // Balance tab — current balance, add/subtract form, active promo
@@ -168,14 +153,14 @@ export function BalanceTab({
disabled={actionLoading || balanceAmount === ''} disabled={actionLoading || balanceAmount === ''}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-success-500 py-2 text-white transition-colors hover:bg-success-600 disabled:opacity-50" className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-success-500 py-2 text-white transition-colors hover:bg-success-600 disabled:opacity-50"
> >
<PlusIcon /> {t('admin.users.detail.balance.add')} <PlusIcon className="h-4 w-4" /> {t('admin.users.detail.balance.add')}
</button> </button>
<button <button
onClick={() => handleUpdateBalance(false)} onClick={() => handleUpdateBalance(false)}
disabled={actionLoading || balanceAmount === ''} disabled={actionLoading || balanceAmount === ''}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-error-500 py-2 text-white transition-colors hover:bg-error-600 disabled:opacity-50" className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-error-500 py-2 text-white transition-colors hover:bg-error-600 disabled:opacity-50"
> >
<MinusIcon /> {t('admin.users.detail.balance.subtract')} <MinusIcon className="h-4 w-4" /> {t('admin.users.detail.balance.subtract')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MinusIcon, PlusIcon, RefreshIcon } from '@/components/icons';
import { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices'; import { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices';
import { createNumberInputHandler } from '../../../utils/inputHelpers'; import { createNumberInputHandler } from '../../../utils/inputHelpers';
import { getFlagEmoji } from '../../../utils/subscriptionHelpers'; import { getFlagEmoji } from '../../../utils/subscriptionHelpers';
@@ -37,28 +38,6 @@ function StatusBadge({ status }: { status: string }) {
); );
} }
const PlusIcon = () => (
<svg className="h-3 w-3" 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>
);
const MinusIcon = () => (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12h-15" />
</svg>
);
const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} 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>
);
// Local device row type (matches the parent's inline type) // Local device row type (matches the parent's inline type)
type DeviceRow = { type DeviceRow = {
hwid: string; hwid: string;
@@ -390,7 +369,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
disabled={actionLoading || selectedSub.device_limit <= 1} disabled={actionLoading || selectedSub.device_limit <= 1}
className="flex h-6 w-6 items-center justify-center rounded-md bg-dark-700 text-dark-300 transition-colors hover:bg-dark-600 disabled:opacity-30" className="flex h-6 w-6 items-center justify-center rounded-md bg-dark-700 text-dark-300 transition-colors hover:bg-dark-600 disabled:opacity-30"
> >
<MinusIcon /> <MinusIcon className="h-3 w-3" />
</button> </button>
<span className="min-w-[2ch] text-center text-dark-100"> <span className="min-w-[2ch] text-center text-dark-100">
{selectedSub.device_limit} {selectedSub.device_limit}
@@ -404,7 +383,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
} }
className="flex h-6 w-6 items-center justify-center rounded-md bg-dark-700 text-dark-300 transition-colors hover:bg-dark-600 disabled:opacity-30" className="flex h-6 w-6 items-center justify-center rounded-md bg-dark-700 text-dark-300 transition-colors hover:bg-dark-600 disabled:opacity-30"
> >
<PlusIcon /> <PlusIcon className="h-3 w-3" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,26 +1,11 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ArrowDownIcon, ArrowUpIcon } from '@/components/icons';
import type { import type {
UserDetailResponse, UserDetailResponse,
UserSubscriptionInfo, UserSubscriptionInfo,
PanelSyncStatusResponse, PanelSyncStatusResponse,
} from '../../../api/adminUsers'; } from '../../../api/adminUsers';
// ──────────────────────────────────────────────────────────────────
// Icons (sync-tab-local — not used outside this view)
// ──────────────────────────────────────────────────────────────────
const ArrowDownIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" />
</svg>
);
const ArrowUpIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
</svg>
);
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────
// Sync tab — compares bot DB vs panel data, offers a 2-way push // Sync tab — compares bot DB vs panel data, offers a 2-way push
// ────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────

View File

@@ -12,6 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks'; import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
import type { BlockRendererProps } from './blocks'; import type { BlockRendererProps } from './blocks';
import TvQuickConnect from './TvQuickConnect'; import TvQuickConnect from './TvQuickConnect';
import { BackIcon } from '@/components/icons';
const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV']; const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
@@ -33,12 +34,6 @@ const RENDERERS: Record<string, React.ComponentType<BlockRendererProps>> = {
minimal: MinimalBlock, minimal: MinimalBlock,
}; };
const BackIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
);
interface Props { interface Props {
appConfig: AppConfig; appConfig: AppConfig;
onOpenDeepLink: (url: string) => void; onOpenDeepLink: (url: string) => void;
@@ -200,7 +195,7 @@ export default function InstallationGuide({
aria-label={t('common.back', 'Back')} aria-label={t('common.back', 'Back')}
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600" className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
> >
<BackIcon /> <BackIcon className="h-6 w-6" />
</button> </button>
)} )}
<h2 className="flex-1 text-lg font-bold text-dark-100"> <h2 className="flex-1 text-lg font-bold text-dark-100">

View File

@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CheckIcon, CopyIcon } from '@/components/icons';
import type { RemnawaveButtonClient, LocalizedText } from '@/types'; import type { RemnawaveButtonClient, LocalizedText } from '@/types';
import { copyToClipboard } from '@/utils/clipboard'; import { copyToClipboard } from '@/utils/clipboard';
@@ -20,22 +21,6 @@ function isValidExternalUrl(url: string | undefined): boolean {
return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://'); return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
} }
const CopyIcon = () => (
<svg className="h-4 w-4" 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>
);
const CheckIcon = () => (
<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>
);
interface BlockButtonsProps { interface BlockButtonsProps {
buttons: RemnawaveButtonClient[] | undefined; buttons: RemnawaveButtonClient[] | undefined;
variant: 'light' | 'subtle'; variant: 'light' | 'subtle';

View File

@@ -1,3 +1,4 @@
import { PiCaretRightDuotone } from 'react-icons/pi';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { useCurrency } from '../../hooks/useCurrency'; import { useCurrency } from '../../hooks/useCurrency';
@@ -12,22 +13,7 @@ interface StatsGridProps {
} }
const ChevronIcon = ({ color }: { color: string }) => ( const ChevronIcon = ({ color }: { color: string }) => (
<svg <PiCaretRightDuotone width={16} height={16} style={{ flexShrink: 0, color }} aria-hidden="true" />
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
style={{ flexShrink: 0 }}
aria-hidden="true"
>
<path
d="M6 4l4 4-4 4"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
); );
export default function StatsGrid({ export default function StatsGrid({

View File

@@ -10,6 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
import { formatTraffic } from '../../utils/formatTraffic'; import { formatTraffic } from '../../utils/formatTraffic';
import { getGlassColors } from '../../utils/glassTheme'; import { getGlassColors } from '../../utils/glassTheme';
import { HoverBorderGradient } from '../ui/hover-border-gradient'; import { HoverBorderGradient } from '../ui/hover-border-gradient';
import { RefreshIcon } from '@/components/icons';
import { useHaptic } from '../../platform'; import { useHaptic } from '../../platform';
import type { Subscription } from '../../types'; import type { Subscription } from '../../types';
@@ -25,23 +26,6 @@ interface SubscriptionCardActiveProps {
connectedDevices: number; connectedDevices: number;
} }
const RefreshIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
<svg
className={className}
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>
);
export default function SubscriptionCardActive({ export default function SubscriptionCardActive({
subscription, subscription,
trafficData, trafficData,

View File

@@ -0,0 +1,85 @@
import {
PiTextBDuotone,
PiTextItalicDuotone,
PiTextUnderlineDuotone,
PiTextStrikethroughDuotone,
PiTextHOneDuotone,
PiTextHTwoDuotone,
PiTextHThreeDuotone,
PiListBulletsDuotone,
PiListNumbersDuotone,
PiQuotesDuotone,
PiCodeBlockDuotone,
PiTextAlignLeftDuotone,
PiTextAlignCenterDuotone,
PiHighlighterDuotone,
} from 'react-icons/pi';
import { cn } from '@/lib/utils';
interface IconProps {
className?: string;
}
/**
* Rich-text editor toolbar icons — Phosphor (react-icons/pi) Duotone, the
* panel's icon family. Shared by the TipTap toolbars in AdminNewsCreate and
* AdminInfoPageEditor. Names match the historical local definitions so the
* toolbars import instead of hand-rolling SVGs.
*/
export const BoldIcon = ({ className }: IconProps) => (
<PiTextBDuotone className={cn('h-5 w-5', className)} />
);
export const ItalicIcon = ({ className }: IconProps) => (
<PiTextItalicDuotone className={cn('h-5 w-5', className)} />
);
export const UnderlineIcon = ({ className }: IconProps) => (
<PiTextUnderlineDuotone className={cn('h-5 w-5', className)} />
);
export const StrikeIcon = ({ className }: IconProps) => (
<PiTextStrikethroughDuotone className={cn('h-5 w-5', className)} />
);
export const H1Icon = ({ className }: IconProps) => (
<PiTextHOneDuotone className={cn('h-5 w-5', className)} />
);
export const H2Icon = ({ className }: IconProps) => (
<PiTextHTwoDuotone className={cn('h-5 w-5', className)} />
);
export const H3Icon = ({ className }: IconProps) => (
<PiTextHThreeDuotone className={cn('h-5 w-5', className)} />
);
export const ListBulletIcon = ({ className }: IconProps) => (
<PiListBulletsDuotone className={cn('h-5 w-5', className)} />
);
export const ListOrderedIcon = ({ className }: IconProps) => (
<PiListNumbersDuotone className={cn('h-5 w-5', className)} />
);
export const QuoteIcon = ({ className }: IconProps) => (
<PiQuotesDuotone className={cn('h-5 w-5', className)} />
);
export const CodeBlockIcon = ({ className }: IconProps) => (
<PiCodeBlockDuotone className={cn('h-5 w-5', className)} />
);
export const AlignLeftIcon = ({ className }: IconProps) => (
<PiTextAlignLeftDuotone className={cn('h-5 w-5', className)} />
);
export const AlignCenterIcon = ({ className }: IconProps) => (
<PiTextAlignCenterDuotone className={cn('h-5 w-5', className)} />
);
export const HighlightIcon = ({ className }: IconProps) => (
<PiHighlighterDuotone className={cn('h-5 w-5', className)} />
);

View File

@@ -0,0 +1,415 @@
import {
PiSlidersHorizontalDuotone,
PiHeadsetDuotone,
PiArrowDownDuotone,
PiArrowRightDuotone,
PiArrowUpDuotone,
PiProhibitDuotone,
PiMoneyDuotone,
PiBellDuotone,
PiLightningDuotone,
PiRobotDuotone,
PiBroadcastDuotone,
PiAppWindowDuotone,
PiCalendarDotsDuotone,
PiCreditCardDuotone,
PiChartBarDuotone,
PiCheckCircleDuotone,
PiCaretUpDownDuotone,
PiCaretDownDuotone,
PiCaretLeftDuotone,
PiCaretUpDuotone,
PiCurrencyBtcDuotone,
PiDevicesDuotone,
PiFileTextDuotone,
PiCircleFill,
PiEnvelopeDuotone,
PiWarningDuotone,
PiArrowSquareOutDuotone,
PiEyeDuotone,
PiFunnelDuotone,
PiDotsSixDuotone,
PiDotsSixVerticalDuotone,
PiHeartbeatDuotone,
PiClockCounterClockwiseDuotone,
PiImageDuotone,
PiInfinityDuotone,
PiLinkDuotone,
PiMegaphoneDuotone,
PiMinusDuotone,
PiNewspaperDuotone,
PiHandshakeDuotone,
PiPushPinDuotone,
PiPlusDuotone,
PiPowerDuotone,
PiQuestionDuotone,
PiRepeatDuotone,
PiFlagDuotone,
PiArrowCounterClockwiseDuotone,
PiArrowClockwiseDuotone,
PiRocketDuotone,
PiFloppyDiskDuotone,
PiPaperPlaneTiltDuotone,
PiPercentDuotone,
PiHardDrivesDuotone,
PiGearSixDuotone,
PiShareNetworkDuotone,
PiSparkleDuotone,
PiChartLineDuotone,
PiGiftDuotone,
PiTimerDuotone,
PiCircleDuotone,
PiTagDuotone,
PiTelegramLogoDuotone,
PiTicketDuotone,
PiGaugeDuotone,
PiTrashDuotone,
PiTrophyDuotone,
PiPushPinSlashDuotone,
PiUserPlusDuotone,
PiUsersThreeDuotone,
PiVideoCameraDuotone,
PiXCircleDuotone,
PiXDuotone,
} from 'react-icons/pi';
import { cn } from '@/lib/utils';
interface IconProps {
className?: string;
}
/**
* Extended cabinet icon set — Phosphor (react-icons/pi) Duotone, the panel's
* icon family. These cover the icons that used to be hand-written inline across
* feature pages and components. Names match the historical local definitions so
* every page can import from the central barrel instead of redefining SVGs.
*/
export const AdjustmentsIcon = ({ className }: IconProps) => (
<PiSlidersHorizontalDuotone className={cn('h-5 w-5', className)} />
);
export const AgentIcon = ({ className }: IconProps) => (
<PiHeadsetDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowDownIcon = ({ className }: IconProps) => (
<PiArrowDownDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowIcon = ({ className }: IconProps) => (
<PiArrowRightDuotone className={cn('h-5 w-5', className)} />
);
export const ArrowUpIcon = ({ className }: IconProps) => (
<PiArrowUpDuotone className={cn('h-5 w-5', className)} />
);
export const BanIcon = ({ className }: IconProps) => (
<PiProhibitDuotone className={cn('h-5 w-5', className)} />
);
export const BanknotesIcon = ({ className }: IconProps) => (
<PiMoneyDuotone className={cn('h-5 w-5', className)} />
);
export const BellIcon = ({ className }: IconProps) => (
<PiBellDuotone className={cn('h-5 w-5', className)} />
);
export const BoltIcon = ({ className }: IconProps) => (
<PiLightningDuotone className={cn('h-5 w-5', className)} />
);
export const BotIcon = ({ className }: IconProps) => (
<PiRobotDuotone className={cn('h-5 w-5', className)} />
);
export const BroadcastIcon = ({ className }: IconProps) => (
<PiBroadcastDuotone className={cn('h-5 w-5', className)} />
);
export const CabinetIcon = ({ className }: IconProps) => (
<PiAppWindowDuotone className={cn('h-5 w-5', className)} />
);
export const CalendarIcon = ({ className }: IconProps) => (
<PiCalendarDotsDuotone className={cn('h-5 w-5', className)} />
);
export const CardIcon = ({ className }: IconProps) => (
<PiCreditCardDuotone className={cn('h-5 w-5', className)} />
);
export const ChannelIcon = ({ className }: IconProps) => (
<PiBroadcastDuotone className={cn('h-5 w-5', className)} />
);
export const ChartBarIcon = ({ className }: IconProps) => (
<PiChartBarDuotone className={cn('h-5 w-5', className)} />
);
export const CheckCircleIcon = ({ className }: IconProps) => (
<PiCheckCircleDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronExpandIcon = ({ className }: IconProps) => (
<PiCaretUpDownDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronIcon = ({ className }: IconProps) => (
<PiCaretDownDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronLeftIcon = ({ className }: IconProps) => (
<PiCaretLeftDuotone className={cn('h-5 w-5', className)} />
);
export const ChevronUpIcon = ({ className }: IconProps) => (
<PiCaretUpDuotone className={cn('h-5 w-5', className)} />
);
export const CryptoIcon = ({ className }: IconProps) => (
<PiCurrencyBtcDuotone className={cn('h-5 w-5', className)} />
);
export const DevicesIcon = ({ className }: IconProps) => (
<PiDevicesDuotone className={cn('h-5 w-5', className)} />
);
export const DocumentIcon = ({ className }: IconProps) => (
<PiFileTextDuotone className={cn('h-5 w-5', className)} />
);
export const DotIcon = ({ className }: IconProps) => (
<PiCircleFill className={cn('h-2 w-2', className)} />
);
export const EmailIcon = ({ className }: IconProps) => (
<PiEnvelopeDuotone className={cn('h-5 w-5', className)} />
);
export const ExclamationIcon = ({ className }: IconProps) => (
<PiWarningDuotone className={cn('h-5 w-5', className)} />
);
export const ExternalLinkIcon = ({ className }: IconProps) => (
<PiArrowSquareOutDuotone className={cn('h-5 w-5', className)} />
);
export const EyeIcon = ({ className }: IconProps) => (
<PiEyeDuotone className={cn('h-5 w-5', className)} />
);
export const FileTextIcon = ({ className }: IconProps) => (
<PiFileTextDuotone className={cn('h-5 w-5', className)} />
);
export const FilterIcon = ({ className }: IconProps) => (
<PiFunnelDuotone className={cn('h-5 w-5', className)} />
);
export const GripIcon = ({ className }: IconProps) => (
<PiDotsSixDuotone className={cn('h-5 w-5', className)} />
);
export const GripVerticalIcon = ({ className }: IconProps) => (
<PiDotsSixVerticalDuotone className={cn('h-5 w-5', className)} />
);
export const HealthIcon = ({ className }: IconProps) => (
<PiHeartbeatDuotone className={cn('h-5 w-5', className)} />
);
export const HistoryIcon = ({ className }: IconProps) => (
<PiClockCounterClockwiseDuotone className={cn('h-5 w-5', className)} />
);
export const ImageIcon = ({ className }: IconProps) => (
<PiImageDuotone className={cn('h-5 w-5', className)} />
);
export const InfinityIcon = ({ className }: IconProps) => (
<PiInfinityDuotone className={cn('h-5 w-5', className)} />
);
export const LinkIcon = ({ className }: IconProps) => (
<PiLinkDuotone className={cn('h-5 w-5', className)} />
);
export const MailIcon = ({ className }: IconProps) => (
<PiEnvelopeDuotone className={cn('h-5 w-5', className)} />
);
export const MegaphoneIcon = ({ className }: IconProps) => (
<PiMegaphoneDuotone className={cn('h-5 w-5', className)} />
);
export const MinusIcon = ({ className }: IconProps) => (
<PiMinusDuotone className={cn('h-5 w-5', className)} />
);
export const NewsIcon = ({ className }: IconProps) => (
<PiNewspaperDuotone className={cn('h-5 w-5', className)} />
);
export const PartnerIcon = ({ className }: IconProps) => (
<PiHandshakeDuotone className={cn('h-5 w-5', className)} />
);
export const PhotoIcon = ({ className }: IconProps) => (
<PiImageDuotone className={cn('h-5 w-5', className)} />
);
export const PercentIcon = ({ className }: IconProps) => (
<PiPercentDuotone className={cn('h-5 w-5', className)} />
);
export const PinIcon = ({ className }: IconProps) => (
<PiPushPinDuotone className={cn('h-5 w-5', className)} />
);
export const PlusSmallIcon = ({ className }: IconProps) => (
<PiPlusDuotone className={cn('h-4 w-4', className)} />
);
export const PowerIcon = ({ className }: IconProps) => (
<PiPowerDuotone className={cn('h-5 w-5', className)} />
);
export const QuestionIcon = ({ className }: IconProps) => (
<PiQuestionDuotone className={cn('h-5 w-5', className)} />
);
export const RepeatIcon = ({ className }: IconProps) => (
<PiRepeatDuotone className={cn('h-5 w-5', className)} />
);
export const ReportIcon = ({ className }: IconProps) => (
<PiFlagDuotone className={cn('h-5 w-5', className)} />
);
export const ResetIcon = ({ className }: IconProps) => (
<PiArrowCounterClockwiseDuotone className={cn('h-5 w-5', className)} />
);
export const RestartIcon = ({ className }: IconProps) => (
<PiArrowClockwiseDuotone className={cn('h-5 w-5', className)} />
);
export const RocketIcon = ({ className }: IconProps) => (
<PiRocketDuotone className={cn('h-5 w-5', className)} />
);
export const SaveIcon = ({ className }: IconProps) => (
<PiFloppyDiskDuotone className={cn('h-5 w-5', className)} />
);
export const SendIcon = ({ className }: IconProps) => (
<PiPaperPlaneTiltDuotone className={cn('h-5 w-5', className)} />
);
export const ServerSmallIcon = ({ className }: IconProps) => (
<PiHardDrivesDuotone className={cn('h-4 w-4', className)} />
);
export const SettingsIcon = ({ className }: IconProps) => (
<PiGearSixDuotone className={cn('h-5 w-5', className)} />
);
export const ShareIcon = ({ className }: IconProps) => (
<PiShareNetworkDuotone className={cn('h-5 w-5', className)} />
);
export const SparklesIcon = ({ className }: IconProps) => (
<PiSparkleDuotone className={cn('h-5 w-5', className)} />
);
export const StatBotIcon = ({ className }: IconProps) => (
<PiRobotDuotone className={cn('h-5 w-5', className)} />
);
export const StatCabinetIcon = ({ className }: IconProps) => (
<PiAppWindowDuotone className={cn('h-5 w-5', className)} />
);
export const StatPaidIcon = ({ className }: IconProps) => (
<PiMoneyDuotone className={cn('h-5 w-5', className)} />
);
export const StatsChartIcon = ({ className }: IconProps) => (
<PiChartLineDuotone className={cn('h-5 w-5', className)} />
);
export const StatTrialIcon = ({ className }: IconProps) => (
<PiGiftDuotone className={cn('h-5 w-5', className)} />
);
export const StatUptimeIcon = ({ className }: IconProps) => (
<PiTimerDuotone className={cn('h-5 w-5', className)} />
);
export const StatusIcon = ({ className }: IconProps) => (
<PiCircleDuotone className={cn('h-5 w-5', className)} />
);
export const TagIcon = ({ className }: IconProps) => (
<PiTagDuotone className={cn('h-5 w-5', className)} />
);
export const TelegramIcon = ({ className }: IconProps) => (
<PiTelegramLogoDuotone className={cn('h-5 w-5', className)} />
);
export const TelegramSmallIcon = ({ className }: IconProps) => (
<PiTelegramLogoDuotone className={cn('h-4 w-4', className)} />
);
export const TicketIcon = ({ className }: IconProps) => (
<PiTicketDuotone className={cn('h-5 w-5', className)} />
);
export const TrafficIcon = ({ className }: IconProps) => (
<PiGaugeDuotone className={cn('h-5 w-5', className)} />
);
export const TrashSmallIcon = ({ className }: IconProps) => (
<PiTrashDuotone className={cn('h-4 w-4', className)} />
);
export const TrophyIcon = ({ className }: IconProps) => (
<PiTrophyDuotone className={cn('h-5 w-5', className)} />
);
export const UnpinIcon = ({ className }: IconProps) => (
<PiPushPinSlashDuotone className={cn('h-5 w-5', className)} />
);
export const UserPlusIcon = ({ className }: IconProps) => (
<PiUserPlusDuotone className={cn('h-5 w-5', className)} />
);
export const UsersOnlineIcon = ({ className }: IconProps) => (
<PiUsersThreeDuotone className={cn('h-5 w-5', className)} />
);
export const VideoIcon = ({ className }: IconProps) => (
<PiVideoCameraDuotone className={cn('h-5 w-5', className)} />
);
export const WarningIcon = ({ className }: IconProps) => (
<PiWarningDuotone className={cn('h-5 w-5', className)} />
);
export const XCircleIcon = ({ className }: IconProps) => (
<PiXCircleDuotone className={cn('h-5 w-5', className)} />
);
export const XCloseIcon = ({ className }: IconProps) => (
<PiXDuotone className={cn('h-5 w-5', className)} />
);
export const XMarkIcon = ({ className }: IconProps) => (
<PiXDuotone className={cn('h-5 w-5', className)} />
);

View File

@@ -1,611 +1,248 @@
import type { CSSProperties } from 'react';
import {
PiArrowLeftDuotone,
PiArrowRightDuotone,
PiArrowsClockwiseDuotone,
PiCaretDownDuotone,
PiCaretRightDuotone,
PiChartBarDuotone,
PiChatCircleDuotone,
PiCheckDuotone,
PiClipboardTextDuotone,
PiClockDuotone,
PiCopyDuotone,
PiCreditCardDuotone,
PiDownloadSimpleDuotone,
PiGameControllerDuotone,
PiGearSixDuotone,
PiGiftDuotone,
PiGlobeDuotone,
PiHardDrivesDuotone,
PiHouseDuotone,
PiInfoDuotone,
PiListDuotone,
PiLockDuotone,
PiMagnifyingGlassDuotone,
PiMegaphoneDuotone,
PiMoonDuotone,
PiPaletteDuotone,
PiPauseCircleDuotone,
PiPencilDuotone,
PiPencilSimpleDuotone,
PiPlayDuotone,
PiPlusDuotone,
PiShieldDuotone,
PiSignOutDuotone,
PiSparkleDuotone,
PiStarDuotone,
PiStarFill,
PiSteeringWheelDuotone,
PiStopDuotone,
PiSunDuotone,
PiTrashDuotone,
PiUploadSimpleDuotone,
PiUserDuotone,
PiUsersDuotone,
PiWalletDuotone,
PiXDuotone,
} from 'react-icons/pi';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
// Re-export the extended Phosphor icon sets so the whole cabinet imports the
// panel's icon family from a single barrel.
export * from './extended-icons';
export * from './editor-icons';
interface IconProps { interface IconProps {
className?: string; className?: string;
} }
/**
* Cabinet icons are thin wrappers over the Remnawave panel's own icon library
* (Phosphor, via `react-icons/pi`, Duotone variants). Each export keeps the
* historical name + default Tailwind sizing so every consumer keeps working,
* while the cabinet now renders the exact icon set the panel uses instead of
* hand-written SVGs.
*
* react-icons components inherit `currentColor` and accept `className`, so
* Tailwind size (`h-5 w-5`) and text-color utilities control them as before.
*/
// Navigation & Layout // Navigation & Layout
export const HomeIcon = ({ className }: IconProps) => ( export const HomeIcon = ({ className }: IconProps) => (
<svg <PiHouseDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
); );
export const BackIcon = ({ className }: IconProps) => ( export const BackIcon = ({ className }: IconProps) => (
<svg <PiArrowLeftDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const ChevronRightIcon = ({ className }: IconProps) => ( export const ChevronRightIcon = ({ className }: IconProps) => (
<svg <PiCaretRightDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const MenuIcon = ({ className }: IconProps) => ( export const MenuIcon = ({ className }: IconProps) => (
<svg <PiListDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
); );
export const CloseIcon = ({ className }: IconProps) => ( export const CloseIcon = ({ className }: IconProps) => (
<svg <PiXDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
); );
export const ChevronDownIcon = ({ className }: IconProps) => ( export const ChevronDownIcon = ({ className }: IconProps) => (
<svg <PiCaretDownDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const ArrowRightIcon = ({ className }: IconProps) => ( export const ArrowRightIcon = ({ className }: IconProps) => (
<svg <PiArrowRightDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
); );
// Actions // Actions
export const SearchIcon = ({ className }: IconProps) => ( export const SearchIcon = ({ className }: IconProps) => (
<svg <PiMagnifyingGlassDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const PlusIcon = ({ className }: IconProps) => ( export const PlusIcon = ({ className }: IconProps) => (
<svg <PiPlusDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
); );
export const EditIcon = ({ className }: IconProps) => ( export const EditIcon = ({ className }: IconProps) => (
<svg <PiPencilSimpleDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
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.125"
/>
</svg>
); );
export const PencilIcon = ({ className }: IconProps) => ( export const PencilIcon = ({ className }: IconProps) => (
<svg <PiPencilDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
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>
); );
export const TrashIcon = ({ className }: IconProps) => ( export const TrashIcon = ({ className }: IconProps) => (
<svg <PiTrashDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const UploadIcon = ({ className }: IconProps) => ( export const UploadIcon = ({ className }: IconProps) => (
<svg <PiUploadSimpleDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<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>
); );
export const DownloadIcon = ({ className }: IconProps) => ( export const DownloadIcon = ({ className }: IconProps) => (
<svg <PiDownloadSimpleDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const RefreshIcon = ({ export const RefreshIcon = ({
className, className,
spinning = false, spinning = false,
}: IconProps & { spinning?: boolean }) => ( }: IconProps & { spinning?: boolean }) => (
<svg <PiArrowsClockwiseDuotone className={cn('h-4 w-4', spinning && 'animate-spin', className)} />
className={cn('h-4 w-4', spinning && 'animate-spin', className)}
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>
); );
export const SyncIcon = ({ className }: IconProps) => ( export const SyncIcon = ({ className }: IconProps) => (
<svg <PiArrowsClockwiseDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
// Status // Status
export const CheckIcon = ({ className }: IconProps) => ( export const CheckIcon = ({ className }: IconProps) => (
<svg <PiCheckDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
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>
); );
export const CopyIcon = ({ className }: IconProps) => ( export const CopyIcon = ({ className }: IconProps) => (
<svg <PiCopyDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
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.75H9a.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>
); );
export const XIcon = ({ className }: IconProps) => ( export const XIcon = ({ className }: IconProps) => (
<svg <PiXDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
); );
export const LockIcon = ({ className }: IconProps) => ( export const LockIcon = ({ className }: IconProps) => (
<svg <PiLockDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
/>
</svg>
); );
export const InfoIcon = ({ className }: IconProps) => ( export const InfoIcon = ({ className }: IconProps) => (
<svg <PiInfoDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
); );
// User & People // User & People
export const UserIcon = ({ className }: IconProps) => ( export const UserIcon = ({ className }: IconProps) => (
<svg <PiUserDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
); );
export const UsersIcon = ({ className }: IconProps) => ( export const UsersIcon = ({ className }: IconProps) => (
<svg <PiUsersDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const LogoutIcon = ({ className }: IconProps) => ( export const LogoutIcon = ({ className }: IconProps) => (
<svg <PiSignOutDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9"
/>
</svg>
); );
// Theme // Theme
export const SunIcon = ({ className }: IconProps) => ( export const SunIcon = ({ className }: IconProps) => (
<svg <PiSunDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
); );
export const MoonIcon = ({ className }: IconProps) => ( export const MoonIcon = ({ className }: IconProps) => (
<svg <PiMoonDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
); );
export const PaletteIcon = ({ className }: IconProps) => ( export const PaletteIcon = ({ className }: IconProps) => (
<svg <PiPaletteDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"
/>
</svg>
); );
// Features & Content // Features & Content
export const SubscriptionIcon = ({ className }: IconProps) => ( export const SubscriptionIcon = ({ className }: IconProps) => (
<svg <PiSparkleDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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.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.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 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"
/>
</svg>
); );
export const WalletIcon = ({ className }: IconProps) => ( export const WalletIcon = ({ className }: IconProps) => (
<svg <PiWalletDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"
/>
</svg>
); );
export const ChatIcon = ({ className }: IconProps) => ( export const ChatIcon = ({ className }: IconProps) => (
<svg <PiChatCircleDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const GiftIcon = ({ className }: IconProps) => ( export const GiftIcon = ({ className }: IconProps) => (
<svg <PiGiftDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
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>
); );
export const ClockIcon = ({ className }: IconProps) => ( export const ClockIcon = ({ className }: IconProps) => (
<svg <PiClockDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
); );
export const StarIcon = ({ className, filled }: IconProps & { filled?: boolean }) => ( export const StarIcon = ({ className, filled }: IconProps & { filled?: boolean }) =>
<svg filled ? (
className={cn('h-5 w-5', className)} <PiStarFill className={cn('h-5 w-5', className)} />
fill={filled ? 'currentColor' : 'none'} ) : (
viewBox="0 0 24 24" <PiStarDuotone className={cn('h-5 w-5', className)} />
stroke="currentColor" );
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
);
export const GamepadIcon = ({ className }: IconProps) => ( export const GamepadIcon = ({ className }: IconProps) => (
<svg <PiGameControllerDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.959.401v0a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.78.78 0 01.79-.869"
/>
</svg>
); );
export const ClipboardIcon = ({ className }: IconProps) => ( export const ClipboardIcon = ({ className }: IconProps) => (
<svg <PiClipboardTextDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
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.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
/>
</svg>
); );
export const CogIcon = ({ className }: IconProps) => ( export const CogIcon = ({ className }: IconProps) => (
<svg <PiGearSixDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
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>
); );
export const WheelIcon = ({ className }: IconProps) => ( export const WheelIcon = ({ className }: IconProps) => (
<svg <PiSteeringWheelDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)} );
fill="none"
viewBox="0 0 24 24" export const ShieldIcon = ({ className }: IconProps) => (
stroke="currentColor" <PiShieldDuotone className={cn('h-5 w-5', className)} />
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
); );
export const ServerIcon = ({ className }: IconProps) => ( export const ServerIcon = ({ className }: IconProps) => (
<svg <PiHardDrivesDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 00-.12-1.03l-2.268-9.64a3.375 3.375 0 00-3.285-2.602H7.923a3.375 3.375 0 00-3.285 2.602l-2.268 9.64a4.5 4.5 0 00-.12 1.03v.228m19.5 0a3 3 0 01-3 3H5.25a3 3 0 01-3-3m19.5 0a3 3 0 00-3-3H5.25a3 3 0 00-3 3m16.5 0h.008v.008h-.008v-.008zm-3 0h.008v.008h-.008v-.008z"
/>
</svg>
); );
export const CampaignIcon = ({ className }: IconProps) => ( export const CampaignIcon = ({ className }: IconProps) => (
<svg <PiMegaphoneDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
/>
</svg>
); );
// Brand mark — the genuine Remnawave panel logo (kept as-is, this is the
// panel's own SVG, not a hand-drawn substitute).
export const RemnawaveIcon = ({ className }: IconProps) => ( export const RemnawaveIcon = ({ className }: IconProps) => (
<svg <svg
className={cn('h-5 w-5', className)} className={cn('h-5 w-5', className)}
@@ -623,114 +260,29 @@ export const RemnawaveIcon = ({ className }: IconProps) => (
); );
export const ChartIcon = ({ className }: IconProps) => ( export const ChartIcon = ({ className }: IconProps) => (
<svg <PiChartBarDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
); );
export const GlobeIcon = ({ className }: IconProps) => ( export const GlobeIcon = ({ className }: IconProps) => (
<svg <PiGlobeDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
); );
export const PlayIcon = ({ className }: IconProps) => ( export const PlayIcon = ({ className }: IconProps) => (
<svg <PiPlayDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"
/>
</svg>
); );
export const StopIcon = ({ className }: IconProps) => ( export const StopIcon = ({ className }: IconProps) => (
<svg <PiStopDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 7.5A2.25 2.25 0 017.5 5.25h9a2.25 2.25 0 012.25 2.25v9a2.25 2.25 0 01-2.25 2.25h-9a2.25 2.25 0 01-2.25-2.25v-9z"
/>
</svg>
); );
export const ArrowPathIcon = ({ className }: IconProps) => ( export const ArrowPathIcon = ({ className }: IconProps) => (
<svg <PiArrowsClockwiseDuotone className={cn('h-4 w-4', className)} />
className={cn('h-4 w-4', className)}
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>
); );
export const PauseIcon = ({ className, style }: IconProps & { style?: React.CSSProperties }) => ( export const PauseIcon = ({ className, style }: IconProps & { style?: CSSProperties }) => (
<svg <PiPauseCircleDuotone className={cn('h-5 w-5', className)} style={style} />
className={cn('h-5 w-5', className)}
style={style}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
); );
export const CreditCardIcon = ({ className }: IconProps) => ( export const CreditCardIcon = ({ className }: IconProps) => (
<svg <PiCreditCardDuotone className={cn('h-5 w-5', className)} />
className={cn('h-5 w-5', className)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
/>
</svg>
); );

View File

@@ -21,173 +21,25 @@ import SuccessNotificationModal from '@/components/SuccessNotificationModal';
import { PromptDialogHost } from '@/components/PromptDialogHost'; import { PromptDialogHost } from '@/components/PromptDialogHost';
import LanguageSwitcher from '@/components/LanguageSwitcher'; import LanguageSwitcher from '@/components/LanguageSwitcher';
import TicketNotificationBell from '@/components/TicketNotificationBell'; import TicketNotificationBell from '@/components/TicketNotificationBell';
import { SubscriptionIcon, GiftIcon } from '@/components/icons'; import {
SubscriptionIcon,
GiftIcon,
HomeIcon,
CreditCardIcon,
ChatIcon,
UserIcon,
UsersIcon,
ShieldIcon,
InfoIcon,
LogoutIcon,
SunIcon,
MoonIcon,
} from '@/components/icons';
import { MobileBottomNav } from './MobileBottomNav'; import { MobileBottomNav } from './MobileBottomNav';
import { AppHeader } from './AppHeader'; import { AppHeader } from './AppHeader';
import { BackgroundRenderer } from '@/components/backgrounds/BackgroundRenderer'; import { BackgroundRenderer } from '@/components/backgrounds/BackgroundRenderer';
// Desktop nav icons
const HomeIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
);
const CreditCardIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
/>
</svg>
);
const ChatIcon = ({ className }: { className?: string }) => (
<svg
className={className}
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>
);
const UserIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
);
const UsersIcon = ({ className }: { className?: string }) => (
<svg
className={className}
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>
);
const ShieldIcon = ({ className }: { className?: string }) => (
<svg
className={className}
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.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
const InfoIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
);
const LogoutIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75"
/>
</svg>
);
const SunIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
);
const MoonIcon = ({ className }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
);
interface AppShellProps { interface AppShellProps {
children: React.ReactNode; children: React.ReactNode;
} }

View File

@@ -6,6 +6,7 @@ import { motion } from 'framer-motion';
import { newsApi } from '../../api/news'; import { newsApi } from '../../api/news';
import { useHapticFeedback } from '../../platform/hooks/useHaptic'; import { useHapticFeedback } from '../../platform/hooks/useHaptic';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { ArrowIcon } from '@/components/icons';
import type { NewsListItem } from '../../types/news'; import type { NewsListItem } from '../../types/news';
// --- Security: hex color validation to prevent CSS injection --- // --- Security: hex color validation to prevent CSS injection ---
@@ -31,19 +32,6 @@ const fadeSlideUp = {
}), }),
}; };
// --- Icons ---
const ArrowIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M3 8h10M9 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// --- Sub-components --- // --- Sub-components ---
interface CategoryBadgeProps { interface CategoryBadgeProps {

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { CheckIcon, CopyIcon } from '@/components/icons';
import type { PartnerCampaignInfo } from '../../api/partners'; import type { PartnerCampaignInfo } from '../../api/partners';
import { PARTNER_STATS } from '../../constants/partner'; import { PARTNER_STATS } from '../../constants/partner';
import { useCurrency } from '../../hooks/useCurrency'; import { useCurrency } from '../../hooks/useCurrency';
@@ -9,32 +11,8 @@ import { copyToClipboard } from '../../utils/clipboard';
import { CampaignDetailStats } from './CampaignDetailStats'; import { CampaignDetailStats } from './CampaignDetailStats';
import { StatCard } from '../stats/StatCard'; import { StatCard } from '../stats/StatCard';
const CopyIcon = () => (
<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.75H9a.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>
);
const CheckIcon = () => (
<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>
);
const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg <PiCaretDownDuotone className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`} />
className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
); );
interface CampaignCardProps { interface CampaignCardProps {

View File

@@ -1,20 +1,8 @@
import { Command as CommandPrimitive } from 'cmdk'; import { Command as CommandPrimitive } from 'cmdk';
import { forwardRef, type ComponentPropsWithoutRef, type HTMLAttributes } from 'react'; import { forwardRef, type ComponentPropsWithoutRef, type HTMLAttributes } from 'react';
import { SearchIcon } from '@/components/icons';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
// Search icon
const SearchIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-dark-400">
<path
d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667ZM14 14l-2.9-2.9"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Root Command // Root Command
export type CommandProps = ComponentPropsWithoutRef<typeof CommandPrimitive>; export type CommandProps = ComponentPropsWithoutRef<typeof CommandPrimitive>;

View File

@@ -8,6 +8,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CloseIcon } from '@/components/icons';
import { backdrop, backdropTransition, scale, scaleTransition } from '../../motion/transitions'; import { backdrop, backdropTransition, scale, scaleTransition } from '../../motion/transitions';
export { export {
@@ -16,19 +17,6 @@ export {
Close as DialogClose, Close as DialogClose,
} from '@radix-ui/react-dialog'; } from '@radix-ui/react-dialog';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Context for AnimatePresence // Context for AnimatePresence
const DialogContext = createContext<{ open: boolean }>({ open: false }); const DialogContext = createContext<{ open: boolean }>({ open: false });

View File

@@ -1,6 +1,7 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react'; import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { CheckIcon, ChevronRightIcon, DotIcon } from '@/components/icons';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
import { dropdown, dropdownTransition } from '../../motion/transitions'; import { dropdown, dropdownTransition } from '../../motion/transitions';
@@ -14,37 +15,6 @@ export {
RadioGroup as DropdownMenuRadioGroup, RadioGroup as DropdownMenuRadioGroup,
} from '@radix-ui/react-dropdown-menu'; } from '@radix-ui/react-dropdown-menu';
// Icons
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M3.5 8.5L6.5 11.5L12.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const ChevronRightIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M6 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const DotIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="3" fill="currentColor" />
</svg>
);
// SubTrigger // SubTrigger
export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef< export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.SubTrigger typeof DropdownMenuPrimitive.SubTrigger

View File

@@ -1,6 +1,7 @@
import * as PopoverPrimitive from '@radix-ui/react-popover'; import * as PopoverPrimitive from '@radix-ui/react-popover';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react'; import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { CloseIcon } from '@/components/icons';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { dropdown, dropdownTransition } from '../../motion/transitions'; import { dropdown, dropdownTransition } from '../../motion/transitions';
@@ -11,19 +12,6 @@ export {
Close as PopoverClose, Close as PopoverClose,
} from '@radix-ui/react-popover'; } from '@radix-ui/react-popover';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Content // Content
export interface PopoverContentProps extends ComponentPropsWithoutRef< export interface PopoverContentProps extends ComponentPropsWithoutRef<
typeof PopoverPrimitive.Content typeof PopoverPrimitive.Content

View File

@@ -2,36 +2,12 @@ import * as SelectPrimitive from '@radix-ui/react-select';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react'; import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckIcon, ChevronDownIcon } from '@/components/icons';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
import { dropdown, dropdownTransition } from '../../motion/transitions'; import { dropdown, dropdownTransition } from '../../motion/transitions';
export { Root as Select, Group as SelectGroup } from '@radix-ui/react-select'; export { Root as Select, Group as SelectGroup } from '@radix-ui/react-select';
// Icons
const ChevronDownIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-dark-400">
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-accent-400">
<path
d="M3.5 8.5L6.5 11.5L12.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Trigger // Trigger
export interface SelectTriggerProps extends ComponentPropsWithoutRef< export interface SelectTriggerProps extends ComponentPropsWithoutRef<
typeof SelectPrimitive.Trigger typeof SelectPrimitive.Trigger

View File

@@ -8,6 +8,7 @@ import {
useState, useState,
useCallback, useCallback,
} from 'react'; } from 'react';
import { CloseIcon } from '@/components/icons';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
import { import {
@@ -23,19 +24,6 @@ export {
Close as SheetClose, Close as SheetClose,
} from '@radix-ui/react-dialog'; } from '@radix-ui/react-dialog';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Context for AnimatePresence and control // Context for AnimatePresence and control
interface SheetContextValue { interface SheetContextValue {
open: boolean; open: boolean;

View File

@@ -6,78 +6,14 @@ import type { TFunction } from 'i18next';
import { rbacApi, AuditLogEntry, AuditLogFilters } from '@/api/rbac'; import { rbacApi, AuditLogEntry, AuditLogFilters } from '@/api/rbac';
import { PermissionGate } from '@/components/auth/PermissionGate'; import { PermissionGate } from '@/components/auth/PermissionGate';
import { usePlatform } from '@/platform/hooks/usePlatform'; import { usePlatform } from '@/platform/hooks/usePlatform';
import {
// === Icons === BackIcon,
ChevronDownIcon,
const BackIcon = () => ( DownloadIcon,
<svg FilterIcon,
className="h-5 w-5 text-dark-400" RefreshIcon,
fill="none" SearchIcon,
viewBox="0 0 24 24" } from '@/components/icons';
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);
const DownloadIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const FilterIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
);
const ChevronDownIcon = ({ className }: { className?: string }) => (
<svg
className={className || 'h-4 w-4'}
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>
);
const RefreshIcon = ({ className }: { className?: string }) => (
<svg
className={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.182"
/>
</svg>
);
const SearchIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
// === Constants === // === Constants ===

View File

@@ -20,137 +20,21 @@ import {
type BanHealthResponse, type BanHealthResponse,
} from '../api/banSystem'; } from '../api/banSystem';
// Icons import {
const ShieldIcon = () => ( ShieldIcon,
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> UsersIcon,
<path BanIcon,
strokeLinecap="round" ServerIcon,
strokeLinejoin="round" AgentIcon,
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" WarningIcon,
/> RefreshIcon,
</svg> ChartIcon,
); SearchIcon,
SettingsIcon,
const UsersIcon = () => ( TrafficIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> ReportIcon,
<path HealthIcon,
strokeLinecap="round" } from '@/components/icons';
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>
);
const BanIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
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>
);
const ServerIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<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>
);
const AgentIcon = () => (
<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.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const WarningIcon = () => (
<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 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>
);
const RefreshIcon = () => (
<svg className="h-5 w-5" 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>
);
const ChartIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
);
const SearchIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const SettingsIcon = () => (
<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>
);
const TrafficIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5"
/>
</svg>
);
const ReportIcon = () => (
<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 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.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
/>
</svg>
);
const HealthIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"
/>
</svg>
);
type TabType = type TabType =
| 'dashboard' | 'dashboard'
@@ -475,7 +359,11 @@ export default function AdminBanSystem() {
}; };
const tabs = [ const tabs = [
{ id: 'dashboard' as TabType, label: t('banSystem.tabs.dashboard'), icon: <ChartIcon /> }, {
id: 'dashboard' as TabType,
label: t('banSystem.tabs.dashboard'),
icon: <ChartIcon className="h-5 w-5" />,
},
{ id: 'users' as TabType, label: t('banSystem.tabs.users'), icon: <UsersIcon /> }, { id: 'users' as TabType, label: t('banSystem.tabs.users'), icon: <UsersIcon /> },
{ id: 'punishments' as TabType, label: t('banSystem.tabs.punishments'), icon: <BanIcon /> }, { id: 'punishments' as TabType, label: t('banSystem.tabs.punishments'), icon: <BanIcon /> },
{ id: 'nodes' as TabType, label: t('banSystem.tabs.nodes'), icon: <ServerIcon /> }, { id: 'nodes' as TabType, label: t('banSystem.tabs.nodes'), icon: <ServerIcon /> },
@@ -601,7 +489,7 @@ export default function AdminBanSystem() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AdminBackButton /> <AdminBackButton />
<div className="rounded-xl bg-error-500/20 p-3"> <div className="rounded-xl bg-error-500/20 p-3">
<ShieldIcon /> <ShieldIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-dark-100">{t('banSystem.title')}</h1> <h1 className="text-2xl font-bold text-dark-100">{t('banSystem.title')}</h1>
@@ -613,7 +501,7 @@ export default function AdminBanSystem() {
disabled={loading} disabled={loading}
className="flex items-center gap-2 rounded-lg bg-dark-800 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50" className="flex items-center gap-2 rounded-lg bg-dark-800 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50"
> >
<RefreshIcon /> <RefreshIcon className="h-5 w-5" />
{t('common.refresh')} {t('common.refresh')}
</button> </button>
</div> </div>
@@ -683,7 +571,7 @@ export default function AdminBanSystem() {
<StatCard <StatCard
title={t('banSystem.stats.totalRequests')} title={t('banSystem.stats.totalRequests')}
value={stats.total_requests.toLocaleString()} value={stats.total_requests.toLocaleString()}
icon={<ChartIcon />} icon={<ChartIcon className="h-5 w-5" />}
color="accent" color="accent"
/> />
<StatCard <StatCard
@@ -699,7 +587,7 @@ export default function AdminBanSystem() {
<StatCard <StatCard
title={t('banSystem.stats.uptime')} title={t('banSystem.stats.uptime')}
value={formatUptime(stats.uptime_seconds)} value={formatUptime(stats.uptime_seconds)}
icon={<ChartIcon />} icon={<ChartIcon className="h-5 w-5" />}
color="info" color="info"
/> />
</div> </div>
@@ -930,7 +818,7 @@ export default function AdminBanSystem() {
<StatCard <StatCard
title={t('banSystem.agents.totalSent')} title={t('banSystem.agents.totalSent')}
value={agents.summary.total_sent.toLocaleString()} value={agents.summary.total_sent.toLocaleString()}
icon={<ChartIcon />} icon={<ChartIcon className="h-5 w-5" />}
color="accent" color="accent"
/> />
<StatCard <StatCard

View File

@@ -11,95 +11,18 @@ import {
} from '../api/adminBroadcasts'; } from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { TelegramPreview, EmailPreview } from '../components/broadcasts/BroadcastPreview'; import { TelegramPreview, EmailPreview } from '../components/broadcasts/BroadcastPreview';
import {
// Icons BroadcastIcon,
const BroadcastIcon = () => ( ChevronDownIcon,
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> DocumentIcon,
<path EmailIcon,
strokeLinecap="round" PhotoIcon,
strokeLinejoin="round" RefreshIcon,
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" TelegramIcon,
/> UsersIcon,
</svg> VideoIcon,
); XIcon,
} from '@/components/icons';
const XIcon = () => (
<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>
);
const RefreshIcon = () => (
<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>
);
const PhotoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const VideoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
/>
</svg>
);
const DocumentIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-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 00-9-9z"
/>
</svg>
);
const UsersIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const ChevronDownIcon = () => (
<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 8.25l-7.5 7.5-7.5-7.5" />
</svg>
);
const TelegramIcon = () => (
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<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>
);
const EmailIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
// Filter labels // Filter labels
const FILTER_GROUP_LABEL_KEYS: Record<string, string> = { const FILTER_GROUP_LABEL_KEYS: Record<string, string> = {
@@ -541,7 +464,7 @@ export default function AdminBroadcastCreate() {
</span> </span>
)} )}
</div> </div>
<ChevronDownIcon /> <ChevronDownIcon className="h-4 w-4" />
</button> </button>
{showFilters && ( {showFilters && (
@@ -584,7 +507,7 @@ export default function AdminBroadcastCreate() {
<AdminBackButton /> <AdminBackButton />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<BroadcastIcon /> <BroadcastIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.create')}</h1> <h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.create')}</h1>
@@ -730,7 +653,7 @@ export default function AdminBroadcastCreate() {
className="rounded-lg p-2 text-dark-400 hover:bg-dark-700 hover:text-error-400" className="rounded-lg p-2 text-dark-400 hover:bg-dark-700 hover:text-error-400"
disabled={isUploading} disabled={isUploading}
> >
<XIcon /> <XIcon className="h-5 w-5" />
</button> </button>
</div> </div>
{mediaPreview && ( {mediaPreview && (
@@ -816,7 +739,7 @@ export default function AdminBroadcastCreate() {
onClick={() => removeCustomButton(index)} onClick={() => removeCustomButton(index)}
className="ml-2 shrink-0 rounded p-1 text-dark-400 hover:bg-dark-700 hover:text-error-400" className="ml-2 shrink-0 rounded p-1 text-dark-400 hover:bg-dark-700 hover:text-error-400"
> >
<XIcon /> <XIcon className="h-5 w-5" />
</button> </button>
</div> </div>
))} ))}
@@ -1016,7 +939,7 @@ export default function AdminBroadcastCreate() {
disabled={!isValid || isPending || isUploading} disabled={!isValid || isPending || isUploading}
className="btn-primary flex items-center gap-2" className="btn-primary flex items-center gap-2"
> >
{isPending ? <RefreshIcon /> : <BroadcastIcon />} {isPending ? <RefreshIcon /> : <BroadcastIcon className="h-6 w-6" />}
{t('admin.broadcasts.send')} {t('admin.broadcasts.send')}
</button> </button>
</div> </div>

View File

@@ -3,74 +3,15 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts'; import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import {
// Icons DocumentIcon,
EmailIcon,
const StopIcon = () => ( PhotoIcon,
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> RefreshIcon,
<path StopIcon,
strokeLinecap="round" TelegramIcon,
strokeLinejoin="round" VideoIcon,
d="M5.25 7.5A2.25 2.25 0 017.5 5.25h9a2.25 2.25 0 012.25 2.25v9a2.25 2.25 0 01-2.25 2.25h-9a2.25 2.25 0 01-2.25-2.25v-9z" } from '@/components/icons';
/>
</svg>
);
const PhotoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const VideoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
/>
</svg>
);
const DocumentIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-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 00-9-9z"
/>
</svg>
);
const RefreshIcon = () => (
<svg className="h-5 w-5" 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>
);
const TelegramIcon = () => (
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<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>
);
const EmailIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
// Channel badge component // Channel badge component
function ChannelBadge({ channel }: { channel?: BroadcastChannel }) { function ChannelBadge({ channel }: { channel?: BroadcastChannel }) {
@@ -240,7 +181,7 @@ export default function AdminBroadcastDetail() {
onClick={() => refetch()} onClick={() => refetch()}
className="rounded-lg p-2 transition-colors hover:bg-dark-700" className="rounded-lg p-2 transition-colors hover:bg-dark-700"
> >
<RefreshIcon /> <RefreshIcon className="h-5 w-5" />
</button> </button>
</div> </div>

View File

@@ -4,76 +4,15 @@ import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { adminBroadcastsApi } from '../api/adminBroadcasts'; import { adminBroadcastsApi } from '../api/adminBroadcasts';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import {
// Icons BackIcon,
BroadcastIcon,
const BackIcon = () => ( DocumentIcon,
<svg PhotoIcon,
className="h-5 w-5 text-dark-400" PlusIcon,
fill="none" RefreshIcon,
viewBox="0 0 24 24" VideoIcon,
stroke="currentColor" } from '@/components/icons';
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);
const BroadcastIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
/>
</svg>
);
const PlusIcon = () => (
<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>
);
const RefreshIcon = () => (
<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>
);
const PhotoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const VideoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
/>
</svg>
);
const DocumentIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-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 00-9-9z"
/>
</svg>
);
// Status badge component // Status badge component
const statusConfig: Record<string, { bg: string; text: string; labelKey: string }> = { const statusConfig: Record<string, { bg: string; text: string; labelKey: string }> = {
@@ -194,7 +133,7 @@ export default function AdminBroadcasts() {
</div> </div>
) : broadcasts.length === 0 ? ( ) : broadcasts.length === 0 ? (
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400"> <div className="rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400">
<BroadcastIcon /> <BroadcastIcon className="h-6 w-6" />
<p className="mt-2">{t('admin.broadcasts.empty')}</p> <p className="mt-2">{t('admin.broadcasts.empty')}</p>
</div> </div>
) : ( ) : (

View File

@@ -20,9 +20,18 @@ import {
type BulkActionType, type BulkActionType,
type BulkActionParams, type BulkActionParams,
} from '../api/adminBulkActions'; } from '../api/adminBulkActions';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import {
BackIcon,
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
RefreshIcon,
SearchIcon,
} from '@/components/icons';
import { ActionModal, type ModalState } from '@/components/admin/bulkActions/ActionModal'; import { ActionModal, type ModalState } from '@/components/admin/bulkActions/ActionModal';
import { DropdownSelect, type DropdownOption } from '@/components/admin/bulkActions/DropdownSelect'; import { DropdownSelect, type DropdownOption } from '@/components/admin/bulkActions/DropdownSelect';
import { FloatingActionBar } from '@/components/admin/bulkActions/FloatingActionBar'; import { FloatingActionBar } from '@/components/admin/bulkActions/FloatingActionBar';
@@ -41,77 +50,15 @@ type SubscriptionStatusFilter = '' | 'active' | 'expired' | 'trial' | 'limited'
// (ProgressState + ModalState moved into <ActionModal>; ModalState is re-exported.) // (ProgressState + ModalState moved into <ActionModal>; ModalState is re-exported.)
// Local CheckIcon — still used by MultiSelectDropdown checkboxes and
// the user-table column defs (not the modal). Kept here rather than
// re-exported from a shared icons module.
const CheckIcon = () => (
<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>
);
// ============ Icons ============ // ============ Icons ============
const BackIcon = () => ( // ChevronExpandIcon keeps a custom `expanded` prop (not the barrel's
<svg // className-only API), so it stays local as a thin wrapper over the panel's
className="h-5 w-5 text-dark-400" // Phosphor caret, preserving the rotate-on-expand behavior.
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>
);
const SearchIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const ChevronLeftIcon = () => (
<svg className="h-5 w-5" 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>
);
const ChevronRightIcon = () => (
<svg className="h-5 w-5" 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>
);
const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} 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>
);
const ChevronExpandIcon = ({ expanded }: { expanded: boolean }) => ( const ChevronExpandIcon = ({ expanded }: { expanded: boolean }) => (
<svg <PiCaretDownDuotone
className={cn('h-4 w-4 text-white transition-transform duration-200', expanded && 'rotate-180')} className={cn('h-4 w-4 text-white 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>
); );
// (SUBSCRIPTION_LEVEL_ACTIONS + isSubscriptionLevelAction moved into ./bulkActions/actionTargets.ts) // (SUBSCRIPTION_LEVEL_ACTIONS + isSubscriptionLevelAction moved into ./bulkActions/actionTargets.ts)
@@ -632,7 +579,7 @@ export default function AdminBulkActions() {
aria-label={t('admin.bulkActions.selectAll')} aria-label={t('admin.bulkActions.selectAll')}
title={t('admin.bulkActions.selectAll')} title={t('admin.bulkActions.selectAll')}
> >
{table.getIsAllRowsSelected() && <CheckIcon />} {table.getIsAllRowsSelected() && <CheckIcon className="h-3 w-3" />}
{table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && ( {table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && (
<div className="h-0.5 w-2 rounded-full bg-white" /> <div className="h-0.5 w-2 rounded-full bg-white" />
)} )}
@@ -652,7 +599,7 @@ export default function AdminBulkActions() {
aria-label={t('admin.bulkActions.selectAllSubs')} aria-label={t('admin.bulkActions.selectAllSubs')}
title={t('admin.bulkActions.selectAllSubs')} title={t('admin.bulkActions.selectAllSubs')}
> >
{allSubsSelected && <CheckIcon />} {allSubsSelected && <CheckIcon className="h-3 w-3" />}
{someSubsSelected && <div className="h-0.5 w-2 rounded-full bg-white" />} {someSubsSelected && <div className="h-0.5 w-2 rounded-full bg-white" />}
</button> </button>
)} )}
@@ -678,7 +625,7 @@ export default function AdminBulkActions() {
: t('admin.bulkActions.selectUser', { name: userName }) : t('admin.bulkActions.selectUser', { name: userName })
} }
> >
{row.getIsSelected() && <CheckIcon />} {row.getIsSelected() && <CheckIcon className="h-3 w-3" />}
</button> </button>
</div> </div>
); );
@@ -988,7 +935,7 @@ export default function AdminBulkActions() {
)} )}
aria-pressed={trialOnly} aria-pressed={trialOnly}
> >
{trialOnly && <CheckIcon />} {trialOnly && <CheckIcon className="h-3 w-3" />}
</button> </button>
<span <span
className={cn('text-sm', trialOnly ? 'font-medium text-warning-400' : 'text-dark-400')} className={cn('text-sm', trialOnly ? 'font-medium text-warning-400' : 'text-dark-400')}

View File

@@ -13,32 +13,7 @@ import { partnerApi } from '../api/partners';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import Twemoji from 'react-twemoji'; import Twemoji from 'react-twemoji';
// Icons import { CampaignIcon, CheckIcon, RefreshIcon } from '@/components/icons';
const CampaignIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
/>
</svg>
);
const CheckIcon = () => (
<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>
);
const RefreshIcon = () => (
<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>
);
// Bonus type config // Bonus type config
const bonusTypeConfig: Record< const bonusTypeConfig: Record<
@@ -283,7 +258,7 @@ export default function AdminCampaignCreate() {
<AdminBackButton /> <AdminBackButton />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<CampaignIcon /> <CampaignIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-100"> <h1 className="text-xl font-bold text-dark-100">
@@ -556,7 +531,7 @@ export default function AdminCampaignCreate() {
disabled={!isValid || createMutation.isPending} disabled={!isValid || createMutation.isPending}
className="btn-primary flex items-center gap-2" className="btn-primary flex items-center gap-2"
> >
{createMutation.isPending ? <RefreshIcon /> : <CampaignIcon />} {createMutation.isPending ? <RefreshIcon /> : <CampaignIcon className="h-6 w-6" />}
{t('admin.campaigns.form.save')} {t('admin.campaigns.form.save')}
</button> </button>
</div> </div>

View File

@@ -10,47 +10,7 @@ import { PARTNER_STATS } from '../constants/partner';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { copyToClipboard } from '../utils/clipboard'; import { copyToClipboard } from '../utils/clipboard';
import { useHaptic } from '../platform'; import { useHaptic } from '../platform';
import { ChartIcon, CopyIcon, LinkIcon, UsersIcon } from '@/components/icons';
// Icons
const CopyIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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.75H9a.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>
);
const LinkIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const UsersIcon = () => (
<svg className="h-5 w-5" 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>
);
const ChartIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
);
// Bonus type config // Bonus type config
const bonusTypeConfig: Record< const bonusTypeConfig: Record<
@@ -205,7 +165,7 @@ export default function AdminCampaignStats() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AdminBackButton to="/admin/campaigns" /> <AdminBackButton to="/admin/campaigns" />
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<ChartIcon /> <ChartIcon className="h-6 w-6" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h1 className="truncate text-xl font-semibold text-dark-100">{stats.name}</h1> <h1 className="truncate text-xl font-semibold text-dark-100">{stats.name}</h1>
@@ -240,7 +200,7 @@ export default function AdminCampaignStats() {
</div> </div>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<LinkIcon /> <LinkIcon className="h-4 w-4" />
<span className="truncate text-sm text-dark-300">{stats.deep_link}</span> <span className="truncate text-sm text-dark-300">{stats.deep_link}</span>
</div> </div>
<button <button
@@ -264,7 +224,7 @@ export default function AdminCampaignStats() {
</div> </div>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<LinkIcon /> <LinkIcon className="h-4 w-4" />
<span className="truncate text-sm text-dark-300">{stats.web_link}</span> <span className="truncate text-sm text-dark-300">{stats.web_link}</span>
</div> </div>
<button <button

View File

@@ -4,7 +4,15 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tansta
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from '../i18n'; import i18n from '../i18n';
import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns'; import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns';
import { PlusIcon, EditIcon, TrashIcon, CheckIcon, XIcon, ChartIcon } from '../components/icons'; import {
PlusIcon,
EditIcon,
TrashIcon,
CheckIcon,
XIcon,
ChartIcon,
BackIcon,
} from '../components/icons';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { useFocusTrap } from '../hooks/useFocusTrap'; import { useFocusTrap } from '../hooks/useFocusTrap';
@@ -37,19 +45,6 @@ const bonusTypeConfig: Record<
}, },
}; };
// Icons
const BackIcon = () => (
<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>
);
// Locale mapping for formatting // Locale mapping for formatting
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };

View File

@@ -12,86 +12,17 @@ import {
import { adminSettingsApi, type SettingDefinition } from '../api/adminSettings'; import { adminSettingsApi, type SettingDefinition } from '../api/adminSettings';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { Toggle } from '../components/admin/Toggle'; import { Toggle } from '../components/admin/Toggle';
import {
// Icons ChannelIcon,
const ChannelIcon = () => ( PlusIcon,
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> RefreshIcon,
<path TrashIcon,
strokeLinecap="round" CheckIcon,
strokeLinejoin="round" XIcon,
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" EditIcon,
/> LinkIcon,
</svg> SettingsIcon,
); } from '@/components/icons';
const PlusIcon = () => (
<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>
);
const RefreshIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const CheckIcon = () => (
<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>
);
const XIcon = () => (
<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>
);
const EditIcon = () => (
<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>
);
const LinkIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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.757m9.86-2.239a4.5 4.5 0 00-1.242-7.244l-4.5-4.5a4.5 4.5 0 00-6.364 6.364L4.5 8.688"
/>
</svg>
);
const SettingsIcon = () => (
<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.325.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.241-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.991l1.004.827c.424.35.534.955.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.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.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-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.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-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
// Setting toggle row for global settings // Setting toggle row for global settings
const CHANNEL_SETTING_KEYS = [ const CHANNEL_SETTING_KEYS = [
@@ -266,7 +197,7 @@ function ChannelCard({
{/* Link */} {/* Link */}
{hasLink && ( {hasLink && (
<div className="mt-1.5 flex items-center gap-1 text-xs text-accent-400"> <div className="mt-1.5 flex items-center gap-1 text-xs text-accent-400">
<LinkIcon /> <LinkIcon className="h-4 w-4" />
<a <a
href={channel.channel_link!} href={channel.channel_link!}
target="_blank" target="_blank"
@@ -352,7 +283,7 @@ function ChannelCard({
onClick={() => onDelete(channel.id)} onClick={() => onDelete(channel.id)}
className="flex items-center gap-1.5 rounded-lg bg-error-500/20 px-3 py-1.5 text-xs text-error-400 transition-colors hover:bg-error-500/30" className="flex items-center gap-1.5 rounded-lg bg-error-500/20 px-3 py-1.5 text-xs text-error-400 transition-colors hover:bg-error-500/30"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
{t('admin.channelSubscriptions.delete')} {t('admin.channelSubscriptions.delete')}
</button> </button>
</div> </div>
@@ -688,7 +619,7 @@ export default function AdminChannelSubscriptions() {
<AdminBackButton /> <AdminBackButton />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<ChannelIcon /> <ChannelIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-100"> <h1 className="text-xl font-bold text-dark-100">
@@ -752,7 +683,7 @@ export default function AdminChannelSubscriptions() {
) : channels.length === 0 ? ( ) : channels.length === 0 ? (
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400"> <div className="rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400">
<div className="mx-auto mb-2 w-fit"> <div className="mx-auto mb-2 w-fit">
<ChannelIcon /> <ChannelIcon className="h-6 w-6" />
</div> </div>
<p>{t('admin.channelSubscriptions.empty')}</p> <p>{t('admin.channelSubscriptions.empty')}</p>
</div> </div>

View File

@@ -9,152 +9,23 @@ const CABINET_VERSION = __APP_VERSION__;
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
// Icons - styled like main navigation import {
BackIcon,
const BackIcon = () => ( BanknotesIcon,
<svg ChartBarIcon,
className="h-5 w-5 text-dark-400" ChevronDownIcon,
fill="none" ExclamationIcon,
viewBox="0 0 24 24" MegaphoneIcon,
stroke="currentColor" PowerIcon,
strokeWidth={2} RefreshIcon,
> RestartIcon,
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" /> ServerIcon,
</svg> SparklesIcon,
); TagIcon,
UsersIcon,
const ServerIcon = () => ( UsersOnlineIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> WalletIcon,
<path } from '@/components/icons';
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 00-.12-1.03l-2.268-9.64a3.375 3.375 0 00-3.285-2.602H7.923a3.375 3.375 0 00-3.285 2.602l-2.268 9.64a4.5 4.5 0 00-.12 1.03v.228m19.5 0a3 3 0 01-3 3H5.25a3 3 0 01-3-3m19.5 0a3 3 0 00-3-3H5.25a3 3 0 00-3 3m16.5 0h.008v.008h-.008v-.008zm-3 0h.008v.008h-.008v-.008z"
/>
</svg>
);
const UsersOnlineIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
/>
</svg>
);
const WalletIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"
/>
</svg>
);
const ChartBarIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
);
const SparklesIcon = () => (
<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.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.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 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"
/>
</svg>
);
const TagIcon = () => (
<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.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6h.008v.008H6V6z" />
</svg>
);
const RefreshIcon = () => (
<svg className="h-5 w-5" 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>
);
const PowerIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
</svg>
);
const RestartIcon = () => (
<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>
);
const ChevronDownIcon = () => (
<svg className="h-5 w-5" 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>
);
const UsersIcon = () => (
<svg className="h-5 w-5" 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>
);
const MegaphoneIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
/>
</svg>
);
const BanknotesIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<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>
);
const ExclamationIcon = () => (
<svg className="h-4 w-4" 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>
);
interface StatCardProps { interface StatCardProps {
title: string; title: string;
@@ -265,7 +136,7 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
{hasError && ( {hasError && (
<div className="mb-3 rounded-lg border border-error-500/20 bg-error-500/10 p-2"> <div className="mb-3 rounded-lg border border-error-500/20 bg-error-500/10 p-2">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<ExclamationIcon /> <ExclamationIcon className="h-4 w-4" />
<span className="break-all text-xs text-error-400">{node.last_status_message}</span> <span className="break-all text-xs text-error-400">{node.last_status_message}</span>
</div> </div>
</div> </div>
@@ -296,7 +167,7 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
: 'bg-warning-500/20 text-warning-400 hover:bg-warning-500/30' : 'bg-warning-500/20 text-warning-400 hover:bg-warning-500/30'
} disabled:opacity-50`} } disabled:opacity-50`}
> >
<PowerIcon /> <PowerIcon className="h-4 w-4" />
{node.is_disabled ? t('adminDashboard.nodes.enable') : t('adminDashboard.nodes.disable')} {node.is_disabled ? t('adminDashboard.nodes.enable') : t('adminDashboard.nodes.disable')}
</button> </button>
<button <button
@@ -304,7 +175,7 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
disabled={isLoading || node.is_disabled} disabled={isLoading || node.is_disabled}
className="flex items-center justify-center gap-1.5 rounded-lg bg-accent-500/20 px-3 py-2 text-sm font-medium text-accent-400 transition-colors hover:bg-accent-500/30 disabled:opacity-50" className="flex items-center justify-center gap-1.5 rounded-lg bg-accent-500/20 px-3 py-2 text-sm font-medium text-accent-400 transition-colors hover:bg-accent-500/30 disabled:opacity-50"
> >
<RestartIcon /> <RestartIcon className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -464,7 +335,7 @@ export default function AdminDashboard() {
disabled={loading} disabled={loading}
className="flex items-center gap-2 rounded-lg bg-dark-800 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50" className="flex items-center gap-2 rounded-lg bg-dark-800 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50"
> >
<RefreshIcon /> <RefreshIcon className="h-5 w-5" />
{t('adminDashboard.refresh')} {t('adminDashboard.refresh')}
</button> </button>
</div> </div>

View File

@@ -13,6 +13,7 @@ import { useIsTelegram } from '../platform/hooks/usePlatform';
import { useNativeDialog } from '../platform/hooks/useNativeDialog'; import { useNativeDialog } from '../platform/hooks/useNativeDialog';
import { useNotify } from '@/platform'; import { useNotify } from '@/platform';
import { copyToClipboard } from '@/utils/clipboard'; import { copyToClipboard } from '@/utils/clipboard';
import { MailIcon, SaveIcon, EyeIcon, SendIcon, ResetIcon } from '@/components/icons';
// Hook to check if on mobile // Hook to check if on mobile
function useIsMobile() { function useIsMobile() {
@@ -30,55 +31,6 @@ function useIsMobile() {
return isMobile; return isMobile;
} }
// Icons
const MailIcon = () => (
<svg className="h-6 w-6" 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>
);
const SaveIcon = () => (
<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>
);
const EyeIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
const SendIcon = () => (
<svg className="h-4 w-4" 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>
);
const ResetIcon = () => (
<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>
);
const LANG_LABELS: Record<string, string> = { const LANG_LABELS: Record<string, string> = {
ru: 'RU', ru: 'RU',
en: 'EN', en: 'EN',
@@ -390,7 +342,7 @@ function TemplateEditor({
disabled={!isDirty || saveMutation.isPending} disabled={!isDirty || saveMutation.isPending}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent-500 px-3 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-40 sm:px-4 sm:py-2" className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent-500 px-3 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-40 sm:px-4 sm:py-2"
> >
<SaveIcon /> <SaveIcon className="h-4 w-4" />
{saveMutation.isPending ? t('common.loading') : t('common.save')} {saveMutation.isPending ? t('common.loading') : t('common.save')}
</button> </button>
@@ -412,7 +364,7 @@ function TemplateEditor({
}} }}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-dark-700 px-3 py-2.5 text-sm font-medium text-dark-200 transition-colors hover:bg-dark-600 sm:px-4 sm:py-2" className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-dark-700 px-3 py-2.5 text-sm font-medium text-dark-200 transition-colors hover:bg-dark-600 sm:px-4 sm:py-2"
> >
<EyeIcon /> <EyeIcon className="h-4 w-4" />
{t('admin.emailTemplates.preview')} {t('admin.emailTemplates.preview')}
</button> </button>
</div> </div>
@@ -423,7 +375,7 @@ function TemplateEditor({
disabled={testMutation.isPending} disabled={testMutation.isPending}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-dark-700 px-3 py-2.5 text-sm font-medium text-dark-200 transition-colors hover:bg-dark-600 disabled:opacity-40 sm:px-4 sm:py-2" className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-dark-700 px-3 py-2.5 text-sm font-medium text-dark-200 transition-colors hover:bg-dark-600 disabled:opacity-40 sm:px-4 sm:py-2"
> >
<SendIcon /> <SendIcon className="h-4 w-4" />
{testMutation.isPending ? t('common.loading') : t('admin.emailTemplates.sendTest')} {testMutation.isPending ? t('common.loading') : t('admin.emailTemplates.sendTest')}
</button> </button>
@@ -437,7 +389,7 @@ function TemplateEditor({
disabled={resetMutation.isPending} disabled={resetMutation.isPending}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-dark-700 px-3 py-2.5 text-sm font-medium text-warning-400 transition-colors hover:bg-dark-600 disabled:opacity-40 sm:ml-auto sm:px-4 sm:py-2" className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-dark-700 px-3 py-2.5 text-sm font-medium text-warning-400 transition-colors hover:bg-dark-600 disabled:opacity-40 sm:ml-auto sm:px-4 sm:py-2"
> >
<ResetIcon /> <ResetIcon className="h-4 w-4" />
<span className="truncate">{t('admin.emailTemplates.resetDefault')}</span> <span className="truncate">{t('admin.emailTemplates.resetDefault')}</span>
</button> </button>
)} )}
@@ -474,7 +426,7 @@ export default function AdminEmailTemplates() {
<AdminBackButton className="flex-shrink-0 rounded-xl border border-dark-700 bg-dark-800 p-1.5 transition-colors hover:bg-dark-700 sm:p-2" /> <AdminBackButton className="flex-shrink-0 rounded-xl border border-dark-700 bg-dark-800 p-1.5 transition-colors hover:bg-dark-700 sm:p-2" />
<div className="flex min-w-0 items-center gap-2 sm:gap-2.5"> <div className="flex min-w-0 items-center gap-2 sm:gap-2.5">
<div className="flex-shrink-0 rounded-xl bg-gradient-to-br from-accent-500/20 to-accent-600/10 p-1.5 text-accent-400 sm:p-2"> <div className="flex-shrink-0 rounded-xl bg-gradient-to-br from-accent-500/20 to-accent-600/10 p-1.5 text-accent-400 sm:p-2">
<MailIcon /> <MailIcon className="h-6 w-6" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h1 className="truncate text-lg font-bold text-dark-100 sm:text-xl"> <h1 className="truncate text-lg font-bold text-dark-100 sm:text-xl">

View File

@@ -18,102 +18,35 @@ import { AdminBackButton } from '../components/admin';
import { Toggle } from '../components/admin/Toggle'; import { Toggle } from '../components/admin/Toggle';
import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { useHapticFeedback } from '../platform/hooks/useHaptic';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import {
BoldIcon,
ItalicIcon,
UnderlineIcon,
StrikeIcon,
ListBulletIcon,
ListOrderedIcon,
QuoteIcon,
CodeBlockIcon,
ImageIcon,
LinkIcon,
AlignLeftIcon,
AlignCenterIcon,
HighlightIcon,
ChevronUpIcon,
ChevronDownIcon,
TrashSmallIcon,
PlusSmallIcon,
} from '@/components/icons';
import type { InfoPageType, FaqItem, ReplacesTab } from '../api/infoPages'; import type { InfoPageType, FaqItem, ReplacesTab } from '../api/infoPages';
const AVAILABLE_LOCALES = ['ru', 'en', 'zh', 'fa'] as const; const AVAILABLE_LOCALES = ['ru', 'en', 'zh', 'fa'] as const;
type LocaleCode = (typeof AVAILABLE_LOCALES)[number]; type LocaleCode = (typeof AVAILABLE_LOCALES)[number];
// --- Icons --- // --- Icons ---
const BoldIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" />
</svg>
);
const ItalicIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" />
</svg>
);
const UnderlineIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" />
</svg>
);
const StrikeIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" />
</svg>
);
const H1Icon = () => <span className="text-xs font-bold">H1</span>; const H1Icon = () => <span className="text-xs font-bold">H1</span>;
const H2Icon = () => <span className="text-xs font-bold">H2</span>; const H2Icon = () => <span className="text-xs font-bold">H2</span>;
const H3Icon = () => <span className="text-xs font-bold">H3</span>; const H3Icon = () => <span className="text-xs font-bold">H3</span>;
const ListBulletIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />
</svg>
);
const ListOrderedIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z" />
</svg>
);
const QuoteIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z" />
</svg>
);
const CodeBlockIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" />
</svg>
);
const ImageIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const LinkIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const AlignLeftIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z" />
</svg>
);
const AlignCenterIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z" />
</svg>
);
const HighlightIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M16.56 3.44a1.5 1.5 0 012.12 0l1.88 1.88a1.5 1.5 0 010 2.12L8.44 19.56 3 21l1.44-5.44L16.56 3.44z" />
</svg>
);
// --- Toolbar Button --- // --- Toolbar Button ---
interface ToolbarButtonProps { interface ToolbarButtonProps {
onClick: () => void; onClick: () => void;
@@ -206,35 +139,6 @@ function generateSlug(title: string): string {
.substring(0, 100); .substring(0, 100);
} }
// --- FAQ Q&A Item Icons ---
const ChevronUpIcon = () => (
<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 15.75l7.5-7.5 7.5 7.5" />
</svg>
);
const ChevronDownIcon = () => (
<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 8.25l-7.5 7.5-7.5-7.5" />
</svg>
);
const TrashSmallIcon = () => (
<svg className="h-4 w-4" 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>
);
const PlusSmallIcon = () => (
<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>
);
// --- FAQ Answer Rich Editor --- // --- FAQ Answer Rich Editor ---
const FAQ_EDITOR_EXTENSIONS = [ const FAQ_EDITOR_EXTENSIONS = [
@@ -415,28 +319,28 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
isActive={editor.isActive('bold')} isActive={editor.isActive('bold')}
title={t('news.admin.toolbar.bold')} title={t('news.admin.toolbar.bold')}
> >
<BoldIcon /> <BoldIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive('italic')} isActive={editor.isActive('italic')}
title={t('news.admin.toolbar.italic')} title={t('news.admin.toolbar.italic')}
> >
<ItalicIcon /> <ItalicIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleUnderline().run()} onClick={() => editor.chain().focus().toggleUnderline().run()}
isActive={editor.isActive('underline')} isActive={editor.isActive('underline')}
title={t('news.admin.toolbar.underline')} title={t('news.admin.toolbar.underline')}
> >
<UnderlineIcon /> <UnderlineIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
isActive={editor.isActive('strike')} isActive={editor.isActive('strike')}
title={t('news.admin.toolbar.strikethrough')} title={t('news.admin.toolbar.strikethrough')}
> >
<StrikeIcon /> <StrikeIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-0.5 h-4 w-px bg-dark-700" /> <div className="mx-0.5 h-4 w-px bg-dark-700" />
<ToolbarButton <ToolbarButton
@@ -458,14 +362,14 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
isActive={editor.isActive('orderedList')} isActive={editor.isActive('orderedList')}
title={t('news.admin.toolbar.orderedList')} title={t('news.admin.toolbar.orderedList')}
> >
<ListOrderedIcon /> <ListOrderedIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={() => editor.chain().focus().toggleBlockquote().run()}
isActive={editor.isActive('blockquote')} isActive={editor.isActive('blockquote')}
title={t('news.admin.toolbar.blockquote')} title={t('news.admin.toolbar.blockquote')}
> >
<QuoteIcon /> <QuoteIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-0.5 h-4 w-px bg-dark-700" /> <div className="mx-0.5 h-4 w-px bg-dark-700" />
<ToolbarButton <ToolbarButton
@@ -473,10 +377,10 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
isActive={editor.isActive('highlight')} isActive={editor.isActive('highlight')}
title={t('news.admin.toolbar.highlight')} title={t('news.admin.toolbar.highlight')}
> >
<HighlightIcon /> <HighlightIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}> <ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}>
<LinkIcon /> <LinkIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => mediaRef.current?.click()} onClick={() => mediaRef.current?.click()}
@@ -486,7 +390,7 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
{isUploading ? ( {isUploading ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" />
) : ( ) : (
<ImageIcon /> <ImageIcon className="h-4 w-4" />
)} )}
</ToolbarButton> </ToolbarButton>
</div> </div>
@@ -660,7 +564,7 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
className="min-h-[44px] min-w-[44px] rounded-lg p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200 disabled:cursor-not-allowed disabled:opacity-30" className="min-h-[44px] min-w-[44px] rounded-lg p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200 disabled:cursor-not-allowed disabled:opacity-30"
title={t('admin.infoPages.faq.moveUp')} title={t('admin.infoPages.faq.moveUp')}
> >
<ChevronUpIcon /> <ChevronUpIcon className="h-4 w-4" />
</button> </button>
<button <button
type="button" type="button"
@@ -669,7 +573,7 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
className="min-h-[44px] min-w-[44px] rounded-lg p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200 disabled:cursor-not-allowed disabled:opacity-30" className="min-h-[44px] min-w-[44px] rounded-lg p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200 disabled:cursor-not-allowed disabled:opacity-30"
title={t('admin.infoPages.faq.moveDown')} title={t('admin.infoPages.faq.moveDown')}
> >
<ChevronDownIcon /> <ChevronDownIcon className="h-4 w-4" />
</button> </button>
<button <button
type="button" type="button"
@@ -713,7 +617,7 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
onClick={handleAdd} onClick={handleAdd}
className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-xl border border-dashed border-dark-600 bg-dark-800/30 py-3 text-sm font-medium text-dark-300 transition-colors hover:border-dark-500 hover:bg-dark-800/50 hover:text-dark-100" className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-xl border border-dashed border-dark-600 bg-dark-800/30 py-3 text-sm font-medium text-dark-300 transition-colors hover:border-dark-500 hover:bg-dark-800/50 hover:text-dark-100"
> >
<PlusSmallIcon /> <PlusSmallIcon className="h-5 w-5" />
{t('admin.infoPages.faq.addQuestion')} {t('admin.infoPages.faq.addQuestion')}
</button> </button>
</div> </div>
@@ -1333,28 +1237,28 @@ export default function AdminInfoPageEditor() {
isActive={editor.isActive('bold')} isActive={editor.isActive('bold')}
title={t('news.admin.toolbar.bold')} title={t('news.admin.toolbar.bold')}
> >
<BoldIcon /> <BoldIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive('italic')} isActive={editor.isActive('italic')}
title={t('news.admin.toolbar.italic')} title={t('news.admin.toolbar.italic')}
> >
<ItalicIcon /> <ItalicIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleUnderline().run()} onClick={() => editor.chain().focus().toggleUnderline().run()}
isActive={editor.isActive('underline')} isActive={editor.isActive('underline')}
title={t('news.admin.toolbar.underline')} title={t('news.admin.toolbar.underline')}
> >
<UnderlineIcon /> <UnderlineIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
isActive={editor.isActive('strike')} isActive={editor.isActive('strike')}
title={t('news.admin.toolbar.strikethrough')} title={t('news.admin.toolbar.strikethrough')}
> >
<StrikeIcon /> <StrikeIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-1 h-5 w-px bg-dark-700" /> <div className="mx-1 h-5 w-px bg-dark-700" />
@@ -1395,21 +1299,21 @@ export default function AdminInfoPageEditor() {
isActive={editor.isActive('orderedList')} isActive={editor.isActive('orderedList')}
title={t('news.admin.toolbar.orderedList')} title={t('news.admin.toolbar.orderedList')}
> >
<ListOrderedIcon /> <ListOrderedIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={() => editor.chain().focus().toggleBlockquote().run()}
isActive={editor.isActive('blockquote')} isActive={editor.isActive('blockquote')}
title={t('news.admin.toolbar.blockquote')} title={t('news.admin.toolbar.blockquote')}
> >
<QuoteIcon /> <QuoteIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
isActive={editor.isActive('codeBlock')} isActive={editor.isActive('codeBlock')}
title={t('news.admin.toolbar.codeBlock')} title={t('news.admin.toolbar.codeBlock')}
> >
<CodeBlockIcon /> <CodeBlockIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-1 h-5 w-px bg-dark-700" /> <div className="mx-1 h-5 w-px bg-dark-700" />
@@ -1419,14 +1323,14 @@ export default function AdminInfoPageEditor() {
isActive={editor.isActive({ textAlign: 'left' })} isActive={editor.isActive({ textAlign: 'left' })}
title={t('news.admin.toolbar.alignLeft')} title={t('news.admin.toolbar.alignLeft')}
> >
<AlignLeftIcon /> <AlignLeftIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().setTextAlign('center').run()} onClick={() => editor.chain().focus().setTextAlign('center').run()}
isActive={editor.isActive({ textAlign: 'center' })} isActive={editor.isActive({ textAlign: 'center' })}
title={t('news.admin.toolbar.alignCenter')} title={t('news.admin.toolbar.alignCenter')}
> >
<AlignCenterIcon /> <AlignCenterIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-1 h-5 w-px bg-dark-700" /> <div className="mx-1 h-5 w-px bg-dark-700" />
@@ -1436,10 +1340,10 @@ export default function AdminInfoPageEditor() {
isActive={editor.isActive('highlight')} isActive={editor.isActive('highlight')}
title={t('news.admin.toolbar.highlight')} title={t('news.admin.toolbar.highlight')}
> >
<HighlightIcon /> <HighlightIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}> <ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}>
<LinkIcon /> <LinkIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => mediaInputRef.current?.click()} onClick={() => mediaInputRef.current?.click()}
@@ -1449,7 +1353,7 @@ export default function AdminInfoPageEditor() {
{isUploading ? ( {isUploading ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" />
) : ( ) : (
<ImageIcon /> <ImageIcon className="h-4 w-4" />
)} )}
</ToolbarButton> </ToolbarButton>
</div> </div>

View File

@@ -8,92 +8,11 @@ import { Toggle } from '../components/admin/Toggle';
import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { useHapticFeedback } from '../platform/hooks/useHaptic';
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog'; import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { FileTextIcon, PencilIcon, PlusIcon, RefreshIcon, TrashIcon } from '@/components/icons';
import type { InfoPageListItem, InfoPageType } from '../api/infoPages'; import type { InfoPageListItem, InfoPageType } from '../api/infoPages';
type FilterTab = 'all' | 'page' | 'faq'; type FilterTab = 'all' | 'page' | 'faq';
// Icons
const PlusIcon = () => (
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
const RefreshIcon = () => (
<svg
className="h-4 w-4"
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>
);
const PencilIcon = () => (
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<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>
);
const TrashIcon = () => (
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<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>
);
const FileTextIcon = () => (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.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 00-9-9z"
/>
</svg>
);
// --- Page Row --- // --- Page Row ---
const PageRow = memo(function PageRow({ const PageRow = memo(function PageRow({
@@ -179,7 +98,7 @@ const PageRow = memo(function PageRow({
title={t('admin.infoPages.delete')} title={t('admin.infoPages.delete')}
aria-label={t('admin.infoPages.delete')} aria-label={t('admin.infoPages.delete')}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -374,7 +293,7 @@ export default function AdminInfoPages() {
</div> </div>
) : items.length === 0 ? ( ) : items.length === 0 ? (
<div className="flex flex-col items-center rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400"> <div className="flex flex-col items-center rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400">
<FileTextIcon /> <FileTextIcon className="h-6 w-6" />
<p className="mt-2">{t('admin.infoPages.noPages')}</p> <p className="mt-2">{t('admin.infoPages.noPages')}</p>
</div> </div>
) : ( ) : (

View File

@@ -1,4 +1,5 @@
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -52,15 +53,7 @@ function isoToDatetimeLocal(iso: string): string {
} }
const ChevronDownIcon = ({ open }: { open: boolean }) => ( const ChevronDownIcon = ({ open }: { open: boolean }) => (
<svg <PiCaretDownDuotone className={cn('h-5 w-5 transition-transform', open && 'rotate-180')} />
className={cn('h-5 w-5 transition-transform', open && '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>
); );
// ============ Collapsible Section ============ // ============ Collapsible Section ============

View File

@@ -26,17 +26,15 @@ import { useCurrency } from '../hooks/useCurrency';
import { useChartColors } from '../hooks/useChartColors'; import { useChartColors } from '../hooks/useChartColors';
import { CHART_COMMON } from '../constants/charts'; import { CHART_COMMON } from '../constants/charts';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import {
// Icons ChartIcon,
const ChartIcon = () => ( EmailIcon,
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> TelegramSmallIcon,
<path ArrowRightIcon,
strokeLinecap="round" GiftIcon,
strokeLinejoin="round" ChevronLeftIcon as ChevronLeftSmall,
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" ChevronRightIcon as ChevronRightSmall,
/> } from '@/components/icons';
</svg>
);
const TARIFF_PALETTE = ['#818cf8', '#34d399', '#f59e0b', '#ec4899', '#06b6d4', '#8b5cf6']; const TARIFF_PALETTE = ['#818cf8', '#34d399', '#f59e0b', '#ec4899', '#06b6d4', '#8b5cf6'];
const GIFT_COLOR = '#a855f7'; const GIFT_COLOR = '#a855f7';
@@ -62,75 +60,15 @@ const PURCHASE_STATUS_OPTIONS: Array<PurchaseItemStatus | 'all'> = [
const PURCHASES_PAGE_SIZE = 20; const PURCHASES_PAGE_SIZE = 20;
// Small icons for the purchase cards
const EmailIcon = () => (
<svg
className="h-3.5 w-3.5 shrink-0"
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>
);
const TelegramSmallIcon = () => (
<svg className="h-3.5 w-3.5 shrink-0" viewBox="0 0 24 24" fill="currentColor">
<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>
);
const ArrowRightIcon = () => (
<svg
className="h-3 w-3 shrink-0 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
);
const GiftIcon = () => (
<svg
className="h-3.5 w-3.5 shrink-0"
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>
);
const ChevronLeftSmall = () => (
<svg className="h-4 w-4" 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>
);
const ChevronRightSmall = () => (
<svg className="h-4 w-4" 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>
);
// Contact display helper // Contact display helper
function ContactDisplay({ type, value }: { type: 'email' | 'telegram'; value: string }) { function ContactDisplay({ type, value }: { type: 'email' | 'telegram'; value: string }) {
return ( return (
<span className="flex items-center gap-1 text-dark-300"> <span className="flex items-center gap-1 text-dark-300">
{type === 'email' ? <EmailIcon /> : <TelegramSmallIcon />} {type === 'email' ? (
<EmailIcon className="h-3.5 w-3.5" />
) : (
<TelegramSmallIcon className="h-3.5 w-3.5" />
)}
<span className="min-w-0 truncate text-xs">{value}</span> <span className="min-w-0 truncate text-xs">{value}</span>
</span> </span>
); );
@@ -184,7 +122,7 @@ function PurchaseCard({ item, formatPrice, lang, t }: PurchaseCardProps) {
<ContactDisplay type={item.contact_type} value={item.contact_value} /> <ContactDisplay type={item.contact_type} value={item.contact_value} />
{item.is_gift && item.gift_recipient_type && item.gift_recipient_value && ( {item.is_gift && item.gift_recipient_type && item.gift_recipient_value && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<ArrowRightIcon /> <ArrowRightIcon className="h-3 w-3" />
<ContactDisplay type={item.gift_recipient_type} value={item.gift_recipient_value} /> <ContactDisplay type={item.gift_recipient_type} value={item.gift_recipient_value} />
</span> </span>
)} )}
@@ -212,7 +150,7 @@ function PurchaseCard({ item, formatPrice, lang, t }: PurchaseCardProps) {
{item.is_gift && ( {item.is_gift && (
<div className="shrink-0"> <div className="shrink-0">
<span className="inline-flex items-center gap-1 rounded-md bg-purple-500/20 px-1.5 py-0.5 text-xs text-purple-400"> <span className="inline-flex items-center gap-1 rounded-md bg-purple-500/20 px-1.5 py-0.5 text-xs text-purple-400">
<GiftIcon /> <GiftIcon className="h-3.5 w-3.5" />
{t('admin.landings.purchases.gift')} {t('admin.landings.purchases.gift')}
</span> </span>
</div> </div>
@@ -379,7 +317,7 @@ export default function AdminLandingStats() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AdminBackButton to="/admin/landings" /> <AdminBackButton to="/admin/landings" />
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<ChartIcon /> <ChartIcon className="h-6 w-6" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h1 className="truncate text-xl font-semibold text-dark-100">{landingTitle}</h1> <h1 className="truncate text-xl font-semibold text-dark-100">{landingTitle}</h1>

View File

@@ -9,6 +9,15 @@ import { getApiErrorMessage } from '../utils/api-error';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { BackIcon, PlusIcon, TrashIcon, GripIcon } from '../components/icons/LandingIcons'; import { BackIcon, PlusIcon, TrashIcon, GripIcon } from '../components/icons/LandingIcons';
import {
EditIcon,
CheckIcon,
XIcon,
GiftIcon,
SaveIcon,
CopyIcon,
StatsChartIcon,
} from '@/components/icons';
import { import {
DndContext, DndContext,
KeyboardSensor, KeyboardSensor,
@@ -26,69 +35,6 @@ import {
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
// Icons (non-shared, page-specific)
const EditIcon = () => (
<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>
);
const CheckIcon = () => (
<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>
);
const XIcon = () => (
<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>
);
const GiftIcon = () => (
<svg className="h-4 w-4" 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>
);
const SaveIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const CopyIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const StatsChartIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
);
// ============ Sortable Landing Card ============ // ============ Sortable Landing Card ============
interface SortableLandingCardProps { interface SortableLandingCardProps {
@@ -211,7 +157,7 @@ function SortableLandingCard({
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100" className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100"
title={t('admin.landings.statistics')} title={t('admin.landings.statistics')}
> >
<StatsChartIcon /> <StatsChartIcon className="h-4 w-4" />
</button> </button>
<button <button
onClick={onToggle} onClick={onToggle}
@@ -273,7 +219,7 @@ function SortableLandingCard({
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100" className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100"
title={t('admin.landings.statistics')} title={t('admin.landings.statistics')}
> >
<StatsChartIcon /> <StatsChartIcon className="h-4 w-4" />
</button> </button>
<button <button
onClick={onToggle} onClick={onToggle}
@@ -470,7 +416,7 @@ export default function AdminLandings() {
{saveOrderMutation.isPending ? ( {saveOrderMutation.isPending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<SaveIcon /> <SaveIcon className="h-4 w-4" />
)} )}
{t('admin.landings.saveOrder')} {t('admin.landings.saveOrder')}
</button> </button>

View File

@@ -8,105 +8,14 @@ import { Toggle } from '../components/admin/Toggle';
import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { useHapticFeedback } from '../platform/hooks/useHaptic';
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog'; import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
import type { NewsListItem } from '../types/news'; import type { NewsListItem } from '../types/news';
import {
// Icons PlusIcon,
const PlusIcon = () => ( RefreshIcon,
<svg PencilIcon,
className="h-5 w-5" TrashIcon,
fill="none" StarIcon,
viewBox="0 0 24 24" NewsIcon,
stroke="currentColor" } from '@/components/icons';
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
const RefreshIcon = () => (
<svg
className="h-4 w-4"
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>
);
const PencilIcon = () => (
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<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>
);
const TrashIcon = () => (
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<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>
);
const StarIcon = ({ filled }: { filled: boolean }) => (
<svg
className="h-4 w-4"
fill={filled ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
);
const NewsIcon = () => (
<svg
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z"
/>
</svg>
);
// --- Security: hex color validation to prevent CSS injection --- // --- Security: hex color validation to prevent CSS injection ---
const HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; const HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
@@ -195,7 +104,7 @@ const ArticleRow = memo(function ArticleRow({
title={t('news.admin.featured')} title={t('news.admin.featured')}
aria-label={t('news.admin.featured')} aria-label={t('news.admin.featured')}
> >
<StarIcon filled={article.is_featured} /> <StarIcon className="h-4 w-4" filled={article.is_featured} />
</button> </button>
<Toggle <Toggle
checked={article.is_published} checked={article.is_published}
@@ -218,7 +127,7 @@ const ArticleRow = memo(function ArticleRow({
title={t('news.admin.delete')} title={t('news.admin.delete')}
aria-label={t('news.admin.delete')} aria-label={t('news.admin.delete')}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -401,7 +310,7 @@ export default function AdminNews() {
</div> </div>
) : articles.length === 0 ? ( ) : articles.length === 0 ? (
<div className="flex flex-col items-center rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400"> <div className="flex flex-col items-center rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400">
<NewsIcon /> <NewsIcon className="h-6 w-6" />
<p className="mt-2">{t('news.noNews')}</p> <p className="mt-2">{t('news.noNews')}</p>
</div> </div>
) : ( ) : (

View File

@@ -19,109 +19,25 @@ import { Toggle } from '../components/admin/Toggle';
import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { useHapticFeedback } from '../platform/hooks/useHaptic';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import type { NewsCategory, NewsTag, NewsCreateRequest } from '../types/news'; import type { NewsCategory, NewsTag, NewsCreateRequest } from '../types/news';
import {
// --- Icons --- BoldIcon,
const BoldIcon = () => ( ItalicIcon,
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor"> UnderlineIcon,
<path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" /> StrikeIcon,
</svg> H1Icon,
); H2Icon,
H3Icon,
const ItalicIcon = () => ( ListBulletIcon,
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor"> ListOrderedIcon,
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" /> QuoteIcon,
</svg> CodeBlockIcon,
); ImageIcon,
LinkIcon,
const UnderlineIcon = () => ( AlignLeftIcon,
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor"> AlignCenterIcon,
<path d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" /> HighlightIcon,
</svg> UploadIcon,
); } from '@/components/icons';
const StrikeIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" />
</svg>
);
const H1Icon = () => <span className="text-xs font-bold">H1</span>;
const H2Icon = () => <span className="text-xs font-bold">H2</span>;
const H3Icon = () => <span className="text-xs font-bold">H3</span>;
const ListBulletIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />
</svg>
);
const ListOrderedIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z" />
</svg>
);
const QuoteIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z" />
</svg>
);
const CodeBlockIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" />
</svg>
);
const ImageIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const LinkIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const AlignLeftIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z" />
</svg>
);
const AlignCenterIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z" />
</svg>
);
const HighlightIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M16.56 3.44a1.5 1.5 0 012.12 0l1.88 1.88a1.5 1.5 0 010 2.12L8.44 19.56 3 21l1.44-5.44L16.56 3.44z" />
</svg>
);
const UploadIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
// --- Toolbar Button --- // --- Toolbar Button ---
interface ToolbarButtonProps { interface ToolbarButtonProps {
@@ -765,7 +681,7 @@ export default function AdminNewsCreate() {
{isFeaturedImageUploading ? ( {isFeaturedImageUploading ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" />
) : ( ) : (
<UploadIcon /> <UploadIcon className="h-4 w-4" />
)} )}
<span className="hidden sm:inline">{t('news.admin.uploadFeaturedImage')}</span> <span className="hidden sm:inline">{t('news.admin.uploadFeaturedImage')}</span>
</button> </button>
@@ -848,28 +764,28 @@ export default function AdminNewsCreate() {
isActive={editor.isActive('bold')} isActive={editor.isActive('bold')}
title={t('news.admin.toolbar.bold')} title={t('news.admin.toolbar.bold')}
> >
<BoldIcon /> <BoldIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive('italic')} isActive={editor.isActive('italic')}
title={t('news.admin.toolbar.italic')} title={t('news.admin.toolbar.italic')}
> >
<ItalicIcon /> <ItalicIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleUnderline().run()} onClick={() => editor.chain().focus().toggleUnderline().run()}
isActive={editor.isActive('underline')} isActive={editor.isActive('underline')}
title={t('news.admin.toolbar.underline')} title={t('news.admin.toolbar.underline')}
> >
<UnderlineIcon /> <UnderlineIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
isActive={editor.isActive('strike')} isActive={editor.isActive('strike')}
title={t('news.admin.toolbar.strikethrough')} title={t('news.admin.toolbar.strikethrough')}
> >
<StrikeIcon /> <StrikeIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-1 h-5 w-px bg-dark-700" /> <div className="mx-1 h-5 w-px bg-dark-700" />
@@ -879,7 +795,7 @@ export default function AdminNewsCreate() {
isActive={editor.isActive('heading', { level: 1 })} isActive={editor.isActive('heading', { level: 1 })}
title={t('news.admin.toolbar.heading1')} title={t('news.admin.toolbar.heading1')}
> >
<H1Icon /> <H1Icon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
@@ -910,21 +826,21 @@ export default function AdminNewsCreate() {
isActive={editor.isActive('orderedList')} isActive={editor.isActive('orderedList')}
title={t('news.admin.toolbar.orderedList')} title={t('news.admin.toolbar.orderedList')}
> >
<ListOrderedIcon /> <ListOrderedIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={() => editor.chain().focus().toggleBlockquote().run()}
isActive={editor.isActive('blockquote')} isActive={editor.isActive('blockquote')}
title={t('news.admin.toolbar.blockquote')} title={t('news.admin.toolbar.blockquote')}
> >
<QuoteIcon /> <QuoteIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
isActive={editor.isActive('codeBlock')} isActive={editor.isActive('codeBlock')}
title={t('news.admin.toolbar.codeBlock')} title={t('news.admin.toolbar.codeBlock')}
> >
<CodeBlockIcon /> <CodeBlockIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-1 h-5 w-px bg-dark-700" /> <div className="mx-1 h-5 w-px bg-dark-700" />
@@ -934,14 +850,14 @@ export default function AdminNewsCreate() {
isActive={editor.isActive({ textAlign: 'left' })} isActive={editor.isActive({ textAlign: 'left' })}
title={t('news.admin.toolbar.alignLeft')} title={t('news.admin.toolbar.alignLeft')}
> >
<AlignLeftIcon /> <AlignLeftIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => editor.chain().focus().setTextAlign('center').run()} onClick={() => editor.chain().focus().setTextAlign('center').run()}
isActive={editor.isActive({ textAlign: 'center' })} isActive={editor.isActive({ textAlign: 'center' })}
title={t('news.admin.toolbar.alignCenter')} title={t('news.admin.toolbar.alignCenter')}
> >
<AlignCenterIcon /> <AlignCenterIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<div className="mx-1 h-5 w-px bg-dark-700" /> <div className="mx-1 h-5 w-px bg-dark-700" />
@@ -951,10 +867,10 @@ export default function AdminNewsCreate() {
isActive={editor.isActive('highlight')} isActive={editor.isActive('highlight')}
title={t('news.admin.toolbar.highlight')} title={t('news.admin.toolbar.highlight')}
> >
<HighlightIcon /> <HighlightIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}> <ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}>
<LinkIcon /> <LinkIcon className="h-4 w-4" />
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
onClick={() => mediaInputRef.current?.click()} onClick={() => mediaInputRef.current?.click()}
@@ -964,7 +880,7 @@ export default function AdminNewsCreate() {
{isUploading ? ( {isUploading ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-accent-400 border-t-transparent" />
) : ( ) : (
<ImageIcon /> <ImageIcon className="h-4 w-4" />
)} )}
</ToolbarButton> </ToolbarButton>
</div> </div>

View File

@@ -7,365 +7,91 @@ import { statsApi, type SystemInfo, type DashboardStats } from '@/api/admin';
import { useAnimatedNumber } from '@/hooks/useAnimatedNumber'; import { useAnimatedNumber } from '@/hooks/useAnimatedNumber';
import { useTelegramSDK } from '@/hooks/useTelegramSDK'; import { useTelegramSDK } from '@/hooks/useTelegramSDK';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import {
ArrowUpIcon,
BroadcastIcon,
CabinetIcon,
ChartBarIcon,
ChevronRightIcon,
ClipboardIcon,
CreditCardIcon,
FileTextIcon,
GiftIcon,
HistoryIcon,
LockIcon,
MailIcon,
MegaphoneIcon,
NewsIcon,
PartnerIcon,
PercentIcon,
PinIcon,
RemnawaveIcon,
SearchIcon,
SendIcon,
ServerIcon,
SettingsIcon,
ShareIcon,
ShieldIcon,
SparklesIcon,
StatBotIcon,
StatCabinetIcon,
StatPaidIcon,
StatsChartIcon,
StatTrialIcon,
StatUptimeIcon,
SyncIcon,
TagIcon,
TicketIcon,
TrafficIcon,
UserPlusIcon,
UsersIcon,
WalletIcon,
WheelIcon,
XIcon,
} from '@/components/icons';
const CABINET_VERSION = __APP_VERSION__; const CABINET_VERSION = __APP_VERSION__;
const IS_MAC = /Mac|iPhone|iPod|iPad/i.test(navigator.userAgent); const IS_MAC = /Mac|iPhone|iPod|iPad/i.test(navigator.userAgent);
// ─── Inline SVG Icons (lightweight, no external deps) ───
const SvgIcon = ({
children,
className,
...props
}: React.SVGProps<SVGSVGElement> & { children: React.ReactNode }) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
{...props}
>
{children}
</svg>
);
// Stats bar icons (16x16 viewBox)
const StatUptimeIcon = () => (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
className="h-3.5 w-3.5"
aria-hidden="true"
>
<circle cx="8" cy="8" r="6.5" />
<path d="M8 4.5V8l2.5 1.5" />
</svg>
);
const StatBotIcon = () => (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
className="h-3.5 w-3.5"
aria-hidden="true"
>
<rect x="3" y="4" width="10" height="8" rx="2" />
<path d="M6 8h.01M10 8h.01" />
<path d="M8 2v2M4 14h8" />
</svg>
);
const StatCabinetIcon = () => (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
className="h-3.5 w-3.5"
aria-hidden="true"
>
<rect x="2" y="3" width="12" height="10" rx="1.5" />
<path d="M2 6h12" />
<path d="M5 3v3" />
</svg>
);
const StatTrialIcon = () => (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
className="h-3.5 w-3.5"
aria-hidden="true"
>
<path d="M8 2v2M8 12v2M4 8H2M14 8h-2" />
<circle cx="8" cy="8" r="3" />
</svg>
);
const StatPaidIcon = () => (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
className="h-3.5 w-3.5"
aria-hidden="true"
>
<path d="M4 6c0-1.7 1.8-3 4-3s4 1.3 4 3-1.8 3-4 3-4 1.3-4 3 1.8 3 4 3 4-1.3 4-3" />
<path d="M8 1v2M8 13v2" />
</svg>
);
// Section nav icons (24x24 viewBox) // Section nav icons (24x24 viewBox)
const icons = { const icons = {
'bar-chart': ( 'bar-chart': <ChartBarIcon />,
<SvgIcon> 'credit-card': <CreditCardIcon />,
<path d="M3 3v18h18" /> activity: <TrafficIcon />,
<path d="M7 16V8" /> trending: <StatsChartIcon />,
<path d="M11 16V11" /> users: <UsersIcon />,
<path d="M15 16V5" /> ticket: <TicketIcon />,
<path d="M19 16v-3" /> 'shield-alert': <ShieldIcon />,
</SvgIcon> tag: <TagIcon />,
), gift: <GiftIcon />,
'credit-card': ( percent: <PercentIcon />,
<SvgIcon> sparkle: <SparklesIcon />,
<rect x="2" y="5" width="20" height="14" rx="2" /> wallet: <WalletIcon />,
<path d="M2 10h20" /> layout: <CabinetIcon />,
</SvgIcon> newspaper: <NewsIcon />,
), megaphone: <MegaphoneIcon />,
activity: ( send: <SendIcon />,
<SvgIcon> pin: <PinIcon />,
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" /> 'circle-dot': <WheelIcon />,
</SvgIcon> handshake: <PartnerIcon />,
), 'arrow-up': <ArrowUpIcon />,
trending: ( network: <ShareIcon />,
<SvgIcon> radio: <BroadcastIcon />,
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17" /> settings: <SettingsIcon />,
<polyline points="16 7 22 7 22 13" /> app: <CabinetIcon />,
</SvgIcon> server: <ServerIcon />,
), remnawave: <RemnawaveIcon />,
users: ( mail: <MailIcon />,
<SvgIcon> refresh: <SyncIcon />,
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> shield: <ShieldIcon />,
<circle cx="9" cy="7" r="4" /> 'user-check': <UserPlusIcon />,
<path d="M22 21v-2a4 4 0 0 0-3-3.87" /> lock: <LockIcon />,
<path d="M16 3.13a4 4 0 0 1 0 7.75" /> scroll: <HistoryIcon />,
</SvgIcon> 'list-checks': <ClipboardIcon />,
), search: <SearchIcon />,
ticket: ( 'file-text': <FileTextIcon />,
<SvgIcon> chevron: <ChevronRightIcon />,
<path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z" /> x: <XIcon />,
<path d="M13 5v2M13 17v2M13 11v2" />
</SvgIcon>
),
'shield-alert': (
<SvgIcon>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="M12 8v4" />
<path d="M12 16h.01" />
</SvgIcon>
),
tag: (
<SvgIcon>
<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />
<circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
</SvgIcon>
),
gift: (
<SvgIcon>
<rect x="3" y="8" width="18" height="4" rx="1" />
<path d="M12 8v13" />
<path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" />
</SvgIcon>
),
percent: (
<SvgIcon>
<line x1="19" y1="5" x2="5" y2="19" />
<circle cx="6.5" cy="6.5" r="2.5" />
<circle cx="17.5" cy="17.5" r="2.5" />
</SvgIcon>
),
sparkle: (
<SvgIcon>
<path d="m12 3-1.9 5.8a2 2 0 0 1-1.287 1.288L3 12l5.8 1.9a2 2 0 0 1 1.288 1.287L12 21l1.9-5.8a2 2 0 0 1 1.287-1.288L21 12l-5.8-1.9a2 2 0 0 1-1.288-1.287Z" />
</SvgIcon>
),
wallet: (
<SvgIcon>
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4" />
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5" />
<path d="M18 12a2 2 0 0 0 0 4h4v-4Z" />
</SvgIcon>
),
layout: (
<SvgIcon>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M3 9h18" />
<path d="M9 21V9" />
</SvgIcon>
),
newspaper: (
<SvgIcon>
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" />
<path d="M18 14h-8M15 18h-5" />
<path d="M10 6h8v4h-8V6Z" />
</SvgIcon>
),
megaphone: (
<SvgIcon>
<path d="m3 11 18-5v12L3 13v-2z" />
<path d="M11.6 16.8a3 3 0 1 1-5.8-1.6" />
</SvgIcon>
),
send: (
<SvgIcon>
<path d="m22 2-7 20-4-9-9-4Z" />
<path d="M22 2 11 13" />
</SvgIcon>
),
pin: (
<SvgIcon>
<line x1="12" y1="17" x2="12" y2="22" />
<path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />
</SvgIcon>
),
'circle-dot': (
<SvgIcon>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="1" fill="currentColor" />
</SvgIcon>
),
handshake: (
<SvgIcon>
<path d="m11 17 2 2a1 1 0 1 0 3-3" />
<path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88" />
<path d="m2 12 5.56-5.56a3 3 0 0 1 2.22-.88L12 5.5" />
<path d="M22 12 16.44 6.44a3 3 0 0 0-2.22-.88L12 5.5" />
</SvgIcon>
),
'arrow-up': (
<SvgIcon>
<path d="m18 9-6-6-6 6" />
<path d="M12 3v14" />
<path d="M5 21h14" />
</SvgIcon>
),
network: (
<SvgIcon>
<rect x="16" y="16" width="6" height="6" rx="1" />
<rect x="2" y="16" width="6" height="6" rx="1" />
<rect x="9" y="2" width="6" height="6" rx="1" />
<path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3" />
<path d="M12 12V8" />
</SvgIcon>
),
radio: (
<SvgIcon>
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9" />
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.4" />
<circle cx="12" cy="12" r="2" />
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.4" />
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19" />
</SvgIcon>
),
settings: (
<SvgIcon>
<circle cx="12" cy="12" r="3" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</SvgIcon>
),
app: (
<SvgIcon>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M10 4v4M2 8h20M6 4v4" />
</SvgIcon>
),
server: (
<SvgIcon>
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" fill="currentColor" />
<circle cx="6" cy="18" r="1" fill="currentColor" />
</SvgIcon>
),
remnawave: (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-[13px] w-[13px]"
aria-hidden="true"
>
<path
clipRule="evenodd"
d="M8 1a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-1.5 0V1.75A.75.75 0 0 1 8 1Zm6 2a.75.75 0 0 1 .75.75v8.5a.75.75 0 0 1-1.5 0v-8.5A.75.75 0 0 1 14 3ZM5 4a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0v-6.5A.75.75 0 0 1 5 4Zm6 1a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 11 5ZM2 6a.75.75 0 0 1 .75.75v2.5a.75.75 0 0 1-1.5 0v-2.5A.75.75 0 0 1 2 6Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
),
mail: (
<SvgIcon>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</SvgIcon>
),
refresh: (
<SvgIcon>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M8 16H3v5" />
</SvgIcon>
),
shield: (
<SvgIcon>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
</SvgIcon>
),
'user-check': (
<SvgIcon>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<polyline points="16 11 18 13 22 9" />
</SvgIcon>
),
lock: (
<SvgIcon>
<rect x="3" y="11" width="18" height="11" rx="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</SvgIcon>
),
scroll: (
<SvgIcon>
<path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4" />
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
<path d="M15 8h-5M15 12h-5" />
</SvgIcon>
),
'list-checks': (
<SvgIcon>
<path d="M10 6h11M10 12h11M10 18h11" />
<path d="m3 6 1 1 2-2M3 12l1 1 2-2M3 18l1 1 2-2" />
</SvgIcon>
),
search: (
<SvgIcon>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</SvgIcon>
),
'file-text': (
<SvgIcon>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<line x1="10" y1="9" x2="8" y2="9" />
</SvgIcon>
),
chevron: (
<SvgIcon>
<path d="m9 18 6-6-6-6" />
</SvgIcon>
),
x: (
<SvgIcon>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</SvgIcon>
),
} as const; } as const;
type IconName = keyof typeof icons; type IconName = keyof typeof icons;
@@ -657,25 +383,25 @@ const StatsBar = memo(function StatsBar({ systemInfo, dashboardStats, loading }:
colorClass: 'text-success-400 bg-success-400/10 border-success-400/20', colorClass: 'text-success-400 bg-success-400/10 border-success-400/20',
}, },
{ {
icon: <StatBotIcon />, icon: <StatBotIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsBot'), label: t('admin.panel.statsBot'),
value: systemInfo?.bot_version ?? '--', value: systemInfo?.bot_version ?? '--',
colorClass: 'text-accent-400 bg-accent-400/10 border-accent-400/20', colorClass: 'text-accent-400 bg-accent-400/10 border-accent-400/20',
}, },
{ {
icon: <StatCabinetIcon />, icon: <StatCabinetIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsCabinet'), label: t('admin.panel.statsCabinet'),
value: `v${CABINET_VERSION}`, value: `v${CABINET_VERSION}`,
colorClass: 'text-accent-300 bg-accent-300/10 border-accent-300/20', colorClass: 'text-accent-300 bg-accent-300/10 border-accent-300/20',
}, },
{ {
icon: <StatTrialIcon />, icon: <StatTrialIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsTrials'), label: t('admin.panel.statsTrials'),
numericValue: trial, numericValue: trial,
colorClass: 'text-warning-400 bg-warning-400/10 border-warning-400/20', colorClass: 'text-warning-400 bg-warning-400/10 border-warning-400/20',
}, },
{ {
icon: <StatPaidIcon />, icon: <StatPaidIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsPaid'), label: t('admin.panel.statsPaid'),
numericValue: paid, numericValue: paid,
delta: purchasedToday > 0 ? `+${purchasedToday}` : undefined, delta: purchasedToday > 0 ? `+${purchasedToday}` : undefined,
@@ -912,19 +638,19 @@ export default function AdminPanel() {
cls: 'text-success-400', cls: 'text-success-400',
}, },
{ {
icon: <StatBotIcon />, icon: <StatBotIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsBot'), label: t('admin.panel.statsBot'),
value: systemInfo?.bot_version ?? '--', value: systemInfo?.bot_version ?? '--',
cls: 'text-accent-400', cls: 'text-accent-400',
}, },
{ {
icon: <StatTrialIcon />, icon: <StatTrialIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsTrials'), label: t('admin.panel.statsTrials'),
value: dashboardStats?.subscriptions.trial?.toLocaleString() ?? '--', value: dashboardStats?.subscriptions.trial?.toLocaleString() ?? '--',
cls: 'text-warning-400', cls: 'text-warning-400',
}, },
{ {
icon: <StatPaidIcon />, icon: <StatPaidIcon className="h-3.5 w-3.5" />,
label: t('admin.panel.statsPaid'), label: t('admin.panel.statsPaid'),
value: dashboardStats?.subscriptions.paid?.toLocaleString() ?? '--', value: dashboardStats?.subscriptions.paid?.toLocaleString() ?? '--',
delta: delta:

View File

@@ -5,20 +5,10 @@ import { useTranslation } from 'react-i18next';
import { partnerApi } from '../api/partners'; import { partnerApi } from '../api/partners';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { toNumber } from '../utils/inputHelpers'; import { toNumber } from '../utils/inputHelpers';
import { SettingsIcon } from '@/components/icons';
type NumberOrEmpty = number | ''; type NumberOrEmpty = number | '';
const SettingsIcon = () => (
<svg className="h-6 w-6" 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>
);
export default function AdminPartnerSettings() { export default function AdminPartnerSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -126,7 +116,7 @@ export default function AdminPartnerSettings() {
<div className="mb-6 flex items-center gap-3"> <div className="mb-6 flex items-center gap-3">
<AdminBackButton to="/admin/partners" /> <AdminBackButton to="/admin/partners" />
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<SettingsIcon /> <SettingsIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.partners.settings')}</h1> <h1 className="text-xl font-semibold text-dark-100">{t('admin.partners.settings')}</h1>

View File

@@ -7,33 +7,7 @@ import { METHOD_LABELS } from '../constants/paymentMethods';
import type { PromoGroupSimple } from '../types'; import type { PromoGroupSimple } from '../types';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
const BackIcon = () => ( import { BackIcon, CheckIcon, SaveIcon } from '@/components/icons';
<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>
);
const CheckIcon = () => (
<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>
);
const SaveIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
export default function AdminPaymentMethodEdit() { export default function AdminPaymentMethodEdit() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -462,7 +436,7 @@ export default function AdminPaymentMethodEdit() {
{updateMethodMutation.isPending ? ( {updateMethodMutation.isPending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<SaveIcon /> <SaveIcon className="h-4 w-4" />
)} )}
{t('admin.paymentMethods.saveButton')} {t('admin.paymentMethods.saveButton')}
</button> </button>

View File

@@ -23,43 +23,7 @@ import {
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods'; import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
import type { PaymentMethodConfig } from '../types'; import type { PaymentMethodConfig } from '../types';
const BackIcon = () => ( import { BackIcon, GripIcon, ChevronRightIcon, SaveIcon } from '@/components/icons';
<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>
);
const GripIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
const ChevronRightIcon = () => (
<svg className="h-5 w-5" 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>
);
const SaveIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
interface SortableCardProps { interface SortableCardProps {
config: PaymentMethodConfig; config: PaymentMethodConfig;
@@ -267,7 +231,7 @@ export default function AdminPaymentMethods() {
{saveOrderMutation.isPending ? ( {saveOrderMutation.isPending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<SaveIcon /> <SaveIcon className="h-4 w-4" />
)} )}
{t('admin.paymentMethods.saveOrder')} {t('admin.paymentMethods.saveOrder')}
</button> </button>

View File

@@ -6,41 +6,7 @@ import { adminPaymentsApi, type SearchStats } from '../api/adminPayments';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import type { PendingPayment, PaginatedResponse } from '../types'; import type { PendingPayment, PaginatedResponse } from '../types';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon, SearchIcon, CalendarIcon } from '@/components/icons';
// BackIcon
const BackIcon = () => (
<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>
);
// SearchIcon
const SearchIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
// CalendarIcon
const CalendarIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
interface StatusBadgeProps { interface StatusBadgeProps {
status: string; status: string;
@@ -338,7 +304,7 @@ export default function AdminPayments() {
: 'bg-dark-800 text-dark-300 hover:bg-dark-700' : 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`} }`}
> >
<CalendarIcon /> <CalendarIcon className="h-4 w-4" />
{t('admin.payments.periodCustom')} {t('admin.payments.periodCustom')}
</button> </button>

View File

@@ -8,60 +8,7 @@ import {
PinnedMessageUpdateRequest, PinnedMessageUpdateRequest,
} from '../api/adminPinnedMessages'; } from '../api/adminPinnedMessages';
import { AdminBackButton, Toggle } from '../components/admin'; import { AdminBackButton, Toggle } from '../components/admin';
import { PinIcon, XIcon, RefreshIcon, PhotoIcon, VideoIcon, SaveIcon } from '@/components/icons';
// Icons
const PinIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
const XIcon = () => (
<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>
);
const RefreshIcon = () => (
<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>
);
const PhotoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const VideoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
/>
</svg>
);
const SaveIcon = () => (
<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>
);
export default function AdminPinnedMessageCreate() { export default function AdminPinnedMessageCreate() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -231,7 +178,7 @@ export default function AdminPinnedMessageCreate() {
<AdminBackButton to="/admin/pinned-messages" /> <AdminBackButton to="/admin/pinned-messages" />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-error-500/20 p-2 text-error-400"> <div className="rounded-lg bg-error-500/20 p-2 text-error-400">
<PinIcon /> <PinIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-100"> <h1 className="text-xl font-bold text-dark-100">
@@ -290,7 +237,7 @@ export default function AdminPinnedMessageCreate() {
className="rounded-lg p-2 text-dark-400 hover:bg-dark-700 hover:text-error-400" className="rounded-lg p-2 text-dark-400 hover:bg-dark-700 hover:text-error-400"
disabled={isUploading} disabled={isUploading}
> >
<XIcon /> <XIcon className="h-5 w-5" />
</button> </button>
</div> </div>
{mediaPreview && ( {mediaPreview && (

View File

@@ -5,122 +5,21 @@ import { useTranslation } from 'react-i18next';
import { adminPinnedMessagesApi, PinnedMessageResponse } from '../api/adminPinnedMessages'; import { adminPinnedMessagesApi, PinnedMessageResponse } from '../api/adminPinnedMessages';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { useNativeDialog } from '../platform/hooks/useNativeDialog'; import { useNativeDialog } from '../platform/hooks/useNativeDialog';
import {
// Icons BroadcastIcon,
const PinIcon = () => ( CheckIcon,
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> EditIcon,
<path MenuIcon,
strokeLinecap="round" PhotoIcon,
strokeLinejoin="round" PinIcon,
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" PlusIcon,
/> RefreshIcon,
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" /> RepeatIcon,
</svg> TrashIcon,
); UnpinIcon,
VideoIcon,
const PlusIcon = () => ( XIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> } from '@/components/icons';
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
);
const RefreshIcon = () => (
<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>
);
const PhotoIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const VideoIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
/>
</svg>
);
const MenuIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
const RepeatIcon = () => (
<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 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3"
/>
</svg>
);
const BroadcastIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
/>
</svg>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const EditIcon = () => (
<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>
);
const CheckIcon = () => (
<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>
);
const XIcon = () => (
<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>
);
const UnpinIcon = () => (
<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>
);
// Message card component // Message card component
function PinnedMessageCard({ function PinnedMessageCard({
@@ -168,17 +67,21 @@ function PinnedMessageCard({
<span className="text-xs text-dark-400">#{message.id}</span> <span className="text-xs text-dark-400">#{message.id}</span>
{message.media_type && ( {message.media_type && (
<span className="text-dark-400"> <span className="text-dark-400">
{message.media_type === 'photo' ? <PhotoIcon /> : <VideoIcon />} {message.media_type === 'photo' ? (
<PhotoIcon className="h-4 w-4" />
) : (
<VideoIcon className="h-4 w-4" />
)}
</span> </span>
)} )}
{message.send_before_menu && ( {message.send_before_menu && (
<span className="text-dark-500" title={t('admin.pinnedMessages.sendBeforeMenu')}> <span className="text-dark-500" title={t('admin.pinnedMessages.sendBeforeMenu')}>
<MenuIcon /> <MenuIcon className="h-4 w-4" />
</span> </span>
)} )}
{message.send_on_every_start && ( {message.send_on_every_start && (
<span className="text-dark-500" title={t('admin.pinnedMessages.sendOnEveryStart')}> <span className="text-dark-500" title={t('admin.pinnedMessages.sendOnEveryStart')}>
<RepeatIcon /> <RepeatIcon className="h-4 w-4" />
</span> </span>
)} )}
</div> </div>
@@ -218,7 +121,7 @@ function PinnedMessageCard({
onClick={onUnpin} onClick={onUnpin}
className="flex items-center gap-1.5 rounded-lg bg-error-500/20 px-3 py-1.5 text-xs text-error-400 transition-colors hover:bg-error-500/30" className="flex items-center gap-1.5 rounded-lg bg-error-500/20 px-3 py-1.5 text-xs text-error-400 transition-colors hover:bg-error-500/30"
> >
<UnpinIcon /> <UnpinIcon className="h-4 w-4" />
{t('admin.pinnedMessages.unpinAll')} {t('admin.pinnedMessages.unpinAll')}
</button> </button>
</> </>
@@ -236,7 +139,7 @@ function PinnedMessageCard({
onClick={() => onBroadcast(message.id)} onClick={() => onBroadcast(message.id)}
className="flex items-center gap-1.5 rounded-lg bg-accent-500/20 px-3 py-1.5 text-xs text-accent-400 transition-colors hover:bg-accent-500/30" className="flex items-center gap-1.5 rounded-lg bg-accent-500/20 px-3 py-1.5 text-xs text-accent-400 transition-colors hover:bg-accent-500/30"
> >
<BroadcastIcon /> <BroadcastIcon className="h-4 w-4" />
{t('admin.pinnedMessages.broadcastToAll')} {t('admin.pinnedMessages.broadcastToAll')}
</button> </button>
@@ -245,7 +148,7 @@ function PinnedMessageCard({
onClick={() => onDelete(message.id)} onClick={() => onDelete(message.id)}
className="flex items-center gap-1.5 rounded-lg bg-error-500/20 px-3 py-1.5 text-xs text-error-400 transition-colors hover:bg-error-500/30" className="flex items-center gap-1.5 rounded-lg bg-error-500/20 px-3 py-1.5 text-xs text-error-400 transition-colors hover:bg-error-500/30"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
{t('admin.pinnedMessages.delete')} {t('admin.pinnedMessages.delete')}
</button> </button>
)} )}
@@ -349,7 +252,7 @@ export default function AdminPinnedMessages() {
<AdminBackButton /> <AdminBackButton />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-error-500/20 p-2 text-error-400"> <div className="rounded-lg bg-error-500/20 p-2 text-error-400">
<PinIcon /> <PinIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-100">{t('admin.pinnedMessages.title')}</h1> <h1 className="text-xl font-bold text-dark-100">{t('admin.pinnedMessages.title')}</h1>
@@ -383,7 +286,7 @@ export default function AdminPinnedMessages() {
) : messages.length === 0 ? ( ) : messages.length === 0 ? (
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400"> <div className="rounded-xl border border-dark-700 bg-dark-800/50 p-8 text-center text-dark-400">
<div className="mx-auto mb-2 w-fit"> <div className="mx-auto mb-2 w-fit">
<PinIcon /> <PinIcon className="h-6 w-6" />
</div> </div>
<p>{t('admin.pinnedMessages.empty')}</p> <p>{t('admin.pinnedMessages.empty')}</p>
</div> </div>

View File

@@ -6,94 +6,17 @@ import { rbacApi, AccessPolicy, AdminRole } from '@/api/rbac';
import { PermissionGate } from '@/components/auth/PermissionGate'; import { PermissionGate } from '@/components/auth/PermissionGate';
import { usePlatform } from '@/platform/hooks/usePlatform'; import { usePlatform } from '@/platform/hooks/usePlatform';
import { useFocusTrap } from '@/hooks/useFocusTrap'; import { useFocusTrap } from '@/hooks/useFocusTrap';
import {
const BackIcon = () => ( BackIcon,
<svg BoltIcon,
className="h-5 w-5 text-dark-400" CalendarIcon,
fill="none" ClockIcon,
viewBox="0 0 24 24" EditIcon,
stroke="currentColor" GlobeIcon,
strokeWidth={2} PlusIcon,
> ShieldIcon,
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" /> TrashIcon,
</svg> } from '@/components/icons';
);
const PlusIcon = () => (
<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>
);
const EditIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const ShieldIcon = () => (
<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 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
const ClockIcon = () => (
<svg className="h-4 w-4" 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>
);
const GlobeIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
);
const BoltIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
/>
</svg>
);
const CalendarIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
interface PolicyConditions { interface PolicyConditions {
time_range?: { start: string; end: string }; time_range?: { start: string; end: string };
@@ -218,7 +141,7 @@ export default function AdminPolicies() {
className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300" className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300"
title={t('admin.policies.conditions.timeRange')} title={t('admin.policies.conditions.timeRange')}
> >
<ClockIcon /> <ClockIcon className="h-4 w-4" />
{parsed.time_range.start}-{parsed.time_range.end} {parsed.time_range.start}-{parsed.time_range.end}
</span>, </span>,
); );
@@ -231,7 +154,7 @@ export default function AdminPolicies() {
className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300" className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300"
title={t('admin.policies.conditions.ipWhitelist')} title={t('admin.policies.conditions.ipWhitelist')}
> >
<GlobeIcon /> <GlobeIcon className="h-4 w-4" />
{t('admin.policies.conditions.ipCount', { count: parsed.ip_whitelist.length })} {t('admin.policies.conditions.ipCount', { count: parsed.ip_whitelist.length })}
</span>, </span>,
); );
@@ -244,7 +167,7 @@ export default function AdminPolicies() {
className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300" className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300"
title={t('admin.policies.conditions.rateLimit')} title={t('admin.policies.conditions.rateLimit')}
> >
<BoltIcon /> <BoltIcon className="h-4 w-4" />
{t('admin.policies.conditions.rateValue', { count: parsed.rate_limit })} {t('admin.policies.conditions.rateValue', { count: parsed.rate_limit })}
</span>, </span>,
); );
@@ -260,7 +183,7 @@ export default function AdminPolicies() {
className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300" className="inline-flex items-center gap-1 rounded bg-dark-700 px-1.5 py-0.5 text-xs text-dark-300"
title={t('admin.policies.conditions.weekdays')} title={t('admin.policies.conditions.weekdays')}
> >
<CalendarIcon /> <CalendarIcon className="h-4 w-4" />
{dayNames.join(', ')} {dayNames.join(', ')}
</span>, </span>,
); );
@@ -434,7 +357,7 @@ export default function AdminPolicies() {
className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 sm:flex-none" className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 sm:flex-none"
title={t('admin.policies.actions.delete')} title={t('admin.policies.actions.delete')}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</PermissionGate> </PermissionGate>
</div> </div>

View File

@@ -9,39 +9,7 @@ import {
PromoGroupUpdateRequest, PromoGroupUpdateRequest,
} from '../api/promocodes'; } from '../api/promocodes';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { PlusIcon, RefreshIcon, TrashIcon } from '@/components/icons';
// Icons
const PlusIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const RefreshIcon = () => (
<svg
className="h-4 w-4 animate-spin"
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>
);
interface PeriodDiscount { interface PeriodDiscount {
days: number | ''; days: number | '';
@@ -340,7 +308,7 @@ export default function AdminPromoGroupCreate() {
onClick={() => removePeriodDiscount(index)} onClick={() => removePeriodDiscount(index)}
className="p-1 text-dark-400 transition-colors hover:text-error-400" className="p-1 text-dark-400 transition-colors hover:text-error-400"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
))} ))}
@@ -429,7 +397,7 @@ export default function AdminPromoGroupCreate() {
disabled={!isValid || isLoading} disabled={!isValid || isLoading}
className="btn-primary flex items-center gap-2" className="btn-primary flex items-center gap-2"
> >
{isLoading && <RefreshIcon />} {isLoading && <RefreshIcon spinning />}
{isLoading ? t('admin.promoGroups.form.saving') : t('admin.promoGroups.form.save')} {isLoading ? t('admin.promoGroups.form.saving') : t('admin.promoGroups.form.save')}
</button> </button>
</div> </div>

View File

@@ -5,55 +5,7 @@ import { useTranslation } from 'react-i18next';
import { promocodesApi, PromoGroup } from '../api/promocodes'; import { promocodesApi, PromoGroup } from '../api/promocodes';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { useFocusTrap } from '../hooks/useFocusTrap'; import { useFocusTrap } from '../hooks/useFocusTrap';
import { BackIcon, PlusIcon, EditIcon, TrashIcon, UsersIcon } from '@/components/icons';
// Icons
const BackIcon = () => (
<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>
);
const PlusIcon = () => (
<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>
);
const EditIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const UsersIcon = () => (
<svg className="h-5 w-5" 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>
);
export default function AdminPromoGroups() { export default function AdminPromoGroups() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -208,7 +160,7 @@ export default function AdminPromoGroups() {
title={t('admin.promoGroups.actions.delete')} title={t('admin.promoGroups.actions.delete')}
disabled={group.is_default} disabled={group.is_default}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -12,59 +12,14 @@ import {
} from '../api/promoOffers'; } from '../api/promoOffers';
import { adminUsersApi, UserListItem } from '../api/adminUsers'; import { adminUsersApi, UserListItem } from '../api/adminUsers';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import {
// Icons SendIcon,
const SendIcon = () => ( CheckIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> UsersIcon,
<path UserIcon,
strokeLinecap="round" SearchIcon,
strokeLinejoin="round" CloseIcon,
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" } from '@/components/icons';
/>
</svg>
);
const CheckIcon = () => (
<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>
);
const UsersIcon = () => (
<svg className="h-5 w-5" 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>
);
const UserIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
);
const SearchIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const CloseIcon = () => (
<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>
);
const getOfferTypeIcon = (offerType: string): string => { const getOfferTypeIcon = (offerType: string): string => {
return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁'; return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁';
@@ -420,7 +375,7 @@ export default function AdminPromoOfferSend() {
onClick={handleClearUser} onClick={handleClearUser}
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-600 hover:text-dark-100" className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-600 hover:text-dark-100"
> >
<CloseIcon /> <CloseIcon className="h-4 w-4" />
</button> </button>
</div> </div>
) : ( ) : (
@@ -428,7 +383,7 @@ export default function AdminPromoOfferSend() {
<> <>
<div className="relative"> <div className="relative">
<div className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-dark-400"> <div className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-dark-400">
<SearchIcon /> <SearchIcon className="h-4 w-4" />
</div> </div>
<input <input
type="text" type="text"

View File

@@ -5,59 +5,7 @@ import { useQuery } from '@tanstack/react-query';
import i18n from '../i18n'; import i18n from '../i18n';
import { promoOffersApi, PromoOfferLog, OFFER_TYPE_CONFIG, OfferType } from '../api/promoOffers'; import { promoOffersApi, PromoOfferLog, OFFER_TYPE_CONFIG, OfferType } from '../api/promoOffers';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon, EditIcon, SendIcon, ClockIcon, UserIcon } from '@/components/icons';
// Icons
const BackIcon = () => (
<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>
);
const EditIcon = () => (
<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>
);
const SendIcon = () => (
<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>
);
const ClockIcon = () => (
<svg className="h-4 w-4" 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>
);
const UserIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
);
// Helper functions // Helper functions
const formatDateTime = (date: string | null): string => { const formatDateTime = (date: string | null): string => {
@@ -295,7 +243,7 @@ export default function AdminPromoOffers() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-dark-700"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-dark-700">
<UserIcon /> <UserIcon className="h-4 w-4" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="mb-1 flex flex-wrap items-center gap-2"> <div className="mb-1 flex flex-wrap items-center gap-2">
@@ -317,7 +265,7 @@ export default function AdminPromoOffers() {
</div> </div>
</div> </div>
<div className="pl-13 flex items-center gap-1 text-xs text-dark-500 sm:pl-0"> <div className="pl-13 flex items-center gap-1 text-xs text-dark-500 sm:pl-0">
<ClockIcon /> <ClockIcon className="h-4 w-4" />
{formatDateTime(log.created_at)} {formatDateTime(log.created_at)}
</div> </div>
</div> </div>

View File

@@ -13,35 +13,7 @@ import {
} from '../api/promocodes'; } from '../api/promocodes';
import { tariffsApi } from '../api/tariffs'; import { tariffsApi } from '../api/tariffs';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon, RefreshIcon } from '@/components/icons';
// Icons
const BackIcon = () => (
<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>
);
const RefreshIcon = () => (
<svg
className="h-4 w-4 animate-spin"
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>
);
export default function AdminPromocodeCreate() { export default function AdminPromocodeCreate() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -557,7 +529,7 @@ export default function AdminPromocodeCreate() {
disabled={!isValid() || isLoading} disabled={!isValid() || isLoading}
className="btn-primary flex items-center gap-2" className="btn-primary flex items-center gap-2"
> >
{isLoading && <RefreshIcon />} {isLoading && <RefreshIcon spinning />}
{isLoading ? t('admin.promocodes.form.saving') : t('admin.promocodes.form.save')} {isLoading ? t('admin.promocodes.form.saving') : t('admin.promocodes.form.save')}
</button> </button>
</div> </div>

View File

@@ -4,37 +4,7 @@ import { useTranslation } from 'react-i18next';
import i18n from '../i18n'; import i18n from '../i18n';
import { promocodesApi, PromoCodeType } from '../api/promocodes'; import { promocodesApi, PromoCodeType } from '../api/promocodes';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { EditIcon, ClockIcon, UserIcon } from '@/components/icons';
// Icons
const EditIcon = () => (
<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>
);
const ClockIcon = () => (
<svg className="h-4 w-4" 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>
);
const UserIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
);
// Helper functions // Helper functions
const getTypeLabel = (type: PromoCodeType): string => { const getTypeLabel = (type: PromoCodeType): string => {
@@ -253,7 +223,7 @@ export default function AdminPromocodeStats() {
{/* Usage History */} {/* Usage History */}
<div className="rounded-xl border border-dark-700 bg-dark-800 p-4"> <div className="rounded-xl border border-dark-700 bg-dark-800 p-4">
<h4 className="mb-4 flex items-center gap-2 font-medium text-dark-200"> <h4 className="mb-4 flex items-center gap-2 font-medium text-dark-200">
<ClockIcon /> <ClockIcon className="h-4 w-4" />
{t('admin.promocodes.stats.usageHistory')} {t('admin.promocodes.stats.usageHistory')}
</h4> </h4>
{promocode.recent_uses.length === 0 ? ( {promocode.recent_uses.length === 0 ? (
@@ -269,7 +239,7 @@ export default function AdminPromocodeStats() {
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-dark-500"> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-dark-500">
<UserIcon /> <UserIcon className="h-4 w-4" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-sm font-medium text-dark-200"> <div className="truncate text-sm font-medium text-dark-200">

View File

@@ -6,72 +6,15 @@ import i18n from '../i18n';
import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes'; import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { copyToClipboard } from '../utils/clipboard'; import { copyToClipboard } from '../utils/clipboard';
import {
// Icons BackIcon,
PlusIcon,
const BackIcon = () => ( EditIcon,
<svg TrashIcon,
className="h-5 w-5 text-dark-400" CheckIcon,
fill="none" CopyIcon,
viewBox="0 0 24 24" ChartIcon,
stroke="currentColor" } from '@/components/icons';
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);
const PlusIcon = () => (
<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>
);
const EditIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const CheckIcon = () => (
<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>
);
const CopyIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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.75H9a.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>
);
const ChartIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
);
// Helper functions // Helper functions
const getTypeLabel = (type: PromoCodeType): string => { const getTypeLabel = (type: PromoCodeType): string => {
@@ -298,7 +241,7 @@ export default function AdminPromocodes() {
className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 sm:flex-none" className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 sm:flex-none"
title={t('admin.promocodes.actions.delete')} title={t('admin.promocodes.actions.delete')}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -7,74 +7,15 @@ import { adminUsersApi, type UserListItem } from '@/api/adminUsers';
import { PermissionGate } from '@/components/auth/PermissionGate'; import { PermissionGate } from '@/components/auth/PermissionGate';
import { usePermissionStore } from '@/store/permissions'; import { usePermissionStore } from '@/store/permissions';
import { usePlatform } from '@/platform/hooks/usePlatform'; import { usePlatform } from '@/platform/hooks/usePlatform';
import {
// === Icons === BackIcon,
SearchIcon,
const BackIcon = () => ( ChevronDownIcon,
<svg ChevronLeftIcon,
className="h-5 w-5 text-dark-400" ChevronRightIcon,
fill="none" XCircleIcon,
viewBox="0 0 24 24" UserPlusIcon,
stroke="currentColor" } from '@/components/icons';
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);
const SearchIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const ChevronDownIcon = ({ className }: { className?: string }) => (
<svg
className={className || 'h-4 w-4'}
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>
);
const ChevronLeftIcon = () => (
<svg className="h-5 w-5" 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>
);
const ChevronRightIcon = () => (
<svg className="h-5 w-5" 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>
);
const XCircleIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
const UserPlusIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z"
/>
</svg>
);
// === Constants === // === Constants ===
@@ -134,7 +75,7 @@ function UserSearchDropdown({
className="shrink-0 text-dark-400 transition-colors hover:text-dark-200" className="shrink-0 text-dark-400 transition-colors hover:text-dark-200"
aria-label={t('admin.roleAssign.clearUser')} aria-label={t('admin.roleAssign.clearUser')}
> >
<XCircleIcon /> <XCircleIcon className="h-4 w-4" />
</button> </button>
</div> </div>
); );
@@ -647,7 +588,7 @@ export default function AdminRoleAssign() {
role: assignment.role_name, role: assignment.role_name,
})} })}
> >
<XCircleIcon /> <XCircleIcon className="h-4 w-4" />
</button> </button>
</PermissionGate> </PermissionGate>
)} )}

View File

@@ -4,20 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { rbacApi, PermissionSection, CreateRolePayload, UpdateRolePayload } from '@/api/rbac'; import { rbacApi, PermissionSection, CreateRolePayload, UpdateRolePayload } from '@/api/rbac';
import { AdminBackButton } from '@/components/admin'; import { AdminBackButton } from '@/components/admin';
import { ChevronDownIcon } from '@/components/icons';
// === Icons ===
const ChevronDownIcon = ({ className }: { className?: string }) => (
<svg
className={className || 'h-4 w-4'}
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>
);
// === Constants === // === Constants ===

View File

@@ -7,54 +7,7 @@ import { PermissionGate } from '@/components/auth/PermissionGate';
import { usePermissionStore } from '@/store/permissions'; import { usePermissionStore } from '@/store/permissions';
import { usePlatform } from '@/platform/hooks/usePlatform'; import { usePlatform } from '@/platform/hooks/usePlatform';
import { useFocusTrap } from '@/hooks/useFocusTrap'; import { useFocusTrap } from '@/hooks/useFocusTrap';
import { BackIcon, PlusIcon, EditIcon, TrashIcon, ShieldIcon } from '@/components/icons';
const BackIcon = () => (
<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>
);
const PlusIcon = () => (
<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>
);
const EditIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const ShieldIcon = () => (
<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 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
export default function AdminRoles() { export default function AdminRoles() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -234,7 +187,7 @@ export default function AdminRoles() {
className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 disabled:cursor-not-allowed disabled:opacity-40 sm:flex-none" className="flex-1 rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400 disabled:cursor-not-allowed disabled:opacity-40 sm:flex-none"
title={t('admin.roles.actions.delete')} title={t('admin.roles.actions.delete')}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</PermissionGate> </PermissionGate>
</div> </div>

View File

@@ -2,23 +2,18 @@ import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { serversApi, ServerListItem } from '../api/servers'; import { serversApi, ServerListItem } from '../api/servers';
import { SyncIcon, EditIcon, CheckIcon, XIcon, UsersIcon, GiftIcon } from '../components/icons'; import {
SyncIcon,
EditIcon,
CheckIcon,
XIcon,
UsersIcon,
GiftIcon,
BackIcon,
} from '../components/icons';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import Twemoji from 'react-twemoji'; import Twemoji from 'react-twemoji';
// BackIcon
const BackIcon = () => (
<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>
);
// Country flags (simple emoji mapping) // Country flags (simple emoji mapping)
import { getFlagEmoji as getCountryFlag } from '../utils/subscriptionHelpers'; import { getFlagEmoji as getCountryFlag } from '../utils/subscriptionHelpers';

View File

@@ -16,32 +16,7 @@ import { SettingsTab } from '../components/admin/SettingsTab';
import { SettingsTreeSidebar } from '../components/admin/SettingsTreeSidebar'; import { SettingsTreeSidebar } from '../components/admin/SettingsTreeSidebar';
import { SettingsMobileTabs } from '../components/admin/SettingsMobileTabs'; import { SettingsMobileTabs } from '../components/admin/SettingsMobileTabs';
import { SettingsSearchMobile, SettingsSearchResults } from '../components/admin/SettingsSearch'; import { SettingsSearchMobile, SettingsSearchResults } from '../components/admin/SettingsSearch';
import { BackIcon, ChevronRightIcon } from '@/components/icons';
// BackIcon
const BackIcon = () => (
<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>
);
// ChevronRight for breadcrumbs
const ChevronRightIcon = () => (
<svg
className="h-3.5 w-3.5 text-dark-600"
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>
);
// Settings that require SALES_MODE=tariffs to be visible // Settings that require SALES_MODE=tariffs to be visible
const TARIFF_MODE_SETTINGS = ['MULTI_TARIFF_ENABLED', 'MAX_ACTIVE_SUBSCRIPTIONS']; const TARIFF_MODE_SETTINGS = ['MULTI_TARIFF_ENABLED', 'MAX_ACTIVE_SUBSCRIPTIONS'];
@@ -298,7 +273,7 @@ export default function AdminSettings() {
> >
{t(`admin.settings.groups.${activeTreeInfo.group.id}`)} {t(`admin.settings.groups.${activeTreeInfo.group.id}`)}
</button> </button>
<ChevronRightIcon /> <ChevronRightIcon className="h-3.5 w-3.5" />
<span className="text-dark-300"> <span className="text-dark-300">
{t(`admin.settings.tree.${activeTreeInfo.child.id}`)} {t(`admin.settings.tree.${activeTreeInfo.child.id}`)}
</span> </span>

View File

@@ -14,75 +14,15 @@ import {
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import Twemoji from 'react-twemoji'; import Twemoji from 'react-twemoji';
import {
// Icons CalendarIcon,
const PlusIcon = () => ( CheckIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> InfinityIcon,
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> PlusIcon,
</svg> RefreshIcon,
); SunIcon,
TrashIcon,
const TrashIcon = () => ( } from '@/components/icons';
<svg className="h-4 w-4" 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>
);
const CheckIcon = () => (
<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>
);
const InfinityIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25"
/>
</svg>
);
const CalendarIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
const SunIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
);
const RefreshIcon = () => (
<svg
className="h-4 w-4 animate-spin"
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>
);
type TariffType = 'period' | 'daily' | null; type TariffType = 'period' | 'daily' | null;
@@ -352,7 +292,7 @@ export default function AdminTariffCreate() {
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="rounded-lg bg-accent-500/20 p-3 text-accent-400 group-hover:bg-accent-500/30"> <div className="rounded-lg bg-accent-500/20 p-3 text-accent-400 group-hover:bg-accent-500/30">
<CalendarIcon /> <CalendarIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h3 className="font-medium text-dark-100">{t('admin.tariffs.periodTariff')}</h3> <h3 className="font-medium text-dark-100">{t('admin.tariffs.periodTariff')}</h3>
@@ -366,7 +306,7 @@ export default function AdminTariffCreate() {
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="rounded-lg bg-warning-500/20 p-3 text-warning-400 group-hover:bg-warning-500/30"> <div className="rounded-lg bg-warning-500/20 p-3 text-warning-400 group-hover:bg-warning-500/30">
<SunIcon /> <SunIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h3 className="font-medium text-dark-100">{t('admin.tariffs.dailyTariff')}</h3> <h3 className="font-medium text-dark-100">{t('admin.tariffs.dailyTariff')}</h3>
@@ -392,7 +332,7 @@ export default function AdminTariffCreate() {
isDaily ? 'bg-warning-500/20 text-warning-400' : 'bg-accent-500/20 text-accent-400' isDaily ? 'bg-warning-500/20 text-warning-400' : 'bg-accent-500/20 text-accent-400'
}`} }`}
> >
{isDaily ? <SunIcon /> : <CalendarIcon />} {isDaily ? <SunIcon className="h-6 w-6" /> : <CalendarIcon className="h-6 w-6" />}
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-dark-100"> <h1 className="text-xl font-bold text-dark-100">
@@ -538,7 +478,7 @@ export default function AdminTariffCreate() {
<span className="text-dark-400">{t('admin.tariffs.gbUnit')}</span> <span className="text-dark-400">{t('admin.tariffs.gbUnit')}</span>
{(trafficLimitGb === 0 || trafficLimitGb === '') && ( {(trafficLimitGb === 0 || trafficLimitGb === '') && (
<span className="flex items-center gap-1 text-sm text-success-500"> <span className="flex items-center gap-1 text-sm text-success-500">
<InfinityIcon /> <InfinityIcon className="h-4 w-4" />
{t('admin.tariffs.unlimited')} {t('admin.tariffs.unlimited')}
</span> </span>
)} )}
@@ -686,7 +626,7 @@ export default function AdminTariffCreate() {
onClick={() => removePeriod(period.days)} onClick={() => removePeriod(period.days)}
className="rounded-lg p-2 text-dark-400 transition-colors hover:bg-error-500/20 hover:text-error-400" className="rounded-lg p-2 text-dark-400 transition-colors hover:bg-error-500/20 hover:text-error-400"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
))} ))}
@@ -1035,7 +975,7 @@ export default function AdminTariffCreate() {
}} }}
className="rounded-lg p-2 text-dark-400 transition-colors hover:bg-error-500/20 hover:text-error-400" className="rounded-lg p-2 text-dark-400 transition-colors hover:bg-error-500/20 hover:text-error-400"
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
))} ))}
@@ -1204,7 +1144,7 @@ export default function AdminTariffCreate() {
disabled={!isValid || isLoading} disabled={!isValid || isLoading}
className="btn-primary flex items-center gap-2" className="btn-primary flex items-center gap-2"
> >
{isLoading && <RefreshIcon />} {isLoading && <RefreshIcon spinning />}
{isLoading ? t('admin.tariffs.savingButton') : t('admin.tariffs.saveButton')} {isLoading ? t('admin.tariffs.savingButton') : t('admin.tariffs.saveButton')}
</button> </button>
</div> </div>

View File

@@ -21,87 +21,17 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import {
// Icons BackIcon,
const PlusIcon = () => ( CheckIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> EditIcon,
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> GiftIcon,
</svg> GripIcon,
); PlusIcon,
SaveIcon,
const EditIcon = () => ( TrashIcon,
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> XIcon,
<path } from '@/components/icons';
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>
);
const TrashIcon = () => (
<svg className="h-4 w-4" 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>
);
const CheckIcon = () => (
<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>
);
const XIcon = () => (
<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>
);
const GiftIcon = () => (
<svg className="h-4 w-4" 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>
);
const BackIcon = () => (
<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>
);
const GripIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
const SaveIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
// ============ Sortable Tariff Card ============ // ============ Sortable Tariff Card ============
@@ -257,7 +187,7 @@ function SortableTariffCard({
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400" className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400"
title={t('admin.tariffs.delete')} title={t('admin.tariffs.delete')}
> >
<TrashIcon /> <TrashIcon className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -403,7 +333,7 @@ export default function AdminTariffs() {
{saveOrderMutation.isPending ? ( {saveOrderMutation.isPending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<SaveIcon /> <SaveIcon className="h-4 w-4" />
)} )}
{t('admin.tariffs.saveOrder')} {t('admin.tariffs.saveOrder')}
</button> </button>

View File

@@ -4,21 +4,11 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { adminApi } from '../api/admin'; import { adminApi } from '../api/admin';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { SettingsIcon } from '@/components/icons';
import { toNumber } from '../utils/inputHelpers'; import { toNumber } from '../utils/inputHelpers';
type NumberOrEmpty = number | ''; type NumberOrEmpty = number | '';
const SettingsIcon = () => (
<svg className="h-6 w-6" 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>
);
export default function AdminTicketSettings() { export default function AdminTicketSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -132,7 +122,7 @@ export default function AdminTicketSettings() {
<div className="mb-6 flex items-center gap-3"> <div className="mb-6 flex items-center gap-3">
<AdminBackButton to="/admin/tickets" /> <AdminBackButton to="/admin/tickets" />
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400"> <div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
<SettingsIcon /> <SettingsIcon className="h-6 w-6" />
</div> </div>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tickets.settings')}</h1> <h1 className="text-xl font-semibold text-dark-100">{t('admin.tickets.settings')}</h1>

View File

@@ -9,6 +9,7 @@ import { adminApi, AdminTicket, AdminTicketDetail } from '../api/admin';
import { ticketsApi } from '../api/tickets'; import { ticketsApi } from '../api/tickets';
import { copyToClipboard as copyText } from '../utils/clipboard'; import { copyToClipboard as copyText } from '../utils/clipboard';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon } from '@/components/icons';
interface MediaAttachment { interface MediaAttachment {
id: string; id: string;
@@ -39,19 +40,6 @@ const ALLOWED_FILE_TYPES: Record<string, string> = {
const ACCEPT_STRING = Object.keys(ALLOWED_FILE_TYPES).join(','); const ACCEPT_STRING = Object.keys(ALLOWED_FILE_TYPES).join(',');
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
// BackIcon
const BackIcon = () => (
<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>
);
export default function AdminTickets() { export default function AdminTickets() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -768,7 +768,7 @@ export default function AdminTrafficUsage() {
{/* Threshold inputs */} {/* Threshold inputs */}
<div className="flex items-center gap-1.5 rounded-lg border border-dark-700 bg-dark-800 px-2 py-1"> <div className="flex items-center gap-1.5 rounded-lg border border-dark-700 bg-dark-800 px-2 py-1">
<ShieldIcon /> <ShieldIcon className="h-3.5 w-3.5" />
<input <input
type="number" type="number"
value={totalThreshold} value={totalThreshold}
@@ -784,12 +784,12 @@ export default function AdminTrafficUsage() {
onClick={() => setTotalThreshold('')} onClick={() => setTotalThreshold('')}
className="text-dark-500 hover:text-dark-300" className="text-dark-500 hover:text-dark-300"
> >
<XIcon /> <XIcon className="h-3 w-3" />
</button> </button>
)} )}
</div> </div>
<div className="flex items-center gap-1.5 rounded-lg border border-dark-700 bg-dark-800 px-2 py-1"> <div className="flex items-center gap-1.5 rounded-lg border border-dark-700 bg-dark-800 px-2 py-1">
<ServerSmallIcon /> <ServerSmallIcon className="h-3.5 w-3.5" />
<input <input
type="number" type="number"
value={nodeThreshold} value={nodeThreshold}
@@ -805,7 +805,7 @@ export default function AdminTrafficUsage() {
onClick={() => setNodeThreshold('')} onClick={() => setNodeThreshold('')}
className="text-dark-500 hover:text-dark-300" className="text-dark-500 hover:text-dark-300"
> >
<XIcon /> <XIcon className="h-3 w-3" />
</button> </button>
)} )}
</div> </div>

View File

@@ -4,96 +4,18 @@ import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { adminUpdatesApi, ReleaseItem, ProjectReleasesInfo } from '../api/adminUpdates'; import { adminUpdatesApi, ReleaseItem, ProjectReleasesInfo } from '../api/adminUpdates';
import {
BackIcon,
RefreshIcon,
BotIcon,
CabinetIcon,
TagIcon,
CalendarIcon,
ExternalLinkIcon,
} from '@/components/icons';
declare const __APP_VERSION__: string; declare const __APP_VERSION__: string;
// ============ Icons ============
const BackIcon = () => (
<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>
);
const RefreshIcon = () => (
<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.992 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.185M2.985 14.652"
/>
</svg>
);
const BotIcon = () => (
<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.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h9a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0015.75 4.5h-7.5A2.25 2.25 0 006 6.75v10.5A2.25 2.25 0 008.25 19.5z"
/>
</svg>
);
const CabinetIcon = () => (
<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 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z"
/>
</svg>
);
const TagIcon = () => (
<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="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6h.008v.008H6V6z" />
</svg>
);
const CalendarIcon = () => (
<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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
const ExternalLinkIcon = () => (
<svg className="h-4 w-4" 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>
);
// ============ Helpers ============ // ============ Helpers ============
function formatDate(iso: string): string { function formatDate(iso: string): string {
@@ -202,7 +124,7 @@ function ReleaseCard({ release }: { release: ReleaseItem }) {
<div className="border-b border-dark-700/30 px-4 py-3 last:border-b-0"> <div className="border-b border-dark-700/30 px-4 py-3 last:border-b-0">
<div className="mb-2 flex flex-wrap items-center gap-2"> <div className="mb-2 flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1.5 text-dark-200"> <div className="flex items-center gap-1.5 text-dark-200">
<TagIcon /> <TagIcon className="h-3.5 w-3.5" />
<span className="text-sm font-semibold">{release.tag_name}</span> <span className="text-sm font-semibold">{release.tag_name}</span>
</div> </div>
{release.name !== release.tag_name && ( {release.name !== release.tag_name && (
@@ -214,7 +136,7 @@ function ReleaseCard({ release }: { release: ReleaseItem }) {
</span> </span>
)} )}
<div className="flex items-center gap-1 text-dark-500 sm:ml-auto"> <div className="flex items-center gap-1 text-dark-500 sm:ml-auto">
<CalendarIcon /> <CalendarIcon className="h-3.5 w-3.5" />
<span className="text-xs">{formatDate(release.published_at)}</span> <span className="text-xs">{formatDate(release.published_at)}</span>
</div> </div>
</div> </div>
@@ -269,7 +191,7 @@ function ProjectSection({
className="flex items-center gap-1 rounded-lg border border-dark-700/50 px-2.5 py-1.5 text-xs text-dark-400 transition-colors hover:border-dark-600 hover:text-dark-300" className="flex items-center gap-1 rounded-lg border border-dark-700/50 px-2.5 py-1.5 text-xs text-dark-400 transition-colors hover:border-dark-600 hover:text-dark-300"
> >
GitHub GitHub
<ExternalLinkIcon /> <ExternalLinkIcon className="h-4 w-4" />
</a> </a>
</div> </div>

View File

@@ -18,6 +18,7 @@ import {
type SubscriptionRequestRecord, type SubscriptionRequestRecord,
} from '../api/adminUsers'; } from '../api/adminUsers';
import { promocodesApi, type PromoGroup } from '../api/promocodes'; import { promocodesApi, type PromoGroup } from '../api/promocodes';
import { RefreshIcon, TelegramSmallIcon as TelegramIcon } from '@/components/icons';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { GiftsTab } from '../components/admin/userDetail/GiftsTab'; import { GiftsTab } from '../components/admin/userDetail/GiftsTab';
import { SyncTab } from '../components/admin/userDetail/SyncTab'; import { SyncTab } from '../components/admin/userDetail/SyncTab';
@@ -33,23 +34,6 @@ import { usePermissionStore } from '../store/permissions';
// StatusBadge / GiftStatusBadge / GiftCard moved to // StatusBadge / GiftStatusBadge / GiftCard moved to
// components/admin/userDetail/{SubscriptionTab,GiftsTab,InfoTab,SyncTab}.tsx) // components/admin/userDetail/{SubscriptionTab,GiftsTab,InfoTab,SyncTab}.tsx)
// RefreshIcon stays here — the page header's reload button uses it.
const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} 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>
);
const TelegramIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<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>
);
// ============ Main Page ============ // ============ Main Page ============
export default function AdminUserDetail() { export default function AdminUserDetail() {

View File

@@ -5,56 +5,14 @@ import { useQuery } from '@tanstack/react-query';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { adminUsersApi, type UserListItem } from '../api/adminUsers'; import { adminUsersApi, type UserListItem } from '../api/adminUsers';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import {
const BackIcon = () => ( BackIcon,
<svg SearchIcon,
className="h-5 w-5 text-dark-400" ChevronLeftIcon,
fill="none" ChevronRightIcon,
viewBox="0 0 24 24" RefreshIcon,
stroke="currentColor" TelegramSmallIcon as TelegramIcon,
strokeWidth={2} } from '@/components/icons';
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);
const SearchIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
);
const ChevronLeftIcon = () => (
<svg className="h-5 w-5" 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>
);
const ChevronRightIcon = () => (
<svg className="h-5 w-5" 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>
);
const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => (
<svg className={className} 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>
);
const TelegramIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<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>
);
interface StatCardProps { interface StatCardProps {
title: string; title: string;

View File

@@ -26,149 +26,25 @@ import { useDestructiveConfirm } from '@/platform';
import { useNotify } from '@/platform/hooks/useNotify'; import { useNotify } from '@/platform/hooks/useNotify';
import FortuneWheel from '../components/wheel/FortuneWheel'; import FortuneWheel from '../components/wheel/FortuneWheel';
import { ColorPicker } from '@/components/ColorPicker'; import { ColorPicker } from '@/components/ColorPicker';
import {
AdjustmentsIcon,
BackIcon,
ChartIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
CogIcon,
GiftIcon,
GripVerticalIcon,
PlusIcon,
StarIcon,
TicketIcon,
TrashIcon,
XMarkIcon,
} from '@/components/icons';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { toNumber } from '../utils/inputHelpers'; import { toNumber } from '../utils/inputHelpers';
const BackIcon = () => (
<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>
);
const CogIcon = () => (
<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"
/>
</svg>
);
const GiftIcon = () => (
<svg className="h-5 w-5" 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>
);
const ChartIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
);
const PlusIcon = () => (
<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>
);
const TrashIcon = () => (
<svg className="h-5 w-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>
);
const GripVerticalIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 6h.01M8 12h.01M8 18h.01M16 6h.01M16 12h.01M16 18h.01"
/>
</svg>
);
const ChevronDownIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
);
const ChevronUpIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
</svg>
);
const XMarkIcon = () => (
<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>
);
const CheckIcon = () => (
<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>
);
const StarIcon = ({ className = 'h-4 w-4' }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
);
const AdjustmentsIcon = ({ className = 'h-4 w-4' }: { className?: string }) => (
<svg
className={className}
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>
);
const TicketIcon = ({ className = 'h-4 w-4' }: { className?: string }) => (
<svg
className={className}
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>
);
const PRIZE_TYPE_KEYS = [ const PRIZE_TYPE_KEYS = [
{ value: 'subscription_days', key: 'subscription_days', emoji: '📅' }, { value: 'subscription_days', key: 'subscription_days', emoji: '📅' },
{ value: 'balance_bonus', key: 'balance_bonus', emoji: '💰' }, { value: 'balance_bonus', key: 'balance_bonus', emoji: '💰' },
@@ -541,7 +417,7 @@ export default function AdminWheel() {
: 'text-dark-400 hover:text-dark-200' : 'text-dark-400 hover:text-dark-200'
}`} }`}
> >
<GiftIcon /> <GiftIcon className="h-5 w-5" />
{t('admin.wheel.tabs.prizes')} ({config.prizes.length}) {t('admin.wheel.tabs.prizes')} ({config.prizes.length})
</button> </button>
<button <button
@@ -552,7 +428,7 @@ export default function AdminWheel() {
: 'text-dark-400 hover:text-dark-200' : 'text-dark-400 hover:text-dark-200'
}`} }`}
> >
<ChartIcon /> <ChartIcon className="h-5 w-5" />
{t('admin.wheel.tabs.statistics')} {t('admin.wheel.tabs.statistics')}
</button> </button>
</div> </div>
@@ -834,7 +710,7 @@ export default function AdminWheel() {
{updateConfigMutation.isPending ? ( {updateConfigMutation.isPending ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<CheckIcon /> <CheckIcon className="h-5 w-5" />
)} )}
{t('common.save')} {t('common.save')}
</button> </button>
@@ -1285,7 +1161,7 @@ function InlinePrizeForm({
{isLoading ? ( {isLoading ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
) : ( ) : (
<CheckIcon /> <CheckIcon className="h-5 w-5" />
)} )}
{prize ? t('common.save') : t('common.confirm')} {prize ? t('common.save') : t('common.confirm')}
</button> </button>

View File

@@ -12,26 +12,10 @@ import type { PaginatedResponse, Transaction } from '../types';
import { Card } from '@/components/data-display/Card'; import { Card } from '@/components/data-display/Card';
import { Button } from '@/components/primitives/Button'; import { Button } from '@/components/primitives/Button';
import { ChevronDownIcon, ChevronRightIcon, CreditCardIcon } from '@/components/icons'; import { ChevronDownIcon, ChevronRightIcon, CreditCardIcon, WalletIcon } from '@/components/icons';
import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import { isPaidStatus, isFailedStatus } from '../utils/paymentStatus'; import { isPaidStatus, isFailedStatus } from '../utils/paymentStatus';
const WalletIcon = ({ className = 'h-8 w-8' }: { className?: string }) => (
<svg
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<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>
);
export default function Balance() { export default function Balance() {
const { t } = useTranslation(); const { t } = useTranslation();
const refreshUser = useAuthStore((state) => state.refreshUser); const refreshUser = useAuthStore((state) => state.refreshUser);

View File

@@ -2,26 +2,7 @@ import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { contestsApi, ContestInfo, ContestGameData } from '../api/contests'; import { contestsApi, ContestInfo, ContestGameData } from '../api/contests';
import { GamepadIcon, TrophyIcon } from '@/components/icons';
const GamepadIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.959.401v0a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.78.78 0 01.79-.869"
/>
</svg>
);
const TrophyIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 18.75h-9m9 0a3 3 0 013 3h-15a3 3 0 013-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 01-.982-3.172M9.497 14.25a7.454 7.454 0 00.981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 007.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 002.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 012.916.52 6.003 6.003 0 01-5.395 4.972m0 0a6.726 6.726 0 01-2.749 1.35m0 0a6.772 6.772 0 01-3.044 0"
/>
</svg>
);
export default function Contests() { export default function Contests() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -92,7 +73,7 @@ export default function Contests() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<GamepadIcon /> <GamepadIcon className="h-6 w-6" />
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('contests.title')}</h1> <h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('contests.title')}</h1>
</div> </div>
@@ -255,7 +236,7 @@ export default function Contests() {
</div> </div>
) : ( ) : (
<div className="card py-12 text-center"> <div className="card py-12 text-center">
<GamepadIcon /> <GamepadIcon className="h-6 w-6" />
<p className="mt-4 text-dark-400">{t('contests.noContests')}</p> <p className="mt-4 text-dark-400">{t('contests.noContests')}</p>
</div> </div>
)} )}

View File

@@ -21,12 +21,7 @@ import { promoApi } from '../api/promo';
import PendingGiftCard from '../components/dashboard/PendingGiftCard'; import PendingGiftCard from '../components/dashboard/PendingGiftCard';
import SubscriptionListCard from '../components/subscription/SubscriptionListCard'; import SubscriptionListCard from '../components/subscription/SubscriptionListCard';
import { API } from '../config/constants'; import { API } from '../config/constants';
import { ChevronRightIcon } from '@/components/icons';
const ChevronRightIcon = () => (
<svg className="h-5 w-5" 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>
);
export default function Dashboard() { export default function Dashboard() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,72 +1,16 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCaretDownDuotone } from 'react-icons/pi';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { infoApi, FaqPage } from '../api/info'; import { infoApi, FaqPage } from '../api/info';
import { infoPagesApi } from '../api/infoPages'; import { infoPagesApi } from '../api/infoPages';
import { promoApi, LoyaltyTierInfo } from '../api/promo'; import { promoApi, LoyaltyTierInfo } from '../api/promo';
import type { FaqItem, ReplacesTab } from '../api/infoPages'; import type { FaqItem, ReplacesTab } from '../api/infoPages';
import { DocumentIcon, InfoIcon, QuestionIcon, ShieldIcon, StarIcon } from '@/components/icons';
const InfoIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<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>
);
const QuestionIcon = () => (
<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.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
);
const DocumentIcon = () => (
<svg className="h-5 w-5" 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 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.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 00-9-9z"
/>
</svg>
);
const ShieldIcon = () => (
<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 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
);
const StarIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
);
const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg <PiCaretDownDuotone className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`} />
className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
); );
const BUILTIN_TABS = new Set<string>(['faq', 'rules', 'privacy', 'offer', 'loyalty']); const BUILTIN_TABS = new Set<string>(['faq', 'rules', 'privacy', 'offer', 'loyalty']);
@@ -860,7 +804,7 @@ export default function Info() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<InfoIcon /> <InfoIcon className="h-6 w-6" />
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('info.title')}</h1> <h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('info.title')}</h1>
</div> </div>

View File

@@ -3,23 +3,12 @@ import { useParams, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { BackIcon, SearchIcon } from '@/components/icons';
import { infoPagesApi } from '../api/infoPages'; import { infoPagesApi } from '../api/infoPages';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import type { FaqItem } from '../api/infoPages'; import type { FaqItem } from '../api/infoPages';
// Icons
const BackIcon = () => (
<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>
);
/** /**
* Sanitization config — same strict allowlist as NewsArticlePage. * Sanitization config — same strict allowlist as NewsArticlePage.
* All HTML content is sanitized with DOMPurify before rendering. * All HTML content is sanitized with DOMPurify before rendering.
@@ -171,31 +160,9 @@ function sanitizeHtml(html: string): string {
// --- FAQ Accordion --- // --- FAQ Accordion ---
const ChevronIcon = ({ open }: { open: boolean }) => ( const ChevronIcon = ({ open }: { open: boolean }) => (
<svg <PiCaretDownDuotone
className={`h-5 w-5 text-dark-400 transition-transform duration-300 ${open ? 'rotate-180' : ''}`} className={`h-5 w-5 text-dark-400 transition-transform duration-300 ${open ? '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>
);
const SearchIcon = () => (
<svg
className="h-4 w-4 text-dark-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<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>
); );
function FaqAccordionItem({ function FaqAccordionItem({
@@ -275,7 +242,7 @@ function FaqView({ items }: { items: FaqItem[] }) {
{items.length > 3 && ( {items.length > 3 && (
<div className="relative"> <div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-3 flex items-center"> <div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
<SearchIcon /> <SearchIcon className="h-4 w-4" />
</div> </div>
<input <input
type="text" type="text"

View File

@@ -6,39 +6,7 @@ import { motion } from 'framer-motion';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { newsApi } from '../api/news'; import { newsApi } from '../api/news';
import { usePlatform } from '../platform/hooks/usePlatform'; import { usePlatform } from '../platform/hooks/usePlatform';
import { BackIcon, ClockIcon, CalendarIcon } from '@/components/icons';
// Icons
const BackIcon = () => (
<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>
);
const ClockIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
const CalendarIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
/** /**
* Sanitizes HTML content using DOMPurify to prevent XSS attacks. * Sanitizes HTML content using DOMPurify to prevent XSS attacks.
@@ -386,12 +354,12 @@ export default function NewsArticlePage() {
<div className="mb-6 flex flex-wrap items-center gap-4 text-sm text-dark-400"> <div className="mb-6 flex flex-wrap items-center gap-4 text-sm text-dark-400">
{article.published_at && ( {article.published_at && (
<span className="inline-flex items-center gap-1.5 font-mono text-xs"> <span className="inline-flex items-center gap-1.5 font-mono text-xs">
<CalendarIcon /> <CalendarIcon className="h-4 w-4" />
{new Date(article.published_at).toLocaleDateString(i18n.language)} {new Date(article.published_at).toLocaleDateString(i18n.language)}
</span> </span>
)} )}
<span className="inline-flex items-center gap-1.5 font-mono text-xs"> <span className="inline-flex items-center gap-1.5 font-mono text-xs">
<ClockIcon /> <ClockIcon className="h-4 w-4" />
{article.read_time_minutes} {t('news.readTime')} {article.read_time_minutes} {t('news.readTime')}
</span> </span>
</div> </div>

View File

@@ -3,32 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { pollsApi, PollInfo, PollQuestion } from '../api/polls'; import { pollsApi, PollInfo, PollQuestion } from '../api/polls';
import { useFocusTrap } from '../hooks/useFocusTrap'; import { useFocusTrap } from '../hooks/useFocusTrap';
import { ClipboardIcon, GiftIcon, CheckIcon } from '@/components/icons';
const ClipboardIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" 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.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
/>
</svg>
);
const GiftIcon = () => (
<svg className="h-5 w-5" 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>
);
const CheckIcon = () => (
<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>
);
export default function Polls() { export default function Polls() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -131,7 +106,7 @@ export default function Polls() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ClipboardIcon /> <ClipboardIcon className="h-6 w-6" />
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('polls.title')}</h1> <h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('polls.title')}</h1>
</div> </div>
@@ -180,7 +155,7 @@ export default function Polls() {
{completionMessage && ( {completionMessage && (
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg bg-success-500/20 p-4 text-center text-success-400"> <div className="rounded-lg bg-success-500/20 p-4 text-center text-success-400">
<CheckIcon /> <CheckIcon className="h-5 w-5" />
<p className="mt-2 font-medium">{completionMessage.message}</p> <p className="mt-2 font-medium">{completionMessage.message}</p>
{completionMessage.reward && ( {completionMessage.reward && (
<p className="mt-1 text-sm"> <p className="mt-1 text-sm">
@@ -252,7 +227,7 @@ export default function Polls() {
</div> </div>
{poll.reward_amount && ( {poll.reward_amount && (
<div className="flex items-center gap-1 text-accent-400"> <div className="flex items-center gap-1 text-accent-400">
<GiftIcon /> <GiftIcon className="h-5 w-5" />
<span className="text-sm font-medium">+{poll.reward_amount}</span> <span className="text-sm font-medium">+{poll.reward_amount}</span>
</div> </div>
)} )}
@@ -261,7 +236,7 @@ export default function Polls() {
<div className="mt-4"> <div className="mt-4">
{poll.is_completed ? ( {poll.is_completed ? (
<button disabled className="btn-secondary w-full cursor-not-allowed opacity-50"> <button disabled className="btn-secondary w-full cursor-not-allowed opacity-50">
<CheckIcon /> <CheckIcon className="h-5 w-5" />
<span className="ml-2">{t('polls.completed')}</span> <span className="ml-2">{t('polls.completed')}</span>
</button> </button>
) : ( ) : (
@@ -275,7 +250,7 @@ export default function Polls() {
</div> </div>
) : ( ) : (
<div className="card py-12 text-center"> <div className="card py-12 text-center">
<ClipboardIcon /> <ClipboardIcon className="h-6 w-6" />
<p className="mt-4 text-dark-400">{t('polls.noPolls')}</p> <p className="mt-4 text-dark-400">{t('polls.noPolls')}</p>
</div> </div>
)} )}

View File

@@ -21,46 +21,7 @@ import { Card } from '@/components/data-display/Card';
import { Button } from '@/components/primitives/Button'; import { Button } from '@/components/primitives/Button';
import { Switch } from '@/components/primitives/Switch'; import { Switch } from '@/components/primitives/Switch';
import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import { CopyIcon, CheckIcon, ShareIcon, ArrowRightIcon, PencilIcon } from '@/components/icons';
// Icons
const CopyIcon = () => (
<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.75H9a.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>
);
const CheckIcon = () => (
<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>
);
const ShareIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8l5-5m0 0l5 5m-5-5v12" />
<path strokeLinecap="round" strokeLinejoin="round" d="M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3" />
</svg>
);
const ArrowRightIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
);
const PencilIcon = () => (
<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.931z"
/>
</svg>
);
export default function Profile() { export default function Profile() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -382,7 +343,7 @@ export default function Profile() {
className="flex items-center gap-1 text-accent-400 transition-colors hover:text-accent-300" className="flex items-center gap-1 text-accent-400 transition-colors hover:text-accent-300"
> >
<span className="text-sm">{t('referral.title')}</span> <span className="text-sm">{t('referral.title')}</span>
<ArrowRightIcon /> <ArrowRightIcon className="h-4 w-4" />
</Link> </Link>
</div> </div>
<div className="flex flex-col gap-3 sm:flex-row"> <div className="flex flex-col gap-3 sm:flex-row">
@@ -401,7 +362,7 @@ export default function Profile() {
</span> </span>
</Button> </Button>
<Button onClick={shareReferralLink} variant="secondary"> <Button onClick={shareReferralLink} variant="secondary">
<ShareIcon /> <ShareIcon className="h-4 w-4" />
<span className="ml-2 hidden sm:inline">{t('referral.shareButton')}</span> <span className="ml-2 hidden sm:inline">{t('referral.shareButton')}</span>
</Button> </Button>
</div> </div>

View File

@@ -10,69 +10,15 @@ import { partnerApi } from '../api/partners';
import { withdrawalApi } from '../api/withdrawals'; import { withdrawalApi } from '../api/withdrawals';
import { CampaignCard } from '../components/partner/CampaignCard'; import { CampaignCard } from '../components/partner/CampaignCard';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import {
const LinkIcon = () => ( CheckIcon,
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> ClockIcon,
<path CopyIcon,
strokeLinecap="round" LinkIcon,
strokeLinejoin="round" PartnerIcon,
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" ShareIcon,
/> WalletIcon,
</svg> } from '@/components/icons';
);
const CopyIcon = () => (
<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.75H9a.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>
);
const CheckIcon = () => (
<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>
);
const ShareIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8l5-5m0 0l5 5m-5-5v12" />
<path strokeLinecap="round" strokeLinejoin="round" d="M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3" />
</svg>
);
const PartnerIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z"
/>
</svg>
);
const ClockIcon = () => (
<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 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
const WalletIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"
/>
</svg>
);
function getWithdrawalStatusBadge(status: string): string { function getWithdrawalStatusBadge(status: string): string {
switch (status) { switch (status) {
@@ -393,7 +339,7 @@ export default function Referral() {
!referralLink ? 'cursor-not-allowed opacity-50' : '' !referralLink ? 'cursor-not-allowed opacity-50' : ''
}`} }`}
> >
<ShareIcon /> <ShareIcon className="h-4 w-4" />
<span className="ml-2">{t('referral.shareButton')}</span> <span className="ml-2">{t('referral.shareButton')}</span>
</button> </button>
</div> </div>
@@ -495,7 +441,7 @@ export default function Referral() {
<div className="bento-card"> <div className="bento-card">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-accent-500/10 text-accent-400"> <div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-accent-500/10 text-accent-400">
<PartnerIcon /> <PartnerIcon className="h-8 w-8" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h2 className="text-lg font-semibold text-dark-100"> <h2 className="text-lg font-semibold text-dark-100">
@@ -546,7 +492,7 @@ export default function Referral() {
<div className="bento-card border-success-500/20"> <div className="bento-card border-success-500/20">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-success-500/10 text-success-400"> <div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-success-500/10 text-success-400">
<PartnerIcon /> <PartnerIcon className="h-8 w-8" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -638,7 +584,7 @@ export default function Referral() {
<div className="bento-card"> <div className="bento-card">
<div className="mb-4 flex items-center gap-3"> <div className="mb-4 flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-500/10 text-accent-400"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-500/10 text-accent-400">
<WalletIcon /> <WalletIcon className="h-8 w-8" />
</div> </div>
<h2 className="text-lg font-semibold text-dark-100"> <h2 className="text-lg font-semibold text-dark-100">
{t('referral.withdrawal.title')} {t('referral.withdrawal.title')}

View File

@@ -12,43 +12,12 @@ import type { TicketDetail } from '../types';
import { Card } from '@/components/data-display/Card'; import { Card } from '@/components/data-display/Card';
import { Button } from '@/components/primitives/Button'; import { Button } from '@/components/primitives/Button';
import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import { CloseIcon, ImageIcon, PlusIcon, SendIcon } from '@/components/icons';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
import { linkifyText } from '../utils/linkify'; import { linkifyText } from '../utils/linkify';
const log = logger.createLogger('Support'); const log = logger.createLogger('Support');
const PlusIcon = () => (
<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>
);
const SendIcon = () => (
<svg className="h-4 w-4" 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>
);
const ImageIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
const CloseIcon = () => (
<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>
);
// Media attachment state // Media attachment state
interface MediaAttachment { interface MediaAttachment {
id: string; id: string;
@@ -360,7 +329,7 @@ export default function Support() {
onClick={() => onRemove(idx)} onClick={() => onRemove(idx)}
className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-dark-600 text-dark-300 hover:bg-error-500 hover:text-white" className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-dark-600 text-dark-300 hover:bg-error-500 hover:text-white"
> >
<CloseIcon /> <CloseIcon className="h-4 w-4" />
</button> </button>
</div> </div>
))} ))}
@@ -593,7 +562,7 @@ export default function Support() {
disabled={createAttachments.some((a) => a.uploading)} disabled={createAttachments.some((a) => a.uploading)}
loading={createMutation.isPending} loading={createMutation.isPending}
> >
<SendIcon /> <SendIcon className="h-4 w-4" />
<span className="ml-2">{t('support.send')}</span> <span className="ml-2">{t('support.send')}</span>
</Button> </Button>
<Button <Button
@@ -745,7 +714,7 @@ export default function Support() {
} }
loading={replyMutation.isPending} loading={replyMutation.isPending}
> >
<SendIcon /> <SendIcon className="h-4 w-4" />
</Button> </Button>
</div> </div>
{rateLimitError && ( {rateLimitError && (

View File

@@ -15,65 +15,15 @@ import BentoCard from '../components/ui/BentoCard';
import { saveTopUpPendingInfo } from '../utils/topUpStorage'; import { saveTopUpPendingInfo } from '../utils/topUpStorage';
import { getSafeRedirectPath } from '../utils/safeRedirect'; import { getSafeRedirectPath } from '../utils/safeRedirect';
import { copyToClipboard } from '@/utils/clipboard'; import { copyToClipboard } from '@/utils/clipboard';
import {
// Icons CardIcon,
const StarIcon = () => ( CheckIcon,
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"> CopyIcon,
<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" /> CryptoIcon,
</svg> ExternalLinkIcon,
); SparklesIcon,
StarIcon,
const CardIcon = () => ( } from '@/components/icons';
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
/>
</svg>
);
const CryptoIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"
/>
</svg>
);
const SparklesIcon = () => (
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<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.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 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" />
</svg>
);
const ExternalLinkIcon = () => (
<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>
);
const CopyIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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.75H9a.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>
);
const CheckIcon = () => (
<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>
);
const getMethodIcon = (methodId: string) => { const getMethodIcon = (methodId: string) => {
const id = methodId.toLowerCase(); const id = methodId.toLowerCase();
@@ -519,7 +469,7 @@ export default function TopUpAmount() {
<span className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <span className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : ( ) : (
<> <>
<SparklesIcon /> <SparklesIcon className="h-4 w-4" />
<span>{t('balance.topUp')}</span> <span>{t('balance.topUp')}</span>
</> </>
)} )}
@@ -594,7 +544,7 @@ export default function TopUpAmount() {
className="space-y-3 rounded-2xl border border-success-500/20 bg-success-500/10 p-4" className="space-y-3 rounded-2xl border border-success-500/20 bg-success-500/10 p-4"
> >
<div className="flex items-center gap-2 text-success-400"> <div className="flex items-center gap-2 text-success-400">
<CheckIcon /> <CheckIcon className="h-5 w-5" />
<span className="font-semibold">{t('balance.paymentReady')}</span> <span className="font-semibold">{t('balance.paymentReady')}</span>
</div> </div>
@@ -623,7 +573,7 @@ export default function TopUpAmount() {
}`} }`}
title={t('common.copy')} title={t('common.copy')}
> >
{copied ? <CheckIcon /> : <CopyIcon />} {copied ? <CheckIcon className="h-5 w-5" /> : <CopyIcon className="h-5 w-5" />}
</button> </button>
</div> </div>
</motion.div> </motion.div>

View File

@@ -10,50 +10,15 @@ import { Card } from '@/components/data-display/Card/Card';
import { Button } from '@/components/primitives/Button/Button'; import { Button } from '@/components/primitives/Button/Button';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import { PiCaretDownDuotone } from 'react-icons/pi';
import { StarIcon, CalendarIcon, HistoryIcon, CloseIcon } from '@/components/icons';
import { cn } from '@/lib/utils';
// Icons // Icons
const StarIcon = () => (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
<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>
);
const CalendarIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
const HistoryIcon = () => (
<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 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
const CloseIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg <PiCaretDownDuotone
className={`h-5 w-5 transition-transform duration-200 ${expanded ? 'rotate-180' : ''}`} className={cn('h-5 w-5 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 9l-7 7-7-7" />
</svg>
); );
/** /**
@@ -732,7 +697,7 @@ export default function Wheel() {
onClick={closeResultModal} onClick={closeResultModal}
className="shrink-0 rounded-lg p-2 text-dark-400 transition-colors hover:bg-white/5 hover:text-dark-200" className="shrink-0 rounded-lg p-2 text-dark-400 transition-colors hover:bg-white/5 hover:text-dark-200"
> >
<CloseIcon /> <CloseIcon className="h-6 w-6" />
</button> </button>
</div> </div>