Add API for frameworks and examples (#3514)

* Add API for frameworks and examples

* Adjust headers

* Update frameworks list

* Always use latest

* Add types

* Use now repo for downloading and listing

* Use .existsSync

* Remove unused packages

* Use 307 for redirect

* Add examples

* Update tsconfig.json

Co-Authored-By: Steven <steven@ceriously.com>

* Make examples unique

* Remove detectors from frameworks API

* Use /api instead of Next.js

* Install dependencies

* Rename project

* Change name

* Empty

* Change name

* Update api/tsconfig.json

Co-Authored-By: Steven <steven@ceriously.com>

* Update examples

Co-authored-by: Steven <steven@ceriously.com>
This commit is contained in:
Andy
2020-01-07 23:55:39 +01:00
committed by GitHub
parent 537d508a1e
commit 890de6a625
912 changed files with 30138 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

83
examples/ionic-react/.gitignore vendored Normal file
View File

@@ -0,0 +1,83 @@
# Logs
.firebase
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.stencil/

View File

@@ -0,0 +1,8 @@
{
"appId": "io.ionic.starter",
"appName": "ionic-react-conference-app",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "build",
"cordova": {}
}

View File

@@ -0,0 +1,7 @@
{
"name": "ionic-react-conference-app",
"integrations": {
"capacitor": {}
},
"type": "react"
}

View File

@@ -0,0 +1,44 @@
{
"name": "ionic-react-conference-app",
"version": "0.0.1",
"private": true,
"dependencies": {
"@capacitor/core": "1.3.0",
"@ionic/react": "^4.11.4",
"@ionic/react-router": "^4.11.4",
"@types/jest": "24.0.18",
"@types/node": "12.7.5",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-router": "^5.0.3",
"@types/react-router-dom": "^4.3.1",
"date-fns": "^2.6.0",
"ionicons": "^4.6.3",
"node-sass": "^4.13.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-scripts": "3.2.0",
"reselect": "^4.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"description": "An Ionic project",
"devDependencies": {
"@capacitor/cli": "1.3.0",
"@testing-library/react": "^9.3.1",
"@types/googlemaps": "^3.38.0",
"typescript": "3.6.3"
}
}

View File

@@ -0,0 +1,27 @@
# Ionic React Example
This directory is a brief example of an [Ionic React](https://ionicframework.com/docs/react/overview) app that can be deployed with ZEIT Now and zero configuration
## Deploy Your Own
Deploy your own Ionic React project with ZEIT Now.
[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/now-examples/tree/master/ionic-react)
_Live Example: https://ionic-react.now-examples.now.sh_
### How We Created This Example
To get started with Ionic React deployed with ZEIT Now, you can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize the project:
```shell
$ npx ionic start [project-name] conference --type react && cd [project-name]
```
### Deploying From Your Terminal
You can deploy your new Ionic React project with a single command from your terminal using [Now CLI](https://zeit.co/download):
```shell
$ now
```

View File

@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { render, fireEvent, waitForElement } from '@testing-library/react'
it('renders without crashing', () => {
// const div = document.createElement('div');
// ReactDOM.render(<App />, div);
// ReactDOM.unmountComponentAtNode(div);
const { asFragment, container } = render(<App />);
expect(asFragment()).toMatchSnapshot();
});

View File

@@ -0,0 +1,107 @@
import React, { useEffect } from 'react';
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, IonSplitPane } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Menu from './components/Menu';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
/* Theme variables */
import './theme/variables.css';
import MainTabs from './pages/MainTabs';
import { connect } from './data/connect';
import { AppContextProvider } from './data/AppContext';
import { loadConfData } from './data/sessions/sessions.actions';
import { setIsLoggedIn, setUsername, loadUserData } from './data/user/user.actions';
import Account from './pages/Account';
import Login from './pages/Login';
import Signup from './pages/Signup';
import Support from './pages/Support';
import Tutorial from './pages/Tutorial';
import HomeOrTutorial from './components/HomeOrTutorial';
import { Session } from "./models/Session";
const App: React.FC = () => {
return (
<AppContextProvider>
<IonicAppConnected />
</AppContextProvider>
);
};
interface StateProps {
darkMode: boolean,
sessions: Session[],
}
interface DispatchProps {
loadConfData: typeof loadConfData;
loadUserData: typeof loadUserData;
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface IonicAppProps extends StateProps, DispatchProps { }
const IonicApp: React.FC<IonicAppProps> = ({ darkMode, sessions, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => {
useEffect(() => {
loadUserData();
loadConfData();
// eslint-disable-next-line
}, []);
return (
sessions.length === 0 ? (
<div></div>
) : (
<IonApp className={`${darkMode ? 'dark-theme' : ''}`}>
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/tabs" component={MainTabs} />
<Route path="/account" component={Account} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/support" component={Support} />
<Route path="/tutorial" component={Tutorial} />
<Route path="/logout" render={() => {
setIsLoggedIn(false);
setUsername(undefined);
return <Redirect to="/tabs" />
}} />
<Route path="/" component={HomeOrTutorial} exact />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
)
)
}
export default App;
const IonicAppConnected = connect<{}, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
darkMode: state.user.darkMode,
sessions: state.data.sessions
}),
mapDispatchToProps: { loadConfData, loadUserData, setIsLoggedIn, setUsername },
component: IonicApp
});

View File

@@ -0,0 +1,280 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders without crashing 1`] = `
<DocumentFragment>
<ion-app>
<ion-split-pane
content-id="main"
>
<ion-menu
content-id="main"
>
<ion-header>
<ion-toolbar>
<ion-title>
Menu
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content
class="outer-content"
>
<ion-list>
<ion-list-header>
Navigate
</ion-list-header>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Schedule
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Speakers
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Map
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
About
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list>
<ion-list-header>
Account
</ion-list-header>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Account
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Support
</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle
auto-hide="false"
>
<ion-item>
<ion-icon
slot="start"
/>
<ion-label>
Logout
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list>
<ion-list-header>
Tutorial
</ion-list-header>
<ion-item>
<ion-icon
slot="start"
/>
Show Tutorial
</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet
id="main"
>
<div
style="display: flex; position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px; flex-direction: column; width: 100%; height: 100%; contain: layout size style;"
>
<div
class="tabs-inner"
style="position: relative; flex: 1; contain: layout size style;"
>
<ion-router-outlet>
<div
class="ion-page ion-page-invisible"
>
<ion-header>
<ion-toolbar
color="primary"
>
<ion-buttons
slot="start"
>
<ion-menu-button />
</ion-buttons>
<ion-segment>
<ion-segment-button
value="all"
>
All
</ion-segment-button>
<ion-segment-button
value="favorites"
>
Favorites
</ion-segment-button>
</ion-segment>
<ion-buttons
slot="end"
>
<ion-button>
<ion-icon
slot="icon-only"
/>
</ion-button>
</ion-buttons>
</ion-toolbar>
<ion-toolbar
color="primary"
>
<ion-searchbar
placeholder="Search"
/>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher
slot="fixed"
>
<ion-refresher-content />
</ion-refresher>
<ion-list>
<ion-list-header>
No Sessions Found
</ion-list-header>
</ion-list>
<ion-list
style="display: none;"
/>
</ion-content>
<ion-fab
horizontal="end"
slot="fixed"
vertical="bottom"
>
<ion-fab-button>
<ion-icon />
</ion-fab-button>
<ion-fab-list
side="top"
>
<ion-fab-button
color="vimeo"
>
<ion-icon />
</ion-fab-button>
<ion-fab-button
color="google"
>
<ion-icon />
</ion-fab-button>
<ion-fab-button
color="twitter"
>
<ion-icon />
</ion-fab-button>
<ion-fab-button
color="facebook"
>
<ion-icon />
</ion-fab-button>
</ion-fab-list>
</ion-fab>
</div>
</ion-router-outlet>
</div>
<ion-tab-bar
current-path="/tabs/schedule"
selected-tab="schedule"
slot="bottom"
>
<ion-tab-button
href="/tabs/schedule"
tab="schedule"
>
<ion-icon />
<ion-label>
Schedule
</ion-label>
</ion-tab-button>
<ion-tab-button
href="/tabs/speakers"
tab="speakers"
>
<ion-icon />
<ion-label>
Speakers
</ion-label>
</ion-tab-button>
<ion-tab-button
href="/tabs/map"
tab="map"
>
<ion-icon />
<ion-label>
Map
</ion-label>
</ion-tab-button>
<ion-tab-button
href="/tabs/about"
tab="about"
>
<ion-icon />
<ion-label>
About
</ion-label>
</ion-tab-button>
</ion-tab-bar>
</div>
</ion-router-outlet>
</ion-split-pane>
</ion-app>
</DocumentFragment>
`;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { IonList, IonItem, IonLabel } from '@ionic/react';
interface AboutPopoverProps {
dismiss: () => void;
};
const AboutPopover: React.FC<AboutPopoverProps> = ({dismiss}) => {
const close = (url: string) => {
window.open(url, '_blank');
dismiss();
};
return (
<IonList>
<IonItem button onClick={() => close('https://ionicframework.com/getting-started')}>
<IonLabel>Learn Ionic</IonLabel>
</IonItem>
<IonItem button onClick={() => close('https://ionicframework.com/docs/react')}>
<IonLabel>Documentation</IonLabel>
</IonItem>
<IonItem button onClick={() => close('https://showcase.ionicframework.com')}>
<IonLabel>Showcase</IonLabel>
</IonItem>
<IonItem button onClick={() => close('https://github.com/ionic-team/ionic')}>
<IonLabel>GitHub Repo</IonLabel>
</IonItem>
<IonItem button onClick={dismiss}>
<IonLabel>Support</IonLabel>
</IonItem>
</IonList >
)
}
export default AboutPopover;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { connect } from '../data/connect';
import { Redirect } from 'react-router';
interface StateProps {
hasSeenTutorial: boolean;
}
const HomeOrTutorial: React.FC<StateProps> = ({ hasSeenTutorial }) => {
return hasSeenTutorial ? <Redirect to="/tabs/schedule" /> : <Redirect to="/tutorial" />
};
export default connect<{}, StateProps, {}>({
mapStateToProps: (state) => ({
hasSeenTutorial: state.user.hasSeenTutorial
}),
component: HomeOrTutorial
});

View File

@@ -0,0 +1,56 @@
import React, { useRef, useEffect } from 'react';
import { Location } from '../models/Location';
interface MapProps {
locations: Location[]
mapCenter: Location
}
const Map: React.FC<MapProps> = ({ mapCenter, locations }) => {
const mapEle = useRef<HTMLDivElement>(null);
const map = useRef<google.maps.Map>();
useEffect(() => {
map.current = new google.maps.Map(mapEle.current, {
center: {
lat: mapCenter.lat,
lng: mapCenter.lng
},
zoom: 16
});
addMarkers();
google.maps.event.addListenerOnce(map.current, 'idle', () => {
if (mapEle.current) {
mapEle.current.classList.add('show-map');
}
});
function addMarkers() {
locations.forEach((markerData) => {
let infoWindow = new google.maps.InfoWindow({
content: `<h5>${markerData.name}</h5>`
});
let marker = new google.maps.Marker({
position: new google.maps.LatLng(markerData.lat, markerData.lng),
map: map.current!,
title: markerData.name
});
marker.addListener('click', () => {
infoWindow.open(map.current!, marker);
});
});
}
}, [mapCenter, locations]);
return (
<div ref={mapEle} className="map-canvas"></div>
);
}
export default Map;

View File

@@ -0,0 +1,119 @@
import {
IonContent,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenu,
IonMenuToggle,
IonTitle,
IonToolbar,
IonToggle
} from '@ionic/react';
import { calendar, contacts, hammer, help, informationCircle, logIn, logOut, map, person, personAdd } from 'ionicons/icons';
import React, { useState } from 'react';
import { connect } from '../data/connect';
import { RouteComponentProps, withRouter } from 'react-router';
import { setDarkMode } from '../data/user/user.actions';
const routes = {
appPages: [
{ title: 'Schedule', path: '/tabs/schedule', icon: calendar },
{ title: 'Speakers', path: '/tabs/speakers', icon: contacts },
{ title: 'Map', path: '/tabs/map', icon: map },
{ title: 'About', path: '/tabs/about', icon: informationCircle }
],
loggedInPages: [
{ title: 'Account', path: '/account', icon: person },
{ title: 'Support', path: '/support', icon: help },
{ title: 'Logout', path: '/logout', icon: logOut }
],
loggedOutPages: [
{ title: 'Login', path: '/login', icon: logIn },
{ title: 'Support', path: '/support', icon: help },
{ title: 'Signup', path: '/signup', icon: personAdd }
]
};
interface Pages {
title: string,
path: string,
icon: { ios: string, md: string },
routerDirection?: string
}
interface StateProps {
darkMode: boolean;
isAuthenticated: boolean;
}
interface DispatchProps {
setDarkMode: typeof setDarkMode
}
interface MenuProps extends RouteComponentProps, StateProps, DispatchProps { }
const Menu: React.FC<MenuProps> = ({ darkMode, history, isAuthenticated, setDarkMode }) => {
const [disableMenu, setDisableMenu] = useState(false);
function renderlistItems(list: Pages[]) {
return list
.filter(route => !!route.path)
.map(p => (
<IonMenuToggle key={p.title} auto-hide="false">
<IonItem button routerLink={p.path} routerDirection="none">
<IonIcon slot="start" icon={p.icon} />
<IonLabel>{p.title}</IonLabel>
</IonItem>
</IonMenuToggle>
));
}
return (
<IonMenu type="overlay" disabled={disableMenu} contentId="main">
<IonHeader>
<IonToolbar>
<IonTitle>Menu</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="outer-content">
<IonList>
<IonListHeader>Navigate</IonListHeader>
{renderlistItems(routes.appPages)}
</IonList>
<IonList>
<IonListHeader>Account</IonListHeader>
{isAuthenticated ? renderlistItems(routes.loggedInPages) : renderlistItems(routes.loggedOutPages)}
</IonList>
<IonList>
<IonListHeader>Tutorial</IonListHeader>
<IonItem onClick={() => {
setDisableMenu(true);
history.push('/tutorial');
}}>
<IonIcon slot="start" icon={hammer} />
Show Tutorial
</IonItem>
</IonList>
<IonList>
<IonItem>
<IonLabel>Dark Theme</IonLabel>
<IonToggle checked={darkMode} onClick={() => setDarkMode(!darkMode)} />
</IonItem>
</IonList>
</IonContent>
</IonMenu>
);
};
export default connect<{}, StateProps, {}>({
mapStateToProps: (state) => ({
darkMode: state.user.darkMode,
isAuthenticated: state.user.isLoggedin
}),
mapDispatchToProps: ({
setDarkMode
}),
component: withRouter(Menu)
})

View File

@@ -0,0 +1,92 @@
import { IonItemDivider, IonItemGroup, IonLabel, IonList, IonListHeader, IonAlert, AlertButton } from '@ionic/react';
import React, { useState, useCallback } from 'react';
import { Session } from '../models/Session';
import SessionListItem from './SessionListItem';
import { SessionGroup } from '../models/SessionGroup';
import { Time } from '../components/Time';
import { connect } from '../data/connect';
import { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
interface OwnProps {
sessionGroups: SessionGroup[]
listType: 'all' | 'favorites'
hide: boolean;
}
interface StateProps {
favoriteSessions: number[];
}
interface DispatchProps {
addFavorite: typeof addFavorite;
removeFavorite: typeof removeFavorite;
}
interface SessionListProps extends OwnProps, StateProps, DispatchProps { };
const SessionList: React.FC<SessionListProps> = ({ addFavorite, removeFavorite, favoriteSessions, hide, sessionGroups, listType }) => {
const [showAlert, setShowAlert] = useState(false);
const [alertHeader, setAlertHeader] = useState('');
const [alertButtons, setAlertButtons] = useState<(AlertButton | string)[]>([]);
const handleShowAlert = useCallback((header: string, buttons: AlertButton[]) => {
setAlertHeader(header);
setAlertButtons(buttons);
setShowAlert(true);
}, []);
if (sessionGroups.length === 0 && !hide) {
return (
<IonList>
<IonListHeader>
No Sessions Found
</IonListHeader>
</IonList>
);
}
return (
<>
<IonList style={hide ? { display: 'none' } : {}}>
{sessionGroups.map((group, index: number) => (
<IonItemGroup key={`group-${index}`}>
<IonItemDivider sticky>
<IonLabel>
<Time date={group.startTime} />
</IonLabel>
</IonItemDivider>
{group.sessions.map((session: Session, sessionIndex: number) => (
<SessionListItem
onShowAlert={handleShowAlert}
isFavorite={favoriteSessions.indexOf(session.id) > -1}
onAddFavorite={addFavorite}
onRemoveFavorite={removeFavorite}
key={`group-${index}-${sessionIndex}`}
session={session}
listType={listType}
/>
))}
</IonItemGroup>
))}
</IonList>
<IonAlert
isOpen={showAlert}
header={alertHeader}
buttons={alertButtons}
onDidDismiss={() => setShowAlert(false)}
></IonAlert>
</>
);
};
export default connect({
mapStateToProps: (state) => ({
favoriteSessions: state.data.favorites
}),
mapDispatchToProps: ({
addFavorite,
removeFavorite
}),
component: SessionList
});

View File

@@ -0,0 +1,4 @@
.filter-icon {
margin: 7px 16px 7px 0;
}

View File

@@ -0,0 +1,108 @@
import React from 'react';
import { IonHeader, IonToolbar, IonButtons, IonButton, IonTitle, IonContent, IonList, IonListHeader, IonItem, IonLabel, IonToggle, IonFooter, IonIcon } from '@ionic/react';
import { logoAngular, call, document, logoIonic, hammer, restaurant, cog, colorPalette, construct, compass } from 'ionicons/icons';
import './SessionListFilter.css'
import { connect } from '../data/connect';
import { updateFilteredTracks } from '../data/sessions/sessions.actions';
interface OwnProps {
onDismissModal: () => void;
}
interface StateProps {
allTracks: string[],
filteredTracks: string[]
}
interface DispatchProps {
updateFilteredTracks: typeof updateFilteredTracks;
}
type SessionListFilterProps = OwnProps & StateProps & DispatchProps;
const SessionListFilter: React.FC<SessionListFilterProps> = ({ allTracks, filteredTracks, onDismissModal, updateFilteredTracks }) => {
const toggleTrackFilter = (track: string) => {
if (filteredTracks.indexOf(track) > -1) {
updateFilteredTracks(filteredTracks.filter(x => x !== track));
} else {
updateFilteredTracks([...filteredTracks, track]);
}
};
const handleDeselectAll = () => {
updateFilteredTracks([]);
};
const handleSelectAll = () => {
updateFilteredTracks([...allTracks]);
};
const iconMap: { [key: string]: any } = {
'Angular': logoAngular,
'Documentation': document,
'Food': restaurant,
'Ionic': logoIonic,
'Tooling': hammer,
'Design': colorPalette,
'Services': cog,
'Workshop': construct,
'Navigation': compass,
'Communication': call
}
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
Filter Sessions
</IonTitle>
<IonButtons slot="end">
<IonButton onClick={onDismissModal} strong>Done</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent class="outer-content">
<IonList>
<IonListHeader>Tracks</IonListHeader>
{allTracks.map((track, index) => (
<IonItem key={track}>
<IonIcon className="filter-icon" icon={iconMap[track]} color="medium" />
<IonLabel>{track}</IonLabel>
<IonToggle
onClick={() => toggleTrackFilter(track)}
checked={filteredTracks.indexOf(track) !== -1}
color="success"
value={track}
></IonToggle>
</IonItem>
))}
</IonList>
</IonContent>
<IonFooter>
<IonToolbar>
<IonButtons slot="start">
<IonButton onClick={handleDeselectAll}>Deselect All</IonButton>
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={handleSelectAll}>Select All</IonButton>
</IonButtons>
</IonToolbar>
</IonFooter>
</>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
allTracks: state.data.allTracks,
filteredTracks: state.data.filteredTracks
}),
mapDispatchToProps: {
updateFilteredTracks
},
component: SessionListFilter
})

View File

@@ -0,0 +1,83 @@
import React, { useRef, useState } from 'react';
import { IonItemSliding, IonAlert, IonItem, IonLabel, IonItemOptions, IonItemOption, AlertButton } from '@ionic/react';
import { Time } from './Time';
import { Session } from '../models/Session';
interface SessionListItemProps {
session: Session;
listType: "all" | "favorites";
onAddFavorite: (id: number) => void;
onRemoveFavorite: (id: number) => void;
onShowAlert: (header: string, buttons: AlertButton[]) => void;
isFavorite: boolean;
}
const SessionListItem: React.FC<SessionListItemProps> = ({ isFavorite, onAddFavorite, onRemoveFavorite, onShowAlert, session, listType }) => {
const ionItemSlidingRef = useRef<HTMLIonItemSlidingElement>(null)
const dismissAlert = () => {
ionItemSlidingRef.current && ionItemSlidingRef.current.close();
}
const removeFavoriteSession = () => {
onAddFavorite(session.id);
onShowAlert('Favorite already added', [
{
text: 'Cancel',
handler: dismissAlert
},
{
text: 'Remove',
handler: () => {
onRemoveFavorite(session.id);
dismissAlert();
}
}
]);
}
const addFavoriteSession = () => {
if (isFavorite) {
// woops, they already favorited it! What shall we do!?
// prompt them to remove it
removeFavoriteSession();
} else {
// remember this session as a user favorite
onAddFavorite(session.id);
onShowAlert('Favorite Added', [
{
text: 'OK',
handler: dismissAlert
}
]);
}
};
return (
<IonItemSliding ref={ionItemSlidingRef} class={'track-' + session.tracks[0].toLowerCase()}>
<IonItem routerLink={`/tabs/schedule/${session.id}`}>
<IonLabel>
<h3>{session.name}</h3>
<p>
<Time date={session.dateTimeStart} /> &mdash;&nbsp;
<Time date={session.dateTimeEnd} /> &mdash;&nbsp;
{session.location}
</p>
</IonLabel>
</IonItem>
<IonItemOptions>
{listType === "favorites" ?
<IonItemOption color="danger" onClick={() => removeFavoriteSession()}>
Remove
</IonItemOption>
:
<IonItemOption color="favorite" onClick={addFavoriteSession}>
Favorite
</IonItemOption>
}
</IonItemOptions>
</IonItemSliding>
);
};
export default React.memo(SessionListItem);

View File

@@ -0,0 +1,46 @@
import { IonLoading, IonFab, IonFabButton, IonIcon, IonFabList } from "@ionic/react"
import { share, logoVimeo, logoGoogleplus, logoTwitter, logoFacebook } from "ionicons/icons"
import React, { useState } from "react"
const ShareSocialFab: React.FC = () => {
const [loadingMessage, setLoadingMessage] = useState('')
const [showLoading, setShowLoading] = useState(false);
const openSocial = (network: string) => {
setLoadingMessage(`Posting to ${network}`);
setShowLoading(true);
};
return(
<>
<IonLoading
isOpen={showLoading}
message={loadingMessage}
duration={2000}
spinner="crescent"
onDidDismiss={() => setShowLoading(false)}
/>
<IonFab slot="fixed" vertical="bottom" horizontal="end">
<IonFabButton>
<IonIcon icon={share} />
</IonFabButton>
<IonFabList side="top">
<IonFabButton color="vimeo" onClick={() => openSocial('Vimeo')}>
<IonIcon icon={logoVimeo} />
</IonFabButton>
<IonFabButton color="google" onClick={() => openSocial('Google+')}>
<IonIcon icon={logoGoogleplus} />
</IonFabButton>
<IonFabButton color="twitter" onClick={() => openSocial('Twitter')}>
<IonIcon icon={logoTwitter} />
</IonFabButton>
<IonFabButton color="facebook" onClick={() => openSocial('Facebook')}>
<IonIcon icon={logoFacebook} />
</IonFabButton>
</IonFabList>
</IonFab>
</>
)
};
export default ShareSocialFab;

View File

@@ -0,0 +1,125 @@
import React, { useState } from 'react';
import { Session } from '../models/Session';
import { Speaker } from '../models/Speaker';
import { IonCard, IonCardHeader, IonItem, IonAvatar, IonCardContent, IonList, IonRow, IonCol, IonButton, IonIcon, IonActionSheet } from '@ionic/react';
import { logoTwitter, shareAlt, chatboxes } from 'ionicons/icons';
import { ActionSheetButton } from '@ionic/core';
interface SpeakerItemProps {
speaker: Speaker;
sessions: Session[];
}
const SpeakerItem: React.FC<SpeakerItemProps> = ({ speaker, sessions }) => {
const [showActionSheet, setShowActionSheet] = useState(false);
const [actionSheetButtons, setActionSheetButtons] = useState<ActionSheetButton[]>([]);
const [actionSheetHeader, setActionSheetHeader] = useState('');
function openSpeakerShare(speaker: Speaker) {
setActionSheetButtons([
{
text: 'Copy Link',
handler: () => {
console.log('Copy Link clicked');
}
},
{
text: 'Share via ...',
handler: () => {
console.log('Share via clicked');
}
},
{
text: 'Cancel',
role: 'cancel',
handler: () => {
console.log('Cancel clicked');
}
}
]);
setActionSheetHeader(`Share ${speaker.name}`);
setShowActionSheet(true);
}
function openContact(speaker: Speaker) {
setActionSheetButtons([
{
text: `Email ( ${speaker.email} )`,
handler: () => {
window.open('mailto:' + speaker.email);
}
},
{
text: `Call ( ${speaker.phone} )`,
handler: () => {
window.open('tel:' + speaker.phone);
}
}
]);
setActionSheetHeader(`Share ${speaker.name}`);
setShowActionSheet(true);
}
return (
<>
<IonCard className="speaker-card">
<IonCardHeader>
<IonItem button detail={false} routerLink={`/tabs/speakers/${speaker.id}`} lines="none">
<IonAvatar slot="start">
<img src={process.env.PUBLIC_URL + speaker.profilePic} alt="Speaker profile pic" />
</IonAvatar>
{speaker.name}
</IonItem>
</IonCardHeader>
<IonCardContent class="outer-content">
<IonList>
{sessions.map(session => (
<IonItem routerLink={`/tabs/speakers/sessions/${session.id}`} key={session.name}>
<h3>{session.name}</h3>
</IonItem>
))}
<IonItem button routerLink={`/tabs/speakers/${speaker.id}`}>
<h3>About {speaker.name}</h3>
</IonItem>
</IonList>
</IonCardContent>
<IonRow justify-content-center>
<IonCol text-left size="4">
<IonButton
fill="clear"
size="small"
color="primary"
href={`https://www.twitter.com/${speaker.twitter}`}
target="_blank"
>
<IonIcon slot="start" icon={logoTwitter} />
Tweet
</IonButton>
</IonCol>
<IonCol text-left size="4">
<IonButton fill="clear" size="small" color="primary" onClick={() => openSpeakerShare(speaker)}>
<IonIcon slot="start" icon={shareAlt} />
Share
</IonButton>
</IonCol>
<IonCol text-left size="4">
<IonButton fill="clear" size="small" color="primary" onClick={() => openContact(speaker)}>
<IonIcon slot="start" icon={chatboxes} />
Contact
</IonButton>
</IonCol>
</IonRow>
</IonCard>
<IonActionSheet
isOpen={showActionSheet}
header={actionSheetHeader}
onDidDismiss={() => setShowActionSheet(false)}
buttons={actionSheetButtons}
/>
</>
);
};
export default SpeakerItem;

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { format, parseISO as parseDate } from 'date-fns';
export const Time: React.FC<{ date: string }> = ({ date }) => (
<>
{format(parseDate(date), "h:mm aaaaa")}m
</>
)

