postio / apps /frontend /src /components /admin /admin-stats.component.tsx
Leon4gr45's picture
Upload folder using huggingface_hub (part 7)
ec4551b verified
Raw
History Blame Contribute Delete
8.28 kB
'use client';
import React, { FC, useCallback, useState } from 'react';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { Button } from '@gitroom/react/form/button';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
interface PerSocial {
provider: string;
count: number;
}
interface StatsBlock {
total: number;
perSocial: PerSocial[];
}
interface StatsResponse {
from: string;
to: string;
errors: StatsBlock;
posts: StatsBlock;
connected: StatsBlock;
}
const isoDaysAgo = (days: number) => {
const d = new Date();
d.setDate(d.getDate() - days);
return d.toISOString().slice(0, 10);
};
const today = () => new Date().toISOString().slice(0, 10);
const startOfWeek = () => {
const d = new Date();
// ISO week: Monday = 0
const day = (d.getDay() + 6) % 7;
d.setDate(d.getDate() - day);
return d.toISOString().slice(0, 10);
};
const startOfMonth = () => {
const d = new Date();
d.setDate(1);
return d.toISOString().slice(0, 10);
};
const PRESETS: { label: string; range: () => { from: string; to: string } }[] = [
{ label: 'Today', range: () => ({ from: today(), to: today() }) },
{ label: 'This week', range: () => ({ from: startOfWeek(), to: today() }) },
{ label: 'This month', range: () => ({ from: startOfMonth(), to: today() }) },
{ label: 'Last 7 days', range: () => ({ from: isoDaysAgo(7), to: today() }) },
{ label: 'Last 30 days', range: () => ({ from: isoDaysAgo(30), to: today() }) },
];
const useStats = (params: {
from: string;
to: string;
unknownOnly: boolean;
}) => {
const fetch = useFetch();
const query = new URLSearchParams({
from: params.from,
to: params.to,
...(params.unknownOnly ? { unknownOnly: 'true' } : {}),
});
const key = `/admin/stats?${query.toString()}`;
return useSWR<StatsResponse>(
key,
async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
throw new Error('Failed to load stats');
}
return res.json();
},
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
};
const SummaryCard: FC<{ label: string; value: number }> = ({
label,
value,
}) => (
<div className="border border-newTableBorder rounded-[8px] p-[16px] bg-newBgColorInner">
<div className="text-[12px] opacity-70">{label}</div>
<div className="text-[28px] font-[600]">{value.toLocaleString()}</div>
</div>
);
const PerSocialTable: FC<{ title: string; block: StatsBlock }> = ({
title,
block,
}) => (
<div className="border border-newTableBorder rounded-[8px] overflow-hidden">
<div className="grid grid-cols-[1fr_120px] gap-[12px] px-[12px] py-[10px] bg-newBgColorInner text-[12px] uppercase opacity-70 border-b border-newTableBorder">
<div>{title}</div>
<div className="text-right">Count</div>
</div>
{block.perSocial.length === 0 ? (
<div className="px-[12px] py-[10px] text-[13px] opacity-70">
No data for this timeframe.
</div>
) : (
block.perSocial.map((row) => (
<div
key={row.provider}
className="grid grid-cols-[1fr_120px] gap-[12px] px-[12px] py-[10px] text-[13px] border-b border-newTableBorder last:border-b-0"
>
<div className="capitalize">{row.provider}</div>
<div className="text-right">{row.count.toLocaleString()}</div>
</div>
))
)}
</div>
);
export const AdminStatsComponent: FC = () => {
const user = useUser();
const [fromInput, setFromInput] = useState(today());
const [toInput, setToInput] = useState(today());
const [range, setRange] = useState({ from: today(), to: today() });
const [unknownOnly, setUnknownOnly] = useState(false);
const { data, isLoading, error } = useStats({ ...range, unknownOnly });
const applyRange = useCallback((next: { from: string; to: string }) => {
setFromInput(next.from);
setToInput(next.to);
setRange(next);
}, []);
if (!user?.isSuperAdmin) {
return (
<div className="text-textColor p-[20px]">
You do not have access to this page.
</div>
);
}
return (
<div className="flex flex-col gap-[16px] text-textColor">
<div className="flex items-center justify-between">
<div className="text-[20px] font-[600]">Admin Stats</div>
{data && (
<div className="text-[13px] opacity-70">
{new Date(data.from).toLocaleDateString()} —{' '}
{new Date(data.to).toLocaleDateString()}
</div>
)}
</div>
<div className="flex flex-wrap gap-[8px]">
{PRESETS.map((preset) => {
const next = preset.range();
const active = range.from === next.from && range.to === next.to;
return (
<button
key={preset.label}
type="button"
onClick={() => applyRange(next)}
className={`h-[32px] px-[12px] rounded-[8px] text-[13px] border cursor-pointer whitespace-nowrap ${
active
? 'bg-forth text-white border-forth'
: 'bg-newBgColorInner text-textColor border-newTableBorder hover:bg-tableBorder'
}`}
>
{preset.label}
</button>
);
})}
</div>
<div className="flex flex-wrap gap-[12px] items-end bg-newBgColorInner border border-newTableBorder rounded-[8px] p-[12px]">
<div className="flex flex-col gap-[6px]">
<div className="text-[12px] opacity-70">From</div>
<input
type="date"
value={fromInput}
max={toInput}
onChange={(e) => setFromInput(e.target.value)}
className="bg-newBgColorInner h-[38px] border border-newTableBorder rounded-[8px] px-[10px] text-[14px] text-textColor"
/>
</div>
<div className="flex flex-col gap-[6px]">
<div className="text-[12px] opacity-70">To</div>
<input
type="date"
value={toInput}
min={fromInput}
max={today()}
onChange={(e) => setToInput(e.target.value)}
className="bg-newBgColorInner h-[38px] border border-newTableBorder rounded-[8px] px-[10px] text-[14px] text-textColor"
/>
</div>
<Button
onClick={() => setRange({ from: fromInput, to: toInput })}
disabled={!fromInput || !toInput || fromInput > toInput}
>
Apply
</Button>
<label
className="flex items-center gap-[6px] text-[13px] cursor-pointer h-[38px]"
title='Only count errors whose message matches "message":"Unknown Error" (affects the error stats only)'
>
<input
type="checkbox"
checked={unknownOnly}
onChange={(e) => setUnknownOnly(e.target.checked)}
/>
Unknown errors only
</label>
</div>
{isLoading ? (
<LoadingComponent />
) : error || !data ? (
<div className="text-red-400">Failed to load stats.</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-[12px]">
<SummaryCard label="Total posts published" value={data.posts.total} />
<SummaryCard
label="Total connected accounts"
value={data.connected.total}
/>
<SummaryCard
label={unknownOnly ? 'Total unknown errors' : 'Total errors'}
value={data.errors.total}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-[12px]">
<PerSocialTable
title="Posts published per social"
block={data.posts}
/>
<PerSocialTable
title="Connected accounts per social"
block={data.connected}
/>
<PerSocialTable
title={
unknownOnly ? 'Unknown errors per social' : 'Errors per social'
}
block={data.errors}
/>
</div>
</>
)}
</div>
);
};