diff --git a/Makefile b/Makefile index 84cc0332..f63cd4ee 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: serve build capsync lint test e2e check clean install pwa android ios ios-live android-live +.PHONY: serve build capsync lint test e2e check clean install pwa android ios ios-live android-live appflow-ship default: @echo "Call a specific subcommand:" @@ -57,3 +57,31 @@ clean: rm -rf www/ rm -rf .angular/ rm -rf node_modules/ + +# Push current main to GitHub, build the web bundle on Appflow, and deploy it +# to the Production live-update channel. Requires: +# - IONIC_TOKEN env var (Appflow personal access token) +# - jq installed +# After build the jq selector below may need tweaking once you see the actual +# JSON shape; if so, run `appflow build web --json | jq .` once and update. +APPFLOW_APP_ID := e8e09c7a +APPFLOW_CHANNEL := Production + +appflow-ship: + @command -v appflow >/dev/null || (echo "appflow CLI not found; install with: npm install -g @ionic/cloud-cli" && exit 1) + @command -v jq >/dev/null || (echo "jq not found; brew install jq" && exit 1) + @test -n "$$IONIC_TOKEN" || (echo "IONIC_TOKEN not set (export from .env: set -x IONIC_TOKEN (grep IONIC_TOKEN .env | cut -d= -f2))" && exit 1) + @COMMIT=$$(git rev-parse HEAD); \ + if [ -z "$$(git branch -r --contains $$COMMIT 2>/dev/null)" ]; then \ + echo "ERROR: $$COMMIT is not on any remote branch yet. Push it first (e.g. 'git push origin HEAD')."; \ + exit 1; \ + fi; \ + echo ">> Building $(APPFLOW_APP_ID) @ $$COMMIT on Appflow..."; \ + BUILD_JSON=$$(appflow build web --app-id $(APPFLOW_APP_ID) --commit $$COMMIT --json); \ + BUILD_ID=$$(echo "$$BUILD_JSON" | jq -r '.buildId // .build_id // .id'); \ + if [ -z "$$BUILD_ID" ] || [ "$$BUILD_ID" = "null" ]; then \ + echo "Could not parse build ID from response:"; echo "$$BUILD_JSON"; exit 1; \ + fi; \ + echo ">> Built #$$BUILD_ID. Deploying to $(APPFLOW_CHANNEL)..."; \ + appflow deploy web --app-id $(APPFLOW_APP_ID) --build-id $$BUILD_ID --destination $(APPFLOW_CHANNEL); \ + echo ">> Done. Existing app installs will pick up build #$$BUILD_ID on next cold launch." diff --git a/src/app/pages/about-pycon/about-pycon.page.html b/src/app/pages/about-pycon/about-pycon.page.html index e9d9f5f3..dfc279ca 100644 --- a/src/app/pages/about-pycon/about-pycon.page.html +++ b/src/app/pages/about-pycon/about-pycon.page.html @@ -34,11 +34,19 @@

PyCon US 2026

