@@ -3,29 +3,18 @@ import {
33 CalendarRange ,
44 ChevronLeft ,
55 ChevronRight ,
6- Filter ,
76 Search ,
87 X ,
98} from "lucide-react" ;
109
1110import type { EventSource } from "@/config/events" ;
1211import { Button } from "@atl/ui/components/button" ;
13- import { Checkbox } from "@atl/ui/components/checkbox" ;
1412import { Input } from "@atl/ui/components/input" ;
15- import {
16- Popover ,
17- PopoverContent ,
18- PopoverHeader ,
19- PopoverTitle ,
20- PopoverTrigger ,
21- } from "@atl/ui/components/popover" ;
2213import { cn } from "@atl/ui/lib/utils" ;
2314
2415import type { CalendarViewMode , ListRangePreset } from "./types" ;
2516
2617interface CalendarHeaderProps {
27- allCategories : string [ ] ;
28- categoryDenyList : Set < string > ;
2918 hasActiveFilters : boolean ;
3019 listRangePreset : ListRangePreset ;
3120 monthTitle : string ;
@@ -35,11 +24,12 @@ interface CalendarHeaderProps {
3524 onPrevMonth : ( ) => void ;
3625 onSearchChange : ( v : string ) => void ;
3726 onSetListRange : ( v : ListRangePreset ) => void ;
38- onToggleCategoryDeny : ( cat : string ) => void ;
27+ onToggleShowEnded : ( ) => void ;
3928 onToggleSource : ( id : string ) => void ;
4029 onViewChange : ( v : CalendarViewMode ) => void ;
4130 search : string ;
4231 selectedSourceIds : Set < string > ;
32+ showEnded : boolean ;
4333 sourcePills : EventSource [ ] ;
4434 viewMode : CalendarViewMode ;
4535}
@@ -53,6 +43,15 @@ function sourceTransportLabel(
5343 if ( src . kind === "discourse" ) {
5444 return "Discourse" ;
5545 }
46+ if ( src . kind === "fedocal" ) {
47+ return "Fedocal" ;
48+ }
49+ if ( src . kind === "lf-scrape" ) {
50+ return "Web" ;
51+ }
52+ if ( src . kind === "dev-events" ) {
53+ return "Web" ;
54+ }
5655 if ( src . kind === "rss" ) {
5756 return "RSS" ;
5857 }
@@ -80,8 +79,6 @@ const VIEW_OPTIONS: {
8079] ;
8180
8281export function CalendarHeader ( {
83- allCategories,
84- categoryDenyList,
8582 hasActiveFilters,
8683 listRangePreset,
8784 monthTitle,
@@ -91,85 +88,21 @@ export function CalendarHeader({
9188 onPrevMonth,
9289 onSearchChange,
9390 onSetListRange,
94- onToggleCategoryDeny ,
91+ onToggleShowEnded ,
9592 onToggleSource,
9693 onViewChange,
9794 search,
9895 selectedSourceIds,
96+ showEnded,
9997 sourcePills,
10098 viewMode,
10199} : CalendarHeaderProps ) {
100+ const isMonth = viewMode === "month" ;
101+
102102 return (
103103 < div className = "border-border/60 bg-muted/20 dark:border-border/40 dark:bg-muted/10 shrink-0 border-b" >
104- { /* Top bar: Today + nav + title + view switcher */ }
105- < div className = "flex flex-wrap items-center gap-2 px-3 py-2 sm:px-4" >
106- < Button
107- className = "h-8 shrink-0 text-xs"
108- onClick = { onGoToday }
109- size = "sm"
110- type = "button"
111- variant = "outline"
112- >
113- Today
114- </ Button >
115-
116- < div className = "flex items-center gap-0.5" >
117- < Button
118- aria-label = "Previous month"
119- className = "size-8"
120- onClick = { onPrevMonth }
121- size = "icon-sm"
122- type = "button"
123- variant = "ghost"
124- >
125- < ChevronLeft className = "size-4" />
126- </ Button >
127- < Button
128- aria-label = "Next month"
129- className = "size-8"
130- onClick = { onNextMonth }
131- size = "icon-sm"
132- type = "button"
133- variant = "ghost"
134- >
135- < ChevronRight className = "size-4" />
136- </ Button >
137- </ div >
138-
139- < p className = "min-w-32 flex-1 text-center text-sm font-semibold sm:min-w-0" >
140- { monthTitle }
141- </ p >
142-
143- { /* View switcher (button group) */ }
144- < div className = "ml-auto inline-flex" >
145- { VIEW_OPTIONS . map ( ( opt , i ) => {
146- const Icon = opt . icon ;
147- const isActive = viewMode === opt . value ;
148- return (
149- < Button
150- aria-label = { `View by ${ opt . label } ` }
151- className = { cn (
152- "gap-1.5 text-xs" ,
153- i === 0 && "rounded-r-none" ,
154- i === VIEW_OPTIONS . length - 1 && "-ml-px rounded-l-none" ,
155- i > 0 && i < VIEW_OPTIONS . length - 1 && "-ml-px rounded-none"
156- ) }
157- key = { opt . value }
158- onClick = { ( ) => onViewChange ( opt . value ) }
159- size = "sm"
160- type = "button"
161- variant = { isActive ? "default" : "outline" }
162- >
163- < Icon className = "size-3.5" />
164- < span className = "hidden min-[480px]:inline" > { opt . label } </ span >
165- </ Button >
166- ) ;
167- } ) }
168- </ div >
169- </ div >
170-
171104 { /* Search bar */ }
172- < div className = "border-border/60 dark:border-border/40 relative border-t " >
105+ < div className = "relative" >
173106 < Search className = "text-muted-foreground absolute top-1/2 left-4 size-4 -translate-y-1/2" />
174107 < Input
175108 className = "h-10 rounded-none border-0 bg-transparent pl-10 shadow-none focus-visible:ring-0"
@@ -179,7 +112,7 @@ export function CalendarHeader({
179112 />
180113 </ div >
181114
182- { /* Source pills + filters */ }
115+ { /* Source pills */ }
183116 < div className = "border-border/60 dark:border-border/40 flex flex-wrap items-center gap-1.5 border-t px-3 py-2 sm:px-4" >
184117 < span className = "text-muted-foreground mr-0.5 text-xs font-medium tracking-wider uppercase" >
185118 Sources
@@ -208,75 +141,6 @@ export function CalendarHeader({
208141 ) ;
209142 } ) }
210143
211- < Popover >
212- < PopoverTrigger
213- render = {
214- < Button
215- className = "ml-1 h-7 gap-1 text-xs"
216- size = "sm"
217- type = "button"
218- variant = "outline"
219- />
220- }
221- >
222- < Filter className = "size-3.5" />
223- Filters
224- { ( categoryDenyList . size > 0 ||
225- listRangePreset !== "forward-90d" ) && (
226- < span className = "bg-primary/15 text-primary rounded-full px-1.5 py-px text-[0.65rem]" >
227- { categoryDenyList . size +
228- ( listRangePreset === "forward-90d" ? 0 : 1 ) }
229- </ span >
230- ) }
231- </ PopoverTrigger >
232- < PopoverContent align = "end" className = "w-80" >
233- < PopoverHeader >
234- < PopoverTitle > Filters</ PopoverTitle >
235- </ PopoverHeader >
236- < div className = "space-y-3" >
237- < div className = "space-y-2" >
238- < p className = "text-muted-foreground text-xs font-medium" >
239- Agenda range
240- </ p >
241- < select
242- className = "border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 h-9 w-full rounded-md border px-2 text-sm shadow-xs outline-none focus-visible:ring-[3px]"
243- onChange = { ( e ) =>
244- onSetListRange ( e . target . value as ListRangePreset )
245- }
246- value = { listRangePreset }
247- >
248- < option value = "forward-90d" > Next 90 days</ option >
249- < option value = "visible-month" >
250- Calendar month ({ monthTitle } )
251- </ option >
252- < option value = "all" > All events</ option >
253- </ select >
254- </ div >
255- { allCategories . length > 0 && (
256- < div className = "space-y-2" >
257- < p className = "text-muted-foreground text-xs font-medium" >
258- Categories
259- </ p >
260- < div className = "max-h-40 space-y-2 overflow-y-auto pr-1" >
261- { allCategories . map ( ( cat ) => (
262- < label
263- className = "flex cursor-pointer items-center gap-2 text-sm"
264- key = { cat }
265- >
266- < Checkbox
267- checked = { ! categoryDenyList . has ( cat ) }
268- onCheckedChange = { ( ) => onToggleCategoryDeny ( cat ) }
269- />
270- < span className = "truncate" > { cat } </ span >
271- </ label >
272- ) ) }
273- </ div >
274- </ div >
275- ) }
276- </ div >
277- </ PopoverContent >
278- </ Popover >
279-
280144 { hasActiveFilters && (
281145 < button
282146 className = "text-muted-foreground hover:text-foreground ml-auto flex items-center gap-1 rounded-md px-2 py-1 text-xs"
@@ -288,6 +152,129 @@ export function CalendarHeader({
288152 </ button >
289153 ) }
290154 </ div >
155+
156+ { /* Top bar */ }
157+ < div className = "border-border/60 dark:border-border/40 flex flex-wrap items-center gap-2 border-t px-3 py-2 sm:px-4" >
158+ { isMonth && (
159+ < >
160+ < Button
161+ className = "h-8 shrink-0 text-xs"
162+ onClick = { onGoToday }
163+ size = "sm"
164+ type = "button"
165+ variant = "outline"
166+ >
167+ Today
168+ </ Button >
169+
170+ < div className = "flex items-center gap-0.5" >
171+ < Button
172+ aria-label = "Previous month"
173+ className = "size-8"
174+ onClick = { onPrevMonth }
175+ size = "icon-sm"
176+ type = "button"
177+ variant = "ghost"
178+ >
179+ < ChevronLeft className = "size-4" />
180+ </ Button >
181+ < Button
182+ aria-label = "Next month"
183+ className = "size-8"
184+ onClick = { onNextMonth }
185+ size = "icon-sm"
186+ type = "button"
187+ variant = "ghost"
188+ >
189+ < ChevronRight className = "size-4" />
190+ </ Button >
191+ </ div >
192+
193+ < p className = "min-w-32 flex-1 text-center text-sm font-semibold sm:min-w-0" >
194+ { monthTitle }
195+ </ p >
196+ </ >
197+ ) }
198+
199+ { ! isMonth && (
200+ < >
201+ < span className = "text-muted-foreground text-xs font-medium" >
202+ Range
203+ </ span >
204+ < div className = "inline-flex" >
205+ { (
206+ [
207+ { label : "7d" , value : "forward-7d" } ,
208+ { label : "30d" , value : "forward-30d" } ,
209+ { label : "90d" , value : "forward-90d" } ,
210+ { label : "6mo" , value : "forward-6mo" } ,
211+ { label : "1y" , value : "forward-1y" } ,
212+ { label : "All" , value : "all" } ,
213+ ] as const
214+ ) . map ( ( opt , i , arr ) => (
215+ < button
216+ className = { cn (
217+ "border px-2.5 py-1 text-xs font-medium transition-colors" ,
218+ i === 0 && "rounded-l-md" ,
219+ i === arr . length - 1 && "rounded-r-md" ,
220+ i > 0 && "-ml-px" ,
221+ listRangePreset === opt . value
222+ ? "border-primary bg-primary/10 text-primary dark:bg-primary/20"
223+ : "border-border/50 text-muted-foreground hover:border-border hover:text-foreground dark:border-border/40"
224+ ) }
225+ key = { opt . value }
226+ onClick = { ( ) => onSetListRange ( opt . value ) }
227+ type = "button"
228+ >
229+ { opt . label }
230+ </ button >
231+ ) ) }
232+ </ div >
233+ { listRangePreset === "all" && (
234+ < button
235+ className = { cn (
236+ "rounded-md border px-2.5 py-1 text-xs font-medium transition-colors" ,
237+ showEnded
238+ ? "border-primary bg-primary/10 text-primary dark:bg-primary/20"
239+ : "border-border/50 text-muted-foreground hover:border-border hover:text-foreground dark:border-border/40"
240+ ) }
241+ onClick = { onToggleShowEnded }
242+ type = "button"
243+ >
244+ Ended
245+ </ button >
246+ ) }
247+ < div className = "flex-1" />
248+ </ >
249+ ) }
250+
251+ { /* View switcher */ }
252+ < div className = "ml-auto inline-flex" >
253+ { VIEW_OPTIONS . map ( ( opt , i ) => {
254+ const Icon = opt . icon ;
255+ const isActive = viewMode === opt . value ;
256+ return (
257+ < Button
258+ aria-label = { `View by ${ opt . label } ` }
259+ className = { cn (
260+ "gap-1.5 text-xs" ,
261+ i === 0 && "rounded-r-none" ,
262+ i === VIEW_OPTIONS . length - 1 && "-ml-px rounded-l-none" ,
263+ i > 0 && i < VIEW_OPTIONS . length - 1 && "-ml-px rounded-none"
264+ ) }
265+ key = { opt . value }
266+ onClick = { ( ) => onViewChange ( opt . value ) }
267+ size = "sm"
268+ type = "button"
269+ variant = { isActive ? "default" : "outline" }
270+ >
271+ < Icon className = "size-3.5" />
272+ < span className = "hidden min-[480px]:inline" > { opt . label } </ span >
273+ </ Button >
274+ ) ;
275+ } ) }
276+ </ div >
277+ </ div >
291278 </ div >
292279 ) ;
293280}
0 commit comments