Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/app/pages/schedule-filter/schedule-filter.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<ion-header translucent="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button *ngIf="ios" (click)="dismiss()">Cancel</ion-button>
<ion-button *ngIf="!ios" (click)="selectAll(true)">Reset</ion-button>
<ion-button (click)="resetToDefault()">Reset</ion-button>
</ion-buttons>

<ion-title>
Expand Down
9 changes: 8 additions & 1 deletion src/app/pages/schedule-filter/schedule-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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);
Expand Down
60 changes: 34 additions & 26 deletions src/app/pages/schedule-list/schedule-list.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,40 @@ <h1>{{trackName | trackName : 'plural'}}</h1>
</ion-button>
</div>

<!-- Regular sessions display -->
<ion-list *ngIf="!isOpenSpaceView">
<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">
<ion-label>
<h2 class="session-name">
<ion-icon *ngIf="session.favorite" name="star" color="warning" style="font-size: 0.85em; margin-right: 4px;"></ion-icon>
{{session.name}}
</h2>
<div class="session-badges">
<span class="track-badge" [attr.data-track]="session.track | lowercase">{{session.track | trackName}}</span>
<span *ngIf="session?.isSpanish" class="track-badge spanish-badge">En Español</span>
<span *ngIf="session?.preRegistered" class="track-badge prereg-badge">Pre-registration required</span>
</div>
<p class="session-meta">
<span class="meta-item"><ion-icon name="time-outline"></ion-icon> {{session.day}} {{session.timeStart}} – {{session.timeEnd}}</span>
<span *ngIf="session.displayLocation || session.location" class="meta-item"><ion-icon name="location-outline"></ion-icon> {{session.displayLocation || session.location}}</span>
</p>
<div *ngIf="session.speakers?.length" class="speaker-avatars">
<ion-avatar *ngFor="let speaker of session.speakers" class="speaker-avatar-small">
<img [src]="speaker.profilePic" [alt]="speaker.name">
</ion-avatar>
<span class="speaker-names">{{session.speakerNames.join(', ')}}</span>
</div>
</ion-label>
</ion-item>
</ion-list>
<!-- Regular sessions display, grouped by conference day -->
<div *ngIf="!isOpenSpaceView">
<div *ngFor="let day of visibleDays">
<ion-item-divider *ngIf="sessionsByDay[day]?.length > 0" class="day-header">
<ion-label>{{ dayLabel(day) }}</ion-label>
</ion-item-divider>

<ion-list *ngIf="sessionsByDay[day]?.length > 0" lines="full">
<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">
<ion-label>
<h2 class="session-name">
<ion-icon *ngIf="session.favorite" name="star" color="warning" style="font-size: 0.85em; margin-right: 4px;"></ion-icon>
{{session.name}}
</h2>
<div class="session-badges">
<span class="track-badge" [attr.data-track]="session.track | lowercase">{{session.track | trackName}}</span>
<span *ngIf="session?.isSpanish" class="track-badge spanish-badge">En Español</span>
<span *ngIf="session?.preRegistered" class="track-badge prereg-badge">Pre-registration required</span>
</div>
<p class="session-meta">
<span class="meta-item"><ion-icon name="time-outline"></ion-icon> {{session.timeStart}} – {{session.timeEnd}}</span>
<span *ngIf="session.displayLocation || session.location" class="meta-item"><ion-icon name="location-outline"></ion-icon> {{session.displayLocation || session.location}}</span>
</p>
<div *ngIf="session.speakers?.length" class="speaker-avatars">
<ion-avatar *ngFor="let speaker of session.speakers" class="speaker-avatar-small">
<img [src]="speaker.profilePic" [alt]="speaker.name">
</ion-avatar>
<span class="speaker-names">{{session.speakerNames.join(', ')}}</span>
</div>
</ion-label>
</ion-item>
</ion-list>
</div>
</div>

<div *ngIf="isOpenSpaceView" class="ion-padding-horizontal">
<p>
Expand Down
36 changes: 31 additions & 5 deletions src/app/pages/schedule-list/schedule-list.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
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;
Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 6 additions & 6 deletions src/app/pages/schedule/schedule.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
</ion-segment-button>
</ion-segment>
<ion-buttons slot="end">
<ion-button (click)="toggleFavorites()" class="header-action">
<ion-button (click)="toggleFavorites()" class="header-action" [attr.aria-label]="segment === 'favorites' ? 'Show all sessions' : 'Show favorites'">
<ion-icon slot="icon-only" [name]="segment === 'favorites' ? 'star' : 'star-outline'"></ion-icon>
</ion-button>
<ion-button *ngIf="excludeTracks.length" (click)="clearFilters()" class="header-action filters-active" aria-label="Clear filters">
<ion-icon slot="icon-only" name="close-circle"></ion-icon>
</ion-button>
<ion-button (click)="presentFilter()" class="header-action" [class.filters-active]="excludeTracks.length">
<ion-icon slot="icon-only" [name]="excludeTracks.length ? 'funnel' : 'funnel-outline'"></ion-icon>
<ion-button (click)="presentFilter()" class="header-action filter-button"
[class.filters-active]="excludeTracks.length"
[class.filters-custom]="hasCustomFilters"
[attr.aria-label]="hasCustomFilters ? 'Filter sessions (custom filters active)' : (excludeTracks.length ? 'Filter sessions (some tracks hidden)' : 'Filter sessions')">
<ion-icon slot="icon-only" name="funnel-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
Expand Down
25 changes: 23 additions & 2 deletions src/app/pages/schedule/schedule.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
14 changes: 7 additions & 7 deletions src/app/pages/schedule/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 8 additions & 7 deletions src/app/providers/conference-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
})
Expand Down
19 changes: 14 additions & 5 deletions src/app/providers/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = ['Open Space'];

export function isCustomScheduleFilter(excluded: ReadonlyArray<string>): 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'
Expand Down Expand Up @@ -279,12 +289,11 @@ export class UserData {

getScheduleFilters(): Promise<Array<string>> {
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;
});
Expand Down
Loading