[examples][frameworks] Add missing examples and add a test to ensure every framework has an example (#4372)

* Add test to frameworks

* Add example for Docusaurus v2

* Add example for ionic-angular

* Update READMEs

* Use existing versions

* Reset yarn.lock

* Add schema validation and add missing Scully.io logo
This commit is contained in:
Andy
2020-05-14 13:30:11 +02:00
committed by GitHub
parent 3c75f1440d
commit ef63247fc0
232 changed files with 16033 additions and 2 deletions

View File

@@ -0,0 +1,42 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CheckTutorial } from './providers/check-tutorial.service';
const routes: Routes = [
{
path: '',
redirectTo: '/tutorial',
pathMatch: 'full'
},
{
path: 'account',
loadChildren: () => import('./pages/account/account.module').then(m => m.AccountModule)
},
{
path: 'support',
loadChildren: () => import('./pages/support/support.module').then(m => m.SupportModule)
},
{
path: 'login',
loadChildren: () => import('./pages/login/login.module').then(m => m.LoginModule)
},
{
path: 'signup',
loadChildren: () => import('./pages/signup/signup.module').then(m => m.SignUpModule)
},
{
path: 'app',
loadChildren: () => import('./pages/tabs-page/tabs-page.module').then(m => m.TabsModule)
},
{
path: 'tutorial',
loadChildren: () => import('./pages/tutorial/tutorial.module').then(m => m.TutorialModule),
canLoad: [CheckTutorial]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,114 @@
<ion-app [class.dark-theme]="dark">
<ion-split-pane contentId="main-content">
<ion-menu contentId="main-content">
<ion-content>
<ion-list lines="none">
<ion-list-header>
Conference
</ion-list-header>
<ion-menu-toggle autoHide="false" *ngFor="let p of appPages; let i = index">
<ion-item [routerLink]="p.url" routerLinkActive="selected" routerDirection="root" detail="false">
<ion-icon slot="start" [name]="p.icon + '-outline'"></ion-icon>
<ion-label>
{{p.title}}
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list *ngIf="loggedIn" lines="none">
<ion-list-header>
Account
</ion-list-header>
<ion-menu-toggle autoHide="false">
<ion-item routerLink="/account" routerLinkActive="active" routerDirection="root" detail="false">
<ion-icon slot="start" name="person"></ion-icon>
<ion-label>
Account
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle autoHide="false">
<ion-item routerLink="/support" routerLinkActive="active" routerDirection="root" detail="false">
<ion-icon slot="start" name="help"></ion-icon>
<ion-label>
Support
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle autoHide="false">
<ion-item button (click)="logout()" detail="false">
<ion-icon slot="start" name="log-out"></ion-icon>
<ion-label>
Logout
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list *ngIf="!loggedIn" lines="none">
<ion-list-header>
Account
</ion-list-header>
<ion-menu-toggle autoHide="false">
<ion-item routerLink="/login" routerLinkActive="active" routerDirection="root" detail="false">
<ion-icon slot="start" name="log-in"></ion-icon>
<ion-label>
Login
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle autoHide="false">
<ion-item routerLink="/support" routerLinkActive="active" routerDirection="root" detail="false">
<ion-icon slot="start" name="help"></ion-icon>
<ion-label>
Support
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle autoHide="false">
<ion-item routerLink="/signup" routerLinkActive="active" routerDirection="root" detail="false">
<ion-icon slot="start" name="person-add"></ion-icon>
<ion-label>
Signup
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-item>
<ion-icon slot="start" name="moon-outline"></ion-icon>
<ion-label>
Dark Mode
</ion-label>
<ion-toggle [(ngModel)]="dark"></ion-toggle>
</ion-item>
</ion-list>
<ion-list lines="none">
<ion-list-header>
Tutorial
</ion-list-header>
<ion-menu-toggle autoHide="false">
<ion-item button (click)="openTutorial()" detail="false">
<ion-icon slot="start" name="hammer"></ion-icon>
<ion-label>Show Tutorial</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
</ion-app>

View File

@@ -0,0 +1,87 @@
ion-menu ion-content {
--padding-top: 20px;
--padding-bottom: 20px;
--background: var(--ion-item-background, var(--ion-background-color, #fff));
}
/* Remove background transitions for switching themes */
ion-menu ion-item {
--transition: none;
}
ion-item.selected {
--color: var(--ion-color-primary);
}
/*
* Material Design Menu
*/
ion-menu.md ion-list {
padding: 20px 0;
}
ion-menu.md ion-list-header {
padding-left: 18px;
padding-right: 18px;
text-transform: uppercase;
letter-spacing: .1em;
font-weight: 450;
}
ion-menu.md ion-item {
--padding-start: 18px;
margin-right: 10px;
border-radius: 0 50px 50px 0;
font-weight: 500;
}
ion-menu.md ion-item.selected {
--background: rgba(var(--ion-color-primary-rgb), 0.14);
}
ion-menu.md ion-item.selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.md ion-list-header,
ion-menu.md ion-item ion-icon {
color: var(--ion-color-step-650, #5f6368);
}
ion-menu.md ion-list:not(:last-of-type) {
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}
/*
* iOS Menu
*/
ion-menu.ios ion-list-header {
padding-left: 16px;
padding-right: 16px;
margin-bottom: 8px;
}
ion-menu.ios ion-list {
padding: 20px 0 0;
}
ion-menu.ios ion-item {
--padding-start: 16px;
--min-height: 50px;
}
ion-menu.ios ion-item ion-icon {
font-size: 24px;
color: #73849a;
}
ion-menu.ios ion-item.selected ion-icon {
color: var(--ion-color-primary);
}

View File

@@ -0,0 +1,65 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router';
import { SwUpdate } from '@angular/service-worker';
import { TestBed, async } from '@angular/core/testing';
import { MenuController, Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { IonicStorageModule } from '@ionic/storage';
import { AppComponent } from './app.component';
import { UserData } from './providers/user-data';
describe('AppComponent', () => {
let menuSpy,
routerSpy,
userDataSpy,
statusBarSpy,
splashScreenSpy,
swUpdateSpy,
platformReadySpy,
platformSpy,
app,
fixture;
beforeEach(async(() => {
menuSpy = jasmine.createSpyObj('MenuController', ['toggle', 'enable']);
routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
userDataSpy = jasmine.createSpyObj('UserData', ['isLoggedIn', 'logout']);
statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
swUpdateSpy = jasmine.createSpyObj('SwUpdate', ['available', 'activateUpdate']);
platformReadySpy = Promise.resolve();
platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });
TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [IonicStorageModule.forRoot()],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: MenuController, useValue: menuSpy },
{ provide: Router, useValue: routerSpy },
{ provide: UserData, useValue: userDataSpy },
{ provide: StatusBar, useValue: statusBarSpy },
{ provide: SplashScreen, useValue: splashScreenSpy },
{ provide: SwUpdate, useValue: swUpdateSpy },
{ provide: Platform, useValue: platformSpy }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
app = fixture.debugElement.componentInstance;
});
it('should create the app', () => {
expect(app).toBeTruthy();
});
it('should initialize the app', async () => {
expect(platformSpy.ready).toHaveBeenCalled();
await platformReadySpy;
expect(statusBarSpy.styleDefault).toHaveBeenCalled();
expect(splashScreenSpy.hide).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,129 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Router } from '@angular/router';
import { SwUpdate } from '@angular/service-worker';
import { MenuController, Platform, ToastController } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { Storage } from '@ionic/storage';
import { UserData } from './providers/user-data';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
appPages = [
{
title: 'Schedule',
url: '/app/tabs/schedule',
icon: 'calendar'
},
{
title: 'Speakers',
url: '/app/tabs/speakers',
icon: 'people'
},
{
title: 'Map',
url: '/app/tabs/map',
icon: 'map'
},
{
title: 'About',
url: '/app/tabs/about',
icon: 'information-circle'
}
];
loggedIn = false;
dark = false;
constructor(
private menu: MenuController,
private platform: Platform,
private router: Router,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
private storage: Storage,
private userData: UserData,
private swUpdate: SwUpdate,
private toastCtrl: ToastController,
) {
this.initializeApp();
}
async ngOnInit() {
this.checkLoginStatus();
this.listenForLoginEvents();
this.swUpdate.available.subscribe(async res => {
const toast = await this.toastCtrl.create({
message: 'Update available!',
position: 'bottom',
buttons: [
{
role: 'cancel',
text: 'Reload'
}
]
});
await toast.present();
toast
.onDidDismiss()
.then(() => this.swUpdate.activateUpdate())
.then(() => window.location.reload());
});
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
});
}
checkLoginStatus() {
return this.userData.isLoggedIn().then(loggedIn => {
return this.updateLoggedInStatus(loggedIn);
});
}
updateLoggedInStatus(loggedIn: boolean) {
setTimeout(() => {
this.loggedIn = loggedIn;
}, 300);
}
listenForLoginEvents() {
window.addEventListener('user:login', () => {
this.updateLoggedInStatus(true);
});
window.addEventListener('user:signup', () => {
this.updateLoggedInStatus(true);
});
window.addEventListener('user:logout', () => {
this.updateLoggedInStatus(false);
});
}
logout() {
this.userData.logout().then(() => {
return this.router.navigateByUrl('/app/tabs/schedule');
});
}
openTutorial() {
this.menu.enable(false);
this.storage.set('ion_did_tutorial', false);
this.router.navigateByUrl('/tutorial');
}
}

View File

@@ -0,0 +1,32 @@
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { IonicModule } from '@ionic/angular';
import { IonicStorageModule } from '@ionic/storage';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
FormsModule,
IonicModule.forRoot(),
IonicStorageModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production
})
],
declarations: [AppComponent],
providers: [InAppBrowser, SplashScreen, StatusBar],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

View File

@@ -0,0 +1,5 @@
export interface UserOptions {
username: string;
password: string;
}

View File

@@ -0,0 +1,38 @@
import { Component } from '@angular/core';
import { PopoverController } from '@ionic/angular';
@Component({
template: `
<ion-list>
<ion-item button (click)="close('https://ionicframework.com/getting-started')">
<ion-label>Learn Ionic</ion-label>
</ion-item>
<ion-item button (click)="close('https://ionicframework.com/docs/')">
<ion-label>Documentation</ion-label>
</ion-item>
<ion-item button (click)="close('https://showcase.ionicframework.com')">
<ion-label>Showcase</ion-label>
</ion-item>
<ion-item button (click)="close('https://github.com/ionic-team/ionic')">
<ion-label>GitHub Repo</ion-label>
</ion-item>
<ion-item button (click)="support()">
<ion-label>Support</ion-label>
</ion-item>
</ion-list>
`
})
export class PopoverPage {
constructor(public popoverCtrl: PopoverController) {}
support() {
// this.app.getRootNavs()[0].push('/support');
this.popoverCtrl.dismiss();
}
close(url: string) {
window.open(url, '_blank');
this.popoverCtrl.dismiss();
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutPage } from './about';
const routes: Routes = [
{
path: '',
component: AboutPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AboutPageRoutingModule { }

View File

@@ -0,0 +1,77 @@
<ion-content>
<ion-header class="ion-no-border">
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-buttons slot="end">
<ion-button (click)="presentPopover($event)">
<ion-icon slot="icon-only" ios="ellipsis-horizontal" md="ellipsis-vertical"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<div class="about-header">
<!-- Instead of loading an image each time the select changes, use opacity to transition them -->
<div class="about-image madison" [ngStyle]="location === 'madison' && {'opacity': '1'}"></div>
<div class="about-image austin" [ngStyle]="location === 'austin' && {'opacity': '1'}"></div>
<div class="about-image chicago" [ngStyle]="location === 'chicago' && {'opacity': '1'}"></div>
<div class="about-image seattle" [ngStyle]="location === 'seattle' && {'opacity': '1'}"></div>
</div>
<div class="about-info">
<h3 class="ion-padding-top ion-padding-start">About</h3>
<p class="ion-padding-start ion-padding-end">
The Ionic Conference is a one-day conference on {{ conferenceDate | date: 'mediumDate' }} featuring talks from the Ionic team. It is focused on Ionic applications being built with Ionic Framework. This includes migrating apps to the latest version of the framework, Angular concepts, Webpack, Sass, and many other technologies used in Ionic 2. Tickets are completely sold out, and were expecting more than 1000 developers making this the largest Ionic conference ever!
</p>
<h3 class="ion-padding-top ion-padding-start">Details</h3>
<ion-list lines="none">
<ion-item>
<ion-label>
Location
</ion-label>
<ion-select [(ngModel)]="location" [interfaceOptions]="selectOptions">
<ion-select-option value="madison">Madison, WI</ion-select-option>
<ion-select-option value="austin">Austin, TX</ion-select-option>
<ion-select-option value="chicago">Chicago, IL</ion-select-option>
<ion-select-option value="seattle">Seattle, WA</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>
Date
</ion-label>
<ion-datetime
displayFormat="MMM DD, YYYY"
max="2056"
[(ngModel)]="conferenceDate">
</ion-datetime>
</ion-item>
</ion-list>
<h3 class="ion-padding-top ion-padding-start">Internet</h3>
<ion-list lines="none">
<ion-item>
<ion-label>
Wifi network
</ion-label>
<ion-label class="ion-text-end">
ica{{conferenceDate | date: 'y'}}
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Password
</ion-label>
<ion-label class="ion-text-end">
makegoodthings
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { AboutPage } from './about';
import { PopoverPage } from '../about-popover/about-popover';
import { AboutPageRoutingModule } from './about-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
AboutPageRoutingModule
],
declarations: [AboutPage, PopoverPage],
entryComponents: [PopoverPage],
bootstrap: [AboutPage],
})
export class AboutModule {}

View File

@@ -0,0 +1,90 @@
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-button,
ion-toolbar ion-back-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 30%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url(/assets/img/about/madison.jpg);
}
.about-header .austin {
background-image: url(/assets/img/about/austin.jpg);
}
.about-header .chicago {
background-image: url(/assets/img/about/chicago.jpg);
}
.about-header .seattle {
background-image: url(/assets/img/about/seattle.jpg);
}
.about-info {
position: absolute;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}

