fix: white home page - coerce pg numeric strings for top-tools ratings
Build & Push Docker Image / build (push) Successful in 2m32s

This commit is contained in:
opencode
2026-08-02 13:10:28 +02:00
parent d033b20dfb
commit 95bf49509c
2 changed files with 47 additions and 29 deletions
+7 -1
View File
@@ -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,
}; };
+40 -28
View File
@@ -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