{{liveUpdateService.appVersion}}
- Build + Native Build {{liveUpdateService.build}}
+
+ Live Update Build + #{{liveUpdateService.snapshot.buildId}} +
+
+ Channel + {{liveUpdateService.channel}} +
- Environment + API {{environmentUrl}}
diff --git a/src/app/pages/expo-hall/expo-hall.page.html b/src/app/pages/expo-hall/expo-hall.page.html index a7617f98..d8605ca0 100644 --- a/src/app/pages/expo-hall/expo-hall.page.html +++ b/src/app/pages/expo-hall/expo-hall.page.html @@ -1,233 +1,92 @@ - - - 1 + + + + + - Expo-Hall - + Expo Hall - + + + + + + -
-
-
-
-
- {{ sponsor.name }} -
-
-
-
- -
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
+ +
+
+ {{ booth.name }} + Booth {{ booth.id }}
+
-
-
-
-
-
+ +
+ +
+ + PyCon US 2026 Expo Hall Floor Plan + +
+
+
+ +
+
+
+ +
+
+
-
-
-
-
+ +
+
+ + {{ selectedBooth.name }} + Booth {{ selectedBooth.id }} · {{ selectedBooth.level }} + + +
+ {{ selectedBooth.name }} + Booth {{ selectedBooth.id }} +
+ + +
-
- -
+ diff --git a/src/app/pages/expo-hall/expo-hall.page.scss b/src/app/pages/expo-hall/expo-hall.page.scss index 7129a359..75320a9d 100644 --- a/src/app/pages/expo-hall/expo-hall.page.scss +++ b/src/app/pages/expo-hall/expo-hall.page.scss @@ -1,52 +1,170 @@ -ion-content{ - white-space: nowrap; -} +// Expo Hall — interactive floor plan with sponsor logo overlays. .map-container { + width: 100%; height: 100%; - overflow-x: scroll!important; - overflow-y: hidden; + overflow: hidden; } .expo-hall-map { + width: 100%; height: 100%; - aspect-ratio: 4096/2885; - background-image:url(/assets/img/pycon-us-2026-floorplan.png); - background-size: contain; - background-repeat: no-repeat; +} + +.map-inner { position: relative; + display: inline-block; + width: 100%; + + img { + width: 100%; + height: auto; + display: block; + } +} + +// Booth overlay positioned on the floor plan +.boothgroup { + position: absolute; + cursor: pointer; + + .boothgroupinner { + position: relative; + width: 100%; + height: 100%; + } + + .booth { + position: absolute; + inset: 0; + border-radius: 4px; + transition: background 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + } + + .booth-img { + max-width: 85%; + max-height: 85%; + object-fit: contain; + pointer-events: none; + } + + &:active .booth, + &:hover .booth { + background: rgba(240, 192, 64, 0.28); + outline: 2px solid rgba(240, 192, 64, 0.85); + } + + &.highlighted .booth { + background: rgba(240, 192, 64, 0.35); + outline: 3px solid #f0c040; + animation: pulse 1.2s ease-in-out 2; + } +} + +@keyframes pulse { + 0% { background: rgba(240, 192, 64, 0.15); } + 50% { background: rgba(240, 192, 64, 0.45); } + 100% { background: rgba(240, 192, 64, 0.15); } +} + +// Search results dropdown +.search-results { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: var(--ion-background-color, #fff); + border-bottom: 1px solid var(--ion-color-light, #e0e0e0); + max-height: 40vh; + overflow-y: auto; +} + +.search-result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--ion-color-light, #f0f0f0); + cursor: pointer; + + &:active { + background: var(--ion-color-light, #f5f5f5); + } + + .booth-name { + font-size: clamp(0.8rem, 2.5vw, 1rem); + font-weight: 600; + color: var(--ion-text-color, #222); + } + + .booth-number { + font-size: clamp(0.7rem, 2vw, 0.85rem); + color: var(--ion-color-medium, #888); + } +} + +// Tap popup at the bottom of the screen +.booth-popup { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 200; + padding: 0 16px 16px; } -.booth { - margin: auto; - text-align: center; - border-radius: .33em; - scroll-margin-left: 3em; - scroll-margin-right: 3em; +.booth-popup-inner { + display: flex; + align-items: center; + justify-content: space-between; + background: var(--ion-background-color, #fff); + border-radius: 12px; + padding: 14px 16px; + box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.15); } -.booth-highlight { - border-style: solid; - border-width: 3%; - border-color: rgba(255, 252, 127, 0.75); - background: rgba(255, 252, 127, 0.33); +.booth-popup-content { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + text-decoration: none; + color: inherit; + position: relative; + + // The chevron sits inline at the right side of the link content so the + // tappable target spans the whole sponsor name + level row. + .booth-popup-chevron { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + color: var(--ion-color-medium, #888); + font-size: 1rem; + } } -.booth-img { - max-height: 33%; - max-width: 85%; - margin-top: 20%; +a.booth-popup-content { + padding-right: 24px; } -.tall-booth .booth-img { - margin-top: 80%; +.booth-popup-name { + font-size: clamp(0.9rem, 3vw, 1.1rem); + font-weight: 700; + color: var(--ion-text-color, #111); } -.sq-booth .booth-img { - margin-top: 20%; - max-height: 50%; +.booth-popup-number { + font-size: clamp(0.75rem, 2.5vw, 0.9rem); + color: var(--ion-color-medium, #888); } -.xtall-booth .booth-img { - margin-top: 50% +.close-btn { + --color: var(--ion-color-medium, #888); + margin-left: 8px; } diff --git a/src/app/pages/expo-hall/expo-hall.page.spec.ts b/src/app/pages/expo-hall/expo-hall.page.spec.ts deleted file mode 100644 index 6e909aad..00000000 --- a/src/app/pages/expo-hall/expo-hall.page.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IonicModule } from '@ionic/angular'; - -import { ExpoHallPage } from './expo-hall.page'; - -describe('ExpoHallPage', () => { - let component: ExpoHallPage; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ ExpoHallPage ], - imports: [IonicModule.forRoot()] - }).compileComponents(); - - fixture = TestBed.createComponent(ExpoHallPage); - component = fixture.componentInstance; - fixture.detectChanges(); - })); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/pages/expo-hall/expo-hall.page.ts b/src/app/pages/expo-hall/expo-hall.page.ts index 345c6306..f31ef673 100644 --- a/src/app/pages/expo-hall/expo-hall.page.ts +++ b/src/app/pages/expo-hall/expo-hall.page.ts @@ -1,128 +1,198 @@ -import { Component, OnInit, ChangeDetectorRef, ViewChild, ViewEncapsulation, AfterViewChecked } from '@angular/core'; -import { KeyValue } from '@angular/common'; -import { Keyboard } from '@capacitor/keyboard'; -import { LoadingController } from '@ionic/angular'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IonSearchbar, LoadingController } from '@ionic/angular'; import { ConferenceData } from '../../providers/conference-data'; import { LiveUpdateService } from '../../providers/live-update.service'; +export interface BoothData { + id: string; + name: string; + top: number; + left: number; + width: number; + height: number; + imgW: number; + imgH: number; + logoUrl?: string; + level?: string; + description?: string; +} @Component({ selector: 'app-expo-hall', templateUrl: './expo-hall.page.html', styleUrls: ['./expo-hall.page.scss'], - encapsulation: ViewEncapsulation.None, }) -export class ExpoHallPage implements OnInit, AfterViewChecked { - sponsors: any; - @ViewChild('search') search : any; - @ViewChild('mapContainer') mapContainer: any; - searchQueryText = ''; - queryResults: any[] = []; - ios: boolean; - showSearchbar: boolean; - private scrolled: boolean = false; - private scrollTarget: any; - private iterableDiffer; +export class ExpoHallPage implements OnInit { + @ViewChild('searchBar') searchBar!: IonSearchbar; + + showSearchbar = false; + searchQuery = ''; + searchResults: BoothData[] = []; + selectedBooth: BoothData | null = null; + highlightedBoothId: string | null = null; + + // Booth coordinates in the original 8000×5655 floor plan image. + // Names are the seed labels — they get overwritten with the live sponsor + // name (and gain logoUrl/level/description) once the API responds. + booths: BoothData[] = [ + { id: '245', name: 'Lerner Python Training', top: 640, left: 3271, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '344', name: 'Zyte', top: 649, left: 3556, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '243', name: 'Analog Devices', top: 925, left: 3271, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '342', name: 'marimo', top: 934, left: 3547, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '638', name: 'AlphaSense', top: 978, left: 5911, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '639', name: 'Snowflake', top: 978, left: 6409, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '140', name: 'Chonkie', top: 1183, left: 1982, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '141', name: 'Tetrix', top: 1191, left: 2418, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '240', name: 'Mission', top: 1191, left: 2684, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '138', name: 'Sublimage', top: 1458, left: 1991, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '238', name: 'Jinja.App', top: 1458, left: 2684, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '635', name: 'ClickHouse', top: 1592, left: 6409, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '735', name: 'Python en Español', top: 1592, left: 7449, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '734', name: 'Djangonauts / DSF', top: 1600, left: 6693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '136', name: 'Capisclo', top: 1725, left: 1991, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '137', name: 'Arcjet', top: 1725, left: 2418, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '336', name: 'Astral', top: 1734, left: 3547, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '236', name: 'Minimus', top: 1743, left: 2693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '237', name: 'Tower Research Capital', top: 1743, left: 3280, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '335', name: 'Pydantic', top: 1743, left: 4080, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '434', name: 'Red Hat', top: 1743, left: 4347, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '633', name: 'Reflex', top: 1858, left: 6418, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '732', name: 'Black Python Devs', top: 1867, left: 6693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '733', name: 'EuroPython Society', top: 1867, left: 7449, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '134', name: 'PixelTable', top: 1992, left: 1991, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '135', name: 'TimeCopilot', top: 2009, left: 2427, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '234', name: 'Python Institute', top: 2009, left: 2693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '235', name: 'Elastic', top: 2009, left: 3280, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '334', name: 'Apify', top: 2009, left: 3556, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '531', name: 'Chainguard', top: 2125, left: 5644, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '630', name: 'Hex', top: 2134, left: 5929, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '730', name: 'CodeDay', top: 2134, left: 6693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '731', name: 'PyLadies', top: 2134, left: 7458, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '631', name: 'Auth0', top: 2143, left: 6418, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '126', name: 'Cloudflare', top: 2321, left: 1991, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '127', name: 'Hudson River Trading', top: 2321, left: 2427, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '226', name: 'Kraken Tech', top: 2330, left: 2684, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '427', name: 'SerpApi', top: 2383, left: 4987, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '729', name: 'Python Asia Organization', top: 2401, left: 7449, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '629', name: 'Temporal', top: 2410, left: 6409, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '728', name: 'SCaLE / Data Con LA', top: 2410, left: 6693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '727', name: 'PyCon Africa / Seneg. / Mozambique',top: 2667, left: 7449, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '627', name: 'ReversingLabs', top: 2676, left: 6418, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '726', name: 'SoCal Python / Inland Empire PUG', top: 2685, left: 6693, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '122', name: 'Sentry', top: 2845, left: 1991, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '621', name: 'Codespeed', top: 3032, left: 6400, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '421', name: 'JetBrains', top: 3041, left: 4987, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '720', name: 'QUBE Research & Technologies', top: 3041, left: 6684, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '119', name: 'AWS', top: 3254, left: 2409, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '717', name: 'Posit, PBC', top: 3397, left: 7209, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '116', name: 'Cubist Systematic Strategies', top: 3414, left: 1991, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '413', name: 'Meta', top: 3681, left: 4987, width: 500, height: 780, imgW: 8000, imgH: 5655 }, + { id: '613', name: 'Bloomberg', top: 3681, left: 6391, width: 500, height: 780, imgW: 8000, imgH: 5655 }, + { id: '213', name: 'GitHub', top: 3690, left: 3280, width: 500, height: 780, imgW: 8000, imgH: 5655 }, + { id: '513', name: 'Vercel', top: 3930, left: 5653, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '112', name: 'Jane Street', top: 3939, left: 1991, width: 250, height: 500, imgW: 8000, imgH: 5655 }, + { id: '313', name: 'Anaconda', top: 3939, left: 4018, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '113', name: 'Capital One', top: 3948, left: 2409, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '709', name: 'Python Packaging Ecosystem Survey', top: 4668, left: 7209, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + { id: '407', name: 'PSF', top: 4677, left: 4978, width: 525, height: 525, imgW: 8000, imgH: 5655 }, + { id: '606', name: 'Attendee Lounge', top: 4686, left: 5502, width: 750, height: 525, imgW: 8000, imgH: 5655 }, + { id: '707', name: 'Codeflash', top: 4935, left: 7209, width: 250, height: 250, imgW: 8000, imgH: 5655 }, + ]; constructor( - private loadingCtrl: LoadingController, private confData: ConferenceData, - private changeDetection: ChangeDetectorRef, + private loadingCtrl: LoadingController, public liveUpdateService: LiveUpdateService, - ) { - this.scrolled = false; - Keyboard.addListener('keyboardWillShow', (info) => { - const height = this.mapContainer.nativeElement.offsetHeight; - this.mapContainer.nativeElement.style.height = height + 'px'; - }); - Keyboard.addListener('keyboardDidShow', info => { - }); - Keyboard.addListener('keyboardWillHide', () => { - }); - Keyboard.addListener('keyboardDidHide', () => { - this.mapContainer.nativeElement.style.height = '100%'; - this.scrollTarget.scrollIntoView(); - }); + ) {} + + ngOnInit() { + this.loadSponsors(); } - reloadSponsors() { - this.loadingCtrl.create({ - message: 'Fetching latest...', - duration: 10000, - }).then((loader) => { - loader.present(); - this.confData.getSponsors().subscribe((sponsors: any[]) => { - this.sponsors = sponsors; - for (const [level, sponsorss] of Object.entries(this.sponsors)) { - for(const [index, sponsor] of Object.entries(sponsorss)) { - if (sponsor.booth_number !== null) { - let elem = document.getElementById("booth"+sponsor.booth_number); - if (elem) { - elem.innerHTML = ""; - } else { - console.log('No booth: ' + sponsor.booth_number); - } - } - } + loadSponsors(showLoader = false) { + const apply = (sponsors: any) => { + for (const list of Object.values(sponsors || {})) { + for (const sponsor of list as any[]) { + if (sponsor.booth_number == null) continue; + const booth = this.booths.find(b => b.id === String(sponsor.booth_number)); + if (!booth) continue; + booth.logoUrl = sponsor.logo_url; + booth.level = sponsor.level; + booth.description = sponsor.description; + if (sponsor.name) booth.name = sponsor.name; } - this.scrolled = false; - this.changeDetection.detectChanges(); - setTimeout(() => {loader.dismiss()}, 100); + } + }; + + if (!showLoader) { + this.confData.getSponsors().subscribe(apply); + return; + } + this.loadingCtrl.create({ message: 'Fetching latest...', duration: 10000 }).then(loader => { + loader.present(); + this.confData.getSponsors().subscribe((sponsors: any) => { + apply(sponsors); + setTimeout(() => loader.dismiss(), 100); }); }); } - resetSearch() { - this.searchQueryText = ""; - this.queryResults = []; - let elems = document.getElementsByClassName('booth-highlight') - Array.from(elems).forEach((elem: any) => {elem.classList.remove('booth-highlight');}) + getBoothStyle(booth: BoothData): { [key: string]: string } { + return { + 'top': `calc(${booth.top} / ${booth.imgH} * 100%)`, + 'left': `calc(${booth.left} / ${booth.imgW} * 100%)`, + 'width': `calc(${booth.width} / ${booth.imgW} * 100%)`, + 'height': `calc(${booth.height} / ${booth.imgH} * 100%)`, + }; } - searchSponsors() { - if (this.searchQueryText === "" || this.searchQueryText === " ") { - this.resetSearch(); - return; + toggleSearch() { + this.showSearchbar = !this.showSearchbar; + if (!this.showSearchbar) { + this.clearSearch(); + } else { + setTimeout(() => this.searchBar?.setFocus(), 150); } - this.queryResults = []; - let elems = document.getElementsByClassName('booth-highlight') - Array.from(elems).forEach((elem: any) => {elem.classList.remove('booth-highlight');}) - this.confData.querySponsors(this.searchQueryText).subscribe((sponsors: any[]) => { - this.queryResults = sponsors; - this.queryResults.forEach((sponsor: any) => { - let elem = document.getElementById("booth"+sponsor.booth_number); - elem.classList.add("booth-highlight"); - }) - }); } - selectSponsor(sponsor) { - this.showSearchbar = false; - this.resetSearch(); - let elem = document.getElementById("booth"+sponsor.booth_number); - elem.classList.add("booth-highlight"); - this.scrollTarget = elem; - this.scrollTarget.scrollIntoView(); + onSearch() { + const q = this.searchQuery.toLowerCase().trim(); + if (!q) { this.searchResults = []; return; } + this.searchResults = this.booths.filter(b => + b.name.toLowerCase().includes(q) || b.id.includes(q) + ); } - async focusButton() { - setTimeout(() => { - this.search.setFocus(); - }, 500); // ms delay - } + clearSearch() { + this.searchQuery = ''; + this.searchResults = []; + this.showSearchbar = false; + this.highlightedBoothId = null; + } - ngOnInit() { - this.reloadSponsors(); + selectBooth(booth: BoothData) { + this.searchResults = []; + this.showSearchbar = false; + this.searchQuery = ''; + this.highlightedBoothId = booth.id; + this.selectedBooth = booth; + setTimeout(() => { + const el = document.getElementById('boothgroup-' + booth.id); + el?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); + }, 100); } - ngAfterViewChecked() { - if (this.scrolled === true) { - return; - } else { - document.getElementById("mapContainer").scrollLeft = 200; - this.scrolled = true; - } + onBoothTap(booth: BoothData) { + this.selectedBooth = booth; + this.highlightedBoothId = booth.id; } + // Mirror sponsors page slug logic so the popup can deep-link into the + // existing sponsor detail page. Booths without a sponsor match (community + // booths like SoCal Python) won't have a level set; the template hides the + // link in that case. + getSponsorSlug(name: string): string { + return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + } } diff --git a/src/app/providers/live-update.service.ts b/src/app/providers/live-update.service.ts index 11be3635..81fae028 100644 --- a/src/app/providers/live-update.service.ts +++ b/src/app/providers/live-update.service.ts @@ -11,6 +11,10 @@ export class LiveUpdateService { needsUpdate: boolean = false; build: string = "base"; appVersion: string = ""; + // Active live-update snapshot from Appflow. null when running the bundled + // native assets (no OTA applied yet, or running via livereload). + snapshot: { id: string; buildId: string } | null = null; + channel: string = ""; constructor(private loadingCtrl: LoadingController) { App.addListener('appStateChange', ({ isActive }) => { @@ -35,6 +39,8 @@ export class LiveUpdateService { await this.updateAppInfo(); const result = await LiveUpdates.sync(); this.updateAvailable = result; + this.snapshot = result.snapshot; + this.channel = result.liveUpdate?.channel || ''; if (this.updateAvailable.activeApplicationPathChanged) { this.needsUpdate = true; } diff --git a/src/assets/img/pycon-us-2026-floorplan-nologo.png b/src/assets/img/pycon-us-2026-floorplan-nologo.png new file mode 100644 index 00000000..f728cd10 Binary files /dev/null and b/src/assets/img/pycon-us-2026-floorplan-nologo.png differ