Skip to content

Commit c6762a0

Browse files
JacobCoffeeclaude
andauthored
fix(schedule): collapse clear-filter X into corner-dot on funnel (#283)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c2939f3 commit c6762a0

9 files changed

Lines changed: 132 additions & 61 deletions

File tree

‎src/app/pages/schedule-filter/schedule-filter.html‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<ion-header translucent="true">
22
<ion-toolbar>
33
<ion-buttons slot="start">
4-
<ion-button *ngIf="ios" (click)="dismiss()">Cancel</ion-button>
5-
<ion-button *ngIf="!ios" (click)="selectAll(true)">Reset</ion-button>
4+
<ion-button (click)="resetToDefault()">Reset</ion-button>
65
</ion-buttons>
76

87
<ion-title>

‎src/app/pages/schedule-filter/schedule-filter.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Config, ModalController } from '@ionic/angular';
33

44
import { ConferenceData } from '../../providers/conference-data';
55
import { LiveUpdateService } from '../../providers/live-update.service';
6-
import { UserData } from '../../providers/user-data';
6+
import { DEFAULT_EXCLUDED_TRACKS, UserData } from '../../providers/user-data';
77

88

99
@Component({
@@ -48,6 +48,13 @@ export class ScheduleFilterPage {
4848
});
4949
}
5050

