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

146 lines
4.2 KiB
TypeScript
Executable File

import {HeaderDatum} from '@common/admin/analytics/use-admin-report';
import React, {
cloneElement,
Fragment,
isValidElement,
ReactElement,
} from 'react';
import {TrendingUpIcon} from '@common/icons/material/TrendingUp';
import {TrendingDownIcon} from '@common/icons/material/TrendingDown';
import {createSvgIconFromTree} from '@common/icons/create-svg-icon';
import {AdminReportPageColGap} from '@common/admin/analytics/visitors-report-charts';
import {FormattedNumber} from '@common/i18n/formatted-number';
import {FormattedBytes} from '@common/uploads/formatted-bytes';
import {TrendingFlatIcon} from '@common/icons/material/TrendingFlat';
import {AnimatePresence, m} from 'framer-motion';
import {opacityAnimation} from '@common/ui/animation/opacity-animation';
import {Skeleton} from '@common/ui/skeleton/skeleton';
interface AdminHeaderReportProps {
report?: HeaderDatum[];
isLoading?: boolean;
}
export function AdminHeaderReport({report, isLoading}: AdminHeaderReportProps) {
return (
<div
className={`flex h-[97px] flex-shrink-0 items-center overflow-x-auto ${AdminReportPageColGap}`}
>
{report?.map(datum => (
<ReportItem key={datum.name} datum={datum} isLoading={isLoading} />
))}
</div>
);
}
interface ValueMetricItemProps {
datum: HeaderDatum;
isLoading?: boolean;
}
function ReportItem({datum, isLoading = false}: ValueMetricItemProps) {
let icon;
if (isValidElement(datum.icon)) {
icon = cloneElement(datum.icon, {size: 'lg'});
} else {
const IconEl = createSvgIconFromTree(datum.icon);
icon = <IconEl size="lg" />;
}
return (
<div
key={datum.name}
className="rounded-panel flex h-full flex-auto items-center gap-18 whitespace-nowrap border p-20"
>
<div className="flex-shrink-0 rounded-lg bg-primary-light/20 p-10 text-primary">
{icon}
</div>
<div className="flex-auto">
<div className="flex items-center justify-between gap-20">
<div className="text-lg font-bold text-main">
<AnimatePresence initial={false} mode="wait">
{isLoading ? (
<m.div key="skeleton" {...opacityAnimation}>
<Skeleton className="min-w-24" />
</m.div>
) : (
<m.div key="value" {...opacityAnimation}>
<FormattedValue datum={datum} />
</m.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex items-center justify-between gap-20">
<h2 className="text-sm text-muted">{datum.name}</h2>
{(datum.percentageChange != null || datum.previousValue != null) && (
<div className="flex items-center gap-10">
<TrendingIndicator datum={datum} />
</div>
)}
</div>
</div>
</div>
);
}
interface FormattedValueProps {
datum: HeaderDatum;
}
function FormattedValue({datum}: FormattedValueProps) {
switch (datum.type) {
case 'fileSize':
return <FormattedBytes bytes={datum.currentValue} />;
case 'percentage':
return (
<FormattedNumber
value={datum.currentValue}
style="percent"
maximumFractionDigits={1}
/>
);
default:
return <FormattedNumber value={datum.currentValue} />;
}
}
interface TrendingIndicatorProps {
datum: HeaderDatum;
}
function TrendingIndicator({datum}: TrendingIndicatorProps) {
const percentage = calculatePercentage(datum);
let icon: ReactElement;
if (percentage > 0) {
icon = <TrendingUpIcon size="md" className="text-positive" />;
} else if (percentage === 0) {
icon = <TrendingFlatIcon className="text-muted" />;
} else {
icon = <TrendingDownIcon className="text-danger" />;
}
return (
<Fragment>
{icon}
<div className="text-sm font-semibold text-muted">{percentage}%</div>
</Fragment>
);
}
function calculatePercentage({
percentageChange,
previousValue,
currentValue,
}: HeaderDatum) {
if (
percentageChange != null ||
previousValue == null ||
currentValue == null
) {
return percentageChange ?? 0;
}
if (previousValue === 0) {
return 100;
}
return Math.round(((currentValue - previousValue) / previousValue) * 100);
}