View File

@@ -0,0 +1,26 @@
import React, { createContext, useReducer } from 'react';
import { initialState, AppState, reducers } from './state'
export interface AppContextState {
state: AppState;
dispatch: React.Dispatch<any>;
}
export const AppContext = createContext<AppContextState>({
state: initialState,
dispatch: () => undefined
});
export const AppContextProvider: React.FC = (props => {
const [store, dispatch] = useReducer(reducers, initialState);
return (
<AppContext.Provider value={{
state: store,
dispatch
}}>
{props.children}
</AppContext.Provider>
)
});

View File

@@ -0,0 +1,14 @@
export function combineReducers<R extends any>(reducers: R) {
type keys = keyof typeof reducers;
type returnType = { [K in keys]: ReturnType<typeof reducers[K]> };
const combinedReducer = (state: any, action: any) => {
const newState: returnType = {} as any;
const keys = Object.keys(reducers);
keys.forEach(key => {
const result = reducers[key](state[key], action);
newState[key as keys] = result || state[key];
});
return newState;
};
return combinedReducer;
}

View File

@@ -0,0 +1,49 @@
import React, { useContext, useEffect, useState, useMemo } from 'react';
import { AppContext } from './AppContext';
import { DispatchObject } from '../util/types';
import { AppState } from './state';
interface ConnectParams<TOwnProps, TStateProps, TDispatchProps> {
mapStateToProps?: (state: AppState, props: TOwnProps) => TStateProps,
mapDispatchToProps?: TDispatchProps,
component: React.ComponentType<any>
};
export function connect<TOwnProps = any, TStateProps = any, TDispatchProps = any>({ mapStateToProps = () => ({} as TStateProps), mapDispatchToProps = {} as TDispatchProps, component }: ConnectParams<TOwnProps, TStateProps, TDispatchProps>): React.FunctionComponent<TOwnProps> {
const Connect = (ownProps: TOwnProps) => {
const context = useContext(AppContext);
const dispatchFuncs = useMemo(() => {
const dispatchFuncs: { [key: string]: any } = {};
Object.keys(mapDispatchToProps).forEach((key) => {
const oldFunc = (mapDispatchToProps as any)[key];
const newFunc = (...args: any) => {
const dispatchFunc = oldFunc(...args);
if (typeof dispatchFunc === 'object') {
context.dispatch(dispatchFunc);
} else {
const result = dispatchFunc(context.dispatch)
if (typeof result === 'object' && result.then) {
result.then((dispatchObject?: DispatchObject) => {
if (dispatchObject && dispatchObject.type) {
context.dispatch(dispatchObject);
}
})
}
}
}
dispatchFuncs[key] = newFunc
});
return dispatchFuncs;
}, [mapDispatchToProps])
const props = useMemo(() => {
return Object.assign({}, ownProps, mapStateToProps(context.state, ownProps), dispatchFuncs);
}, [ownProps, context.state]);
return React.createElement<TOwnProps>(component, props);
}
return React.memo(Connect as any);
}

