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
163 changes: 81 additions & 82 deletions src/app/pages/door-check/door-check.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,99 +7,98 @@
</ion-toolbar>
</ion-header>

<ion-content [style.visibility]="content_visibility">
<ion-list>
<ion-item *ngIf="show_permissions_error">
<ion-card>
<ion-card-header>
<ion-card-title>Uh oh..</ion-card-title>
<ion-card-subtitle>Camera Permissions Not Granted</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-text>Looks like you did not grant this application permission to access your camera.</ion-text>
<ion-text>
You can resolve this by opening<br><br>
<code *ngIf="ios">Settings > Privacy & Security > Camera > PyCon US 2026<br><br></code>
<code *ngIf="!ios">Settings > Apps > PyCon US 2026 > Permissions > Camera<br><br></code>
And enabling camera permssions for this app.
</ion-text>
</ion-card-content>
</ion-card>
</ion-item>
<ion-content [style.visibility]="scanning ? 'hidden' : ''">

<ion-item>
<ion-select [(ngModel)]="category" (ionChange)="refreshProducts()" interface="popover" [interfaceOptions]="{'cssClass': 'mycomponent-wider-popover'}" aria-label="categories" label="Category" placeholder="Category">
<ion-select-option *ngFor="let category of redeemable_categories" [value]="category.id">{{category.name}}</ion-select-option>
</ion-select>
</ion-item>
<ion-card *ngIf="show_permissions_error" color="warning" class="permission-card">
<ion-card-header>
<ion-card-title>Camera permission required</ion-card-title>
</ion-card-header>
<ion-card-content>
<p>Looks like you didn't grant the app permission to access your camera.</p>
<p>Re-enable it under:</p>
<p *ngIf="ios"><code>Settings &rsaquo; Privacy &amp; Security &rsaquo; Camera &rsaquo; PyCon US 2026</code></p>
<p *ngIf="!ios"><code>Settings &rsaquo; Apps &rsaquo; PyCon US 2026 &rsaquo; Permissions &rsaquo; Camera</code></p>
</ion-card-content>
</ion-card>

<ion-item *ngIf="!dirty && display_products">
<ion-searchbar [(ngModel)]="productSearch" (ionInput)="filterProductList()" placeholder="Search sessions..." debounce="150"></ion-searchbar>
</ion-item>
<section class="category-section">
<h2 class="section-title">Category</h2>
<p class="section-hint" *ngIf="!category">
Tap a category to choose a session.
</p>
<div class="category-chips" *ngIf="redeemable_categories?.length">
<ion-chip
*ngFor="let c of redeemable_categories"
[outline]="category !== c.id"
[color]="category === c.id ? 'primary' : 'medium'"
(click)="selectCategory(c.id)">
<ion-icon *ngIf="category === c.id" name="checkmark-outline"></ion-icon>
<ion-label>{{c.name}}</ion-label>
</ion-chip>
</div>
</section>

<ion-item *ngIf="!dirty && display_products" lines="none" class="ion-no-padding">
<ion-list class="product-list">
<ion-item button (click)="selectProduct('all')" [color]="product === 'all' ? 'primary' : ''">
<ion-label><strong>ALL</strong></ion-label>
<ion-icon *ngIf="product === 'all'" slot="end" name="checkmark"></ion-icon>
</ion-item>
<ion-item *ngFor="let p of filtered_products" button (click)="selectProduct(p.id)" [color]="product === p.id ? 'primary' : ''">
<ion-label class="ion-text-wrap">{{p.name}}</ion-label>
<ion-icon *ngIf="product === p.id" slot="end" name="checkmark"></ion-icon>
</ion-item>
</ion-list>
</ion-item>
<div *ngIf="display_products" class="search-row">
<ion-searchbar
[(ngModel)]="productSearch"
(ionInput)="filterProductList()"
placeholder="Search sessions..."
debounce="150">
</ion-searchbar>
</div>

