glam/frontend/src/components/LoadingScreen.tsx
2025-12-07 23:08:02 +01:00

126 lines
4.2 KiB
TypeScript

/**
* LoadingScreen Component
*
* A beautiful loading screen with a circular progress meter that stays still
* while the progress arc fills. Replaces the spinning loader animation.
*
* © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import './LoadingScreen.css';
interface LoadingScreenProps {
/** Loading message to display */
message?: string;
/** Optional: show determinate progress (0-100). If not provided, shows indeterminate animation */
progress?: number;
/** Size of the circular meter: 'small' (40px), 'medium' (80px), 'large' (120px) */
size?: 'small' | 'medium' | 'large';
/** Whether to show fullscreen overlay */
fullscreen?: boolean;
}
export const LoadingScreen: React.FC<LoadingScreenProps> = ({
message = 'Loading...',
progress,
size = 'medium',
fullscreen = true,
}) => {
// For indeterminate mode, animate progress from 0 to 100 smoothly
const [animatedProgress, setAnimatedProgress] = useState(0);
useEffect(() => {
if (progress !== undefined) {
// Use the provided progress value
setAnimatedProgress(progress);
} else {
// Indeterminate mode: smoothly animate progress
const interval = setInterval(() => {
setAnimatedProgress((prev) => {
// Slow start, speed up in middle, slow at end (ease-in-out feel)
const increment = prev < 30 ? 0.8 : prev < 70 ? 1.2 : prev < 90 ? 0.6 : 0.2;
const next = prev + increment;
return next >= 95 ? 0 : next; // Reset at 95% for smooth loop
});
}, 50);
return () => clearInterval(interval);
}
}, [progress]);
// SVG circle properties
const sizeMap = { small: 40, medium: 80, large: 120 };
const strokeWidthMap = { small: 3, medium: 5, large: 7 };
const diameter = sizeMap[size];
const strokeWidth = strokeWidthMap[size];
const radius = (diameter - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (animatedProgress / 100) * circumference;
const containerClass = fullscreen
? 'loading-screen loading-screen--fullscreen'
: 'loading-screen loading-screen--inline';
return (
<div className={containerClass}>
<div className="loading-screen__content">
{/* Circular progress meter - stationary circle with filling arc */}
<div className={`loading-screen__meter loading-screen__meter--${size}`}>
<svg
width={diameter}
height={diameter}
viewBox={`0 0 ${diameter} ${diameter}`}
className="loading-screen__svg"
>
{/* Background circle (track) */}
<circle
className="loading-screen__track"
cx={diameter / 2}
cy={diameter / 2}
r={radius}
strokeWidth={strokeWidth}
fill="none"
/>
{/* Progress arc - fills up clockwise from top */}
<circle
className="loading-screen__progress"
cx={diameter / 2}
cy={diameter / 2}
r={radius}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${diameter / 2} ${diameter / 2})`}
/>
</svg>
{/* Center icon or percentage */}
<div className="loading-screen__center">
{progress !== undefined ? (
<span className="loading-screen__percentage">{Math.round(animatedProgress)}%</span>
) : (
<span className="loading-screen__icon"></span>
)}
</div>
</div>
{/* Message text */}
<p className="loading-screen__message">{message}</p>
{/* Optional linear progress bar below */}
{progress !== undefined && (
<div className="loading-screen__bar">
<div
className="loading-screen__bar-fill"
style={{ width: `${animatedProgress}%` }}
/>
</div>
)}
</div>
</div>
);
};
export default LoadingScreen;