fix: white home page - coerce pg numeric strings for top-tools ratings
Build & Push Docker Image / build (push) Successful in 2m32s
Build & Push Docker Image / build (push) Successful in 2m32s
This commit is contained in:
@@ -104,7 +104,13 @@ router.get("/analytics/top-tools", async (req, res): Promise<void> => {
|
|||||||
? (Number(avgUsefulness) + Number(avgUsability)) / 2
|
? (Number(avgUsefulness) + Number(avgUsability)) / 2
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
tool: { ...tool, ratingCount, avgUsefulness, avgUsability, avgCombined },
|
tool: {
|
||||||
|
...tool,
|
||||||
|
ratingCount,
|
||||||
|
avgUsefulness: avgUsefulness != null ? Number(avgUsefulness) : null,
|
||||||
|
avgUsability: avgUsability != null ? Number(avgUsability) : null,
|
||||||
|
avgCombined,
|
||||||
|
},
|
||||||
score: Number(score),
|
score: Number(score),
|
||||||
ratingCount,
|
ratingCount,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function pct(v: number | null | undefined): number {
|
function num(v: number | string | null | undefined): number | null {
|
||||||
if (v == null) return 0;
|
if (v == null || v === "") return null;
|
||||||
return Math.max(0, Math.min(5, v)) / 5 * 100;
|
const n = typeof v === "string" ? Number(v) : v;
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pct(v: number | string | null | undefined): number {
|
||||||
|
const n = num(v);
|
||||||
|
if (n == null) return 0;
|
||||||
|
return Math.max(0, Math.min(5, n)) / 5 * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MiniBars({
|
export function MiniBars({
|
||||||
@@ -10,8 +17,8 @@ export function MiniBars({
|
|||||||
usability,
|
usability,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
usefulness: number | null | undefined;
|
usefulness: number | string | null | undefined;
|
||||||
usability: number | null | undefined;
|
usability: number | string | null | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const rows = [
|
const rows = [
|
||||||
@@ -20,26 +27,29 @@ export function MiniBars({
|
|||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-1 w-full", className)}>
|
<div className={cn("space-y-1 w-full", className)}>
|
||||||
{rows.map(({ label, value }) => (
|
{rows.map(({ label, value }) => {
|
||||||
<div
|
const n = num(value);
|
||||||
key={label}
|
return (
|
||||||
className="flex items-center gap-2"
|
<div
|
||||||
title={`${label}: ${value != null ? value.toFixed(1) : "N/A"} / 5`}
|
key={label}
|
||||||
>
|
className="flex items-center gap-2"
|
||||||
<span className="text-[10px] uppercase tracking-wide text-muted-foreground w-[3.5rem] shrink-0">
|
title={`${label}: ${n != null ? n.toFixed(1) : "N/A"} / 5`}
|
||||||
{label}
|
>
|
||||||
</span>
|
<span className="text-[10px] uppercase tracking-wide text-muted-foreground w-[3.5rem] shrink-0">
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
{label}
|
||||||
<div
|
</span>
|
||||||
className="h-full rounded-full bg-primary/70"
|
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||||
style={{ width: `${pct(value)}%` }}
|
<div
|
||||||
/>
|
className="h-full rounded-full bg-primary/70"
|
||||||
|
style={{ width: `${pct(value)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground tabular-nums w-7 text-right shrink-0">
|
||||||
|
{n != null ? n.toFixed(1) : "–"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-muted-foreground tabular-nums w-7 text-right shrink-0">
|
);
|
||||||
{value != null ? value.toFixed(1) : "–"}
|
})}
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,15 +59,17 @@ export function MiniBarStack({
|
|||||||
usability,
|
usability,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
usefulness: number | null | undefined;
|
usefulness: number | string | null | undefined;
|
||||||
usability: number | null | undefined;
|
usability: number | string | null | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const u = num(usefulness);
|
||||||
|
const a = num(usability);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("h-1.5 w-16 rounded-full bg-muted overflow-hidden flex", className)}
|
className={cn("h-1.5 w-16 rounded-full bg-muted overflow-hidden flex", className)}
|
||||||
title={`Usefulness ${usefulness != null ? usefulness.toFixed(1) : "N/A"} / Usability ${
|
title={`Usefulness ${u != null ? u.toFixed(1) : "N/A"} / Usability ${
|
||||||
usability != null ? usability.toFixed(1) : "N/A"
|
a != null ? a.toFixed(1) : "N/A"
|
||||||
} (of 5)`}
|
} (of 5)`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user