Skip to content

Commit 8ffa88a

Browse files
committed
Update design for favourites
1 parent 77dc4aa commit 8ffa88a

1 file changed

Lines changed: 180 additions & 2 deletions

File tree

Lines changed: 180 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,183 @@
1-
import { FavoritesSection } from "@/components/Home/FavoritesSection/FavoritesSection";
1+
import { useNavigate } from "@tanstack/react-router";
2+
import { ChevronLeft, ChevronRight, GitBranch, Play, X } from "lucide-react";
3+
import { useState } from "react";
4+
5+
import { Button } from "@/components/ui/button";
6+
import { Icon } from "@/components/ui/icon";
7+
import { Input } from "@/components/ui/input";
8+
import { BlockStack, InlineStack } from "@/components/ui/layout";
9+
import { Paragraph, Text } from "@/components/ui/typography";
10+
import { type FavoriteItem, useFavorites } from "@/hooks/useFavorites";
11+
import { EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router";
12+
13+
const PAGE_SIZE = 16;
14+
15+
function getFavoriteUrl(item: FavoriteItem): string {
16+
if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`;
17+
return `${RUNS_BASE_PATH}/${item.id}`;
18+
}
19+
20+
const FavoriteCard = ({ item }: { item: FavoriteItem }) => {
21+
const navigate = useNavigate();
22+
const { removeFavorite } = useFavorites();
23+
24+
const isPipeline = item.type === "pipeline";
25+
26+
return (
27+
<div
28+
onClick={() => navigate({ to: getFavoriteUrl(item) })}
29+
className={`group relative flex flex-col gap-2 p-3 border rounded-lg cursor-pointer transition-colors ${
30+
isPipeline
31+
? "bg-violet-50/40 hover:bg-violet-50 border-violet-100"
32+
: "bg-emerald-50/40 hover:bg-emerald-50 border-emerald-100"
33+
}`}
34+
>
35+
{/* Remove button */}
36+
<button
37+
onClick={(e) => {
38+
e.stopPropagation();
39+
removeFavorite(item.type, item.id);
40+
}}
41+
className="absolute top-2 right-2 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer"
42+
aria-label="Remove from favorites"
43+
>
44+
<X className="h-3.5 w-3.5" />
45+
</button>
46+
47+
{/* Type badge */}
48+
<InlineStack gap="1" blockAlign="center">
49+
{isPipeline ? (
50+
<GitBranch className="h-3.5 w-3.5 shrink-0 text-violet-500" />
51+
) : (
52+
<Play className="h-3.5 w-3.5 shrink-0 text-emerald-500" />
53+
)}
54+
<Text
55+
size="xs"
56+
weight="semibold"
57+
className={isPipeline ? "text-violet-600" : "text-emerald-600"}
58+
>
59+
{isPipeline ? "Pipeline" : "Run"}
60+
</Text>
61+
</InlineStack>
62+
63+
{/* Name */}
64+
<Text size="sm" weight="semibold" className="truncate pr-4 leading-tight">
65+
{item.name}
66+
</Text>
67+
68+
{/* ID */}
69+
<Text size="xs" className="truncate text-muted-foreground font-mono">
70+
{item.id}
71+
</Text>
72+
</div>
73+
);
74+
};
275

376
export function DashboardFavoritesView() {
4-
return <FavoritesSection />;
77+
const { favorites } = useFavorites();
78+
const [page, setPage] = useState(0);
79+
const [query, setQuery] = useState("");
80+
81+
const filtered = query.trim()
82+
? favorites.filter((f) => {
83+
const q = query.toLowerCase();
84+
return (
85+
f.id.toLowerCase().includes(q) || f.name.toLowerCase().includes(q)
86+
);
87+
})
88+
: favorites;
89+
90+
const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
91+
const safePage = Math.min(page, Math.max(0, totalPages - 1));
92+
const paginated = filtered.slice(
93+
safePage * PAGE_SIZE,
94+
(safePage + 1) * PAGE_SIZE,
95+
);
96+
97+
return (
98+
<BlockStack gap="4">
99+
<Text as="h2" size="lg" weight="semibold">
100+
Favorites
101+
</Text>
102+
103+
{favorites.length === 0 ? (
104+
<Paragraph tone="subdued" size="sm">
105+
No favorites yet. Star a pipeline or run to pin it here.
106+
</Paragraph>
107+
) : (
108+
<BlockStack gap="4">
109+
{/* Search */}
110+
<div className="relative w-64">
111+
<Icon
112+
name="Search"
113+
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
114+
/>
115+
<Input
116+
placeholder="Search by name or ID..."
117+
value={query}
118+
onChange={(e) => {
119+
setPage(0);
120+
setQuery(e.target.value);
121+
}}
122+
className="pl-9 pr-8"
123+
/>
124+
{query && (
125+
<Button
126+
variant="ghost"
127+
size="icon"
128+
onClick={() => {
129+
setPage(0);
130+
setQuery("");
131+
}}
132+
className="absolute right-2 top-1/2 -translate-y-1/2 size-6 text-muted-foreground hover:text-foreground"
133+
aria-label="Clear search"
134+
>
135+
<Icon name="X" size="sm" />
136+
</Button>
137+
)}
138+
</div>
139+
140+
{/* Grid */}
141+
{paginated.length === 0 ? (
142+
<Paragraph tone="subdued" size="sm">
143+
No results for &ldquo;{query}&rdquo;.
144+
</Paragraph>
145+
) : (
146+
<div className="grid grid-cols-4 gap-3">
147+
{paginated.map((item) => (
148+
<FavoriteCard key={`${item.type}-${item.id}`} item={item} />
149+
))}
150+
</div>
151+
)}
152+
153+
{/* Pagination */}
154+
{totalPages > 1 && (
155+
<InlineStack blockAlign="center" gap="2">
156+
<Button
157+
variant="ghost"
158+
size="icon"
159+
className="h-7 w-7"
160+
disabled={safePage === 0}
161+
onClick={() => setPage(safePage - 1)}
162+
>
163+
<ChevronLeft className="h-4 w-4" />
164+
</Button>
165+
<Text size="sm" className="text-muted-foreground">
166+
{safePage + 1} / {totalPages}
167+
</Text>
168+
<Button
169+
variant="ghost"
170+
size="icon"
171+
className="h-7 w-7"
172+
disabled={safePage >= totalPages - 1}
173+
onClick={() => setPage(safePage + 1)}
174+
>
175+
<ChevronRight className="h-4 w-4" />
176+
</Button>
177+
</InlineStack>
178+
)}
179+
</BlockStack>
180+
)}
181+
</BlockStack>
182+
);
5183
}

0 commit comments

Comments
 (0)