Skip to content

Commit b36235a

Browse files
committed
feat: enhance loading experience with new resource loading indicators and improved state management in LearningProgressSection
1 parent 7907df6 commit b36235a

File tree

3 files changed

+205
-93
lines changed

3 files changed

+205
-93
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import { Loader2 } from 'lucide-react';
3+
4+
/**
5+
* ResourceLoadingIndicator - A specialized loading component for resource calculations
6+
* This shows in individual league cards while resource data is being calculated
7+
*/
8+
const ResourceLoadingIndicator = ({
9+
isLoading = false,
10+
completedCount = 0,
11+
totalCount = 0,
12+
compact = false
13+
}) => {
14+
if (!isLoading) return null;
15+
16+
if (compact) {
17+
return (
18+
<div className="inline-flex items-center space-x-1">
19+
<Loader2 className="w-3 h-3 animate-spin text-blue-500" />
20+
<span className="text-xs text-gray-500">Loading...</span>
21+
</div>
22+
);
23+
}
24+
25+
return (
26+
<div className="rounded-lg p-3 text-center">
27+
<div className="flex items-center justify-center space-x-2">
28+
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
29+
<span className="text-sm font-medium text-blue-700">
30+
{Math.round((completedCount / totalCount) * 100)}% Fetching dashboard data...
31+
</span>
32+
</div>
33+
</div>
34+
);
35+
};
36+
37+
// StatisticLoader component for individual stats in cards
38+
export const StatisticLoader = ({ color = 'gray' }) => (
39+
<div className="inline-flex items-center">
40+
<div className={`w-3 h-3 animate-spin rounded-full border border-${color}-300 border-t-${color}-600`}></div>
41+
<span className="ml-1 text-gray-400">...</span>
42+
</div>
43+
);
44+
45+
// Resource Progress Badge - shows in cards when resources are still calculating
46+
export const ResourceProgressBadge = ({ isCalculating, completedCount, totalCount }) => {
47+
if (!isCalculating) return null;
48+
49+
return (
50+
<div className="absolute top-2 right-2 bg-blue-100 text-blue-700 text-xs px-2 py-1 rounded-full border border-blue-200">
51+
<div className="flex items-center space-x-1">
52+
<Loader2 className="w-3 h-3 animate-spin" />
53+
<span>
54+
{totalCount > 0 ? `${completedCount}/${totalCount}` : 'Loading...'}
55+
</span>
56+
</div>
57+
</div>
58+
);
59+
};
60+
61+
export default ResourceLoadingIndicator;

src/components/dashboard/LearningProgressSection.jsx

Lines changed: 125 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,12 @@ import WelcomeBanner from './WelcomeBanner';
55
import AssignmentManagement from './AssignmentManagement';
66
import OptimizedDashboardService from '../../utils/optimizedDashboardService';
77
import ProgressService from '../../utils/progressService';
8+
import ResourceLoadingIndicator, { StatisticLoader, ResourceProgressBadge } from '../common/ResourceLoadingIndicator';
89

910
// OPTIMIZATION 1: Memoized sub-components to prevent unnecessary re-renders
1011
const MemoizedWelcomeBanner = React.memo(WelcomeBanner);
1112
const MemoizedAssignmentManagement = React.memo(AssignmentManagement);
1213

