Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/components/Home/FavoritesSection/FavoritesSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useNavigate } from "@tanstack/react-router";
import { ChevronLeft, ChevronRight, GitBranch, Play, Star, X } from "lucide-react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { InlineStack } from "@/components/ui/layout";
import { Paragraph, Text } from "@/components/ui/typography";
import { type FavoriteItem, useFavorites } from "@/hooks/useFavorites";
import { EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router";

const PAGE_SIZE = 10;

function getFavoriteUrl(item: FavoriteItem): string {
if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`;
return `${RUNS_BASE_PATH}/${item.id}`;
}

const FavoriteChip = ({ item }: { item: FavoriteItem }) => {
const navigate = useNavigate();
const { removeFavorite } = useFavorites();

const handleClick = () => {
navigate({ to: getFavoriteUrl(item) });
};

const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation();
removeFavorite(item.type, item.id);
};

return (
<div
onClick={handleClick}
title={item.name}
className={`group flex items-center gap-1.5 pl-2 pr-1 py-1 border rounded-md cursor-pointer min-w-0 ${
item.type === "pipeline"
? "bg-violet-50/50 hover:bg-violet-50 border-violet-100"
: "bg-emerald-50/50 hover:bg-emerald-50 border-emerald-100"
}`}
>
{item.type === "pipeline" ? (
<GitBranch className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
) : (
<Play className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
)}
<span className="text-sm truncate">{item.name}</span>
<button
onClick={handleRemove}
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer"
>
<X className="h-3 w-3" />
</button>
</div>
);
};

export const FavoritesSection = () => {
const { favorites } = useFavorites();
const [page, setPage] = useState(0);

const totalPages = Math.ceil(favorites.length / PAGE_SIZE);
// Reset to last valid page if favorites shrink (e.g. after removing items)
const safePage = Math.min(page, Math.max(0, totalPages - 1));
const paginated = favorites.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);

return (
<div className="flex flex-col gap-2">
<InlineStack blockAlign="center" gap="1">
<Star className="h-4 w-4 text-warning" fill="oklch(79.5% 0.184 86.047)" />
<Text as="h2" size="sm" weight="semibold">
Favorites
</Text>
</InlineStack>

{favorites.length === 0 ? (
<Paragraph tone="subdued" size="sm">
No favorites yet. Star a pipeline or run to pin it here.
</Paragraph>
) : (
<div className="flex flex-col gap-2">
<div className="flex flex-wrap gap-2">
{paginated.map((item) => (
<FavoriteChip key={`${item.type}-${item.id}`} item={item} />
))}
</div>

{totalPages > 1 && (
<InlineStack blockAlign="center" gap="2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
disabled={safePage === 0}
onClick={() => setPage(safePage - 1)}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Paragraph tone="subdued" size="sm">
{safePage + 1} / {totalPages}
</Paragraph>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
disabled={safePage >= totalPages - 1}
onClick={() => setPage(safePage + 1)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</InlineStack>
)}
</div>
)}
</div>
);
};
2 changes: 2 additions & 0 deletions src/routes/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FavoritesSection } from "@/components/Home/FavoritesSection/FavoritesSection";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";

Expand All @@ -17,6 +18,7 @@ export const Dashboard = () => {
Beta
</Text>
</InlineStack>
<FavoritesSection />
</BlockStack>
);
};
Loading