<ion-item *ngIf="category || product">
<ion-card>
<ion-card-header>
<ion-card-title>Scanning for...</ion-card-title>
</ion-card-header>
<ion-card-content>
<h1>{{ getCategoryName(category) }}</h1>
<h2>{{ getProductName(product) }}</h2>
</ion-card-content>
</ion-card>
<ion-list *ngIf="display_products" lines="full" class="product-list">
<ion-item button detail="false" (click)="selectProduct('all')" [color]="product === 'all' ? 'primary' : ''">
<ion-label><strong>All sessions in this category</strong></ion-label>
<ion-icon *ngIf="product === 'all'" slot="end" name="checkmark"></ion-icon>
</ion-item>

<ion-item *ngIf="!product && category">
<ion-text>Select a session to start scanning!</ion-text>
<ion-item *ngFor="let p of filtered_products" button detail="false" (click)="selectProduct(p.id)" [color]="product === p.id ? 'primary' : ''">
<ion-label class="ion-text-wrap">{{p.name}}</ion-label>
<ion-icon *ngIf="product === p.id" slot="end" name="checkmark"></ion-icon>
</ion-item>

</ion-list>

<ion-card *ngIf="category && product" class="scanning-for-card">
<ion-card-content class="kicker-stack">
<span class="kicker">Scanning for</span>
<span class="primary">{{ getProductName(product) || 'All sessions' }}</span>
<span *ngIf="product !== 'all'" class="secondary">{{ getCategoryName(category) }}</span>
</ion-card-content>
</ion-card>

<p *ngIf="!product && category" class="empty-hint ion-text-center">
Select a session to start scanning.
</p>

</ion-content>

<ion-footer>
<ion-card *ngIf="last_scan">
<ion-item>
<ion-label>
<ion-grid>
<ion-row>
<ion-col>
<h1>
<ion-icon
[color]="(last_scan.status)? 'success' : 'danger'"
[name]="(last_scan.status)? 'checkmark-circle-outline' : 'alert-circle-outline'">
</ion-icon>
<ion-text *ngIf="last_scan.status">All good attendee has access!</ion-text>
<ion-text *ngIf="!last_scan.status"> Entry not permitted.</ion-text><br>
<ion-text color="tertiary" *ngIf="last_scan.status > 1">({{ last_scan.status }})</ion-text><br>
<ion-text><small><code>{{ last_scan.code }}</code></small></ion-text>
</h1>
</ion-col>
</ion-row>
</ion-grid>
</ion-label>
</ion-item>
<ion-card *ngIf="last_scan" class="last-scan-card" [color]="last_scan.status ? 'success' : 'danger'">
<ion-card-content>
<div class="last-scan-row">
<ion-icon
[name]="last_scan.status ? 'checkmark-circle-outline' : 'alert-circle-outline'"
aria-hidden="true">
</ion-icon>
<div class="last-scan-text">
<strong *ngIf="last_scan.status">Attendee has access</strong>
<strong *ngIf="!last_scan.status">Entry not permitted</strong>
<code>{{ last_scan.code }}</code>
</div>
</div>
</ion-card-content>
</ion-card>