View File

@@ -0,0 +1,73 @@
import { Plugins } from '@capacitor/core';
import { Session } from '../models/Session';
import { Speaker } from '../models/Speaker';
import { Location } from '../models/Location';
const { Storage } = Plugins;
const locationsUrl = '/assets/data/locations.json';
const sessionsUrl = '/assets/data/sessions.json';
const speakersUrl = '/assets/data/speakers.json';
const HAS_LOGGED_IN = 'hasLoggedIn';
const HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
const USERNAME = 'username';
export const getConfData = async () => {
const response = await Promise.all([
fetch(sessionsUrl),
fetch(locationsUrl),
fetch(speakersUrl),
]);
const sessions = (await response[0].json()) as Session[];
const locations = (await response[1].json()) as Location[];
const speakers = (await response[2].json()) as Speaker[];
const allTracks = sessions
.reduce((all, session) => all.concat(session.tracks), [] as string[])
.filter((trackName, index, array) => array.indexOf(trackName) === index)
.sort();
const data = {
sessions,
locations,
speakers,
allTracks,
filteredTracks: [...allTracks],
};
return data;
};
export const getUserData = async () => {
const response = await Promise.all([
Storage.get({ key: HAS_LOGGED_IN }),
Storage.get({ key: HAS_SEEN_TUTORIAL }),
Storage.get({ key: USERNAME }),
]);
const isLoggedin = (await response[0].value) === 'true';
const hasSeenTutorial = (await response[1].value) === 'true';
const username = (await response[2].value) || undefined;
const data = {
isLoggedin,
hasSeenTutorial,
username,
};
return data;
};
export const setIsLoggedInData = async (isLoggedIn: boolean) => {
await Storage.set({ key: HAS_LOGGED_IN, value: JSON.stringify(isLoggedIn) });
};
export const setHasSeenTutorialData = async (hasSeenTutorial: boolean) => {
await Storage.set({
key: HAS_SEEN_TUTORIAL,
value: JSON.stringify(hasSeenTutorial),
});
};
export const setUsernameData = async (username?: string) => {
if (!username) {
await Storage.remove({ key: USERNAME });
} else {
await Storage.set({ key: USERNAME, value: username });
}
};

