11import { useAtom , useAtomValue , useSetAtom } from "jotai" ;
2- import { CircleSmall , Edit2Icon , PlusIcon } from "lucide-react" ;
2+ import {
3+ AlertCircleIcon ,
4+ AlertTriangleIcon ,
5+ CircleSmall ,
6+ Edit2Icon ,
7+ MinusIcon ,
8+ PlusIcon ,
9+ } from "lucide-react" ;
310import { useCallback , useRef , useState } from "react" ;
411import { EditFeedDialog } from "~/components/AddFeedDialog" ;
512import { ButtonWithShortcut } from "~/components/ButtonWithShortcut" ;
@@ -23,7 +30,19 @@ import { doesFeedItemPassFilters } from "~/lib/data/feed-items";
2330import { useFeeds } from "~/lib/data/feeds" ;
2431import { useDeselectViewFilter } from "~/lib/data/views" ;
2532import { useDialogStore } from "./dialogStore" ;
26- import { useFeedItemsDict , useFeedItemsOrder } from "~/lib/data/store" ;
33+ import {
34+ useFeedItemsDict ,
35+ useFeedItemsOrder ,
36+ useFeedStatusDict ,
37+ useFetchFeedItemsStatus ,
38+ } from "~/lib/data/store" ;
39+ import {
40+ Tooltip ,
41+ TooltipContent ,
42+ TooltipTrigger ,
43+ } from "~/components/ui/tooltip" ;
44+ import { error } from "node:console" ;
45+ import { ApplicationFeed } from "~/server/db/schema" ;
2746
2847function useCheckFilteredFeedItemsForFeed ( ) {
2948 const feedItemsOrder = useFeedItemsOrder ( ) ;
@@ -91,6 +110,10 @@ function useDebouncedState(defaultValue: string, delay: number) {
91110 return [ searchQuery , setDebouncedQuery ] as const ;
92111}
93112
113+ function sortFeedOptions ( a : ApplicationFeed , b : ApplicationFeed ) {
114+ return a . name . localeCompare ( b . name ) ;
115+ }
116+
94117export function SidebarFeeds ( ) {
95118 const [ searchQuery , setSearchQuery ] = useDebouncedState ( "" , 300 ) ;
96119
@@ -107,52 +130,78 @@ export function SidebarFeeds() {
107130 const viewFilter = useAtomValue ( viewFilterAtom ) ;
108131 const deselectViewFilter = useDeselectViewFilter ( ) ;
109132
133+ const feedStatusDict = useFeedStatusDict ( ) ;
134+ const fetchFeedItemsStatus = useFetchFeedItemsStatus ( ) ;
135+
110136 const checkFilteredFeedItemsForFeed = useCheckFilteredFeedItemsForFeed ( ) ;
111137
112- const feedOptions = feeds ?. map ( ( category ) => ( {
113- ...category ,
114- hasEntries : ! ! checkFilteredFeedItemsForFeed ( category . id ) . length ,
138+ const feedOptions = feeds ?. map ( ( feed ) => ( {
139+ ...feed ,
140+ hasEntries : ! ! checkFilteredFeedItemsForFeed ( feed . id ) . length ,
115141 } ) ) ;
116142
117- const preferredFeedOptions = feedOptions
118- ?. filter ( ( feedOption ) => {
143+ const {
144+ preferredFeedOptions,
145+ feedOptionsWithContent,
146+ emptyFeedOptions,
147+ errorFeedOptions,
148+ } = feedOptions ?. reduce (
149+ ( acc , feedOption ) => {
150+ const {
151+ preferredFeedOptions,
152+ feedOptionsWithContent,
153+ emptyFeedOptions,
154+ errorFeedOptions,
155+ } = acc ;
119156 if ( ! ! searchQuery ) {
120157 const lowercaseQuery = searchQuery . toLowerCase ( ) ;
121158 const lowercaseName = feedOption . name . toLowerCase ( ) ;
122159
123160 if ( lowercaseName . includes ( lowercaseQuery ) ) {
124- return true ;
161+ preferredFeedOptions . push ( feedOption ) ;
162+ preferredFeedOptions . sort ( sortFeedOptions ) ;
163+ return acc ;
125164 }
126165 } else {
127- if ( feedOption . hasEntries ) return true ;
166+ if ( feedOption . hasEntries ) {
167+ preferredFeedOptions . push ( feedOption ) ;
168+ preferredFeedOptions . sort ( sortFeedOptions ) ;
169+ return acc ;
170+ }
128171 }
129172
130173 if ( feedOption . id === feedFilter ) {
131- return true ;
174+ preferredFeedOptions . push ( feedOption ) ;
175+ preferredFeedOptions . sort ( sortFeedOptions ) ;
176+ return acc ;
132177 }
133178
134- return false ;
135- } )
136- . toSorted ( ( a , b ) => {
137- if ( a . id === feedFilter ) {
138- return - 1 ;
139- }
140- if ( b . id === feedFilter ) {
141- return 1 ;
142- }
179+ const feedStatus = ! ! feedStatusDict [ feedOption . id ]
180+ ? feedStatusDict [ feedOption . id ]
181+ : fetchFeedItemsStatus === "fetching"
182+ ? "success"
183+ : "empty" ;
143184
144- return a . name . localeCompare ( b . name ) ;
145- } ) ;
185+ if ( feedStatus === "success" ) {
186+ feedOptionsWithContent . push ( feedOption ) ;
187+ feedOptionsWithContent . sort ( sortFeedOptions ) ;
188+ } else if ( feedStatus === "empty" ) {
189+ emptyFeedOptions . push ( feedOption ) ;
190+ emptyFeedOptions . sort ( sortFeedOptions ) ;
191+ } else if ( feedStatus === "error" ) {
192+ errorFeedOptions . push ( feedOption ) ;
193+ errorFeedOptions . sort ( sortFeedOptions ) ;
194+ }
146195
147- const otherFeedOptions = feedOptions
148- ?. filter ( ( feedOption ) => {
149- return ! preferredFeedOptions . some (
150- ( option ) => option . id === feedOption . id ,
151- ) ;
152- } )
153- . toSorted ( ( a , b ) => {
154- return a . name . localeCompare ( b . name ) ;
155- } ) ;
196+ return acc ;
197+ } ,
198+ {
199+ preferredFeedOptions : [ ] as typeof feedOptions ,
200+ feedOptionsWithContent : [ ] as typeof feedOptions ,
201+ emptyFeedOptions : [ ] as typeof feedOptions ,
202+ errorFeedOptions : [ ] as typeof feedOptions ,
203+ } ,
204+ ) ;
156205
157206 const hasAnyItems = ! ! checkFilteredFeedItemsForFeed ( - 1 ) . length ;
158207
@@ -207,6 +256,14 @@ export function SidebarFeeds() {
207256 </ SidebarMenuButton >
208257 </ SidebarMenuItem >
209258 { preferredFeedOptions . map ( ( feed ) => {
259+ const feedStatus = ! ! feedStatusDict [ feed . id ]
260+ ? feedStatusDict [ feed . id ]
261+ : fetchFeedItemsStatus === "fetching"
262+ ? "success"
263+ : "empty" ;
264+
265+ const isSuccess = feedStatus === "success" ;
266+
210267 return (
211268 < SidebarMenuItem key = { feed . id } className = "group flex gap-1" >
212269 < SidebarMenuButton
@@ -218,10 +275,35 @@ export function SidebarFeeds() {
218275 }
219276 } }
220277 >
221- { ! feed . hasEntries && (
278+ { feedStatus === "error" && (
279+ < Tooltip >
280+ < TooltipTrigger asChild >
281+ < AlertCircleIcon
282+ size = { 16 }
283+ className = "text-sidebar-accent"
284+ />
285+ </ TooltipTrigger >
286+ < TooltipContent className = "max-w-xs text-center" >
287+ Something went wrong fetching content for this feed. If
288+ this continues, try deleting this feed and adding it
289+ again with the correct URL.
290+ </ TooltipContent >
291+ </ Tooltip >
292+ ) }
293+ { feedStatus === "empty" && (
294+ < Tooltip >
295+ < TooltipTrigger asChild >
296+ < MinusIcon size = { 16 } className = "text-sidebar-accent" />
297+ </ TooltipTrigger >
298+ < TooltipContent >
299+ This feed has no new content within the last 30 days.
300+ </ TooltipContent >
301+ </ Tooltip >
302+ ) }
303+ { isSuccess && ! feed . hasEntries && (
222304 < CircleSmall size = { 16 } className = "text-sidebar-accent" />
223305 ) }
224- { feed . hasEntries && (
306+ { isSuccess && feed . hasEntries && (
225307 < div className = "grid size-4 place-items-center" >
226308 < div className = "bg-sidebar-accent size-2.5 rounded-full" />
227309 </ div >
@@ -238,10 +320,10 @@ export function SidebarFeeds() {
238320 </ SidebarMenuItem >
239321 ) ;
240322 } ) }
241- { ! ! preferredFeedOptions . length && ! ! otherFeedOptions . length && (
323+ { ! ! preferredFeedOptions . length && ! ! feedOptionsWithContent . length && (
242324 < hr className = "my-2 opacity-50" />
243325 ) }
244- { otherFeedOptions . map ( ( feed ) => {
326+ { feedOptionsWithContent . map ( ( feed ) => {
245327 return (
246328 < SidebarMenuItem key = { feed . id } className = "group flex gap-1" >
247329 < SidebarMenuButton
@@ -273,6 +355,82 @@ export function SidebarFeeds() {
273355 </ SidebarMenuItem >
274356 ) ;
275357 } ) }
358+ { ! ! feedOptionsWithContent . length && ! ! emptyFeedOptions . length && (
359+ < hr className = "my-2 opacity-50" />
360+ ) }
361+ { emptyFeedOptions . map ( ( feed ) => {
362+ return (
363+ < SidebarMenuItem key = { feed . id } className = "group flex gap-1" >
364+ < SidebarMenuButton
365+ variant = { feed . id === feedFilter ? "outline" : "default" }
366+ onClick = { ( ) => {
367+ setFeedFilter ( feed . id ) ;
368+ if ( ! feed . hasEntries ) {
369+ deselectViewFilter ( ) ;
370+ }
371+ } }
372+ >
373+ < Tooltip >
374+ < TooltipTrigger asChild >
375+ < MinusIcon size = { 16 } className = "text-sidebar-accent" />
376+ </ TooltipTrigger >
377+ < TooltipContent >
378+ This feed has no new content within the last 30 days.
379+ </ TooltipContent >
380+ </ Tooltip >
381+ < div className = "line-clamp-1" > { feed . name } </ div >
382+ </ SidebarMenuButton >
383+ < div className = "group/button flex w-fit items-center justify-end" >
384+ < SidebarMenuButton
385+ onClick = { ( ) => setSelectedFeedForEditing ( feed . id ) }
386+ >
387+ < Edit2Icon className = "opacity-30 transition-opacity group-hover/button:opacity-100" />
388+ </ SidebarMenuButton >
389+ </ div >
390+ </ SidebarMenuItem >
391+ ) ;
392+ } ) }
393+ { ! ! emptyFeedOptions . length && ! ! errorFeedOptions . length && (
394+ < hr className = "my-2 opacity-50" />
395+ ) }
396+ { errorFeedOptions . map ( ( feed ) => {
397+ return (
398+ < SidebarMenuItem key = { feed . id } className = "group flex gap-1" >
399+ < SidebarMenuButton
400+ variant = { feed . id === feedFilter ? "outline" : "default" }
401+ onClick = { ( ) => {
402+ setFeedFilter ( feed . id ) ;
403+ if ( ! feed . hasEntries ) {
404+ deselectViewFilter ( ) ;
405+ }
406+ } }
407+ >
408+ < Tooltip >
409+ < TooltipTrigger asChild >
410+ < AlertCircleIcon
411+ size = { 16 }
412+ className = "text-sidebar-accent"
413+ />
414+ </ TooltipTrigger >
415+ < TooltipContent className = "max-w-xs text-center" >
416+ Something went wrong fetching content for this feed. If
417+ this continues, try deleting this feed and adding it again
418+ with the correct URL.
419+ </ TooltipContent >
420+ </ Tooltip >
421+
422+ < div className = "line-clamp-1" > { feed . name } </ div >
423+ </ SidebarMenuButton >
424+ < div className = "group/button flex w-fit items-center justify-end" >
425+ < SidebarMenuButton
426+ onClick = { ( ) => setSelectedFeedForEditing ( feed . id ) }
427+ >
428+ < Edit2Icon className = "opacity-30 transition-opacity group-hover/button:opacity-100" />
429+ </ SidebarMenuButton >
430+ </ div >
431+ </ SidebarMenuItem >
432+ ) ;
433+ } ) }
276434 </ SidebarMenu >
277435 </ SidebarGroup >
278436 </ >
0 commit comments