From d42d21827956631883b0ec451aba90365964d9df Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Tue, 28 Apr 2026 11:40:26 -0500 Subject: [PATCH 1/2] fix(schedule): collapse clear-filter X into corner-dot on funnel The Schedule header reserved ~132px on the right (star + clear-X + funnel) which squeezed day tabs (Wed/Thu/Fri/Sat/Sun) on small screens. The standalone clear-filter X was effectively permanent because Open Spaces is excluded by default, so it was pure horizontal tax. Drop the standalone X. The funnel keeps its solid+accent treatment when anything is excluded (so users still see Open Spaces is hidden), and gains a small corner dot indicator with two tiers: - Faint dot when only the app default is excluded (Open Spaces) - Solid accent dot + accent icon color when the user has customized Clearing now lives inside the filter sheet as a "Reset" button (wired to a new resetToDefault action that restores the app default rather than the misleading "show all" behavior the old Material Reset had). Extract DEFAULT_EXCLUDED_TRACKS / isCustomScheduleFilter into user-data.ts so the default isn't a magic string in three places. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schedule-filter/schedule-filter.html | 3 +-- .../pages/schedule-filter/schedule-filter.ts | 9 ++++++- src/app/pages/schedule/schedule.html | 12 ++++----- src/app/pages/schedule/schedule.scss | 25 +++++++++++++++++-- src/app/pages/schedule/schedule.ts | 14 +++++------ src/app/providers/user-data.ts | 19 ++++++++++---- 6 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/app/pages/schedule-filter/schedule-filter.html b/src/app/pages/schedule-filter/schedule-filter.html index da2686f6..0828c3f3 100644 --- a/src/app/pages/schedule-filter/schedule-filter.html +++ b/src/app/pages/schedule-filter/schedule-filter.html @@ -1,8 +1,7 @@ - Cancel - Reset + Reset diff --git a/src/app/pages/schedule-filter/schedule-filter.ts b/src/app/pages/schedule-filter/schedule-filter.ts index c7fbf4bc..40fb8e56 100644 --- a/src/app/pages/schedule-filter/schedule-filter.ts +++ b/src/app/pages/schedule-filter/schedule-filter.ts @@ -3,7 +3,7 @@ import { Config, ModalController } from '@ionic/angular'; import { ConferenceData } from '../../providers/conference-data'; import { LiveUpdateService } from '../../providers/live-update.service'; -import { UserData } from '../../providers/user-data'; +import { DEFAULT_EXCLUDED_TRACKS, UserData } from '../../providers/user-data'; @Component({ @@ -48,6 +48,13 @@ export class ScheduleFilterPage { }); } + resetToDefault() { + const defaults = new Set(DEFAULT_EXCLUDED_TRACKS); + this.tracks.forEach(track => { + track.isChecked = !defaults.has(track.name); + }); + } + applyFilters() { // Pass back a new array of track names to exclude const excludedTrackNames = this.tracks.filter(c => !c.isChecked).map(c => c.name); diff --git a/src/app/pages/schedule/schedule.html b/src/app/pages/schedule/schedule.html index dfb1f6a6..94653e29 100644 --- a/src/app/pages/schedule/schedule.html +++ b/src/app/pages/schedule/schedule.html @@ -9,14 +9,14 @@ - + - - - - - + + diff --git a/src/app/pages/schedule/schedule.scss b/src/app/pages/schedule/schedule.scss index 8a8afe1d..eeb7c30c 100644 --- a/src/app/pages/schedule/schedule.scss +++ b/src/app/pages/schedule/schedule.scss @@ -77,9 +77,30 @@ $tracks: ( &:active { --color: #680579; } +} + +.filter-button { + position: relative; + + &.filters-active::after { + content: ''; + position: absolute; + top: 8px; + right: 8px; + width: 7px; + height: 7px; + border-radius: 50%; + background: color-mix(in srgb, var(--pycon-accent, #680579) 55%, transparent); + box-shadow: 0 0 0 2px var(--ion-toolbar-background, var(--ion-background-color, #fff)); + pointer-events: none; + } + + &.filters-custom { + --color: var(--pycon-accent, #680579); + } - &.filters-active { - --color: #DD04D2; + &.filters-custom::after { + background: var(--pycon-accent, #680579); } } diff --git a/src/app/pages/schedule/schedule.ts b/src/app/pages/schedule/schedule.ts index c73b0890..a516b78c 100644 --- a/src/app/pages/schedule/schedule.ts +++ b/src/app/pages/schedule/schedule.ts @@ -5,7 +5,7 @@ import { Subscription } from 'rxjs'; import { ScheduleFilterPage } from '../schedule-filter/schedule-filter'; import { ConferenceData } from '../../providers/conference-data'; -import { UserData } from '../../providers/user-data'; +import { UserData, isCustomScheduleFilter } from '../../providers/user-data'; import { LiveUpdateService } from '../../providers/live-update.service'; import { environment } from '../../../environments/environment'; @@ -92,6 +92,12 @@ export class SchedulePage implements OnInit, OnDestroy { this.updateSchedule(); } + // True when the user has changed filters from the app default + // (default = ['Open Space']). Drives the brighter "customized" dot tier. + get hasCustomFilters(): boolean { + return isCustomScheduleFilter(this.excludeTracks || []); + } + ionViewDidEnter() { this.changeDetectorRef.detectChanges(); this.jumpBtnCollapsed = false; @@ -245,12 +251,6 @@ export class SchedulePage implements OnInit, OnDestroy { }, 500); // ms delay } - clearFilters() { - this.excludeTracks = []; - this.user.setScheduleFilters([]); - this.updateSchedule(); - } - async presentFilter() { const modal = await this.modalCtrl.create({ component: ScheduleFilterPage, diff --git a/src/app/providers/user-data.ts b/src/app/providers/user-data.ts index e98a2b56..da8bd5b2 100644 --- a/src/app/providers/user-data.ts +++ b/src/app/providers/user-data.ts @@ -4,6 +4,16 @@ import { Subject } from 'rxjs'; import { PyConAPI } from '../providers/pycon-api'; +// Default schedule filter: Open Spaces are conference-day-only and clutter +// the timeline before then, so first-time users see them excluded. +export const DEFAULT_EXCLUDED_TRACKS: ReadonlyArray = ['Open Space']; + +export function isCustomScheduleFilter(excluded: ReadonlyArray): boolean { + if (excluded.length !== DEFAULT_EXCLUDED_TRACKS.length) return true; + const defaults = new Set(DEFAULT_EXCLUDED_TRACKS); + return !excluded.every(track => defaults.has(track)); +} + @Injectable({ providedIn: 'root' @@ -279,12 +289,11 @@ export class UserData { getScheduleFilters(): Promise> { return this.storage.get('scheduleFilters').then((value) => { - // First-time users default to hiding Open Spaces from the schedule - // (they're conference-day-only and clutter the timeline before then). - // Anyone who has explicitly toggled the filter — even to clear all - // exclusions, which is stored as `[]` — keeps their preference. + // First-time users get the app default. Anyone who has explicitly + // toggled the filter — even to clear all exclusions, stored as `[]` — + // keeps their preference. if (value === null || value === undefined) { - return ['Open Space']; + return [...DEFAULT_EXCLUDED_TRACKS]; } return value; }); From 925b1c5947f71f93ccde0df48cf52b9ad5f74cdf Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Tue, 28 Apr 2026 11:40:37 -0500 Subject: [PATCH 2/2] fix(tracks): chronologically sort sessions and group by day MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track / presentation pages (Keynotes, Talks, Tutorials, etc.) rendered sessions in source-data order — Keynotes appeared as Sun/Sat/Fri/Sun on screen. The chronological sort in getSessions() was gated on sessions[0].track === "Open Space", so every other track came back unsorted. - getSessions() now always sorts by startUtc (ISO timestamp on every session), with a parseTime fallback for defensiveness. - The track list (schedule-list page) groups regular sessions by day with the same .day-header dividers the Open Spaces view already uses, mirroring the Room detail page pattern. dayOrder widened from [Fri/Sat/Sun] to Mon..Sun to cover tutorial / sprints days. - Removed the redundant "{{session.day}}" prefix from the per-row meta line — the day is now established by the section header above. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schedule-list/schedule-list.page.html | 60 +++++++++++-------- .../pages/schedule-list/schedule-list.page.ts | 36 +++++++++-- src/app/providers/conference-data.ts | 15 ++--- 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/app/pages/schedule-list/schedule-list.page.html b/src/app/pages/schedule-list/schedule-list.page.html index 9ad2ec9e..6be5096f 100644 --- a/src/app/pages/schedule-list/schedule-list.page.html +++ b/src/app/pages/schedule-list/schedule-list.page.html @@ -29,32 +29,40 @@

