mirror of
https://github.com/lux-tel/websites.git
synced 2026-10-30 20:31:36 +00:00
163 lines
4.2 KiB
TypeScript
163 lines
4.2 KiB
TypeScript
import { motion } from 'framer-motion';
|
|
import { type HTMLAttributes, type ReactNode, forwardRef, useMemo } from 'react';
|
|
import { cn } from '../../lib/utils';
|
|
import { Circle } from './shapes/circle';
|
|
|
|
export enum PROGRESS_STATUS {
|
|
ERROR = 0,
|
|
IDLE = 1,
|
|
PENDING = 2,
|
|
SUCCESS = 3,
|
|
}
|
|
|
|
type Step = {
|
|
text: Partial<Record<PROGRESS_STATUS, ReactNode>> & { [PROGRESS_STATUS.IDLE]: ReactNode };
|
|
status: PROGRESS_STATUS;
|
|
};
|
|
|
|
interface ProgressProps extends HTMLAttributes<HTMLDivElement> {
|
|
steps: Array<Step>;
|
|
}
|
|
|
|
const variants = {
|
|
idle: { opacity: 0, pathLength: 0 },
|
|
pending: { opacity: 1, pathLength: 0.5 },
|
|
error: { opacity: 1, pathLength: 0.5 },
|
|
success: { opacity: 1, pathLength: 1 },
|
|
};
|
|
|
|
const circleVariants = {
|
|
idle: { scale: 0.75 },
|
|
pending: { scale: 1 },
|
|
error: { scale: 1 },
|
|
success: { scale: 1 },
|
|
};
|
|
|
|
function ProgressStep({
|
|
isFirst,
|
|
isLast,
|
|
circleRadius,
|
|
text,
|
|
status,
|
|
}: {
|
|
isFirst: boolean;
|
|
isLast: boolean;
|
|
status: PROGRESS_STATUS;
|
|
circleRadius: number;
|
|
text: Partial<Record<PROGRESS_STATUS, ReactNode>> & { [PROGRESS_STATUS.IDLE]: ReactNode };
|
|
}) {
|
|
const circleVariant = useMemo(() => {
|
|
switch (status) {
|
|
case PROGRESS_STATUS.IDLE:
|
|
return 'grey' as const;
|
|
case PROGRESS_STATUS.PENDING:
|
|
return 'blue' as const;
|
|
case PROGRESS_STATUS.SUCCESS:
|
|
return 'green' as const;
|
|
case PROGRESS_STATUS.ERROR:
|
|
return 'red' as const;
|
|
}
|
|
}, [status]);
|
|
|
|
const statusText = useMemo(() => {
|
|
switch (status) {
|
|
case PROGRESS_STATUS.IDLE:
|
|
return 'idle';
|
|
case PROGRESS_STATUS.PENDING:
|
|
return 'pending';
|
|
case PROGRESS_STATUS.SUCCESS:
|
|
return 'success';
|
|
case PROGRESS_STATUS.ERROR:
|
|
return 'error';
|
|
}
|
|
}, [status]);
|
|
|
|
const height = circleRadius * 7;
|
|
const width = circleRadius * 7;
|
|
const x1 = width / 2;
|
|
|
|
return (
|
|
<div className={cn('-ml-4 relative w-full')} style={{ height }}>
|
|
<svg
|
|
/** The +1 ensures the lines overlap and prevents rendering gaps */
|
|
height={height + 1}
|
|
width={height}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="absolute"
|
|
>
|
|
<line
|
|
x1={x1}
|
|
x2={x1}
|
|
y1={isFirst ? '50%' : '0%'}
|
|
y2={isLast ? '50%' : '100%'}
|
|
strokeWidth={2}
|
|
strokeDasharray={height / 20}
|
|
className="stroke-session-text"
|
|
/>
|
|
<motion.line
|
|
x1={x1}
|
|
x2={x1}
|
|
y1={isFirst ? '50%' : '0'}
|
|
y2={isLast ? '50%' : '100%'}
|
|
strokeWidth={2.5}
|
|
className="stroke-session-white"
|
|
variants={variants}
|
|
animate={statusText}
|
|
/>
|
|
</svg>
|
|
<svg height={height} width={width} xmlns="http://www.w3.org/2000/svg" className="absolute">
|
|
<Circle
|
|
cx="50%"
|
|
cy="50%"
|
|
r={circleRadius + 2.5}
|
|
variant={circleVariant}
|
|
variants={circleVariants}
|
|
glow={circleVariant}
|
|
animate={statusText}
|
|
/>
|
|
</svg>
|
|
<svg
|
|
height={height}
|
|
width={width}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={cn('absolute', status === PROGRESS_STATUS.PENDING && 'animate-spin')}
|
|
>
|
|
<Circle
|
|
cx="50%"
|
|
cy="50%"
|
|
r={circleRadius}
|
|
strokeWidth={1.5}
|
|
variant={circleVariant}
|
|
partial={status === PROGRESS_STATUS.PENDING ? '25' : '100'}
|
|
/>
|
|
</svg>
|
|
<span
|
|
style={{ marginLeft: width }}
|
|
className="mt-0.5 flex h-full items-center text-start align-middle leading-tight"
|
|
>
|
|
{text[status] ?? text[PROGRESS_STATUS.IDLE]}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const Progress = forwardRef<HTMLDivElement, ProgressProps>(
|
|
({ steps, className, ...props }, ref) => {
|
|
return (
|
|
<div ref={ref} className={cn('flex w-full flex-col items-start', className)} {...props}>
|
|
{steps.map(({ text, status }, i) => (
|
|
<ProgressStep
|
|
key={i}
|
|
circleRadius={8}
|
|
isFirst={i === 0}
|
|
isLast={i === steps.length - 1}
|
|
status={status}
|
|
text={text}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
Progress.displayName = 'Progress';
|