Files
tool-evaluator/artifacts/toolrate/src/pages/home.tsx
T
opencode 8f2fd89847
Build & Push Docker Image / build (push) Successful in 2m49s
feat(import): bulk tool import (CSV/JSON/YAML) for admins; NetBox-style help button
- Add POST /admin/tools/import with format auto-detect, CSV delimiters
  (comma/semicolon/tab), per-row validation via CreateToolBody, bulk insert,
  audit log entries per imported tool; gated by new 'tool-import' feature
  flag (premium/enterprise; admins always pass)
- Add tool-import-dialog UI (format tabs, delimiter select, textarea, file
  upload, result/error list) behind hasFeature('tool-import')
- Replace FieldHelp question marks and bare GuideHelp links with a NetBox-style
  'Hilfe/Help' outline button (HelpCircle + text) in form headers only
- Sync locales to 482 keys per language (de/en), update handbook docs
  (administration import section, index/plaene feature tables), regenerate
  API client + zod schemas, add yaml dependency
2026-08-04 23:09:11 +02:00

174 lines
7.2 KiB
TypeScript

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, Plus } from "lucide-react";
import { Link } from "wouter";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
export default function Home() {
const { t } = useTranslation();
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 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">
{t("home.welcome")}
</h1>
<p className="text-muted-foreground">{t("home.tagline")}</p>
</div>
<Button asChild>
<Link href="/tools/new">
<Plus className="w-4 h-4 mr-1" />
{t("browse.addTool")}
</Link>
</Button>
</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">{t("home.totalTools")}</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">{t("home.totalRatings")}</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">{t("home.avgRating")}</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">{t("home.topRated")}</h2>
<p className="text-muted-foreground text-sm">{t("home.topRatedSub")}</p>
</div>
<Button variant="ghost" className="text-primary hidden sm:flex" asChild>
<Link href="/tools?sort=top_rated">
{t("common.viewAll")} <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">{t("home.noRatingsYet")}</h3>
<p className="text-muted-foreground max-w-sm mt-1 mb-4">{t("home.noRatingsSub")}</p>
<Button asChild>
<Link href="/tools">{t("common.browseTools")}</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">{t("home.recentlyAdded")}</h2>
<p className="text-muted-foreground text-sm">{t("home.recentlyAddedSub")}</p>
</div>
<Button variant="ghost" className="text-primary hidden sm:flex" asChild>
<Link href="/tools?sort=newest">
{t("common.viewAll")} <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">{t("home.noTools")}</h3>
<p className="text-muted-foreground max-w-sm mt-1 mb-4">{t("home.noToolsSub")}</p>
<Button asChild>
<Link href="/tools/new">{t("browse.addTool")}</Link>
</Button>
</CardContent>
</Card>
)}
</section>
</div>
</Layout>
);
}