1- import { Select , type SelectOption } from '~/components/Select'
1+ import { Check , ChevronsUpDown } from 'lucide-react'
2+ import { twMerge } from 'tailwind-merge'
3+ import {
4+ Dropdown ,
5+ DropdownContent ,
6+ DropdownItem ,
7+ DropdownSeparator ,
8+ DropdownTrigger ,
9+ } from '~/components/Dropdown'
210import { findMaintainerByAuthorName } from '~/utils/authors'
311import authorFallbackAvatar from '~/images/author-fallback.svg'
412
5- const ALL_AUTHORS_VALUE = 'all'
6-
713function getAuthorAvatar ( name : string ) : string {
814 return findMaintainerByAuthorName ( name ) ?. avatar ?? authorFallbackAvatar
915}
@@ -21,37 +27,107 @@ export function BlogAuthorFilter({
2127 onSelect,
2228 className,
2329} : BlogAuthorFilterProps ) {
24- const available : SelectOption [ ] = [
25- { label : 'All authors' , value : ALL_AUTHORS_VALUE } ,
26- ...authors . map ( ( name ) => ( {
27- label : name ,
28- value : name ,
29- logo : getAuthorAvatar ( name ) ,
30- } ) ) ,
31- ]
32-
33- // If the URL has an author that isn't in the post list, surface it anyway
34- // so the trigger renders and the user can still reset to "All authors".
35- if (
36- selected &&
37- selected !== ALL_AUTHORS_VALUE &&
38- ! authors . includes ( selected )
39- ) {
40- available . push ( {
41- label : selected ,
42- value : selected ,
43- logo : getAuthorAvatar ( selected ) ,
44- } )
45- }
30+ // Ignore URL values that don't correspond to a real author in the list.
31+ const activeAuthor =
32+ selected && authors . includes ( selected ) ? selected : undefined
4633
4734 return (
48- < Select
49- className = { className }
50- selected = { selected ?? ALL_AUTHORS_VALUE }
51- available = { available }
52- onSelect = { ( option ) =>
53- onSelect ( option . value === ALL_AUTHORS_VALUE ? undefined : option . value )
54- }
55- />
35+ < div className = { twMerge ( 'w-full' , className ) } >
36+ < Dropdown >
37+ < DropdownTrigger >
38+ < button
39+ type = "button"
40+ className = { twMerge (
41+ 'relative w-full flex items-center gap-2 rounded-md py-1.5 px-2 text-left text-sm cursor-pointer transition-colors' ,
42+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500' ,
43+ activeAuthor
44+ ? 'bg-blue-50 dark:bg-blue-950/40 ring-1 ring-blue-500/50 text-blue-700 dark:text-blue-200'
45+ : 'hover:bg-gray-500/10' ,
46+ ) }
47+ >
48+ < span
49+ className = { twMerge (
50+ 'flex items-center justify-center w-6 h-6 rounded border overflow-hidden flex-shrink-0' ,
51+ activeAuthor ? 'border-blue-500/30' : 'border-gray-500/20' ,
52+ ) }
53+ >
54+ < img
55+ height = { 20 }
56+ width = { 20 }
57+ src = {
58+ activeAuthor
59+ ? getAuthorAvatar ( activeAuthor )
60+ : authorFallbackAvatar
61+ }
62+ alt = ""
63+ className = "w-full h-full object-cover"
64+ />
65+ </ span >
66+ < span className = "truncate font-medium flex-1" >
67+ { activeAuthor ?? 'All authors' }
68+ </ span >
69+ < span className = "flex items-center pr-1" >
70+ < ChevronsUpDown
71+ className = "h-4 w-4 opacity-40"
72+ aria-hidden = "true"
73+ />
74+ </ span >
75+ </ button >
76+ </ DropdownTrigger >
77+ < DropdownContent
78+ align = "start"
79+ className = "max-h-80 overflow-auto min-w-[16rem]"
80+ >
81+ < DropdownItem
82+ onSelect = { ( ) => onSelect ( undefined ) }
83+ className = { twMerge (
84+ 'pr-8 relative' ,
85+ ! activeAuthor
86+ ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-200 font-medium'
87+ : 'font-normal' ,
88+ ) }
89+ >
90+ < span className = "truncate" > All authors</ span >
91+ { ! activeAuthor ? (
92+ < Check
93+ className = "h-4 w-4 absolute right-2 text-blue-600 dark:text-blue-300"
94+ aria-hidden = "true"
95+ />
96+ ) : null }
97+ </ DropdownItem >
98+ { authors . length > 0 ? < DropdownSeparator /> : null }
99+ { authors . map ( ( name ) => {
100+ const isSelected = activeAuthor === name
101+ return (
102+ < DropdownItem
103+ key = { name }
104+ onSelect = { ( ) => onSelect ( name ) }
105+ className = { twMerge (
106+ 'pr-8 pl-2 relative' ,
107+ isSelected
108+ ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-200 font-medium'
109+ : 'font-normal' ,
110+ ) }
111+ >
112+ < img
113+ height = { 20 }
114+ width = { 20 }
115+ src = { getAuthorAvatar ( name ) }
116+ alt = ""
117+ className = "w-5 h-5 rounded object-cover flex-shrink-0"
118+ />
119+ < span className = "truncate" > { name } </ span >
120+ { isSelected ? (
121+ < Check
122+ className = "h-4 w-4 absolute right-2 text-blue-600 dark:text-blue-300"
123+ aria-hidden = "true"
124+ />
125+ ) : null }
126+ </ DropdownItem >
127+ )
128+ } ) }
129+ </ DropdownContent >
130+ </ Dropdown >
131+ </ div >
56132 )
57133}
0 commit comments