From 30ea4b6ecb66e62400eea67484981beb75d49755 Mon Sep 17 00:00:00 2001 From: fahed Date: Wed, 25 Mar 2026 18:10:42 +0300 Subject: [PATCH] Add route-based code splitting and loading skeletons - Lazy-load Dashboard, Comparison, Slides via React.lazy + Suspense - Main bundle reduced from 606KB to 256KB - Replace full-screen spinner with skeleton cards during load - Skeleton used for both initial data fetch and route transitions Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.css | 71 +++++++++++++++++++++++ src/App.tsx | 27 +++++---- src/components/shared/LoadingSkeleton.tsx | 27 +++++++++ 3 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 src/components/shared/LoadingSkeleton.tsx diff --git a/src/App.css b/src/App.css index d1a6a2e..0cd4b4c 100644 --- a/src/App.css +++ b/src/App.css @@ -1987,3 +1987,74 @@ html[dir="rtl"] .chart-export-btn.visible { direction: ltr !important; text-align: left !important; } + +/* ======================================== + Loading Skeleton + ======================================== */ + +@keyframes skeleton-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 1; } +} + +.skeleton-container { + padding: 80px 24px 24px; + max-width: 1200px; + margin: 0 auto; +} + +.skeleton-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 24px; +} + +.skeleton-card { + background: #f1f5f9; + border-radius: 12px; + padding: 20px; + animation: skeleton-pulse 1.5s ease-in-out infinite; +} + +.skeleton-card-wide { + grid-column: span 2; +} + +.skeleton-line { + background: #e2e8f0; + border-radius: 6px; +} + +.skeleton-line-short { + width: 40%; + height: 14px; + margin-bottom: 12px; +} + +.skeleton-line-tall { + width: 100%; + height: 120px; +} + +.skeleton-card-wide .skeleton-line-tall { + height: 200px; +} + +.skeleton-charts { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +@media (max-width: 768px) { + .skeleton-stats { + grid-template-columns: repeat(2, 1fr); + } + .skeleton-charts { + grid-template-columns: 1fr; + } + .skeleton-card-wide { + grid-column: span 1; + } +} diff --git a/src/App.tsx b/src/App.tsx index 33f65c0..86f8288 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ -import React, { useState, useEffect, useCallback, ReactNode } from 'react'; +import React, { useState, useEffect, useCallback, ReactNode, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'; -import Dashboard from './components/Dashboard'; -import Comparison from './components/Comparison'; -import Slides from './components/Slides'; + +const Dashboard = lazy(() => import('./components/Dashboard')); +const Comparison = lazy(() => import('./components/Comparison')); +const Slides = lazy(() => import('./components/Slides')); +import LoadingSkeleton from './components/shared/LoadingSkeleton'; import { fetchData, getCacheStatus, refreshData } from './services/dataService'; import { useLanguage } from './contexts/LanguageContext'; import type { MuseumRecord, CacheStatus, DataErrorType } from './types'; @@ -83,9 +85,8 @@ function App() { if (loading) { return ( -
-
-

{t('app.loading')}

+
+
); } @@ -191,11 +192,13 @@ function App() {
- - } /> - } /> - } /> - + }> + + } /> + } /> + } /> + + {/* Mobile Bottom Navigation */}