feat: watchlist (premium), mobile bottom nav
Build & Push Docker Image / build (push) Successful in 2m35s
Build & Push Docker Image / build (push) Successful in 2m35s
This commit is contained in:
@@ -41,9 +41,11 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend } from "recharts";
|
||||
import { ExternalLink, Star, ArrowLeft, Plus, Pencil, Trash2, Link as LinkIcon, DollarSign, Euro } from "lucide-react";
|
||||
import { ExternalLink, Star, ArrowLeft, Plus, Pencil, Trash2, Link as LinkIcon, DollarSign, Euro, Bookmark } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDeleteTool, getListToolsQueryKey } from "@workspace/api-client-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -77,6 +79,7 @@ export default function ToolDetail() {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
const { user, isAdmin, hasFeature } = useAuth();
|
||||
const { isWatched, toggle: toggleWatchlist, canWatchlist } = useWatchlist();
|
||||
const canManageCosts = hasFeature("costs");
|
||||
const hasTrash = hasFeature("trash");
|
||||
const deleteTool = useDeleteTool();
|
||||
@@ -355,6 +358,17 @@ export default function ToolDetail() {
|
||||
Based on {tool.ratingCount} reviews
|
||||
</div>
|
||||
|
||||
{canWatchlist && (
|
||||
<Button
|
||||
variant={isWatched(id) ? "default" : "outline"}
|
||||
className="w-full gap-2"
|
||||
onClick={() => toggleWatchlist(Number(id))}
|
||||
>
|
||||
<Bookmark className={cn("w-4 h-4", isWatched(id) && "fill-current")} />
|
||||
{isWatched(id) ? "Saved to watchlist" : "Save to watchlist"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tool.websiteUrl && (
|
||||
<Button asChild className="w-full mt-2" variant="outline">
|
||||
<a href={tool.websiteUrl} target="_blank" rel="noopener noreferrer">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FilterPopover } from "@/components/filter-popover";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
import { useBrowsePreferences, isViewMode, isDensity } from "@/hooks/use-browse-preferences";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||
import { CompareBar } from "@/components/compare-bar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -87,6 +88,7 @@ export default function ToolsBrowse() {
|
||||
|
||||
const { serverView, serverDensity, localView, localDensity, persist } = useBrowsePreferences();
|
||||
const { hasFeature } = useAuth();
|
||||
const { isWatched, toggle: toggleWatchlist, canWatchlist } = useWatchlist();
|
||||
|
||||
const [compareIds, setCompareIds] = useState<number[]>([]);
|
||||
const [compareUpsellOpen, setCompareUpsellOpen] = useState(false);
|
||||
@@ -402,6 +404,7 @@ export default function ToolsBrowse() {
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
compare={{ selected: compareIds.includes(tool.id), onToggle: () => toggleCompare(tool.id) }}
|
||||
watchlist={canWatchlist ? { watched: isWatched(tool.id), onToggle: () => toggleWatchlist(tool.id) } : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
useGetMeWatchlist,
|
||||
getGetMeWatchlistQueryKey,
|
||||
} from "@workspace/api-client-react";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||
import { Layout } from "@/components/layout";
|
||||
import { ToolCard } from "@/components/tool-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ShieldAlert, Bookmark } from "lucide-react";
|
||||
|
||||
export default function Watchlist() {
|
||||
const { hasFeature, isLoading: authLoading } = useAuth();
|
||||
const { toggle } = useWatchlist();
|
||||
const canWatchlist = hasFeature("watchlist");
|
||||
|
||||
const { data: tools, isLoading: loading } = useGetMeWatchlist({
|
||||
query: { queryKey: getGetMeWatchlistQueryKey(), enabled: canWatchlist },
|
||||
});
|
||||
|
||||
if (!authLoading && !canWatchlist) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
||||
<ShieldAlert className="w-12 h-12 text-muted-foreground" />
|
||||
<h2 className="text-2xl font-bold">Watchlist requires a higher tier</h2>
|
||||
<p className="text-muted-foreground">Saving tools to your watchlist is available to Premium and Enterprise users.</p>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="/tools">Back to tools</a>
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const list = tools ?? [];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6 pb-10">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2">Watchlist</h1>
|
||||
<p className="text-muted-foreground">Tools you saved for later, with live scores.</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => <Skeleton key={i} className="h-[200px] w-full rounded-xl" />)}
|
||||
</div>
|
||||
) : list.length === 0 ? (
|
||||
<div className="bg-muted/30 border border-dashed rounded-xl py-24 flex flex-col items-center justify-center text-center gap-3">
|
||||
<Bookmark className="w-8 h-8 text-muted-foreground" />
|
||||
<h3 className="text-xl font-medium">Your watchlist is empty</h3>
|
||||
<p className="text-muted-foreground max-w-md mx-auto">
|
||||
Browse tools and click the bookmark to save them here.
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="/tools">Browse tools</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{list.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
watchlist={{ watched: true, onToggle: () => toggle(tool.id) }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user