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
17 changes: 17 additions & 0 deletions src/app/pages/sponsor-detail/sponsor-detail-routing.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { SponsorDetailPage } from './sponsor-detail';

const routes: Routes = [
{
path: '',
component: SponsorDetailPage
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SponsorDetailPageRoutingModule { }
52 changes: 52 additions & 0 deletions src/app/pages/sponsor-detail/sponsor-detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<ion-header class="ion-no-border" translucent="false">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="backHref"></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

<ion-content class="sponsor-detail">
<div class="sponsor-hero">
<div class="sponsor-logo-container">
<img *ngIf="sponsor?.logo_url" class="sponsor-logo" [src]="sponsor?.logo_url" [alt]="sponsor?.name + ' logo'" />
</div>
</div>

<div class="ion-padding-horizontal sponsor-name">
<h1>{{ sponsor?.name }}</h1>
<span *ngIf="sponsor?.level" class="level-badge">{{ sponsor?.level }}</span>
</div>

<div *ngIf="sponsor?.booth_number" class="ion-padding-horizontal sponsor-booth">
<ion-icon name="location-outline"></ion-icon>
<span>Expo Hall Booth {{ sponsor?.booth_number }}</span>
</div>

<div *ngIf="sponsor?.description" class="ion-padding-horizontal sponsor-description">
<p [innerHtml]="sponsor?.description"></p>
</div>

<div *ngIf="sponsor?.external_url" class="ion-padding-horizontal sponsor-website">
<ion-button expand="block" fill="outline" (click)="openUrl(sponsor.external_url)">
<ion-icon name="globe-outline" slot="start"></ion-icon>
Visit Website
</ion-button>
</div>

<div *ngIf="jobListings.length > 0" class="ion-padding-horizontal sponsor-jobs">
<h2>Open Positions</h2>
<div *ngFor="let listing of jobListings" class="listing-card">
<ion-list *ngIf="listing.roles?.length > 0" class="roles-list" lines="none">
<ion-item *ngFor="let role of listing.roles" class="role-item" [button]="!!role.url" (click)="openUrl(role.url)" [detail]="!!role.url">
<ion-icon name="briefcase-outline" slot="start" color="medium" class="role-icon"></ion-icon>
<ion-label class="role-label">
<span class="role-title">{{ role.title }}</span>
</ion-label>
</ion-item>
</ion-list>
</div>
</div>

<div style="height: 80px"></div>
</ion-content>
18 changes: 18 additions & 0 deletions src/app/pages/sponsor-detail/sponsor-detail.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { SponsorDetailPage } from './sponsor-detail';
import { SponsorDetailPageRoutingModule } from './sponsor-detail-routing.module';
import { IonicModule } from '@ionic/angular';

@NgModule({
imports: [
CommonModule,
IonicModule,
SponsorDetailPageRoutingModule,
],
declarations: [
SponsorDetailPage,
]
})
export class SponsorDetailModule { }
150 changes: 150 additions & 0 deletions src/app/pages/sponsor-detail/sponsor-detail.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Sponsor Hero
*/

ion-header {
background: #3B3EA9;

&::after {
display: none;
}
}

ion-toolbar {
--background: transparent;
--border-color: transparent;
}

ion-toolbar ion-button,
ion-toolbar ion-back-button,
ion-toolbar ion-menu-button {
--color: #ffffff;
}

.sponsor-hero {
background: linear-gradient(180deg, #3B3EA9 23.5%, #101136 53.29%);
display: flex;
align-items: center;
justify-content: center;
padding: 24px 32px 40px;
}

.sponsor-logo-container {
background: #ffffff;
border-radius: 16px;
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: center;
max-width: 280px;
width: 100%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}

.sponsor-logo {
max-width: 100%;
max-height: 120px;
object-fit: contain;
}

/*
* Sponsor Info
*/

.sponsor-name {
padding-top: 24px;
text-align: center;

h1 {
margin: 0 0 8px;
font-size: 1.6rem;
font-weight: 700;
}
}

.level-badge {
display: inline-block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 3px 12px;
border-radius: 4px;
background: var(--ion-color-step-100, rgba(255, 255, 255, 0.1));
color: var(--ion-color-step-600, rgba(255, 255, 255, 0.6));
font-weight: 600;
}

.sponsor-booth {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding-top: 16px;
font-size: 0.9rem;
opacity: 0.7;

ion-icon {
font-size: 1.1rem;
}
}

.sponsor-description {
padding-top: 20px;

p {
font-size: 0.95rem;
line-height: 1.6;
margin: 0;
}
}

.sponsor-website {
padding-top: 24px;

ion-button {
--border-radius: 10px;
--border-color: var(--ion-color-primary);
--color: var(--ion-color-primary);
font-weight: 600;
letter-spacing: 0.3px;
}
}

/*
* Job Listings
*/

.sponsor-jobs {
padding-top: 32px;

h2 {
font-size: 1.15rem;
font-weight: 700;
margin: 0 0 12px;
}
}

.listing-card {
padding: 0;
}

.roles-list {
background: transparent;
padding: 0;

ion-item {
--background: transparent;
--padding-start: 0;
--padding-end: 0;
--min-height: 44px;
}
}

.role-icon {
font-size: 16px;
margin-right: 8px;
}

.role-title {
font-size: 0.9rem;
}
112 changes: 112 additions & 0 deletions src/app/pages/sponsor-detail/sponsor-detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ConferenceData } from '../../providers/conference-data';
import { LiveUpdateService } from '../../providers/live-update.service';

@Component({
selector: 'page-sponsor-detail',
templateUrl: 'sponsor-detail.html',
styleUrls: ['./sponsor-detail.scss'],
})
export class SponsorDetailPage {
sponsor: any;
jobListings: any[] = [];
backHref = '';

constructor(
private dataProvider: ConferenceData,
private route: ActivatedRoute,
public liveUpdateService: LiveUpdateService,
) {}

ionViewDidEnter() {
this.backHref = this.route.snapshot.queryParamMap.get('prevUrl') || '/app/tabs/sponsors';
}

ionViewWillEnter() {
const sponsorId = this.route.snapshot.paramMap.get('sponsorId');

this.dataProvider.load().subscribe((data: any) => {
if (data && data.conference && data.conference.sponsors) {
for (const [level, sponsors] of Object.entries(data.conference.sponsors)) {
for (const [index, sponsor] of Object.entries(sponsors as any[])) {
if (sponsor && String(sponsor.name).toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') === sponsorId) {
this.sponsor = sponsor;
break;
}
}
if (this.sponsor) break;
}
}

// Find job listings associated with this sponsor
const listings = data['job-listings'] || [];
if (this.sponsor) {
this.jobListings = listings.filter(
(listing: any) => listing.sponsor_name === this.sponsor.name
).map((listing: any) => ({
...listing,
roles: this.parseRoles(listing.description_html),
}));
}
});
}

parseRoles(html: string): {title: string, url: string}[] {
if (!html) return [];
const text = html.replace(/<[^>]+>/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/\s+/g, ' ').trim();
const urlRegex = /https?:\/\/[^\s<>"]+/g;
const urls = html.match(urlRegex) || [];
if (urls.length === 0) {
return text.length > 0 ? [{title: text, url: ''}] : [];
}
const roles: {title: string, url: string}[] = [];
let remaining = text;
for (const url of urls) {
const idx = remaining.indexOf(url);
if (idx >= 0) {
const before = remaining.substring(0, idx).replace(/[-|–,]\s*$/, '').trim();
if (before.length > 0) {
const lines = before.split(/\n/).filter(l => l.trim());
const title = lines[lines.length - 1].replace(/^[-|–]\s*/, '').trim();
if (title.length > 0 && title.length < 200) {
roles.push({title, url});
} else {
roles.push({title: this.shortenUrl(url), url});
}
} else {
roles.push({title: this.shortenUrl(url), url});
}
remaining = remaining.substring(idx + url.length).replace(/^\s*[-|–,]\s*/, '').trim();
}
}
if (roles.length === 0 && text.length > 0) {
return [{title: text, url: ''}];
}
return roles;
}

shortenUrl(url: string): string {
try {
const u = new URL(url);
const path = u.pathname.replace(/\/$/, '');
const parts = path.split('/').filter(p => p);
if (parts.length > 0) {
return parts[parts.length - 1].replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase());
}
return u.hostname;
} catch {
return url;
}
}

openUrl(url: string) {
if (url) {
window.open(url, '_system', 'location=yes');
}
}

getSponsorSlug(name: string): string {
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
}
}
2 changes: 1 addition & 1 deletion src/app/pages/sponsors/sponsors.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<ion-grid>
<ion-row *ngFor="let sponsorLevel of sponsors | keyvalue : levelOrder">
<ion-col size-xl="3" size-md="4" size-sm="6" size-xs="12" *ngFor="let sponsor of sponsorLevel.value" [hidden]="sponsor.hide">
<ion-card>
<ion-card button="true" [routerLink]="'/app/tabs/sponsors/sponsor-detail/' + getSponsorSlug(sponsor.name)">
<div style="text-align: center; width: 100%; padding-top: 2em;">
<img style="min-width: 128px; max-width: 33%; max-height: 33%;" [src]="sponsor.logo_url" [alt]="sponsor.name + ' logo'" />
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/app/pages/sponsors/sponsors.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export class SponsorsPage implements OnInit {
});
}

getSponsorSlug(name: string): string {
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
}

ngOnInit() {
this.reloadSponsors();
}
Expand Down
4 changes: 4 additions & 0 deletions src/app/pages/tabs-page/tabs-page-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const routes: Routes = [
{
path: '',
loadChildren: () => import('../sponsors/sponsors.module').then(m => m.SponsorsPageModule)
},
{
path: 'sponsor-detail/:sponsorId',
loadChildren: () => import('../sponsor-detail/sponsor-detail.module').then(m => m.SponsorDetailModule)
}
]
},
Expand Down
Loading