mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Merge pull request #445 from BEDOLAGA-DEV/dev
Релиз: миграция иконок, /admin/remnawave, колесо, флаги, подарки
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -55,6 +55,7 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-router": "^7.13.0",
|
||||
"react-twemoji": "^0.7.2",
|
||||
"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": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-router": "^7.13.0",
|
||||
"react-twemoji": "^0.7.2",
|
||||
"recharts": "^3.7.0",
|
||||
|
||||
BIN
public/fonts/TwemojiCountryFlags.woff2
Normal file
BIN
public/fonts/TwemojiCountryFlags.woff2
Normal file
Binary file not shown.
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { BrowserRouter, useLocation, useNavigate } from 'react-router';
|
||||
import { BrowserRouter, useLocation, useNavigate, useNavigationType } from 'react-router';
|
||||
import {
|
||||
showBackButton,
|
||||
hideBackButton,
|
||||
onBackButtonClick,
|
||||
offBackButtonClick,
|
||||
} from '@telegram-apps/sdk-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Twemoji from 'react-twemoji';
|
||||
import App from './App';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
@@ -15,7 +16,8 @@ import { WebSocketProvider } from './providers/WebSocketProvider';
|
||||
import { ToastProvider } from './components/Toast';
|
||||
import { TooltipProvider } from './components/primitives/Tooltip';
|
||||
import { isInTelegramWebApp } from './hooks/useTelegramSDK';
|
||||
import { hasInAppHistory, getFallbackParentPath } from './utils/navigation';
|
||||
import { getFallbackParentPath } from './utils/navigation';
|
||||
import { subscriptionApi } from './api/subscription';
|
||||
|
||||
const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const;
|
||||
|
||||
@@ -26,35 +28,98 @@ const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as
|
||||
/** Pages reachable from bottom nav — treat as top-level (no back button). */
|
||||
const BOTTOM_NAV_PATHS = ['/', '/subscriptions', '/balance', '/referral', '/support', '/wheel'];
|
||||
|
||||
/** Matches /subscriptions/:numericId. Single-tariff users land here straight
|
||||
* from bot deep-links, and their /subscriptions list auto-redirects right back
|
||||
* to this page (Subscriptions.tsx). So on a genuine deep-link entry (in-app
|
||||
* navigation depth 0) we hide the back button and let Telegram surface its
|
||||
* native Close (X); when there IS in-app history we show it and navigate back. */
|
||||
const SUBSCRIPTION_DETAIL_RE = /^\/subscriptions\/\d+\/?$/;
|
||||
|
||||
function TelegramBackButton() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const navType = useNavigationType();
|
||||
const navigateRef = useRef(navigate);
|
||||
navigateRef.current = navigate;
|
||||
const pathnameRef = useRef(location.pathname);
|
||||
pathnameRef.current = location.pathname;
|
||||
|
||||
// Reliable in-app navigation depth (the app's entry point is 0). Driven by
|
||||
// React Router's navigation TYPE — NOT window.history.state.idx, which the
|
||||
// app's own redirects mutate unpredictably and which is the root flake behind
|
||||
// issue #436 (the back button shows/acts on the wrong state). PUSH goes
|
||||
// deeper, POP unwinds, REPLACE (e.g. the Subscriptions.tsx auto-redirect) is
|
||||
// flat. De-duped by location.key so StrictMode's double-effect can't miscount.
|
||||
const depthRef = useRef(0);
|
||||
const lastKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (lastKeyRef.current === location.key) return;
|
||||
lastKeyRef.current = location.key;
|
||||
if (navType === 'PUSH') depthRef.current += 1;
|
||||
else if (navType === 'POP') depthRef.current = Math.max(0, depthRef.current - 1);
|
||||
// REPLACE: depth unchanged (replaces the current entry, adds no history)
|
||||
}, [location.key, navType]);
|
||||
|
||||
// Share the subscriptions-list query with the page-level components.
|
||||
// React Query dedupes by key so this does not cause an extra fetch when
|
||||
// Subscriptions/Subscription/Dashboard pages mount.
|
||||
const { data: subData } = useQuery({
|
||||
queryKey: ['subscriptions-list'],
|
||||
queryFn: () => subscriptionApi.getSubscriptions(),
|
||||
staleTime: 30_000,
|
||||
// Don't fetch outside Telegram — the cabinet still loads on the web.
|
||||
enabled: isInTelegramWebApp(),
|
||||
});
|
||||
const isMultiTariff = subData?.multi_tariff_enabled ?? false;
|
||||
|
||||
// Refs so the stable back handler (memoised with []) reads fresh values
|
||||
// without re-subscribing — re-subscription lets a component's local handler
|
||||
// overwrite ours via Telegram's singleton onBackButtonClick (issue #436).
|
||||
const isMultiTariffRef = useRef(isMultiTariff);
|
||||
isMultiTariffRef.current = isMultiTariff;
|
||||
const subsCountRef = useRef(subData?.subscriptions?.length ?? 0);
|
||||
subsCountRef.current = subData?.subscriptions?.length ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname);
|
||||
const isSingleTariffDetailDeepLink =
|
||||
!isMultiTariff && SUBSCRIPTION_DETAIL_RE.test(location.pathname) && depthRef.current === 0;
|
||||
try {
|
||||
if (isTopLevel) {
|
||||
if (isTopLevel || isSingleTariffDetailDeepLink) {
|
||||
hideBackButton();
|
||||
} else {
|
||||
showBackButton();
|
||||
}
|
||||
} catch {}
|
||||
}, [location]);
|
||||
}, [location, isMultiTariff]);
|
||||
|
||||
// Stable handler — ref prevents re-subscription on every render
|
||||
const handler = useCallback(() => {
|
||||
// When opened via a bot deep-link directly on a nested route, there is no
|
||||
// in-app history and navigate(-1) is a no-op — the back button looks dead.
|
||||
// Fall back to the parent route so it always navigates somewhere sensible.
|
||||
if (hasInAppHistory()) {
|
||||
// Real in-app history (depth > 0): a normal back. Otherwise we were opened
|
||||
// directly on this route via a deep-link — navigate(-1) is a no-op, so fall
|
||||
// back to a sensible parent route instead.
|
||||
if (depthRef.current > 0) {
|
||||
navigateRef.current(-1);
|
||||
} else {
|
||||
navigateRef.current(getFallbackParentPath(pathnameRef.current), { replace: true });
|
||||
return;
|
||||
}
|
||||
// /subscriptions/:id is special: the /subscriptions list auto-redirects
|
||||
// straight back to a detail page when single-tariff with exactly one
|
||||
// subscription (Subscriptions.tsx), so falling back there loops silently and
|
||||
// the back button looks dead (issue #436). Land on the list ONLY when it is
|
||||
// PROVABLY safe (multi-tariff, or more than one subscription — neither of
|
||||
// which auto-redirects); otherwise escape to root.
|
||||
//
|
||||
// Fail-closed on purpose: `subsCount <= 1` is treated as not-safe, which
|
||||
// also covers the stale default 0 before the shared subscriptions query
|
||||
// resolves — so a fast tap on a cold cache can never route into the
|
||||
// redirecting list and re-open the loop.
|
||||
const pathname = pathnameRef.current;
|
||||
const listIsSafe = isMultiTariffRef.current || subsCountRef.current > 1;
|
||||
const fallback =
|
||||
SUBSCRIPTION_DETAIL_RE.test(pathname) && !listIsSafe
|
||||
? '/'
|
||||
: getFallbackParentPath(pathnameRef.current);
|
||||
navigateRef.current(fallback, { replace: true });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface LocalizedText {
|
||||
}
|
||||
|
||||
export const adminAppsApi = {
|
||||
// Get RemnaWave config status
|
||||
// Get Remnawave config status
|
||||
getRemnaWaveStatus: async (): Promise<{ enabled: boolean; config_uuid: string | null }> => {
|
||||
const response = await apiClient.get<{ enabled: boolean; config_uuid: string | null }>(
|
||||
'/cabinet/admin/apps/remnawave/status',
|
||||
@@ -17,7 +17,7 @@ export const adminAppsApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Set RemnaWave config UUID
|
||||
// Set Remnawave config UUID
|
||||
setRemnaWaveUuid: async (
|
||||
uuid: string | null,
|
||||
): Promise<{ enabled: boolean; config_uuid: string | null }> => {
|
||||
@@ -28,7 +28,7 @@ export const adminAppsApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// List available RemnaWave configs
|
||||
// List available Remnawave configs
|
||||
listRemnaWaveConfigs: async (): Promise<
|
||||
{ uuid: string; name: string; view_position: number }[]
|
||||
> => {
|
||||
@@ -38,7 +38,7 @@ export const adminAppsApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get RemnaWave subscription config
|
||||
// Get Remnawave subscription config
|
||||
getRemnaWaveConfig: async (): Promise<RemnawaveConfig> => {
|
||||
const response = await apiClient.get<{
|
||||
uuid: string;
|
||||
@@ -50,7 +50,7 @@ export const adminAppsApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// ============== RemnaWave Format Types ==============
|
||||
// ============== Remnawave Format Types ==============
|
||||
|
||||
export interface RemnawaveButton {
|
||||
url: string;
|
||||
|
||||
@@ -21,12 +21,57 @@ export interface SystemSummary {
|
||||
total_users: number;
|
||||
active_connections: number;
|
||||
nodes_online: number;
|
||||
total_nodes: number;
|
||||
users_last_day: number;
|
||||
users_last_week: number;
|
||||
users_never_online: number;
|
||||
total_user_traffic: number;
|
||||
}
|
||||
|
||||
// Panel recap / devices / top consumers
|
||||
export interface RecapResponse {
|
||||
version: string | null;
|
||||
init_date: string | null;
|
||||
total: {
|
||||
users: number;
|
||||
nodes: number;
|
||||
traffic_bytes: number;
|
||||
nodes_ram_bytes: number;
|
||||
nodes_cpu_cores: number;
|
||||
distinct_countries: number;
|
||||
};
|
||||
this_month: { users: number; traffic_bytes: number };
|
||||
}
|
||||
|
||||
export interface DevicesStatsResponse {
|
||||
by_platform: { platform: string; count: number }[];
|
||||
by_app: { app: string; count: number }[];
|
||||
top_users: { username: string; devices_count: number }[];
|
||||
total_unique_devices: number;
|
||||
total_hwid_devices: number;
|
||||
average_devices_per_user: number;
|
||||
}
|
||||
|
||||
export interface TopConsumersResponse {
|
||||
period_days: number;
|
||||
users: { username: string; total_bytes: number }[];
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
instances: number;
|
||||
rss_bytes: number;
|
||||
heap_used_bytes: number;
|
||||
heap_total_bytes: number;
|
||||
event_loop_delay_ms: number;
|
||||
event_loop_p99_ms: number;
|
||||
uptime_seconds: number;
|
||||
instance_id: string | null;
|
||||
}
|
||||
|
||||
export interface SubscriptionRequestStatsResponse {
|
||||
by_app: { app: string; count: number }[];
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
cpu_cores: number;
|
||||
memory_total: number;
|
||||
@@ -89,6 +134,8 @@ export interface NodeInfo {
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
provider_uuid?: string;
|
||||
provider_name?: string | null;
|
||||
provider_favicon?: string | null;
|
||||
versions?: { xray: string; node: string } | null;
|
||||
system?: {
|
||||
info: {
|
||||
@@ -292,6 +339,34 @@ export const adminRemnawaveApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Panel recap / devices / top consumers
|
||||
getRecap: async (): Promise<RecapResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/recap');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getDevicesStats: async (): Promise<DevicesStatsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/devices-stats');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTopConsumers: async (days = 7, limit = 10): Promise<TopConsumersResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/top-consumers', {
|
||||
params: { days, limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getHealth: async (): Promise<HealthResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/health');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getSubscriptionRequests: async (): Promise<SubscriptionRequestStatsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/subscription-requests');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Nodes
|
||||
getNodes: async (): Promise<NodesListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/nodes');
|
||||
|
||||
@@ -698,7 +698,7 @@ export const adminUsersApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get subscription request history from RemnaWave panel
|
||||
// Get subscription request history from Remnawave panel
|
||||
getSubscriptionRequestHistory: async (
|
||||
userId: number,
|
||||
subscriptionId?: number,
|
||||
|
||||
@@ -134,7 +134,7 @@ export const serversApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Sync servers with RemnaWave
|
||||
// Sync servers with Remnawave
|
||||
syncServers: async (): Promise<ServerSyncResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/servers/sync');
|
||||
return response.data;
|
||||
|
||||
@@ -87,7 +87,7 @@ export interface TariffDetail {
|
||||
daily_price_kopeks: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'MONTH_ROLLING', 'NO_RESET', null = глобальная настройка
|
||||
// Внешний сквад RemnaWave
|
||||
// Внешний сквад Remnawave
|
||||
external_squad_uuid: string | null;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
@@ -126,7 +126,7 @@ export interface TariffCreateRequest {
|
||||
daily_price_kopeks?: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode?: string | null;
|
||||
// Внешний сквад RemnaWave
|
||||
// Внешний сквад Remnawave
|
||||
external_squad_uuid?: string | null;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export interface TariffUpdateRequest {
|
||||
daily_price_kopeks?: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode?: string | null;
|
||||
// Внешний сквад RemnaWave
|
||||
// Внешний сквад Remnawave
|
||||
external_squad_uuid?: string | null;
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ export const tariffsApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available external squads from RemnaWave
|
||||
// Get available external squads from Remnawave
|
||||
getAvailableExternalSquads: async (): Promise<ExternalSquadInfo[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tariffs/available-external-squads');
|
||||
return response.data;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { InfoIcon, WalletIcon, PlusIcon } from '@/components/icons';
|
||||
|
||||
interface InsufficientBalancePromptProps {
|
||||
/** Amount missing in kopeks */
|
||||
@@ -55,19 +56,7 @@ export default function InsufficientBalancePrompt({
|
||||
className={`flex items-center justify-between gap-3 rounded-xl border border-error-500/30 bg-error-500/10 p-3 ${className}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-error-400">
|
||||
<svg
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<InfoIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
{message || t('balance.insufficientFunds')}:{' '}
|
||||
<span className="font-semibold">
|
||||
@@ -96,19 +85,7 @@ export default function InsufficientBalancePrompt({
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-error-500/20">
|
||||
<svg
|
||||
className="h-5 w-5 text-error-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z"
|
||||
/>
|
||||
</svg>
|
||||
<WalletIcon className="h-5 w-5 text-error-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 font-medium text-error-400">{t('balance.insufficientFunds')}</div>
|
||||
@@ -132,15 +109,7 @@ export default function InsufficientBalancePrompt({
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
{t('balance.topUpBalance')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { infoApi, type LanguageInfo } from '@/api/info';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n } = useTranslation();
|
||||
@@ -57,14 +58,9 @@ export default function LanguageSwitcher() {
|
||||
>
|
||||
<span>{currentLang.flag}</span>
|
||||
<span className="font-medium text-dark-200">{currentLang.code.toUpperCase()}</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-3.5 w-3.5 text-dark-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { promoApi, PromoOffer } from '../api/promo';
|
||||
import { ClockIcon, CheckIcon } from './icons';
|
||||
import { ClockIcon, CheckIcon, XCircleIcon } from './icons';
|
||||
import { useDestructiveConfirm } from '@/platform/hooks/useNativeDialog';
|
||||
|
||||
// Helper functions
|
||||
@@ -69,17 +69,6 @@ const getOfferDescription = (
|
||||
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 {
|
||||
className?: string;
|
||||
}
|
||||
@@ -237,7 +226,7 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
||||
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"
|
||||
>
|
||||
<XCircleIcon />
|
||||
<XCircleIcon className="h-4 w-4" />
|
||||
<span>{t('promo.deactivate.button')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EmailIcon as CentralEmailIcon } from '@/components/icons';
|
||||
import OAuthProviderIcon from './OAuthProviderIcon';
|
||||
|
||||
export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) {
|
||||
@@ -13,22 +14,7 @@ export function TelegramIcon({ className = 'h-5 w-5' }: { className?: string })
|
||||
}
|
||||
|
||||
export function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return <CentralEmailIcon className={className} />;
|
||||
}
|
||||
|
||||
export default function ProviderIcon({
|
||||
|
||||
@@ -12,69 +12,14 @@ import { useCurrency } from '../hooks/useCurrency';
|
||||
import { useTelegramSDK } from '../hooks/useTelegramSDK';
|
||||
import { useFocusTrap } from '../hooks/useFocusTrap';
|
||||
import { useHaptic } from '@/platform';
|
||||
|
||||
// Icons
|
||||
const CheckCircleIcon = () => (
|
||||
<svg
|
||||
className="h-16 w-16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CloseIcon,
|
||||
DevicesIcon,
|
||||
RocketIcon,
|
||||
TrafficIcon,
|
||||
WalletIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
export default function SuccessNotificationModal() {
|
||||
const { t } = useTranslation();
|
||||
@@ -161,33 +106,33 @@ export default function SuccessNotificationModal() {
|
||||
// Determine title and message
|
||||
let title = data.title;
|
||||
const message = data.message;
|
||||
let icon = <CheckCircleIcon />;
|
||||
let icon = <CheckCircleIcon className="h-16 w-16" />;
|
||||
let gradientClass = 'from-success-500 to-success-600';
|
||||
|
||||
if (!title) {
|
||||
if (isBalanceTopup) {
|
||||
title = t('successNotification.balanceTopup.title', 'Balance topped up!');
|
||||
icon = <WalletIcon />;
|
||||
icon = <WalletIcon className="h-8 w-8" />;
|
||||
gradientClass = 'from-success-500 to-success-600';
|
||||
} else if (data.type === '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';
|
||||
} else if (data.type === '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';
|
||||
} else if (data.type === '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';
|
||||
} else if (data.type === 'devices_purchased') {
|
||||
title = t('successNotification.devicesPurchased.title', 'Devices added!');
|
||||
icon = <DevicesIcon />;
|
||||
icon = <DevicesIcon className="h-8 w-8" />;
|
||||
gradientClass = 'from-blue-500 to-cyan-600';
|
||||
} else if (data.type === 'traffic_purchased') {
|
||||
title = t('successNotification.trafficPurchased.title', 'Traffic added!');
|
||||
icon = <TrafficIcon />;
|
||||
icon = <TrafficIcon className="h-8 w-8" />;
|
||||
gradientClass = 'from-success-500 to-success-600';
|
||||
}
|
||||
}
|
||||
@@ -334,7 +279,7 @@ export default function SuccessNotificationModal() {
|
||||
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"
|
||||
>
|
||||
<RocketIcon />
|
||||
<RocketIcon className="h-8 w-8" />
|
||||
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -344,7 +289,7 @@ export default function SuccessNotificationModal() {
|
||||
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"
|
||||
>
|
||||
<WalletIcon />
|
||||
<WalletIcon className="h-8 w-8" />
|
||||
<span>{t('successNotification.goToBalance', 'Go to Balance')}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -354,7 +299,7 @@ export default function SuccessNotificationModal() {
|
||||
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"
|
||||
>
|
||||
<DevicesIcon />
|
||||
<DevicesIcon className="h-8 w-8" />
|
||||
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -364,7 +309,7 @@ export default function SuccessNotificationModal() {
|
||||
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"
|
||||
>
|
||||
<TrafficIcon />
|
||||
<TrafficIcon className="h-8 w-8" />
|
||||
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -8,22 +8,7 @@ import { useToast } from './Toast';
|
||||
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
|
||||
import { useHeaderHeight } from '../hooks/useHeaderHeight';
|
||||
import type { TicketNotification } from '../types';
|
||||
|
||||
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>
|
||||
);
|
||||
import { BellIcon, CheckIcon } from '@/components/icons';
|
||||
|
||||
interface TicketNotificationBellProps {
|
||||
isAdmin?: boolean;
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { CheckIcon, XIcon, ExclamationIcon, InfoIcon } from '@/components/icons';
|
||||
|
||||
interface ToastOptions {
|
||||
type?: 'success' | 'error' | 'info' | 'warning';
|
||||
message: string;
|
||||
@@ -163,58 +165,10 @@ function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) {
|
||||
};
|
||||
|
||||
const defaultIcons = {
|
||||
success: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
),
|
||||
error: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
),
|
||||
warning: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
info: (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
success: <CheckIcon className="h-5 w-5" />,
|
||||
error: <XIcon className="h-5 w-5" />,
|
||||
warning: <ExclamationIcon className="h-5 w-5" />,
|
||||
info: <InfoIcon className="h-5 w-5" />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { brandingApi } from '../../api/branding';
|
||||
import { CheckIcon, CloseIcon } from './icons';
|
||||
import { CheckIcon, CloseIcon, PencilIcon } from './icons';
|
||||
|
||||
export function AnalyticsTab() {
|
||||
const { t } = useTranslation();
|
||||
@@ -143,19 +143,7 @@ export function AnalyticsTab() {
|
||||
}}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -294,19 +282,7 @@ export function AnalyticsTab() {
|
||||
}}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -360,19 +336,7 @@ export function AnalyticsTab() {
|
||||
}}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ButtonSection,
|
||||
BOT_LOCALES,
|
||||
} from '../../api/buttonStyles';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
import { Toggle } from './Toggle';
|
||||
import { useNotify } from '../../platform/hooks/useNotify';
|
||||
import { useNativeDialog } from '../../platform/hooks/useNativeDialog';
|
||||
@@ -284,15 +285,9 @@ export function ButtonsTab() {
|
||||
{t('admin.buttons.customLabels')}
|
||||
{hasCustomLabels && <span className="h-1.5 w-1.5 rounded-full bg-accent-500" />}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-3.5 w-3.5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="mt-2 space-y-2">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckIcon, ChevronDownIcon, CloseIcon, PlusIcon } from '@/components/icons';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
|
||||
@@ -209,9 +210,7 @@ export function ColoredItemCombobox({
|
||||
className="shrink-0 rounded p-0.5 text-dark-500 transition-colors hover:text-dark-300"
|
||||
aria-label={t('news.admin.combobox.clear')}
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -222,16 +221,12 @@ export function ColoredItemCombobox({
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-dark-500 transition-transform duration-200',
|
||||
isOpen && 'rotate-180',
|
||||
)}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7 10l5 5 5-5z" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
@@ -279,13 +274,7 @@ export function ColoredItemCombobox({
|
||||
/>
|
||||
<span className="flex-1 truncate">{item.name}</span>
|
||||
{value?.id === item.id && (
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 text-accent-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
<CheckIcon className="h-4 w-4 shrink-0 text-accent-400" />
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
@@ -298,9 +287,7 @@ export function ColoredItemCombobox({
|
||||
{deletingId === item.id ? (
|
||||
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-error-400 border-t-transparent" />
|
||||
) : (
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -365,9 +352,7 @@ export function ColoredItemCombobox({
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
) : (
|
||||
<>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('news.admin.combobox.create')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,15 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { PiCaretDown } from 'react-icons/pi';
|
||||
import {
|
||||
GripIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
LinkIcon,
|
||||
ArrowUpIcon,
|
||||
ArrowDownIcon,
|
||||
} from '@/components/icons';
|
||||
import {
|
||||
menuLayoutApi,
|
||||
type MenuConfig,
|
||||
@@ -31,82 +40,8 @@ import { Toggle } from './Toggle';
|
||||
import { useNotify } from '../../platform/hooks/useNotify';
|
||||
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 }) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretDown className={`h-3.5 w-3.5 transition-transform ${expanded ? 'rotate-180' : ''}`} />
|
||||
);
|
||||
|
||||
function generateId(): string {
|
||||
@@ -200,7 +135,7 @@ function ButtonChip({
|
||||
: 'cursor-default text-dark-700'
|
||||
}`}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<ArrowUpIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onMoveDown ?? undefined}
|
||||
@@ -212,7 +147,7 @@ function ButtonChip({
|
||||
: 'cursor-default text-dark-700'
|
||||
}`}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
<ArrowDownIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${colorDotClass}`} />
|
||||
@@ -221,7 +156,7 @@ function ButtonChip({
|
||||
</span>
|
||||
{!isBuiltin && (
|
||||
<span className="text-dark-500" title="URL">
|
||||
<LinkIcon />
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
)}
|
||||
<Toggle checked={button.enabled} onChange={() => onUpdate({ enabled: !button.enabled })} />
|
||||
@@ -236,7 +171,7 @@ function ButtonChip({
|
||||
onClick={onRemove}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
@@ -425,7 +360,7 @@ function SortableRow({
|
||||
onClick={() => onRemoveRow(row.id)}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
@@ -481,7 +416,7 @@ function InlineAddPanel({ rowId, usedBuiltinIds, onAddBuiltin, onAddCustom }: In
|
||||
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"
|
||||
>
|
||||
<PlusIcon />
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('admin.menuEditor.addButton')}
|
||||
</button>
|
||||
);
|
||||
@@ -516,7 +451,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"
|
||||
>
|
||||
<LinkIcon />
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
{t('admin.menuEditor.addUrlButton')}
|
||||
</button>
|
||||
<button
|
||||
@@ -843,7 +778,7 @@ export function MenuEditorTab() {
|
||||
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"
|
||||
>
|
||||
<PlusIcon />
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('admin.menuEditor.addRow')}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { PiCaretDown } from 'react-icons/pi';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { GripIcon, TrashIcon } from '../icons/LandingIcons';
|
||||
import type { AdminLandingPaymentMethod, EditableMethodField } from '../../api/landings';
|
||||
import type { PaymentMethodSubOptionInfo } from '../../types';
|
||||
@@ -10,15 +12,7 @@ import type { PaymentMethodSubOptionInfo } from '../../types';
|
||||
export type MethodWithId = AdminLandingPaymentMethod & { _id: string };
|
||||
|
||||
const ChevronDownIcon = ({ open }: { open: boolean }) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretDown className={cn('h-5 w-5 transition-transform', open && 'rotate-180')} />
|
||||
);
|
||||
|
||||
interface SortableSelectedMethodProps {
|
||||
@@ -225,17 +219,7 @@ export function SortableSelectedMethodCard({
|
||||
: 'border border-dark-600 bg-dark-700',
|
||||
)}
|
||||
>
|
||||
{enabled && (
|
||||
<svg
|
||||
className="h-2.5 w-2.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{enabled && <CheckIcon className="h-2.5 w-2.5" />}
|
||||
</div>
|
||||
{opt.name}
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, ChevronDownIcon, XCloseIcon, XIcon } from '@/components/icons';
|
||||
import { useFocusTrap } from '@/hooks/useFocusTrap';
|
||||
import { DropdownSelect } from './DropdownSelect';
|
||||
import type { UserListItem } from '../../../api/adminUsers';
|
||||
@@ -48,24 +49,6 @@ export interface ModalState {
|
||||
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 }) {
|
||||
const { t } = useTranslation();
|
||||
const logEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -115,31 +98,11 @@ function ProgressView({ progress }: { progress: ProgressState }) {
|
||||
<div key={idx} className="flex items-start gap-2 text-xs">
|
||||
{entry.success ? (
|
||||
<span className="mt-0.5 shrink-0 text-success-400" aria-hidden="true">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
<span className="font-mono text-dark-400">
|
||||
@@ -176,18 +139,12 @@ function ErrorDetails({ result }: { result: BulkActionResult }) {
|
||||
<span className="text-sm font-medium text-error-400">
|
||||
{t('admin.bulkActions.errors.title', { count: result.error_count })}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'h-4 w-4 text-error-400 transition-transform duration-200',
|
||||
expanded && 'rotate-180',
|
||||
)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="max-h-48 overflow-y-auto border-t border-error-500/20 px-4 py-3">
|
||||
@@ -195,15 +152,7 @@ function ErrorDetails({ result }: { result: BulkActionResult }) {
|
||||
{result.errors.map((err, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-xs">
|
||||
<span className="mt-0.5 shrink-0 text-error-400" aria-hidden="true">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</span>
|
||||
<span className="shrink-0 font-mono text-dark-400">
|
||||
{err.username ? `@${err.username}` : `#${err.user_id}`}
|
||||
@@ -520,7 +469,7 @@ export function ActionModal({
|
||||
)}
|
||||
aria-pressed={forceDeleteActivePaid}
|
||||
>
|
||||
{forceDeleteActivePaid && <CheckIcon />}
|
||||
{forceDeleteActivePaid && <CheckIcon className="h-3 w-3" />}
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -558,7 +507,7 @@ export function ActionModal({
|
||||
)}
|
||||
aria-pressed={deleteFromPanel}
|
||||
>
|
||||
{deleteFromPanel && <CheckIcon />}
|
||||
{deleteFromPanel && <CheckIcon className="h-3 w-3" />}
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -7,11 +8,10 @@ import { cn } from '@/lib/utils';
|
||||
// chrome (border, focus ring, chevron).
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export 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>
|
||||
);
|
||||
// Re-exported so the sibling FloatingActionBar / MultiSelectDropdown keep
|
||||
// importing the chevron from this module while the glyph itself now comes
|
||||
// from the central Phosphor barrel instead of a hand-written SVG.
|
||||
export { ChevronDownIcon };
|
||||
|
||||
export interface DropdownOption {
|
||||
value: string;
|
||||
@@ -40,7 +40,7 @@ export function DropdownSelect({ value, options, onChange, className }: Dropdown
|
||||
))}
|
||||
</select>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TrashIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon } from './DropdownSelect';
|
||||
import { isSubscriptionLevelAction } from './actionTargets';
|
||||
import type { BulkActionType } from '../../../api/adminBulkActions';
|
||||
@@ -112,21 +113,7 @@ export function FloatingActionBar({
|
||||
{
|
||||
type: 'delete_subscription',
|
||||
labelKey: 'admin.bulkActions.actions.deleteSubscription',
|
||||
icon: (
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
icon: <TrashIcon className="h-3.5 w-3.5" />,
|
||||
colorClass: 'text-error-400 hover:bg-error-500/10',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon } from './DropdownSelect';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -135,21 +136,7 @@ export function MultiSelectDropdown({
|
||||
: 'border-dark-500 bg-dark-700/60',
|
||||
)}
|
||||
>
|
||||
{isChecked && (
|
||||
<svg
|
||||
className="h-2.5 w-2.5 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={4}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{isChecked && <CheckIcon className="h-2.5 w-2.5 text-white" />}
|
||||
</div>
|
||||
<span className={cn('text-sm', isChecked ? 'text-dark-100' : 'text-dark-300')}>
|
||||
{option.label}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import type { UserListItemSubscription } from '../../../api/adminUsers';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -68,17 +69,7 @@ export function SubscriptionSubRow({
|
||||
: t('admin.bulkActions.selectUser', { name: subscription.tariff_name || '' })
|
||||
}
|
||||
>
|
||||
{isSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={4}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{isSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,164 +1,35 @@
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// TrafficIcons — the inline SVG set used across AdminTrafficUsage
|
||||
// (header controls, filter buttons, sort indicator, etc.). Page-local
|
||||
// in spirit; co-located here so the parent page imports a single
|
||||
// flat barrel rather than redefining 15 inline icons.
|
||||
// TrafficIcons — the icon set used across AdminTrafficUsage (header
|
||||
// controls, filter buttons, sort indicator, etc.). The glyphs now come
|
||||
// from the central Phosphor barrel (`@/components/icons`); only the
|
||||
// sort indicator keeps its custom `direction` prop and is therefore a
|
||||
// thin local wrapper over the panel's Phosphor caret icons.
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export 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>
|
||||
);
|
||||
import { PiCaretDown, PiCaretUpDown, PiCaretUp } from 'react-icons/pi';
|
||||
|
||||
export 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>
|
||||
);
|
||||
export {
|
||||
CalendarIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DownloadIcon,
|
||||
FilterIcon,
|
||||
GlobeIcon,
|
||||
RefreshIcon,
|
||||
SearchIcon,
|
||||
ServerIcon,
|
||||
ServerSmallIcon,
|
||||
ShieldIcon,
|
||||
StatusIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
export 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 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>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
export const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) =>
|
||||
direction === 'asc' ? (
|
||||
<PiCaretUp className="ml-1 inline h-3 w-3" />
|
||||
) : direction === 'desc' ? (
|
||||
<PiCaretDown className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<PiCaretUpDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon, GlobeIcon } from '../TrafficIcons';
|
||||
import { getFlagEmoji } from '../trafficUsageHelpers';
|
||||
|
||||
@@ -49,13 +50,13 @@ export function CountryFilter({
|
||||
: '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 && (
|
||||
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDownIcon />
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
@@ -71,17 +72,7 @@ export function CountryFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
All
|
||||
</button>
|
||||
@@ -102,21 +93,7 @@ export function CountryFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{getFlagEmoji(code)} {code.toUpperCase()}
|
||||
<span className="ml-auto text-dark-500">{count}</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronDownIcon, ServerIcon } from '../TrafficIcons';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { getFlagEmoji } from '../trafficUsageHelpers';
|
||||
import type { TrafficNodeInfo } from '../../../../api/adminTraffic';
|
||||
|
||||
@@ -52,14 +53,14 @@ export function NodeFilter({
|
||||
: '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')}
|
||||
{activeCount > 0 && (
|
||||
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDownIcon />
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
@@ -75,17 +76,7 @@ export function NodeFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{t('admin.trafficUsage.allNodes')}
|
||||
</button>
|
||||
@@ -106,21 +97,7 @@ export function NodeFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{getFlagEmoji(node.country_code)} {node.node_name}
|
||||
</button>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function PeriodSelector({
|
||||
if (dateMode) {
|
||||
return (
|
||||
<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>
|
||||
<input
|
||||
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"
|
||||
title={t('admin.trafficUsage.period')}
|
||||
>
|
||||
<XIcon />
|
||||
<XIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</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"
|
||||
title={t('admin.trafficUsage.customDates')}
|
||||
>
|
||||
<CalendarIcon />
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
import { ChevronDownIcon, StatusIcon } from '../TrafficIcons';
|
||||
|
||||
// Status colour pills shared with the StatusFilter dropdown.
|
||||
@@ -63,14 +64,14 @@ export function StatusFilter({
|
||||
: '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')}
|
||||
{activeCount > 0 && (
|
||||
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDownIcon />
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
@@ -86,17 +87,7 @@ export function StatusFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{t('admin.trafficUsage.allStatuses')}
|
||||
</button>
|
||||
@@ -117,21 +108,7 @@ export function StatusFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
<span className={`h-2 w-2 rounded-full ${STATUS_COLORS[s] || 'bg-dark-500'}`} />
|
||||
{statusLabel(s)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronDownIcon, FilterIcon } from '../TrafficIcons';
|
||||
import { CheckIcon } from '@/components/icons';
|
||||
|
||||
export function TariffFilter({
|
||||
available,
|
||||
@@ -50,14 +51,14 @@ export function TariffFilter({
|
||||
: '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')}
|
||||
{activeCount > 0 && (
|
||||
<span className="rounded-full bg-accent-500 px-1.5 text-[10px] text-white">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDownIcon />
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
@@ -73,17 +74,7 @@ export function TariffFilter({
|
||||
allSelected ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{allSelected && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
{allSelected && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{t('admin.trafficUsage.allTariffs')}
|
||||
</button>
|
||||
@@ -104,21 +95,7 @@ export function TariffFilter({
|
||||
checked ? 'border-accent-500 bg-accent-500' : 'border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{checked && <CheckIcon className="h-3 w-3 text-white" />}
|
||||
</span>
|
||||
{tariff}
|
||||
</button>
|
||||
|
||||
@@ -6,22 +6,7 @@ import { adminUsersApi, type UserDetailResponse } from '../../../api/adminUsers'
|
||||
import { promocodesApi } from '../../../api/promocodes';
|
||||
import { promoOffersApi } from '../../../api/promoOffers';
|
||||
import { createNumberInputHandler, toNumber } from '../../../utils/inputHelpers';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// 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>
|
||||
);
|
||||
import { PlusIcon, MinusIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Balance tab — current balance, add/subtract form, active promo
|
||||
@@ -168,14 +153,14 @@ export function BalanceTab({
|
||||
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"
|
||||
>
|
||||
<PlusIcon /> {t('admin.users.detail.balance.add')}
|
||||
<PlusIcon className="h-4 w-4" /> {t('admin.users.detail.balance.add')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdateBalance(false)}
|
||||
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"
|
||||
>
|
||||
<MinusIcon /> {t('admin.users.detail.balance.subtract')}
|
||||
<MinusIcon className="h-4 w-4" /> {t('admin.users.detail.balance.subtract')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SendIcon } from '@/components/icons';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import type { AdminUserGiftItem, AdminUserGiftsResponse } from '../../../api/adminUsers';
|
||||
|
||||
@@ -224,19 +225,7 @@ export function GiftsTab({ giftsLoading, giftsData, locale, onNavigateToUser }:
|
||||
{/* Sent Gifts */}
|
||||
<div>
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-dark-200">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<SendIcon className="h-4 w-4 text-accent-400" />
|
||||
{t('admin.users.detail.gifts.sentTitle')}
|
||||
<span className="text-dark-500">({giftsData.sent_total})</span>
|
||||
</h3>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
UserSubscriptionInfo,
|
||||
} from '../../../api/adminUsers';
|
||||
import type { PromoGroup } from '../../../api/promocodes';
|
||||
import { ServerIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Local status badge (parent has its own — duplicating here to keep
|
||||
@@ -260,19 +261,7 @@ export function InfoTab(props: InfoTabProps) {
|
||||
{t('admin.users.detail.lastNode')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-dark-100">
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<ServerIcon className="h-4 w-4 shrink-0 text-dark-400" />
|
||||
{panelInfo.last_connected_node_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import { useNotify } from '../../../platform/hooks/useNotify';
|
||||
import { adminUsersApi, type UserDetailResponse, type UserListItem } from '../../../api/adminUsers';
|
||||
import { XIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Referrals tab — top-of-graph referrer + stats + referrals list,
|
||||
@@ -481,15 +482,7 @@ export function ReferralsTab({ user, userId, onUserRefresh }: ReferralsTabProps)
|
||||
className="rounded-lg p-2 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400 disabled:opacity-50"
|
||||
title={t('admin.users.detail.referrals.removeReferral')}
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
BackIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
EditIcon,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
RefreshIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
import { DEVICE_ALIAS_MAX_LENGTH } from '../../../constants/devices';
|
||||
import { createNumberInputHandler } from '../../../utils/inputHelpers';
|
||||
import { getFlagEmoji } from '../../../utils/subscriptionHelpers';
|
||||
@@ -37,28 +48,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)
|
||||
type DeviceRow = {
|
||||
hwid: string;
|
||||
@@ -238,19 +227,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
</span>
|
||||
<StatusBadge status={sub.status} />
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-4 w-4 text-dark-500" />
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-dark-400">
|
||||
<span>
|
||||
@@ -330,15 +307,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
onClick={() => onSubscriptionDetailViewChange(false)}
|
||||
className="flex items-center gap-1.5 text-sm text-dark-400 transition-colors hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<BackIcon className="h-4 w-4" />
|
||||
{t('admin.users.detail.subscription.backToList', 'Все подписки')}
|
||||
</button>
|
||||
)}
|
||||
@@ -390,7 +359,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
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"
|
||||
>
|
||||
<MinusIcon />
|
||||
<MinusIcon className="h-3 w-3" />
|
||||
</button>
|
||||
<span className="min-w-[2ch] text-center text-dark-100">
|
||||
{selectedSub.device_limit}
|
||||
@@ -404,7 +373,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"
|
||||
>
|
||||
<PlusIcon />
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -964,19 +933,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
)}
|
||||
aria-label={t('admin.users.detail.devices.renameSave', 'Сохранить')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CheckIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -989,19 +946,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
title={t('common.cancel', 'Отмена')}
|
||||
aria-label={t('common.cancel', 'Отмена')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -1016,19 +961,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
title={t('admin.users.detail.devices.rename', 'Переименовать')}
|
||||
aria-label={t('admin.users.detail.devices.rename', 'Переименовать')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
|
||||
</svg>
|
||||
<EditIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
@@ -1081,19 +1014,9 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-dark-500 transition-transform ${requestHistoryExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{requestHistoryExpanded && (
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ArrowDownIcon, ArrowUpIcon } from '@/components/icons';
|
||||
import type {
|
||||
UserDetailResponse,
|
||||
UserSubscriptionInfo,
|
||||
PanelSyncStatusResponse,
|
||||
} 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
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { adminApi, type AdminTicket, type AdminTicketDetail } from '../../../api/admin';
|
||||
import { MessageMediaGrid } from '../../tickets/MessageMediaGrid';
|
||||
import { linkifyText } from '../../../utils/linkify';
|
||||
import { ChatIcon, BackIcon, SendIcon } from '@/components/icons';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Tickets tab — list view + chat view (selected ticket replaces list).
|
||||
@@ -155,19 +156,7 @@ function EmptyState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-dark-800/50 py-12">
|
||||
<svg
|
||||
className="mb-3 h-12 w-12 text-dark-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
||||
/>
|
||||
</svg>
|
||||
<ChatIcon className="mb-3 h-12 w-12 text-dark-600" />
|
||||
<p className="text-dark-400">{t('admin.users.detail.noTickets')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -265,15 +254,7 @@ function ChatView({
|
||||
aria-label={t('common.back', 'Back')}
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-dark-800 transition-colors hover:bg-dark-700 sm:h-8 sm:w-8"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
<BackIcon className="h-4 w-4 text-dark-400" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-dark-100">
|
||||
@@ -372,19 +353,7 @@ function ChatView({
|
||||
{replySending ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
) : (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<SendIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlatform } from '@/platform';
|
||||
import { InfoIcon } from '@/components/icons';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
|
||||
@@ -55,20 +56,7 @@ export default function AccountDeletedScreen() {
|
||||
<div className="w-full max-w-md text-center">
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-warning-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<InfoIcon className="h-12 w-12 text-warning-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { BanIcon } from '@/components/icons';
|
||||
|
||||
export default function BlacklistedScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,19 +21,7 @@ export default function BlacklistedScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-error-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
/>
|
||||
</svg>
|
||||
<BanIcon className="h-12 w-12 text-error-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useBlockingStore } from '../../store/blocking';
|
||||
import { apiClient, isChannelSubscriptionError } from '../../api/client';
|
||||
import { usePlatform } from '../../platform';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { TelegramIcon, ClockIcon, CheckIcon } from '@/components/icons';
|
||||
|
||||
const CHECK_COOLDOWN_SECONDS = 5;
|
||||
|
||||
@@ -96,9 +97,7 @@ export default function ChannelSubscriptionScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-gradient-to-br from-blue-500/20 to-cyan-500/20">
|
||||
<svg className="h-12 w-12 text-blue-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
<TelegramIcon className="h-12 w-12 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -183,31 +182,12 @@ export default function ChannelSubscriptionScreen() {
|
||||
</>
|
||||
) : cooldown > 0 ? (
|
||||
<>
|
||||
<svg
|
||||
className="h-5 w-5 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<ClockIcon className="h-5 w-5 text-dark-500" />
|
||||
{t('blocking.channel.waitSeconds', { seconds: cooldown })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
{t('blocking.channel.checkSubscription')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockingStore } from '../../store/blocking';
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||
import { WrenchIcon } from '@/components/icons';
|
||||
|
||||
export default function MaintenanceScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,19 +21,7 @@ export default function MaintenanceScreen() {
|
||||
{/* Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
|
||||
<svg
|
||||
className="h-12 w-12 text-warning-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<WrenchIcon className="h-12 w-12 text-warning-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
|
||||
import type { BlockRendererProps } from './blocks';
|
||||
import TvQuickConnect from './TvQuickConnect';
|
||||
import { BackIcon, BookOpenIcon, ChevronIcon } from '@/components/icons';
|
||||
|
||||
const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'];
|
||||
|
||||
@@ -33,12 +34,6 @@ const RENDERERS: Record<string, React.ComponentType<BlockRendererProps>> = {
|
||||
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 {
|
||||
appConfig: AppConfig;
|
||||
onOpenDeepLink: (url: string) => void;
|
||||
@@ -200,7 +195,7 @@ export default function InstallationGuide({
|
||||
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"
|
||||
>
|
||||
<BackIcon />
|
||||
<BackIcon className="h-6 w-6" />
|
||||
</button>
|
||||
)}
|
||||
<h2 className="flex-1 text-lg font-bold text-dark-100">
|
||||
@@ -264,15 +259,7 @@ export default function InstallationGuide({
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-2.5 text-dark-400">
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 9l4-4 4 4M8 15l4 4 4-4" />
|
||||
</svg>
|
||||
<ChevronIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -320,19 +307,7 @@ export default function InstallationGuide({
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary w-full justify-center"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
</svg>
|
||||
<BookOpenIcon className="h-5 w-5" />
|
||||
{getBaseTranslation('tutorial', 'subscription.connection.tutorial')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDownIcon } from '@/components/icons';
|
||||
import { getColorGradient } from '@/utils/colorParser';
|
||||
import { ThemeIcon } from './ThemeIcon';
|
||||
import type { BlockRendererProps } from './types';
|
||||
@@ -52,15 +53,9 @@ export function AccordionBlock({
|
||||
<span className="min-w-0 flex-1 truncate font-semibold text-dark-100">
|
||||
{getLocalizedText(block.title)}
|
||||
</span>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-[18px] w-[18px] shrink-0 text-dark-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
{/* Panel */}
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckIcon, CopyIcon } from '@/components/icons';
|
||||
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
|
||||
@@ -20,22 +21,6 @@ function isValidExternalUrl(url: string | undefined): boolean {
|
||||
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 {
|
||||
buttons: RemnawaveButtonClient[] | undefined;
|
||||
variant: 'light' | 'subtle';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GiftIcon } from '@/components/icons';
|
||||
import type { PendingGift } from '../../api/gift';
|
||||
|
||||
interface PendingGiftCardProps {
|
||||
@@ -28,19 +29,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro
|
||||
<div className="relative flex items-start gap-4">
|
||||
{/* Gift icon */}
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-accent-500/20">
|
||||
<svg
|
||||
className="h-6 w-6 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
<GiftIcon className="h-6 w-6 text-accent-400" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PiCaretRight } from 'react-icons/pi';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
@@ -12,22 +13,7 @@ interface StatsGridProps {
|
||||
}
|
||||
|
||||
const ChevronIcon = ({ color }: { color: string }) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretRight width={16} height={16} style={{ flexShrink: 0, color }} aria-hidden="true" />
|
||||
);
|
||||
|
||||
export default function StatsGrid({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTrafficZone } from '../../hooks/useTrafficZone';
|
||||
import { formatTraffic } from '../../utils/formatTraffic';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import { CalendarIcon, RefreshIcon } from '@/components/icons';
|
||||
import { useHaptic } from '../../platform';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
@@ -25,23 +26,6 @@ interface SubscriptionCardActiveProps {
|
||||
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({
|
||||
subscription,
|
||||
trafficData,
|
||||
@@ -325,20 +309,14 @@ export default function SubscriptionCardActive({
|
||||
background: daysLeft <= 3 ? 'rgba(var(--color-warning-400), 0.1)' : g.hoverBg,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<span
|
||||
style={{
|
||||
color: daysLeft <= 3 ? 'rgb(var(--color-warning-400))' : g.textSecondary,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-[13px] w-[13px]" />
|
||||
</span>
|
||||
</div>
|
||||
{t('dashboard.remaining')}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { getInsufficientBalanceError } from '../../utils/subscriptionHelpers';
|
||||
import { ClockIcon, ExclamationIcon, PlusIcon, SubscriptionIcon } from '@/components/icons';
|
||||
|
||||
interface SubscriptionCardExpiredProps {
|
||||
subscription: Subscription;
|
||||
@@ -167,39 +168,13 @@ export default function SubscriptionCardExpired({
|
||||
style={{
|
||||
background: `rgba(${accent.r},${accent.g},${accent.b},0.1)`,
|
||||
border: `1px solid rgba(${accent.r},${accent.g},${accent.b},0.15)`,
|
||||
color: accent.hex,
|
||||
}}
|
||||
>
|
||||
{isLimited ? (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<ExclamationIcon className="h-[22px] w-[22px]" />
|
||||
) : (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accent.hex}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
<ClockIcon className="h-[22px] w-[22px]" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||
@@ -274,19 +249,7 @@ export default function SubscriptionCardExpired({
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('subscription.buyTraffic')}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -311,19 +274,7 @@ export default function SubscriptionCardExpired({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
<SubscriptionIcon className="h-4 w-4" />
|
||||
)}
|
||||
{isRenewing
|
||||
? t('common.loading')
|
||||
@@ -341,19 +292,7 @@ export default function SubscriptionCardExpired({
|
||||
boxShadow: `0 4px 20px rgba(${accent.r},${accent.g},${accent.b},0.2)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t('dashboard.expired.topUp')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { TrialInfo } from '../../types';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { BoltIcon, SparklesIcon } from '@/components/icons';
|
||||
|
||||
interface TrialOfferCardProps {
|
||||
trialInfo: TrialInfo;
|
||||
@@ -94,37 +95,21 @@ export default function TrialOfferCard({
|
||||
}}
|
||||
>
|
||||
{isFree ? (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgb(var(--color-accent-400))"
|
||||
strokeWidth="1.5"
|
||||
<span
|
||||
className="flex"
|
||||
style={{ color: 'rgb(var(--color-accent-400))' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<SparklesIcon className="h-[26px] w-[26px]" />
|
||||
</span>
|
||||
) : (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgb(var(--color-urgent-400))"
|
||||
strokeWidth="1.5"
|
||||
<span
|
||||
className="flex"
|
||||
style={{ color: 'rgb(var(--color-urgent-400))' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<BoltIcon className="h-[26px] w-[26px]" />
|
||||
</span>
|
||||
)}
|
||||
{/* Glow effect */}
|
||||
<div
|
||||
|
||||
85
src/components/icons/editor-icons.tsx
Normal file
85
src/components/icons/editor-icons.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
PiTextB,
|
||||
PiTextItalic,
|
||||
PiTextUnderline,
|
||||
PiTextStrikethrough,
|
||||
PiTextHOne,
|
||||
PiTextHTwo,
|
||||
PiTextHThree,
|
||||
PiListBullets,
|
||||
PiListNumbers,
|
||||
PiQuotes,
|
||||
PiCodeBlock,
|
||||
PiTextAlignLeft,
|
||||
PiTextAlignCenter,
|
||||
PiHighlighter,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich-text editor toolbar icons — Phosphor (react-icons/pi), the panel's
|
||||
* icon family (regular weight). 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) => (
|
||||
<PiTextB className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ItalicIcon = ({ className }: IconProps) => (
|
||||
<PiTextItalic className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const UnderlineIcon = ({ className }: IconProps) => (
|
||||
<PiTextUnderline className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StrikeIcon = ({ className }: IconProps) => (
|
||||
<PiTextStrikethrough className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const H1Icon = ({ className }: IconProps) => (
|
||||
<PiTextHOne className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const H2Icon = ({ className }: IconProps) => (
|
||||
<PiTextHTwo className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const H3Icon = ({ className }: IconProps) => (
|
||||
<PiTextHThree className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ListBulletIcon = ({ className }: IconProps) => (
|
||||
<PiListBullets className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ListOrderedIcon = ({ className }: IconProps) => (
|
||||
<PiListNumbers className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const QuoteIcon = ({ className }: IconProps) => (
|
||||
<PiQuotes className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CodeBlockIcon = ({ className }: IconProps) => (
|
||||
<PiCodeBlock className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const AlignLeftIcon = ({ className }: IconProps) => (
|
||||
<PiTextAlignLeft className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const AlignCenterIcon = ({ className }: IconProps) => (
|
||||
<PiTextAlignCenter className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const HighlightIcon = ({ className }: IconProps) => (
|
||||
<PiHighlighter className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
474
src/components/icons/extended-icons.tsx
Normal file
474
src/components/icons/extended-icons.tsx
Normal file
@@ -0,0 +1,474 @@
|
||||
import {
|
||||
PiSlidersHorizontal,
|
||||
PiWrench,
|
||||
PiBookOpen,
|
||||
PiHeadset,
|
||||
PiArrowDown,
|
||||
PiArrowRight,
|
||||
PiArrowUp,
|
||||
PiProhibit,
|
||||
PiMoney,
|
||||
PiBell,
|
||||
PiLightning,
|
||||
PiRobot,
|
||||
PiBroadcast,
|
||||
PiAppWindow,
|
||||
PiCalendarDots,
|
||||
PiCreditCard,
|
||||
PiChartBar,
|
||||
PiCheckCircle,
|
||||
PiCaretUpDown,
|
||||
PiCaretDown,
|
||||
PiCaretLeft,
|
||||
PiCaretUp,
|
||||
PiCurrencyBtc,
|
||||
PiDevices,
|
||||
PiFileText,
|
||||
PiCircleFill,
|
||||
PiEnvelope,
|
||||
PiWarning,
|
||||
PiArrowSquareOut,
|
||||
PiEye,
|
||||
PiFunnel,
|
||||
PiDotsSix,
|
||||
PiDotsSixVertical,
|
||||
PiHeartbeat,
|
||||
PiClockCounterClockwise,
|
||||
PiImage,
|
||||
PiInfinity,
|
||||
PiLink,
|
||||
PiMegaphone,
|
||||
PiMinus,
|
||||
PiNewspaper,
|
||||
PiHandshake,
|
||||
PiPushPin,
|
||||
PiPlus,
|
||||
PiPower,
|
||||
PiQuestion,
|
||||
PiRepeat,
|
||||
PiFlag,
|
||||
PiArrowCounterClockwise,
|
||||
PiArrowClockwise,
|
||||
PiRocket,
|
||||
PiFloppyDisk,
|
||||
PiPaperPlaneTilt,
|
||||
PiPercent,
|
||||
PiHardDrives,
|
||||
PiGearSix,
|
||||
PiShareNetwork,
|
||||
PiSparkle,
|
||||
PiChartLine,
|
||||
PiGift,
|
||||
PiTimer,
|
||||
PiCircle,
|
||||
PiTag,
|
||||
PiTelegramLogo,
|
||||
PiTicket,
|
||||
PiGauge,
|
||||
PiTrash,
|
||||
PiTrophy,
|
||||
PiPushPinSlash,
|
||||
PiUserPlus,
|
||||
PiUsersThree,
|
||||
PiVideoCamera,
|
||||
PiXCircle,
|
||||
PiX,
|
||||
PiKey,
|
||||
PiTray,
|
||||
PiExport,
|
||||
PiWarningCircle,
|
||||
PiCalendarBlank,
|
||||
PiCalendarStar,
|
||||
PiChartPie,
|
||||
PiChartDonut,
|
||||
PiCpu,
|
||||
PiMemory,
|
||||
PiPulse,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended cabinet icon set — Phosphor (react-icons/pi), 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) => (
|
||||
<PiSlidersHorizontal className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const WrenchIcon = ({ className }: IconProps) => (
|
||||
<PiWrench className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BookOpenIcon = ({ className }: IconProps) => (
|
||||
<PiBookOpen className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const AgentIcon = ({ className }: IconProps) => (
|
||||
<PiHeadset className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ArrowDownIcon = ({ className }: IconProps) => (
|
||||
<PiArrowDown className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ArrowIcon = ({ className }: IconProps) => (
|
||||
<PiArrowRight className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ArrowUpIcon = ({ className }: IconProps) => (
|
||||
<PiArrowUp className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BanIcon = ({ className }: IconProps) => (
|
||||
<PiProhibit className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const KeyIcon = ({ className }: IconProps) => <PiKey className={cn('h-5 w-5', className)} />;
|
||||
|
||||
export const InboxIcon = ({ className }: IconProps) => (
|
||||
<PiTray className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ExportIcon = ({ className }: IconProps) => (
|
||||
<PiExport className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const WarningCircleIcon = ({ className }: IconProps) => (
|
||||
<PiWarningCircle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const HeartbeatIcon = ({ className }: IconProps) => (
|
||||
<PiHeartbeat className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CalendarBlankIcon = ({ className }: IconProps) => (
|
||||
<PiCalendarBlank className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CalendarStarIcon = ({ className }: IconProps) => (
|
||||
<PiCalendarStar className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChartPieIcon = ({ className }: IconProps) => (
|
||||
<PiChartPie className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChartDonutIcon = ({ className }: IconProps) => (
|
||||
<PiChartDonut className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CpuIcon = ({ className }: IconProps) => <PiCpu className={cn('h-5 w-5', className)} />;
|
||||
|
||||
export const MemoryIcon = ({ className }: IconProps) => (
|
||||
<PiMemory className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PulseIcon = ({ className }: IconProps) => (
|
||||
<PiPulse className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BanknotesIcon = ({ className }: IconProps) => (
|
||||
<PiMoney className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BellIcon = ({ className }: IconProps) => (
|
||||
<PiBell className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BoltIcon = ({ className }: IconProps) => (
|
||||
<PiLightning className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BotIcon = ({ className }: IconProps) => (
|
||||
<PiRobot className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BroadcastIcon = ({ className }: IconProps) => (
|
||||
<PiBroadcast className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CabinetIcon = ({ className }: IconProps) => (
|
||||
<PiAppWindow className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CalendarIcon = ({ className }: IconProps) => (
|
||||
<PiCalendarDots className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CardIcon = ({ className }: IconProps) => (
|
||||
<PiCreditCard className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChannelIcon = ({ className }: IconProps) => (
|
||||
<PiBroadcast className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChartBarIcon = ({ className }: IconProps) => (
|
||||
<PiChartBar className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CheckCircleIcon = ({ className }: IconProps) => (
|
||||
<PiCheckCircle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChevronExpandIcon = ({ className }: IconProps) => (
|
||||
<PiCaretUpDown className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChevronIcon = ({ className }: IconProps) => (
|
||||
<PiCaretDown className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChevronLeftIcon = ({ className }: IconProps) => (
|
||||
<PiCaretLeft className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChevronUpIcon = ({ className }: IconProps) => (
|
||||
<PiCaretUp className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CryptoIcon = ({ className }: IconProps) => (
|
||||
<PiCurrencyBtc className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const DevicesIcon = ({ className }: IconProps) => (
|
||||
<PiDevices className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const DocumentIcon = ({ className }: IconProps) => (
|
||||
<PiFileText 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) => (
|
||||
<PiEnvelope className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ExclamationIcon = ({ className }: IconProps) => (
|
||||
<PiWarning className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ExternalLinkIcon = ({ className }: IconProps) => (
|
||||
<PiArrowSquareOut className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const EyeIcon = ({ className }: IconProps) => <PiEye className={cn('h-5 w-5', className)} />;
|
||||
|
||||
export const FileTextIcon = ({ className }: IconProps) => (
|
||||
<PiFileText className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const FilterIcon = ({ className }: IconProps) => (
|
||||
<PiFunnel className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const GripIcon = ({ className }: IconProps) => (
|
||||
<PiDotsSix className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const GripVerticalIcon = ({ className }: IconProps) => (
|
||||
<PiDotsSixVertical className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const HealthIcon = ({ className }: IconProps) => (
|
||||
<PiHeartbeat className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const HistoryIcon = ({ className }: IconProps) => (
|
||||
<PiClockCounterClockwise className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ImageIcon = ({ className }: IconProps) => (
|
||||
<PiImage className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const InfinityIcon = ({ className }: IconProps) => (
|
||||
<PiInfinity className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const LinkIcon = ({ className }: IconProps) => (
|
||||
<PiLink className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const MailIcon = ({ className }: IconProps) => (
|
||||
<PiEnvelope className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const MegaphoneIcon = ({ className }: IconProps) => (
|
||||
<PiMegaphone className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const MinusIcon = ({ className }: IconProps) => (
|
||||
<PiMinus className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const NewsIcon = ({ className }: IconProps) => (
|
||||
<PiNewspaper className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PartnerIcon = ({ className }: IconProps) => (
|
||||
<PiHandshake className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PhotoIcon = ({ className }: IconProps) => (
|
||||
<PiImage className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PercentIcon = ({ className }: IconProps) => (
|
||||
<PiPercent className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PinIcon = ({ className }: IconProps) => (
|
||||
<PiPushPin className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PlusSmallIcon = ({ className }: IconProps) => (
|
||||
<PiPlus className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const PowerIcon = ({ className }: IconProps) => (
|
||||
<PiPower className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const QuestionIcon = ({ className }: IconProps) => (
|
||||
<PiQuestion className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const RepeatIcon = ({ className }: IconProps) => (
|
||||
<PiRepeat className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ReportIcon = ({ className }: IconProps) => (
|
||||
<PiFlag className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ResetIcon = ({ className }: IconProps) => (
|
||||
<PiArrowCounterClockwise className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const RestartIcon = ({ className }: IconProps) => (
|
||||
<PiArrowClockwise className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const RocketIcon = ({ className }: IconProps) => (
|
||||
<PiRocket className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const SaveIcon = ({ className }: IconProps) => (
|
||||
<PiFloppyDisk className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const SendIcon = ({ className }: IconProps) => (
|
||||
<PiPaperPlaneTilt className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ServerSmallIcon = ({ className }: IconProps) => (
|
||||
<PiHardDrives className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const SettingsIcon = ({ className }: IconProps) => (
|
||||
<PiGearSix className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ShareIcon = ({ className }: IconProps) => (
|
||||
<PiShareNetwork className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const SparklesIcon = ({ className }: IconProps) => (
|
||||
<PiSparkle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatBotIcon = ({ className }: IconProps) => (
|
||||
<PiRobot className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatCabinetIcon = ({ className }: IconProps) => (
|
||||
<PiAppWindow className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatPaidIcon = ({ className }: IconProps) => (
|
||||
<PiMoney className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatsChartIcon = ({ className }: IconProps) => (
|
||||
<PiChartLine className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatTrialIcon = ({ className }: IconProps) => (
|
||||
<PiGift className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatUptimeIcon = ({ className }: IconProps) => (
|
||||
<PiTimer className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StatusIcon = ({ className }: IconProps) => (
|
||||
<PiCircle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const TagIcon = ({ className }: IconProps) => <PiTag className={cn('h-5 w-5', className)} />;
|
||||
|
||||
export const TelegramIcon = ({ className }: IconProps) => (
|
||||
<PiTelegramLogo className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const TelegramSmallIcon = ({ className }: IconProps) => (
|
||||
<PiTelegramLogo className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const TicketIcon = ({ className }: IconProps) => (
|
||||
<PiTicket className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const TrafficIcon = ({ className }: IconProps) => (
|
||||
<PiGauge className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const TrashSmallIcon = ({ className }: IconProps) => (
|
||||
<PiTrash className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const TrophyIcon = ({ className }: IconProps) => (
|
||||
<PiTrophy className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const UnpinIcon = ({ className }: IconProps) => (
|
||||
<PiPushPinSlash className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const UserPlusIcon = ({ className }: IconProps) => (
|
||||
<PiUserPlus className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const UsersOnlineIcon = ({ className }: IconProps) => (
|
||||
<PiUsersThree className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const VideoIcon = ({ className }: IconProps) => (
|
||||
<PiVideoCamera className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const WarningIcon = ({ className }: IconProps) => (
|
||||
<PiWarning className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const XCircleIcon = ({ className }: IconProps) => (
|
||||
<PiXCircle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const XCloseIcon = ({ className }: IconProps) => (
|
||||
<PiX className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const XMarkIcon = ({ className }: IconProps) => <PiX className={cn('h-5 w-5', className)} />;
|
||||
@@ -1,611 +1,242 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import {
|
||||
PiArrowLeft,
|
||||
PiArrowRight,
|
||||
PiArrowsClockwise,
|
||||
PiCaretDown,
|
||||
PiCaretRight,
|
||||
PiChartBar,
|
||||
PiChatCircle,
|
||||
PiCheck,
|
||||
PiClipboardText,
|
||||
PiClock,
|
||||
PiCopy,
|
||||
PiCreditCard,
|
||||
PiDownloadSimple,
|
||||
PiGameController,
|
||||
PiGearSix,
|
||||
PiGift,
|
||||
PiGlobe,
|
||||
PiHardDrives,
|
||||
PiHouse,
|
||||
PiInfo,
|
||||
PiList,
|
||||
PiLock,
|
||||
PiMagnifyingGlass,
|
||||
PiMegaphone,
|
||||
PiMoon,
|
||||
PiPalette,
|
||||
PiPauseCircle,
|
||||
PiPencil,
|
||||
PiPencilSimple,
|
||||
PiPlay,
|
||||
PiPlus,
|
||||
PiShield,
|
||||
PiSignOut,
|
||||
PiSparkle,
|
||||
PiStar,
|
||||
PiStarFill,
|
||||
PiDiceFive,
|
||||
PiStop,
|
||||
PiSun,
|
||||
PiTrash,
|
||||
PiUploadSimple,
|
||||
PiUser,
|
||||
PiUsers,
|
||||
PiWallet,
|
||||
PiX,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
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 {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cabinet icons are thin wrappers over the Remnawave panel's own icon library
|
||||
* (Phosphor, via `react-icons/pi`, regular weight). 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
|
||||
export const HomeIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiHouse className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const BackIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiArrowLeft className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChevronRightIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretRight className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const MenuIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiList className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CloseIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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 CloseIcon = ({ className }: IconProps) => <PiX className={cn('h-5 w-5', className)} />;
|
||||
|
||||
export const ChevronDownIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretDown className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ArrowRightIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiArrowRight className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
// Actions
|
||||
export const SearchIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiMagnifyingGlass className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PlusIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiPlus className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const EditIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiPencilSimple className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const PencilIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiPencil className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const TrashIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiTrash className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const UploadIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiUploadSimple className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const DownloadIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiDownloadSimple className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const RefreshIcon = ({
|
||||
className,
|
||||
spinning = false,
|
||||
}: IconProps & { spinning?: boolean }) => (
|
||||
<svg
|
||||
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>
|
||||
<PiArrowsClockwise className={cn('h-4 w-4', spinning && 'animate-spin', className)} />
|
||||
);
|
||||
|
||||
export const SyncIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiArrowsClockwise className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
// Status
|
||||
export const CheckIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCheck className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const CopyIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCopy className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const XIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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 XIcon = ({ className }: IconProps) => <PiX className={cn('h-4 w-4', className)} />;
|
||||
|
||||
export const LockIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiLock className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const InfoIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiInfo className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
// User & People
|
||||
export const UserIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiUser className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const UsersIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiUsers className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const LogoutIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiSignOut className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
// Theme
|
||||
export const SunIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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 SunIcon = ({ className }: IconProps) => <PiSun className={cn('h-5 w-5', className)} />;
|
||||
|
||||
export const MoonIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiMoon className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PaletteIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiPalette className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
// Features & Content
|
||||
export const SubscriptionIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiSparkle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const WalletIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiWallet className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ChatIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiChatCircle className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const GiftIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiGift className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const ClockIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiClock className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const StarIcon = ({ className, filled }: IconProps & { filled?: boolean }) => (
|
||||
<svg
|
||||
className={cn('h-5 w-5', className)}
|
||||
fill={filled ? 'currentColor' : '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>
|
||||
);
|
||||
export const StarIcon = ({ className, filled }: IconProps & { filled?: boolean }) =>
|
||||
filled ? (
|
||||
<PiStarFill className={cn('h-5 w-5', className)} />
|
||||
) : (
|
||||
<PiStar className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const GamepadIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiGameController className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ClipboardIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiClipboardText className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CogIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiGearSix className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const WheelIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiDiceFive className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ShieldIcon = ({ className }: IconProps) => (
|
||||
<PiShield className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const ServerIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiHardDrives className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const CampaignIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiMegaphone className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
// 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) => (
|
||||
<svg
|
||||
className={cn('h-5 w-5', className)}
|
||||
@@ -623,114 +254,45 @@ export const RemnawaveIcon = ({ className }: IconProps) => (
|
||||
);
|
||||
|
||||
export const ChartIcon = ({ className }: IconProps) => (
|
||||
<PiChartBar className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
// Xray core brand logo (4-blade pinwheel) — matches the panel's node rows.
|
||||
// Not a Phosphor glyph; kept custom like RemnawaveIcon for brand fidelity.
|
||||
export const XrayIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
className={cn('h-4 w-4', className)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
className={cn('h-5 w-5', className)}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 35 35"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<path d="M16.6961 15.2606L16.5825 3.49701C16.5718 2.38439 15.025 2.11843 14.6433 3.16356L11.7279 11.1447C11.6384 11.3898 11.4566 11.5902 11.2213 11.7031L5.66765 14.3687C4.70841 14.8291 5.03635 16.2703 6.10036 16.2703H15.6962C16.2522 16.2703 16.7015 15.8166 16.6961 15.2606Z" />
|
||||
<path d="M18.6471 15.2703V5.88936C18.6471 4.84679 20.0428 4.49998 20.5308 5.4213L23.5833 11.1845C23.7 11.4049 23.8948 11.5737 24.1296 11.6578L31.5829 14.3289C32.6388 14.7073 32.3671 16.2703 31.2455 16.2703H19.6471C19.0948 16.2703 18.6471 15.8226 18.6471 15.2703Z" />
|
||||
<path d="M18.6471 31.4643V19.3784C18.6471 18.8261 19.0948 18.3784 19.6471 18.3784H29.2853C30.3376 18.3784 30.676 19.7947 29.7374 20.2704L24.1129 23.1208C23.889 23.2343 23.716 23.4278 23.6281 23.663L20.5839 31.8141C20.1941 32.8578 18.6471 32.5783 18.6471 31.4643Z" />
|
||||
<path d="M16.7059 28.9873V19.3784C16.7059 18.8261 16.2582 18.3784 15.7059 18.3784H3.83963C2.71522 18.3784 2.44656 19.9473 3.50691 20.3214L11.5457 23.1578C11.7987 23.247 12.0052 23.4342 12.1188 23.6772L14.8 29.4109C15.2531 30.3797 16.7059 30.0568 16.7059 28.9873Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GlobeIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiGlobe className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
export const PlayIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiPlay className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const StopIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiStop className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const ArrowPathIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiArrowsClockwise className={cn('h-4 w-4', className)} />
|
||||
);
|
||||
|
||||
export const PauseIcon = ({ className, style }: IconProps & { style?: React.CSSProperties }) => (
|
||||
<svg
|
||||
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 PauseIcon = ({ className, style }: IconProps & { style?: CSSProperties }) => (
|
||||
<PiPauseCircle className={cn('h-5 w-5', className)} style={style} />
|
||||
);
|
||||
|
||||
export const CreditCardIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCreditCard className={cn('h-5 w-5', className)} />
|
||||
);
|
||||
|
||||
@@ -21,173 +21,25 @@ import SuccessNotificationModal from '@/components/SuccessNotificationModal';
|
||||
import { PromptDialogHost } from '@/components/PromptDialogHost';
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher';
|
||||
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 { AppHeader } from './AppHeader';
|
||||
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 {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { motion } from 'framer-motion';
|
||||
import { newsApi } from '../../api/news';
|
||||
import { useHapticFeedback } from '../../platform/hooks/useHaptic';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { ArrowIcon, NewsIcon } from '@/components/icons';
|
||||
import type { NewsListItem } from '../../types/news';
|
||||
|
||||
// --- 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 ---
|
||||
|
||||
interface CategoryBadgeProps {
|
||||
@@ -427,35 +415,7 @@ export default function NewsSection() {
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-accent-400 to-accent-600">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
fill="currentColor"
|
||||
className="text-dark-950/20"
|
||||
/>
|
||||
<path
|
||||
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
<path
|
||||
d="M7 8h4M7 11h10M7 14h10M7 17h6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
<rect
|
||||
x="14"
|
||||
y="7"
|
||||
width="4"
|
||||
height="4"
|
||||
rx="0.5"
|
||||
fill="currentColor"
|
||||
className="text-dark-950"
|
||||
/>
|
||||
</svg>
|
||||
<NewsIcon className="h-[18px] w-[18px] text-dark-950" />
|
||||
</div>
|
||||
<span className="font-mono text-[11px] font-bold uppercase tracking-[0.18em] text-dark-500">
|
||||
{t('news.title')}
|
||||
@@ -469,11 +429,20 @@ export default function NewsSection() {
|
||||
|
||||
{/* Grid */}
|
||||
{items.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
{featured && <FeaturedCard item={featured} onClick={handleFeaturedClick} />}
|
||||
{regular.map((item, i) => (
|
||||
<NewsCardWrapper key={item.id} item={item} index={i} onCardClick={handleCardClick} />
|
||||
))}
|
||||
{regular.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:[&>*:last-child:nth-child(odd)]:col-span-2">
|
||||
{regular.map((item, i) => (
|
||||
<NewsCardWrapper
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={i}
|
||||
onCardClick={handleCardClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDown } from 'react-icons/pi';
|
||||
|
||||
import { CheckIcon, CopyIcon } from '@/components/icons';
|
||||
import type { PartnerCampaignInfo } from '../../api/partners';
|
||||
import { PARTNER_STATS } from '../../constants/partner';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
@@ -9,32 +11,8 @@ import { copyToClipboard } from '../../utils/clipboard';
|
||||
import { CampaignDetailStats } from './CampaignDetailStats';
|
||||
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 }) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretDown className={`h-5 w-5 transition-transform ${expanded ? 'rotate-180' : ''}`} />
|
||||
);
|
||||
|
||||
interface CampaignCardProps {
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { forwardRef, type ComponentPropsWithoutRef, type HTMLAttributes } from 'react';
|
||||
import { SearchIcon } from '@/components/icons';
|
||||
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
|
||||
export type CommandProps = ComponentPropsWithoutRef<typeof CommandPrimitive>;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CloseIcon } from '@/components/icons';
|
||||
import { backdrop, backdropTransition, scale, scaleTransition } from '../../motion/transitions';
|
||||
|
||||
export {
|
||||
@@ -16,19 +17,6 @@ export {
|
||||
Close as DialogClose,
|
||||
} 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
|
||||
const DialogContext = createContext<{ open: boolean }>({ open: false });
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import { CheckIcon, ChevronRightIcon, DotIcon } from '@/components/icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { usePlatform } from '@/platform';
|
||||
import { dropdown, dropdownTransition } from '../../motion/transitions';
|
||||
@@ -14,37 +15,6 @@ export {
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
} 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
|
||||
export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef<
|
||||
typeof DropdownMenuPrimitive.SubTrigger
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import { CloseIcon } from '@/components/icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { dropdown, dropdownTransition } from '../../motion/transitions';
|
||||
|
||||
@@ -11,19 +12,6 @@ export {
|
||||
Close as PopoverClose,
|
||||
} 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
|
||||
export interface PopoverContentProps extends ComponentPropsWithoutRef<
|
||||
typeof PopoverPrimitive.Content
|
||||
|
||||
@@ -2,36 +2,12 @@ import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, ChevronDownIcon } from '@/components/icons';
|
||||
import { usePlatform } from '@/platform';
|
||||
import { dropdown, dropdownTransition } from '../../motion/transitions';
|
||||
|
||||
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
|
||||
export interface SelectTriggerProps extends ComponentPropsWithoutRef<
|
||||
typeof SelectPrimitive.Trigger
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { CloseIcon } from '@/components/icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { usePlatform } from '@/platform';
|
||||
import {
|
||||
@@ -23,19 +24,6 @@ export {
|
||||
Close as SheetClose,
|
||||
} 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
|
||||
interface SheetContextValue {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import { ChevronRightIcon, SubscriptionIcon } from '@/components/icons';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
interface PurchaseCTAButtonProps {
|
||||
@@ -73,21 +74,10 @@ export default function PurchaseCTAButton({
|
||||
background: isExpired
|
||||
? 'rgba(255,59,92,0.12)'
|
||||
: 'rgba(var(--color-accent-400), 0.12)',
|
||||
color: accentColor,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accentColor}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
|
||||
</svg>
|
||||
<SubscriptionIcon className="h-[18px] w-[18px]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[15px] font-semibold text-dark-50">{buttonText}</div>
|
||||
@@ -96,20 +86,7 @@ export default function PurchaseCTAButton({
|
||||
</div>
|
||||
|
||||
{/* Right: chevron */}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</HoverBorderGradient>
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
import { useHaptic } from '../../platform';
|
||||
import { CalendarIcon, CheckIcon, ChevronRightIcon, DevicesIcon } from '@/components/icons';
|
||||
import type { SubscriptionListItem } from '../../types';
|
||||
|
||||
function formatDate(iso: string | null, locale?: string): string {
|
||||
@@ -136,15 +137,7 @@ export default function SubscriptionListCard({
|
||||
</span>
|
||||
<StatusBadge status={subscription.status} isTrial={isTrial} t={t} />
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 opacity-30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-4 w-4 shrink-0 opacity-30" />
|
||||
</div>
|
||||
|
||||
{/* Traffic mini progress bar */}
|
||||
@@ -177,29 +170,11 @@ export default function SubscriptionListCard({
|
||||
style={{ color: g.textSecondary }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" />
|
||||
<path d="M12 18h.01" />
|
||||
</svg>
|
||||
<DevicesIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
{subscription.device_limit}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</svg>
|
||||
<CalendarIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
{formatDate(subscription.end_date, i18n.language)}
|
||||
</span>
|
||||
{!isTrial &&
|
||||
@@ -213,19 +188,19 @@ export default function SubscriptionListCard({
|
||||
<span
|
||||
className={`flex items-center gap-1 ${enabled ? 'text-success-400/70' : 'text-error-400/50'}`}
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
{enabled ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
) : (
|
||||
{enabled ? (
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
) : (
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
)}
|
||||
</svg>
|
||||
</svg>
|
||||
)}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTheme } from '../../../hooks/useTheme';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
||||
import { getGlassColors } from '../../../utils/glassTheme';
|
||||
import { ArrowDownIcon, DevicesIcon, RestartIcon } from '@/components/icons';
|
||||
import type { Tariff, Subscription, PurchaseOptions } from '../../../types';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -169,35 +170,11 @@ export function TariffPickerGrid({
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
<ArrowDownIcon className="h-4 w-4 text-accent-400" />
|
||||
<span className="font-medium text-dark-200">{tariff.traffic_limit_label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||
/>
|
||||
</svg>
|
||||
<DevicesIcon className="h-4 w-4 text-dark-400" />
|
||||
<span className="text-dark-300">
|
||||
{tariff.device_limit === 0
|
||||
? '∞'
|
||||
@@ -206,19 +183,7 @@ export function TariffPickerGrid({
|
||||
</div>
|
||||
{tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M2.985 19.644l3.181-3.182"
|
||||
/>
|
||||
</svg>
|
||||
<RestartIcon className="h-4 w-4 text-dark-400" />
|
||||
<span className="text-dark-300">
|
||||
{t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TrashIcon } from '@/components/icons';
|
||||
import { subscriptionApi } from '../../../api/subscription';
|
||||
import { usePlatform } from '../../../platform';
|
||||
import { useDestructiveConfirm } from '../../../platform/hooks/useNativeDialog';
|
||||
@@ -72,19 +73,7 @@ export function DeleteSubscriptionSheet({
|
||||
disabled={deleteLoading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-error-400/20 bg-error-400/5 p-3.5 text-sm font-medium text-error-400 transition-colors hover:bg-error-400/10 disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
{t('subscription.delete', 'Удалить подписку')}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, DocumentIcon, XIcon } from '@/components/icons';
|
||||
|
||||
import { ticketsApi } from '../../api/tickets';
|
||||
|
||||
export interface MediaItem {
|
||||
@@ -150,19 +153,7 @@ export function MessageMediaGrid({
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-dark-700 px-3 py-2 text-sm text-dark-200 transition-colors hover:bg-dark-600"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
|
||||
/>
|
||||
</svg>
|
||||
<DocumentIcon className="h-4 w-4" />
|
||||
{item.caption || `Download ${item.type}`}
|
||||
</a>
|
||||
);
|
||||
@@ -180,15 +171,7 @@ export function MessageMediaGrid({
|
||||
className="absolute right-4 top-4 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white text-black shadow-xl transition-colors hover:bg-gray-200"
|
||||
onClick={closeFullscreen}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{photoItems.length > 1 && (
|
||||
@@ -199,15 +182,7 @@ export function MessageMediaGrid({
|
||||
onClick={() => setFullscreenIndex(fullscreenIndex - 1)}
|
||||
className="absolute left-4 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-xl transition-colors hover:bg-white disabled:opacity-30"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -215,15 +190,7 @@ export function MessageMediaGrid({
|
||||
onClick={() => setFullscreenIndex(fullscreenIndex + 1)}
|
||||
className="absolute right-4 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-black shadow-xl transition-colors hover:bg-white disabled:opacity-30"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 rounded-full bg-dark-950/70 px-3 py-1 text-sm text-white">
|
||||
{fullscreenIndex + 1} / {photoItems.length}
|
||||
|
||||
@@ -8,13 +8,6 @@ interface FortuneWheelProps {
|
||||
onSpinComplete: () => void;
|
||||
}
|
||||
|
||||
// Pre-generate sparkle positions to avoid recalculating on each render
|
||||
const SPARKLE_POSITIONS = Array.from({ length: 8 }, (_, i) => ({
|
||||
top: `${20 + ((i * 10) % 60)}%`,
|
||||
left: `${15 + ((i * 13) % 70)}%`,
|
||||
delay: `${i * 0.15}s`,
|
||||
}));
|
||||
|
||||
const FortuneWheel = memo(function FortuneWheel({
|
||||
prizes,
|
||||
isSpinning,
|
||||
@@ -206,6 +199,21 @@ const FortuneWheel = memo(function FortuneWheel({
|
||||
<filter id="textShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="#000" floodOpacity="0.7" />
|
||||
</filter>
|
||||
|
||||
{/* LED glow as a soft gradient instead of a CSS blur. CSS filters on
|
||||
cx/cy-positioned SVG elements mis-render under the spin's GPU
|
||||
compositing — phantom streaks toward the centre that are often
|
||||
invisible in screen recordings but visible live. */}
|
||||
<radialGradient id="ledGlowGrad">
|
||||
<stop offset="0%" stopColor="#FEF08A" stopOpacity="0.95" />
|
||||
<stop offset="45%" stopColor="#FDE047" stopOpacity="0.55" />
|
||||
<stop offset="100%" stopColor="#FDE047" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
|
||||
{/* SVG-native emoji shadow (not a CSS filter) for the same reason */}
|
||||
<filter id="emojiShadow" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2" floodColor="#000" floodOpacity="0.5" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Background shadow */}
|
||||
@@ -262,9 +270,9 @@ const FortuneWheel = memo(function FortuneWheel({
|
||||
className="led-glow"
|
||||
cx={dotX}
|
||||
cy={dotY}
|
||||
r={5}
|
||||
fill="#FEF08A"
|
||||
style={{ filter: 'blur(2px)', animationDelay: delay }}
|
||||
r={9}
|
||||
fill="url(#ledGlowGrad)"
|
||||
style={{ animationDelay: delay }}
|
||||
/>
|
||||
<circle
|
||||
className="led-dot"
|
||||
@@ -331,7 +339,7 @@ const FortuneWheel = memo(function FortuneWheel({
|
||||
dominantBaseline="middle"
|
||||
fontSize={prizes.length <= 6 ? '32' : '26'}
|
||||
transform={`rotate(${pos.rotation}, ${pos.x}, ${pos.y})`}
|
||||
style={{ filter: 'drop-shadow(0 2px 3px rgba(0,0,0,0.5))' }}
|
||||
filter="url(#emojiShadow)"
|
||||
>
|
||||
{prize.emoji}
|
||||
</text>
|
||||
@@ -400,25 +408,6 @@ const FortuneWheel = memo(function FortuneWheel({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sparkle effects when spinning - optimized with pre-calculated positions */}
|
||||
{isSpinning && (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
{SPARKLE_POSITIONS.map((pos, i) => (
|
||||
<div
|
||||
key={`sparkle-${i}`}
|
||||
className="absolute h-2 w-2 animate-ping rounded-full bg-yellow-300"
|
||||
style={{
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
animationDelay: pos.delay,
|
||||
animationDuration: '1.5s',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1207,7 +1207,7 @@
|
||||
"broadcasts": "Broadcasts",
|
||||
"users": "Users",
|
||||
"payments": "Payments",
|
||||
"remnawave": "RemnaWave",
|
||||
"remnawave": "Remnawave",
|
||||
"emailTemplates": "Email Templates",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"campaigns": "Campaigns",
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"notFound": "Payment method not found"
|
||||
},
|
||||
"remnawave": {
|
||||
"title": "RemnaWave",
|
||||
"title": "Remnawave",
|
||||
"subtitle": "Panel management and statistics",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Not configured",
|
||||
@@ -1719,9 +1719,9 @@
|
||||
"success": "Success",
|
||||
"runAutoSyncNow": "Run Auto Sync Now",
|
||||
"fromPanel": "From Panel",
|
||||
"fromPanelDesc": "Import users from RemnaWave to bot",
|
||||
"fromPanelDesc": "Import users from Remnawave to bot",
|
||||
"toPanel": "To Panel",
|
||||
"toPanelDesc": "Export users from bot to RemnaWave"
|
||||
"toPanelDesc": "Export users from bot to Remnawave"
|
||||
}
|
||||
},
|
||||
"wheel": {
|
||||
@@ -2128,7 +2128,7 @@
|
||||
"db_sqlite": "SQLite",
|
||||
"db_redis": "Redis",
|
||||
"sys_core": "Core & Debug",
|
||||
"sys_remnawave": "RemnaWave",
|
||||
"sys_remnawave": "Remnawave",
|
||||
"sys_webapi": "Web API",
|
||||
"sys_webhook": "Webhook",
|
||||
"sys_server": "Server status",
|
||||
@@ -2178,7 +2178,7 @@
|
||||
"deleteUser": "Delete users"
|
||||
},
|
||||
"deleteSubscription": {
|
||||
"warning": "Selected subscriptions will be permanently deleted from the bot and RemnaWave panel!",
|
||||
"warning": "Selected subscriptions will be permanently deleted from the bot and Remnawave panel!",
|
||||
"hint": "Users will lose VPN access for deleted subscriptions. This action cannot be undone.",
|
||||
"activePaidWarning": "{{count}} of {{total}} selected subscriptions are active paid",
|
||||
"forceDeleteConfirm": "I confirm deletion of active paid subscriptions"
|
||||
@@ -2186,7 +2186,7 @@
|
||||
"deleteUser": {
|
||||
"warning": "Selected users will be permanently deleted from the bot! All their subscriptions, balance, and data will be lost!",
|
||||
"hint": "This action cannot be undone. Users will need to restart the bot to create a new account.",
|
||||
"deleteFromPanel": "Also delete from RemnaWave panel"
|
||||
"deleteFromPanel": "Also delete from Remnawave panel"
|
||||
},
|
||||
"params": {
|
||||
"days": "Number of days",
|
||||
@@ -2370,16 +2370,16 @@
|
||||
"importError": "Invalid JSON file format",
|
||||
"importCreateError": "Failed to create some apps",
|
||||
"noAppsToImport": "No apps to import",
|
||||
"remnaWaveToggle": "Toggle RemnaWave source",
|
||||
"refreshRemnaWave": "Refresh from RemnaWave",
|
||||
"remnaWaveMode": "Showing apps from RemnaWave panel. Read-only mode.",
|
||||
"remnaWaveSettings": "RemnaWave Settings",
|
||||
"remnaWaveToggle": "Toggle Remnawave source",
|
||||
"refreshRemnaWave": "Refresh from Remnawave",
|
||||
"remnaWaveMode": "Showing apps from Remnawave panel. Read-only mode.",
|
||||
"remnaWaveSettings": "Remnawave Settings",
|
||||
"remnaWaveConfigUuid": "Config UUID",
|
||||
"remnaWaveConfigUuidHint": "UUID of subscription page config from RemnaWave panel",
|
||||
"remnaWaveConfigUuidHint": "UUID of subscription page config from Remnawave panel",
|
||||
"availableConfigs": "Available configs",
|
||||
"noConfigs": "No configs",
|
||||
"remnaWaveConnected": "RemnaWave connected",
|
||||
"remnaWaveDisconnected": "RemnaWave disconnected"
|
||||
"remnaWaveConnected": "Remnawave connected",
|
||||
"remnaWaveDisconnected": "Remnawave disconnected"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Ticket Management",
|
||||
@@ -2528,7 +2528,7 @@
|
||||
"daysShort": "d.",
|
||||
"serversTitle": "Servers",
|
||||
"externalSquadTitle": "External Squad",
|
||||
"externalSquadHint": "Assign a RemnaWave external squad for users on this tariff. When a subscription is created, the user will be automatically added to the selected squad.",
|
||||
"externalSquadHint": "Assign a Remnawave external squad for users on this tariff. When a subscription is created, the user will be automatically added to the selected squad.",
|
||||
"noExternalSquad": "No external squad",
|
||||
"externalSquadUsers": "users",
|
||||
"serversTabHint": "Select servers available on this tariff.",
|
||||
@@ -2607,7 +2607,7 @@
|
||||
"sync": "Sync",
|
||||
"syncing": "Syncing...",
|
||||
"syncNow": "Sync now",
|
||||
"noServers": "No servers. Sync with RemnaWave.",
|
||||
"noServers": "No servers. Sync with Remnawave.",
|
||||
"edit": "Edit Server",
|
||||
"originalName": "Original Name",
|
||||
"displayName": "Display Name",
|
||||
|
||||
@@ -1086,7 +1086,7 @@
|
||||
"promoOffers": "پیشنهادات تبلیغاتی",
|
||||
"promocodes": "کدهای تخفیف",
|
||||
"promoGroups": "گروههای تخفیف",
|
||||
"remnawave": "RemnaWave",
|
||||
"remnawave": "Remnawave",
|
||||
"users": "کاربران",
|
||||
"trafficUsage": "مصرف ترافیک",
|
||||
"updates": "بهروزرسانیها",
|
||||
@@ -1832,7 +1832,7 @@
|
||||
"db_sqlite": "SQLite",
|
||||
"db_redis": "Redis",
|
||||
"sys_core": "هسته و اشکالزدایی",
|
||||
"sys_remnawave": "RemnaWave",
|
||||
"sys_remnawave": "Remnawave",
|
||||
"sys_webapi": "Web API",
|
||||
"sys_webhook": "Webhook",
|
||||
"sys_server": "وضعیت سرور",
|
||||
@@ -1957,15 +1957,15 @@
|
||||
"importError": "فرمت فایل JSON نامعتبر",
|
||||
"importPreview": "پیشنمایش وارد کردن",
|
||||
"noAppsToImport": "هیچ برنامهای برای وارد کردن نیست",
|
||||
"refreshRemnaWave": "بازخوانی از RemnaWave",
|
||||
"refreshRemnaWave": "بازخوانی از Remnawave",
|
||||
"remnaWaveConfigUuid": "UUID پیکربندی",
|
||||
"remnaWaveConfigUuidHint": "UUID پیکربندی صفحه اشتراک از پنل RemnaWave",
|
||||
"remnaWaveMode": "نمایش برنامهها از پنل RemnaWave. حالت فقط خواندنی.",
|
||||
"remnaWaveSettings": "تنظیمات RemnaWave",
|
||||
"remnaWaveToggle": "تغییر منبع RemnaWave",
|
||||
"remnaWaveConfigUuidHint": "UUID پیکربندی صفحه اشتراک از پنل Remnawave",
|
||||
"remnaWaveMode": "نمایش برنامهها از پنل Remnawave. حالت فقط خواندنی.",
|
||||
"remnaWaveSettings": "تنظیمات Remnawave",
|
||||
"remnaWaveToggle": "تغییر منبع Remnawave",
|
||||
"noConfigs": "هیچ تنظیماتی وجود ندارد",
|
||||
"remnaWaveConnected": "RemnaWave متصل است",
|
||||
"remnaWaveDisconnected": "RemnaWave قطع است"
|
||||
"remnaWaveConnected": "Remnawave متصل است",
|
||||
"remnaWaveDisconnected": "Remnawave قطع است"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "مدیریت تیکتها",
|
||||
@@ -2107,7 +2107,7 @@
|
||||
"daysShort": "روز",
|
||||
"serversTitle": "سرورها",
|
||||
"externalSquadTitle": "گروه خارجی",
|
||||
"externalSquadHint": "یک گروه خارجی RemnaWave برای کاربران این تعرفه تعیین کنید. هنگام ایجاد اشتراک، کاربر به طور خودکار به گروه انتخاب شده اضافه میشود.",
|
||||
"externalSquadHint": "یک گروه خارجی Remnawave برای کاربران این تعرفه تعیین کنید. هنگام ایجاد اشتراک، کاربر به طور خودکار به گروه انتخاب شده اضافه میشود.",
|
||||
"noExternalSquad": "بدون گروه خارجی",
|
||||
"externalSquadUsers": "کاربران",
|
||||
"serversTabHint": "سرورهای موجود در این تعرفه را انتخاب کنید.",
|
||||
@@ -2186,7 +2186,7 @@
|
||||
"sync": "همگامسازی",
|
||||
"syncing": "در حال همگامسازی...",
|
||||
"syncNow": "همگامسازی الان",
|
||||
"noServers": "سروری نیست. با RemnaWave همگامسازی کنید.",
|
||||
"noServers": "سروری نیست. با Remnawave همگامسازی کنید.",
|
||||
"edit": "ویرایش سرور",
|
||||
"originalName": "نام اصلی",
|
||||
"displayName": "نام نمایشی",
|
||||
@@ -3138,7 +3138,7 @@
|
||||
"connected": "متصل",
|
||||
"disconnected": "تنظیم نشده",
|
||||
"noData": "بارگذاری دادهها ناموفق بود",
|
||||
"title": "RemnaWave",
|
||||
"title": "Remnawave",
|
||||
"subtitle": "مدیریت پنل و آمار",
|
||||
"tabs": {
|
||||
"overview": "نمای کلی",
|
||||
@@ -3246,9 +3246,9 @@
|
||||
"success": "موفق",
|
||||
"runAutoSyncNow": "اجرای همگامسازی خودکار",
|
||||
"fromPanel": "از پنل",
|
||||
"fromPanelDesc": "وارد کردن کاربران از RemnaWave به ربات",
|
||||
"fromPanelDesc": "وارد کردن کاربران از Remnawave به ربات",
|
||||
"toPanel": "به پنل",
|
||||
"toPanelDesc": "صادر کردن کاربران از ربات به RemnaWave"
|
||||
"toPanelDesc": "صادر کردن کاربران از ربات به Remnawave"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
@@ -3824,7 +3824,7 @@
|
||||
"deleteUser": "حذف کاربران"
|
||||
},
|
||||
"deleteSubscription": {
|
||||
"warning": "اشتراکهای انتخاب شده برای همیشه از ربات و پنل RemnaWave حذف خواهند شد!",
|
||||
"warning": "اشتراکهای انتخاب شده برای همیشه از ربات و پنل Remnawave حذف خواهند شد!",
|
||||
"hint": "کاربران دسترسی VPN اشتراکهای حذف شده را از دست خواهند داد. این عملیات قابل بازگشت نیست.",
|
||||
"activePaidWarning": "{{count}} از {{total}} اشتراک انتخابشده اشتراکهای فعال پرداختی هستند",
|
||||
"forceDeleteConfirm": "حذف اشتراکهای فعال پرداختی را تأیید میکنم"
|
||||
@@ -3832,7 +3832,7 @@
|
||||
"deleteUser": {
|
||||
"warning": "کاربران انتخاب شده برای همیشه از ربات حذف خواهند شد! تمام اشتراکها، موجودی و دادههای آنها از بین خواهد رفت!",
|
||||
"hint": "این عملیات قابل بازگشت نیست. کاربران باید ربات را دوباره راهاندازی کنند تا حساب جدید بسازند.",
|
||||
"deleteFromPanel": "همچنین از پنل RemnaWave حذف شود"
|
||||
"deleteFromPanel": "همچنین از پنل Remnawave حذف شود"
|
||||
},
|
||||
"params": {
|
||||
"days": "تعداد روز",
|
||||
|
||||
@@ -1229,7 +1229,7 @@
|
||||
"broadcasts": "Рассылки",
|
||||
"users": "Пользователи",
|
||||
"payments": "Платежи",
|
||||
"remnawave": "RemnaWave",
|
||||
"remnawave": "Remnawave",
|
||||
"emailTemplates": "Email-шаблоны",
|
||||
"paymentMethods": "Платёжные методы",
|
||||
"campaigns": "Кампании",
|
||||
@@ -1627,7 +1627,7 @@
|
||||
"notFound": "Метод оплаты не найден"
|
||||
},
|
||||
"remnawave": {
|
||||
"title": "RemnaWave",
|
||||
"title": "Remnawave",
|
||||
"subtitle": "Управление панелью и статистика",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Не настроено",
|
||||
@@ -1645,6 +1645,7 @@
|
||||
"totalUpload": "Отдача",
|
||||
"totalTraffic": "Всего",
|
||||
"online": "онлайн",
|
||||
"realtimeTitle": "Текущий трафик",
|
||||
"inbounds": "Inbounds",
|
||||
"outbounds": "Outbounds"
|
||||
},
|
||||
@@ -1744,9 +1745,9 @@
|
||||
"success": "Успешно",
|
||||
"runAutoSyncNow": "Запустить автосинхронизацию",
|
||||
"fromPanel": "Из панели",
|
||||
"fromPanelDesc": "Импорт пользователей из RemnaWave в бота",
|
||||
"fromPanelDesc": "Импорт пользователей из Remnawave в бота",
|
||||
"toPanel": "В панель",
|
||||
"toPanelDesc": "Экспорт пользователей из бота в RemnaWave"
|
||||
"toPanelDesc": "Экспорт пользователей из бота в Remnawave"
|
||||
}
|
||||
},
|
||||
"wheel": {
|
||||
@@ -2145,7 +2146,7 @@
|
||||
"db_sqlite": "SQLite",
|
||||
"db_redis": "Redis",
|
||||
"sys_core": "Ядро и отладка",
|
||||
"sys_remnawave": "RemnaWave",
|
||||
"sys_remnawave": "Remnawave",
|
||||
"sys_webapi": "Web API",
|
||||
"sys_webhook": "Webhook",
|
||||
"sys_server": "Статус сервера",
|
||||
@@ -2355,8 +2356,8 @@
|
||||
"Api Key": "API ключ",
|
||||
"Api Token": "API токен",
|
||||
"Webhook Secret": "Секрет вебхука",
|
||||
"Remnawave Url": "URL RemnaWave",
|
||||
"Remnawave Token": "Токен RemnaWave",
|
||||
"Remnawave Url": "URL Remnawave",
|
||||
"Remnawave Token": "Токен Remnawave",
|
||||
"Server Status Enabled": "Статус сервера",
|
||||
"Monitoring Enabled": "Мониторинг",
|
||||
"Backup Enabled": "Бэкап",
|
||||
@@ -2646,7 +2647,7 @@
|
||||
"SQLITE": "SQLite",
|
||||
"REDIS": "Redis",
|
||||
"CORE": "Бот",
|
||||
"REMNAWAVE": "RemnaWave",
|
||||
"REMNAWAVE": "Remnawave",
|
||||
"SERVER_STATUS": "Статус сервера",
|
||||
"MONITORING": "Мониторинг",
|
||||
"MAINTENANCE": "Обслуживание",
|
||||
@@ -2765,16 +2766,16 @@
|
||||
"importError": "Неверный формат JSON файла",
|
||||
"importCreateError": "Ошибка при создании приложений",
|
||||
"noAppsToImport": "Нет приложений для импорта",
|
||||
"remnaWaveToggle": "Переключить источник RemnaWave",
|
||||
"refreshRemnaWave": "Обновить данные из RemnaWave",
|
||||
"remnaWaveMode": "Отображаются приложения из панели RemnaWave. Режим только для чтения.",
|
||||
"remnaWaveSettings": "Настройки RemnaWave",
|
||||
"remnaWaveToggle": "Переключить источник Remnawave",
|
||||
"refreshRemnaWave": "Обновить данные из Remnawave",
|
||||
"remnaWaveMode": "Отображаются приложения из панели Remnawave. Режим только для чтения.",
|
||||
"remnaWaveSettings": "Настройки Remnawave",
|
||||
"remnaWaveConfigUuid": "UUID конфигурации",
|
||||
"remnaWaveConfigUuidHint": "UUID конфигурации страницы подписки из панели RemnaWave",
|
||||
"remnaWaveConfigUuidHint": "UUID конфигурации страницы подписки из панели Remnawave",
|
||||
"availableConfigs": "Доступные конфигурации",
|
||||
"noConfigs": "Нет настроенных конфигов",
|
||||
"remnaWaveConnected": "RemnaWave подключён",
|
||||
"remnaWaveDisconnected": "RemnaWave отключён"
|
||||
"remnaWaveConnected": "Remnawave подключён",
|
||||
"remnaWaveDisconnected": "Remnawave отключён"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Управление тикетами",
|
||||
@@ -2926,7 +2927,7 @@
|
||||
"daysShort": "дн.",
|
||||
"serversTitle": "Серверы",
|
||||
"externalSquadTitle": "Внешний сквад",
|
||||
"externalSquadHint": "Назначьте внешний сквад RemnaWave для пользователей этого тарифа. При создании подписки пользователь будет автоматически добавлен в выбранный сквад.",
|
||||
"externalSquadHint": "Назначьте внешний сквад Remnawave для пользователей этого тарифа. При создании подписки пользователь будет автоматически добавлен в выбранный сквад.",
|
||||
"noExternalSquad": "Без внешнего сквада",
|
||||
"externalSquadUsers": "пользователей",
|
||||
"serversTabHint": "Выберите серверы, доступные на этом тарифе.",
|
||||
@@ -3005,7 +3006,7 @@
|
||||
"sync": "Синхронизировать",
|
||||
"syncing": "Синхронизация...",
|
||||
"syncNow": "Синхронизировать сейчас",
|
||||
"noServers": "Нет серверов. Синхронизируйте с RemnaWave.",
|
||||
"noServers": "Нет серверов. Синхронизируйте с Remnawave.",
|
||||
"edit": "Редактировать сервер",
|
||||
"originalName": "Оригинальное название",
|
||||
"displayName": "Отображаемое название",
|
||||
@@ -3976,7 +3977,7 @@
|
||||
"deleteUser": "Удалить пользователей"
|
||||
},
|
||||
"deleteSubscription": {
|
||||
"warning": "Выбранные подписки будут безвозвратно удалены из бота и панели RemnaWave!",
|
||||
"warning": "Выбранные подписки будут безвозвратно удалены из бота и панели Remnawave!",
|
||||
"hint": "Пользователи потеряют доступ к VPN по удалённым подпискам. Это действие нельзя отменить.",
|
||||
"activePaidWarning": "{{count}} из {{total}} выбранных подписок: активные платные",
|
||||
"forceDeleteConfirm": "Подтверждаю удаление активных платных подписок"
|
||||
@@ -3984,7 +3985,7 @@
|
||||
"deleteUser": {
|
||||
"warning": "Выбранные пользователи будут безвозвратно удалены из бота! Все их подписки, баланс и данные будут потеряны!",
|
||||
"hint": "Это действие нельзя отменить. Пользователям придётся заново запустить бота для создания нового аккаунта.",
|
||||
"deleteFromPanel": "Также удалить из панели RemnaWave"
|
||||
"deleteFromPanel": "Также удалить из панели Remnawave"
|
||||
},
|
||||
"params": {
|
||||
"days": "Количество дней",
|
||||
|
||||
@@ -1086,7 +1086,7 @@
|
||||
"promoOffers": "促销优惠",
|
||||
"promocodes": "促销码",
|
||||
"promoGroups": "折扣组",
|
||||
"remnawave": "RemnaWave",
|
||||
"remnawave": "Remnawave",
|
||||
"users": "用户",
|
||||
"trafficUsage": "流量使用",
|
||||
"updates": "更新",
|
||||
@@ -1873,7 +1873,7 @@
|
||||
"db_sqlite": "SQLite",
|
||||
"db_redis": "Redis",
|
||||
"sys_core": "核心与调试",
|
||||
"sys_remnawave": "RemnaWave",
|
||||
"sys_remnawave": "Remnawave",
|
||||
"sys_webapi": "Web API",
|
||||
"sys_webhook": "Webhook",
|
||||
"sys_server": "服务器状态",
|
||||
@@ -1991,12 +1991,12 @@
|
||||
"importError": "无效的JSON文件格式",
|
||||
"importPreview": "导入预览",
|
||||
"noAppsToImport": "没有可导入的应用",
|
||||
"refreshRemnaWave": "从RemnaWave刷新",
|
||||
"refreshRemnaWave": "从Remnawave刷新",
|
||||
"remnaWaveConfigUuid": "配置UUID",
|
||||
"remnaWaveConfigUuidHint": "来自RemnaWave面板的订阅页面配置UUID",
|
||||
"remnaWaveMode": "显示来自RemnaWave面板的应用。只读模式。",
|
||||
"remnaWaveSettings": "RemnaWave设置",
|
||||
"remnaWaveToggle": "切换RemnaWave源",
|
||||
"remnaWaveConfigUuidHint": "来自Remnawave面板的订阅页面配置UUID",
|
||||
"remnaWaveMode": "显示来自Remnawave面板的应用。只读模式。",
|
||||
"remnaWaveSettings": "Remnawave设置",
|
||||
"remnaWaveToggle": "切换Remnawave源",
|
||||
"tabs": {
|
||||
"basic": "基本",
|
||||
"installation": "安装",
|
||||
@@ -2005,8 +2005,8 @@
|
||||
"additional": "附加"
|
||||
},
|
||||
"noConfigs": "未配置任何配置",
|
||||
"remnaWaveConnected": "RemnaWave 已连接",
|
||||
"remnaWaveDisconnected": "RemnaWave 未连接"
|
||||
"remnaWaveConnected": "Remnawave 已连接",
|
||||
"remnaWaveDisconnected": "Remnawave 未连接"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "工单管理",
|
||||
@@ -2147,7 +2147,7 @@
|
||||
"daysShort": "天",
|
||||
"serversTitle": "服务器",
|
||||
"externalSquadTitle": "外部小组",
|
||||
"externalSquadHint": "为此套餐的用户分配RemnaWave外部小组。创建订阅时,用户将自动添加到所选小组。",
|
||||
"externalSquadHint": "为此套餐的用户分配Remnawave外部小组。创建订阅时,用户将自动添加到所选小组。",
|
||||
"noExternalSquad": "无外部小组",
|
||||
"externalSquadUsers": "用户",
|
||||
"serversTabHint": "选择此套餐上可用的服务器。",
|
||||
@@ -2226,7 +2226,7 @@
|
||||
"sync": "同步",
|
||||
"syncing": "同步中...",
|
||||
"syncNow": "立即同步",
|
||||
"noServers": "暂无服务器。请与RemnaWave同步。",
|
||||
"noServers": "暂无服务器。请与Remnawave同步。",
|
||||
"edit": "编辑服务器",
|
||||
"originalName": "原始名称",
|
||||
"displayName": "显示名称",
|
||||
@@ -3146,7 +3146,7 @@
|
||||
"connected": "已连接",
|
||||
"disconnected": "未配置",
|
||||
"noData": "加载数据失败",
|
||||
"title": "RemnaWave",
|
||||
"title": "Remnawave",
|
||||
"subtitle": "面板管理和统计",
|
||||
"tabs": {
|
||||
"overview": "概览",
|
||||
@@ -3254,9 +3254,9 @@
|
||||
"success": "成功",
|
||||
"runAutoSyncNow": "立即运行自动同步",
|
||||
"fromPanel": "从面板",
|
||||
"fromPanelDesc": "从RemnaWave导入用户到机器人",
|
||||
"fromPanelDesc": "从Remnawave导入用户到机器人",
|
||||
"toPanel": "到面板",
|
||||
"toPanelDesc": "从机器人导出用户到RemnaWave"
|
||||
"toPanelDesc": "从机器人导出用户到Remnawave"
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
@@ -3823,7 +3823,7 @@
|
||||
"deleteUser": "删除用户"
|
||||
},
|
||||
"deleteSubscription": {
|
||||
"warning": "选中的订阅将从机器人和RemnaWave面板中永久删除!",
|
||||
"warning": "选中的订阅将从机器人和Remnawave面板中永久删除!",
|
||||
"hint": "用户将失去已删除订阅的VPN访问权限。此操作无法撤销。",
|
||||
"activePaidWarning": "所选订阅中有 {{count}} / {{total}} 是有效的付费订阅",
|
||||
"forceDeleteConfirm": "我确认删除有效的付费订阅"
|
||||
@@ -3831,7 +3831,7 @@
|
||||
"deleteUser": {
|
||||
"warning": "选中的用户将从机器人中永久删除!他们的所有订阅、余额和数据将丢失!",
|
||||
"hint": "此操作无法撤销。用户需要重新启动机器人以创建新账户。",
|
||||
"deleteFromPanel": "同时从RemnaWave面板删除"
|
||||
"deleteFromPanel": "同时从Remnawave面板删除"
|
||||
},
|
||||
"params": {
|
||||
"days": "天数",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminAppsApi } from '../api/adminApps';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { BackIcon } from '@/components/icons';
|
||||
|
||||
export default function AdminApps() {
|
||||
const { t } = useTranslation();
|
||||
@@ -10,7 +11,7 @@ export default function AdminApps() {
|
||||
const queryClient = useQueryClient();
|
||||
const { capabilities } = usePlatform();
|
||||
|
||||
// RemnaWave status
|
||||
// Remnawave status
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ['remnawave-status'],
|
||||
queryFn: adminAppsApi.getRemnaWaveStatus,
|
||||
@@ -45,15 +46,7 @@ export default function AdminApps() {
|
||||
onClick={() => navigate('/admin')}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
<BackIcon className="h-5 w-5 text-dark-400" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('admin.apps.title')}</h1>
|
||||
@@ -67,8 +60,8 @@ export default function AdminApps() {
|
||||
/>
|
||||
<span className="text-sm font-medium text-dark-200">
|
||||
{status?.enabled
|
||||
? t('admin.apps.remnaWaveConnected', 'RemnaWave connected')
|
||||
: t('admin.apps.remnaWaveDisconnected', 'RemnaWave not connected')}
|
||||
? t('admin.apps.remnaWaveConnected', 'Remnawave connected')
|
||||
: t('admin.apps.remnaWaveDisconnected', 'Remnawave not connected')}
|
||||
</span>
|
||||
</div>
|
||||
{status?.config_uuid && (
|
||||
|
||||
@@ -6,78 +6,14 @@ import type { TFunction } from 'i18next';
|
||||
import { rbacApi, AuditLogEntry, AuditLogFilters } from '@/api/rbac';
|
||||
import { PermissionGate } from '@/components/auth/PermissionGate';
|
||||
import { usePlatform } from '@/platform/hooks/usePlatform';
|
||||
|
||||
// === 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 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>
|
||||
);
|
||||
import {
|
||||
BackIcon,
|
||||
ChevronDownIcon,
|
||||
DownloadIcon,
|
||||
FilterIcon,
|
||||
RefreshIcon,
|
||||
SearchIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// === Constants ===
|
||||
|
||||
|
||||
@@ -20,137 +20,24 @@ import {
|
||||
type BanHealthResponse,
|
||||
} from '../api/banSystem';
|
||||
|
||||
// Icons
|
||||
const ShieldIcon = () => (
|
||||
<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 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 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 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>
|
||||
);
|
||||
import {
|
||||
ShieldIcon,
|
||||
UsersIcon,
|
||||
BanIcon,
|
||||
ServerIcon,
|
||||
AgentIcon,
|
||||
WarningIcon,
|
||||
RefreshIcon,
|
||||
ChartIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
TrafficIcon,
|
||||
ReportIcon,
|
||||
HealthIcon,
|
||||
ExclamationIcon,
|
||||
BackIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
type TabType =
|
||||
| 'dashboard'
|
||||
@@ -475,7 +362,11 @@ export default function AdminBanSystem() {
|
||||
};
|
||||
|
||||
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: 'punishments' as TabType, label: t('banSystem.tabs.punishments'), icon: <BanIcon /> },
|
||||
{ id: 'nodes' as TabType, label: t('banSystem.tabs.nodes'), icon: <ServerIcon /> },
|
||||
@@ -505,34 +396,10 @@ export default function AdminBanSystem() {
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="relative">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br from-error-500/20 to-warning-500/20">
|
||||
<svg
|
||||
className="h-10 w-10 text-error-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<ExclamationIcon className="h-10 w-10 text-error-400" />
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full border border-dark-600 bg-dark-800">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
</svg>
|
||||
<SettingsIcon className="h-3.5 w-3.5 text-dark-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,19 +433,7 @@ export default function AdminBanSystem() {
|
||||
onClick={() => window.history.back()}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dark-600 bg-dark-700 px-4 py-2 text-sm font-medium text-dark-200 transition-all duration-200 hover:border-dark-500 hover:bg-dark-600 hover:text-dark-100"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||
/>
|
||||
</svg>
|
||||
<BackIcon className="h-5 w-5" />
|
||||
{t('common.back')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -601,7 +456,7 @@ export default function AdminBanSystem() {
|
||||
<div className="flex items-center gap-3">
|
||||
<AdminBackButton />
|
||||
<div className="rounded-xl bg-error-500/20 p-3">
|
||||
<ShieldIcon />
|
||||
<ShieldIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-dark-100">{t('banSystem.title')}</h1>
|
||||
@@ -613,7 +468,7 @@ export default function AdminBanSystem() {
|
||||
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"
|
||||
>
|
||||
<RefreshIcon />
|
||||
<RefreshIcon className="h-5 w-5" />
|
||||
{t('common.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -683,7 +538,7 @@ export default function AdminBanSystem() {
|
||||
<StatCard
|
||||
title={t('banSystem.stats.totalRequests')}
|
||||
value={stats.total_requests.toLocaleString()}
|
||||
icon={<ChartIcon />}
|
||||
icon={<ChartIcon className="h-5 w-5" />}
|
||||
color="accent"
|
||||
/>
|
||||
<StatCard
|
||||
@@ -699,7 +554,7 @@ export default function AdminBanSystem() {
|
||||
<StatCard
|
||||
title={t('banSystem.stats.uptime')}
|
||||
value={formatUptime(stats.uptime_seconds)}
|
||||
icon={<ChartIcon />}
|
||||
icon={<ChartIcon className="h-5 w-5" />}
|
||||
color="info"
|
||||
/>
|
||||
</div>
|
||||
@@ -930,7 +785,7 @@ export default function AdminBanSystem() {
|
||||
<StatCard
|
||||
title={t('banSystem.agents.totalSent')}
|
||||
value={agents.summary.total_sent.toLocaleString()}
|
||||
icon={<ChartIcon />}
|
||||
icon={<ChartIcon className="h-5 w-5" />}
|
||||
color="accent"
|
||||
/>
|
||||
<StatCard
|
||||
@@ -1556,14 +1411,7 @@ export default function AdminBanSystem() {
|
||||
aria-label={t('common.close')}
|
||||
className="text-dark-400 hover:text-dark-200"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4 p-4">
|
||||
|
||||
@@ -11,95 +11,18 @@ import {
|
||||
} from '../api/adminBroadcasts';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { TelegramPreview, EmailPreview } from '../components/broadcasts/BroadcastPreview';
|
||||
|
||||
// Icons
|
||||
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 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>
|
||||
);
|
||||
import {
|
||||
BroadcastIcon,
|
||||
ChevronDownIcon,
|
||||
DocumentIcon,
|
||||
EmailIcon,
|
||||
PhotoIcon,
|
||||
RefreshIcon,
|
||||
TelegramIcon,
|
||||
UsersIcon,
|
||||
VideoIcon,
|
||||
XIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// Filter labels
|
||||
const FILTER_GROUP_LABEL_KEYS: Record<string, string> = {
|
||||
@@ -173,7 +96,10 @@ export default function AdminBroadcastCreate() {
|
||||
if (selectedButtons.length > 0) {
|
||||
const presetLabels: Record<string, string> = {
|
||||
balance: t('admin.broadcasts.btnBalance', 'Пополнить баланс'),
|
||||
partners: t('admin.broadcasts.btnPartners', 'Партнёрка'),
|
||||
// Бот отдаёт ключ кнопки как 'referrals' (см. BROADCAST_BUTTONS в admin.py),
|
||||
// раньше тут был 'partners' — из-за рассинхрона кнопка показывалась сырым
|
||||
// ключом 'referrals' вместо «Партнёрка» (Telegram-баг #602989).
|
||||
referrals: t('admin.broadcasts.btnPartners', 'Партнёрка'),
|
||||
promocode: t('admin.broadcasts.btnPromocode', 'Промокод'),
|
||||
connect: t('admin.broadcasts.btnConnect', 'Подключиться'),
|
||||
subscription: t('admin.broadcasts.btnSubscription', 'Подписка'),
|
||||
@@ -538,7 +464,7 @@ export default function AdminBroadcastCreate() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDownIcon />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{showFilters && (
|
||||
@@ -581,7 +507,7 @@ export default function AdminBroadcastCreate() {
|
||||
<AdminBackButton />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
|
||||
<BroadcastIcon />
|
||||
<BroadcastIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.create')}</h1>
|
||||
@@ -727,7 +653,7 @@ export default function AdminBroadcastCreate() {
|
||||
className="rounded-lg p-2 text-dark-400 hover:bg-dark-700 hover:text-error-400"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<XIcon />
|
||||
<XIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
{mediaPreview && (
|
||||
@@ -813,7 +739,7 @@ export default function AdminBroadcastCreate() {
|
||||
onClick={() => removeCustomButton(index)}
|
||||
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>
|
||||
</div>
|
||||
))}
|
||||
@@ -1013,7 +939,7 @@ export default function AdminBroadcastCreate() {
|
||||
disabled={!isValid || isPending || isUploading}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
{isPending ? <RefreshIcon /> : <BroadcastIcon />}
|
||||
{isPending ? <RefreshIcon /> : <BroadcastIcon className="h-6 w-6" />}
|
||||
{t('admin.broadcasts.send')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,74 +3,15 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
|
||||
// Icons
|
||||
|
||||
const StopIcon = () => (
|
||||
<svg className="h-4 w-4" 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>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
import {
|
||||
DocumentIcon,
|
||||
EmailIcon,
|
||||
PhotoIcon,
|
||||
RefreshIcon,
|
||||
StopIcon,
|
||||
TelegramIcon,
|
||||
VideoIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// Channel badge component
|
||||
function ChannelBadge({ channel }: { channel?: BroadcastChannel }) {
|
||||
@@ -240,7 +181,7 @@ export default function AdminBroadcastDetail() {
|
||||
onClick={() => refetch()}
|
||||
className="rounded-lg p-2 transition-colors hover:bg-dark-700"
|
||||
>
|
||||
<RefreshIcon />
|
||||
<RefreshIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,76 +4,15 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminBroadcastsApi } from '../api/adminBroadcasts';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
|
||||
// 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 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>
|
||||
);
|
||||
import {
|
||||
BackIcon,
|
||||
BroadcastIcon,
|
||||
DocumentIcon,
|
||||
PhotoIcon,
|
||||
PlusIcon,
|
||||
RefreshIcon,
|
||||
VideoIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// Status badge component
|
||||
const statusConfig: Record<string, { bg: string; text: string; labelKey: string }> = {
|
||||
@@ -194,7 +133,7 @@ export default function AdminBroadcasts() {
|
||||
</div>
|
||||
) : broadcasts.length === 0 ? (
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -20,9 +20,18 @@ import {
|
||||
type BulkActionType,
|
||||
type BulkActionParams,
|
||||
} from '../api/adminBulkActions';
|
||||
import { PiCaretDown } from 'react-icons/pi';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
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 { DropdownSelect, type DropdownOption } from '@/components/admin/bulkActions/DropdownSelect';
|
||||
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.)
|
||||
|
||||
// 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 ============
|
||||
|
||||
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 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>
|
||||
);
|
||||
|
||||
// ChevronExpandIcon keeps a custom `expanded` prop (not the barrel's
|
||||
// className-only API), so it stays local as a thin wrapper over the panel's
|
||||
// Phosphor caret, preserving the rotate-on-expand behavior.
|
||||
const ChevronExpandIcon = ({ expanded }: { expanded: boolean }) => (
|
||||
<svg
|
||||
<PiCaretDown
|
||||
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)
|
||||
@@ -632,7 +579,7 @@ export default function AdminBulkActions() {
|
||||
aria-label={t('admin.bulkActions.selectAll')}
|
||||
title={t('admin.bulkActions.selectAll')}
|
||||
>
|
||||
{table.getIsAllRowsSelected() && <CheckIcon />}
|
||||
{table.getIsAllRowsSelected() && <CheckIcon className="h-3 w-3" />}
|
||||
{table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && (
|
||||
<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')}
|
||||
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" />}
|
||||
</button>
|
||||
)}
|
||||
@@ -678,7 +625,7 @@ export default function AdminBulkActions() {
|
||||
: t('admin.bulkActions.selectUser', { name: userName })
|
||||
}
|
||||
>
|
||||
{row.getIsSelected() && <CheckIcon />}
|
||||
{row.getIsSelected() && <CheckIcon className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -988,7 +935,7 @@ export default function AdminBulkActions() {
|
||||
)}
|
||||
aria-pressed={trialOnly}
|
||||
>
|
||||
{trialOnly && <CheckIcon />}
|
||||
{trialOnly && <CheckIcon className="h-3 w-3" />}
|
||||
</button>
|
||||
<span
|
||||
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 { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||
import Twemoji from 'react-twemoji';
|
||||
// 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>
|
||||
);
|
||||
import { CampaignIcon, CheckIcon, LinkIcon, RefreshIcon } from '@/components/icons';
|
||||
|
||||
// Bonus type config
|
||||
const bonusTypeConfig: Record<
|
||||
@@ -283,7 +258,7 @@ export default function AdminCampaignCreate() {
|
||||
<AdminBackButton />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
|
||||
<CampaignIcon />
|
||||
<CampaignIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-dark-100">
|
||||
@@ -298,19 +273,7 @@ export default function AdminCampaignCreate() {
|
||||
{partnerId && partner && (
|
||||
<div className="rounded-xl border border-accent-500/20 bg-accent-500/5 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg
|
||||
className="mt-0.5 h-5 w-5 shrink-0 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
<LinkIcon className="mt-0.5 h-5 w-5 shrink-0 text-accent-400" />
|
||||
<p className="text-sm text-accent-300">
|
||||
{t('admin.campaigns.form.partnerAutoAssign', {
|
||||
name: partner.first_name || partner.username || `#${partnerId}`,
|
||||
@@ -556,7 +519,7 @@ export default function AdminCampaignCreate() {
|
||||
disabled={!isValid || createMutation.isPending}
|
||||
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')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -10,47 +10,7 @@ import { PARTNER_STATS } from '../constants/partner';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import { useHaptic } from '../platform';
|
||||
|
||||
// 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>
|
||||
);
|
||||
import { ChartIcon, ChevronDownIcon, CopyIcon, LinkIcon, UsersIcon } from '@/components/icons';
|
||||
|
||||
// Bonus type config
|
||||
const bonusTypeConfig: Record<
|
||||
@@ -205,7 +165,7 @@ export default function AdminCampaignStats() {
|
||||
<div className="flex items-center gap-3">
|
||||
<AdminBackButton to="/admin/campaigns" />
|
||||
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
|
||||
<ChartIcon />
|
||||
<ChartIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-xl font-semibold text-dark-100">{stats.name}</h1>
|
||||
@@ -240,7 +200,7 @@ export default function AdminCampaignStats() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between 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>
|
||||
</div>
|
||||
<button
|
||||
@@ -264,7 +224,7 @@ export default function AdminCampaignStats() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between 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>
|
||||
</div>
|
||||
<button
|
||||
@@ -489,15 +449,9 @@ export default function AdminCampaignStats() {
|
||||
{t('admin.campaigns.stats.users')} ({stats.registrations})
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
<ChevronDownIcon
|
||||
className={`h-5 w-5 text-dark-400 transition-transform ${showUsers ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{showUsers && (
|
||||
|
||||
@@ -4,7 +4,15 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tansta
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
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 { 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
|
||||
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 { AdminBackButton } from '../components/admin';
|
||||
import { Toggle } from '../components/admin/Toggle';
|
||||
|
||||
// Icons
|
||||
const ChannelIcon = () => (
|
||||
<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 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>
|
||||
);
|
||||
import {
|
||||
ChannelIcon,
|
||||
PlusIcon,
|
||||
RefreshIcon,
|
||||
TrashIcon,
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
EditIcon,
|
||||
LinkIcon,
|
||||
SettingsIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// Setting toggle row for global settings
|
||||
const CHANNEL_SETTING_KEYS = [
|
||||
@@ -266,7 +197,7 @@ function ChannelCard({
|
||||
{/* Link */}
|
||||
{hasLink && (
|
||||
<div className="mt-1.5 flex items-center gap-1 text-xs text-accent-400">
|
||||
<LinkIcon />
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<a
|
||||
href={channel.channel_link!}
|
||||
target="_blank"
|
||||
@@ -352,7 +283,7 @@ function ChannelCard({
|
||||
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"
|
||||
>
|
||||
<TrashIcon />
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
{t('admin.channelSubscriptions.delete')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -688,7 +619,7 @@ export default function AdminChannelSubscriptions() {
|
||||
<AdminBackButton />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
|
||||
<ChannelIcon />
|
||||
<ChannelIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-dark-100">
|
||||
@@ -752,7 +683,7 @@ export default function AdminChannelSubscriptions() {
|
||||
) : channels.length === 0 ? (
|
||||
<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">
|
||||
<ChannelIcon />
|
||||
<ChannelIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<p>{t('admin.channelSubscriptions.empty')}</p>
|
||||
</div>
|
||||
|
||||
@@ -9,152 +9,23 @@ const CABINET_VERSION = __APP_VERSION__;
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
|
||||
// Icons - styled like main navigation
|
||||
|
||||
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 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="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>
|
||||
);
|
||||
import {
|
||||
BackIcon,
|
||||
BanknotesIcon,
|
||||
ChartBarIcon,
|
||||
ChevronDownIcon,
|
||||
ExclamationIcon,
|
||||
MegaphoneIcon,
|
||||
PowerIcon,
|
||||
RefreshIcon,
|
||||
RestartIcon,
|
||||
ServerIcon,
|
||||
SparklesIcon,
|
||||
TagIcon,
|
||||
UsersIcon,
|
||||
UsersOnlineIcon,
|
||||
WalletIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
@@ -265,7 +136,7 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
|
||||
{hasError && (
|
||||
<div className="mb-3 rounded-lg border border-error-500/20 bg-error-500/10 p-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>
|
||||
</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'
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
<PowerIcon />
|
||||
<PowerIcon className="h-4 w-4" />
|
||||
{node.is_disabled ? t('adminDashboard.nodes.enable') : t('adminDashboard.nodes.disable')}
|
||||
</button>
|
||||
<button
|
||||
@@ -304,7 +175,7 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
|
||||
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"
|
||||
>
|
||||
<RestartIcon />
|
||||
<RestartIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -464,7 +335,7 @@ export default function AdminDashboard() {
|
||||
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"
|
||||
>
|
||||
<RefreshIcon />
|
||||
<RefreshIcon className="h-5 w-5" />
|
||||
{t('adminDashboard.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useIsTelegram } from '../platform/hooks/usePlatform';
|
||||
import { useNativeDialog } from '../platform/hooks/useNativeDialog';
|
||||
import { useNotify } from '@/platform';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { MailIcon, SaveIcon, EyeIcon, SendIcon, ResetIcon } from '@/components/icons';
|
||||
|
||||
// Hook to check if on mobile
|
||||
function useIsMobile() {
|
||||
@@ -30,55 +31,6 @@ function useIsMobile() {
|
||||
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> = {
|
||||
ru: 'RU',
|
||||
en: 'EN',
|
||||
@@ -390,7 +342,7 @@ function TemplateEditor({
|
||||
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"
|
||||
>
|
||||
<SaveIcon />
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{saveMutation.isPending ? t('common.loading') : t('common.save')}
|
||||
</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"
|
||||
>
|
||||
<EyeIcon />
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
{t('admin.emailTemplates.preview')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -423,7 +375,7 @@ function TemplateEditor({
|
||||
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"
|
||||
>
|
||||
<SendIcon />
|
||||
<SendIcon className="h-4 w-4" />
|
||||
{testMutation.isPending ? t('common.loading') : t('admin.emailTemplates.sendTest')}
|
||||
</button>
|
||||
|
||||
@@ -437,7 +389,7 @@ function TemplateEditor({
|
||||
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"
|
||||
>
|
||||
<ResetIcon />
|
||||
<ResetIcon className="h-4 w-4" />
|
||||
<span className="truncate">{t('admin.emailTemplates.resetDefault')}</span>
|
||||
</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" />
|
||||
<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">
|
||||
<MailIcon />
|
||||
<MailIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<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 { useHapticFeedback } from '../platform/hooks/useHaptic';
|
||||
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';
|
||||
|
||||
const AVAILABLE_LOCALES = ['ru', 'en', 'zh', 'fa'] as const;
|
||||
type LocaleCode = (typeof AVAILABLE_LOCALES)[number];
|
||||
|
||||
// --- 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 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>
|
||||
);
|
||||
|
||||
// --- Toolbar Button ---
|
||||
interface ToolbarButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -206,35 +139,6 @@ function generateSlug(title: string): string {
|
||||
.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 ---
|
||||
|
||||
const FAQ_EDITOR_EXTENSIONS = [
|
||||
@@ -415,28 +319,28 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
|
||||
isActive={editor.isActive('bold')}
|
||||
title={t('news.admin.toolbar.bold')}
|
||||
>
|
||||
<BoldIcon />
|
||||
<BoldIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive('italic')}
|
||||
title={t('news.admin.toolbar.italic')}
|
||||
>
|
||||
<ItalicIcon />
|
||||
<ItalicIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
isActive={editor.isActive('underline')}
|
||||
title={t('news.admin.toolbar.underline')}
|
||||
>
|
||||
<UnderlineIcon />
|
||||
<UnderlineIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
isActive={editor.isActive('strike')}
|
||||
title={t('news.admin.toolbar.strikethrough')}
|
||||
>
|
||||
<StrikeIcon />
|
||||
<StrikeIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-0.5 h-4 w-px bg-dark-700" />
|
||||
<ToolbarButton
|
||||
@@ -458,14 +362,14 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
|
||||
isActive={editor.isActive('orderedList')}
|
||||
title={t('news.admin.toolbar.orderedList')}
|
||||
>
|
||||
<ListOrderedIcon />
|
||||
<ListOrderedIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
isActive={editor.isActive('blockquote')}
|
||||
title={t('news.admin.toolbar.blockquote')}
|
||||
>
|
||||
<QuoteIcon />
|
||||
<QuoteIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-0.5 h-4 w-px bg-dark-700" />
|
||||
<ToolbarButton
|
||||
@@ -473,10 +377,10 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
|
||||
isActive={editor.isActive('highlight')}
|
||||
title={t('news.admin.toolbar.highlight')}
|
||||
>
|
||||
<HighlightIcon />
|
||||
<HighlightIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}>
|
||||
<LinkIcon />
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => mediaRef.current?.click()}
|
||||
@@ -486,7 +390,7 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
|
||||
{isUploading ? (
|
||||
<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>
|
||||
</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"
|
||||
title={t('admin.infoPages.faq.moveUp')}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<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"
|
||||
title={t('admin.infoPages.faq.moveDown')}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -713,7 +617,7 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
|
||||
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"
|
||||
>
|
||||
<PlusSmallIcon />
|
||||
<PlusSmallIcon className="h-5 w-5" />
|
||||
{t('admin.infoPages.faq.addQuestion')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1333,28 +1237,28 @@ export default function AdminInfoPageEditor() {
|
||||
isActive={editor.isActive('bold')}
|
||||
title={t('news.admin.toolbar.bold')}
|
||||
>
|
||||
<BoldIcon />
|
||||
<BoldIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive('italic')}
|
||||
title={t('news.admin.toolbar.italic')}
|
||||
>
|
||||
<ItalicIcon />
|
||||
<ItalicIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
isActive={editor.isActive('underline')}
|
||||
title={t('news.admin.toolbar.underline')}
|
||||
>
|
||||
<UnderlineIcon />
|
||||
<UnderlineIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
isActive={editor.isActive('strike')}
|
||||
title={t('news.admin.toolbar.strikethrough')}
|
||||
>
|
||||
<StrikeIcon />
|
||||
<StrikeIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-dark-700" />
|
||||
@@ -1395,21 +1299,21 @@ export default function AdminInfoPageEditor() {
|
||||
isActive={editor.isActive('orderedList')}
|
||||
title={t('news.admin.toolbar.orderedList')}
|
||||
>
|
||||
<ListOrderedIcon />
|
||||
<ListOrderedIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
isActive={editor.isActive('blockquote')}
|
||||
title={t('news.admin.toolbar.blockquote')}
|
||||
>
|
||||
<QuoteIcon />
|
||||
<QuoteIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
isActive={editor.isActive('codeBlock')}
|
||||
title={t('news.admin.toolbar.codeBlock')}
|
||||
>
|
||||
<CodeBlockIcon />
|
||||
<CodeBlockIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-dark-700" />
|
||||
@@ -1419,14 +1323,14 @@ export default function AdminInfoPageEditor() {
|
||||
isActive={editor.isActive({ textAlign: 'left' })}
|
||||
title={t('news.admin.toolbar.alignLeft')}
|
||||
>
|
||||
<AlignLeftIcon />
|
||||
<AlignLeftIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
isActive={editor.isActive({ textAlign: 'center' })}
|
||||
title={t('news.admin.toolbar.alignCenter')}
|
||||
>
|
||||
<AlignCenterIcon />
|
||||
<AlignCenterIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-dark-700" />
|
||||
@@ -1436,10 +1340,10 @@ export default function AdminInfoPageEditor() {
|
||||
isActive={editor.isActive('highlight')}
|
||||
title={t('news.admin.toolbar.highlight')}
|
||||
>
|
||||
<HighlightIcon />
|
||||
<HighlightIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}>
|
||||
<LinkIcon />
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => mediaInputRef.current?.click()}
|
||||
@@ -1449,7 +1353,7 @@ export default function AdminInfoPageEditor() {
|
||||
{isUploading ? (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -8,92 +8,11 @@ import { Toggle } from '../components/admin/Toggle';
|
||||
import { useHapticFeedback } from '../platform/hooks/useHaptic';
|
||||
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||
import { cn } from '../lib/utils';
|
||||
import { FileTextIcon, PencilIcon, PlusIcon, RefreshIcon, TrashIcon } from '@/components/icons';
|
||||
import type { InfoPageListItem, InfoPageType } from '../api/infoPages';
|
||||
|
||||
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 ---
|
||||
|
||||
const PageRow = memo(function PageRow({
|
||||
@@ -179,7 +98,7 @@ const PageRow = memo(function PageRow({
|
||||
title={t('admin.infoPages.delete')}
|
||||
aria-label={t('admin.infoPages.delete')}
|
||||
>
|
||||
<TrashIcon />
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,7 +293,7 @@ export default function AdminInfoPages() {
|
||||
</div>
|
||||
) : 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">
|
||||
<FileTextIcon />
|
||||
<FileTextIcon className="h-6 w-6" />
|
||||
<p className="mt-2">{t('admin.infoPages.noPages')}</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { PiCaretDown } from 'react-icons/pi';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -52,15 +53,7 @@ function isoToDatetimeLocal(iso: string): string {
|
||||
}
|
||||
|
||||
const ChevronDownIcon = ({ open }: { open: boolean }) => (
|
||||
<svg
|
||||
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>
|
||||
<PiCaretDown className={cn('h-5 w-5 transition-transform', open && 'rotate-180')} />
|
||||
);
|
||||
|
||||
// ============ Collapsible Section ============
|
||||
|
||||
@@ -26,17 +26,15 @@ import { useCurrency } from '../hooks/useCurrency';
|
||||
import { useChartColors } from '../hooks/useChartColors';
|
||||
import { CHART_COMMON } from '../constants/charts';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
|
||||
// Icons
|
||||
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>
|
||||
);
|
||||
import {
|
||||
ChartIcon,
|
||||
EmailIcon,
|
||||
TelegramSmallIcon,
|
||||
ArrowRightIcon,
|
||||
GiftIcon,
|
||||
ChevronLeftIcon as ChevronLeftSmall,
|
||||
ChevronRightIcon as ChevronRightSmall,
|
||||
} from '@/components/icons';
|
||||
|
||||
const TARIFF_PALETTE = ['#818cf8', '#34d399', '#f59e0b', '#ec4899', '#06b6d4', '#8b5cf6'];
|
||||
const GIFT_COLOR = '#a855f7';
|
||||
@@ -62,75 +60,15 @@ const PURCHASE_STATUS_OPTIONS: Array<PurchaseItemStatus | 'all'> = [
|
||||
|
||||
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
|
||||
function ContactDisplay({ type, value }: { type: 'email' | 'telegram'; value: string }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
@@ -184,7 +122,7 @@ function PurchaseCard({ item, formatPrice, lang, t }: PurchaseCardProps) {
|
||||
<ContactDisplay type={item.contact_type} value={item.contact_value} />
|
||||
{item.is_gift && item.gift_recipient_type && item.gift_recipient_value && (
|
||||
<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} />
|
||||
</span>
|
||||
)}
|
||||
@@ -212,7 +150,7 @@ function PurchaseCard({ item, formatPrice, lang, t }: PurchaseCardProps) {
|
||||
{item.is_gift && (
|
||||
<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">
|
||||
<GiftIcon />
|
||||
<GiftIcon className="h-3.5 w-3.5" />
|
||||
{t('admin.landings.purchases.gift')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -379,7 +317,7 @@ export default function AdminLandingStats() {
|
||||
<div className="flex items-center gap-3">
|
||||
<AdminBackButton to="/admin/landings" />
|
||||
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
|
||||
<ChartIcon />
|
||||
<ChartIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<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 { cn } from '../lib/utils';
|
||||
import { BackIcon, PlusIcon, TrashIcon, GripIcon } from '../components/icons/LandingIcons';
|
||||
import {
|
||||
EditIcon,
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
GiftIcon,
|
||||
SaveIcon,
|
||||
CopyIcon,
|
||||
StatsChartIcon,
|
||||
} from '@/components/icons';
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
@@ -26,69 +35,6 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
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 ============
|
||||
|
||||
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"
|
||||
title={t('admin.landings.statistics')}
|
||||
>
|
||||
<StatsChartIcon />
|
||||
<StatsChartIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
title={t('admin.landings.statistics')}
|
||||
>
|
||||
<StatsChartIcon />
|
||||
<StatsChartIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
@@ -470,7 +416,7 @@ export default function AdminLandings() {
|
||||
{saveOrderMutation.isPending ? (
|
||||
<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')}
|
||||
</button>
|
||||
|
||||
@@ -8,105 +8,14 @@ import { Toggle } from '../components/admin/Toggle';
|
||||
import { useHapticFeedback } from '../platform/hooks/useHaptic';
|
||||
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||
import type { NewsListItem } from '../types/news';
|
||||
|
||||
// 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 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>
|
||||
);
|
||||
import {
|
||||
PlusIcon,
|
||||
RefreshIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
StarIcon,
|
||||
NewsIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// --- 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})$/;
|
||||
@@ -195,7 +104,7 @@ const ArticleRow = memo(function ArticleRow({
|
||||
title={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>
|
||||
<Toggle
|
||||
checked={article.is_published}
|
||||
@@ -218,7 +127,7 @@ const ArticleRow = memo(function ArticleRow({
|
||||
title={t('news.admin.delete')}
|
||||
aria-label={t('news.admin.delete')}
|
||||
>
|
||||
<TrashIcon />
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -401,7 +310,7 @@ export default function AdminNews() {
|
||||
</div>
|
||||
) : 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">
|
||||
<NewsIcon />
|
||||
<NewsIcon className="h-6 w-6" />
|
||||
<p className="mt-2">{t('news.noNews')}</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -19,109 +19,25 @@ import { Toggle } from '../components/admin/Toggle';
|
||||
import { useHapticFeedback } from '../platform/hooks/useHaptic';
|
||||
import { cn } from '../lib/utils';
|
||||
import type { NewsCategory, NewsTag, NewsCreateRequest } from '../types/news';
|
||||
|
||||
// --- 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 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>
|
||||
);
|
||||
import {
|
||||
BoldIcon,
|
||||
ItalicIcon,
|
||||
UnderlineIcon,
|
||||
StrikeIcon,
|
||||
H1Icon,
|
||||
H2Icon,
|
||||
H3Icon,
|
||||
ListBulletIcon,
|
||||
ListOrderedIcon,
|
||||
QuoteIcon,
|
||||
CodeBlockIcon,
|
||||
ImageIcon,
|
||||
LinkIcon,
|
||||
AlignLeftIcon,
|
||||
AlignCenterIcon,
|
||||
HighlightIcon,
|
||||
UploadIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
// --- Toolbar Button ---
|
||||
interface ToolbarButtonProps {
|
||||
@@ -765,7 +681,7 @@ export default function AdminNewsCreate() {
|
||||
{isFeaturedImageUploading ? (
|
||||
<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>
|
||||
</button>
|
||||
@@ -848,28 +764,28 @@ export default function AdminNewsCreate() {
|
||||
isActive={editor.isActive('bold')}
|
||||
title={t('news.admin.toolbar.bold')}
|
||||
>
|
||||
<BoldIcon />
|
||||
<BoldIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive('italic')}
|
||||
title={t('news.admin.toolbar.italic')}
|
||||
>
|
||||
<ItalicIcon />
|
||||
<ItalicIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
isActive={editor.isActive('underline')}
|
||||
title={t('news.admin.toolbar.underline')}
|
||||
>
|
||||
<UnderlineIcon />
|
||||
<UnderlineIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
isActive={editor.isActive('strike')}
|
||||
title={t('news.admin.toolbar.strikethrough')}
|
||||
>
|
||||
<StrikeIcon />
|
||||
<StrikeIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<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 })}
|
||||
title={t('news.admin.toolbar.heading1')}
|
||||
>
|
||||
<H1Icon />
|
||||
<H1Icon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
@@ -910,21 +826,21 @@ export default function AdminNewsCreate() {
|
||||
isActive={editor.isActive('orderedList')}
|
||||
title={t('news.admin.toolbar.orderedList')}
|
||||
>
|
||||
<ListOrderedIcon />
|
||||
<ListOrderedIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
isActive={editor.isActive('blockquote')}
|
||||
title={t('news.admin.toolbar.blockquote')}
|
||||
>
|
||||
<QuoteIcon />
|
||||
<QuoteIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
isActive={editor.isActive('codeBlock')}
|
||||
title={t('news.admin.toolbar.codeBlock')}
|
||||
>
|
||||
<CodeBlockIcon />
|
||||
<CodeBlockIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-dark-700" />
|
||||
@@ -934,14 +850,14 @@ export default function AdminNewsCreate() {
|
||||
isActive={editor.isActive({ textAlign: 'left' })}
|
||||
title={t('news.admin.toolbar.alignLeft')}
|
||||
>
|
||||
<AlignLeftIcon />
|
||||
<AlignLeftIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
isActive={editor.isActive({ textAlign: 'center' })}
|
||||
title={t('news.admin.toolbar.alignCenter')}
|
||||
>
|
||||
<AlignCenterIcon />
|
||||
<AlignCenterIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-dark-700" />
|
||||
@@ -951,10 +867,10 @@ export default function AdminNewsCreate() {
|
||||
isActive={editor.isActive('highlight')}
|
||||
title={t('news.admin.toolbar.highlight')}
|
||||
>
|
||||
<HighlightIcon />
|
||||
<HighlightIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={addLink} title={t('news.admin.toolbar.link')}>
|
||||
<LinkIcon />
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => mediaInputRef.current?.click()}
|
||||
@@ -964,7 +880,7 @@ export default function AdminNewsCreate() {
|
||||
{isUploading ? (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -7,365 +7,91 @@ import { statsApi, type SystemInfo, type DashboardStats } from '@/api/admin';
|
||||
import { useAnimatedNumber } from '@/hooks/useAnimatedNumber';
|
||||
import { useTelegramSDK } from '@/hooks/useTelegramSDK';
|
||||
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 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)
|
||||
const icons = {
|
||||
'bar-chart': (
|
||||
<SvgIcon>
|
||||
<path d="M3 3v18h18" />
|
||||
<path d="M7 16V8" />
|
||||
<path d="M11 16V11" />
|
||||
<path d="M15 16V5" />
|
||||
<path d="M19 16v-3" />
|
||||
</SvgIcon>
|
||||
),
|
||||
'credit-card': (
|
||||
<SvgIcon>
|
||||
<rect x="2" y="5" width="20" height="14" rx="2" />
|
||||
<path d="M2 10h20" />
|
||||
</SvgIcon>
|
||||
),
|
||||
activity: (
|
||||
<SvgIcon>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</SvgIcon>
|
||||
),
|
||||
trending: (
|
||||
<SvgIcon>
|
||||
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
|
||||
<polyline points="16 7 22 7 22 13" />
|
||||
</SvgIcon>
|
||||
),
|
||||
users: (
|
||||
<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" />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</SvgIcon>
|
||||
),
|
||||
ticket: (
|
||||
<SvgIcon>
|
||||
<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" />
|
||||
<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>
|
||||
),
|
||||
'bar-chart': <ChartBarIcon />,
|
||||
'credit-card': <CreditCardIcon />,
|
||||
activity: <TrafficIcon />,
|
||||
trending: <StatsChartIcon />,
|
||||
users: <UsersIcon />,
|
||||
ticket: <TicketIcon />,
|
||||
'shield-alert': <ShieldIcon />,
|
||||
tag: <TagIcon />,
|
||||
gift: <GiftIcon />,
|
||||
percent: <PercentIcon />,
|
||||
sparkle: <SparklesIcon />,
|
||||
wallet: <WalletIcon />,
|
||||
layout: <CabinetIcon />,
|
||||
newspaper: <NewsIcon />,
|
||||
megaphone: <MegaphoneIcon />,
|
||||
send: <SendIcon />,
|
||||
pin: <PinIcon />,
|
||||
'circle-dot': <WheelIcon />,
|
||||
handshake: <PartnerIcon />,
|
||||
'arrow-up': <ArrowUpIcon />,
|
||||
network: <ShareIcon />,
|
||||
radio: <BroadcastIcon />,
|
||||
settings: <SettingsIcon />,
|
||||
app: <CabinetIcon />,
|
||||
server: <ServerIcon />,
|
||||
remnawave: <RemnawaveIcon />,
|
||||
mail: <MailIcon />,
|
||||
refresh: <SyncIcon />,
|
||||
shield: <ShieldIcon />,
|
||||
'user-check': <UserPlusIcon />,
|
||||
lock: <LockIcon />,
|
||||
scroll: <HistoryIcon />,
|
||||
'list-checks': <ClipboardIcon />,
|
||||
search: <SearchIcon />,
|
||||
'file-text': <FileTextIcon />,
|
||||
chevron: <ChevronRightIcon />,
|
||||
x: <XIcon />,
|
||||
} as const;
|
||||
|
||||
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',
|
||||
},
|
||||
{
|
||||
icon: <StatBotIcon />,
|
||||
icon: <StatBotIcon className="h-3.5 w-3.5" />,
|
||||
label: t('admin.panel.statsBot'),
|
||||
value: systemInfo?.bot_version ?? '--',
|
||||
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'),
|
||||
value: `v${CABINET_VERSION}`,
|
||||
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'),
|
||||
numericValue: trial,
|
||||
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'),
|
||||
numericValue: paid,
|
||||
delta: purchasedToday > 0 ? `+${purchasedToday}` : undefined,
|
||||
@@ -912,19 +638,19 @@ export default function AdminPanel() {
|
||||
cls: 'text-success-400',
|
||||
},
|
||||
{
|
||||
icon: <StatBotIcon />,
|
||||
icon: <StatBotIcon className="h-3.5 w-3.5" />,
|
||||
label: t('admin.panel.statsBot'),
|
||||
value: systemInfo?.bot_version ?? '--',
|
||||
cls: 'text-accent-400',
|
||||
},
|
||||
{
|
||||
icon: <StatTrialIcon />,
|
||||
icon: <StatTrialIcon className="h-3.5 w-3.5" />,
|
||||
label: t('admin.panel.statsTrials'),
|
||||
value: dashboardStats?.subscriptions.trial?.toLocaleString() ?? '--',
|
||||
cls: 'text-warning-400',
|
||||
},
|
||||
{
|
||||
icon: <StatPaidIcon />,
|
||||
icon: <StatPaidIcon className="h-3.5 w-3.5" />,
|
||||
label: t('admin.panel.statsPaid'),
|
||||
value: dashboardStats?.subscriptions.paid?.toLocaleString() ?? '--',
|
||||
delta:
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { partnerApi } from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { XIcon } from '@/components/icons';
|
||||
|
||||
// Status badge config — keys must match backend PartnerStatus enum values
|
||||
const statusConfig: Record<string, { labelKey: string; color: string; bgColor: string }> = {
|
||||
@@ -259,19 +260,7 @@ export default function AdminPartnerDetail() {
|
||||
className="rounded p-1 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400"
|
||||
title={t('admin.partnerDetail.campaigns.unassign')}
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,20 +5,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { partnerApi } from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { toNumber } from '../utils/inputHelpers';
|
||||
import { SettingsIcon } from '@/components/icons';
|
||||
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -126,7 +116,7 @@ export default function AdminPartnerSettings() {
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<AdminBackButton to="/admin/partners" />
|
||||
<div className="rounded-lg bg-accent-500/20 p-2 text-accent-400">
|
||||
<SettingsIcon />
|
||||
<SettingsIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.partners.settings')}</h1>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import { ChevronRightIcon, SettingsIcon } from '@/components/icons';
|
||||
|
||||
export default function AdminPartners() {
|
||||
const { t } = useTranslation();
|
||||
@@ -50,24 +51,7 @@ export default function AdminPartners() {
|
||||
className="rounded-lg bg-dark-800 p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
|
||||
title={t('admin.partners.settings')}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<SettingsIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -167,19 +151,7 @@ export default function AdminPartners() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="h-5 w-5 shrink-0 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5 shrink-0 text-dark-500" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -7,33 +7,7 @@ import { METHOD_LABELS } from '../constants/paymentMethods';
|
||||
import type { PromoGroupSimple } from '../types';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { createNumberInputHandler, 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 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>
|
||||
);
|
||||
import { BackIcon, CheckIcon, SaveIcon } from '@/components/icons';
|
||||
|
||||
export default function AdminPaymentMethodEdit() {
|
||||
const { t } = useTranslation();
|
||||
@@ -462,7 +436,7 @@ export default function AdminPaymentMethodEdit() {
|
||||
{updateMethodMutation.isPending ? (
|
||||
<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')}
|
||||
</button>
|
||||
|
||||
@@ -23,43 +23,7 @@ import {
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
|
||||
import type { PaymentMethodConfig } from '../types';
|
||||
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 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>
|
||||
);
|
||||
import { BackIcon, GripIcon, ChevronRightIcon, SaveIcon } from '@/components/icons';
|
||||
|
||||
interface SortableCardProps {
|
||||
config: PaymentMethodConfig;
|
||||
@@ -267,7 +231,7 @@ export default function AdminPaymentMethods() {
|
||||
{saveOrderMutation.isPending ? (
|
||||
<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')}
|
||||
</button>
|
||||
|
||||
@@ -6,41 +6,13 @@ import { adminPaymentsApi, type SearchStats } from '../api/adminPayments';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import type { PendingPayment, PaginatedResponse } from '../types';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
|
||||
// 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>
|
||||
);
|
||||
import {
|
||||
BackIcon,
|
||||
SearchIcon,
|
||||
CalendarIcon,
|
||||
RefreshIcon,
|
||||
CheckCircleIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
@@ -244,19 +216,7 @@ export default function AdminPayments() {
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => refetch()} className="btn-secondary flex items-center gap-2">
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
<RefreshIcon className="h-4 w-4" />
|
||||
{t('common.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -338,7 +298,7 @@ export default function AdminPayments() {
|
||||
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
|
||||
}`}
|
||||
>
|
||||
<CalendarIcon />
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
{t('admin.payments.periodCustom')}
|
||||
</button>
|
||||
|
||||
@@ -608,19 +568,7 @@ export default function AdminPayments() {
|
||||
) : (
|
||||
<div className="py-12 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
|
||||
<svg
|
||||
className="h-8 w-8 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<CheckCircleIcon className="h-8 w-8 text-dark-500" />
|
||||
</div>
|
||||
<div className="text-dark-400">{t('admin.payments.noPayments')}</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user