Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
70d12b5
fix(venue): use full "Long Beach Convention & Entertainment Center" name
JacobCoffee Apr 30, 2026
beaf272
fix(nav): rename "Conference Map" to "Conference Maps" (plural)
JacobCoffee Apr 30, 2026
0cac867
fix(about): conference window is May 13-19, not 14-18
JacobCoffee Apr 30, 2026
ae49ac9
fix(session-types): talks are 30-45 min; clarify digital Open Space b…
JacobCoffee Apr 30, 2026
25f8e81
fix(social-media): correct PyCon US Bluesky handle to pycon.us
JacobCoffee Apr 30, 2026
942a719
fix(psf): correct Sponsors/Volunteer URLs and add LinkedIn
JacobCoffee Apr 30, 2026
8319087
revert(rooms): drop 103AB PyLadies Lunch pin
JacobCoffee Apr 30, 2026
79a52dd
feat(sprints): add intro blurb and link to main Sprints page
JacobCoffee Apr 30, 2026
f192e15
fix(wifi): make "Open Wi-Fi Settings" button work across platforms
JacobCoffee Apr 30, 2026
06b1a7a
feat(wifi): show Temporal as Wi-Fi sponsor on the Wi-Fi page
JacobCoffee Apr 30, 2026
c164c48
feat(venues-hours): show Google as Quiet Room sponsor
JacobCoffee Apr 30, 2026
9462c9f
fix(session-detail): wire add-to-calendar with web fallback and errors
JacobCoffee Apr 30, 2026
7b0b424
feat(conference-map): add Job Fair floor-plan as a 4th tab
JacobCoffee Apr 30, 2026
6775a89
feat(session-detail): surface Job Fair floor-plan on Job Fair sessions
JacobCoffee Apr 30, 2026
5491cbc
fix(sponsors): wrong-booth nav was a stale-state bug, not a routing one
JacobCoffee Apr 30, 2026
64ea0a9
feat(job-listings): surface Job Fair floor plan on the listings page
JacobCoffee Apr 30, 2026
7ac2d2b
fix(schedule): flag summit sessions as pre-registration required
JacobCoffee Apr 30, 2026
172ee7f
fix(room-detail): show pre-registration badge on session rows
JacobCoffee Apr 30, 2026
bb36107
fix(venues): use singular "Venue & Hours" in app header and side menu
JacobCoffee Apr 30, 2026
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
4 changes: 2 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ export class AppComponent implements OnInit {
{ title: 'Conference Info', url: '/app/tabs/about-pycon', icon: 'information-circle-outline' },
{ title: 'Code of Conduct', url: '/app/tabs/coc', icon: 'shield-checkmark-outline' },
{ title: 'Wi-Fi', url: '/app/tabs/wifi', icon: 'wifi-outline' },
{ title: 'Venues & Hours', url: '/app/tabs/venues-hours', icon: 'location-outline' },
{ title: 'Venue & Hours', url: '/app/tabs/venues-hours', icon: 'location-outline' },
{ title: 'Session Types', url: '/app/tabs/session-types', icon: 'pricetags-outline' },
]
infoPages = [
{ title: 'About The PSF', url: '/app/tabs/about-psf', icon: 'logo-python' },
{ title: 'Social', url: '/app/tabs/social-media', icon: 'chatbubbles-outline' },
{ title: 'Conference Map', url: '/app/tabs/conference-map', icon: 'map-outline' },
{ title: 'Conference Maps', url: '/app/tabs/conference-map', icon: 'map-outline' },
{ title: 'Help & Safety', url: '/app/tabs/help', icon: 'help-circle-outline' },
]
nickname = null;
Expand Down
99 changes: 52 additions & 47 deletions src/app/expo-hall-map/expo-hall-map.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { IonSearchbar, LoadingController } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';

import { ConferenceData } from '../providers/conference-data';