View File

@@ -0,0 +1,139 @@
import { createSelector } from 'reselect';
import { parseISO as parseDate } from 'date-fns';
import { Session } from '../models/Session';
import { SessionGroup } from '../models/SessionGroup';
import { AppState } from './state';
const getSessions = (state: AppState) => state.data.sessions;
export const getSpeakers = (state: AppState) => state.data.speakers;
const getFilteredTracks = (state: AppState) => state.data.filteredTracks;
const getFavoriteIds = (state: AppState) => state.data.favorites;
const getSearchText = (state: AppState) => state.data.searchText;
export const getFilteredSessions = createSelector(
getSessions,
getFilteredTracks,
(sessions, filteredTracks) => {
return sessions.filter(session => {
let include = false;
session.tracks.forEach(track => {
if (filteredTracks.indexOf(track) > -1) {
include = true;
}
});
return include;
});
}
);
export const getSearchedSessions = createSelector(
getFilteredSessions,
getSearchText,
(sessions, searchText) => {
if (!searchText) {
return sessions;
}
return sessions.filter(
session =>
session.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
);
}
);
export const getGroupedSessions = createSelector(
getSearchedSessions,
sessions => {
return groupSessions(sessions);
}
);
export const getFavorites = createSelector(
getSearchedSessions,
getFavoriteIds,
(sessions, favoriteIds) =>
sessions.filter(x => favoriteIds.indexOf(x.id) > -1)
);
export const getGroupedFavorites = createSelector(
getFavorites,
sessions => {
return groupSessions(sessions);
}
);
const getIdParam = (_state: AppState, props: any) => {
const stringParam = props.match.params['id'];
return parseInt(stringParam, 10);
};
export const getSession = createSelector(
getSessions,
getIdParam,
(sessions, id) => sessions.find(x => x.id === id)
);
function groupSessions(sessions: Session[]) {
return sessions
.sort(
(a, b) =>
parseDate(a.dateTimeStart).valueOf() -
parseDate(b.dateTimeStart).valueOf()
)
.reduce(
(groups, session) => {
let starterHour = parseDate(session.dateTimeStart);
starterHour.setMinutes(0);
starterHour.setSeconds(0);
const starterHourStr = starterHour.toJSON();
const foundGroup = groups.find(
group => group.startTime === starterHourStr
);
if (foundGroup) {
foundGroup.sessions.push(session);
} else {
groups.push({
startTime: starterHourStr,
sessions: [session],
});
}
return groups;
},
[] as SessionGroup[]
);
}
export const getSpeaker = createSelector(
getSpeakers,
getIdParam,
(speakers, id) => speakers.find(x => x.id === id)
);
export const getSpeakerSessions = createSelector(
getSessions,
sessions => {
const speakerSessions: { [key: number]: Session[] } = {};
sessions.forEach(session => {
session.speakerIds.forEach(speakerId => {
if (speakerSessions[speakerId]) {
speakerSessions[speakerId].push(session);
} else {
speakerSessions[speakerId] = [session];
}
});
});
return speakerSessions;
}
);
export const mapCenter = (state: AppState) => {
const item = state.data.locations.find(l => l.id === state.data.mapCenterId);
if (item == null) {
return {
id: 1,
name: 'Map Center',
lat: 43.071584,
lng: -89.38012,
};
}
return item;
};

View File

@@ -0,0 +1,54 @@
import { getConfData } from '../dataApi';
import { ActionType } from '../../util/types';
import { SessionsState } from './sessions.state';
export const loadConfData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
const data = await getConfData();
dispatch(setData(data));
dispatch(setLoading(false));
};
export const setLoading = (isLoading: boolean) =>
({
type: 'set-conf-loading',
isLoading,
} as const);
export const setData = (data: Partial<SessionsState>) =>
({
type: 'set-conf-data',
data,
} as const);
export const addFavorite = (sessionId: number) =>
({
type: 'add-favorite',
sessionId,
} as const);
export const removeFavorite = (sessionId: number) =>
({
type: 'remove-favorite',
sessionId,
} as const);
export const updateFilteredTracks = (filteredTracks: string[]) =>
({
type: 'update-filtered-tracks',
filteredTracks,
} as const);
export const setSearchText = (searchText?: string) =>
({
type: 'set-search-text',
searchText,
} as const);
export type SessionsActions =
| ActionType<typeof setLoading>
| ActionType<typeof setData>
| ActionType<typeof addFavorite>
| ActionType<typeof removeFavorite>
| ActionType<typeof updateFilteredTracks>
| ActionType<typeof setSearchText>;

View File

@@ -0,0 +1,31 @@
import { SessionsActions } from './sessions.actions';
import { SessionsState } from './sessions.state';
export const sessionsReducer = (
state: SessionsState,
action: SessionsActions
): SessionsState => {
switch (action.type) {
case 'set-conf-loading': {
return { ...state, loading: action.isLoading };
}
case 'set-conf-data': {
return { ...state, ...action.data };
}
case 'add-favorite': {
return { ...state, favorites: [...state.favorites, action.sessionId] };
}
case 'remove-favorite': {
return {
...state,
favorites: [...state.favorites.filter(x => x !== action.sessionId)],
};
}
case 'update-filtered-tracks': {
return { ...state, filteredTracks: action.filteredTracks };
}
case 'set-search-text': {
return { ...state, searchText: action.searchText };
}
}
};