13-
// Small loading component for statistics
14-
const StatisticLoader = ({ color = 'gray' }) => (
15-
<div className="flex items-center">
16-
<div className={`w-3 h-3 animate-spin rounded-full border border-${color}-300 border-t-${color}-600`}></div>
17-
<span className="ml-1 text-gray-400">...</span>
18-
</div>
19-
);
20-
2114
const LearningProgressSection = ({ user }) => {
2215
const navigate = useNavigate();
2316

@@ -35,10 +28,12 @@ const LearningProgressSection = ({ user }) => {
3528
const [leagueStatistics, setLeagueStatistics] = useState({});
3629
const [searchTerm, setSearchTerm] = useState('');
3730
const [isSearchActive, setIsSearchActive] = useState(false);
38-
const [statisticsLoading, setStatisticsLoading] = useState(true);
39-
const [resourcesCalculationComplete, setResourcesCalculationComplete] = useState(false);
40-
const [completedResourceCalculations, setCompletedResourceCalculations] = useState(new Set());
41-
const [totalLeaguesForCalculation, setTotalLeaguesForCalculation] = useState(0);
31+
32+
// IMPROVEMENT: Separate resource calculation state from main loading state
33+
const [resourceCalculationsInProgress, setResourceCalculationsInProgress] = useState(new Set());
34+
const [resourceCalculationsCompleted, setResourceCalculationsCompleted] = useState(new Set());
35+
const [totalResourceCalculations, setTotalResourceCalculations] = useState(0);
36+
const [showResourcesCompleteToast, setShowResourcesCompleteToast] = useState(false);
4237

4338
// OPTIMIZATION 3: Progressive data loading with immediate UI feedback
4439
const loadDashboardDataOptimized = useCallback(async () => {
@@ -56,10 +51,13 @@ const LearningProgressSection = ({ user }) => {
5651

5752
// Set basic league statistics immediately for instant display
5853
if (data.basicLeagueStats) {
54+
console.log('Setting basic league statistics:', data.basicLeagueStats);
5955
setLeagueStatistics(data.basicLeagueStats);
60-
setStatisticsLoading(false);
6156
}
6257

58+
// IMPROVEMENT: Main loading is complete - show dashboard immediately
59+
setLoading(false);
60+
6361
// Load resource progress in background for enrolled leagues
6462
if (data.dashboardData?.enrollments?.length > 0) {
6563
OptimizedDashboardService.loadResourceProgressOptimized(data.dashboardData)
@@ -72,57 +70,77 @@ const LearningProgressSection = ({ user }) => {
7270

7371
// Calculate statistics immediately for display, then update in background
7472
if (data.leagues?.length > 0) {
75-
setTotalLeaguesForCalculation(data.leagues.length);
76-
console.log(`Starting resource calculations for ${data.leagues.length} leagues:`, data.leagues.map(l => l.id));
73+
// IMPROVEMENT: Check which leagues already have complete resource calculations
74+
const leaguesNeedingCalculation = data.leagues.filter(league => {
75+
const cachedStats = data.basicLeagueStats[league.id];
76+
// Only calculate if no cached stats or resource count is missing/zero
77+
return !cachedStats || cachedStats.resourcesCount === 0;
78+
});
7779

78-
// Create callback to update resource counts when background calculation completes
79-
const handleResourceUpdate = (leagueId, resourceCount) => {
80-
console.log(`Resource calculation completed for league ${leagueId}: ${resourceCount} resources`);
81-
82-
setLeagueStatistics(prevStats => ({
83-
...prevStats,
84-
[leagueId]: {
85-
...prevStats[leagueId],
86-
resourcesCount: resourceCount
87-
}
88-
}));
80+
setTotalResourceCalculations(leaguesNeedingCalculation.length);
81+
setResourceCalculationsInProgress(new Set(leaguesNeedingCalculation.map(l => l.id)));
82+
83+
if (leaguesNeedingCalculation.length > 0) {
84+
console.log(`Starting resource calculations for ${leaguesNeedingCalculation.length} leagues:`, leaguesNeedingCalculation.map(l => l.id));
8985

90-
// Track completion of each league's resource calculation
91-
setCompletedResourceCalculations(prevCompleted => {
92-
const newCompleted = new Set([...prevCompleted, leagueId]);
93-
console.log(`Completed resource calculations: ${newCompleted.size}/${data.leagues.length}`, [...newCompleted]);
94-
95-
// Check if all resource calculations are complete
96-
if (newCompleted.size === data.leagues.length) {
97-
console.log('All resource calculations completed!');
98-
setResourcesCalculationComplete(true);
99-
setStatisticsLoading(false);
100-
}
86+
// Create callback to update resource counts when background calculation completes
87+
const handleResourceUpdate = (leagueId, resourceCount) => {
88+
console.log(`Resource calculation completed for league ${leagueId}: ${resourceCount} resources`);
10189

102-
return newCompleted;
103-
});
104-
};
105-
106-
// Calculate accurate statistics in background to update basic stats
107-
OptimizedDashboardService.calculateAllLeagueStatistics(data.leagues, handleResourceUpdate)
108-
.then(accurateStats => {
109-
console.log('Initial statistics calculated:', accurateStats);
11090
setLeagueStatistics(prevStats => ({
11191
...prevStats,
112-
...accurateStats
92+
[leagueId]: {
93+
...prevStats[leagueId],
94+
resourcesCount: resourceCount
95+
}
11396
}));
11497

115-
// Note: We don't set statisticsLoading to false here anymore
116-
// It will be set to false only when all resource calculations complete via the callback
117-
});
98+
// Track completion of each league's resource calculation
99+
setResourceCalculationsCompleted(prevCompleted => {
100+
const newCompleted = new Set([...prevCompleted, leagueId]);
101+
console.log(`Completed resource calculations: ${newCompleted.size}/${leaguesNeedingCalculation.length}`, [...newCompleted]);
102+
103+
// Check if all resource calculations are complete
104+
if (newCompleted.size === leaguesNeedingCalculation.length) {
105+
console.log('All resource calculations completed!');
106+
setShowResourcesCompleteToast(true);
107+
// Hide the toast after 4 seconds
108+
setTimeout(() => {
109+
setShowResourcesCompleteToast(false);
110+
}, 4000);
111+
}
112+
113+
return newCompleted;
114+
});
115+
116+
// Remove from in-progress set
117+
setResourceCalculationsInProgress(prevInProgress => {
118+
const newInProgress = new Set(prevInProgress);
119+
newInProgress.delete(leagueId);
120+
return newInProgress;
121+
});
122+
};
123+
124+
// Calculate accurate statistics in background to update basic stats
125+
OptimizedDashboardService.calculateAllLeagueStatistics(leaguesNeedingCalculation, handleResourceUpdate)
126+
.then(accurateStats => {
127+
console.log('Initial statistics calculated:', accurateStats);
128+
setLeagueStatistics(prevStats => ({
129+
...prevStats,
130+
...accurateStats
131+
}));
132+
});
133+
} else {
134+
console.log('All leagues already have complete resource calculations');
135+
// All leagues already have complete data, no calculations needed
136+
setTotalResourceCalculations(0);
137+
setResourceCalculationsInProgress(new Set());
138+
}
118139
} else {
119140
console.log('No leagues found, marking calculations as complete');
120-
setStatisticsLoading(false);
121-
setResourcesCalculationComplete(true);
141+
setTotalResourceCalculations(0);
122142
}
123143

124-
// Only hide initial loading, but keep showing progress for resource calculations
125-
setLoading(false);
126144
} catch (err) {
127145
console.error('Error loading dashboard data:', err);
128146
setError(`Failed to connect to the learning platform. Please try again later. (${err.message})`);
@@ -199,31 +217,14 @@ const LearningProgressSection = ({ user }) => {
199217
);
200218
}, [searchTerm, isSearchActive]);
201219

202-
const getLoadingMessage = () => {
203-
if (loading) return 'Loading dashboard...';
204-
if (!resourcesCalculationComplete && totalLeaguesForCalculation > 0) {
205-
return `Loading Dashboard... (${completedResourceCalculations.size}/${totalLeaguesForCalculation})`;
206-
}
207-
return 'Almost ready!';
208-
};
209-
210-
// Show loading until both basic loading and resource calculations are complete
211-
const isFullyLoaded = !loading && resourcesCalculationComplete;
212-
213-
if (!isFullyLoaded) {
214-
const loadingMessage = getLoadingMessage();
220+
// IMPROVEMENT: Show loading skeleton only for basic dashboard loading, not resource calculations
221+
// This allows users to see and interact with the dashboard immediately while resources load in background
222+
if (loading) {
215223

216224
return (
217225
<div className="min-h-screen bg-gray-50">
218226
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
219227
<div className="space-y-6">
220-
{/* Minimal Loading Text */}
221-
<div className="bg-white rounded-2xl border border-gray-100 p-4">
222-
<div className="text-center">
223-
<h3 className="text-lg font-medium text-gray-900">{loadingMessage}</h3>
224-
</div>
225-
</div>
226-
227228
{/* Header skeleton */}
228229
<div className="bg-white rounded-2xl border border-gray-100 p-6 space-y-4">
229230
<div className="h-8 bg-gray-200 rounded w-1/3 animate-pulse"></div>
@@ -317,6 +318,16 @@ const LearningProgressSection = ({ user }) => {
317318
</div>
318319
)}
319320
</div>
321+
322+
{/* IMPROVEMENT: Global Resource Calculation Progress */}
323+
{resourceCalculationsInProgress.size > 0 && (
324+
<ResourceLoadingIndicator
325+
isLoading={true}
326+
completedCount={resourceCalculationsCompleted.size}
327+
totalCount={totalResourceCalculations}
328+
compact={false}
329+
/>
330+
)}
320331

321332
{/* Welcome Banner for New Users */}
322333
{(!dashboardData?.enrollments || dashboardData.enrollments.length === 0) && (
@@ -666,11 +677,9 @@ const LearningProgressSection = ({ user }) => {
666677
const leagueName = league.name || 'Learning League';
667678
const leagueDescription = league.description || 'A comprehensive learning journey designed to build your skills.';
668679

669-
// Determine if we should show loading indicators
670-
// Show loading when statistics are being calculated AND we don't have dynamic stats yet
671-
const showWeeksLoading = statisticsLoading && !dynamicStats;
672-
const showSectionsLoading = statisticsLoading && !dynamicStats;
673-
const showResourcesLoading = statisticsLoading && !dynamicStats;
680+
// IMPROVEMENT: Check if this specific league's resources are still being calculated
681+
const isCalculatingResources = resourceCalculationsInProgress.has(league.id);
682+
const showResourcesLoading = isCalculatingResources;
674683

675684
return (
676685
<div
@@ -686,6 +695,15 @@ const LearningProgressSection = ({ user }) => {
686695
}
687696
}}
688697
>
698+
{/* IMPROVEMENT: Add resource calculation progress badge */}
699+
{isCalculatingResources && (
700+
<ResourceProgressBadge
701+
isCalculating={true}
702+
completedCount={resourceCalculationsCompleted.size}
703+
totalCount={totalResourceCalculations}
704+
/>
705+
)}
706+
689707
<div className="p-5">
690708
<div className="flex items-start justify-between">
691709
<div className="flex-1">
@@ -698,19 +716,11 @@ const LearningProgressSection = ({ user }) => {
698716
<div className="flex items-center space-x-2 text-xs text-gray-500">
699717
<span className="flex items-center">
700718
<div className="w-1.5 h-1.5 bg-blue-400 rounded-full mr-1"></div>
701-
{showWeeksLoading ? (
702-
<StatisticLoader color="blue" />
703-
) : (
704-
`${weeksCount} ${weeksCount === 1 ? 'week' : 'weeks'}`
705-
)}
719+
{`${weeksCount} ${weeksCount === 1 ? 'week' : 'weeks'}`}
706720
</span>
707721
<span className="flex items-center">
708722
<div className="w-1.5 h-1.5 bg-green-400 rounded-full mr-1"></div>
709-
{showSectionsLoading ? (
710-
<StatisticLoader color="green" />
711-
) : (
712-
`${sectionsCount} ${sectionsCount === 1 ? 'section' : 'sections'}`
713-
)}
723+
{`${sectionsCount} ${sectionsCount === 1 ? 'section' : 'sections'}`}
714724
</span>
715725
<span className="flex items-center">
716726
<div className="w-1.5 h-1.5 bg-purple-400 rounded-full mr-1"></div>
@@ -809,6 +819,33 @@ const LearningProgressSection = ({ user }) => {
809819
{/* Bottom spacer */}
810820
<div className="h-6"></div>
811821
</div>
822+
823+
{/* IMPROVEMENT: Resource Calculations Complete Toast */}
824+
{showResourcesCompleteToast && (
825+
<div className="fixed top-4 right-4 z-50 animate-slide-in-right">
826+
<div className="bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3 max-w-sm">
827+
<div className="flex-shrink-0">
828+
<div className="w-6 h-6 bg-white rounded-full flex items-center justify-center">
829+
<svg className="w-4 h-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
830+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
831+
</svg>
832+
</div>
833+
</div>
834+
<div className="flex-1">
835+
<p className="text-sm font-medium">All resources loaded! 🎉</p>
836+
<p className="text-xs text-green-100">Your dashboard is now fully updated</p>
837+
</div>
838+
<button
839+
onClick={() => setShowResourcesCompleteToast(false)}
840+
className="flex-shrink-0 text-white/80 hover:text-white transition-colors"
841+
>
842+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
843+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
844+
</svg>
845+
</button>
846+
</div>
847+
</div>
848+
)}
812849
</div>
813850
);
814851
};

0 commit comments

Comments
 (0)