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
@@ -0,0 +1,148 @@
import { useState } from "react";
import {
useListTools,
useListCategories,
ListToolsSort
} from "@workspace/api-client-react";
import { Layout } from "@/components/layout";
import { ToolCard } from "@/components/tool-card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Search, Wrench, SlidersHorizontal, X } from "lucide-react";
import { Link } from "wouter";
export default function ToolsBrowse() {
const [search, setSearch] = useState("");
const [searchInput, setSearchInput] = useState("");
const [category, setCategory] = useState<string>("all");
const [sort, setSort] = useState<ListToolsSort>(ListToolsSort.newest);
const { data: categories, isLoading: loadingCategories } = useListCategories();
const queryParams = {
...(search ? { search } : {}),
...(category && category !== "all" ? { category } : {}),
...(sort ? { sort } : {})
};
const { data: tools, isLoading: loadingTools } = useListTools(queryParams);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setSearch(searchInput);
};
const clearFilters = () => {
setSearch("");
setSearchInput("");
setCategory("all");
setSort(ListToolsSort.newest);
};
const hasFilters = search !== "" || category !== "all" || sort !== ListToolsSort.newest;
return (
<Layout>
<div className="space-y-6 pb-8">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Browse Tools</h1>
<p className="text-muted-foreground">Discover and evaluate the best tools for your stack.</p>
</div>
<Button asChild>
<Link href="/tools/new">Add a Tool</Link>
</Button>
</div>
{/* Filters */}
<div className="bg-card border rounded-xl p-4 flex flex-col md:flex-row gap-4">
<form onSubmit={handleSearch} className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search tools..."
className="pl-9 w-full"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
data-testid="input-search"
/>
</form>
<div className="flex flex-wrap sm:flex-nowrap gap-4 shrink-0">
<Select value={category} onValueChange={setCategory}>
<SelectTrigger className="w-full sm:w-[180px]" data-testid="select-category">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{!loadingCategories && categories?.map((c) => (
<SelectItem key={c} value={c}>{c}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={sort} onValueChange={(val) => setSort(val as ListToolsSort)}>
<SelectTrigger className="w-full sm:w-[180px]" data-testid="select-sort">
<SelectValue placeholder="Sort By" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ListToolsSort.newest}>Newest</SelectItem>
<SelectItem value={ListToolsSort.top_rated}>Top Rated</SelectItem>
<SelectItem value={ListToolsSort.most_reviewed}>Most Reviewed</SelectItem>
</SelectContent>
</Select>
{hasFilters && (
<Button variant="ghost" size="icon" onClick={clearFilters} className="shrink-0" title="Clear filters">
<X className="w-4 h-4" />
</Button>
)}
</div>
</div>
{/* Results */}
<div>
<div className="flex items-center text-sm text-muted-foreground mb-4">
<SlidersHorizontal className="w-4 h-4 mr-2" />
{loadingTools ? "Loading tools..." : `Showing ${tools?.length || 0} tools`}
</div>
{loadingTools ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{[1, 2, 3, 4, 5, 6, 7, 8].map(i => (
<Skeleton key={`sk-tools-${i}`} className="h-[200px] w-full rounded-xl" />
))}
</div>
) : tools && tools.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{tools.map((tool) => (
<ToolCard key={tool.id} tool={tool} />
))}
</div>
) : (
<div className="bg-muted/30 border border-dashed rounded-xl py-24 flex flex-col items-center justify-center text-center">
<div className="bg-muted p-4 rounded-full mb-4">
<Wrench className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="text-xl font-medium mb-2">No tools found</h3>
<p className="text-muted-foreground max-w-md mx-auto mb-6">
We couldn't find any tools matching your current filters. Try adjusting your search criteria or add a new tool.
</p>
<div className="flex gap-4">
{hasFilters && (
<Button variant="outline" onClick={clearFilters}>
Clear Filters
</Button>
)}
<Button asChild>
<Link href="/tools/new">Add Tool</Link>
</Button>
</div>
</div>
)}
</div>
</div>
</Layout>
);
}