|
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 | +}; |
2 | 75 |
|
3 | 76 | 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 “{query}”. |
| 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 | + ); |
5 | 183 | } |
0 commit comments