Files
mtdb_movie/common/resources/client/ui/progress/progress-circle.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

160 lines
4.1 KiB
TypeScript
Executable File

import React, {ComponentPropsWithoutRef, CSSProperties} from 'react';
import clsx from 'clsx';
import {clamp} from '../../utils/number/clamp';
import {useNumberFormatter} from '../../i18n/use-number-formatter';
export interface ProgressCircleProps extends ComponentPropsWithoutRef<'div'> {
value?: number;
minValue?: number;
maxValue?: number;
size?: 'xs' | 'sm' | 'md' | 'lg' | string;
isIndeterminate?: boolean;
className?: string;
position?: string;
trackColor?: string;
fillColor?: string;
}
export const ProgressCircle = React.forwardRef<
HTMLDivElement,
ProgressCircleProps
>((props, ref) => {
let {
value = 0,
minValue = 0,
maxValue = 100,
size = 'md',
isIndeterminate = false,
className,
position = 'relative',
trackColor,
fillColor = 'border-primary',
...domProps
} = props;
value = clamp(value, minValue, maxValue);
const circleSize = getCircleStyle(size);
const percentage = (value - minValue) / (maxValue - minValue);
const formatter = useNumberFormatter({style: 'percent'});
let valueLabel = '';
if (!isIndeterminate && !valueLabel) {
valueLabel = formatter.format(percentage);
}
const subMask1Style: CSSProperties = {};
const subMask2Style: CSSProperties = {};
if (!isIndeterminate) {
const percentage = ((value - minValue) / (maxValue - minValue)) * 100;
let angle;
if (percentage > 0 && percentage <= 50) {
angle = -180 + (percentage / 50) * 180;
subMask1Style.transform = `rotate(${angle}deg)`;
subMask2Style.transform = 'rotate(-180deg)';
} else if (percentage > 50) {
angle = -180 + ((percentage - 50) / 50) * 180;
subMask1Style.transform = 'rotate(0deg)';
subMask2Style.transform = `rotate(${angle}deg)`;
}
}
return (
<div
{...domProps}
aria-valuenow={isIndeterminate ? undefined : value}
aria-valuemin={minValue}
aria-valuemax={maxValue}
aria-valuetext={isIndeterminate ? undefined : valueLabel}
role="progressbar"
ref={ref}
className={clsx(
'progress-circle',
position,
circleSize,
isIndeterminate && 'indeterminate',
className
)}
>
<div className={clsx(circleSize, trackColor, 'rounded-full border-4')} />
<div
className={clsx(
'fills absolute left-0 top-0 h-full w-full',
isIndeterminate && 'progress-circle-fills-animate'
)}
>
<FillMask
circleSize={circleSize}
subMaskStyle={subMask1Style}
isIndeterminate={isIndeterminate}
className="rotate-180"
fillColor={fillColor}
subMaskClassName={clsx(
isIndeterminate && 'progress-circle-fill-submask-1-animate'
)}
/>
<FillMask
circleSize={circleSize}
subMaskStyle={subMask2Style}
isIndeterminate={isIndeterminate}
fillColor={fillColor}
subMaskClassName={clsx(
isIndeterminate && 'progress-circle-fill-submask-2-animate'
)}
/>
</div>
</div>
);
});
interface FillMaskProps {
className?: string;
circleSize?: string;
subMaskStyle: CSSProperties;
subMaskClassName: string;
isIndeterminate?: boolean;
fillColor?: string;
}
function FillMask({
subMaskStyle,
subMaskClassName,
className,
circleSize,
isIndeterminate,
fillColor,
}: FillMaskProps) {
return (
<div
className={clsx(
'absolute h-full w-1/2 origin-[100%] overflow-hidden',
className
)}
>
<div
className={clsx(
'h-full w-full origin-[100%] rotate-180 overflow-hidden',
!isIndeterminate && 'transition-transform duration-100',
subMaskClassName
)}
style={subMaskStyle}
>
<div className={clsx(circleSize, fillColor, 'rounded-full border-4')} />
</div>
</div>
);
}
function getCircleStyle(size: ProgressCircleProps['size']) {
switch (size) {
case 'xs':
return 'w-20 h-20';
case 'sm':
return 'w-24 h-24';
case 'md':
return 'w-32 h-32';
case 'lg':
return 'w-42 h-42';
default:
return size;
}
}