refactor: full codebase cleanup, dependency updates, and lint fixes

Phase 0: Remove ~920 lines of dead code (ThemeBentoPicker, PromoDiscountBadge,
AdminLayout, SettingsSidebar, MovingGradient, EmptyState, miniapp API, unused
types/functions/transitions/skeleton helpers). Fix API barrel file (add 20 missing exports).

Phase 1: Add ErrorBoundary (app/page/widget levels), centralize constants,
add axios timeout, fix staleTime:0 in React Query.

Phase 2: Consolidate hexToHsl, extract email validation, fix duplicate code.

Phase 3: Fix auth race condition (await checkAdminStatus), memoize useCurrency,
add WebSocket message validation.

Phase 4: Extract useBranding, useFeatureFlags, useScrollRestoration from AppShell.

Phase 5: Remove all eslint-disable react-hooks/exhaustive-deps (14 total),
simplify logger, remove deprecated hooks (useBackButton, useTelegramDnd,
useTelegramWebApp).

Dependencies: React 19, react-router 7, zustand 5, i18next 25, react-i18next 16,
eslint-plugin-react-refresh 0.5. Remove unused @lottiefiles/dotlottie-react.
Convert vite manualChunks to function-based approach for react-router v7 compat.

Lint: Fix logger.ts no-unused-expressions, fix react-refresh/only-export-components
in 6 Radix primitive files (const re-exports → direct re-exports), fix
WebSocketProvider exhaustive-deps. Result: 0 errors, 0 warnings.

Add CLAUDE.md to .gitignore.
This commit is contained in:
c0mrade
2026-02-06 01:35:12 +03:00
parent c5cad20a6f
commit 562ab7abf7
118 changed files with 1243 additions and 2746 deletions

View File