View File

@@ -0,0 +1,14 @@
import { Location } from '../../models/Location';
import { Speaker } from '../../models/Speaker';
import { Session } from '../../models/Session';
export interface SessionsState {
sessions: Session[];
speakers: Speaker[];
favorites: number[];
locations: Location[];
filteredTracks: string[];
searchText?: string;
mapCenterId?: number;
loading?: boolean;
allTracks: string[];
}

View File

@@ -0,0 +1,29 @@
import { combineReducers } from './combineReducers';
import { sessionsReducer } from './sessions/sessions.reducer';
import { userReducer } from './user/user.reducer';
export const initialState: AppState = {
data: {
sessions: [],
speakers: [],
favorites: [],
locations: [],
allTracks: [],
filteredTracks: [],
mapCenterId: 0,
loading: false,
},
user: {
hasSeenTutorial: false,
darkMode: false,
isLoggedin: false,
loading: false,
},
};
export const reducers = combineReducers({
data: sessionsReducer,
user: userReducer,
});
export type AppState = ReturnType<typeof reducers>;

View File

@@ -0,0 +1,76 @@
import {
getUserData,
setIsLoggedInData,
setUsernameData,
setHasSeenTutorialData,
} from '../dataApi';
import { ActionType } from '../../util/types';
import { UserState } from './user.state';
export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
const data = await getUserData();
dispatch(setData(data));
dispatch(setLoading(false));
};
export const setLoading = (isLoading: boolean) =>
({
type: 'set-user-loading',
isLoading,
} as const);
export const setData = (data: Partial<UserState>) =>
({
type: 'set-user-data',
data,
} as const);
export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(false);
dispatch(setUsername());
};
export const setIsLoggedIn = (loggedIn: boolean) => async (
dispatch: React.Dispatch<any>
) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setUsername = (username?: string) => async (
dispatch: React.Dispatch<any>
) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setHasSeenTutorial = (hasSeenTutorial: boolean) => async (
dispatch: React.Dispatch<any>
) => {
await setHasSeenTutorialData(hasSeenTutorial);
return {
type: 'set-has-seen-tutorial',
hasSeenTutorial,
} as const;
};
export const setDarkMode = (darkMode: boolean) =>
({
type: 'set-dark-mode',
darkMode,
} as const);
export type UserActions =
| ActionType<typeof setLoading>
| ActionType<typeof setData>
| ActionType<typeof setIsLoggedIn>
| ActionType<typeof setUsername>
| ActionType<typeof setHasSeenTutorial>
| ActionType<typeof setDarkMode>;

View File

@@ -0,0 +1,19 @@
import { UserActions } from './user.actions';
import { UserState } from './user.state';
export function userReducer(state: UserState, action: UserActions): UserState {
switch (action.type) {
case 'set-user-loading':
return { ...state, loading: action.isLoading };
case 'set-user-data':
return { ...state, ...action.data };
case 'set-username':
return { ...state, username: action.username };
case 'set-has-seen-tutorial':
return { ...state, hasSeenTutorial: action.hasSeenTutorial };
case 'set-dark-mode':
return { ...state, darkMode: action.darkMode };
case 'set-is-loggedin':
return { ...state, isLoggedin: action.loggedIn };
}
}

View File

@@ -0,0 +1,7 @@
export interface UserState {
isLoggedin: boolean;
username?: string;
darkMode: boolean;
hasSeenTutorial: boolean;
loading: boolean;
}

View File

@@ -0,0 +1,5 @@
export interface AppPage {
url: string;
icon: object;
title: string;
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

View File

@@ -0,0 +1,6 @@
export interface Location {
id: number;
name?: string;
lat: number;
lng: number;
}

View File

@@ -0,0 +1,10 @@
export interface Session {
id: number;
dateTimeStart: string;
dateTimeEnd: string;
name: string;
location: string;
description: string;
speakerIds: number[];
tracks: string[];
}

View File

@@ -0,0 +1,5 @@
import { Session } from './Session';
export interface SessionGroup {
startTime: string;
sessions: Session[];
}

View File

@@ -0,0 +1,10 @@
export interface Speaker {
id: number;
name: string;
profilePic: string;
twitter: string;
about: string;
location: string;
email: string;
phone: string;
}

View File

@@ -0,0 +1,26 @@
#about-page {
.about-header {
background-color: #222;
padding: 16px;
width: 100%;
height: 30%;
text-align: center;
}
.about-header img {
max-height: 100%;
}
.about-info p {
color: var(--ion-color-dark);
text-align: left;
}
.about-info ion-icon {
margin-inline-end: 32px;
}
.ios .about-info {
text-align: center;
}
}

View File

@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonButton, IonIcon, IonDatetime, IonSelectOption, IonList, IonItem, IonLabel, IonSelect, IonPopover } from '@ionic/react';
import './About.scss';
import { calendar, pin, more } from 'ionicons/icons';
import AboutPopover from '../components/AboutPopover';
interface AboutProps { }
const About: React.FC<AboutProps> = () => {
const [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState();
const presentPopover = (e: React.MouseEvent) => {
setPopoverEvent(e.nativeEvent);
setShowPopover(true);
};
const conferenceDate = '2047-05-17';
return (
<IonPage id="about-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>About</IonTitle>
<IonButtons slot="end">
<IonButton icon-only onClick={presentPopover}>
<IonIcon slot="icon-only" icon={more}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="about-header">
<img src="assets/img/ionic-logo-white.svg" alt="ionic logo" />
</div>
<div className="about-info">
<h4 className="ion-padding-start">Ionic Conference</h4>
<IonList lines="none">
<IonItem>
<IonIcon icon={calendar} slot="start"></IonIcon>
<IonLabel position="stacked">Date</IonLabel>
<IonDatetime displayFormat="MMM DD, YYYY" max="2056" value={conferenceDate}></IonDatetime>
</IonItem>
<IonItem>
<IonIcon icon={pin} slot="start"></IonIcon>
<IonLabel position="stacked">Location</IonLabel>
<IonSelect>
<IonSelectOption value="madison" selected>Madison, WI</IonSelectOption>
<IonSelectOption value="austin">Austin, TX</IonSelectOption>
<IonSelectOption value="chicago">Chicago, IL</IonSelectOption>
<IonSelectOption value="seattle">Seattle, WA</IonSelectOption>
</IonSelect>
</IonItem>
</IonList>
<p className="ion-padding-start ion-padding-end">
The Ionic Conference is a one-day conference featuring talks from the Ionic team. It is focused on Ionic applications being
built with Ionic 2. This includes migrating apps from Ionic 1 to Ionic 2, 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>
</div>
</IonContent>
<IonPopover
isOpen={showPopover}
event={popoverEvent}
onDidDismiss={() => setShowPopover(false)}
>
<AboutPopover dismiss={() => setShowPopover(false)} />
</IonPopover>
</IonPage>
);
};
export default React.memo(About);

View File

@@ -0,0 +1,6 @@
#account-page {
img {
max-width: 140px;
border-radius: 50%;
}
}

View File

@@ -0,0 +1,87 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonList, IonItem, IonAlert } from '@ionic/react';
import './Account.scss';
import { setUsername } from '../data/user/user.actions';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps { }
interface StateProps {
username?: string;
}
interface DispatchProps {
setUsername: typeof setUsername;
}
interface AccountProps extends OwnProps, StateProps, DispatchProps { }
const Account: React.FC<AccountProps> = ({ setUsername, username }) => {
const [showAlert, setShowAlert] = useState(false);
const clicked = (text: string) => {
console.log(`Clicked ${text}`);
}
return (
<IonPage id="account-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Account</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{username &&
(<div className="ion-padding-top ion-text-center">
<img src="https://www.gravatar.com/avatar?d=mm&s=140" alt="avatar" />
<h2>{ username }</h2>
<IonList inset>
<IonItem onClick={() => clicked('Update Picture')}>Update Picture</IonItem>
<IonItem onClick={() => setShowAlert(true)}>Change Username</IonItem>
<IonItem onClick={() => clicked('Change Password')}>Change Password</IonItem>
<IonItem routerLink="/support" routerDirection="none">Support</IonItem>
<IonItem routerLink="/logout" routerDirection="none">Logout</IonItem>
</IonList>
</div>)
}
</IonContent>
<IonAlert
isOpen={showAlert}
header="Change Username"
buttons={[
'Cancel',
{
text: 'Ok',
handler: (data) => {
setUsername(data.username);
}
}
]}
inputs={[
{
type: 'text',
name: 'username',
value: username,
placeholder: 'username'
}
]}
onDidDismiss={() => setShowAlert(false)}
/>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
username: state.user.username
}),
mapDispatchToProps: {
setUsername,
},
component: Account
})

View File

