Skip to content

Commit 48c7908

Browse files
committed
feat(portal): add event sources, improve toolbar, fix lint-staged
- Add Fedora events via Fedocal JSON API (9 events) - Add GNOME events via Indico ICS (GUADEC etc.) - Add NixOS events via Discourse API - Add Linux Foundation events via HTML scraper (65 events) - Add dev.events/linux and dev.events/oss scrapers (~57 events) - Add OpenSSF events via ICS feed (5 events) - Disable openSUSE source (blocked by proof-of-work challenge) - Add virtual location inference from event descriptions - Replace range dropdown with quick toggle buttons (7d/30d/90d/6mo/1y/All) - Add Ended toggle for past events (visible when All selected) - Move search bar and sources above nav controls - Hide month nav in agenda view - Fix lint-staged to handle parentheses in Next.js route paths by falling back to oxfmt+oxlint directly for affected files
1 parent a400893 commit 48c7908

File tree

10 files changed

+922
-234
lines changed

10 files changed

+922
-234
lines changed

apps/portal/src/app/(dashboard)/app/events/components/calendar-header.tsx

Lines changed: 140 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,18 @@ import {
33
CalendarRange,
44
ChevronLeft,
55
ChevronRight,
6-
Filter,
76
Search,
87
X,
98
} from "lucide-react";
109

1110
import type { EventSource } from "@/config/events";
1211
import { Button } from "@atl/ui/components/button";
13-
import { Checkbox } from "@atl/ui/components/checkbox";
1412
import { Input } from "@atl/ui/components/input";
15-
import {
16-
Popover,
17-
PopoverContent,
18-
PopoverHeader,
19-
PopoverTitle,
20-
PopoverTrigger,
21-
} from "@atl/ui/components/popover";
2213
import { cn } from "@atl/ui/lib/utils";
2314

2415
import type { CalendarViewMode, ListRangePreset } from "./types";
2516

2617
interface 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

8281
export 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
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
export type CalendarViewMode = "month" | "agenda";
2-
export type ListRangePreset = "visible-month" | "forward-90d" | "all";
2+
export type ListRangePreset =
3+
| "forward-7d"
4+
| "forward-30d"
5+
| "forward-90d"
6+
| "forward-6mo"
7+
| "forward-1y"
8+
| "all";

0 commit comments

Comments
 (0)