126 lines
4.2 KiB
TypeScript
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;
|