View File

@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { PopoverController } from '@ionic/angular';
import { PopoverPage } from '../about-popover/about-popover';
@Component({
selector: 'page-about',
templateUrl: 'about.html',
styleUrls: ['./about.scss'],
})
export class AboutPage {
location = 'madison';
conferenceDate = '2047-05-17';
selectOptions = {
header: 'Select a Location'
};
constructor(public popoverCtrl: PopoverController) { }
async presentPopover(event: Event) {
const popover = await this.popoverCtrl.create({
component: PopoverPage,
event
});
await popover.present();
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AccountPage } from './account';
const routes: Routes = [
{
path: '',
component: AccountPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AccountPageRoutingModule { }

View File

@@ -0,0 +1,23 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div *ngIf="username" class="ion-padding-top ion-text-center">
<img src="https://www.gravatar.com/avatar?d=mm&s=140" alt="avatar">
<h2>{{username}}</h2>
<ion-list inset>
<ion-item (click)="updatePicture()">Update Picture</ion-item>
<ion-item (click)="changeUsername()">Change Username</ion-item>
<ion-item (click)="changePassword()">Change Password</ion-item>
<ion-item (click)="support()">Support</ion-item>
<ion-item (click)="logout()">Logout</ion-item>
</ion-list>
</div>
</ion-content>

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { AccountPage } from './account';
import { AccountPageRoutingModule } from './account-routing.module';
@NgModule({
imports: [
CommonModule,
IonicModule,
AccountPageRoutingModule
],
declarations: [
AccountPage,
]
})
export class AccountModule { }

View File

@@ -0,0 +1,4 @@
img {
max-width: 140px;
border-radius: 50%;
}

View File

@@ -0,0 +1,77 @@
import { AfterViewInit, Component } from '@angular/core';
import { Router } from '@angular/router';
import { AlertController } from '@ionic/angular';
import { UserData } from '../../providers/user-data';
@Component({
selector: 'page-account',
templateUrl: 'account.html',
styleUrls: ['./account.scss'],
})
export class AccountPage implements AfterViewInit {
username: string;
constructor(
public alertCtrl: AlertController,
public router: Router,
public userData: UserData
) { }
ngAfterViewInit() {
this.getUsername();
}
updatePicture() {
console.log('Clicked to update picture');
}
// Present an alert with the current username populated
// clicking OK will update the username and display it
// clicking Cancel will close the alert and do nothing
async changeUsername() {
const alert = await this.alertCtrl.create({
header: 'Change Username',
buttons: [
'Cancel',
{
text: 'Ok',
handler: (data: any) => {
this.userData.setUsername(data.username);
this.getUsername();
}
}
],
inputs: [
{
type: 'text',
name: 'username',
value: this.username,
placeholder: 'username'
}
]
});
await alert.present();
}
getUsername() {
this.userData.getUsername().then((username) => {
this.username = username;
});
}
changePassword() {
console.log('Clicked to change password');
}
logout() {
this.userData.logout();
this.router.navigateByUrl('/login');
}
support() {
this.router.navigateByUrl('/support');
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginPage } from './login';
const routes: Routes = [
{
path: '',
component: LoginPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LoginPageRoutingModule { }

View File

@@ -0,0 +1,54 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo">
</div>
<form #loginForm="ngForm" novalidate>
<ion-list>
<ion-item>
<ion-label position="stacked" color="primary">Username</ion-label>
<ion-input [(ngModel)]="login.username" name="username" type="text" #username="ngModel" spellcheck="false" autocapitalize="off"
required>
</ion-input>
</ion-item>
<ion-text color="danger">
<p [hidden]="username.valid || submitted == false" class="ion-padding-start">
Username is required
</p>
</ion-text>
<ion-item>
<ion-label position="stacked" color="primary">Password</ion-label>
<ion-input [(ngModel)]="login.password" name="password" type="password" #password="ngModel" required>
</ion-input>
</ion-item>
<ion-text color="danger">
<p [hidden]="password.valid || submitted == false" class="ion-padding-start">
Password is required
</p>
</ion-text>
</ion-list>
<ion-row>
<ion-col>
<ion-button (click)="onLogin(loginForm)" type="submit" expand="block">Login</ion-button>
</ion-col>
<ion-col>
<ion-button (click)="onSignup()" color="light" expand="block">Signup</ion-button>
</ion-col>
</ion-row>
</form>
</ion-content>

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { LoginPage } from './login';
import { LoginPageRoutingModule } from './login-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
LoginPageRoutingModule
],
declarations: [
LoginPage,
]
})
export class LoginModule { }

View File

@@ -0,0 +1,13 @@
.login-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.login-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}

View File

@@ -0,0 +1,37 @@
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { UserData } from '../../providers/user-data';
import { UserOptions } from '../../interfaces/user-options';
@Component({
selector: 'page-login',
templateUrl: 'login.html',
styleUrls: ['./login.scss'],
})
export class LoginPage {
login: UserOptions = { username: '', password: '' };
submitted = false;
constructor(
public userData: UserData,
public router: Router
) { }
onLogin(form: NgForm) {
this.submitted = true;
if (form.valid) {
this.userData.login(this.login.username);
this.router.navigateByUrl('/app/tabs/schedule');
}
}
onSignup() {
this.router.navigateByUrl('/signup');
}
}

View File

@@ -0,0 +1,161 @@
export const darkStyle = [
{
"elementType": "geometry",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#263c3f"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#6b9a76"
}
]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9ca5b3"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#1f2835"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#f3d19c"
}
]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [
{
"color": "#2f3948"
}
]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#17263c"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#515c6d"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#17263c"
}
]
}
]

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MapPage } from './map';
const routes: Routes = [
{
path: '',
component: MapPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MapPageRoutingModule { }

View File

@@ -0,0 +1,12 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Map</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div #mapCanvas class="map-canvas"></div>
</ion-content>

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { MapPage } from './map';
import { MapPageRoutingModule } from './map-routing.module';
@NgModule({
imports: [
CommonModule,
IonicModule,
MapPageRoutingModule
],
declarations: [
MapPage,
]
})
export class MapModule { }

View File

@@ -0,0 +1,15 @@
.map-canvas {
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
opacity: 0;
transition: opacity 150ms ease-in;
}
.show-map {
opacity: 1;
}

View File

@@ -0,0 +1,107 @@
import { Component, ElementRef, Inject, ViewChild, AfterViewInit } from '@angular/core';
import { ConferenceData } from '../../providers/conference-data';
import { Platform } from '@ionic/angular';
import { DOCUMENT} from '@angular/common';
import { darkStyle } from './map-dark-style';
@Component({
selector: 'page-map',
templateUrl: 'map.html',
styleUrls: ['./map.scss']
})
export class MapPage implements AfterViewInit {
@ViewChild('mapCanvas', { static: true }) mapElement: ElementRef;
constructor(
@Inject(DOCUMENT) private doc: Document,
public confData: ConferenceData,
public platform: Platform) {}
async ngAfterViewInit() {
const appEl = this.doc.querySelector('ion-app');
let isDark = false;
let style = [];
if (appEl.classList.contains('dark-theme')) {
style = darkStyle;
}
const googleMaps = await getGoogleMaps(
'AIzaSyB8pf6ZdFQj5qw7rc_HSGrhUwQKfIe9ICw'
);
let map;
this.confData.getMap().subscribe((mapData: any) => {
const mapEle = this.mapElement.nativeElement;
map = new googleMaps.Map(mapEle, {
center: mapData.find((d: any) => d.center),
zoom: 16,
styles: style
});
mapData.forEach((markerData: any) => {
const infoWindow = new googleMaps.InfoWindow({
content: `<h5>${markerData.name}</h5>`
});
const marker = new googleMaps.Marker({
position: markerData,
map,
title: markerData.name
});
marker.addListener('click', () => {
infoWindow.open(map, marker);
});
});
googleMaps.event.addListenerOnce(map, 'idle', () => {
mapEle.classList.add('show-map');
});
});
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
const el = mutation.target as HTMLElement;
isDark = el.classList.contains('dark-theme');
if (map && isDark) {
map.setOptions({styles: darkStyle});
} else if (map) {
map.setOptions({styles: []});
}
}
});
});
observer.observe(appEl, {
attributes: true
});
}
}
function getGoogleMaps(apiKey: string): Promise<any> {
const win = window as any;
const googleModule = win.google;
if (googleModule && googleModule.maps) {
return Promise.resolve(googleModule.maps);
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&v=3.31`;
script.async = true;
script.defer = true;
document.body.appendChild(script);
script.onload = () => {
const googleModule2 = win.google;
if (googleModule2 && googleModule2.maps) {
resolve(googleModule2.maps);
} else {
reject('google maps not available');
}
};
});
}

View File

@@ -0,0 +1,41 @@
<ion-header translucent="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button *ngIf="ios" (click)="dismiss()">Cancel</ion-button>
<ion-button *ngIf="!ios" (click)="selectAll(false)">Reset</ion-button>
</ion-buttons>
<ion-title>
Filter Sessions
</ion-title>
<ion-buttons slot="end">
<ion-button (click)="applyFilters()" strong>Done</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list [lines]="ios ? 'inset' : 'full'">
<ion-list-header>Tracks</ion-list-header>
<ion-item *ngFor="let track of tracks" [attr.track]="track.name | lowercase">
<ion-icon *ngIf="ios" slot="start" [name]="track.icon" color="medium"></ion-icon>
<ion-label>{{track.name}}</ion-label>
<ion-checkbox [(ngModel)]="track.isChecked"></ion-checkbox>
</ion-item>
</ion-list>
</ion-content>
<ion-footer translucent="true" *ngIf="ios">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="selectAll(false)">Deselect All</ion-button>
</ion-buttons>
<ion-buttons slot="end">
<ion-button (click)="selectAll(true)">Select All</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,32 @@
/*
* Material Design
*/
.md ion-toolbar ion-button {
text-transform: capitalize;
letter-spacing: 0;
}
.md ion-checkbox {
--background-checked: transparent;
--border-color: transparent;
--border-color-checked: transparent;
--checkmark-color: var(--ion-color-primary);
}
.md ion-list {
background: inherit;
}
/*
* iOS
*/
.ios ion-list-header {
margin-top: 10px;
}
.ios ion-label {
color: var(--ion-color-primary);
}

View File

@@ -0,0 +1,59 @@
import { Component } from '@angular/core';
import { Config, ModalController, NavParams } from '@ionic/angular';
import { ConferenceData } from '../../providers/conference-data';
@Component({
selector: 'page-schedule-filter',
templateUrl: 'schedule-filter.html',
styleUrls: ['./schedule-filter.scss'],
})
export class ScheduleFilterPage {
ios: boolean;
tracks: {name: string, icon: string, isChecked: boolean}[] = [];
constructor(
public confData: ConferenceData,
private config: Config,
public modalCtrl: ModalController,
public navParams: NavParams
) { }
ionViewWillEnter() {
this.ios = this.config.get('mode') === `ios`;
// passed in array of track names that should be excluded (unchecked)
const excludedTrackNames = this.navParams.get('excludedTracks');
this.confData.getTracks().subscribe((tracks: any[]) => {
tracks.forEach(track => {
this.tracks.push({
name: track.name,
icon: track.icon,
isChecked: (excludedTrackNames.indexOf(track.name) === -1)
});
});
});
}
selectAll(check: boolean) {
// set all to checked or unchecked
this.tracks.forEach(track => {
track.isChecked = check;
});
}
applyFilters() {
// Pass back a new array of track names to exclude
const excludedTrackNames = this.tracks.filter(c => !c.isChecked).map(c => c.name);
this.dismiss(excludedTrackNames);
}
dismiss(data?: any) {
// using the injected ModalController this page
// can "dismiss" itself and pass back data
this.modalCtrl.dismiss(data);
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SchedulePage } from './schedule';
const routes: Routes = [
{
path: '',
component: SchedulePage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SchedulePageRoutingModule { }

View File

@@ -0,0 +1,104 @@
<ion-header translucent="true">
<ion-toolbar>
<ion-buttons *ngIf="!showSearchbar" slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-segment *ngIf="ios" [(ngModel)]="segment" (ionChange)="updateSchedule()">
<ion-segment-button value="all">
All
</ion-segment-button>
<ion-segment-button value="favorites">
Favorites
</ion-segment-button>
</ion-segment>
<ion-title *ngIf="!ios && !showSearchbar">Schedule</ion-title>
<ion-searchbar *ngIf="showSearchbar" showCancelButton="always" [(ngModel)]="queryText" (ionChange)="updateSchedule()" (ionCancel)="showSearchbar = false" placeholder="Search"></ion-searchbar>
<ion-buttons slot="end">
<ion-button *ngIf="!ios && !showSearchbar" (click)="showSearchbar = true">
<ion-icon slot="icon-only" name="search"></ion-icon>
</ion-button>
<ion-button *ngIf="!showSearchbar" (click)="presentFilter()">
<span *ngIf="ios">Filter</span>
<span *ngIf="!ios">
<ion-icon slot="icon-only" name="options"></ion-icon>
</span>
</ion-button>
</ion-buttons>
</ion-toolbar>
<ion-toolbar *ngIf="!ios">
<ion-segment [(ngModel)]="segment" (ionChange)="updateSchedule()">
<ion-segment-button value="all">
All
</ion-segment-button>
<ion-segment-button value="favorites">
Favorites
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Schedule</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-searchbar [(ngModel)]="queryText" (ionChange)="updateSchedule()" placeholder="Search"></ion-searchbar>
</ion-toolbar>
</ion-header>
<ion-list #scheduleList [hidden]="shownSessions === 0">
<ion-item-group *ngFor="let group of groups" [hidden]="group.hide">
<ion-item-divider sticky>
<ion-label>
{{group.time}}
</ion-label>
</ion-item-divider>
<ion-item-sliding *ngFor="let session of group.sessions" #slidingItem [attr.track]="session.tracks[0] | lowercase"
[hidden]="session.hide">
<ion-item routerLink="/app/tabs/schedule/session/{{session.id}}">
<ion-label>
<h3>{{session.name}}</h3>
<p>
{{session.timeStart}} &mdash; {{session.timeEnd}}: {{session.location}}
</p>
</ion-label>
</ion-item>
<ion-item-options>
<ion-item-option color="favorite" (click)="addFavorite(slidingItem, session)" *ngIf="segment === 'all'">
Favorite
</ion-item-option>
<ion-item-option color="danger" (click)="removeFavorite(slidingItem, session, 'Remove Favorite')"
*ngIf="segment === 'favorites'">
Remove
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</ion-item-group>
</ion-list>
<ion-list-header [hidden]="shownSessions > 0">
No Sessions Found
</ion-list-header>
<ion-fab slot="fixed" vertical="bottom" horizontal="end" #fab>
<ion-fab-button>
<ion-icon name="share-social"></ion-icon>
</ion-fab-button>
<ion-fab-list side="top">
<ion-fab-button color="vimeo" (click)="openSocial('Vimeo', fab)">
<ion-icon name="logo-vimeo"></ion-icon>
</ion-fab-button>
<ion-fab-button color="instagram" (click)="openSocial('Instagram', fab)">
<ion-icon name="logo-instagram"></ion-icon>
</ion-fab-button>
<ion-fab-button color="twitter" (click)="openSocial('Twitter', fab)">
<ion-icon name="logo-twitter"></ion-icon>
</ion-fab-button>
<ion-fab-button color="facebook" (click)="openSocial('Facebook', fab)">
<ion-icon name="logo-facebook"></ion-icon>
</ion-fab-button>
</ion-fab-list>
</ion-fab>
</ion-content>

View File

@@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { SchedulePage } from './schedule';
import { ScheduleFilterPage } from '../schedule-filter/schedule-filter';
import { SchedulePageRoutingModule } from './schedule-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
SchedulePageRoutingModule
],
declarations: [
SchedulePage,
ScheduleFilterPage
],
entryComponents: [
ScheduleFilterPage
]
})
export class ScheduleModule { }

View File

@@ -0,0 +1,35 @@
ion-fab-button {
--background: var(--ion-color-step-150, #fff);
--background-hover: var(--ion-color-step-200, #f2f2f2);
--background-focused: var(--ion-color-step-250, #d9d9d9);
--color: var(--ion-color-primary, #3880ff);
}
/*
* Material Design uses the ripple for activated
* so only style the iOS activated background
*/
.ios ion-fab-button {
--background-activated: var(--ion-color-step-250, #d9d9d9);
}
$categories: (
ionic: var(--ion-color-primary),
angular: #ac282b,
communication: #8e8d93,
tooling: #fe4c52,
services: #fd8b2d,
design: #fed035,
workshop: #69bb7b,
food: #3bc7c4,
documentation: #b16be3,
navigation: #6600cc
);
@each $track, $value in map-remove($categories) {
ion-item-sliding[track='#{$track}'] ion-label {
border-left: 2px solid $value;
padding-left: 10px;
}
}

View File

@@ -0,0 +1,140 @@
import { Component, ViewChild, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AlertController, IonList, IonRouterOutlet, LoadingController, ModalController, ToastController, Config } from '@ionic/angular';
import { ScheduleFilterPage } from '../schedule-filter/schedule-filter';
import { ConferenceData } from '../../providers/conference-data';
import { UserData } from '../../providers/user-data';
@Component({
selector: 'page-schedule',
templateUrl: 'schedule.html',
styleUrls: ['./schedule.scss'],
})
export class SchedulePage implements OnInit {
// Gets a reference to the list element
@ViewChild('scheduleList', { static: true }) scheduleList: IonList;
ios: boolean;
dayIndex = 0;
queryText = '';
segment = 'all';
excludeTracks: any = [];
shownSessions: any = [];
groups: any = [];
confDate: string;
showSearchbar: boolean;
constructor(
public alertCtrl: AlertController,
public confData: ConferenceData,
public loadingCtrl: LoadingController,
public modalCtrl: ModalController,
public router: Router,
public routerOutlet: IonRouterOutlet,
public toastCtrl: ToastController,
public user: UserData,
public config: Config
) { }
ngOnInit() {
this.updateSchedule();
this.ios = this.config.get('mode') === 'ios';
}
updateSchedule() {
// Close any open sliding items when the schedule updates
if (this.scheduleList) {
this.scheduleList.closeSlidingItems();
}
this.confData.getTimeline(this.dayIndex, this.queryText, this.excludeTracks, this.segment).subscribe((data: any) => {
this.shownSessions = data.shownSessions;
this.groups = data.groups;
});
}
async presentFilter() {
const modal = await this.modalCtrl.create({
component: ScheduleFilterPage,
swipeToClose: true,
presentingElement: this.routerOutlet.nativeEl,
componentProps: { excludedTracks: this.excludeTracks }
});
await modal.present();
const { data } = await modal.onWillDismiss();
if (data) {
this.excludeTracks = data;
this.updateSchedule();
}
}
async addFavorite(slidingItem: HTMLIonItemSlidingElement, sessionData: any) {
if (this.user.hasFavorite(sessionData.name)) {
// Prompt to remove favorite
this.removeFavorite(slidingItem, sessionData, 'Favorite already added');
} else {
// Add as a favorite
this.user.addFavorite(sessionData.name);
// Close the open item
slidingItem.close();
// Create a toast
const toast = await this.toastCtrl.create({
header: `${sessionData.name} was successfully added as a favorite.`,
duration: 3000,
buttons: [{
text: 'Close',
role: 'cancel'
}]
});
// Present the toast at the bottom of the page
await toast.present();
}
}
async removeFavorite(slidingItem: HTMLIonItemSlidingElement, sessionData: any, title: string) {
const alert = await this.alertCtrl.create({
header: title,
message: 'Would you like to remove this session from your favorites?',
buttons: [
{
text: 'Cancel',
handler: () => {
// they clicked the cancel button, do not remove the session
// close the sliding item and hide the option buttons
slidingItem.close();
}
},
{
text: 'Remove',
handler: () => {
// they want to remove this session from their favorites
this.user.removeFavorite(sessionData.name);
this.updateSchedule();
// close the sliding item and hide the option buttons
slidingItem.close();
}
}
]
});
// now present the alert on top of all other content
await alert.present();
}
async openSocial(network: string, fab: HTMLIonFabElement) {
const loading = await this.loadingCtrl.create({
message: `Posting to ${network}`,
duration: (Math.random() * 1000) + 500
});
await loading.present();
await loading.onWillDismiss();
fab.close();
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SessionDetailPage } from './session-detail';
const routes: Routes = [
{
path: '',
component: SessionDetailPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SessionDetailPageRoutingModule { }

View File

@@ -0,0 +1,47 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="defaultHref"></ion-back-button>
</ion-buttons>
<ion-buttons slot="end">
<ion-button (click)="toggleFavorite()">
<ion-icon *ngIf="!isFavorite" slot="icon-only" name="star-outline"></ion-icon>
<ion-icon *ngIf="isFavorite" slot="icon-only" name="star"></ion-icon>
</ion-button>
<ion-button (click)="shareSession()">
<ion-icon slot="icon-only" name="share"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div *ngIf="session" class="ion-padding">
<h1>{{session.name}}</h1>
<span *ngFor="let track of session?.tracks" [class]="'session-track-'+track.toLowerCase()">{{track}}</span>
<p>{{session.description}}</p>
<ion-text color="medium">
{{session.timeStart}} &ndash; {{session.timeEnd}}
<br /> {{session.location}}
</ion-text>
</div>
<ion-list>
<ion-item (click)="sessionClick('watch')" button>
<ion-label color="primary">Watch</ion-label>
</ion-item>
<ion-item (click)="sessionClick('add to calendar')" button>
<ion-label color="primary">Add to Calendar</ion-label>
</ion-item>
<ion-item (click)="sessionClick('mark as unwatched')" button>
<ion-label color="primary">Mark as Unwatched</ion-label>
</ion-item>
<ion-item (click)="sessionClick('download video')" button>
<ion-label color="primary">Download Video</ion-label>
<ion-icon slot="end" color="primary" size="small" name="cloud-download"></ion-icon>
</ion-item>
<ion-item (click)="sessionClick('leave feedback')" button>
<ion-label color="primary">Leave Feedback</ion-label>
</ion-item>
</ion-list>
</ion-content>

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SessionDetailPage } from './session-detail';
import { SessionDetailPageRoutingModule } from './session-detail-routing.module';
import { IonicModule } from '@ionic/angular';
@NgModule({
imports: [
CommonModule,
IonicModule,
SessionDetailPageRoutingModule
],
declarations: [
SessionDetailPage,
]
})
export class SessionDetailModule { }

View File

@@ -0,0 +1,86 @@
.session-track-ionic {
color: var(--ion-color-primary);
}
.session-track-angular {
color: var(--ion-color-angular);
}
.session-track-communication {
color: var(--ion-color-communication);
}
.session-track-tooling {
color: var(--ion-color-tooling);
}
.session-track-services {
color: var(--ion-color-services);
}
.session-track-design {
color: var(--ion-color-design);
}
.session-track-workshop {
color: var(--ion-color-workshop);
}
.session-track-food {
color: var(--ion-color-food);
}
.session-track-documentation {
color: var(--ion-color-documentation);
}
.session-track-navigation {
color: var(--ion-color-navigation);
}
/* Favorite Icon
* --------------------------------------------------------
*/
.show-favorite {
position: relative;
}
.icon-heart-empty,
.icon-heart {
--border-radius: 50%;
--padding-start: 0;
--padding-end: 0;
position: absolute;
top: 5px;
right: 5px;
width: 48px;
height: 48px;
font-size: 16px;
transition: transform 300ms ease;
}
.icon-heart-empty {
transform: scale(1);
}
.icon-heart {
transform: scale(0);
}
.show-favorite .icon-heart {
transform: scale(1);
}
.show-favorite .icon-heart-empty {
transform: scale(0);
}
h1 {
margin: 0;
}

View File

@@ -0,0 +1,67 @@
import { Component } from '@angular/core';
import { ConferenceData } from '../../providers/conference-data';
import { ActivatedRoute } from '@angular/router';
import { UserData } from '../../providers/user-data';
@Component({
selector: 'page-session-detail',
styleUrls: ['./session-detail.scss'],
templateUrl: 'session-detail.html'
})
export class SessionDetailPage {
session: any;
isFavorite = false;
defaultHref = '';
constructor(
private dataProvider: ConferenceData,
private userProvider: UserData,
private route: ActivatedRoute
) { }
ionViewWillEnter() {
this.dataProvider.load().subscribe((data: any) => {
if (data && data.schedule && data.schedule[0] && data.schedule[0].groups) {
const sessionId = this.route.snapshot.paramMap.get('sessionId');
for (const group of data.schedule[0].groups) {
if (group && group.sessions) {
for (const session of group.sessions) {
if (session && session.id === sessionId) {
this.session = session;
this.isFavorite = this.userProvider.hasFavorite(
this.session.name
);
break;
}
}
}
}
}
});
}
ionViewDidEnter() {
this.defaultHref = `/app/tabs/schedule`;
}
sessionClick(item: string) {
console.log('Clicked', item);
}
toggleFavorite() {
if (this.userProvider.hasFavorite(this.session.name)) {
this.userProvider.removeFavorite(this.session.name);
this.isFavorite = false;
} else {
this.userProvider.addFavorite(this.session.name);
this.isFavorite = true;
}
}
shareSession() {
console.log('Clicked share session');
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SignupPage } from './signup';
const routes: Routes = [
{
path: '',
component: SignupPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SignupPageRoutingModule { }

View File

@@ -0,0 +1,46 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Signup</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="signup-logo">
<img src="assets/img/appicon.svg" alt="Ionic Logo">
</div>
<form #signupForm="ngForm" novalidate>
<ion-list lines="none">
<ion-item>
<ion-label position="stacked" color="primary">Username</ion-label>
<ion-input [(ngModel)]="signup.username" name="username" type="text" #username="ngModel" required>
</ion-input>
</ion-item>
<ion-text color="danger">
<p [hidden]="username.valid || submitted == false" class="ion-padding-start">
Username is required
</p>
</ion-text>
<ion-item>
<ion-label position="stacked" color="primary">Password</ion-label>
<ion-input [(ngModel)]="signup.password" name="password" type="password" #password="ngModel" required>
</ion-input>
</ion-item>
<ion-text color="danger">
<p [hidden]="password.valid || submitted == false" class="ion-padding-start">
Password is required
</p>
</ion-text>
</ion-list>
<div class="ion-padding">
<ion-button (click)="onSignup(signupForm)" type="submit" expand="block">Create</ion-button>
</div>
</form>
</ion-content>

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { SignupPage } from './signup';
import { SignupPageRoutingModule } from './signup-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
SignupPageRoutingModule
],
declarations: [
SignupPage,
]
})
export class SignUpModule { }

View File

@@ -0,0 +1,13 @@
.signup-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.signup-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}

View File

@@ -0,0 +1,33 @@
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { UserData } from '../../providers/user-data';
import { UserOptions } from '../../interfaces/user-options';
@Component({
selector: 'page-signup',
templateUrl: 'signup.html',
styleUrls: ['./signup.scss'],
})
export class SignupPage {
signup: UserOptions = { username: '', password: '' };
submitted = false;
constructor(
public router: Router,
public userData: UserData
) {}
onSignup(form: NgForm) {
this.submitted = true;
if (form.valid) {
this.userData.signup(this.signup.username);
this.router.navigateByUrl('/app/tabs/schedule');
}
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SpeakerDetailPage } from './speaker-detail';
const routes: Routes = [
{
path: '',
component: SpeakerDetailPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SpeakerDetailPageRoutingModule { }

View File

@@ -0,0 +1,43 @@
<ion-content class="speaker-detail">
<ion-header class="ion-no-border">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/app/tabs/speakers"></ion-back-button>
</ion-buttons>
<ion-buttons *ngIf="speaker" slot="end">
<ion-button (click)="openContact(speaker)">
<ion-icon slot="icon-only" ios="call-outline" md="call-sharp"></ion-icon>
</ion-button>
<ion-button (click)="openSpeakerShare(speaker)">
<ion-icon slot="icon-only" ios="share-outline" md="share-sharp"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<div class="speaker-background">
<img [src]="speaker?.profilePic" [alt]="speaker?.name">
<h2>{{speaker?.name}}</h2>
</div>
<div class="ion-padding speaker-detail">
<p>{{speaker?.about}} Say hello on social media!</p>
<hr>
<ion-chip color="twitter" button (click)="openExternalUrl('https://twitter.com/' + speaker.twitter)">
<ion-icon name="logo-twitter"></ion-icon>
<ion-label>Twitter</ion-label>
</ion-chip>
<ion-chip color="dark" button (click)="openExternalUrl('https://github.com/ionic-team/ionic')">
<ion-icon name="logo-github"></ion-icon>
<ion-label>GitHub</ion-label>
</ion-chip>
<ion-chip color="instagram" button (click)="openExternalUrl('https://instagram.com/ionicframework')">
<ion-icon name="logo-instagram"></ion-icon>
<ion-label>Instagram</ion-label>
</ion-chip>
</div>
</ion-content>

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SpeakerDetailPage } from './speaker-detail';
import { SpeakerDetailPageRoutingModule } from './speaker-detail-routing.module';
import { IonicModule } from '@ionic/angular';
@NgModule({
imports: [
CommonModule,
IonicModule,
SpeakerDetailPageRoutingModule
],
declarations: [
SpeakerDetailPage,
]
})
export class SpeakerDetailModule { }

View File

@@ -0,0 +1,75 @@
/*
* Speaker Background
*/
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-button,
ion-toolbar ion-back-button,
ion-toolbar ion-menu-button {
--color: white;
}
.speaker-background {
position: relative;
display: flex;
padding-top: var(--ion-safe-area-top);
align-items: center;
justify-content: center;
flex-direction: column;
height: calc(250px + var(--ion-safe-area-top));
background: center / cover url(/assets/img/speaker-background.png) no-repeat;
}
.speaker-background img {
width: 70px;
border-radius: 50%;
margin-top: calc(-1 * var(--ion-safe-area-top));
}
.speaker-background h2 {
position: absolute;
bottom: 10px;
color: white;
}
.md .speaker-background {
box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px;
}
.ios .speaker-background {
box-shadow: rgba(0, 0, 0, 0.12) 0px 4px 16px;
}
/*
* Speaker Details
*/
.speaker-detail p {
margin-left: 6px;
margin-right: 6px;
}
.speaker-detail hr {
margin-top: 20px;
margin-bottom: 20px;
background: var(--ion-color-step-150, #d7d8da);
}

View File

@@ -0,0 +1,106 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ConferenceData } from '../../providers/conference-data';
import { ActionSheetController } from '@ionic/angular';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
@Component({
selector: 'page-speaker-detail',
templateUrl: 'speaker-detail.html',
styleUrls: ['./speaker-detail.scss'],
})
export class SpeakerDetailPage {
speaker: any;
constructor(
private dataProvider: ConferenceData,
private route: ActivatedRoute,
public actionSheetCtrl: ActionSheetController,
public confData: ConferenceData,
public inAppBrowser: InAppBrowser,
) {}
ionViewWillEnter() {
this.dataProvider.load().subscribe((data: any) => {
const speakerId = this.route.snapshot.paramMap.get('speakerId');
if (data && data.speakers) {
for (const speaker of data.speakers) {
if (speaker && speaker.id === speakerId) {
this.speaker = speaker;
break;
}
}
}
});
}
openExternalUrl(url: string) {
this.inAppBrowser.create(
url,
'_blank'
);
}
async openSpeakerShare(speaker: any) {
const actionSheet = await this.actionSheetCtrl.create({
header: 'Share ' + speaker.name,
buttons: [
{
text: 'Copy Link',
handler: () => {
console.log(
'Copy link clicked on https://twitter.com/' + speaker.twitter
);
if (
(window as any).cordova &&
(window as any).cordova.plugins.clipboard
) {
(window as any).cordova.plugins.clipboard.copy(
'https://twitter.com/' + speaker.twitter
);
}
}
},
{
text: 'Share via ...'
},
{
text: 'Cancel',
role: 'cancel'
}
]
});
await actionSheet.present();
}
async openContact(speaker: any) {
const mode = 'ios'; // this.config.get('mode');
const actionSheet = await this.actionSheetCtrl.create({
header: 'Contact ' + speaker.name,
buttons: [
{
text: `Email ( ${speaker.email} )`,
icon: mode !== 'ios' ? 'mail' : null,
handler: () => {
window.open('mailto:' + speaker.email);
}
},
{
text: `Call ( ${speaker.phone} )`,
icon: mode !== 'ios' ? 'call' : null,
handler: () => {
window.open('tel:' + speaker.phone);
}
},
{
text: 'Cancel',
role: 'cancel'
}
]
});
await actionSheet.present();
}
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SpeakerListPage } from './speaker-list';
const routes: Routes = [
{
path: '',
component: SpeakerListPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SpeakerListPageRoutingModule {}

View File

@@ -0,0 +1,52 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Speakers</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Speakers</ion-title>
</ion-toolbar>
</ion-header>
<ion-grid fixed>
<ion-row>
<ion-col size="12" size-md="6" *ngFor="let speaker of speakers">
<ion-card class="speaker-card">
<ion-card-header>
<ion-item detail="false" lines="none" class="speaker-item" routerLink="/app/tabs/speakers/speaker-details/{{speaker.id}}">
<ion-avatar slot="start">
<img [src]="speaker.profilePic" [alt]="speaker.name + ' profile picture'">
</ion-avatar>
<ion-label>
<h2>{{speaker.name}}</h2>
<p>{{speaker.title}}</p>
</ion-label>
</ion-item>
</ion-card-header>
<ion-card-content>
<ion-list lines="none">
<ion-item *ngFor="let session of speaker.sessions" detail="false" routerLink="/app/tabs/speakers/session/{{session.id}}">
<ion-label>
<h3>{{session.name}}</h3>
</ion-label>
</ion-item>
<ion-item detail="false" routerLink="/app/tabs/speakers/speaker-details/{{speaker.id}}">
<ion-label>
<h3>About {{speaker.name}}</h3>
</ion-label>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { SpeakerListPage } from './speaker-list';
import { SpeakerListPageRoutingModule } from './speaker-list-routing.module';
@NgModule({
imports: [
CommonModule,
IonicModule,
SpeakerListPageRoutingModule
],
declarations: [SpeakerListPage],
})
export class SpeakerListModule {}

View File

@@ -0,0 +1,46 @@
.speaker-card {
display: flex;
flex-direction: column;
}
/* Due to the fact the cards are inside of columns the margins don't overlap
* properly so we want to remove the extra margin between cards
*/
ion-col:not(:last-of-type) .speaker-card {
margin-bottom: 0;
}
.speaker-card .speaker-item {
--min-height: 85px;
}
.speaker-card .speaker-item h2 {
font-size: 18px;
font-weight: 500;
letter-spacing: 0.02em;
}
.speaker-card .speaker-item p {
font-size: 13px;
letter-spacing: 0.02em;
}
.speaker-card ion-card-header {
padding: 0;
}
.speaker-card ion-card-content {
flex: 1 1 auto;
padding: 0;
}
.ios ion-list {
margin-bottom: 10px;
}
.md ion-list {
border-top: 1px solid var(--ion-color-step-150, #d7d8da);
padding: 0;
}

View File

@@ -0,0 +1,39 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router';
import { TestBed, async } from '@angular/core/testing';
import { ActionSheetController } from '@ionic/angular';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import { SpeakerListPage } from './speaker-list';
import { ConferenceData } from '../../providers/conference-data';
const confDataSub = {};
describe('SpeakerListPage', () => {
let fixture, app;
beforeEach(async(() => {
const actionSheetSpy = jasmine.createSpyObj('ActionSheetController', [
'create'
]);
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const iabSpy = jasmine.createSpyObj('InAppBrowser', ['create']);
TestBed.configureTestingModule({
declarations: [SpeakerListPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: ActionSheetController, useValue: actionSheetSpy },
{ provide: InAppBrowser, useValue: iabSpy },
{ provide: Router, useValue: routerSpy },
{ provide: ConferenceData, useValue: confDataSub }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SpeakerListPage);
app = fixture.debugElement.componentInstance;
});
it('should create the speaker list page', () => {
expect(app).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { ConferenceData } from '../../providers/conference-data';
@Component({
selector: 'page-speaker-list',
templateUrl: 'speaker-list.html',
styleUrls: ['./speaker-list.scss'],
})
export class SpeakerListPage {
speakers: any[] = [];
constructor(public confData: ConferenceData) {}
ionViewDidEnter() {
this.confData.getSpeakers().subscribe((speakers: any[]) => {
this.speakers = speakers;
});
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SupportPage } from './support';
const routes: Routes = [
{
path: '',
component: SupportPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SupportPageRoutingModule { }

View File

@@ -0,0 +1,34 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Support</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="support-logo">
<img src="assets/img/appicon.svg" alt="Ionic Logo">
</div>
<form #submitForm="ngForm" novalidate (ngSubmit)="submit(submitForm)">
<ion-list lines="none">
<ion-item>
<ion-label position="stacked" color="primary">Enter your support message below</ion-label>
<ion-textarea [(ngModel)]="supportMessage" name="supportQuestion" #supportQuestion="ngModel" rows="6" required></ion-textarea>
</ion-item>
</ion-list>
<ion-text color="danger">
<p [hidden]="supportQuestion.valid || submitted === false" class="ion-padding-start">
Support message is required
</p>
</ion-text>
<div class="ion-padding">
<ion-button expand="block" type="submit">Submit</ion-button>
</div>
</form>
</ion-content>

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { SupportPage } from './support';
import { SupportPageRoutingModule } from './support-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
SupportPageRoutingModule
],
declarations: [
SupportPage,
]
})
export class SupportModule { }

View File

@@ -0,0 +1,13 @@
.support-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.support-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}

View File

@@ -0,0 +1,65 @@
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { AlertController, ToastController } from '@ionic/angular';
@Component({
selector: 'page-support',
templateUrl: 'support.html',
styleUrls: ['./support.scss'],
})
export class SupportPage {
submitted = false;
supportMessage: string;
constructor(
public alertCtrl: AlertController,
public toastCtrl: ToastController
) { }
async ionViewDidEnter() {
const toast = await this.toastCtrl.create({
message: 'This does not actually send a support request.',
duration: 3000
});
await toast.present();
}
async submit(form: NgForm) {
this.submitted = true;
if (form.valid) {
this.supportMessage = '';
this.submitted = false;
const toast = await this.toastCtrl.create({
message: 'Your support request has been sent.',
duration: 3000
});
await toast.present();
}
}
// If the user enters text in the support question and then navigates
// without submitting first, ask if they meant to leave the page
// async ionViewCanLeave(): Promise<boolean> {
// // If the support message is empty we should just navigate
// if (!this.supportMessage || this.supportMessage.trim().length === 0) {
// return true;
// }
// return new Promise((resolve: any, reject: any) => {
// const alert = await this.alertCtrl.create({
// title: 'Leave this page?',
// message: 'Are you sure you want to leave this page? Your support message will not be submitted.',
// buttons: [
// { text: 'Stay', handler: reject },
// { text: 'Leave', role: 'cancel', handler: resolve }
// ]
// });
// await alert.present();
// });
// }
}

View File

@@ -0,0 +1,74 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs-page';
import { SchedulePage } from '../schedule/schedule';
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'schedule',
children: [
{
path: '',
component: SchedulePage,
},
{
path: 'session/:sessionId',
loadChildren: () => import('../session-detail/session-detail.module').then(m => m.SessionDetailModule)
}
]
},
{
path: 'speakers',
children: [
{
path: '',
loadChildren: () => import('../speaker-list/speaker-list.module').then(m => m.SpeakerListModule)
},
{
path: 'session/:sessionId',
loadChildren: () => import('../session-detail/session-detail.module').then(m => m.SessionDetailModule)
},
{
path: 'speaker-details/:speakerId',
loadChildren: () => import('../speaker-detail/speaker-detail.module').then(m => m.SpeakerDetailModule)
}
]
},
{
path: 'map',
children: [
{
path: '',
loadChildren: () => import('../map/map.module').then(m => m.MapModule)
}
]
},
{
path: 'about',
children: [
{
path: '',
loadChildren: () => import('../about/about.module').then(m => m.AboutModule)
}
]
},
{
path: '',
redirectTo: '/app/tabs/schedule',
pathMatch: 'full'
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class TabsPageRoutingModule { }

View File

@@ -0,0 +1,25 @@
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="schedule">
<ion-icon name="calendar"></ion-icon>
<ion-label>Schedule</ion-label>
</ion-tab-button>
<ion-tab-button tab="speakers">
<ion-icon name="people"></ion-icon>
<ion-label>Speakers</ion-label>
</ion-tab-button>
<ion-tab-button tab="map">
<ion-icon name="location"></ion-icon>
<ion-label>Map</ion-label>
</ion-tab-button>
<ion-tab-button tab="about">
<ion-icon name="information-circle"></ion-icon>
<ion-label>About</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>

View File

@@ -0,0 +1,31 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TabsPage } from './tabs-page';
import { TabsPageRoutingModule } from './tabs-page-routing.module';
import { AboutModule } from '../about/about.module';
import { MapModule } from '../map/map.module';
import { ScheduleModule } from '../schedule/schedule.module';
import { SessionDetailModule } from '../session-detail/session-detail.module';
import { SpeakerDetailModule } from '../speaker-detail/speaker-detail.module';
import { SpeakerListModule } from '../speaker-list/speaker-list.module';
@NgModule({
imports: [
AboutModule,
CommonModule,
IonicModule,
MapModule,
ScheduleModule,
SessionDetailModule,
SpeakerDetailModule,
SpeakerListModule,
TabsPageRoutingModule
],
declarations: [
TabsPage,
]
})
export class TabsModule { }

View File

@@ -0,0 +1,7 @@
.tabbar {
justify-content: center;
}
.tab-button {
max-width: 200px;
}

View File

@@ -0,0 +1,19 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { TabsPage } from './tabs-page';
describe('TabsPage', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TabsPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));
it('should create the tabs page', () => {
const fixture = TestBed.createComponent(TabsPage);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -0,0 +1,6 @@
import { Component } from '@angular/core';
@Component({
templateUrl: 'tabs-page.html'
})
export class TabsPage {}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TutorialPage } from './tutorial';
const routes: Routes = [
{
path: '',
component: TutorialPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class TutorialPageRoutingModule { }

View File

@@ -0,0 +1,49 @@
<ion-header no-border>
<ion-toolbar>
<ion-buttons slot="end">
<ion-button color='primary' (click)="startApp()" [hidden]="!showSkip">Skip</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-slides #slides (ionSlideWillChange)="onSlideChangeStart($event)" pager="false">
<ion-slide>
<img src="assets/img/ica-slidebox-img-1.png" class="slide-image" />
<h2 class="slide-title">
Welcome to
<b>ICA</b>
</h2>
<p>
The
<b>ionic conference app</b> is a practical preview of the ionic framework in action, and a demonstration of proper code
use.
</p>
</ion-slide>
<ion-slide>
<img src="assets/img/ica-slidebox-img-2.png" class="slide-image" />
<h2 class="slide-title">What is Ionic?</h2>
<p>
<b>Ionic Framework</b> is an open source SDK that enables developers to build high quality mobile apps with web technologies
like HTML, CSS, and JavaScript.</p>
</ion-slide>
<ion-slide>
<img src="assets/img/ica-slidebox-img-3.png" class="slide-image" />
<h2 class="slide-title">What is Ionic Appflow?</h2>
<p>
<b>Ionic Appflow</b> is a powerful set of services and features built on top of Ionic Framework that brings a totally new
level of app development agility to mobile dev teams.</p>
</ion-slide>
<ion-slide>
<img src="assets/img/ica-slidebox-img-4.png" class="slide-image" />
<h2 class="slide-title">Ready to Play?</h2>
<ion-button fill="clear" (click)="startApp()">
Continue
<ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>
</ion-slide>
</ion-slides>
</ion-content>

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TutorialPage } from './tutorial';
import { TutorialPageRoutingModule } from './tutorial-routing.module';
@NgModule({
imports: [
CommonModule,
IonicModule,
TutorialPageRoutingModule
],
declarations: [TutorialPage],
entryComponents: [TutorialPage],
})
export class TutorialModule {}

View File

@@ -0,0 +1,34 @@
ion-toolbar {
--background: transparent;
--border-color: transparent;
}
.swiper-slide {
display: block;
}
.slide-title {
margin-top: 2.8rem;
}
.slide-image {
max-height: 50%;
max-width: 60%;
margin: 36px 0;
pointer-events: none;
}
b {
font-weight: 500;
}
p {
padding: 0 40px;
font-size: 14px;
line-height: 1.5;
color: var(--ion-color-step-600, #60646b);
b {
color: var(--ion-text-color, #000000);
}
}

View File

@@ -0,0 +1,41 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router';
import { TestBed, async } from '@angular/core/testing';
import { MenuController } from '@ionic/angular';
import { TutorialPage } from './tutorial';
import { IonicStorageModule } from '@ionic/storage';
describe('TutorialPage', () => {
let fixture, app;
beforeEach(async(() => {
const menuSpy = jasmine.createSpyObj('MenuController', [
'toggle',
'enable'
]);
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
TestBed.configureTestingModule({
declarations: [TutorialPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [IonicStorageModule.forRoot()],
providers: [
{ provide: MenuController, useValue: menuSpy },
{ provide: Router, useValue: routerSpy }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TutorialPage);
app = fixture.debugElement.componentInstance;
});
it('should create the tutorial page', () => {
expect(app).toBeTruthy();
});
it('should check the tutorial status', async () => {
const didTuts = await app.storage.get('ion_did_tutorial');
expect(didTuts).toBeFalsy();
});
});

View File

@@ -0,0 +1,50 @@
import { Component, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { MenuController, IonSlides } from '@ionic/angular';
import { Storage } from '@ionic/storage';
@Component({
selector: 'page-tutorial',
templateUrl: 'tutorial.html',
styleUrls: ['./tutorial.scss'],
})
export class TutorialPage {
showSkip = true;
@ViewChild('slides', { static: true }) slides: IonSlides;
constructor(
public menu: MenuController,
public router: Router,
public storage: Storage
) {}
startApp() {
this.router
.navigateByUrl('/app/tabs/schedule', { replaceUrl: true })
.then(() => this.storage.set('ion_did_tutorial', true));
}
onSlideChangeStart(event) {
event.target.isEnd().then(isEnd => {
this.showSkip = !isEnd;
});
}
ionViewWillEnter() {
this.storage.get('ion_did_tutorial').then(res => {
if (res === true) {
this.router.navigateByUrl('/app/tabs/schedule', { replaceUrl: true });
}
});
this.menu.enable(false);
}
ionViewDidLeave() {
// enable the root left menu when leaving the tutorial page
this.menu.enable(true);
}
}

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { CanLoad, Router } from '@angular/router';
import { Storage } from '@ionic/storage';
@Injectable({
providedIn: 'root'
})
export class CheckTutorial implements CanLoad {
constructor(private storage: Storage, private router: Router) {}
canLoad() {
return this.storage.get('ion_did_tutorial').then(res => {
if (res) {
this.router.navigate(['/app', 'tabs', 'schedule']);
return false;
} else {
return true;
}
});
}
}

View File

@@ -0,0 +1,161 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserData } from './user-data';
@Injectable({
providedIn: 'root'
})
export class ConferenceData {
data: any;
constructor(public http: HttpClient, public user: UserData) {}
load(): any {
if (this.data) {
return of(this.data);
} else {
return this.http
.get('assets/data/data.json')
.pipe(map(this.processData, this));
}
}
processData(data: any) {
// just some good 'ol JS fun with objects and arrays
// build up the data by linking speakers to sessions
this.data = data;
// loop through each day in the schedule
this.data.schedule.forEach((day: any) => {
// loop through each timeline group in the day
day.groups.forEach((group: any) => {
// loop through each session in the timeline group
group.sessions.forEach((session: any) => {
session.speakers = [];
if (session.speakerNames) {
session.speakerNames.forEach((speakerName: any) => {
const speaker = this.data.speakers.find(
(s: any) => s.name === speakerName
);
if (speaker) {
session.speakers.push(speaker);
speaker.sessions = speaker.sessions || [];
speaker.sessions.push(session);
}
});
}
});
});
});
return this.data;
}
getTimeline(
dayIndex: number,
queryText = '',
excludeTracks: any[] = [],
segment = 'all'
) {
return this.load().pipe(
map((data: any) => {
const day = data.schedule[dayIndex];
day.shownSessions = 0;
queryText = queryText.toLowerCase().replace(/,|\.|-/g, ' ');
const queryWords = queryText.split(' ').filter(w => !!w.trim().length);
day.groups.forEach((group: any) => {
group.hide = true;
group.sessions.forEach((session: any) => {
// check if this session should show or not
this.filterSession(session, queryWords, excludeTracks, segment);
if (!session.hide) {
// if this session is not hidden then this group should show
group.hide = false;
day.shownSessions++;
}
});
});
return day;
})
);
}
filterSession(
session: any,
queryWords: string[],
excludeTracks: any[],
segment: string
) {
let matchesQueryText = false;
if (queryWords.length) {
// of any query word is in the session name than it passes the query test
queryWords.forEach((queryWord: string) => {
if (session.name.toLowerCase().indexOf(queryWord) > -1) {
matchesQueryText = true;
}
});
} else {
// if there are no query words then this session passes the query test
matchesQueryText = true;
}
// if any of the sessions tracks are not in the
// exclude tracks then this session passes the track test
let matchesTracks = false;
session.tracks.forEach((trackName: string) => {
if (excludeTracks.indexOf(trackName) === -1) {
matchesTracks = true;
}
});
// if the segment is 'favorites', but session is not a user favorite
// then this session does not pass the segment test
let matchesSegment = false;
if (segment === 'favorites') {
if (this.user.hasFavorite(session.name)) {
matchesSegment = true;
}
} else {
matchesSegment = true;
}
// all tests must be true if it should not be hidden
session.hide = !(matchesQueryText && matchesTracks && matchesSegment);
}
getSpeakers() {
return this.load().pipe(
map((data: any) => {
return data.speakers.sort((a: any, b: any) => {
const aName = a.name.split(' ').pop();
const bName = b.name.split(' ').pop();
return aName.localeCompare(bName);
});
})
);
}
getTracks() {
return this.load().pipe(
map((data: any) => {
return data.tracks.sort();
})
);
}
getMap() {
return this.load().pipe(
map((data: any) => {
return data.map;
})
);
}
}

View File

@@ -0,0 +1,75 @@
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
@Injectable({
providedIn: 'root'
})
export class UserData {
favorites: string[] = [];
HAS_LOGGED_IN = 'hasLoggedIn';
HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
constructor(
public storage: Storage
) { }
hasFavorite(sessionName: string): boolean {
return (this.favorites.indexOf(sessionName) > -1);
}
addFavorite(sessionName: string): void {
this.favorites.push(sessionName);
}
removeFavorite(sessionName: string): void {
const index = this.favorites.indexOf(sessionName);
if (index > -1) {
this.favorites.splice(index, 1);
}
}
login(username: string): Promise<any> {
return this.storage.set(this.HAS_LOGGED_IN, true).then(() => {
this.setUsername(username);
return window.dispatchEvent(new CustomEvent('user:login'));
});
}
signup(username: string): Promise<any> {
return this.storage.set(this.HAS_LOGGED_IN, true).then(() => {
this.setUsername(username);
return window.dispatchEvent(new CustomEvent('user:signup'));
});
}
logout(): Promise<any> {
return this.storage.remove(this.HAS_LOGGED_IN).then(() => {
return this.storage.remove('username');
}).then(() => {
window.dispatchEvent(new CustomEvent('user:logout'));
});
}
setUsername(username: string): Promise<any> {
return this.storage.set('username', username);
}
getUsername(): Promise<string> {
return this.storage.get('username').then((value) => {
return value;
});
}
isLoggedIn(): Promise<boolean> {
return this.storage.get(this.HAS_LOGGED_IN).then((value) => {
return value === true;
});
}
checkHasSeenTutorial(): Promise<string> {
return this.storage.get(this.HAS_SEEN_TUTORIAL).then((value) => {
return value;
});
}
}

View File

@@ -0,0 +1,467 @@
{
"schedule": [
{
"date": "2047-05-17",
"groups": [
{
"time": "8:00 am",
"sessions": [
{
"name": "Breakfast",
"timeStart": "8:00 am",
"timeEnd": "9:00 am",
"location": "Dining Hall",
"tracks": ["Food"],
"id": "1"
}
]
},
{
"time": "9:15 am",
"sessions": [
{
"name": "Getting Started with Ionic",
"location": "Hall 2",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Ted Turtle"],
"timeStart": "9:30 am",
"timeEnd": "9:45 am",
"tracks": ["Ionic"],
"id": "2"
},
{
"name": "Ionic Tooling",
"location": "Executive Ballroom",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Rachel Rabbit"],
"timeStart": "9:45 am",
"timeEnd": "10:00 am",
"tracks": ["Tooling"],
"id": "3"
},
{
"name": "University of Ionic",
"location": "Hall 3",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Ellie Elephant"],
"timeStart": "9:15 am",
"timeEnd": "9:30 am",
"tracks": ["Ionic"],
"id": "4"
}
]
},
{
"time": "10:00 am",
"sessions": [
{
"name": "Migrating to Ionic",
"location": "Hall 1",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Eva Eagle", "Lionel Lion"],
"timeStart": "10:00 am",
"timeEnd": "10:15 am",
"tracks": ["Ionic"],
"id": "5"
},
{
"name": "What's New in Angular",
"location": "Hall 3",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Rachel Rabbit"],
"timeStart": "10:15 am",
"timeEnd": "10:30 am",
"tracks": ["Angular"],
"id": "6"
},
{
"name": "The Evolution of Ionicons",
"location": "Hall 2",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Isabella Iguana", "Eva Eagle"],
"timeStart": "10:15 am",
"timeEnd": "10:30 am",
"tracks": ["Design"],
"id": "7"
},
{
"name": "Ionic Pro",
"location": "Grand Ballroom A",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Charlie Cheetah"],
"timeStart": "10:45 am",
"timeEnd": "11:00 am",
"tracks": ["Services"],
"id": "8"
}
]
},
{
"time": "11:00 am",
"sessions": [
{
"name": "Ionic Workshop",
"location": "Hall 1",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Karl Kitten", "Lionel Lion"],
"timeStart": "11:00 am",
"timeEnd": "11:45 am",
"tracks": ["Workshop"],
"id": "9"
},
{
"name": "Community Interaction",
"location": "Hall 3",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Lionel Lion", "Gino Giraffe"],
"timeStart": "11:30 am",
"timeEnd": "11:50 am",
"tracks": ["Communication"],
"id": "10"
},
{
"name": "Navigation in Ionic",
"location": "Grand Ballroom A",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Rachel Rabbit", "Eva Eagle"],
"timeStart": "11:30 am",
"timeEnd": "12:00 pm",
"tracks": ["Navigation"],
"id": "11"
}
]
},
{
"time": "12:00 pm",
"sessions": [
{
"name": "Lunch",
"location": "Dining Hall",
"description": "Come grab lunch with all the Ionic fanatics and talk all things Ionic",
"timeStart": "12:00 pm",
"timeEnd": "1:00 pm",
"tracks": ["Food"],
"id": "12"
}
]
},
{
"time": "1:00 pm",
"sessions": [
{
"name": "Ionic in the Enterprise",
"location": "Hall 1",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Paul Puppy"],
"timeStart": "1:00 pm",
"timeEnd": "1:15 pm",
"tracks": ["Communication"],
"id": "13"
},
{
"name": "Ionic Worldwide",
"location": "Hall 1",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Gino Giraffe"],
"timeStart": "1:15 pm",
"timeEnd": "1:30 pm",
"tracks": ["Communication"],
"id": "14"
},
{
"name": "The Ionic Package",
"location": "Grand Ballroom B",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Molly Mouse", "Burt Bear"],
"timeStart": "1:30 pm",
"timeEnd": "2:00 pm",
"tracks": ["Services"],
"id": "15"
}
]
},
{
"time": "2:00 pm",
"sessions": [
{
"name": "Push Notifications in Ionic",
"location": "Hall 2",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Burt Bear", "Charlie Cheetah"],
"timeStart": "2:00 pm",
"timeEnd": "2:30 pm",
"tracks": ["Services"],
"id": "16"
},
{
"name": "Ionic Documentation",
"location": "Grand Ballroom B",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Donald Duck"],
"timeStart": "2:30 pm",
"timeEnd": "2:45 pm",
"tracks": ["Documentation"],
"id": "17"
},
{
"name": "UX in Ionic",
"location": "Hall 3",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Isabella Iguana", "Ellie Elephant"],
"timeStart": "2:45 pm",
"timeEnd": "3:00 pm",
"tracks": ["Design"],
"id": "18"
}
]
},
{
"time": "3:00",
"sessions": [
{
"name": "Angular Directives in Ionic",
"location": "Hall 1",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Ted Turtle"],
"timeStart": "3:00 pm",
"timeEnd": "3:30 pm",
"tracks": ["Angular"],
"id": "19"
},
{
"name": "Mobile States",
"location": "Hall 2",
"description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, well provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. Well also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.",
"speakerNames": ["Rachel Rabbit"],
"timeStart": "3:30 pm",
"timeEnd": "3:45 pm",
"tracks": ["Navigation"],
"id": "20"
}
]
}
]
}
],
"speakers": [
{
"name": "Burt Bear",
"profilePic": "/assets/img/speakers/bear.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Burt is a Bear. Burt's interests include poetry, dashing space heroes, and lions.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "burt@example.com",
"phone": "+1-541-754-3010",
"id": "1"
},
{
"name": "Charlie Cheetah",
"profilePic": "/assets/img/speakers/cheetah.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Charlie is a Cheetah. Charlie's interests include country music, plush animals, pyrotechnics, and skeletons.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "charlie@example.com",
"phone": "+1-541-754-3010",
"id": "2"
},
{
"name": "Donald Duck",
"profilePic": "/assets/img/speakers/duck.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Donald is a Duck. Donald's interests include carpentry, superheroes, merpeople, and glam rock.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "donald@example.com",
"phone": "+1-541-754-3010",
"id": "3"
},
{
"name": "Eva Eagle",
"profilePic": "/assets/img/speakers/eagle.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Eva is an Eagle. Eva's interests include ants, seashells, and cupcakes.",
"title": "Developer Advocate",
"location": "Everywhere",
"email": "eva@example.com",
"phone": "+1-541-754-3010",
"id": "4"
},
{
"name": "Ellie Elephant",
"profilePic": "/assets/img/speakers/elephant.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Ellie is an Elephant. Ellie's interests include pocket watches, pool, hand fans, and ninjas.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "ellie@example.com",
"phone": "+1-541-754-3010",
"id": "5"
},
{
"name": "Gino Giraffe",
"profilePic": "/assets/img/speakers/giraffe.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Gino is a Giraffe. Gino's interests include candy-making, unicorns, and birdhouses.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "gino@example.com",
"phone": "+1-541-754-3010",
"id": "6"
},
{
"name": "Isabella Iguana",
"profilePic": "/assets/img/speakers/iguana.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Isabella is an Iguana. Isabella's interests include crystals, architecture, and candle-making.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "isabella@example.com",
"phone": "+1-541-754-3010",
"id": "7"
},
{
"name": "Karl Kitten",
"profilePic": "/assets/img/speakers/kitten.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Karl is a Kitten. Karl's interests include skiing, jewelry, and needlepoint.",
"title": "Developer Advocate",
"location": "Everywhere",
"email": "karl@example.com",
"phone": "+1-541-754-3010",
"id": "8"
},
{
"name": "Lionel Lion",
"profilePic": "/assets/img/speakers/lion.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Lionel is a Lion. Lionel's interests include lizards and mathematics.",
"title": "Developer Advocate",
"location": "Everywhere",
"email": "lionel@example.com",
"phone": "+1-541-754-3010",
"id": "9"
},
{
"name": "Molly Mouse",
"profilePic": "/assets/img/speakers/mouse.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Molly is a Mouse. Molly's interests include werewolves and magic.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "molly@example.com",
"phone": "+1-541-754-3010",
"id": "10"
},
{
"name": "Paul Puppy",
"profilePic": "/assets/img/speakers/puppy.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Paul is a Puppy. Paul's interests include maps, whales, and dragons.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "paul@example.com",
"phone": "+1-541-754-3010",
"id": "11"
},
{
"name": "Rachel Rabbit",
"profilePic": "/assets/img/speakers/rabbit.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Rachel is a Rabbit. Rachel's interests include clowns, skeletons, and yo-yos.",
"title": "Senior Software Engineer",
"location": "Everywhere",
"email": "rachel@example.com",
"phone": "+1-541-754-3010",
"id": "12"
},
{
"name": "Ted Turtle",
"profilePic": "/assets/img/speakers/turtle.jpg",
"instagram": "ionicframework",
"twitter": "ionicframework",
"about": "Ted is a Turtle. Ted's interests include butterflies, skiing, and cupcakes.",
"title": "Software Engineer",
"location": "Everywhere",
"email": "ted@example.com",
"phone": "+1-541-754-3010",
"id": "13"
}
],
"map": [
{
"name": "Monona Terrace Convention Center",
"lat": 43.071584,
"lng": -89.38012,
"center": true
},
{
"name": "Ionic HQ",
"lat": 43.074395,
"lng": -89.381056
},
{
"name": "Afterparty - Brocach Irish Pub",
"lat": 43.07336,
"lng": -89.38335
}
],
"tracks": [
{
"name": "Angular",
"icon": "logo-angular"
},
{
"name": "Documentation",
"icon": "document"
},
{
"name": "Food",
"icon": "restaurant"
},
{
"name": "Ionic",
"icon": "logo-ionic"
},
{
"name": "Tooling",
"icon": "hammer"
},
{
"name": "Design",
"icon": "color-palette"
},
{
"name": "Services",
"icon": "cog"
},
{
"name": "Workshop",
"icon": "construct"
},
{
"name": "Communication",
"icon": "call"
},
{
"name": "Navigation",
"icon": "compass"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#3880FF" d="M256 139.3c-64.3 0-116.7 52.3-116.7 116.7 0 64.3 52.3 116.7 116.7 116.7S372.7 320.3 372.7 256 320.3 139.3 256 139.3z"/><circle fill="#3880FF" cx="423.5" cy="96.5" r="53.2"/><path fill="#3880FF" d="M489 149.9l-2.2-4.9-3.6 4c-8.7 9.9-19.8 17.5-32.1 22.1l-3.4 1.3 1.4 3.3c10.6 25.5 16 52.5 16 80.2 0 115.3-93.8 209.2-209.2 209.2S46.8 371.3 46.8 256 140.7 46.8 256 46.8c31.3 0 61.5 6.8 89.6 20.2l3.3 1.6 1.4-3.3c5.1-12 13.3-22.7 23.6-31l4.2-3.4-4.8-2.5C336.8 9.6 297.3 0 256 0 114.8 0 0 114.8 0 256s114.8 256 256 256 256-114.8 256-256c0-36.9-7.7-72.6-23-106.1z"/></svg>

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#FFF" d="M256 139.3c-64.3 0-116.7 52.3-116.7 116.7 0 64.3 52.3 116.7 116.7 116.7S372.7 320.3 372.7 256 320.3 139.3 256 139.3z"/><circle fill="#FFF" cx="423.5" cy="96.5" r="53.2"/><path fill="#FFF" d="M489 149.9l-2.2-4.9-3.6 4c-8.7 9.9-19.8 17.5-32.1 22.1l-3.4 1.3 1.4 3.3c10.6 25.5 16 52.5 16 80.2 0 115.3-93.8 209.2-209.2 209.2S46.8 371.3 46.8 256 140.7 46.8 256 46.8c31.3 0 61.5 6.8 89.6 20.2l3.3 1.6 1.4-3.3c5.1-12 13.3-22.7 23.6-31l4.2-3.4-4.8-2.5C336.8 9.6 297.3 0 256 0 114.8 0 0 114.8 0 256s114.8 256 256 256 256-114.8 256-256c0-36.9-7.7-72.6-23-106.1z"/></svg>

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Some files were not shown because too many files have changed in this diff Show More