mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
// ──────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
85
src/components/icons/editor-icons.tsx
Normal file
85
src/components/icons/editor-icons.tsx
Normal 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)} />
|
||||||
|
);
|
||||||
415
src/components/icons/extended-icons.tsx
Normal file
415
src/components/icons/extended-icons.tsx
Normal 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)} />
|
||||||
|
);
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 ===
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' };
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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 ============
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 ===
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user