{{trackName | trackName : 'plural'}}

- - - - -

- - {{session.name}} -

-
- {{session.track | trackName}} - En Español - Pre-registration required -
-

- {{session.day}} {{session.timeStart}} – {{session.timeEnd}} - {{session.displayLocation || session.location}} -

-
- - - - {{session.speakerNames.join(', ')}} -
-
-
-
+ +
+
+ + {{ dayLabel(day) }} + + + + + +

+ + {{session.name}} +

+
+ {{session.track | trackName}} + En Español + Pre-registration required +
+

+ {{session.timeStart}} – {{session.timeEnd}} + {{session.displayLocation || session.location}} +

+
+ + + + {{session.speakerNames.join(', ')}} +
+
+
+
+
+

diff --git a/src/app/pages/schedule-list/schedule-list.page.ts b/src/app/pages/schedule-list/schedule-list.page.ts index 0aaf0398..5254ce10 100644 --- a/src/app/pages/schedule-list/schedule-list.page.ts +++ b/src/app/pages/schedule-list/schedule-list.page.ts @@ -32,10 +32,22 @@ export class ScheduleListPage implements OnInit { sessions: any[] = []; displaySessions: any[] = []; sessionsByDay: any = {}; - dayOrder = ['Fri', 'Sat', 'Sun']; + dayOrder = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + // Days that actually have sessions in the current view, in canonical order. + // Drives the day-separator headers on the track list. + visibleDays: string[] = []; sessionQueryText = ''; isOpenSpaceView = false; + private readonly dayLongLabels: Record = { + Mon: 'Monday', Tue: 'Tuesday', Wed: 'Wednesday', Thu: 'Thursday', + Fri: 'Friday', Sat: 'Saturday', Sun: 'Sunday', + }; + + dayLabel(day: string): string { + return this.dayLongLabels[day] || day; + } + loggedIn: boolean = false; ios: boolean; showSearchbar: boolean; @@ -91,16 +103,30 @@ export class ScheduleListPage implements OnInit { } organizeSessionsByDay() { - if (!this.isOpenSpaceView) return; - this.sessionsByDay = {}; this.dayOrder.forEach(day => this.sessionsByDay[day] = []); this.displaySessions.forEach(session => { - if (this.sessionsByDay[session.day]) { - this.sessionsByDay[session.day].push(session); + const bucket = this.sessionsByDay[session.day]; + if (bucket) { + bucket.push(session); + } else { + // Unknown day code — keep the session findable rather than silently dropping it. + this.sessionsByDay[session.day] = [session]; } }); + + this.visibleDays = Object.keys(this.sessionsByDay).filter( + day => this.sessionsByDay[day]?.length > 0, + ); + this.visibleDays.sort((a, b) => { + const ai = this.dayOrder.indexOf(a); + const bi = this.dayOrder.indexOf(b); + if (ai === -1 && bi === -1) return a.localeCompare(b); + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }); } async generateSessions() { diff --git a/src/app/providers/conference-data.ts b/src/app/providers/conference-data.ts index 7be7af80..4c58ad0f 100644 --- a/src/app/providers/conference-data.ts +++ b/src/app/providers/conference-data.ts @@ -787,13 +787,14 @@ export class ConferenceData { } }); - if (sessions.length > 0 && sessions[0].track === "Open Space") { - const dayPriority = { "Fri": 0, "Sat": 1, "Sun": 2 }; - sessions.sort((a, b) => { - const dayDiff = (dayPriority[a.day] || 0) - (dayPriority[b.day] || 0); - return dayDiff || this.parseTime(a.timeStart) - this.parseTime(b.timeStart); - }); - } + sessions.sort((a, b) => { + const aTime = a.startUtc ? new Date(a.startUtc).getTime() : 0; + const bTime = b.startUtc ? new Date(b.startUtc).getTime() : 0; + if (aTime && bTime && aTime !== bTime) return aTime - bTime; + // Fallback for sessions missing startUtc (defensive — shouldn't happen + // for normal track sessions, but keep behavior stable). + return this.parseTime(a.timeStart || '') - this.parseTime(b.timeStart || ''); + }); return sessions; })