Skip to content

Commit 77ef9f6

Browse files
JacobCoffeeclaude
andauthored
Bulk fixups from staff review (#285)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7687a5b commit 77ef9f6

32 files changed

Lines changed: 685 additions & 94 deletions

src/app/app.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ export class AppComponent implements OnInit {
5050
{ title: 'Conference Info', url: '/app/tabs/about-pycon', icon: 'information-circle-outline' },
5151
{ title: 'Code of Conduct', url: '/app/tabs/coc', icon: 'shield-checkmark-outline' },
5252
{ title: 'Wi-Fi', url: '/app/tabs/wifi', icon: 'wifi-outline' },
53-
{ title: 'Venues & Hours', url: '/app/tabs/venues-hours', icon: 'location-outline' },
53+
{ title: 'Venue & Hours', url: '/app/tabs/venues-hours', icon: 'location-outline' },
5454
{ title: 'Session Types', url: '/app/tabs/session-types', icon: 'pricetags-outline' },
5555
]
5656
infoPages = [
5757
{ title: 'About The PSF', url: '/app/tabs/about-psf', icon: 'logo-python' },
5858
{ title: 'Social', url: '/app/tabs/social-media', icon: 'chatbubbles-outline' },
59-
{ title: 'Conference Map', url: '/app/tabs/conference-map', icon: 'map-outline' },
59+
{ title: 'Conference Maps', url: '/app/tabs/conference-map', icon: 'map-outline' },
6060
{ title: 'Help & Safety', url: '/app/tabs/help', icon: 'help-circle-outline' },
6161
]
6262
nickname = null;

src/app/expo-hall-map/expo-hall-map.component.ts

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
2-
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
1+
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
32
import { IonSearchbar, LoadingController } from '@ionic/angular';
4-
import { Subscription } from 'rxjs';
5-
import { filter } from 'rxjs/operators';
63

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

@@ -25,7 +22,7 @@ export interface BoothData {
2522
templateUrl: './expo-hall-map.component.html',
2623
styleUrls: ['./expo-hall-map.component.scss'],
2724
})
28-
export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
25+
export class ExpoHallMapComponent implements OnInit, AfterViewInit {
2926
@ViewChild('searchBar') searchBar!: IonSearchbar;
3027
@ViewChild('pinchZoom', { read: ElementRef }) pinchZoomEl?: ElementRef<HTMLElement>;
3128
@ViewChild('pinchZoom') pinchZoomCmp?: {
@@ -126,8 +123,6 @@ export class ExpoHallMapComponent implements OnInit, AfterViewInit, OnDestroy {
126123
constructor(
127124
private confData: ConferenceData,
128125
private loadingCtrl: LoadingController,
129-
private route: ActivatedRoute,
130-
private router: Router,
131126
) {}
132127

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

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

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

198-
ngOnDestroy() {
199-
this.querySub?.unsubscribe();
200-
this.routerSub?.unsubscribe();
201-
}
202-
203-
private maybeZoomToQueryBooth(wantId: string | null) {
204-
if (!wantId) return;
205-
if (wantId === this.lastZoomedBoothId) return; // already there
206-
const booth = this.booths.find(b => b.id === String(wantId));
184+
/**
185+
* Public entry point used by ConferenceMapPage to request a zoom-to-booth.
186+
* Driven by Ionic's ionViewWillEnter on the parent page so it fires
187+
* reliably on first nav, cached re-entry with a different ?booth=, the
188+
* same ?booth= twice in a row (re-centers if the user has panned away),
189+
* tab-switch return, and cold-start deeplinks.
190+
*
191+
* No `lastZoomedBoothId` guard: if the parent calls us, it's because the
192+
* user explicitly asked to see this booth — we should always re-center,
193+
* even if the id matches the previous zoom (the user may have panned).
194+
*/
195+
zoomToBoothId(boothId: string | null | undefined) {
196+
if (!boothId) return;
197+
const id = String(boothId);
198+
if (!this.pinchReady) {
199+
this.pendingBoothId = id;
200+
return;
201+
}
202+
const booth = this.booths.find(b => b.id === id);
207203
if (!booth) return;
208-
this.lastZoomedBoothId = wantId;
209-
requestAnimationFrame(() => this.zoomToBooth(booth));
204+
const token = ++this.zoomToken;
205+
requestAnimationFrame(() => {
206+
// Bail if a newer request superseded us between the rAF schedule
207+
// and its callback (extremely unlikely but cheap to guard).
208+
if (token !== this.zoomToken) return;
209+
this.zoomToBooth(booth, token);
210+
});
210211
}
211212

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

232+
// If a newer zoomToBoothId() arrived while we were waiting on the image,
233+
// abandon this stale request so it doesn't clobber the latest target.
234+
if (token !== undefined && token !== this.zoomToken) return;
235+
231236
// We bypass IvyPinch.setZoom() because it always runs centeringImage() →
232237
// limitPanY() afterwards, and that clamp assumes the image fills the
233238
// host. Our floor plan PNG is wider than tall (W:H ≈ 1.41:1) inside a

src/app/location-map/room-locations.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ const ROOM_LOCATIONS_RAW: Record<string, RoomLocation> = {
9292
'103a': concourse('Room 103A', 39, 50, 'Talk Track / Security Track'),
9393
'103b': concourse('Room 103B', 43, 50, 'Talk Track / Security Track'),
9494
'103c': concourse('Room 103C', 46, 50, 'Talk Track / Security Track'),
95-
'103ab': concourse('Room 103AB', 41, 50, 'PyLadies Lunch'),
9695
'103abc': concourse('Room 103', 43, 50, 'Talk Track / Security Track'),
9796
'104a': concourse('Room 104A', 72, 38, 'Talk Track'),
9897
'104b': concourse('Room 104B', 80, 35, 'Talk Track'),

src/app/pages/about-psf/about-psf.page.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ <h1>Python Software Foundation</h1>
9292
<p>Learn how you can help the PSF and the greater Python community!</p>
9393
</ion-label>
9494
</ion-item>
95-
<ion-item button="true" (click)="openUrl('https://www.python.org/psf/sponsorship/')" detail="true">
95+
<ion-item button="true" (click)="openUrl('https://www.python.org/psf/sponsors/')" detail="true">
9696
<ion-icon slot="start" name="business-outline" color="primary"></ion-icon>
9797
<ion-label class="ion-text-wrap">
9898
<strong>Sponsors</strong>
@@ -113,7 +113,7 @@ <h1>Python Software Foundation</h1>
113113
<p>Organizations partnering with the PSF to support the Python community worldwide.</p>
114114
</ion-label>
115115
</ion-item>
116-
<ion-item button="true" (click)="openUrl('https://www.python.org/psf/volunteer/pycon/')" detail="true">
116+
<ion-item button="true" (click)="openUrl('https://us.pycon.org/2026/volunteer/volunteering/')" detail="true">
117117
<ion-icon slot="start" name="star-outline" color="primary"></ion-icon>
118118
<ion-label class="ion-text-wrap">
119119
<strong>Volunteer at PyCon US</strong>
@@ -189,6 +189,14 @@ <h2>&#64;ThePSF</h2>
189189
<p>PSF on X / Twitter</p>
190190
</ion-label>
191191
</ion-item>
192+
193+
<ion-item button="true" (click)="openUrl('https://www.linkedin.com/company/thepsf/')" detail="true">
194+
<ion-icon slot="start" name="logo-linkedin" color="primary"></ion-icon>
195+
<ion-label>
196+
<h2>Python Software Foundation</h2>
197+
<p>PSF on LinkedIn</p>
198+
</ion-label>
199+
</ion-item>
192200
</ion-list>
193201

194202
<div class="ion-padding ion-text-center">

src/app/pages/about-pycon/about-pycon.page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<div class="about-hero">
1212
<ion-icon name="information-circle" class="about-hero-icon"></ion-icon>
1313
<h1>PyCon US 2026</h1>
14-
<p>Long Beach, CA &bull; May 14-18</p>
14+
<p>Long Beach, CA &bull; May 13-19</p>
1515
</div>
1616

1717
<ion-card class="about-card">

src/app/pages/conference-map/conference-map.page.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<ion-segment-button value="expo-hall">
2222
<ion-label>Expo Hall</ion-label>
2323
</ion-segment-button>
24+
<ion-segment-button value="job-fair">
25+
<ion-label>Job Fair</ion-label>
26+
</ion-segment-button>
2427
</ion-segment>
2528
</ion-toolbar>
2629
</ion-header>
@@ -64,4 +67,22 @@
6467

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

70+
<ng-container *ngIf="mapView === 'job-fair'">
71+
<pinch-zoom
72+
class="job-fair-zoom"
73+
[autoZoomOut]="false"
74+
[doubleTap]="true"
75+
[wheel]="true"
76+
[draggableImage]="false"
77+
[limitZoom]="'original image size'"
78+
backgroundColor="#ffffff">
79+
<div class="job-fair-canvas">
80+
<img
81+
src="assets/img/floor-plans/job-fair.jpg"
82+
alt="Job Fair & Community Showcase floor plan"
83+
decoding="async" />
84+
</div>
85+
</pinch-zoom>
86+
</ng-container>
87+
6788
</ion-content>

src/app/pages/conference-map/conference-map.page.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,23 @@
6767
font-size: 13px;
6868
color: var(--ion-color-medium);
6969
}
70+
71+
.job-fair-zoom {
72+
width: 100%;
73+
height: 100%;
74+
background: #ffffff;
75+
}
76+
77+
.job-fair-canvas {
78+
position: relative;
79+
width: 100%;
80+
display: block;
81+
}
82+
83+
.job-fair-canvas img {
84+
width: 100%;
85+
height: auto;
86+
display: block;
87+
-webkit-user-drag: none;
88+
user-select: none;
89+
}

src/app/pages/conference-map/conference-map.page.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ExpoHallMapComponent } from '../../expo-hall-map/expo-hall-map.componen
66
import { FloorPlanModalComponent } from '../../floor-plan-modal/floor-plan-modal.component';
77
import { LiveUpdateService } from '../../providers/live-update.service';
88

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

1111
interface FloorPlan {
1212
id: string;
@@ -85,6 +85,28 @@ export class ConferenceMapPage implements OnInit {
8585
}
8686
}
8787

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

src/app/pages/help/help.page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ <h1>Help & Safety</h1>
3434
<ion-item button="true" routerLink="/app/tabs/conference-map" detail="true">
3535
<ion-icon slot="start" name="map-outline" color="primary"></ion-icon>
3636
<ion-label class="ion-text-wrap">
37-
<h2>Full Conference Map</h2>
37+
<h2>Full Conference Maps</h2>
3838
<p>Browse all floor plans and the expo hall</p>
3939
</ion-label>
4040
</ion-item>

src/app/pages/job-listings/job-listings.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import { IonicModule } from '@ionic/angular';
77
import { JobListingsPageRoutingModule } from './job-listings-routing.module';
88

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

1112
@NgModule({
1213
imports: [
1314
CommonModule,
1415
FormsModule,
1516
IonicModule,
16-
JobListingsPageRoutingModule
17+
JobListingsPageRoutingModule,
18+
FloorPlanModalModule
1719
],
1820
declarations: [JobListingsPage]
1921
})

0 commit comments

Comments
 (0)