Expand All @@ -25,7 +22,7 @@ export interface BoothData {
templateUrl: './expo-hall-map.component.html',
styleUrls: ['./expo-hall-map.component.scss'],
})
export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
export class ExpoHallMapComponent implements OnInit, AfterViewInit {
@ViewChild('searchBar') searchBar!: IonSearchbar;
@ViewChild('pinchZoom', { read: ElementRef }) pinchZoomEl?: ElementRef<HTMLElement>;
@ViewChild('pinchZoom') pinchZoomCmp?: {
Expand Down Expand Up @@ -126,8 +123,6 @@ export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
constructor(
private confData: ConferenceData,
private loadingCtrl: LoadingController,
private route: ActivatedRoute,
private router: Router,
) {}

ngOnInit() {
Expand All @@ -144,9 +139,19 @@ export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
// popup instead of underneath it.
private static readonly DEEPLINK_POPUP_OFFSET_PX = 60;

private querySub?: Subscription;
private routerSub?: Subscription;
private lastZoomedBoothId: string | null = null;
// Booth id queued before pinch-zoom finishes initializing. ngAfterViewInit
// polls for the IvyPinch instance, and the parent ConferenceMapPage may
// call zoomToBoothId() before that polling completes (especially on cold
// entry to the tab when image + pinch-zoom are still hydrating). We hold
// the id here and apply it the moment pinch-zoom is ready.
private pendingBoothId: string | null = null;
private pinchReady = false;
// Token incremented per zoom request so async work (image-load wait)
// belonging to a superseded request can short-circuit instead of
// clobbering the latest zoom — protects against the rare race where the
// user taps two booth pills in fast succession before the floor-plan
// image has finished loading.
private zoomToken = 0;

ngAfterViewInit() {
// @ciag/ngx-pinch-zoom hardcodes defaultMaxScale=3 and only auto-derives
Expand All @@ -161,31 +166,12 @@ export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
const inner = this.pinchZoomCmp?.pinchZoom;
if (inner) {
inner.maxScale = 25;
// React to the current ?booth=<id>, and to any future change while
// the component stays mounted (e.g. user pops back to a different
// sponsor and taps that booth's pill — Angular reuses this instance
// and only the query param changes).
this.querySub = this.route.queryParamMap.subscribe(params => {
this.maybeZoomToQueryBooth(params.get('booth'));
});
// Belt-and-braces: Ionic page caching keeps this component alive
// across nav, and ActivatedRoute.queryParamMap doesn't always
// re-emit when the cached page is re-entered with a new query
// string (sponsor → booth → back to sponsors → other sponsor →
// booth would otherwise leave us pinned to the first booth). On
// every navigation that lands on /expo-hall, parse the live URL
// (NOT route.snapshot, which is set on route activation and stays
// stale when Ionic just shows a cached page) and zoom if the
// booth id changed.
this.routerSub = this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
.subscribe(e => {
const url = e.urlAfterRedirects || this.router.url;
if (!url.includes('/expo-hall')) return;
const tree = this.router.parseUrl(url);
const wantId = tree.queryParamMap.get('booth');
this.maybeZoomToQueryBooth(wantId);
});
this.pinchReady = true;
if (this.pendingBoothId) {
const id = this.pendingBoothId;
this.pendingBoothId = null;
this.zoomToBoothId(id);
}
return;
}
if (Date.now() - start < 2000) {
Expand All @@ -195,21 +181,36 @@ export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
tick();
}

ngOnDestroy() {
this.querySub?.unsubscribe();
this.routerSub?.unsubscribe();
}

private maybeZoomToQueryBooth(wantId: string | null) {
if (!wantId) return;
if (wantId === this.lastZoomedBoothId) return; // already there
const booth = this.booths.find(b => b.id === String(wantId));
/**
* Public entry point used by ConferenceMapPage to request a zoom-to-booth.
* Driven by Ionic's ionViewWillEnter on the parent page so it fires
* reliably on first nav, cached re-entry with a different ?booth=, the
* same ?booth= twice in a row (re-centers if the user has panned away),
* tab-switch return, and cold-start deeplinks.
*
* No `lastZoomedBoothId` guard: if the parent calls us, it's because the
* user explicitly asked to see this booth — we should always re-center,
* even if the id matches the previous zoom (the user may have panned).
*/
zoomToBoothId(boothId: string | null | undefined) {
if (!boothId) return;
const id = String(boothId);
if (!this.pinchReady) {
this.pendingBoothId = id;
return;
}
const booth = this.booths.find(b => b.id === id);
if (!booth) return;
this.lastZoomedBoothId = wantId;
requestAnimationFrame(() => this.zoomToBooth(booth));
const token = ++this.zoomToken;
requestAnimationFrame(() => {
// Bail if a newer request superseded us between the rAF schedule
// and its callback (extremely unlikely but cheap to guard).
if (token !== this.zoomToken) return;
this.zoomToBooth(booth, token);
});
}

private async zoomToBooth(booth: BoothData) {
private async zoomToBooth(booth: BoothData, token?: number) {
const inner = this.pinchZoomCmp?.pinchZoom;
const host = this.pinchZoomEl?.nativeElement;
if (!inner || !host || !host.offsetWidth) return;
Expand All @@ -228,6 +229,10 @@ export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
});
}

// If a newer zoomToBoothId() arrived while we were waiting on the image,
// abandon this stale request so it doesn't clobber the latest target.
if (token !== undefined && token !== this.zoomToken) return;

// We bypass IvyPinch.setZoom() because it always runs centeringImage() →
// limitPanY() afterwards, and that clamp assumes the image fills the
// host. Our floor plan PNG is wider than tall (W:H ≈ 1.41:1) inside a
Expand Down
1 change: 0 additions & 1 deletion src/app/location-map/room-locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ const ROOM_LOCATIONS_RAW: Record<string, RoomLocation> = {
'103a': concourse('Room 103A', 39, 50, 'Talk Track / Security Track'),
'103b': concourse('Room 103B', 43, 50, 'Talk Track / Security Track'),
'103c': concourse('Room 103C', 46, 50, 'Talk Track / Security Track'),
'103ab': concourse('Room 103AB', 41, 50, 'PyLadies Lunch'),
'103abc': concourse('Room 103', 43, 50, 'Talk Track / Security Track'),
'104a': concourse('Room 104A', 72, 38, 'Talk Track'),
'104b': concourse('Room 104B', 80, 35, 'Talk Track'),
Expand Down
12 changes: 10 additions & 2 deletions src/app/pages/about-psf/about-psf.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ <h1>Python Software Foundation</h1>
<p>Learn how you can help the PSF and the greater Python community!</p>
</ion-label>
</ion-item>
<ion-item button="true" (click)="openUrl('https://www.python.org/psf/sponsorship/')" detail="true">
<ion-item button="true" (click)="openUrl('https://www.python.org/psf/sponsors/')" detail="true">
<ion-icon slot="start" name="business-outline" color="primary"></ion-icon>
<ion-label class="ion-text-wrap">
<strong>Sponsors</strong>
Expand All @@ -113,7 +113,7 @@ <h1>Python Software Foundation</h1>
<p>Organizations partnering with the PSF to support the Python community worldwide.</p>
</ion-label>
</ion-item>
<ion-item button="true" (click)="openUrl('https://www.python.org/psf/volunteer/pycon/')" detail="true">
<ion-item button="true" (click)="openUrl('https://us.pycon.org/2026/volunteer/volunteering/')" detail="true">
<ion-icon slot="start" name="star-outline" color="primary"></ion-icon>
<ion-label class="ion-text-wrap">
<strong>Volunteer at PyCon US</strong>
Expand Down Expand Up @@ -189,6 +189,14 @@ <h2>&#64;ThePSF</h2>
<p>PSF on X / Twitter</p>
</ion-label>
</ion-item>

<ion-item button="true" (click)="openUrl('https://www.linkedin.com/company/thepsf/')" detail="true">
<ion-icon slot="start" name="logo-linkedin" color="primary"></ion-icon>
<ion-label>
<h2>Python Software Foundation</h2>
<p>PSF on LinkedIn</p>
</ion-label>
</ion-item>
</ion-list>

<div class="ion-padding ion-text-center">
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/about-pycon/about-pycon.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<div class="about-hero">
<ion-icon name="information-circle" class="about-hero-icon"></ion-icon>
<h1>PyCon US 2026</h1>
<p>Long Beach, CA &bull; May 14-18</p>
<p>Long Beach, CA &bull; May 13-19</p>
</div>

<ion-card class="about-card">
Expand Down
21 changes: 21 additions & 0 deletions src/app/pages/conference-map/conference-map.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<ion-segment-button value="expo-hall">
<ion-label>Expo Hall</ion-label>
</ion-segment-button>
<ion-segment-button value="job-fair">
<ion-label>Job Fair</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
Expand Down Expand Up @@ -64,4 +67,22 @@

<app-expo-hall-map *ngIf="mapView === 'expo-hall'" #expoMap></app-expo-hall-map>

<ng-container *ngIf="mapView === 'job-fair'">
<pinch-zoom
class="job-fair-zoom"
[autoZoomOut]="false"
[doubleTap]="true"
[wheel]="true"
[draggableImage]="false"
[limitZoom]="'original image size'"
backgroundColor="#ffffff">
<div class="job-fair-canvas">
<img
src="assets/img/floor-plans/job-fair.jpg"
alt="Job Fair & Community Showcase floor plan"
decoding="async" />
</div>
</pinch-zoom>
</ng-container>

</ion-content>
20 changes: 20 additions & 0 deletions src/app/pages/conference-map/conference-map.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,23 @@
font-size: 13px;
color: var(--ion-color-medium);
}

.job-fair-zoom {
width: 100%;
height: 100%;
background: #ffffff;
}

.job-fair-canvas {
position: relative;
width: 100%;
display: block;
}

.job-fair-canvas img {
width: 100%;
height: auto;
display: block;
-webkit-user-drag: none;
user-select: none;
}
24 changes: 23 additions & 1 deletion src/app/pages/conference-map/conference-map.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ExpoHallMapComponent } from '../../expo-hall-map/expo-hall-map.componen
import { FloorPlanModalComponent } from '../../floor-plan-modal/floor-plan-modal.component';
import { LiveUpdateService } from '../../providers/live-update.service';

type MapView = 'floor-plans' | '3d-tour' | 'expo-hall';
type MapView = 'floor-plans' | '3d-tour' | 'expo-hall' | 'job-fair';

interface FloorPlan {
id: string;
Expand Down Expand Up @@ -85,6 +85,28 @@ export class ConferenceMapPage implements OnInit {
}
}

// Single source of truth for "go zoom to booth X". Driven by Ionic's
// ionViewWillEnter, which (unlike Angular's ActivatedRoute observables)
// is GUARANTEED to fire on every entry — first nav, cached re-entry with
// a new ?booth= value, tab-switch return, and cold-start deeplink. This
// replaces the previous brittle dual-subscription (queryParamMap +
// router.events) inside the child map component, which could miss
// re-entries when Angular treated the activation as a no-op.
ionViewWillEnter() {
const boothId = this.route.snapshot.queryParamMap.get('booth');
if (!boothId) return;
// Force the segment to expo-hall when arriving via ?booth=, even if the
// user had switched the segment to floor-plans before leaving the tab.
// Without this, the *ngIf gate keeps the map component unmounted and
// the zoom request would be dropped.
this.mapView = 'expo-hall';
// The expo map *ngIf may not have rendered the child yet on this tick
// (cold entry, or just-flipped segment). Defer to next macrotask so
// @ViewChild has resolved before we call into it; the component
// queues the request internally if pinch-zoom isn't ready yet.
setTimeout(() => this.expoMap?.zoomToBoothId(boothId), 0);
}

toggleExpoSearch() {
this.expoMap?.toggleSearch();
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/help/help.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ <h1>Help & Safety</h1>
<ion-item button="true" routerLink="/app/tabs/conference-map" detail="true">
<ion-icon slot="start" name="map-outline" color="primary"></ion-icon>
<ion-label class="ion-text-wrap">
<h2>Full Conference Map</h2>
<h2>Full Conference Maps</h2>
<p>Browse all floor plans and the expo hall</p>
</ion-label>
</ion-item>
Expand Down
4 changes: 3 additions & 1 deletion src/app/pages/job-listings/job-listings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { IonicModule } from '@ionic/angular';
import { JobListingsPageRoutingModule } from './job-listings-routing.module';

import { JobListingsPage } from './job-listings.page';
import { FloorPlanModalModule } from '../../floor-plan-modal/floor-plan-modal.module';

@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
JobListingsPageRoutingModule
JobListingsPageRoutingModule,
FloorPlanModalModule
],
declarations: [JobListingsPage]
})
Expand Down
8 changes: 8 additions & 0 deletions src/app/pages/job-listings/job-listings.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ <h1>Job Listings</h1>
<ion-searchbar [(ngModel)]="searchText" [debounce]="250" showCancelButton="focus" inputmode="search" (ionClear)="resetSearch()" (ionCancel)="resetSearch()" (ionChange)="filterListings()" placeholder="Search sponsors"></ion-searchbar>
</ion-toolbar>

<div class="job-fair-floor-plan-wrapper">
<button type="button" class="job-fair-floor-plan-card" (click)="openJobFairFloorPlan()">
<img src="assets/img/floor-plans/job-fair-thumb.jpg" alt="" class="job-fair-floor-plan-thumb">
<span class="job-fair-floor-plan-label">View Job Fair Floor Plan</span>
<ion-icon name="chevron-forward" class="job-fair-floor-plan-chevron"></ion-icon>
</button>
</div>

<div *ngIf="allListings.length === 0" class="ion-padding-horizontal">
<p>No job listings have been posted yet. Check back closer to the conference!</p>
</div>
Expand Down
Loading
Loading