51+
resetToDefault() {
52+
const defaults = new Set(DEFAULT_EXCLUDED_TRACKS);
53+
this.tracks.forEach(track => {
54+
track.isChecked = !defaults.has(track.name);
55+
});
56+
}
57+
5158
applyFilters() {
5259
// Pass back a new array of track names to exclude
5360
const excludedTrackNames = this.tracks.filter(c => !c.isChecked).map(c => c.name);

‎src/app/pages/schedule-list/schedule-list.page.html‎

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,40 @@ <h1>{{trackName | trackName : 'plural'}}</h1>
2929
</ion-button>
3030
</div>
3131

32-
<!-- Regular sessions display -->
33-
<ion-list *ngIf="!isOpenSpaceView">
34-
<ion-item *ngFor="let session of displaySessions" [hidden]="session.hide" detail="true" routerLink="/app/tabs/schedule/session/{{session.id}}" [attr.track]="session.track | lowercase" class="session-list-item">
35-
<ion-label>
36-
<h2 class="session-name">
37-
<ion-icon *ngIf="session.favorite" name="star" color="warning" style="font-size: 0.85em; margin-right: 4px;"></ion-icon>
38-
{{session.name}}
39-
</h2>
40-
<div class="session-badges">
41-
<span class="track-badge" [attr.data-track]="session.track | lowercase">{{session.track | trackName}}</span>
42-
<span *ngIf="session?.isSpanish" class="track-badge spanish-badge">En Español</span>
43-
<span *ngIf="session?.preRegistered" class="track-badge prereg-badge">Pre-registration required</span>
44-
</div>
45-
<p class="session-meta">
46-
<span class="meta-item"><ion-icon name="time-outline"></ion-icon> {{session.day}} {{session.timeStart}} – {{session.timeEnd}}</span>
47-
<span *ngIf="session.displayLocation || session.location" class="meta-item"><ion-icon name="location-outline"></ion-icon> {{session.displayLocation || session.location}}</span>
48-
</p>
49-
<div *ngIf="session.speakers?.length" class="speaker-avatars">
50-
<ion-avatar *ngFor="let speaker of session.speakers" class="speaker-avatar-small">
51-
<img [src]="speaker.profilePic" [alt]="speaker.name">
52-
</ion-avatar>
53-
<span class="speaker-names">{{session.speakerNames.join(', ')}}</span>
54-
</div>
55-
</ion-label>
56-
</ion-item>
57-
</ion-list>
32+
<!-- Regular sessions display, grouped by conference day -->
33+
<div *ngIf="!isOpenSpaceView">
34+
<div *ngFor="let day of visibleDays">
35+
<ion-item-divider *ngIf="sessionsByDay[day]?.length > 0" class="day-header">
36+
<ion-label>{{ dayLabel(day) }}</ion-label>
37+
</ion-item-divider>
38+
39+
<ion-list *ngIf="sessionsByDay[day]?.length > 0" lines="full">
40+
<ion-item *ngFor="let session of sessionsByDay[day]" [hidden]="session.hide" detail="true" routerLink="/app/tabs/schedule/session/{{session.id}}" [attr.track]="session.track | lowercase" class="session-list-item">
41+
<ion-label>
42+
<h2 class="session-name">
43+
<ion-icon *ngIf="session.favorite" name="star" color="warning" style="font-size: 0.85em; margin-right: 4px;"></ion-icon>
44+
{{session.name}}
45+
</h2>
46+
<div class="session-badges">
47+
<span class="track-badge" [attr.data-track]="session.track | lowercase">{{session.track | trackName}}</span>
48+
<span *ngIf="session?.isSpanish" class="track-badge spanish-badge">En Español</span>
49+
<span *ngIf="session?.preRegistered" class="track-badge prereg-badge">Pre-registration required</span>
50+
</div>
51+
<p class="session-meta">
52+
<span class="meta-item"><ion-icon name="time-outline"></ion-icon> {{session.timeStart}} – {{session.timeEnd}}</span>
53+
<span *ngIf="session.displayLocation || session.location" class="meta-item"><ion-icon name="location-outline"></ion-icon> {{session.displayLocation || session.location}}</span>
54+
</p>
55+
<div *ngIf="session.speakers?.length" class="speaker-avatars">
56+
<ion-avatar *ngFor="let speaker of session.speakers" class="speaker-avatar-small">
57+
<img [src]="speaker.profilePic" [alt]="speaker.name">
58+
</ion-avatar>
59+
<span class="speaker-names">{{session.speakerNames.join(', ')}}</span>
60+
</div>
61+
</ion-label>
62+
</ion-item>
63+
</ion-list>
64+
</div>
65+
</div>
5866

5967
<div *ngIf="isOpenSpaceView" class="ion-padding-horizontal">
6068
<p>

‎src/app/pages/schedule-list/schedule-list.page.ts‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,22 @@ export class ScheduleListPage implements OnInit {
3232
sessions: any[] = [];
3333
displaySessions: any[] = [];
3434
sessionsByDay: any = {};
35-
dayOrder = ['Fri', 'Sat', 'Sun'];
35+
dayOrder = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
36+
// Days that actually have sessions in the current view, in canonical order.
37+
// Drives the day-separator headers on the track list.
38+
visibleDays: string[] = [];
3639
sessionQueryText = '';
3740
isOpenSpaceView = false;
3841

42+
private readonly dayLongLabels: Record<string, string> = {
43+
Mon: 'Monday', Tue: 'Tuesday', Wed: 'Wednesday', Thu: 'Thursday',
44+
Fri: 'Friday', Sat: 'Saturday', Sun: 'Sunday',
45+
};
46+
47+
dayLabel(day: string): string {
48+
return this.dayLongLabels[day] || day;
49+
}
50+
3951
loggedIn: boolean = false;
4052
ios: boolean;
4153
showSearchbar: boolean;
@@ -91,16 +103,30 @@ export class ScheduleListPage implements OnInit {
91103
}
92104

93105
organizeSessionsByDay() {
94-
if (!this.isOpenSpaceView) return;
95-
96106
this.sessionsByDay = {};
97107
this.dayOrder.forEach(day => this.sessionsByDay[day] = []);
98108

99109
this.displaySessions.forEach(session => {
100-
if (this.sessionsByDay[session.day]) {
101-
this.sessionsByDay[session.day].push(session);
110+
const bucket = this.sessionsByDay[session.day];
111+
if (bucket) {
112+
bucket.push(session);
113+
} else {
114+
// Unknown day code — keep the session findable rather than silently dropping it.
115+
this.sessionsByDay[session.day] = [session];
102116
}
103117
});
118+
119+
this.visibleDays = Object.keys(this.sessionsByDay).filter(
120+
day => this.sessionsByDay[day]?.length > 0,
121+
);
122+
this.visibleDays.sort((a, b) => {
123+
const ai = this.dayOrder.indexOf(a);
124+
const bi = this.dayOrder.indexOf(b);
125+
if (ai === -1 && bi === -1) return a.localeCompare(b);
126+
if (ai === -1) return 1;
127+
if (bi === -1) return -1;
128+
return ai - bi;
129+
});
104130
}
105131

106132
async generateSessions() {

‎src/app/pages/schedule/schedule.html‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
</ion-segment-button>
1010
</ion-segment>
1111
<ion-buttons slot="end">
12-
<ion-button (click)="toggleFavorites()" class="header-action">
12+
<ion-button (click)="toggleFavorites()" class="header-action" [attr.aria-label]="segment === 'favorites' ? 'Show all sessions' : 'Show favorites'">
1313
<ion-icon slot="icon-only" [name]="segment === 'favorites' ? 'star' : 'star-outline'"></ion-icon>
1414
</ion-button>
15-
<ion-button *ngIf="excludeTracks.length" (click)="clearFilters()" class="header-action filters-active" aria-label="Clear filters">
16-
<ion-icon slot="icon-only" name="close-circle"></ion-icon>
17-
</ion-button>
18-
<ion-button (click)="presentFilter()" class="header-action" [class.filters-active]="excludeTracks.length">
19-
<ion-icon slot="icon-only" [name]="excludeTracks.length ? 'funnel' : 'funnel-outline'"></ion-icon>
15+
<ion-button (click)="presentFilter()" class="header-action filter-button"
16+
[class.filters-active]="excludeTracks.length"
17+
[class.filters-custom]="hasCustomFilters"
18+
[attr.aria-label]="hasCustomFilters ? 'Filter sessions (custom filters active)' : (excludeTracks.length ? 'Filter sessions (some tracks hidden)' : 'Filter sessions')">
19+
<ion-icon slot="icon-only" name="funnel-outline"></ion-icon>
2020
</ion-button>
2121
</ion-buttons>
2222
</ion-toolbar>

‎src/app/pages/schedule/schedule.scss‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,30 @@ $tracks: (
7777
&:active {
7878
--color: #680579;
7979
}
80+
}
81+
82+
.filter-button {
83+
position: relative;
84+
85+
&.filters-active::after {
86+
content: '';
87+
position: absolute;
88+
top: 8px;
89+
right: 8px;
90+
width: 7px;
91+
height: 7px;
92+
border-radius: 50%;
93+
background: color-mix(in srgb, var(--pycon-accent, #680579) 55%, transparent);
94+
box-shadow: 0 0 0 2px var(--ion-toolbar-background, var(--ion-background-color, #fff));
95+
pointer-events: none;
96+
}
97+
98+
&.filters-custom {
99+
--color: var(--pycon-accent, #680579);
100+
}
80101

81-
&.filters-active {
82-
--color: #DD04D2;
102+
&.filters-custom::after {
103+
background: var(--pycon-accent, #680579);
83104
}
84105
}
85106

‎src/app/pages/schedule/schedule.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Subscription } from 'rxjs';
55

66
import { ScheduleFilterPage } from '../schedule-filter/schedule-filter';
77
import { ConferenceData } from '../../providers/conference-data';
8-
import { UserData } from '../../providers/user-data';
8+
import { UserData, isCustomScheduleFilter } from '../../providers/user-data';
99
import { LiveUpdateService } from '../../providers/live-update.service';
1010
import { environment } from '../../../environments/environment';
1111

@@ -92,6 +92,12 @@ export class SchedulePage implements OnInit, OnDestroy {
9292
this.updateSchedule();
9393
}
9494

95+
// True when the user has changed filters from the app default
96+
// (default = ['Open Space']). Drives the brighter "customized" dot tier.
97+
get hasCustomFilters(): boolean {
98+
return isCustomScheduleFilter(this.excludeTracks || []);
99+
}
100+
95101
ionViewDidEnter() {
96102
this.changeDetectorRef.detectChanges();
97103
this.jumpBtnCollapsed = false;
@@ -245,12 +251,6 @@ export class SchedulePage implements OnInit, OnDestroy {
245251
}, 500); // ms delay
246252
}
247253

248-
clearFilters() {
249-
this.excludeTracks = [];
250-
this.user.setScheduleFilters([]);
251-
this.updateSchedule();
252-
}
253-
254254
async presentFilter() {
255255
const modal = await this.modalCtrl.create({
256256
component: ScheduleFilterPage,

‎src/app/providers/conference-data.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -787,13 +787,14 @@ export class ConferenceData {
787787
}
788788
});
789789

790-
if (sessions.length > 0 && sessions[0].track === "Open Space") {
791-
const dayPriority = { "Fri": 0, "Sat": 1, "Sun": 2 };
792-
sessions.sort((a, b) => {
793-
const dayDiff = (dayPriority[a.day] || 0) - (dayPriority[b.day] || 0);
794-
return dayDiff || this.parseTime(a.timeStart) - this.parseTime(b.timeStart);
795-
});
796-
}
790+
sessions.sort((a, b) => {
791+
const aTime = a.startUtc ? new Date(a.startUtc).getTime() : 0;
792+
const bTime = b.startUtc ? new Date(b.startUtc).getTime() : 0;
793+
if (aTime && bTime && aTime !== bTime) return aTime - bTime;
794+
// Fallback for sessions missing startUtc (defensive — shouldn't happen
795+
// for normal track sessions, but keep behavior stable).
796+
return this.parseTime(a.timeStart || '') - this.parseTime(b.timeStart || '');
797+
});
797798

798799
return sessions;
799800
})

‎src/app/providers/user-data.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import { Subject } from 'rxjs';
44

55
import { PyConAPI } from '../providers/pycon-api';
66

7+
// Default schedule filter: Open Spaces are conference-day-only and clutter
8+
// the timeline before then, so first-time users see them excluded.
9+
export const DEFAULT_EXCLUDED_TRACKS: ReadonlyArray<string> = ['Open Space'];
10+
11+
export function isCustomScheduleFilter(excluded: ReadonlyArray<string>): boolean {
12+
if (excluded.length !== DEFAULT_EXCLUDED_TRACKS.length) return true;
13+
const defaults = new Set(DEFAULT_EXCLUDED_TRACKS);
14+
return !excluded.every(track => defaults.has(track));
15+
}
16+
717

818
@Injectable({
919
providedIn: 'root'
@@ -279,12 +289,11 @@ export class UserData {
279289

280290
getScheduleFilters(): Promise<Array<string>> {
281291
return this.storage.get('scheduleFilters').then((value) => {
282-
// First-time users default to hiding Open Spaces from the schedule
283-
// (they're conference-day-only and clutter the timeline before then).
284-
// Anyone who has explicitly toggled the filter — even to clear all
285-
// exclusions, which is stored as `[]` — keeps their preference.
292+
// First-time users get the app default. Anyone who has explicitly
293+
// toggled the filter — even to clear all exclusions, stored as `[]` —
294+
// keeps their preference.
286295
if (value === null || value === undefined) {
287-
return ['Open Space'];
296+
return [...DEFAULT_EXCLUDED_TRACKS];
288297
}
289298
return value;
290299
});

0 commit comments

Comments
 (0)