@@ -0,0 +1,16 @@
#login-page, #signup-page, #support-page {
.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,108 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonRow, IonCol, IonButton, IonList, IonItem, IonLabel, IonInput, IonText } from '@ionic/react';
import './Login.scss';
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps {}
interface DispatchProps {
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface LoginProps extends OwnProps, DispatchProps { }
const Login: React.FC<LoginProps> = ({setIsLoggedIn, history, setUsername: setUsernameAction}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [usernameError, setUsernameError] = useState(false);
const [passwordError, setPasswordError] = useState(false);
const login = async (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if(!username) {
setUsernameError(true);
}
if(!password) {
setPasswordError(true);
}
if(username && password) {
await setIsLoggedIn(true);
await setUsernameAction(username);
history.push('/tabs/schedule', {direction: 'none'});
}
};
return (
<IonPage id="login-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Login</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={login}>
<IonList>
<IonItem>
<IonLabel position="stacked" color="primary">Username</IonLabel>
<IonInput name="username" type="text" value={username} spellCheck={false} autocapitalize="off" onIonChange={e => setUsername(e.detail.value!)}
required>
</IonInput>
</IonItem>
{formSubmitted && usernameError && <IonText color="danger">
<p className="ion-padding-start">
Username is required
</p>
</IonText>}
<IonItem>
<IonLabel position="stacked" color="primary">Password</IonLabel>
<IonInput name="password" type="password" value={password} onIonChange={e => setPassword(e.detail.value!)}>
</IonInput>
</IonItem>
{formSubmitted && passwordError && <IonText color="danger">
<p className="ion-padding-start">
Password is required
</p>
</IonText>}
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">Login</IonButton>
</IonCol>
<IonCol>
<IonButton routerLink="/signup" color="light" expand="block">Signup</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: {
setIsLoggedIn,
setUsername
},
component: Login
})

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { calendar, contacts, map, informationCircle } from 'ionicons/icons';
import SchedulePage from './SchedulePage';
import SpeakerList from './SpeakerList';
import SpeakerDetail from './SpeakerDetail';
import SessionDetail from './SessionDetail';
import MapView from './MapView';
import About from './About';
interface MainTabsProps { }
const MainTabs: React.FC<MainTabsProps> = () => {
return (
<IonTabs>
<IonRouterOutlet>
<Redirect exact path="/tabs" to="/tabs/schedule" />
{/*
Using the render method prop cuts down the number of renders your components will have due to route changes.
Use the component prop when your component depends on the RouterComponentProps passed in automatically.
*/}
<Route path="/tabs/schedule" render={() => <SchedulePage />} exact={true} />
<Route path="/tabs/speakers" render={() => <SpeakerList />} exact={true} />
<Route path="/tabs/speakers/:id" component={SpeakerDetail} exact={true} />
<Route path="/tabs/schedule/:id" component={SessionDetail} />
<Route path="/tabs/speakers/sessions/:id" component={SessionDetail} />
<Route path="/tabs/map" render={() => <MapView />} exact={true} />
<Route path="/tabs/about" render={() => <About />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="schedule" href="/tabs/schedule">
<IonIcon icon={calendar} />
<IonLabel>Schedule</IonLabel>
</IonTabButton>
<IonTabButton tab="speakers" href="/tabs/speakers">
<IonIcon icon={contacts} />
<IonLabel>Speakers</IonLabel>
</IonTabButton>
<IonTabButton tab="map" href="/tabs/map">
<IonIcon icon={map} />
<IonLabel>Map</IonLabel>
</IonTabButton>
<IonTabButton tab="about" href="/tabs/about">
<IonIcon icon={informationCircle} />
<IonLabel>About</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default MainTabs;

View File

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

View File

@@ -0,0 +1,44 @@
import React from 'react';
import Map from '../components/Map';
import { IonHeader, IonToolbar, IonButtons, IonMenuButton, IonTitle, IonContent, IonPage } from '@ionic/react';
import { Location } from '../models/Location';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import './MapView.scss';
interface OwnProps { }
interface StateProps {
locations: Location[];
mapCenter: Location;
}
interface DispatchProps { }
interface MapViewProps extends OwnProps, StateProps, DispatchProps { };
const MapView: React.FC<MapViewProps> = ({ locations, mapCenter }) => {
return (
<IonPage id="map-view">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Map</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="map-page">
<Map locations={locations} mapCenter={mapCenter} />
</IonContent>
</IonPage>
)};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
locations: state.data.locations,
mapCenter: selectors.mapCenter(state)
}),
component: MapView
});

View File

@@ -0,0 +1,42 @@
#schedule-page {
ion-item-sliding.track-ionic ion-label {
border-left: 2px solid var(--ion-color-primary);
padding-left: 10px;
}
ion-item-sliding.track-angular ion-label {
border-left: 2px solid var(--ion-color-angular);
padding-left: 10px;
}
ion-item-sliding.track-communication ion-label {
border-left: 2px solid var(--ion-color-communication);
padding-left: 10px;
}
ion-item-sliding.track-tooling ion-label {
border-left: 2px solid var(--ion-color-tooling);
padding-left: 10px;
}
ion-item-sliding.track-services ion-label {
border-left: 2px solid var(--ion-color-services);
padding-left: 10px;
}
ion-item-sliding.track-design ion-label {
border-left: 2px solid var(--ion-color-design);
padding-left: 10px;
}
ion-item-sliding.track-workshop ion-label {
border-left: 2px solid var(--ion-color-workshop);
padding-left: 10px;
}
ion-item-sliding.track-food ion-label {
border-left: 2px solid var(--ion-color-food);
padding-left: 10px;
}
ion-item-sliding.track-documentation ion-label {
border-left: 2px solid var(--ion-color-documentation);
padding-left: 10px;
}
ion-item-sliding.track-navigation ion-label {
border-left: 2px solid var(--ion-color-navigation);
padding-left: 10px;
}
}

View File

@@ -0,0 +1,120 @@
import React, { useState, useRef } from 'react';
import { IonToolbar, IonContent, IonPage, IonButtons, IonMenuButton, IonSegment, IonSegmentButton, IonButton, IonIcon, IonSearchbar, IonRefresher, IonRefresherContent, IonToast, IonModal, IonHeader, getConfig } from '@ionic/react';
import { connect } from '../data/connect';
import { options } from 'ionicons/icons';
import SessionList from '../components/SessionList';
import SessionListFilter from '../components/SessionListFilter';
import './SchedulePage.scss'
import * as selectors from '../data/selectors';
import { setSearchText, addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
import ShareSocialFab from '../components/ShareSocialFab';
import { SessionGroup } from '../models/SessionGroup';
interface OwnProps { }
interface StateProps {
sessionGroups: SessionGroup[];
favoriteGroups: SessionGroup[];
mode: 'ios' | 'md'
}
interface DispatchProps {
setSearchText: typeof setSearchText;
}
type SchedulePageProps = OwnProps & StateProps & DispatchProps;
const SchedulePage: React.FC<SchedulePageProps> = ({ favoriteGroups, sessionGroups, setSearchText, mode }) => {
const [segment, setSegment] = useState<'all' | 'favorites'>('all');
const [showFilterModal, setShowFilterModal] = useState(false);
const ionRefresherRef = useRef<HTMLIonRefresherElement>(null);
const [showCompleteToast, setShowCompleteToast] = useState(false);
const doRefresh = () => {
setTimeout(() => {
ionRefresherRef.current!.complete();
setShowCompleteToast(true);
}, 2500)
};
return (
<IonPage id="schedule-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonSegment onIonChange={(e) => setSegment(e.detail.value as any)}>
<IonSegmentButton value="all" checked={segment === 'all'}>
All
</IonSegmentButton>
<IonSegmentButton value="favorites" checked={segment === 'favorites'}>
Favorites
</IonSegmentButton>
</IonSegment>
<IonButtons slot="end">
<IonButton onClick={() => setShowFilterModal(true)}>
{mode === 'ios' ? 'Filter' : <IonIcon icon={options} slot="icon-only" />}
</IonButton>
</IonButtons>
</IonToolbar>
<IonToolbar>
<IonSearchbar
placeholder="Search"
onIonChange={(e: CustomEvent) => setSearchText(e.detail.value)}
/>
</IonToolbar>
</IonHeader>
<IonContent>
<IonRefresher slot="fixed" ref={ionRefresherRef} onIonRefresh={doRefresh}>
<IonRefresherContent />
</IonRefresher>
<IonToast
isOpen={showCompleteToast}
message="Refresh complete"
duration={2000}
onDidDismiss={() => setShowCompleteToast(false)}
/>
<SessionList
sessionGroups={sessionGroups}
listType={segment}
hide={segment === 'favorites'}
/>
<SessionList
sessionGroups={favoriteGroups}
listType={segment}
hide={segment === 'all'}
/>
</IonContent>
<IonModal
isOpen={showFilterModal}
onDidDismiss={() => setShowFilterModal(false)}
>
<SessionListFilter
onDismissModal={() => setShowFilterModal(false)}
/>
</IonModal>
<ShareSocialFab />
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
sessionGroups: selectors.getGroupedSessions(state),
favoriteGroups: selectors.getGroupedFavorites(state),
mode: getConfig()!.get('mode')
}),
mapDispatchToProps: {
setSearchText
},
component: React.memo(SchedulePage)
});

View File

@@ -0,0 +1,73 @@
#session-detail-page {
.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);
}
.show-favorite {
position: relative;
}
.icon-heart-empty {
position: absolute;
top: 5px;
right: 5px;
transform: scale(1);
transition: transform 0.3s ease;
}
.icon-heart {
position: absolute;
top: 5px;
right: 5px;
transform: scale(0);
transition: transform 0.3s ease;
}
.show-favorite .icon-heart {
transform: scale(1);
}
.show-favorite .icon-heart-empty {
transform: scale(0);
}
h1 {
margin: 0;
}
}

View File

