Add API endpoints and frontend components for tool management and analytics

Implement CRUD operations for tools and ratings, introduce analytics endpoints, and develop frontend components for displaying tools, ratings, and analytics data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: feaa4ce1-5aed-4cc0-bcea-47855b615b48
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/rx9K7bW
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
cheffe01
2026-05-25 13:04:53 +00:00
parent 1de22a6979
commit 036973ea32
111 changed files with 11006 additions and 96 deletions
+161
View File
@@ -0,0 +1,161 @@
import {
useGetAnalyticsSummary,
useGetTopTools,
useListTools,
GetTopToolsMetric
} from "@workspace/api-client-react";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Layout } from "@/components/layout";
import { ToolCard } from "@/components/tool-card";
import { Star, Wrench, MessageSquare, ArrowRight } from "lucide-react";
import { Link } from "wouter";
import { Button } from "@/components/ui/button";
export default function Home() {
const { data: summary, isLoading: loadingSummary } = useGetAnalyticsSummary();
const { data: topTools, isLoading: loadingTopTools } = useGetTopTools({ limit: 4, metric: GetTopToolsMetric.combined });
const { data: recentTools, isLoading: loadingRecent } = useListTools({ sort: "newest" });
return (
<Layout>
<div className="space-y-8 pb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Welcome to ToolRate</h1>
<p className="text-muted-foreground">The community hub where engineers honestly rate the tools they use daily.</p>
</div>
{/* Stats Banner */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="bg-primary/5 border-primary/20">
<CardContent className="p-6 flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg text-primary">
<Wrench className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground mb-1">Total Tools</p>
{loadingSummary ? (
<Skeleton className="h-8 w-16" />
) : (
<h3 className="text-3xl font-bold">{summary?.totalTools || 0}</h3>
)}
</div>
</CardContent>
</Card>
<Card className="bg-primary/5 border-primary/20">
<CardContent className="p-6 flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg text-primary">
<MessageSquare className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground mb-1">Total Ratings</p>
{loadingSummary ? (
<Skeleton className="h-8 w-16" />
) : (
<h3 className="text-3xl font-bold">{summary?.totalRatings || 0}</h3>
)}
</div>
</CardContent>
</Card>
<Card className="bg-primary/5 border-primary/20">
<CardContent className="p-6 flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg text-primary">
<Star className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground mb-1">Avg Rating</p>
{loadingSummary ? (
<Skeleton className="h-8 w-16" />
) : (
<h3 className="text-3xl font-bold">{summary?.avgCombined ? summary.avgCombined.toFixed(1) : "0.0"}</h3>
)}
</div>
</CardContent>
</Card>
</div>
{/* Top Tools Section */}
<section>
<div className="flex justify-between items-end mb-4">
<div>
<h2 className="text-2xl font-bold tracking-tight mb-1">Top Rated Tools</h2>
<p className="text-muted-foreground text-sm">Highest combined usefulness and usability.</p>
</div>
<Button variant="ghost" className="text-primary hidden sm:flex" asChild>
<Link href="/tools?sort=top_rated">
View all <ArrowRight className="w-4 h-4 ml-2" />
</Link>
</Button>
</div>
{loadingTopTools ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map(i => (
<Skeleton key={`sk-top-${i}`} className="h-[200px] w-full rounded-xl" />
))}
</div>
) : topTools && topTools.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{topTools.map(({ tool }) => (
<ToolCard key={tool.id} tool={tool} />
))}
</div>
) : (
<Card className="bg-muted/30 border-dashed">
<CardContent className="flex flex-col items-center justify-center p-12 text-center">
<Star className="w-12 h-12 text-muted-foreground/30 mb-4" />
<h3 className="text-lg font-medium">No ratings yet</h3>
<p className="text-muted-foreground max-w-sm mt-1 mb-4">Be the first to rate a tool and help the community.</p>
<Button asChild>
<Link href="/tools">Browse Tools</Link>
</Button>
</CardContent>
</Card>
)}
</section>
{/* Recently Added Section */}
<section>
<div className="flex justify-between items-end mb-4">
<div>
<h2 className="text-2xl font-bold tracking-tight mb-1">Recently Added</h2>
<p className="text-muted-foreground text-sm">The latest additions to the catalog.</p>
</div>
<Button variant="ghost" className="text-primary hidden sm:flex" asChild>
<Link href="/tools?sort=newest">
View all <ArrowRight className="w-4 h-4 ml-2" />
</Link>
</Button>
</div>
{loadingRecent ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map(i => (
<Skeleton key={`sk-recent-${i}`} className="h-[200px] w-full rounded-xl" />
))}
</div>
) : recentTools && recentTools.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{recentTools.slice(0, 4).map((tool) => (
<ToolCard key={tool.id} tool={tool} />
))}
</div>
) : (
<Card className="bg-muted/30 border-dashed">
<CardContent className="flex flex-col items-center justify-center p-12 text-center">
<Wrench className="w-12 h-12 text-muted-foreground/30 mb-4" />
<h3 className="text-lg font-medium">No tools found</h3>
<p className="text-muted-foreground max-w-sm mt-1 mb-4">There are no tools in the database yet.</p>
<Button asChild>
<Link href="/tools/new">Add a Tool</Link>
</Button>
</CardContent>
</Card>
)}
</section>
</div>
</Layout>
);
}