fix: compact filter popover; feat: i18n (de/en)
Build & Push Docker Image / build (push) Successful in 2m31s
Build & Push Docker Image / build (push) Successful in 2m31s
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user