Download apps/frontend/src/components/admin/admin-stats.component.tsx from Leon4gr45/postio: direct link, hf CLI and curl.
- Browser
- Download file 8.28 kB
-
https://huggingface.co/spaces/Leon4gr45/postio/resolve/main/apps/frontend/src/components/admin/admin-stats.component.tsx
- Command line
-
hf download hf://spaces/Leon4gr45/postio/apps/frontend/src/components/admin/admin-stats.component.tsx
-
curl -L -o admin-stats.component.tsx https://huggingface.co/spaces/Leon4gr45/postio/resolve/main/apps/frontend/src/components/admin/admin-stats.component.tsx
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> | |
| ); | |
| }; | |