From 0d3c97ea5f778aea74feabd1302f6e1ec259562e Mon Sep 17 00:00:00 2001 From: poduck Date: Wed, 3 Dec 2025 16:37:34 -0500 Subject: [PATCH] fix(onboarding): Improve loading indicator with elapsed time and better pacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add elapsed time counter (MM:SS) - Spread animation steps over ~30 seconds before final step - Final step stays spinning (doesn't complete early) - Progress bar caps at 90% until actually done, pulses on final step - Show "Finalizing..." and helpful message during long final step - Clear "45-90 seconds" time estimate upfront 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/src/pages/TenantOnboardPage.tsx | 115 ++++++++++++++++------- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/frontend/src/pages/TenantOnboardPage.tsx b/frontend/src/pages/TenantOnboardPage.tsx index bd42399..c864dde 100644 --- a/frontend/src/pages/TenantOnboardPage.tsx +++ b/frontend/src/pages/TenantOnboardPage.tsx @@ -5,11 +5,13 @@ import { useInvitationByToken, useAcceptInvitation } from '../hooks/usePlatform' import { getBaseDomain, buildSubdomainUrl } from '../utils/domain'; // Loading step configuration for the animated progress +// The last step stays active (spinning) until the API returns const LOADING_STEPS = [ - { id: 'account', label: 'Creating your account', icon: User, duration: 800 }, - { id: 'database', label: 'Setting up your database', icon: Database, duration: 3000 }, - { id: 'config', label: 'Configuring your workspace', icon: Settings, duration: 1500 }, - { id: 'features', label: 'Activating features', icon: Sparkles, duration: 1000 }, + { id: 'account', label: 'Creating your account', icon: User, duration: 2000 }, + { id: 'database', label: 'Setting up your database', icon: Database, duration: 8000 }, + { id: 'config', label: 'Configuring your workspace', icon: Settings, duration: 6000 }, + { id: 'migrations', label: 'Running database migrations', icon: Database, duration: 15000 }, + { id: 'features', label: 'Activating features', icon: Sparkles, duration: null }, // null = stays spinning until done ]; const TenantOnboardPage: React.FC = () => { @@ -24,7 +26,9 @@ const TenantOnboardPage: React.FC = () => { const [isCreating, setIsCreating] = useState(false); const [creationComplete, setCreationComplete] = useState(false); const [loadingStepIndex, setLoadingStepIndex] = useState(0); + const [elapsedSeconds, setElapsedSeconds] = useState(0); const loadingIntervalRef = useRef(null); + const elapsedTimerRef = useRef(null); const [formData, setFormData] = useState({ // Step 1: Account email: '', @@ -125,29 +129,54 @@ const TenantOnboardPage: React.FC = () => { setCurrentStep(prev => prev - 1); }; - // Cleanup loading interval on unmount + // Cleanup timers on unmount useEffect(() => { return () => { if (loadingIntervalRef.current) { clearTimeout(loadingIntervalRef.current); } + if (elapsedTimerRef.current) { + clearInterval(elapsedTimerRef.current); + } }; }, []); // Animate through loading steps const startLoadingAnimation = () => { setLoadingStepIndex(0); + setElapsedSeconds(0); let stepIndex = 0; + // Start elapsed time counter + elapsedTimerRef.current = setInterval(() => { + setElapsedSeconds(prev => prev + 1); + }, 1000); + const advanceStep = () => { stepIndex++; if (stepIndex < LOADING_STEPS.length) { setLoadingStepIndex(stepIndex); - loadingIntervalRef.current = setTimeout(advanceStep, LOADING_STEPS[stepIndex].duration); + const nextDuration = LOADING_STEPS[stepIndex].duration; + // If duration is null, this step stays active until API returns + if (nextDuration !== null) { + loadingIntervalRef.current = setTimeout(advanceStep, nextDuration); + } } }; - loadingIntervalRef.current = setTimeout(advanceStep, LOADING_STEPS[0].duration); + const firstDuration = LOADING_STEPS[0].duration; + if (firstDuration !== null) { + loadingIntervalRef.current = setTimeout(advanceStep, firstDuration); + } + }; + + const stopLoadingAnimation = () => { + if (loadingIntervalRef.current) { + clearTimeout(loadingIntervalRef.current); + } + if (elapsedTimerRef.current) { + clearInterval(elapsedTimerRef.current); + } }; const handleSubmit = () => { @@ -172,10 +201,7 @@ const TenantOnboardPage: React.FC = () => { { token, data }, { onSuccess: () => { - // Clear the loading animation - if (loadingIntervalRef.current) { - clearTimeout(loadingIntervalRef.current); - } + stopLoadingAnimation(); // Show all steps as complete, then show success setLoadingStepIndex(LOADING_STEPS.length); setTimeout(() => { @@ -184,10 +210,7 @@ const TenantOnboardPage: React.FC = () => { }, 500); }, onError: (error: any) => { - // Clear the loading animation - if (loadingIntervalRef.current) { - clearTimeout(loadingIntervalRef.current); - } + stopLoadingAnimation(); setIsCreating(false); setAcceptError( error.response?.data?.detail || @@ -513,11 +536,11 @@ const TenantOnboardPage: React.FC = () => { Creating Your Business

- Please wait while we set everything up for you... + This typically takes 45-90 seconds. Please don't close this page.

-
+
{LOADING_STEPS.map((step, index) => { const StepIcon = step.icon; const isComplete = index < loadingStepIndex; @@ -526,7 +549,7 @@ const TenantOnboardPage: React.FC = () => { return (
{ }`} >
{isComplete ? ( - + ) : isCurrent ? ( - + ) : ( - + )}

{ })}

- {/* Progress bar */} -
-
+ {/* Progress section with time */} +
+
+ + {loadingStepIndex < LOADING_STEPS.length - 1 + ? `Step ${loadingStepIndex + 1} of ${LOADING_STEPS.length}` + : 'Finalizing...'} + + + {Math.floor(elapsedSeconds / 60)}:{(elapsedSeconds % 60).toString().padStart(2, '0')} elapsed + +
+ + {/* Progress bar - doesn't go to 100% until actually done */} +
+
= LOADING_STEPS.length - 1 + ? 'bg-indigo-400 animate-pulse' + : 'bg-indigo-600' + }`} + style={{ + // Cap at 90% until we're actually complete + width: loadingStepIndex >= LOADING_STEPS.length + ? '100%' + : `${Math.min(90, ((loadingStepIndex + 1) / LOADING_STEPS.length) * 100)}%`, + }} + /> +
+ + {/* Show extra message when on final step */} + {loadingStepIndex >= LOADING_STEPS.length - 1 && ( +

+ Almost there! Setting up your database tables... +

+ )}
)}