Merge pull request #34 from BEDOLAGA-DEV/PEDZ

Refactor App component to implement lazy loading for user and admin p…
This commit is contained in:
PEDZEO
2026-01-19 01:56:41 +03:00
committed by GitHub
3 changed files with 98 additions and 61 deletions

View File

@@ -1,39 +1,46 @@
import { lazy, Suspense } from 'react'
import { Routes, Route, Navigate, useLocation } from 'react-router-dom' import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from './store/auth' import { useAuthStore } from './store/auth'
import Layout from './components/layout/Layout' import Layout from './components/layout/Layout'
import PageLoader from './components/common/PageLoader' import PageLoader from './components/common/PageLoader'
import { saveReturnUrl } from './utils/token' import { saveReturnUrl } from './utils/token'
// Auth pages - load immediately (small)
import Login from './pages/Login' import Login from './pages/Login'
import TelegramCallback from './pages/TelegramCallback' import TelegramCallback from './pages/TelegramCallback'
import TelegramRedirect from './pages/TelegramRedirect' import TelegramRedirect from './pages/TelegramRedirect'
import DeepLinkRedirect from './pages/DeepLinkRedirect' import DeepLinkRedirect from './pages/DeepLinkRedirect'
import Dashboard from './pages/Dashboard'
import Subscription from './pages/Subscription'
import Balance from './pages/Balance'
import Referral from './pages/Referral'
import Support from './pages/Support'
import Profile from './pages/Profile'
import AdminTickets from './pages/AdminTickets'
import AdminSettings from './pages/AdminSettings'
import AdminApps from './pages/AdminApps'
import VerifyEmail from './pages/VerifyEmail' import VerifyEmail from './pages/VerifyEmail'
import Contests from './pages/Contests'
import Polls from './pages/Polls' // User pages - lazy load
import Info from './pages/Info' const Dashboard = lazy(() => import('./pages/Dashboard'))
import Wheel from './pages/Wheel' const Subscription = lazy(() => import('./pages/Subscription'))
import AdminWheel from './pages/AdminWheel' const Balance = lazy(() => import('./pages/Balance'))
import AdminTariffs from './pages/AdminTariffs' const Referral = lazy(() => import('./pages/Referral'))
import AdminServers from './pages/AdminServers' const Support = lazy(() => import('./pages/Support'))
import AdminPanel from './pages/AdminPanel' const Profile = lazy(() => import('./pages/Profile'))
import AdminDashboard from './pages/AdminDashboard' const Contests = lazy(() => import('./pages/Contests'))
import AdminBanSystem from './pages/AdminBanSystem' const Polls = lazy(() => import('./pages/Polls'))
import AdminBroadcasts from './pages/AdminBroadcasts' const Info = lazy(() => import('./pages/Info'))
import AdminPromocodes from './pages/AdminPromocodes' const Wheel = lazy(() => import('./pages/Wheel'))
import AdminCampaigns from './pages/AdminCampaigns'
import AdminUsers from './pages/AdminUsers' // Admin pages - lazy load (only for admins)
import AdminPayments from './pages/AdminPayments' const AdminPanel = lazy(() => import('./pages/AdminPanel'))
import AdminPromoOffers from './pages/AdminPromoOffers' const AdminTickets = lazy(() => import('./pages/AdminTickets'))
import AdminRemnawave from './pages/AdminRemnawave' const AdminSettings = lazy(() => import('./pages/AdminSettings'))
const AdminApps = lazy(() => import('./pages/AdminApps'))
const AdminWheel = lazy(() => import('./pages/AdminWheel'))
const AdminTariffs = lazy(() => import('./pages/AdminTariffs'))
const AdminServers = lazy(() => import('./pages/AdminServers'))
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'))
const AdminBanSystem = lazy(() => import('./pages/AdminBanSystem'))
const AdminBroadcasts = lazy(() => import('./pages/AdminBroadcasts'))
const AdminPromocodes = lazy(() => import('./pages/AdminPromocodes'))
const AdminCampaigns = lazy(() => import('./pages/AdminCampaigns'))
const AdminUsers = lazy(() => import('./pages/AdminUsers'))
const AdminPayments = lazy(() => import('./pages/AdminPayments'))
const AdminPromoOffers = lazy(() => import('./pages/AdminPromoOffers'))
const AdminRemnawave = lazy(() => import('./pages/AdminRemnawave'))
function ProtectedRoute({ children }: { children: React.ReactNode }) { function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthStore() const { isAuthenticated, isLoading } = useAuthStore()
@@ -73,6 +80,15 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout> return <Layout>{children}</Layout>
} }
// Suspense wrapper for lazy components
function LazyPage({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<PageLoader variant="dark" />}>
{children}
</Suspense>
)
}
function App() { function App() {
return ( return (
<Routes> <Routes>
@@ -90,7 +106,7 @@ function App() {
path="/" path="/"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Dashboard /> <LazyPage><Dashboard /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -98,7 +114,7 @@ function App() {
path="/subscription" path="/subscription"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Subscription /> <LazyPage><Subscription /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -106,7 +122,7 @@ function App() {
path="/balance" path="/balance"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Balance /> <LazyPage><Balance /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -114,7 +130,7 @@ function App() {
path="/referral" path="/referral"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Referral /> <LazyPage><Referral /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -122,7 +138,7 @@ function App() {
path="/support" path="/support"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Support /> <LazyPage><Support /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -130,7 +146,7 @@ function App() {
path="/profile" path="/profile"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Profile /> <LazyPage><Profile /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -138,7 +154,7 @@ function App() {
path="/contests" path="/contests"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Contests /> <LazyPage><Contests /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -146,7 +162,7 @@ function App() {
path="/polls" path="/polls"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Polls /> <LazyPage><Polls /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -154,7 +170,7 @@ function App() {
path="/info" path="/info"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Info /> <LazyPage><Info /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -162,7 +178,7 @@ function App() {
path="/wheel" path="/wheel"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Wheel /> <LazyPage><Wheel /></LazyPage>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -172,7 +188,7 @@ function App() {
path="/admin" path="/admin"
element={ element={
<AdminRoute> <AdminRoute>
<AdminPanel /> <LazyPage><AdminPanel /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -180,7 +196,7 @@ function App() {
path="/admin/tickets" path="/admin/tickets"
element={ element={
<AdminRoute> <AdminRoute>
<AdminTickets /> <LazyPage><AdminTickets /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -188,7 +204,7 @@ function App() {
path="/admin/settings" path="/admin/settings"
element={ element={
<AdminRoute> <AdminRoute>
<AdminSettings /> <LazyPage><AdminSettings /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -196,7 +212,7 @@ function App() {
path="/admin/apps" path="/admin/apps"
element={ element={
<AdminRoute> <AdminRoute>
<AdminApps /> <LazyPage><AdminApps /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -204,7 +220,7 @@ function App() {
path="/admin/wheel" path="/admin/wheel"
element={ element={
<AdminRoute> <AdminRoute>
<AdminWheel /> <LazyPage><AdminWheel /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -212,7 +228,7 @@ function App() {
path="/admin/tariffs" path="/admin/tariffs"
element={ element={
<AdminRoute> <AdminRoute>
<AdminTariffs /> <LazyPage><AdminTariffs /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -220,7 +236,7 @@ function App() {
path="/admin/servers" path="/admin/servers"
element={ element={
<AdminRoute> <AdminRoute>
<AdminServers /> <LazyPage><AdminServers /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -228,7 +244,7 @@ function App() {
path="/admin/dashboard" path="/admin/dashboard"
element={ element={
<AdminRoute> <AdminRoute>
<AdminDashboard /> <LazyPage><AdminDashboard /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -236,7 +252,7 @@ function App() {
path="/admin/ban-system" path="/admin/ban-system"
element={ element={
<AdminRoute> <AdminRoute>
<AdminBanSystem /> <LazyPage><AdminBanSystem /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -244,7 +260,7 @@ function App() {
path="/admin/broadcasts" path="/admin/broadcasts"
element={ element={
<AdminRoute> <AdminRoute>
<AdminBroadcasts /> <LazyPage><AdminBroadcasts /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -252,7 +268,7 @@ function App() {
path="/admin/promocodes" path="/admin/promocodes"
element={ element={
<AdminRoute> <AdminRoute>
<AdminPromocodes /> <LazyPage><AdminPromocodes /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -260,7 +276,7 @@ function App() {
path="/admin/campaigns" path="/admin/campaigns"
element={ element={
<AdminRoute> <AdminRoute>
<AdminCampaigns /> <LazyPage><AdminCampaigns /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -268,7 +284,7 @@ function App() {
path="/admin/users" path="/admin/users"
element={ element={
<AdminRoute> <AdminRoute>
<AdminUsers /> <LazyPage><AdminUsers /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -276,7 +292,7 @@ function App() {
path="/admin/payments" path="/admin/payments"
element={ element={
<AdminRoute> <AdminRoute>
<AdminPayments /> <LazyPage><AdminPayments /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -284,7 +300,7 @@ function App() {
path="/admin/promo-offers" path="/admin/promo-offers"
element={ element={
<AdminRoute> <AdminRoute>
<AdminPromoOffers /> <LazyPage><AdminPromoOffers /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />
@@ -292,7 +308,7 @@ function App() {
path="/admin/remnawave" path="/admin/remnawave"
element={ element={
<AdminRoute> <AdminRoute>
<AdminRemnawave /> <LazyPage><AdminRemnawave /></LazyPage>
</AdminRoute> </AdminRoute>
} }
/> />

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useCallback, ReactNode } from 'react' import { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'
interface ToastOptions { interface ToastOptions {
type?: 'success' | 'error' | 'info' | 'warning' type?: 'success' | 'error' | 'info' | 'warning'
@@ -29,22 +29,41 @@ export function useToast() {
export function ToastProvider({ children }: { children: ReactNode }) { export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]) const [toasts, setToasts] = useState<Toast[]>([])
const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map())
const showToast = useCallback((options: ToastOptions) => { const showToast = useCallback((options: ToastOptions) => {
const id = Date.now() const id = Date.now() + Math.random() // Avoid ID collision
const toast: Toast = { id, duration: 5000, type: 'info', ...options } const toast: Toast = { id, duration: 5000, type: 'info', ...options }
setToasts(prev => [...prev, toast]) setToasts(prev => [...prev, toast])
setTimeout(() => { const timer = setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id)) setToasts(prev => prev.filter(t => t.id !== id))
timersRef.current.delete(id)
}, toast.duration) }, toast.duration)
timersRef.current.set(id, timer)
}, []) }, [])
const removeToast = useCallback((id: number) => { const removeToast = useCallback((id: number) => {
// Clear timer when manually removing
const timer = timersRef.current.get(id)
if (timer) {
clearTimeout(timer)
timersRef.current.delete(id)
}
setToasts(prev => prev.filter(t => t.id !== id)) setToasts(prev => prev.filter(t => t.id !== id))
}, []) }, [])
// Cleanup all timers on unmount
useEffect(() => {
const timers = timersRef.current
return () => {
timers.forEach(timer => clearTimeout(timer))
timers.clear()
}
}, [])
return ( return (
<ToastContext.Provider value={{ showToast }}> <ToastContext.Provider value={{ showToast }}>
{children} {children}

View File

@@ -1,6 +1,8 @@
import { useEffect, useRef, useCallback, useState } from 'react' import { useEffect, useRef, useCallback, useState } from 'react'
import { useAuthStore } from '../store/auth' import { useAuthStore } from '../store/auth'
const isDev = import.meta.env.DEV
export interface WSMessage { export interface WSMessage {
type: string type: string
ticket_id?: number ticket_id?: number
@@ -80,7 +82,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}) {
wsRef.current = ws wsRef.current = ws
ws.onopen = () => { ws.onopen = () => {
console.log('[WS] Connected') if (isDev) console.log('[WS] Connected')
setIsConnected(true) setIsConnected(true)
reconnectAttemptsRef.current = 0 reconnectAttemptsRef.current = 0
optionsRef.current.onConnect?.() optionsRef.current.onConnect?.()
@@ -104,12 +106,12 @@ export function useWebSocket(options: UseWebSocketOptions = {}) {
optionsRef.current.onMessage?.(message) optionsRef.current.onMessage?.(message)
} catch (e) { } catch (e) {
console.error('[WS] Failed to parse message:', e) if (isDev) console.error('[WS] Failed to parse message:', e)
} }
} }
ws.onclose = (event) => { ws.onclose = (event) => {
console.log('[WS] Disconnected:', event.code, event.reason) if (isDev) console.log('[WS] Disconnected:', event.code, event.reason)
setIsConnected(false) setIsConnected(false)
optionsRef.current.onDisconnect?.() optionsRef.current.onDisconnect?.()
@@ -121,7 +123,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}) {
// Attempt to reconnect if not closed intentionally // Attempt to reconnect if not closed intentionally
if (event.code !== 1000 && reconnectAttemptsRef.current < maxReconnectAttempts) { if (event.code !== 1000 && reconnectAttemptsRef.current < maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000) const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000)
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current + 1})`) if (isDev) console.log(`[WS] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current + 1})`)
reconnectTimeoutRef.current = setTimeout(() => { reconnectTimeoutRef.current = setTimeout(() => {
reconnectAttemptsRef.current++ reconnectAttemptsRef.current++
@@ -131,10 +133,10 @@ export function useWebSocket(options: UseWebSocketOptions = {}) {
} }
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('[WS] Error:', error) if (isDev) console.error('[WS] Error:', error)
} }
} catch (e) { } catch (e) {
console.error('[WS] Failed to connect:', e) if (isDev) console.error('[WS] Failed to connect:', e)
} }
}, [accessToken, isAuthenticated, cleanup]) }, [accessToken, isAuthenticated, cleanup])