@@ -0,0 +1,108 @@
import React from 'react';
import { IonHeader, IonToolbar, IonContent, IonPage, IonButtons, IonBackButton, IonButton, IonIcon, IonText, IonList, IonItem, IonLabel } from '@ionic/react';
import { connect } from '../data/connect';
import { withRouter, RouteComponentProps } from 'react-router';
import * as selectors from '../data/selectors';
import { starOutline, star, share, cloudDownload } from 'ionicons/icons';
import './SessionDetail.scss';
import { Time } from '../components/Time';
import { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
import { Session } from '../models/Session';
interface OwnProps extends RouteComponentProps { };
interface StateProps {
session?: Session;
favoriteSessions: number[],
};
interface DispatchProps {
addFavorite: typeof addFavorite;
removeFavorite: typeof removeFavorite;
}
type SessionDetailProps = OwnProps & StateProps & DispatchProps;
const SessionDetail: React.FC<SessionDetailProps> = ({ session, addFavorite, removeFavorite, favoriteSessions }) => {
if (!session) {
return <div>Session not found</div>
}
const isFavorite = favoriteSessions.indexOf(session.id) > -1;
const toggleFavorite = () => {
isFavorite ? removeFavorite(session.id) : addFavorite(session.id);
};
const shareSession = () => { };
const sessionClick = (text: string) => {
console.log(`Clicked ${text}`);
};
return (
<IonPage id="session-detail-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/tabs/schedule"></IonBackButton>
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={() => toggleFavorite()}>
{isFavorite ?
<IonIcon slot="icon-only" icon={star}></IonIcon> :
<IonIcon slot="icon-only" icon={starOutline}></IonIcon>
}
</IonButton>
<IonButton onClick={() => shareSession}>
<IonIcon slot="icon-only" icon={share}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="ion-padding">
<h1>{session.name}</h1>
{session.tracks.map(track => (
<span key={track} className={`session-track-${track.toLowerCase()}`}>{track}</span>
))}
<p>{session.description}</p>
<IonText color="medium">
<Time date={session.dateTimeStart} /> &ndash; <Time date={session.dateTimeEnd} />
<br />
{session.location}
</IonText>
</div>
<IonList>
<IonItem onClick={() => sessionClick('watch')} button>
<IonLabel color="primary">Watch</IonLabel>
</IonItem>
<IonItem onClick={() => sessionClick('add to calendar')} button>
<IonLabel color="primary">Add to Calendar</IonLabel>
</IonItem>
<IonItem onClick={() => sessionClick('mark as unwatched')} button>
<IonLabel color="primary">Mark as Unwatched</IonLabel>
</IonItem>
<IonItem onClick={() => sessionClick('download video')} button>
<IonLabel color="primary">Download Video</IonLabel>
<IonIcon slot="end" color="primary" size="small" icon={cloudDownload}></IonIcon>
</IonItem>
<IonItem onClick={() => sessionClick('leave feedback')} button>
<IonLabel color="primary">Leave Feedback</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state, OwnProps) => ({
session: selectors.getSession(state, OwnProps),
favoriteSessions: state.data.favorites
}),
mapDispatchToProps: {
addFavorite,
removeFavorite
},
component: withRouter(SessionDetail)
})

View File

@@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonRow, IonCol, IonButton, IonList, IonItem, IonLabel, IonInput, IonText } from '@ionic/react';
import './Login.scss';
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps {}
interface DispatchProps {
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface LoginProps extends OwnProps, DispatchProps { }
const Login: React.FC<LoginProps> = ({setIsLoggedIn, history, setUsername: setUsernameAction}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [usernameError, setUsernameError] = useState(false);
const [passwordError, setPasswordError] = useState(false);
const login = async (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if(!username) {
setUsernameError(true);
}
if(!password) {
setPasswordError(true);
}
if(username && password) {
await setIsLoggedIn(true);
await setUsernameAction(username);
history.push('/tabs/schedule', {direction: 'none'});
}
};
return (
<IonPage id="signup-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Signup</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={login}>
<IonList>
<IonItem>
<IonLabel position="stacked" color="primary">Username</IonLabel>
<IonInput name="username" type="text" value={username} spellCheck={false} autocapitalize="off" onIonChange={e => {
setUsername(e.detail.value!);
setUsernameError(false);
}}
required>
</IonInput>
</IonItem>
{formSubmitted && usernameError && <IonText color="danger">
<p className="ion-padding-start">
Username is required
</p>
</IonText>}
<IonItem>
<IonLabel position="stacked" color="primary">Password</IonLabel>
<IonInput name="password" type="password" value={password} onIonChange={e => {
setPassword(e.detail.value!);
setPasswordError(false);
}}>
</IonInput>
</IonItem>
{formSubmitted && passwordError && <IonText color="danger">
<p className="ion-padding-start">
Password is required
</p>
</IonText>}
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">Create</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: {
setIsLoggedIn,
setUsername
},
component: Login
})

View File

@@ -0,0 +1,11 @@
#speaker-detail {
img {
max-width: 140px;
border-radius: 50%;
}
p {
color: #60646B;
}
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { IonIcon, IonHeader, IonToolbar, IonButtons, IonTitle, IonContent, IonButton, IonBackButton, IonPage } from '@ionic/react'
import './SpeakerDetail.scss';
import { logoTwitter, logoGithub, logoInstagram } from 'ionicons/icons';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import { Speaker } from '../models/Speaker';
interface OwnProps extends RouteComponentProps {
speaker?: Speaker;
};
interface StateProps {};
interface DispatchProps {};
interface SpeakerDetailProps extends OwnProps, StateProps, DispatchProps {};
const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
if (!speaker) {
return <div>Speaker not found</div>
}
return (
<IonPage id="speaker-detail">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/tabs/speakers" />
</IonButtons>
<IonTitle>{speaker.name}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding speaker-detail speaker-page-list">
<div className="ion-text-center">
<img src={speaker.profilePic} alt={speaker.name} />
<br />
<IonButton fill="clear" size="small" color="twitter">
<IonIcon icon={logoTwitter} slot="icon-only"></IonIcon>
</IonButton>
<IonButton fill="clear" size="small" color="github">
<IonIcon icon={logoGithub} slot="icon-only"></IonIcon>
</IonButton>
<IonButton fill="clear" size="small" color="instagram">
<IonIcon icon={logoInstagram} slot="icon-only"></IonIcon>
</IonButton>
</div>
<p>{speaker.about}</p>
</IonContent>
</IonPage>
);
};
export default connect({
mapStateToProps: (state, ownProps) => ({
speaker: selectors.getSpeaker(state, ownProps)
}),
component: SpeakerDetail
});

View File

@@ -0,0 +1,24 @@
#speaker-list {
.scroll {
background: #ededed;
}
.speaker-card {
height: 100%;
display: flex;
flex-direction: column;
}
.speaker-card ion-card-header {
padding: 0;
}
.speaker-card ion-card-header .item {
padding: 4px 16px;
}
.speaker-card ion-card-content {
flex: 1 1 auto;
padding: 0;
}
}

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonList, IonGrid, IonRow, IonCol } from '@ionic/react';
import SpeakerItem from '../components/SpeakerItem';
import { Speaker } from '../models/Speaker';
import { Session } from '../models/Session';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import './SpeakerList.scss';
interface OwnProps { };
interface StateProps {
speakers: Speaker[];
speakerSessions: { [key: number]: Session[] };
};
interface DispatchProps { };
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps { };
const SpeakerList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) => {
return (
<IonPage id="speaker-list">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Speakers</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className={`outer-content`}>
<IonList>
<IonGrid fixed>
<IonRow align-items-stretch>
{speakers.map(speaker => (
<IonCol size="12" size-md="6" key={speaker.id}>
<SpeakerItem
key={speaker.id}
speaker={speaker}
sessions={speakerSessions[speaker.id]}
/>
</IonCol>
))}
</IonRow>
</IonGrid>
</IonList>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
speakers: selectors.getSpeakers(state),
speakerSessions: selectors.getSpeakerSessions(state)
}),
component: React.memo(SpeakerList)
});

View File

@@ -0,0 +1,83 @@
import React, { useState } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonButtons, IonMenuButton, IonRow, IonCol, IonButton, IonList, IonItem, IonLabel, IonText, IonTextarea, IonToast } from '@ionic/react';
import './Login.scss';
import { connect } from '../data/connect';
interface OwnProps { }
interface DispatchProps { }
interface SupportProps extends OwnProps, DispatchProps { }
const Support: React.FC<SupportProps> = () => {
const [message, setMessage] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [messageError, setMessageError] = useState(false);
const [showToast, setShowToast] = useState(false);
const send = (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if (!message) {
setMessageError(true);
}
if (message) {
setMessage('');
setShowToast(true);
}
};
return (
<IonPage id="support-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Support</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={send}>
<IonList>
<IonItem>
<IonLabel position="stacked" color="primary">Enter your support message below</IonLabel>
<IonTextarea name="message" value={message} spellCheck={false} autocapitalize="off" rows={6} onIonChange={e => setMessage(e.detail.value!)}
required>
</IonTextarea>
</IonItem>
{formSubmitted && messageError && <IonText color="danger">
<p className="ion-padding-start">
Support message is required
</p>
</IonText>}
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">Submit</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
<IonToast
isOpen={showToast}
duration={3000}
message="Your support request has been sent"
onDidDismiss={() => setShowToast(false)} />
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
component: Support
})

View File

