fix(onboarding): Improve loading indicator with elapsed time and better pacing
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<NodeJS.Timeout | null>(null);
|
||||
const elapsedTimerRef = useRef<NodeJS.Timeout | null>(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
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Please wait while we set everything up for you...
|
||||
This typically takes 45-90 seconds. Please don't close this page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{LOADING_STEPS.map((step, index) => {
|
||||
const StepIcon = step.icon;
|
||||
const isComplete = index < loadingStepIndex;
|
||||
@@ -526,7 +549,7 @@ const TenantOnboardPage: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`flex items-center gap-4 p-4 rounded-lg transition-all duration-300 ${
|
||||
className={`flex items-center gap-4 p-3 rounded-lg transition-all duration-300 ${
|
||||
isComplete
|
||||
? 'bg-green-50 dark:bg-green-900/20'
|
||||
: isCurrent
|
||||
@@ -535,25 +558,25 @@ const TenantOnboardPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${
|
||||
className={`w-9 h-9 rounded-full flex items-center justify-center transition-all duration-300 ${
|
||||
isComplete
|
||||
? 'bg-green-500 text-white'
|
||||
: isCurrent
|
||||
? 'bg-indigo-500 text-white'
|
||||
? 'bg-indigo-500 text-white animate-pulse'
|
||||
: 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{isComplete ? (
|
||||
<CheckCircle size={20} />
|
||||
<CheckCircle size={18} />
|
||||
) : isCurrent ? (
|
||||
<Loader size={20} className="animate-spin" />
|
||||
<Loader size={18} className="animate-spin" />
|
||||
) : (
|
||||
<StepIcon size={20} />
|
||||
<StepIcon size={18} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p
|
||||
className={`font-medium transition-colors duration-300 ${
|
||||
className={`font-medium text-sm transition-colors duration-300 ${
|
||||
isComplete
|
||||
? 'text-green-700 dark:text-green-400'
|
||||
: isCurrent
|
||||
@@ -570,14 +593,42 @@ const TenantOnboardPage: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-indigo-600 h-2 rounded-full transition-all duration-500 ease-out"
|
||||
style={{
|
||||
width: `${((loadingStepIndex + 1) / LOADING_STEPS.length) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
{/* Progress section with time */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>
|
||||
{loadingStepIndex < LOADING_STEPS.length - 1
|
||||
? `Step ${loadingStepIndex + 1} of ${LOADING_STEPS.length}`
|
||||
: 'Finalizing...'}
|
||||
</span>
|
||||
<span className="tabular-nums">
|
||||
{Math.floor(elapsedSeconds / 60)}:{(elapsedSeconds % 60).toString().padStart(2, '0')} elapsed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress bar - doesn't go to 100% until actually done */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-500 ease-out ${
|
||||
loadingStepIndex >= 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)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Show extra message when on final step */}
|
||||
{loadingStepIndex >= LOADING_STEPS.length - 1 && (
|
||||
<p className="text-xs text-center text-gray-500 dark:text-gray-400 animate-pulse">
|
||||
Almost there! Setting up your database tables...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user