feat: auth foundation, similar tools, costs, redundancy, anonymous voting

This commit is contained in:
root
2026-07-29 21:59:29 +02:00
parent 8b3b7c9955
commit 59badeaa48
22 changed files with 793 additions and 15 deletions
+107
View File
@@ -0,0 +1,107 @@
import { useState, useEffect } from "react";
import { Layout } from "@/components/layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Progress } from "@/components/ui/progress";
import { Star, AlertTriangle } from "lucide-react";
import { Link } from "wouter";
import { customFetch } from "@workspace/api-client-react";
export default function RedundancyPage() {
const [data, setData] = useState<any[] | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
customFetch<any[]>("/api/admin/redundancy")
.then(setData)
.catch(() => {})
.finally(() => setLoading(false));
}, []);
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center gap-3">
<AlertTriangle className="w-6 h-6 text-amber-500" />
<h1 className="text-3xl font-bold">Redundancy Dashboard</h1>
</div>
<p className="text-muted-foreground">
Tools grouped by category with feature overlap analysis.
</p>
{loading ? (
<div className="space-y-6">
{Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-40 rounded-xl" />)}
</div>
) : data && data.length > 0 ? (
<div className="space-y-8">
{data.map((group) => (
<div key={group.category}>
<h2 className="text-xl font-semibold mb-4 capitalize">{group.category}</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{group.tools.map((tool: any) => (
<Link key={tool.id} href={`/tools/${tool.id}`}>
<Card className="hover-elevate transition-all cursor-pointer hover:border-primary/50">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<span className="font-medium">{tool.name}</span>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
<span>{tool.ratingCount} reviews</span>
{tool.avgCombined != null && (
<>
<span>·</span>
<span className="flex items-center gap-0.5">
<Star className="w-3 h-3 fill-primary text-primary" />
{tool.avgCombined.toFixed(1)}
</span>
</>
)}
</div>
</div>
<Badge variant="outline">{tool.features.length} features</Badge>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
{group.pairs.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">Overlap Analysis</h3>
{group.pairs.map((pair: any, i: number) => (
<Card key={i} className="border-dashed">
<CardContent className="p-3 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0 flex-1">
<span className="font-medium text-sm truncate">{pair.a.name}</span>
<span className="text-muted-foreground text-xs shrink-0">vs</span>
<span className="font-medium text-sm truncate">{pair.b.name}</span>
</div>
<div className="flex items-center gap-4 shrink-0">
<div className="flex items-center gap-2">
<Progress value={pair.overlap} className="w-16 h-2" />
<span className="text-xs text-muted-foreground w-8">{pair.overlap}%</span>
</div>
{pair.scoreDiff !== 0 && (
<Badge variant={pair.scoreDiff > 0 ? "default" : "secondary"} className="text-[10px]">
{pair.scoreDiff > 0 ? `${pair.b.name} +${pair.scoreDiff.toFixed(1)}` : `${pair.a.name} +${Math.abs(pair.scoreDiff).toFixed(1)}`}
</Badge>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
))}
</div>
) : (
<p className="text-muted-foreground py-8 text-center">No tools found.</p>
)}
</div>
</Layout>
);
}