postio / apps /frontend /src /components /platform-analytics /render.analytics.tsx
Leon4gr45's picture
Upload folder using huggingface_hub (part 7)
ec4551b verified
Raw
History Blame Contribute Delete
7.39 kB
import { FC, useCallback, useMemo, useState } from 'react';
import { Integration } from '@prisma/client';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { ChartSocial } from '@gitroom/frontend/components/analytics/chart-social';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
interface AnalyticsDataItem {
label: string;
data: Array<{ total: number; date: string }>;
average?: boolean;
percentageChange?: number;
}
const TrendIndicator: FC<{ value: number; average?: boolean }> = ({
value,
average,
}) => {
if (value === 0) return null;
const isPositive = value > 0;
const displayValue = Math.abs(value).toFixed(1);
return (
<div
className={`flex items-center gap-[4px] text-[13px] font-medium ${
isPositive ? 'text-[#32d583]' : 'text-[#f97066]'
}`}
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
className={isPositive ? '' : 'rotate-180'}
>
<path
d="M6 2.5L10 7.5H2L6 2.5Z"
fill="currentColor"
/>
</svg>
<span>
{displayValue}
{average ? 'pp' : '%'}
</span>
</div>
);
};
const AnalyticsCard: FC<{
item: AnalyticsDataItem;
total: string | number;
index: number;
}> = ({ item, total, index }) => {
const colorVariants = ['purple', 'green', 'blue'] as const;
const color = colorVariants[index % colorVariants.length];
const hasDataPoints = item.data.length >= 1;
return (
<div className="group relative">
<div
className={`
flex flex-col h-full
bg-newTableHeader
border border-newTableBorder
rounded-[12px]
overflow-hidden
transition-all duration-200
hover:border-[#612bd3]/50
`}
>
{/* Header */}
<div className="flex items-center justify-between px-[16px] pt-[14px] pb-[8px]">
<div className="flex items-center gap-[10px]">
<div
className={`
w-[8px] h-[8px] rounded-full
${color === 'purple' ? 'bg-[#612bd3]' : ''}
${color === 'green' ? 'bg-[#32d583]' : ''}
${color === 'blue' ? 'bg-[#1d9bf0]' : ''}
`}
/>
<span className="text-[15px] font-medium text-newTableText">
{item.label}
</span>
</div>
{item.percentageChange !== undefined && (
<TrendIndicator value={item.percentageChange} average={item.average} />
)}
</div>
{/* Content */}
{hasDataPoints ? (
<>
{/* Chart */}
<div className="flex-1 px-[12px] py-[8px]">
<div className="h-[120px] relative">
<ChartSocial data={item.data} color={color} key={`chart-${index}`} />
</div>
</div>
{/* Value */}
<div className="px-[16px] pb-[14px]">
<div className="text-[36px] leading-[42px] font-semibold tracking-tight">
{total}
</div>
</div>
</>
) : (
/* Single value display */
<div className="flex-1 flex flex-col items-center justify-center py-[32px] px-[16px]">
<div className="text-[48px] leading-[56px] font-semibold tracking-tight">
{total}
</div>
</div>
)}
</div>
</div>
);
};
const EmptyState: FC<{ onRefresh: () => void }> = ({ onRefresh }) => {
const t = useT();
return (
<div className="col-span-full flex flex-col items-center justify-center py-[48px] px-[24px] bg-newTableHeader border border-newTableBorder rounded-[12px]">
<div className="w-[48px] h-[48px] mb-[16px] rounded-full bg-[#612bd3]/10 flex items-center justify-center">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="text-[#612bd3]"
>
<path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path d="M12 8v4l2 2" />
</svg>
</div>
<p className="text-[15px] text-newTableText text-center mb-[12px]">
{t(
'this_channel_needs_to_be_refreshed',
'This channel needs to be refreshed to display analytics'
)}
</p>
<button
onClick={onRefresh}
className="inline-flex items-center gap-[6px] px-[16px] py-[8px] text-[14px] font-medium text-white bg-[#612bd3] hover:bg-[#5023b8] rounded-[8px] transition-colors"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M23 4v6h-6M1 20v-6h6" />
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" />
</svg>
{t('refresh_channel', 'Refresh Channel')}
</button>
</div>
);
};
export const RenderAnalytics: FC<{
integration: Integration;
date: number;
}> = (props) => {
const { integration, date } = props;
const [loading, setLoading] = useState(true);
const fetch = useFetch();
const load = useCallback(async () => {
setLoading(true);
const load = (
await fetch(`/analytics/${integration.id}?date=${date}`)
).json();
setLoading(false);
return load;
}, [integration, date]);
const { data } = useSWR(`/analytics-${integration?.id}-${date}`, load, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
refreshWhenOffline: false,
revalidateOnMount: true,
});
const refreshChannel = useCallback(
(
integrationData: Integration & {
identifier: string;
}
) =>
async () => {
const { url } = await (
await fetch(
`/integrations/social/${integrationData.identifier}?refresh=${integrationData.internalId}`,
{
method: 'GET',
}
)
).json();
window.location.href = url;
},
[]
);
const t = useT();
const totals = useMemo(() => {
return data?.map((p: AnalyticsDataItem) => {
const value =
(p?.data.reduce((acc: number, curr: { total: number }) => acc + curr.total, 0) || 0) /
(p.average ? p.data.length : 1);
if (p.average) {
return value.toFixed(2) + '%';
}
return new Intl.NumberFormat().format(Math.round(value));
});
}, [data]);
if (loading) {
return (
<div className="flex items-center justify-center py-[48px]">
<LoadingComponent />
</div>
);
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-[16px]">
{data?.length === 0 && (
<EmptyState onRefresh={refreshChannel(integration as any)} />
)}
{data?.map((item: AnalyticsDataItem, index: number) => (
<AnalyticsCard
key={`analytics-${index}`}
item={item}
total={totals[index]}
index={index}
/>
))}
</div>
);
};