@@ -1,8 +1,7 @@
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminAppsApi } from '../api/adminApps';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
export default function AdminApps() {
@@ -11,8 +10,6 @@ export default function AdminApps() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
useBackButton(() => navigate('/admin'));
// RemnaWave status
const { data: status } = useQuery({
queryKey: ['remnawave-status'],

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
banSystemApi,
@@ -206,6 +206,8 @@ export default function AdminBanSystem() {
const [report, setReport] = useState<BanReportResponse | null>(null);
const [health, setHealth] = useState<BanHealthResponse | null>(null);
const [reportHours, setReportHours] = useState(24);
const reportHoursRef = useRef(reportHours);
reportHoursRef.current = reportHours;
const [settingLoading, setSettingLoading] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -240,19 +242,7 @@ export default function AdminBanSystem() {
const [searchQuery, setSearchQuery] = useState('');
const [actionLoading, setActionLoading] = useState<string | null>(null);
useEffect(() => {
loadStatus();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (status?.enabled && status?.configured) {
loadTabData(activeTab);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, status]);
const loadStatus = async () => {
const loadStatus = useCallback(async () => {
try {
setLoading(true);
const data = await banSystemApi.getStatus();
@@ -265,71 +255,84 @@ export default function AdminBanSystem() {
} finally {
setLoading(false);
}
};
}, [t]);
const loadTabData = async (tab: TabType) => {
try {
setLoading(true);
setError(null);
const loadTabData = useCallback(
async (tab: TabType) => {
try {
setLoading(true);
setError(null);
switch (tab) {
case 'dashboard': {
const statsData = await banSystemApi.getStats();
setStats(statsData);
break;
}
case 'users': {
const usersData = await banSystemApi.getUsers({ limit: 50 });
setUsers(usersData);
break;
}
case 'punishments': {
const punishmentsData = await banSystemApi.getPunishments();
setPunishments(punishmentsData);
break;
}
case 'nodes': {
const nodesData = await banSystemApi.getNodes();
setNodes(nodesData);
break;
}
case 'agents': {
const agentsData = await banSystemApi.getAgents();
setAgents(agentsData);
break;
}
case 'violations': {
const violationsData = await banSystemApi.getTrafficViolations();
setViolations(violationsData);
break;
}
case 'settings': {
const settingsData = await banSystemApi.getSettings();
setSettings(settingsData);
break;
}
case 'traffic': {
const trafficData = await banSystemApi.getTraffic();
setTraffic(trafficData);
break;
}
case 'reports': {
const reportData = await banSystemApi.getReport(reportHours);
setReport(reportData);
break;
}
case 'health': {
const healthData = await banSystemApi.getHealth();
setHealth(healthData);
break;
switch (tab) {
case 'dashboard': {
const statsData = await banSystemApi.getStats();
setStats(statsData);
break;
}
case 'users': {
const usersData = await banSystemApi.getUsers({ limit: 50 });
setUsers(usersData);
break;
}
case 'punishments': {
const punishmentsData = await banSystemApi.getPunishments();
setPunishments(punishmentsData);
break;
}
case 'nodes': {
const nodesData = await banSystemApi.getNodes();
setNodes(nodesData);
break;
}
case 'agents': {
const agentsData = await banSystemApi.getAgents();
setAgents(agentsData);
break;
}
case 'violations': {
const violationsData = await banSystemApi.getTrafficViolations();
setViolations(violationsData);
break;
}
case 'settings': {
const settingsData = await banSystemApi.getSettings();
setSettings(settingsData);
break;
}
case 'traffic': {
const trafficData = await banSystemApi.getTraffic();
setTraffic(trafficData);
break;
}
case 'reports': {
const reportData = await banSystemApi.getReport(reportHoursRef.current);
setReport(reportData);
break;
}
case 'health': {
const healthData = await banSystemApi.getHealth();
setHealth(healthData);
break;
}
}
} catch {
setError(t('banSystem.loadError'));
} finally {
setLoading(false);
}
} catch {
setError(t('banSystem.loadError'));
} finally {
setLoading(false);
},
[t],
);
useEffect(() => {
loadStatus();
}, [loadStatus]);
useEffect(() => {
if (status?.enabled && status?.configured) {
loadTabData(activeTab);
}
};
}, [activeTab, status, loadTabData]);
const handleSearch = async () => {
if (!searchQuery.trim()) {
@@ -403,8 +406,7 @@ export default function AdminBanSystem() {
if (activeTab === 'reports' && status?.enabled) {
loadTabData('reports');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportHours]);
}, [reportHours, activeTab, status, loadTabData]);
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -10,7 +10,6 @@ import {
CombinedBroadcastCreateRequest,
} from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const BroadcastIcon = () => (
@@ -119,8 +118,6 @@ export default function AdminBroadcastCreate() {
const queryClient = useQueryClient();
const fileInputRef = useRef<HTMLInputElement>(null);
useBackButton(() => navigate('/admin/broadcasts'));
// Channel selection
const [channel, setChannel] = useState<BroadcastChannel>('telegram');

View File

@@ -1,9 +1,8 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
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';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
@@ -159,8 +158,6 @@ export default function AdminBroadcastDetail() {
const broadcastId = id ? parseInt(id, 10) : null;
useBackButton(() => navigate('/admin/broadcasts'));
// Fetch broadcast details
const {
data: broadcast,

View File

@@ -1,9 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminBroadcastsApi } from '../api/adminBroadcasts';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -131,9 +130,6 @@ export default function AdminBroadcasts() {
const navigate = useNavigate();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [page, setPage] = useState(0);
const limit = 20;

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -11,8 +11,6 @@ import {
} from '../api/campaigns';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const CampaignIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
@@ -156,8 +154,6 @@ export default function AdminCampaignCreate() {
const navigate = useNavigate();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/campaigns'));
// Form state
const [name, setName] = useState('');
const [startParameter, setStartParameter] = useState('');

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -12,7 +12,6 @@ import {
import { AdminBackButton } from '../components/admin';
import { CheckIcon, CampaignIcon } from '../components/icons';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Bonus type config
const bonusTypeConfig: Record<
@@ -133,8 +132,6 @@ export default function AdminCampaignEdit() {
const campaignId = parseInt(id || '0');
useBackButton(() => navigate('/admin/campaigns'));
// Fetch campaign
const {
data: campaign,

View File

@@ -1,10 +1,9 @@
import { useState } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useParams, useNavigate, Link } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { campaignsApi, CampaignBonusType } from '../api/campaigns';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const CopyIcon = () => (
@@ -98,8 +97,6 @@ export default function AdminCampaignStats() {
const [copied, setCopied] = useState(false);
const [showUsers, setShowUsers] = useState(false);
useBackButton(() => navigate('/admin/campaigns'));
// Fetch stats
const {
data: stats,

View File

@@ -1,11 +1,10 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
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 { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Bonus type labels and colors
@@ -67,9 +66,6 @@ export default function AdminCampaigns() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
// Queries

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import {
statsApi,
@@ -10,7 +10,6 @@ import {
type RecentPaymentsResponse,
} from '../api/admin';
import { useCurrency } from '../hooks/useCurrency';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons - styled like main navigation
@@ -365,9 +364,6 @@ export default function AdminDashboard() {
const { formatAmount, currencySymbol } = useCurrency();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -380,7 +376,7 @@ export default function AdminDashboard() {
const [payments, setPayments] = useState<RecentPaymentsResponse | null>(null);
const [referrersTab, setReferrersTab] = useState<'earnings' | 'invited'>('earnings');
const fetchStats = async () => {
const fetchStats = useCallback(async () => {
try {
setLoading(true);
setError(null);
@@ -392,9 +388,9 @@ export default function AdminDashboard() {
} finally {
setLoading(false);
}
};
}, [t]);
const fetchExtendedStats = async () => {
const fetchExtendedStats = useCallback(async () => {
try {
const [referrersData, campaignsData, paymentsData] = await Promise.all([
statsApi.getTopReferrers(10),
@@ -407,7 +403,7 @@ export default function AdminDashboard() {
} catch (err) {
console.error('Failed to load extended stats:', err);
}
};
}, []);
useEffect(() => {
fetchStats();
@@ -418,8 +414,7 @@ export default function AdminDashboard() {
fetchExtendedStats();
}, 30000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [fetchStats, fetchExtendedStats]);
const handleRestartNode = async (uuid: string) => {
try {

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router';
import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminEmailTemplatesApi } from '../api/adminEmailTemplates';
@@ -23,7 +23,11 @@ export default function AdminEmailTemplatePreview() {
const state = location.state as PreviewState | null;
// Preview mutation
const previewMutation = useMutation({
const {
mutate: loadPreview,
isPending: previewPending,
isError: previewError,
} = useMutation({
mutationFn: () => {
if (!type || !lang || !state) {
throw new Error('Missing required data');
@@ -45,9 +49,8 @@ export default function AdminEmailTemplatePreview() {
navigate('/admin/email-templates');
return;
}
previewMutation.mutate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type, lang]);
loadPreview();
}, [type, lang, state, navigate, loadPreview]);
// Write preview HTML into iframe
useEffect(() => {
@@ -85,11 +88,11 @@ export default function AdminEmailTemplatePreview() {
{/* Preview content */}
<div className="flex-1 overflow-hidden rounded-xl border border-dark-700 bg-white">
{previewMutation.isPending ? (
{previewPending ? (
<div className="flex h-full items-center justify-center bg-dark-800">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
</div>
) : previewMutation.isError ? (
) : previewError ? (
<div className="flex h-full flex-col items-center justify-center gap-4 bg-dark-800">
<p className="text-dark-400">{t('common.error')}</p>
<button

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
import { useTranslation } from 'react-i18next';
import { RemnawaveIcon } from '../components/icons';

View File

@@ -1,10 +1,9 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
import type { PromoGroupSimple } from '../types';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
const BackIcon = () => (
@@ -72,9 +71,6 @@ export default function AdminPaymentMethodEdit() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin/payment-methods'));
// Fetch payment methods
const { data: methods, isLoading } = useQuery({
queryKey: ['admin-payment-methods'],

View File

@@ -1,8 +1,7 @@
import { useState, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
import {
DndContext,
@@ -11,9 +10,8 @@ import {
useSensor,
useSensors,
type DragEndEvent,
type DragStartEvent,
} from '@dnd-kit/core';
import { useTelegramDnd } from '../hooks/useTelegramDnd';
import {
arrayMove,
SortableContext,
@@ -239,9 +237,6 @@ export default function AdminPaymentMethods() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [methods, setMethods] = useState<PaymentMethodConfig[]>([]);
const [orderChanged, setOrderChanged] = useState(false);
const [toastMessage, setToastMessage] = useState<string | null>(null);
@@ -278,40 +273,18 @@ export default function AdminPaymentMethods() {
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
// Telegram swipe behavior for drag-and-drop
const {
onDragStart: onTelegramDragStart,
onDragEnd: onTelegramDragEnd,
onDragCancel: onTelegramDragCancel,
} = useTelegramDnd();
const handleDragStart = useCallback(
(_event: DragStartEvent) => {
onTelegramDragStart();
},
[onTelegramDragStart],
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
onTelegramDragEnd();
const { active, over } = event;
if (over && active.id !== over.id) {
setMethods((prev) => {
const oldIndex = prev.findIndex((m) => m.method_id === active.id);
const newIndex = prev.findIndex((m) => m.method_id === over.id);
if (oldIndex === -1 || newIndex === -1) return prev;
return arrayMove(prev, oldIndex, newIndex);
});
setOrderChanged(true);
}
},
[onTelegramDragEnd],
);
const handleDragCancel = useCallback(() => {
onTelegramDragCancel();
}, [onTelegramDragCancel]);
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setMethods((prev) => {
const oldIndex = prev.findIndex((m) => m.method_id === active.id);
const newIndex = prev.findIndex((m) => m.method_id === over.id);
if (oldIndex === -1 || newIndex === -1) return prev;
return arrayMove(prev, oldIndex, newIndex);
});
setOrderChanged(true);
}
}, []);
const handleSaveOrder = () => {
saveOrderMutation.mutate(methods.map((m) => m.method_id));
@@ -367,12 +340,7 @@ export default function AdminPaymentMethods() {
<div className="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
</div>
) : methods.length > 0 ? (
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext
items={methods.map((m) => m.method_id)}
strategy={verticalListSortingStrategy}

View File

@@ -1,11 +1,10 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminPaymentsApi } from '../api/adminPayments';
import { useCurrency } from '../hooks/useCurrency';
import type { PendingPayment, PaginatedResponse } from '../types';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// BackIcon
@@ -28,9 +27,6 @@ export default function AdminPayments() {
const { formatAmount, currencySymbol } = useCurrency();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [page, setPage] = useState(1);
const [methodFilter, setMethodFilter] = useState<string>('');
const [checkingPaymentId, setCheckingPaymentId] = useState<string | null>(null);

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -9,7 +9,6 @@ import {
PromoGroupUpdateRequest,
} from '../api/promocodes';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const PlusIcon = () => (
@@ -56,8 +55,6 @@ export default function AdminPromoGroupCreate() {
const queryClient = useQueryClient();
const isEdit = !!id;
useBackButton(() => navigate('/admin/promo-groups'));
// Form state
const [name, setName] = useState('');
const [serverDiscount, setServerDiscount] = useState<number | ''>(0);

View File

@@ -1,9 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { promocodesApi, PromoGroup } from '../api/promocodes';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -61,9 +60,6 @@ export default function AdminPromoGroups() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
// Query

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -12,7 +12,6 @@ import {
} from '../api/promoOffers';
import { adminUsersApi, UserListItem } from '../api/adminUsers';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const SendIcon = () => (
@@ -76,8 +75,6 @@ export default function AdminPromoOfferSend() {
const navigate = useNavigate();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/promo-offers'));
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
const [sendMode, setSendMode] = useState<'segment' | 'user'>('segment');
const [selectedTarget, setSelectedTarget] = useState<TargetSegment>('active');

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -10,7 +10,6 @@ import {
} from '../api/promoOffers';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
const getOfferTypeIcon = (offerType: string): string => {
return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁';
@@ -22,8 +21,6 @@ export default function AdminPromoOfferTemplateEdit() {
const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/promo-offers'));
// Form state
const [name, setName] = useState('');
const [messageText, setMessageText] = useState('');

View File

@@ -1,10 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import i18n from '../i18n';
import { promoOffersApi, PromoOfferLog, OFFER_TYPE_CONFIG, OfferType } from '../api/promoOffers';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -108,9 +107,6 @@ export default function AdminPromoOffers() {
const navigate = useNavigate();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [activeTab, setActiveTab] = useState<'templates' | 'logs'>('templates');
// Queries

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -10,7 +10,6 @@ import {
PromoCodeUpdateRequest,
PromoGroup,
} from '../api/promocodes';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -50,9 +49,6 @@ export default function AdminPromocodeCreate() {
const { capabilities } = usePlatform();
const isEdit = !!id;
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin/promocodes'));
// Form state
const [code, setCode] = useState('');
const [type, setType] = useState<PromoCodeType>('balance');

View File

@@ -1,10 +1,9 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { promocodesApi, PromoCodeType } from '../api/promocodes';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const EditIcon = () => (
@@ -89,8 +88,6 @@ export default function AdminPromocodeStats() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
useBackButton(() => navigate('/admin/promocodes'));
const {
data: promocode,
isLoading,

View File

@@ -1,10 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -113,9 +112,6 @@ export default function AdminPromocodes() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
const [copiedCode, setCopiedCode] = useState<string | null>(null);

View File

@@ -1,5 +1,5 @@
import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -9,7 +9,6 @@ import {
SystemStatsResponse,
AutoSyncStatus,
} from '../api/adminRemnawave';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
import {
ServerIcon,
@@ -878,9 +877,6 @@ export default function AdminRemnawave() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// State
const [activeTab, setActiveTab] = useState<TabType>('overview');
const [syncResults, setSyncResults] = useState<

View File

@@ -1,11 +1,9 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminRemnawaveApi, SquadWithLocalInfo } from '../api/adminRemnawave';
import { AdminBackButton } from '../components/admin';
import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons';
import { useBackButton } from '../platform/hooks/useBackButton';
// Country flags helper
const getCountryFlag = (code: string | null | undefined): string => {
if (!code) return '🌍';
@@ -55,8 +53,6 @@ export default function AdminRemnawaveSquadDetail() {
const { uuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
useBackButton(() => navigate('/admin/remnawave'));
// Fetch all squads and find the one we need
const {
data: squadsData,

View File

@@ -1,12 +1,11 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { serversApi, ServerUpdateRequest } from '../api/servers';
import { AdminBackButton } from '../components/admin';
import { ServerIcon } from '../components/icons';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Country flags (simple emoji mapping)
const getCountryFlag = (code: string | null): string => {
@@ -49,8 +48,6 @@ export default function AdminServerEdit() {
const serverId = parseInt(id || '0');
useBackButton(() => navigate('/admin/servers'));
const {
data: server,
isLoading,

View File

@@ -1,9 +1,8 @@
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { serversApi, ServerListItem } from '../api/servers';
import { SyncIcon, EditIcon, CheckIcon, XIcon, UsersIcon, GiftIcon } from '../components/icons';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// BackIcon
@@ -58,9 +57,6 @@ export default function AdminServers() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// Queries
const { data: serversData, isLoading } = useQuery({
queryKey: ['admin-servers'],

View File

@@ -1,12 +1,11 @@
import { useState, useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminSettingsApi, SettingDefinition } from '../api/adminSettings';
import { themeColorsApi } from '../api/themeColors';
import { useFavoriteSettings } from '../hooks/useFavoriteSettings';
import { MENU_SECTIONS, MenuItem, formatSettingKey } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
import { AnalyticsTab } from '../components/admin/AnalyticsTab';
import { BrandingTab } from '../components/admin/BrandingTab';
@@ -50,9 +49,6 @@ export default function AdminSettings() {
const navigate = useNavigate();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// State
const [activeSection, setActiveSection] = useState('branding');
const [searchQuery, setSearchQuery] = useState('');

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -12,7 +12,6 @@ import {
} from '../api/tariffs';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const PlusIcon = () => (
@@ -92,8 +91,6 @@ export default function AdminTariffCreate() {
const queryClient = useQueryClient();
const isEdit = !!id;
useBackButton(() => navigate('/admin/tariffs'));
// Step: null = type selection, 'period' or 'daily' = form
const [tariffType, setTariffType] = useState<TariffType>(null);

View File

@@ -1,9 +1,8 @@
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { tariffsApi, TariffListItem } from '../api/tariffs';
import { useDestructiveConfirm, useNotify } from '@/platform';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -76,9 +75,6 @@ export default function AdminTariffs() {
const notify = useNotify();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// Queries
const { data: tariffsData, isLoading } = useQuery({
queryKey: ['admin-tariffs'],

View File

@@ -1,10 +1,9 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminApi } from '../api/admin';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
const SettingsIcon = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
@@ -22,8 +21,6 @@ export default function AdminTicketSettings() {
const navigate = useNavigate();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/tickets'));
const {
data: settings,
isLoading,

View File

@@ -1,10 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin';
import { ticketsApi } from '../api/tickets';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
function AdminMessageMedia({
@@ -123,9 +122,6 @@ export default function AdminTickets() {
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [selectedTicketId, setSelectedTicketId] = useState<number | null>(null);
const [statusFilter, setStatusFilter] = useState<string>('');
const [replyText, setReplyText] = useState('');

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { useCurrency } from '../hooks/useCurrency';
@@ -11,7 +11,6 @@ import {
type UpdateSubscriptionRequest,
} from '../api/adminUsers';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// ============ Icons ============
@@ -82,8 +81,6 @@ export default function AdminUserDetail() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
useBackButton(() => navigate('/admin/users'));
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
const locale = localeMap[i18n.language] || 'ru-RU';

View File

@@ -1,10 +1,9 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useCurrency } from '../hooks/useCurrency';
import { useToast } from '../components/Toast';
import { adminUsersApi, type UserListItem, type UsersStatsResponse } from '../api/adminUsers';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// ============ Icons ============
@@ -398,9 +397,6 @@ export default function AdminUsers() {
const { showToast } = useToast();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [users, setUsers] = useState<UserListItem[]>([]);
const [stats, setStats] = useState<UsersStatsResponse | null>(null);
const [loading, setLoading] = useState(true);

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -11,7 +11,7 @@ import {
closestCenter,
type DragEndEvent,
} from '@dnd-kit/core';
import { useTelegramDnd } from '../hooks/useTelegramDnd';
import {
arrayMove,
SortableContext,
@@ -26,7 +26,6 @@ import { useDestructiveConfirm } from '@/platform';
import { useNotify } from '@/platform/hooks/useNotify';
import FortuneWheel from '../components/wheel/FortuneWheel';
import { ColorPicker } from '@/components/ColorPicker';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
@@ -295,9 +294,6 @@ export default function AdminWheel() {
const { capabilities } = usePlatform();
const notify = useNotify();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [activeTab, setActiveTab] = useState<Tab>('settings');
const [expandedPrizeId, setExpandedPrizeId] = useState<number | null>(null);
const [isCreating, setIsCreating] = useState(false);
@@ -433,22 +429,13 @@ export default function AdminWheel() {
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
// Telegram DnD support
const {
onDragStart: onTelegramDragStart,
onDragEnd: onTelegramDragEnd,
onDragCancel: onTelegramDragCancel,
} = useTelegramDnd();
const handleDragStart = useCallback(() => {
onTelegramDragStart();
// Collapse expanded card to avoid collision detection issues with varying heights
setExpandedPrizeId(null);
}, [onTelegramDragStart]);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
onTelegramDragEnd();
const { active, over } = event;
if (over && active.id !== over.id && config) {
// Use current displayed order (either local or from config)
@@ -468,7 +455,7 @@ export default function AdminWheel() {
}
}
},
[config, localPrizeOrder, onTelegramDragEnd],
[config, localPrizeOrder],
);
// Save prize order
@@ -894,7 +881,6 @@ export default function AdminWheel() {
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={onTelegramDragCancel}
>
<SortableContext
items={displayedPrizes.map((p) => p.id)}

View File

@@ -1,12 +1,13 @@
import { useState, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useSearchParams, useNavigate } from 'react-router';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuthStore } from '../store/auth';
import { balanceApi } from '../api/balance';
import { useCurrency } from '../hooks/useCurrency';
import { API } from '../config/constants';
import { useToast } from '../components/Toast';
import type { PaginatedResponse, Transaction } from '../types';
@@ -47,19 +48,18 @@ export default function Balance() {
const { showToast } = useToast();
const paymentHandledRef = useRef(false);
// Fetch balance directly from API with no caching
// Fetch balance from API
const { data: balanceData, refetch: refetchBalance } = useQuery({
queryKey: ['balance'],
queryFn: balanceApi.getBalance,
staleTime: 0,
staleTime: API.BALANCE_STALE_TIME_MS,
refetchOnMount: 'always',
});
// Refresh user data on mount to sync balance in store
useEffect(() => {
refreshUser();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [refreshUser]);
// Handle payment return from payment gateway
useEffect(() => {

View File

@@ -1,11 +1,11 @@
import { useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
import { subscriptionApi } from '../api/subscription';
import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
import { useBackButton, useHaptic } from '@/platform';
import { useTelegramSDK } from '../hooks/useTelegramSDK';
import { useHaptic } from '@/platform';
import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
import { useAuthStore } from '../store/auth';
import type { AppConfig, LocalizedText } from '../types';
@@ -15,7 +15,7 @@ export default function Connection() {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { user } = useAuthStore();
const { isTelegramWebApp } = useTelegramWebApp();
const { isTelegramWebApp } = useTelegramSDK();
const { impact: hapticImpact } = useHaptic();
const hapticRef = useRef(hapticImpact);
@@ -45,13 +45,6 @@ export default function Connection() {
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleGoBack]);
const handleBackButton = useCallback(() => {
hapticRef.current('light');
handleGoBack();
}, [handleGoBack]);
useBackButton(handleBackButton);
const resolveUrl = useCallback(
(url: string): string => {
if (!hasTemplates(url) || !appConfig?.subscriptionUrl) return url;

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useMemo, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/auth';
import { subscriptionApi } from '../api/subscription';
@@ -10,6 +10,7 @@ import { wheelApi } from '../api/wheel';
import Onboarding, { useOnboarding } from '../components/Onboarding';
import PromoOffersSection from '../components/PromoOffersSection';
import { useCurrency } from '../hooks/useCurrency';
import { API } from '../config/constants';
// Icons
const ArrowRightIcon = () => (
@@ -57,14 +58,13 @@ export default function Dashboard() {
// Refresh user data on mount
useEffect(() => {
refreshUser();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [refreshUser]);
// Fetch balance from API with no caching
// Fetch balance from API
const { data: balanceData } = useQuery({
queryKey: ['balance'],
queryFn: balanceApi.getBalance,
staleTime: 0,
staleTime: API.BALANCE_STALE_TIME_MS,
refetchOnMount: 'always',
});
@@ -72,7 +72,7 @@ export default function Dashboard() {
queryKey: ['subscription'],
queryFn: subscriptionApi.getSubscription,
retry: false,
staleTime: 0,
staleTime: API.BALANCE_STALE_TIME_MS,
refetchOnMount: 'always',
});
@@ -167,7 +167,7 @@ export default function Dashboard() {
const lastRefresh = localStorage.getItem('traffic_refresh_ts');
const now = Date.now();
const cacheMs = 30 * 1000;
const cacheMs = API.TRAFFIC_CACHE_MS;
if (lastRefresh && now - parseInt(lastRefresh, 10) < cacheMs) {
const elapsed = now - parseInt(lastRefresh, 10);
@@ -179,8 +179,7 @@ export default function Dashboard() {
}
refreshTrafficMutation.mutate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscription]);
}, [subscription, refreshTrafficMutation]);
// User has no subscription if API returns has_subscription: false
const hasNoSubscription = subscriptionResponse?.has_subscription === false && !subLoading;
@@ -243,8 +242,8 @@ export default function Dashboard() {
// Calculate traffic percentage color
const getTrafficColor = (percent: number) => {
if (percent > 90) return 'bg-error-500';
if (percent > 70) return 'bg-warning-500';
if (percent > API.TRAFFIC_CRITICAL_PERCENT) return 'bg-error-500';
if (percent > API.TRAFFIC_WARN_PERCENT) return 'bg-warning-500';
return 'bg-success-500';
};

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useSearchParams, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { brandingApi } from '../api/branding';

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { useNavigate, useLocation, useSearchParams } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '../store/auth';
import { authApi } from '../api/auth';
import { isValidEmail } from '../utils/validation';
import {
brandingApi,
getCachedBranding,
@@ -139,8 +140,7 @@ export default function Login() {
setError('');
// Валидация email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email.trim() || !emailRegex.test(email.trim())) {
if (!email.trim() || !isValidEmail(email.trim())) {
setError(t('auth.invalidEmail', 'Please enter a valid email address'));
return;
}
@@ -200,8 +200,7 @@ export default function Login() {
e.preventDefault();
setForgotPasswordError('');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!forgotPasswordEmail.trim() || !emailRegex.test(forgotPasswordEmail.trim())) {
if (!forgotPasswordEmail.trim() || !isValidEmail(forgotPasswordEmail.trim())) {
setForgotPasswordError(t('auth.invalidEmail', 'Please enter a valid email address'));
return;
}

View File

@@ -1,10 +1,11 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { useAuthStore } from '../store/auth';
import { authApi } from '../api/auth';
import { isValidEmail } from '../utils/validation';
import {
notificationsApi,
NotificationSettings,
@@ -196,8 +197,7 @@ export default function Profile() {
setError(null);
setSuccess(null);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email.trim() || !emailRegex.test(email.trim())) {
if (!email.trim() || !isValidEmail(email.trim())) {
setError(t('profile.invalidEmail', 'Please enter a valid email address'));
return;
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useSearchParams, Link, useNavigate } from 'react-router-dom';
import { useSearchParams, Link, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { authApi } from '../api/auth';
import LanguageSwitcher from '../components/LanguageSwitcher';

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router';
import { AxiosError } from 'axios';
import { subscriptionApi } from '../api/subscription';
import { promoApi } from '../api/promo';
@@ -537,8 +537,7 @@ export default function Subscription() {
}
refreshTrafficMutation.mutate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscription]);
}, [subscription, refreshTrafficMutation]);
// Auto-scroll to switch tariff modal when it appears
useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/auth';

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '../store/auth';

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useNavigate, useParams, useSearchParams } from 'react-router';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
@@ -8,7 +8,7 @@ import { balanceApi } from '../api/balance';
import { useCurrency } from '../hooks/useCurrency';
import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit';
import { useCloseOnSuccessNotification } from '../store/successNotification';
import { useBackButton, useHaptic, usePlatform } from '@/platform';
import { useHaptic, usePlatform } from '@/platform';
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import type { PaymentMethod } from '../types';
import BentoCard from '../components/ui/BentoCard';
@@ -108,9 +108,6 @@ export default function TopUpAmount() {
navigate(returnTo || '/balance', { replace: true });
}, [navigate, returnTo]);
// Telegram back button
useBackButton(handleNavigateBack);
// Keyboard: Escape to go back
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {

View File

@@ -1,11 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
import { balanceApi } from '../api/balance';
import { useCurrency } from '../hooks/useCurrency';
import { useBackButton } from '@/platform';
import { Card } from '@/components/data-display/Card';
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
@@ -15,8 +14,6 @@ export default function TopUpMethodSelect() {
const [searchParams] = useSearchParams();
const { formatAmount, currencySymbol } = useCurrency();
useBackButton(() => navigate('/balance'));
const { data: paymentMethods, isLoading } = useQuery({
queryKey: ['payment-methods'],
queryFn: balanceApi.getPaymentMethods,

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useSearchParams, Link, useNavigate } from 'react-router-dom';
import { useSearchParams, Link, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { authApi } from '../api/auth';
import { useAuthStore } from '../store/auth';