@@ -0,0 +1,38 @@
#tutorial-page {
ion-toolbar {
// TODO test transparent and fullscreen
--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,87 @@
import React, { useState, useRef } from 'react';
import { IonContent, IonPage, IonHeader, IonToolbar, IonButtons, IonButton, IonSlides, IonSlide, IonIcon } from '@ionic/react';
import { arrowForward } from 'ionicons/icons';
import { setHasSeenTutorial } from '../data/user/user.actions';
import './Tutorial.scss';
import { connect } from '../data/connect';
import { RouteComponentProps } from 'react-router';
interface OwnProps extends RouteComponentProps {};
interface DispatchProps {
setHasSeenTutorial: typeof setHasSeenTutorial
}
interface TutorialProps extends OwnProps, DispatchProps { };
const Tutorial: React.FC<TutorialProps> = ({ history, setHasSeenTutorial }) => {
const [showSkip, setShowSkip] = useState(true);
const slideRef = useRef<HTMLIonSlidesElement>(null);
const startApp = async () => {
await setHasSeenTutorial(true);
history.push('/tabs/schedule', { direction: 'none' });
};
const handleSlideChangeStart = () => {
slideRef.current!.isEnd().then(isEnd => setShowSkip(!isEnd));
};
return (
<IonPage id="tutorial-page">
<IonHeader no-border>
<IonToolbar>
<IonButtons slot="end">
{showSkip && <IonButton color='primary' onClick={startApp}>Skip</IonButton>}
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonSlides ref={slideRef} onIonSlideWillChange={handleSlideChangeStart} pager={false}>
<IonSlide>
<img src="assets/img/ica-slidebox-img-1.png" alt="" className="slide-image" />
<h2 className="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>
</IonSlide>
<IonSlide>
<img src="assets/img/ica-slidebox-img-2.png" alt="" className="slide-image" />
<h2 className="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>
</IonSlide>
<IonSlide>
<img src="assets/img/ica-slidebox-img-3.png" alt="" className="slide-image" />
<h2 className="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>
</IonSlide>
<IonSlide>
<img src="assets/img/ica-slidebox-img-4.png" alt="" className="slide-image" />
<h2 className="slide-title">Ready to Play?</h2>
<IonButton fill="clear" onClick={startApp}>
Continue
<IonIcon slot="end" icon={arrowForward} />
</IonButton>
</IonSlide>
</IonSlides>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: ({
setHasSeenTutorial
}),
component: Tutorial
});

View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,179 @@
/* Ionic Variables and Theming. For more information, please see
// https://beta.ionicframework.com/docs/theming/
// The app direction is used to include
// rtl styles in your app. For more information, please see
// https://beta.ionicframework.com/docs/layout/rtl
// $app-direction: ltr;
// Ionic Colors
// --------------------------------------------------
// Named colors makes it easy to reuse colors on various components.
// It's highly recommended to change the default colors
// to match your app's branding. Ionic provides eight layered colors
// that can be changed to theme an app. Additional colors can be
// added as well (see below). For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
// To easily create custom color palettes for your apps UI,
// check out our color generator:
// https://beta.ionicframework.com/docs/theming/color-generator
*/
:root {
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
--ion-color-secondary: #0cd1e8;
--ion-color-secondary-rgb: 12, 209, 232;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #0bb8cc;
--ion-color-secondary-tint: #24d6ea;
--ion-color-tertiary: #7044ff;
--ion-color-tertiary-rgb: 112, 68, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #633ce0;
--ion-color-tertiary-tint: #7e57ff;
--ion-color-success: #10dc60;
--ion-color-success-rgb: 16, 220, 96;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #0ec254;
--ion-color-success-tint: #28e070;
--ion-color-warning: #ffce00;
--ion-color-warning-rgb: 255, 206, 0;
--ion-color-warning-contrast: #ffffff;
--ion-color-warning-contrast-rgb: 255, 255, 255;
--ion-color-warning-shade: #e0b500;
--ion-color-warning-tint: #ffd31a;
--ion-color-danger: #f04141;
--ion-color-danger-rgb: 245, 61, 61;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #d33939;
--ion-color-danger-tint: #f25454;
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 34, 34;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 244, 244;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
/* Additional Ionic Colors
// --------------------------------------------------
// In order to add colors to be used with Ionic components,
// the color should be added as a class with the convention `.ion-color-{COLOR}`
// where `{COLOR}` is the color to be used on the Ionic component
// and each variant is defined for the color. For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
*/
.ion-color-favorite {
--ion-color-base: #69bb7b;
--ion-color-base-rgb: 105, 187, 123;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #5ca56c;
--ion-color-tint: #78c288;
}
.ion-color-twitter {
--ion-color-base: #1da1f4;
--ion-color-base-rgb: 29, 161, 244;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1a8ed7;
--ion-color-tint: #34aaf5;
}
.ion-color-google {
--ion-color-base: #dc4a38;
--ion-color-base-rgb: 220, 74, 56;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #c24131;
--ion-color-tint: #e05c4c;
}
.ion-color-vimeo {
--ion-color-base: #23b6ea;
--ion-color-base-rgb: 35, 182, 234;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1fa0ce;
--ion-color-tint: #39bdec;
}
.ion-color-facebook {
--ion-color-base: #3b5998;
--ion-color-base-rgb: 59, 89, 152;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #344e86;
--ion-color-tint: #4f6aa2;
}
/* Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the CSS variables found in Ionic's source files.
// To view all the possible Ionic variables, see:
// https://beta.ionicframework.com/docs/theming/css-variables#ionic-variables
*/
:root {
--ion-headings-font-weight: 300;
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
}
.md {
--ion-toolbar-background: var(--ion-color-primary);
--ion-toolbar-color: #fff;
--ion-toolbar-color-activated: #fff;
}

View File

@@ -0,0 +1,320 @@
/* Ionic Variables and Theming. For more information, please see
// https://beta.ionicframework.com/docs/theming/
// The app direction is used to include
// rtl styles in your app. For more information, please see
// https://beta.ionicframework.com/docs/layout/rtl
// $app-direction: ltr;
// Ionic Colors
// --------------------------------------------------
// Named colors makes it easy to reuse colors on various components.
// It's highly recommended to change the default colors
// to match your app's branding. Ionic provides eight layered colors
// that can be changed to theme an app. Additional colors can be
// added as well (see below). For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
// To easily create custom color palettes for your apps UI,
// check out our color generator:
// https://beta.ionicframework.com/docs/theming/color-generator
*/
:root {
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
--ion-color-secondary: #0cd1e8;
--ion-color-secondary-rgb: 12, 209, 232;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #0bb8cc;
--ion-color-secondary-tint: #24d6ea;
--ion-color-tertiary: #7044ff;
--ion-color-tertiary-rgb: 112, 68, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #633ce0;
--ion-color-tertiary-tint: #7e57ff;
--ion-color-success: #10dc60;
--ion-color-success-rgb: 16, 220, 96;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #0ec254;
--ion-color-success-tint: #28e070;
--ion-color-warning: #ffce00;
--ion-color-warning-rgb: 255, 206, 0;
--ion-color-warning-contrast: #ffffff;
--ion-color-warning-contrast-rgb: 255, 255, 255;
--ion-color-warning-shade: #e0b500;
--ion-color-warning-tint: #ffd31a;
--ion-color-danger: #f04141;
--ion-color-danger-rgb: 245, 61, 61;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #d33939;
--ion-color-danger-tint: #f25454;
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 34, 34;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 244, 244;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
/* Additional Ionic Colors
// --------------------------------------------------
// In order to add colors to be used with Ionic components,
// the color should be added as a class with the convention `.ion-color-{COLOR}`
// where `{COLOR}` is the color to be used on the Ionic component
// and each variant is defined for the color. For more information, please see
// https://beta.ionicframework.com/docs/theming/advanced
*/
.ion-color-favorite {
--ion-color-base: #69bb7b;
--ion-color-base-rgb: 105, 187, 123;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #5ca56c;
--ion-color-tint: #78c288;
}
.ion-color-twitter {
--ion-color-base: #1da1f4;
--ion-color-base-rgb: 29, 161, 244;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1a8ed7;
--ion-color-tint: #34aaf5;
}
.ion-color-google {
--ion-color-base: #dc4a38;
--ion-color-base-rgb: 220, 74, 56;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #c24131;
--ion-color-tint: #e05c4c;
}
.ion-color-vimeo {
--ion-color-base: #23b6ea;
--ion-color-base-rgb: 35, 182, 234;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #1fa0ce;
--ion-color-tint: #39bdec;
}
.ion-color-facebook {
--ion-color-base: #3b5998;
--ion-color-base-rgb: 59, 89, 152;
--ion-color-contrast: #ffffff;
--ion-color-contrast-rgb: 255, 255, 255;
--ion-color-shade: #344e86;
--ion-color-tint: #4f6aa2;
}
/* Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the CSS variables found in Ionic's source files.
// To view all the possible Ionic variables, see:
// https://beta.ionicframework.com/docs/theming/css-variables#ionic-variables
*/
:root {
--ion-headings-font-weight: 300;
--ion-color-angular: #ac282b;
--ion-color-communication: #8e8d93;
--ion-color-tooling: #fe4c52;
--ion-color-services: #fd8b2d;
--ion-color-design: #fed035;
--ion-color-workshop: #69bb7b;
--ion-color-food: #3bc7c4;
--ion-color-documentation: #b16be3;
--ion-color-navigation: #6600cc;
}
/*
* Dark Theme
* ----------------------------------------------------------------------------
*/
.dark-theme {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66,140,255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* ----------------------------------------------------------------------------
*/
.dark-theme.ios {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-toolbar-background: #0d0d0d;
--ion-item-background: #1c1c1c;
--ion-item-background-activated: #313131;
}
/*
* Material Design Dark Theme
* ----------------------------------------------------------------------------
*/
.dark-theme.md {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
}

View File

@@ -0,0 +1,15 @@
export interface DispatchObject {
[key: string]: any;
type: string;
}
type PromiseResolveValue<T> = T extends Promise<infer R> ? R : T;
type EffectType<T extends (...args: any) => any> = ReturnType<ReturnType<T>>;
type EffectReturnValue<T extends (...args: any) => any> = PromiseResolveValue<
EffectType<T>
>;
export type ActionType<T extends (...args: any) => any> = ReturnType<
T
> extends DispatchObject
? ReturnType<T>
: EffectReturnValue<T>;

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve"
},
"include": ["src"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": ["tslint-react"]
}