fix: compact filter popover; feat: i18n (de/en)
Build & Push Docker Image / build (push) Successful in 2m31s

This commit is contained in:
opencode
2026-08-02 13:42:53 +02:00
parent 95bf49509c
commit 2f38889726
19 changed files with 662 additions and 160 deletions
+25 -23
View File
@@ -46,6 +46,7 @@ import { Link } from "wouter";
import { useAuth } from "@/hooks/use-auth";
import { useWatchlist } from "@/hooks/use-watchlist";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { useDeleteTool, getListToolsQueryKey } from "@workspace/api-client-react";
import {
AlertDialog,
@@ -79,6 +80,7 @@ export default function ToolDetail() {
const [deleteOpen, setDeleteOpen] = useState(false);
const { user, isAdmin, hasFeature } = useAuth();
const { t } = useTranslation();
const { isWatched, toggle: toggleWatchlist, canWatchlist } = useWatchlist();
const canManageCosts = hasFeature("costs");
const hasTrash = hasFeature("trash");
@@ -355,7 +357,7 @@ export default function ToolDetail() {
<span className="text-muted-foreground self-end mb-1">/ 5</span>
</div>
<div className="text-sm text-muted-foreground">
Based on {tool.ratingCount} reviews
{t("detail.basedOnReview", { count: tool.ratingCount })}
</div>
{canWatchlist && (
@@ -365,14 +367,14 @@ export default function ToolDetail() {
onClick={() => toggleWatchlist(Number(id))}
>
<Bookmark className={cn("w-4 h-4", isWatched(id) && "fill-current")} />
{isWatched(id) ? "Saved to watchlist" : "Save to watchlist"}
{isWatched(id) ? t("detail.savedToWatchlist") : t("detail.saveToWatchlist")}
</Button>
)}
{tool.websiteUrl && (
<Button asChild className="w-full mt-2" variant="outline">
<a href={tool.websiteUrl} target="_blank" rel="noopener noreferrer">
Visit Website <ExternalLink className="w-4 h-4 ml-2" />
{t("detail.visitWebsite")} <ExternalLink className="w-4 h-4 ml-2" />
</a>
</Button>
)}
@@ -381,7 +383,7 @@ export default function ToolDetail() {
<div className="flex gap-2 w-full mt-1">
<Button asChild variant="outline" size="sm" className="flex-1 gap-2">
<Link href={`/tools/${id}/edit`}>
<Pencil className="w-3.5 h-3.5" /> Edit
<Pencil className="w-3.5 h-3.5" /> {t("detail.edit")}
</Link>
</Button>
<Button
@@ -390,7 +392,7 @@ export default function ToolDetail() {
className="flex-1 gap-2 text-destructive hover:bg-destructive hover:text-destructive-foreground"
onClick={() => setDeleteOpen(true)}
>
<Trash2 className="w-3.5 h-3.5" /> Delete
<Trash2 className="w-3.5 h-3.5" /> {t("detail.delete")}
</Button>
</div>
)}
@@ -556,10 +558,10 @@ export default function ToolDetail() {
{tool && (
<div className="space-y-4 mt-8">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">Costs</h3>
<h3 className="text-xl font-bold">{t("detail.costs")}</h3>
{canManageCosts && (
<Button variant="outline" size="sm" onClick={() => { resetCostForm(); setCostDialogOpen(true); }} className="gap-2">
<Plus className="w-4 h-4" /> Add Cost
<Plus className="w-4 h-4" /> {t("detail.addCost")}
</Button>
)}
</div>
@@ -607,7 +609,7 @@ export default function ToolDetail() {
<Dialog open={costDialogOpen} onOpenChange={(o) => { if (!o) setCostDialogOpen(false); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editCost ? "Edit Cost" : "Add Cost"}</DialogTitle>
<DialogTitle>{editCost ? t("detail.edit") + " " + t("detail.costs") : t("detail.addCost")}</DialogTitle>
<DialogDescription>Manage license cost information for this tool.</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
@@ -674,19 +676,19 @@ export default function ToolDetail() {
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Rating Breakdown</CardTitle>
<CardTitle>{t("detail.ratingBreakdown")}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<div className="flex justify-between items-center mb-2">
<span className="font-medium">Usefulness</span>
<span className="font-medium">{t("detail.usefulness")}</span>
<span className="font-bold">{tool.avgUsefulness ? tool.avgUsefulness.toFixed(1) : "0.0"}</span>
</div>
<Progress value={((tool.avgUsefulness || 0) / 5) * 100} className="h-2" />
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="font-medium">Usability</span>
<span className="font-medium">{t("detail.usability")}</span>
<span className="font-bold">{tool.avgUsability ? tool.avgUsability.toFixed(1) : "0.0"}</span>
</div>
<Progress value={((tool.avgUsability || 0) / 5) * 100} className="h-2" />
@@ -696,7 +698,7 @@ export default function ToolDetail() {
<Card>
<CardHeader>
<CardTitle>Score Distribution</CardTitle>
<CardTitle>{t("detail.scoreDistribution")}</CardTitle>
</CardHeader>
<CardContent>
{loadingDistribution ? (
@@ -718,7 +720,7 @@ export default function ToolDetail() {
<Card>
<CardHeader>
<CardTitle>Score Trend</CardTitle>
<CardTitle>{t("detail.scoreTrend")}</CardTitle>
</CardHeader>
<CardContent>
{loadingRatingHistory ? (
@@ -739,7 +741,7 @@ export default function ToolDetail() {
</ResponsiveContainer>
</div>
) : (
<p className="text-sm text-muted-foreground">Not enough ratings yet to show a trend.</p>
<p className="text-sm text-muted-foreground">{t("detail.notEnoughRatings")}</p>
)}
</CardContent>
</Card>
@@ -748,16 +750,16 @@ export default function ToolDetail() {
{/* Right Column: Reviews */}
<div className="lg:col-span-2 space-y-6">
<div className="flex justify-between items-center">
<h3 className="text-2xl font-bold">Reviews</h3>
<h3 className="text-2xl font-bold">{t("detail.reviews")}</h3>
{!isReviewFormOpen && user && (
<Button onClick={() => setIsReviewFormOpen(true)}>Write a Review</Button>
<Button onClick={() => setIsReviewFormOpen(true)}>{t("detail.addReview")}</Button>
)}
</div>
{isReviewFormOpen && (
<Card className="border-primary shadow-sm">
<CardHeader>
<CardTitle>Write a Review</CardTitle>
<CardTitle>{t("detail.addReview")}</CardTitle>
<CardDescription>Share your experience with {tool.name}</CardDescription>
</CardHeader>
<CardContent>
@@ -769,7 +771,7 @@ export default function ToolDetail() {
name="usefulness"
render={({ field }) => (
<FormItem>
<FormLabel>Usefulness</FormLabel>
<FormLabel>{t("detail.usefulness")}</FormLabel>
<div className="py-2">
<RatingStars
value={field.value}
@@ -787,7 +789,7 @@ export default function ToolDetail() {
name="usability"
render={({ field }) => (
<FormItem>
<FormLabel>Usability</FormLabel>
<FormLabel>{t("detail.usability")}</FormLabel>
<div className="py-2">
<RatingStars
value={field.value}
@@ -844,7 +846,7 @@ export default function ToolDetail() {
Cancel
</Button>
<Button type="submit" disabled={createRating.isPending}>
{createRating.isPending ? "Submitting..." : "Submit Review"}
{createRating.isPending ? t("common.loading") : t("detail.submit")}
</Button>
</div>
</form>
@@ -873,11 +875,11 @@ export default function ToolDetail() {
<div className="flex gap-6 mb-4">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground uppercase tracking-wider font-semibold">Usefulness</span>
<span className="text-xs text-muted-foreground uppercase tracking-wider font-semibold">{t("detail.usefulness")}</span>
<RatingStars value={rating.usefulness} size="sm" />
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground uppercase tracking-wider font-semibold">Usability</span>
<span className="text-xs text-muted-foreground uppercase tracking-wider font-semibold">{t("detail.usability")}</span>
<RatingStars value={rating.usability} size="sm" />
</div>
</div>
@@ -893,7 +895,7 @@ export default function ToolDetail() {
) : (
<div className="text-center py-12 bg-muted/30 border border-dashed rounded-xl">
<Star className="w-12 h-12 text-muted-foreground/30 mx-auto mb-3" />
<h4 className="text-lg font-medium">No reviews yet</h4>
<h4 className="text-lg font-medium">{t("detail.noReviews")}</h4>
<p className="text-muted-foreground mt-1">Be the first to share your thoughts on this tool.</p>
</div>
)}