| 1 | import { ChevronRight, Pencil, Trash2 } from "lucide-react"; |
| 2 | |
| 3 | import { |
| 4 | Collapsible, |
| 5 | CollapsibleContent, |
| 6 | CollapsibleTrigger, |
| 7 | } from "@/components/ui/collapsible"; |
| 8 | import { |
| 9 | SidebarGroup, |
| 10 | SidebarGroupLabel, |
| 11 | SidebarMenu, |
| 12 | SidebarMenuButton, |
| 13 | SidebarMenuItem, |
| 14 | SidebarMenuSub, |
| 15 | SidebarMenuSubButton, |
| 16 | SidebarMenuSubItem, |
| 17 | } from "@/components/ui/sidebar"; |
| 18 | import { |
| 19 | ContextMenu, |
| 20 | ContextMenuContent, |
| 21 | ContextMenuItem, |
| 22 | ContextMenuTrigger, |
| 23 | } from "@/components/ui/context-menu"; |
| 24 | |
| 25 | interface Feed { |
| 26 | id: string; |
| 27 | title: string | null; |
| 28 | category: string | null; |
| 29 | } |
| 30 | |
| 31 | interface NavFeedsProps { |
| 32 | feeds: readonly Feed[]; |
| 33 | selectedFeedId?: string | null; |
| 34 | onFeedSelect: (feedId: string | null) => void; |
| 35 | onCategoryEdit?: (category: string) => void; |
| 36 | onCategoryDelete?: (category: string) => void; |
| 37 | } |
| 38 | |
| 39 | export function NavFeeds({ |
| 40 | feeds, |
| 41 | selectedFeedId, |
| 42 | onFeedSelect, |
| 43 | onCategoryEdit, |
| 44 | onCategoryDelete, |
| 45 | }: NavFeedsProps) { |
| 46 | // Group feeds by category |
| 47 | const feedsByCategory = feeds.reduce( |
| 48 | (acc, feed) => { |
| 49 | const category = feed.category || "Uncategorized"; |
| 50 | if (!acc[category]) { |
| 51 | acc[category] = []; |
| 52 | } |
| 53 | acc[category].push(feed); |
| 54 | return acc; |
| 55 | }, |
| 56 | {} as Record<string, Feed[]>, |
| 57 | ); |
| 58 | |
| 59 | // Sort feeds within each category alphabetically by title |
| 60 | Object.keys(feedsByCategory).forEach((category) => { |
| 61 | feedsByCategory[category].sort((a, b) => |
| 62 | (a.title || "").localeCompare(b.title || ""), |
| 63 | ); |
| 64 | }); |
| 65 | |
| 66 | const categories = Object.keys(feedsByCategory).sort(); |
| 67 | |
| 68 | return ( |
| 69 | <SidebarGroup> |
| 70 | <SidebarGroupLabel>Feeds</SidebarGroupLabel> |
| 71 | <SidebarMenu> |
| 72 | {/* All Feeds option */} |
| 73 | <SidebarMenuItem> |
| 74 | <SidebarMenuButton |
| 75 | onClick={() => onFeedSelect(null)} |
| 76 | isActive={selectedFeedId === null} |
| 77 | > |
| 78 | All Feeds |
| 79 | </SidebarMenuButton> |
| 80 | </SidebarMenuItem> |
| 81 | |
| 82 | {/* Unread option */} |
| 83 | <SidebarMenuItem> |
| 84 | <SidebarMenuButton |
| 85 | onClick={() => onFeedSelect("unread")} |
| 86 | isActive={selectedFeedId === "unread"} |
| 87 | > |
| 88 | Unread |
| 89 | </SidebarMenuButton> |
| 90 | </SidebarMenuItem> |
| 91 | |
| 92 | {/* Categories with feeds */} |
| 93 | {categories.map((category) => { |
| 94 | const isUncategorized = category === "Uncategorized"; |
| 95 | const categoryTrigger = ( |
| 96 | <CollapsibleTrigger asChild> |
| 97 | <SidebarMenuButton tooltip={category}> |
| 98 | <span className="font-medium">{category}</span> |
| 99 | <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> |
| 100 | </SidebarMenuButton> |
| 101 | </CollapsibleTrigger> |
| 102 | ); |
| 103 | |
| 104 | return ( |
| 105 | <Collapsible |
| 106 | key={category} |
| 107 | asChild |
| 108 | defaultOpen={true} |
| 109 | className="group/collapsible" |
| 110 | > |
| 111 | <SidebarMenuItem> |
| 112 | {isUncategorized ? ( |
| 113 | categoryTrigger |
| 114 | ) : ( |
| 115 | <ContextMenu> |
| 116 | <ContextMenuTrigger asChild> |
| 117 | {categoryTrigger} |
| 118 | </ContextMenuTrigger> |
| 119 | <ContextMenuContent> |
| 120 | <ContextMenuItem |
| 121 | onClick={() => onCategoryEdit?.(category)} |
| 122 | > |
| 123 | <Pencil className="h-4 w-4 mr-2" /> |
| 124 | Rename Category |
| 125 | </ContextMenuItem> |
| 126 | <ContextMenuItem |
| 127 | variant="destructive" |
| 128 | onClick={() => onCategoryDelete?.(category)} |
| 129 | > |
| 130 | <Trash2 className="h-4 w-4 mr-2" /> |
| 131 | Delete Category |
| 132 | </ContextMenuItem> |
| 133 | </ContextMenuContent> |
| 134 | </ContextMenu> |
| 135 | )} |
| 136 | <CollapsibleContent> |
| 137 | <SidebarMenuSub> |
| 138 | {feedsByCategory[category].map((feed) => ( |
| 139 | <SidebarMenuSubItem key={feed.id}> |
| 140 | <SidebarMenuSubButton |
| 141 | onClick={() => onFeedSelect(feed.id)} |
| 142 | isActive={selectedFeedId === feed.id} |
| 143 | > |
| 144 | <span>{feed.title || "Untitled"}</span> |
| 145 | </SidebarMenuSubButton> |
| 146 | </SidebarMenuSubItem> |
| 147 | ))} |
| 148 | </SidebarMenuSub> |
| 149 | </CollapsibleContent> |
| 150 | </SidebarMenuItem> |
| 151 | </Collapsible> |
| 152 | ); |
| 153 | })} |
| 154 | </SidebarMenu> |
| 155 | </SidebarGroup> |
| 156 | ); |
| 157 | } |