Remove unnecessary rank tracking details cards
This commit is contained in:
parent
383531c5cd
commit
fbef08fe8e
@ -12,7 +12,6 @@ import {
|
|||||||
import type { TooltipContentProps } from "recharts";
|
import type { TooltipContentProps } from "recharts";
|
||||||
import { getRankConfigTrend } from "@/serverFunctions/rank-tracking";
|
import { getRankConfigTrend } from "@/serverFunctions/rank-tracking";
|
||||||
import type { RankTrackingRow } from "@/types/schemas/rank-tracking";
|
import type { RankTrackingRow } from "@/types/schemas/rank-tracking";
|
||||||
import { computeScorecards } from "./rankTrackingScorecards";
|
|
||||||
import {
|
import {
|
||||||
formatDateTick,
|
formatDateTick,
|
||||||
TrendRangeToggle,
|
TrendRangeToggle,
|
||||||
@ -45,11 +44,6 @@ export function RankTrackingOverview({
|
|||||||
}) {
|
}) {
|
||||||
const [sinceDays, setSinceDays] = useState(730);
|
const [sinceDays, setSinceDays] = useState(730);
|
||||||
|
|
||||||
const scorecards = useMemo(
|
|
||||||
() => computeScorecards(rows, device),
|
|
||||||
[rows, device],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: trend, isLoading: trendLoading } = useQuery({
|
const { data: trend, isLoading: trendLoading } = useQuery({
|
||||||
queryKey: ["rankConfigTrend", projectId, configId, device, sinceDays],
|
queryKey: ["rankConfigTrend", projectId, configId, device, sinceDays],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@ -74,153 +68,111 @@ export function RankTrackingOverview({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 pt-4 pb-4">
|
<div className="px-4 pt-4 pb-4">
|
||||||
<div className="grid items-start gap-3 lg:grid-cols-2">
|
<div className="rounded-lg border border-base-300 p-3 space-y-2">
|
||||||
{/* All metrics in one card */}
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="rounded-lg border border-base-300 bg-base-100 p-4">
|
<span className="text-sm font-medium">Position distribution</span>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-3">
|
<TrendRangeToggle value={sinceDays} onChange={setSinceDays} />
|
||||||
<Scorecard
|
|
||||||
label="Visibility"
|
|
||||||
value={
|
|
||||||
scorecards.visibility === null
|
|
||||||
? "-"
|
|
||||||
: `${Math.round(scorecards.visibility)}%`
|
|
||||||
}
|
|
||||||
delta={scorecards.visibilityDelta}
|
|
||||||
hint="of click potential"
|
|
||||||
/>
|
|
||||||
<Scorecard
|
|
||||||
label="Ranking"
|
|
||||||
value={String(scorecards.ranking)}
|
|
||||||
delta={scorecards.rankingDelta}
|
|
||||||
hint={`of ${rows.length} tracked`}
|
|
||||||
/>
|
|
||||||
<Scorecard
|
|
||||||
label="In Top 3"
|
|
||||||
value={String(scorecards.top3)}
|
|
||||||
hint="of ranked keywords"
|
|
||||||
/>
|
|
||||||
<Scorecard
|
|
||||||
label="In Top 10"
|
|
||||||
value={String(scorecards.top10)}
|
|
||||||
hint="includes Top 3"
|
|
||||||
/>
|
|
||||||
<Scorecard
|
|
||||||
label="Improved / Declined"
|
|
||||||
value={`${scorecards.improved} / ${scorecards.declined}`}
|
|
||||||
hint="vs comparison period"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Position distribution */}
|
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
||||||
<div className="rounded-lg border border-base-300 p-3 space-y-2">
|
{BUCKETS.map((b) => (
|
||||||
<div className="flex items-center justify-between gap-2">
|
<span
|
||||||
<span className="text-sm font-medium">Position distribution</span>
|
key={b.key}
|
||||||
<TrendRangeToggle value={sinceDays} onChange={setSinceDays} />
|
className="inline-flex items-center gap-1 text-[11px] text-base-content/60"
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
|
||||||
{BUCKETS.map((b) => (
|
|
||||||
<span
|
|
||||||
key={b.key}
|
|
||||||
className="inline-flex items-center gap-1 text-[11px] text-base-content/60"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="size-2 rounded-sm"
|
|
||||||
style={{ backgroundColor: b.color }}
|
|
||||||
/>
|
|
||||||
{b.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{trendLoading ? (
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<Loader2 className="size-4 animate-spin text-base-content/50" />
|
|
||||||
</div>
|
|
||||||
) : chartData.length <= 1 ? (
|
|
||||||
<div className="rounded-lg border border-dashed border-base-300 p-8 text-center text-xs text-base-content/60">
|
|
||||||
{chartData.length === 0
|
|
||||||
? "No history yet — run a check to start tracking positions over time."
|
|
||||||
: "Only 1 check so far — the trend fills in after the next check."}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="w-full min-w-0"
|
|
||||||
style={{ height: 180 }}
|
|
||||||
>
|
>
|
||||||
{width > 0 ? (
|
<span
|
||||||
<AreaChart
|
className="size-2 rounded-sm"
|
||||||
width={width}
|
style={{ backgroundColor: b.color }}
|
||||||
height={180}
|
/>
|
||||||
data={chartData}
|
{b.label}
|
||||||
margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
|
</span>
|
||||||
>
|
))}
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
stroke="currentColor"
|
|
||||||
opacity={0.1}
|
|
||||||
vertical={false}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="checkedAt"
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
domain={["dataMin", "dataMax"]}
|
|
||||||
tickFormatter={formatDateTick}
|
|
||||||
tick={{ fontSize: 10, fill: "#888" }}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
minTickGap={32}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
allowDecimals={false}
|
|
||||||
tick={{ fontSize: 10, fill: "#888" }}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
width={28}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
content={(props: TooltipContentProps<number, string>) => {
|
|
||||||
const { active, payload, label } = props;
|
|
||||||
if (
|
|
||||||
!active ||
|
|
||||||
!payload?.length ||
|
|
||||||
typeof label !== "number"
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const byKey = new Map(
|
|
||||||
payload.map((p: PayloadEntry) => [
|
|
||||||
String(p.dataKey),
|
|
||||||
typeof p.value === "number" ? p.value : 0,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<DistributionTooltip label={label} byKey={byKey} />
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
cursor={{ stroke: "rgba(150,150,150,0.3)" }}
|
|
||||||
/>
|
|
||||||
{BUCKETS.map((b) => (
|
|
||||||
<Area
|
|
||||||
key={b.key}
|
|
||||||
type="monotone"
|
|
||||||
dataKey={b.key}
|
|
||||||
name={b.label}
|
|
||||||
stackId="positions"
|
|
||||||
stroke={b.color}
|
|
||||||
fill={b.color}
|
|
||||||
fillOpacity={0.7}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</AreaChart>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{trendLoading ? (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<Loader2 className="size-4 animate-spin text-base-content/50" />
|
||||||
|
</div>
|
||||||
|
) : chartData.length <= 1 ? (
|
||||||
|
<div className="rounded-lg border border-dashed border-base-300 p-8 text-center text-xs text-base-content/60">
|
||||||
|
{chartData.length === 0
|
||||||
|
? "No history yet — run a check to start tracking positions over time."
|
||||||
|
: "Only 1 check so far — the trend fills in after the next check."}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="w-full min-w-0"
|
||||||
|
style={{ height: 220 }}
|
||||||
|
>
|
||||||
|
{width > 0 ? (
|
||||||
|
<AreaChart
|
||||||
|
width={width}
|
||||||
|
height={220}
|
||||||
|
data={chartData}
|
||||||
|
margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
stroke="currentColor"
|
||||||
|
opacity={0.1}
|
||||||
|
vertical={false}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="checkedAt"
|
||||||
|
type="number"
|
||||||
|
scale="time"
|
||||||
|
domain={["dataMin", "dataMax"]}
|
||||||
|
tickFormatter={formatDateTick}
|
||||||
|
tick={{ fontSize: 10, fill: "#888" }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
minTickGap={32}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
allowDecimals={false}
|
||||||
|
tick={{ fontSize: 10, fill: "#888" }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
width={28}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
content={(props: TooltipContentProps<number, string>) => {
|
||||||
|
const { active, payload, label } = props;
|
||||||
|
if (
|
||||||
|
!active ||
|
||||||
|
!payload?.length ||
|
||||||
|
typeof label !== "number"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const byKey = new Map(
|
||||||
|
payload.map((p: PayloadEntry) => [
|
||||||
|
String(p.dataKey),
|
||||||
|
typeof p.value === "number" ? p.value : 0,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
return <DistributionTooltip label={label} byKey={byKey} />;
|
||||||
|
}}
|
||||||
|
cursor={{ stroke: "rgba(150,150,150,0.3)" }}
|
||||||
|
/>
|
||||||
|
{BUCKETS.map((b) => (
|
||||||
|
<Area
|
||||||
|
key={b.key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={b.key}
|
||||||
|
name={b.label}
|
||||||
|
stackId="positions"
|
||||||
|
stroke={b.color}
|
||||||
|
fill={b.color}
|
||||||
|
fillOpacity={0.7}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AreaChart>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -257,37 +209,3 @@ function DistributionTooltip({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Scorecard({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
delta,
|
|
||||||
hint,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
delta?: number | null;
|
|
||||||
hint?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-xs text-base-content/55">{label}</p>
|
|
||||||
<div className="flex items-baseline gap-1.5">
|
|
||||||
<span className="text-xl font-semibold tabular-nums">{value}</span>
|
|
||||||
{delta != null && delta !== 0 && (
|
|
||||||
<span
|
|
||||||
className={`text-xs font-medium ${
|
|
||||||
delta > 0 ? "text-success" : "text-warning"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{delta > 0 ? "▲" : "▼"}{" "}
|
|
||||||
{Number.isInteger(delta)
|
|
||||||
? Math.abs(delta)
|
|
||||||
: Math.abs(delta).toFixed(1)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{hint && <p className="text-[11px] text-base-content/45">{hint}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user