<ion-toolbar class="footer-toolbar">
<ion-buttons class="footer-buttons">
<ion-button fill="solid" [disabled]="(product === null)" color="success" class="footer-button" (click)="startScan()" [style.visibility]="scan_start_button_visibility">
Start Scanner!
</ion-button>
<ion-button fill="solid" color="danger" class="footer-button" (click)="stopScan()" [style.visibility]="scan_stop_button_visibility">
Stop Scanner
</ion-button>
</ion-buttons>
<ion-button
expand="block"
size="large"
class="scan-toggle-button"
[disabled]="!scanning && product === null"
[color]="scanning ? 'danger' : 'success'"
(click)="scanning ? stopScan() : startScan()">
<ion-icon slot="start" [name]="scanning ? 'stop-circle-outline' : 'qr-code-outline'"></ion-icon>
{{ scanning ? 'Stop Scanner' : 'Start Scanner' }}
</ion-button>
</ion-toolbar>
</ion-footer>
128 changes: 109 additions & 19 deletions src/app/pages/door-check/door-check.page.scss
Original file line number Diff line number Diff line change
@@ -1,32 +1,122 @@
.footer-buttons {
.permission-card {
margin: 16px;
}

.category-section {
padding: 16px 16px 0;
}

.section-title {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ion-color-medium, #666);
margin: 0 0 8px;
}

.section-hint {
color: var(--ion-color-medium, #888);
font-size: 0.9rem;
margin: 0 0 8px;
}

.category-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;

ion-chip {
margin: 0;
height: 40px;
font-size: 0.95rem;
}
}

.search-row {
padding: 4px 8px 0;
}

.scanning-for-card {
margin: 16px;
}

.kicker-stack {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
gap: 4px;

.kicker {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ion-color-medium, #666);
}

.primary {
font-size: 1.2rem;
font-weight: 700;
color: var(--ion-text-color, #111);
line-height: 1.25;
}

.secondary {
font-size: 0.85rem;
color: var(--ion-color-medium, #888);
}
}

.footer-button {
position: absolute;
.empty-hint {
color: var(--ion-color-medium, #888);
margin: 12px 16px;
}

.product-list {
// Flat list scrolls with the page — no nested overflow.
background: transparent;
}

.footer-toolbar {
padding-top: 1em;
padding-bottom: 1em;
--padding-top: 8px;
--padding-bottom: 8px;
--padding-start: 16px;
--padding-end: 16px;
}

.header-info {
font-size: .6em;
padding-bottom: .5em;
.scan-toggle-button {
margin: 0;
height: 56px;
font-weight: 700;
letter-spacing: 0.02em;
}

::ng-deep .mycomponent-wider-popover
{
--width: 95%;
--max-width: 400px;
.last-scan-card {
margin: 8px 16px 0;
}

.product-list {
width: 100%;
max-height: 300px;
overflow-y: auto;
.last-scan-row {
display: flex;
align-items: center;
gap: 12px;

ion-icon {
font-size: 28px;
flex-shrink: 0;
}

.last-scan-text {
display: flex;
flex-direction: column;
gap: 2px;

strong {
font-size: 1rem;
}

code {
font-size: 0.75rem;
opacity: 0.85;
}
}
}
28 changes: 19 additions & 9 deletions src/app/pages/door-check/door-check.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import { LiveUpdateService } from '../../providers/live-update.service';
styleUrls: ['./door-check.page.scss'],
})
export class DoorCheckPage implements OnInit, OnDestroy {
content_visibility = 'show';
scan_start_button_visibility = 'show';
scan_stop_button_visibility = 'hidden';
scanning: boolean = false;
scan_presentation = [];
dirty: boolean = false;

Expand Down Expand Up @@ -71,6 +69,22 @@ export class DoorCheckPage implements OnInit, OnDestroy {
this.detectorRef.detectChanges();
}

selectCategory(categoryId: number) {
// Toggle off if the same chip is tapped again so users can clear
// their selection without leaving the page.
if (this.category === categoryId) {
this.category = null;
this.display_products = null;
this.filtered_products = null;
this.product = null;
this.productSearch = '';
this.detectorRef.detectChanges();
return;
}
this.category = categoryId;
this.refreshProducts();
}

filterProductList() {
if (!this.productSearch || !this.productSearch.trim()) {
this.filtered_products = this.display_products;
Expand Down Expand Up @@ -290,9 +304,7 @@ export class DoorCheckPage implements OnInit, OnDestroy {
return;
}
this.show_permissions_error = false;
this.content_visibility = 'hidden';
this.scan_start_button_visibility = 'hidden';
this.scan_stop_button_visibility = '';
this.scanning = true;
await this.addListeners();
BarcodeScanner.startScan({
formats: [BarcodeFormat.QrCode],
Expand All @@ -307,9 +319,7 @@ export class DoorCheckPage implements OnInit, OnDestroy {
clearTimeout(this.scan_timeout);
await BarcodeScanner.removeAllListeners();
await BarcodeScanner.stopScan()
this.scan_stop_button_visibility = 'hidden';
this.scan_start_button_visibility = '';
this.content_visibility = '';
this.scanning = false;
}

ionViewWillLeave() {
Expand Down
Loading
Loading