init commit,
This commit is contained in:
103
03_source/mobile_baseline.bak/src/pages/About.scss
Normal file
103
03_source/mobile_baseline.bak/src/pages/About.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
#about-page {
|
||||
ion-toolbar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
--background: transparent;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
ion-toolbar ion-back-button,
|
||||
ion-toolbar ion-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: relative;
|
||||
margin-top: -10px;
|
||||
border-radius: 10px;
|
||||
background: var(--ion-background-color, #fff);
|
||||
z-index: 2; // display rounded border above header image
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
#date-input-popover {
|
||||
--offset-y: -var(--ion-safe-area-bottom);
|
||||
|
||||
--max-width: 90%;
|
||||
--width: 336px;
|
||||
}
|
171
03_source/mobile_baseline.bak/src/pages/About.tsx
Normal file
171
03_source/mobile_baseline.bak/src/pages/About.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonDatetime,
|
||||
IonSelectOption,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonSelect,
|
||||
IonPopover,
|
||||
IonText,
|
||||
} from '@ionic/react';
|
||||
import './About.scss';
|
||||
import { ellipsisHorizontal, ellipsisVertical } from 'ionicons/icons';
|
||||
import AboutPopover from '../components/AboutPopover';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
|
||||
interface AboutProps {}
|
||||
|
||||
const About: React.FC<AboutProps> = () => {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
|
||||
const [location, setLocation] = useState<
|
||||
'madison' | 'austin' | 'chicago' | 'seattle'
|
||||
>('madison');
|
||||
const [conferenceDate, setConferenceDate] = useState(
|
||||
'2047-05-17T00:00:00-05:00'
|
||||
);
|
||||
|
||||
const selectOptions = {
|
||||
header: 'Select a Location',
|
||||
};
|
||||
|
||||
const presentPopover = (e: React.MouseEvent) => {
|
||||
setPopoverEvent(e.nativeEvent);
|
||||
setShowPopover(true);
|
||||
};
|
||||
|
||||
function displayDate(date: string, dateFormat: string) {
|
||||
return format(parseISO(date), dateFormat);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage id="about-page">
|
||||
<IonContent>
|
||||
<IonHeader className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton></IonMenuButton>
|
||||
</IonButtons>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={presentPopover}>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
ios={ellipsisHorizontal}
|
||||
md={ellipsisVertical}
|
||||
></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<div className="about-header">
|
||||
{/* Instead of loading an image each time the select changes, use opacity to transition them */}
|
||||
<div
|
||||
className="about-image madison"
|
||||
style={{ opacity: location === 'madison' ? '1' : undefined }}
|
||||
></div>
|
||||
<div
|
||||
className="about-image austin"
|
||||
style={{ opacity: location === 'austin' ? '1' : undefined }}
|
||||
></div>
|
||||
<div
|
||||
className="about-image chicago"
|
||||
style={{ opacity: location === 'chicago' ? '1' : undefined }}
|
||||
></div>
|
||||
<div
|
||||
className="about-image seattle"
|
||||
style={{ opacity: location === 'seattle' ? '1' : undefined }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="about-info">
|
||||
<h3 className="ion-padding-top ion-padding-start">About</h3>
|
||||
|
||||
<p className="ion-padding-start ion-padding-end">
|
||||
The Ionic Conference is a one-day event happening on {' '}
|
||||
{displayDate(conferenceDate, 'MMM dd, yyyy')}, featuring talks from the
|
||||
Ionic team. The conference focuses on building applications with Ionic
|
||||
Framework, including topics such as app migration to the latest version,
|
||||
React best practices, Webpack, Sass, and other technologies
|
||||
commonly used in the Ionic ecosystem. Tickets are completely sold out,
|
||||
and we're expecting over 1,000 developers — making this the largest
|
||||
Ionic conference to date!
|
||||
</p>
|
||||
|
||||
<h3 className="ion-padding-top ion-padding-start">Details</h3>
|
||||
|
||||
<IonList lines="none">
|
||||
<IonItem>
|
||||
<IonSelect
|
||||
label="Location"
|
||||
value={location}
|
||||
interfaceOptions={selectOptions}
|
||||
onIonChange={(e) => setLocation(e.detail.value as any)}
|
||||
>
|
||||
<IonSelectOption value="madison">Madison, WI</IonSelectOption>
|
||||
<IonSelectOption value="austin">Austin, TX</IonSelectOption>
|
||||
<IonSelectOption value="chicago">Chicago, IL</IonSelectOption>
|
||||
<IonSelectOption value="seattle">Seattle, WA</IonSelectOption>
|
||||
</IonSelect>
|
||||
</IonItem>
|
||||
<IonItem button={true} id="open-date-input">
|
||||
<IonLabel>Date</IonLabel>
|
||||
<IonText slot="end">
|
||||
{displayDate(conferenceDate, 'MMM dd, yyyy')}
|
||||
</IonText>
|
||||
<IonPopover
|
||||
id="date-input-popover"
|
||||
trigger="open-date-input"
|
||||
showBackdrop={false}
|
||||
side="top"
|
||||
alignment="end"
|
||||
>
|
||||
<IonDatetime
|
||||
max="2056"
|
||||
value={conferenceDate}
|
||||
onIonChange={(e) =>
|
||||
setConferenceDate(e.detail.value! as string)
|
||||
}
|
||||
presentation="date"
|
||||
></IonDatetime>
|
||||
</IonPopover>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<h3 className="ion-padding-top ion-padding-start">Internet</h3>
|
||||
|
||||
<IonList lines="none">
|
||||
<IonItem>
|
||||
<IonLabel>Wifi network</IonLabel>
|
||||
<IonLabel className="ion-text-end">
|
||||
ica{displayDate(conferenceDate, 'y')}
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
<IonItem>
|
||||
<IonLabel>Password</IonLabel>
|
||||
<IonLabel className="ion-text-end">makegoodthings</IonLabel>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
</div>
|
||||
</IonContent>
|
||||
|
||||
<IonPopover
|
||||
isOpen={showPopover}
|
||||
event={popoverEvent}
|
||||
onDidDismiss={() => setShowPopover(false)}
|
||||
>
|
||||
<AboutPopover dismiss={() => setShowPopover(false)} />
|
||||
</IonPopover>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(About);
|
6
03_source/mobile_baseline.bak/src/pages/Account.scss
Normal file
6
03_source/mobile_baseline.bak/src/pages/Account.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
#account-page {
|
||||
img {
|
||||
max-width: 140px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
110
03_source/mobile_baseline.bak/src/pages/Account.tsx
Normal file
110
03_source/mobile_baseline.bak/src/pages/Account.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
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,
|
||||
});
|
23
03_source/mobile_baseline.bak/src/pages/Login.scss
Normal file
23
03_source/mobile_baseline.bak/src/pages/Login.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
#login-page, #signup-page, #support-page {
|
||||
.login-logo {
|
||||
min-height: 200px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-logo img {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ion-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
124
03_source/mobile_baseline.bak/src/pages/Login.tsx
Normal file
124
03_source/mobile_baseline.bak/src/pages/Login.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonRow,
|
||||
IonCol,
|
||||
IonButton,
|
||||
IonInput,
|
||||
} from '@ionic/react';
|
||||
import { useHistory } from 'react-router';
|
||||
import './Login.scss';
|
||||
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
|
||||
import { connect } from '../data/connect';
|
||||
|
||||
interface LoginProps {
|
||||
setIsLoggedIn: typeof setIsLoggedIn;
|
||||
setUsername: typeof setUsername;
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({
|
||||
setIsLoggedIn,
|
||||
setUsername: setUsernameAction,
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const [login, setLogin] = useState({ username: '', password: '' });
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const onLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
|
||||
if (login.username && login.password) {
|
||||
await setIsLoggedIn(true);
|
||||
await setUsernameAction(login.username);
|
||||
history.push('/tabs/schedule');
|
||||
}
|
||||
};
|
||||
|
||||
const onSignup = () => {
|
||||
history.push('/signup');
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div className="login-form">
|
||||
<form onSubmit={onLogin} noValidate>
|
||||
<IonInput
|
||||
label="Username"
|
||||
labelPlacement="stacked"
|
||||
fill="solid"
|
||||
value={login.username}
|
||||
name="username"
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autocapitalize="off"
|
||||
errorText={
|
||||
submitted && !login.username ? 'Username is required' : ''
|
||||
}
|
||||
onIonInput={(e) =>
|
||||
setLogin({ ...login, username: e.detail.value! })
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<IonInput
|
||||
label="Password"
|
||||
labelPlacement="stacked"
|
||||
fill="solid"
|
||||
value={login.password}
|
||||
name="password"
|
||||
type="password"
|
||||
errorText={
|
||||
submitted && !login.password ? 'Password is required' : ''
|
||||
}
|
||||
onIonInput={(e) =>
|
||||
setLogin({ ...login, password: e.detail.value! })
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<IonRow>
|
||||
<IonCol>
|
||||
<IonButton type="submit" expand="block">
|
||||
Login
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
<IonCol>
|
||||
<IonButton onClick={onSignup} color="light" expand="block">
|
||||
Signup
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</form>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<{}, {}, LoginProps>({
|
||||
mapDispatchToProps: {
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
},
|
||||
component: Login,
|
||||
});
|
72
03_source/mobile_baseline.bak/src/pages/MainTabs.tsx
Normal file
72
03_source/mobile_baseline.bak/src/pages/MainTabs.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
IonTabs,
|
||||
IonRouterOutlet,
|
||||
IonTabBar,
|
||||
IonTabButton,
|
||||
IonIcon,
|
||||
IonLabel,
|
||||
} from '@ionic/react';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
import { calendar, location, informationCircle, people } 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={people} />
|
||||
<IonLabel>Speakers</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="map" href="/tabs/map">
|
||||
<IonIcon icon={location} />
|
||||
<IonLabel>Map</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="about" href="/tabs/about">
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>About</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainTabs;
|
18
03_source/mobile_baseline.bak/src/pages/MapView.scss
Normal file
18
03_source/mobile_baseline.bak/src/pages/MapView.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.map-canvas {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 150ms ease-in;
|
||||
}
|
||||
|
||||
.show-map {
|
||||
opacity: 1;
|
||||
}
|
129
03_source/mobile_baseline.bak/src/pages/MapView.tsx
Normal file
129
03_source/mobile_baseline.bak/src/pages/MapView.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonViewDidEnter,
|
||||
} from '@ionic/react';
|
||||
import { Location } from '../models/Location';
|
||||
import { connect } from '../data/connect';
|
||||
import { loadLocations } from '../data/locations/locations.actions';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import markerIconUrl from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconRetinaUrl from "leaflet/dist/images/marker-icon-2x.png";
|
||||
import markerShadowUrl from "leaflet/dist/images/marker-shadow.png";
|
||||
import './MapView.scss';
|
||||
|
||||
// Fix for marker icons in Vite
|
||||
L.Icon.Default.prototype.options.iconUrl = markerIconUrl;
|
||||
L.Icon.Default.prototype.options.iconRetinaUrl = markerIconRetinaUrl;
|
||||
L.Icon.Default.prototype.options.shadowUrl = markerShadowUrl;
|
||||
L.Icon.Default.imagePath = "";
|
||||
|
||||
interface StateProps {
|
||||
locations: Location[];
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
loadLocations: typeof loadLocations;
|
||||
}
|
||||
|
||||
const MapView: React.FC<StateProps & DispatchProps> = ({
|
||||
locations,
|
||||
loadLocations,
|
||||
}) => {
|
||||
const mapCanvas = useRef<HTMLDivElement>(null);
|
||||
const map = useRef<L.Map | null>(null);
|
||||
const markers = useRef<L.Marker[]>([]);
|
||||
|
||||
// Add useEffect to load locations when component mounts
|
||||
useEffect(() => {
|
||||
loadLocations();
|
||||
}, []);
|
||||
|
||||
const initMap = () => {
|
||||
if (!locations?.length || !mapCanvas.current || map.current) return;
|
||||
|
||||
map.current = L.map(mapCanvas.current, {
|
||||
zoomControl: true,
|
||||
attributionControl: true,
|
||||
});
|
||||
|
||||
// Get the center location (first item marked as center, or first item if none marked)
|
||||
const centerLocation = locations.find((loc) => loc.center) || locations[0];
|
||||
map.current.setView([centerLocation.lat, centerLocation.lng], 16);
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
}).addTo(map.current);
|
||||
|
||||
// Add markers for all locations
|
||||
locations.forEach((location: Location) => {
|
||||
const marker = L.marker([location.lat, location.lng])
|
||||
.addTo(map.current!)
|
||||
.bindPopup(`${location.name}`);
|
||||
markers.current.push(marker);
|
||||
});
|
||||
|
||||
// Show map
|
||||
mapCanvas.current.classList.add('show-map');
|
||||
};
|
||||
|
||||
const resizeMap = () => {
|
||||
if (map.current) {
|
||||
map.current.invalidateSize();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize map
|
||||
useEffect(() => {
|
||||
initMap();
|
||||
return () => {
|
||||
if (map.current) {
|
||||
markers.current.forEach((marker) => marker.remove());
|
||||
map.current.remove();
|
||||
map.current = null;
|
||||
}
|
||||
};
|
||||
}, [locations]);
|
||||
|
||||
// Handle resize after content is visible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
resizeMap();
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useIonViewDidEnter(() => {
|
||||
resizeMap();
|
||||
});
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Map</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<div ref={mapCanvas} className="map-canvas"></div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<{}, StateProps, DispatchProps>({
|
||||
mapStateToProps: (state) => ({
|
||||
locations: state.locations.locations,
|
||||
}),
|
||||
mapDispatchToProps: {
|
||||
loadLocations,
|
||||
},
|
||||
component: MapView,
|
||||
});
|
58
03_source/mobile_baseline.bak/src/pages/SchedulePage.scss
Normal file
58
03_source/mobile_baseline.bak/src/pages/SchedulePage.scss
Normal file
@@ -0,0 +1,58 @@
|
||||
#schedule-page {
|
||||
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);
|
||||
}
|
||||
|
||||
ion-item-sliding.track-ionic ion-label {
|
||||
border-left: 2px solid var(--ion-color-primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
ion-item-sliding.track-react ion-label {
|
||||
border-left: 2px solid var(--ion-color-react);
|
||||
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;
|
||||
}
|
||||
}
|
194
03_source/mobile_baseline.bak/src/pages/SchedulePage.tsx
Normal file
194
03_source/mobile_baseline.bak/src/pages/SchedulePage.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
|
||||
import {
|
||||
IonToolbar,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonTitle,
|
||||
IonMenuButton,
|
||||
IonSegment,
|
||||
IonSegmentButton,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonSearchbar,
|
||||
IonRefresher,
|
||||
IonRefresherContent,
|
||||
IonToast,
|
||||
IonModal,
|
||||
IonHeader,
|
||||
getConfig,
|
||||
} from '@ionic/react';
|
||||
import { options, search } from 'ionicons/icons';
|
||||
|
||||
import SessionList from '../components/SessionList';
|
||||
import SessionListFilter from '../components/SessionListFilter';
|
||||
import './SchedulePage.scss';
|
||||
|
||||
import ShareSocialFab from '../components/ShareSocialFab';
|
||||
|
||||
import * as selectors from '../data/selectors';
|
||||
import { connect } from '../data/connect';
|
||||
import { setSearchText } from '../data/sessions/sessions.actions';
|
||||
import { Schedule } from '../models/Schedule';
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface StateProps {
|
||||
schedule: Schedule;
|
||||
favoritesSchedule: Schedule;
|
||||
mode: 'ios' | 'md';
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
setSearchText: typeof setSearchText;
|
||||
}
|
||||
|
||||
type SchedulePageProps = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
const SchedulePage: React.FC<SchedulePageProps> = ({
|
||||
favoritesSchedule,
|
||||
schedule,
|
||||
setSearchText,
|
||||
mode,
|
||||
}) => {
|
||||
const [segment, setSegment] = useState<'all' | 'favorites'>('all');
|
||||
const [showSearchbar, setShowSearchbar] = useState<boolean>(false);
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
const ionRefresherRef = useRef<HTMLIonRefresherElement>(null);
|
||||
const [showCompleteToast, setShowCompleteToast] = useState(false);
|
||||
|
||||
const pageRef = useRef<HTMLElement>(null);
|
||||
|
||||
const ios = mode === 'ios';
|
||||
|
||||
const doRefresh = () => {
|
||||
setTimeout(() => {
|
||||
ionRefresherRef.current!.complete();
|
||||
setShowCompleteToast(true);
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage ref={pageRef} id="schedule-page">
|
||||
<IonHeader translucent={true}>
|
||||
<IonToolbar>
|
||||
{!showSearchbar && (
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
)}
|
||||
{ios && (
|
||||
<IonSegment
|
||||
value={segment}
|
||||
onIonChange={(e) => setSegment(e.detail.value as any)}
|
||||
>
|
||||
<IonSegmentButton value="all">All</IonSegmentButton>
|
||||
<IonSegmentButton value="favorites">Favorites</IonSegmentButton>
|
||||
</IonSegment>
|
||||
)}
|
||||
{!ios && !showSearchbar && <IonTitle>Schedule</IonTitle>}
|
||||
{showSearchbar && (
|
||||
<IonSearchbar
|
||||
showCancelButton="always"
|
||||
placeholder="Search"
|
||||
onIonInput={(e: CustomEvent) => setSearchText(e.detail.value)}
|
||||
onIonCancel={() => setShowSearchbar(false)}
|
||||
></IonSearchbar>
|
||||
)}
|
||||
|
||||
<IonButtons slot="end">
|
||||
{!ios && !showSearchbar && (
|
||||
<IonButton onClick={() => setShowSearchbar(true)}>
|
||||
<IonIcon slot="icon-only" icon={search}></IonIcon>
|
||||
</IonButton>
|
||||
)}
|
||||
{!showSearchbar && (
|
||||
<IonButton onClick={() => setShowFilterModal(true)}>
|
||||
{mode === 'ios' ? (
|
||||
'Filter'
|
||||
) : (
|
||||
<IonIcon icon={options} slot="icon-only" />
|
||||
)}
|
||||
</IonButton>
|
||||
)}
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
|
||||
{!ios && (
|
||||
<IonToolbar>
|
||||
<IonSegment
|
||||
value={segment}
|
||||
onIonChange={(e) => setSegment(e.detail.value as any)}
|
||||
>
|
||||
<IonSegmentButton value="all">All</IonSegmentButton>
|
||||
<IonSegmentButton value="favorites">Favorites</IonSegmentButton>
|
||||
</IonSegment>
|
||||
</IonToolbar>
|
||||
)}
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen={true}>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Schedule</IonTitle>
|
||||
</IonToolbar>
|
||||
<IonToolbar>
|
||||
<IonSearchbar
|
||||
placeholder="Search"
|
||||
onIonInput={(e: CustomEvent) => setSearchText(e.detail.value)}
|
||||
></IonSearchbar>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRefresher
|
||||
slot="fixed"
|
||||
ref={ionRefresherRef}
|
||||
onIonRefresh={doRefresh}
|
||||
>
|
||||
<IonRefresherContent />
|
||||
</IonRefresher>
|
||||
|
||||
<IonToast
|
||||
isOpen={showCompleteToast}
|
||||
message="Refresh complete"
|
||||
duration={2000}
|
||||
onDidDismiss={() => setShowCompleteToast(false)}
|
||||
/>
|
||||
|
||||
<SessionList
|
||||
schedule={schedule}
|
||||
listType={segment}
|
||||
hide={segment === 'favorites'}
|
||||
/>
|
||||
<SessionList
|
||||
schedule={favoritesSchedule}
|
||||
listType={segment}
|
||||
hide={segment === 'all'}
|
||||
/>
|
||||
</IonContent>
|
||||
|
||||
<IonModal
|
||||
isOpen={showFilterModal}
|
||||
onDidDismiss={() => setShowFilterModal(false)}
|
||||
presentingElement={pageRef.current!}
|
||||
>
|
||||
<SessionListFilter onDismissModal={() => setShowFilterModal(false)} />
|
||||
</IonModal>
|
||||
|
||||
<ShareSocialFab />
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<OwnProps, StateProps, DispatchProps>({
|
||||
mapStateToProps: (state) => ({
|
||||
schedule: selectors.getSearchedSchedule(state),
|
||||
favoritesSchedule: selectors.getGroupedFavorites(state),
|
||||
mode: getConfig()!.get('mode'),
|
||||
}),
|
||||
mapDispatchToProps: {
|
||||
setSearchText,
|
||||
},
|
||||
component: React.memo(SchedulePage),
|
||||
});
|
73
03_source/mobile_baseline.bak/src/pages/SessionDetail.scss
Normal file
73
03_source/mobile_baseline.bak/src/pages/SessionDetail.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
#session-detail-page {
|
||||
.session-track-ionic {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.session-track-react {
|
||||
color: var(--ion-color-react);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
135
03_source/mobile_baseline.bak/src/pages/SessionDetail.tsx
Normal file
135
03_source/mobile_baseline.bak/src/pages/SessionDetail.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
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 { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
|
||||
import { Session } from '../models/Schedule';
|
||||
|
||||
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">
|
||||
{session.timeStart} – {session.timeEnd}
|
||||
<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),
|
||||
});
|
17
03_source/mobile_baseline.bak/src/pages/Signup.scss
Normal file
17
03_source/mobile_baseline.bak/src/pages/Signup.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.signup-logo {
|
||||
min-height: 200px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signup-logo img {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.signup-form {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ion-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
113
03_source/mobile_baseline.bak/src/pages/Signup.tsx
Normal file
113
03_source/mobile_baseline.bak/src/pages/Signup.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonRow,
|
||||
IonCol,
|
||||
IonButton,
|
||||
IonInput,
|
||||
} from '@ionic/react';
|
||||
import { useHistory } from 'react-router';
|
||||
import './Signup.scss';
|
||||
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
|
||||
import { connect } from '../data/connect';
|
||||
|
||||
interface SignupProps {
|
||||
setIsLoggedIn: typeof setIsLoggedIn;
|
||||
setUsername: typeof setUsername;
|
||||
}
|
||||
|
||||
const Signup: React.FC<SignupProps> = ({
|
||||
setIsLoggedIn,
|
||||
setUsername: setUsernameAction,
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const [signup, setSignup] = useState({ username: '', password: '' });
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const onSignup = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
|
||||
if (signup.username && signup.password) {
|
||||
await setIsLoggedIn(true);
|
||||
await setUsernameAction(signup.username);
|
||||
history.push('/tabs/schedule');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage id="signup-page">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton></IonMenuButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Signup</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<div className="signup-logo">
|
||||
<img src="/assets/img/appicon.svg" alt="Ionic Logo" />
|
||||
</div>
|
||||
|
||||
<div className="signup-form">
|
||||
<form onSubmit={onSignup} noValidate>
|
||||
<IonInput
|
||||
label="Username"
|
||||
labelPlacement="stacked"
|
||||
fill="solid"
|
||||
value={signup.username}
|
||||
name="username"
|
||||
type="text"
|
||||
errorText={
|
||||
submitted && !signup.username ? 'Username is required' : ''
|
||||
}
|
||||
onIonInput={(e) =>
|
||||
setSignup({ ...signup, username: e.detail.value! })
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<IonInput
|
||||
label="Password"
|
||||
labelPlacement="stacked"
|
||||
fill="solid"
|
||||
value={signup.password}
|
||||
name="password"
|
||||
type="password"
|
||||
errorText={
|
||||
submitted && !signup.password ? 'Password is required' : ''
|
||||
}
|
||||
onIonInput={(e) =>
|
||||
setSignup({ ...signup, password: e.detail.value! })
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<IonRow>
|
||||
<IonCol>
|
||||
<IonButton type="submit" expand="block">
|
||||
Create
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</form>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<{}, {}, SignupProps>({
|
||||
mapDispatchToProps: {
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
},
|
||||
component: Signup,
|
||||
});
|
79
03_source/mobile_baseline.bak/src/pages/SpeakerDetail.scss
Normal file
79
03_source/mobile_baseline.bak/src/pages/SpeakerDetail.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
#speaker-detail {
|
||||
/*
|
||||
* Speaker Background
|
||||
*/
|
||||
|
||||
ion-toolbar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
--background: transparent;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
ion-toolbar ion-back-button,
|
||||
ion-toolbar ion-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) 0 3px 1px -2px,
|
||||
rgba(0, 0, 0, 0.14) 0 2px 2px 0px, rgba(0, 0, 0, 0.12) 0 1px 5px 0;
|
||||
}
|
||||
|
||||
.ios .speaker-background {
|
||||
box-shadow: rgba(0, 0, 0, 0.12) 0 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);
|
||||
}
|
||||
}
|
188
03_source/mobile_baseline.bak/src/pages/SpeakerDetail.tsx
Normal file
188
03_source/mobile_baseline.bak/src/pages/SpeakerDetail.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
|
||||
import './SpeakerDetail.scss';
|
||||
|
||||
import { ActionSheetButton } from '@ionic/core';
|
||||
import {
|
||||
IonActionSheet,
|
||||
IonChip,
|
||||
IonIcon,
|
||||
IonHeader,
|
||||
IonLabel,
|
||||
IonToolbar,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonButton,
|
||||
IonBackButton,
|
||||
IonPage,
|
||||
} from '@ionic/react';
|
||||
import {
|
||||
callOutline,
|
||||
callSharp,
|
||||
logoTwitter,
|
||||
logoGithub,
|
||||
logoInstagram,
|
||||
shareOutline,
|
||||
shareSharp,
|
||||
} 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 }) => {
|
||||
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);
|
||||
}
|
||||
|
||||
function openExternalUrl(url: string) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
if (!speaker) {
|
||||
return <div>Speaker not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage id="speaker-detail">
|
||||
<IonContent>
|
||||
<IonHeader className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref="/tabs/speakers" />
|
||||
</IonButtons>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={() => openContact(speaker)}>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
ios={callOutline}
|
||||
md={callSharp}
|
||||
></IonIcon>
|
||||
</IonButton>
|
||||
<IonButton onClick={() => openSpeakerShare(speaker)}>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
ios={shareOutline}
|
||||
md={shareSharp}
|
||||
></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<div className="speaker-background">
|
||||
<img src={speaker.profilePic} alt={speaker.name} />
|
||||
<h2>{speaker.name}</h2>
|
||||
</div>
|
||||
|
||||
<div className="ion-padding speaker-detail">
|
||||
<p>{speaker.about} Say hello on social media!</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<IonChip
|
||||
color="twitter"
|
||||
onClick={() =>
|
||||
openExternalUrl(`https://twitter.com/${speaker.twitter}`)
|
||||
}
|
||||
>
|
||||
<IonIcon icon={logoTwitter}></IonIcon>
|
||||
<IonLabel>Twitter</IonLabel>
|
||||
</IonChip>
|
||||
|
||||
<IonChip
|
||||
color="dark"
|
||||
onClick={() =>
|
||||
openExternalUrl('https://github.com/ionic-team/ionic-framework')
|
||||
}
|
||||
>
|
||||
<IonIcon icon={logoGithub}></IonIcon>
|
||||
<IonLabel>GitHub</IonLabel>
|
||||
</IonChip>
|
||||
|
||||
<IonChip
|
||||
color="instagram"
|
||||
onClick={() =>
|
||||
openExternalUrl('https://instagram.com/ionicframework')
|
||||
}
|
||||
>
|
||||
<IonIcon icon={logoInstagram}></IonIcon>
|
||||
<IonLabel>Instagram</IonLabel>
|
||||
</IonChip>
|
||||
</div>
|
||||
</IonContent>
|
||||
<IonActionSheet
|
||||
isOpen={showActionSheet}
|
||||
header={actionSheetHeader}
|
||||
onDidDismiss={() => setShowActionSheet(false)}
|
||||
buttons={actionSheetButtons}
|
||||
/>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect({
|
||||
mapStateToProps: (state, ownProps) => ({
|
||||
speaker: selectors.getSpeaker(state, ownProps),
|
||||
}),
|
||||
component: SpeakerDetail,
|
||||
});
|
48
03_source/mobile_baseline.bak/src/pages/SpeakerList.scss
Normal file
48
03_source/mobile_baseline.bak/src/pages/SpeakerList.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
#speaker-list {
|
||||
.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;
|
||||
}
|
||||
}
|
78
03_source/mobile_baseline.bak/src/pages/SpeakerList.tsx
Normal file
78
03_source/mobile_baseline.bak/src/pages/SpeakerList.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonGrid,
|
||||
IonRow,
|
||||
IonCol,
|
||||
} from '@ionic/react';
|
||||
import SpeakerItem from '../components/SpeakerItem';
|
||||
import { Speaker } from '../models/Speaker';
|
||||
import { Session } from '../models/Schedule';
|
||||
import { connect } from '../data/connect';
|
||||
import * as selectors from '../data/selectors';
|
||||
import './SpeakerList.scss';
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface StateProps {
|
||||
speakers: Speaker[];
|
||||
speakerSessions: { [key: string]: Session[] };
|
||||
}
|
||||
|
||||
interface DispatchProps {}
|
||||
|
||||
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
const SpeakerList: React.FC<SpeakerListProps> = ({
|
||||
speakers,
|
||||
speakerSessions,
|
||||
}) => {
|
||||
return (
|
||||
<IonPage id="speaker-list">
|
||||
<IonHeader translucent={true}>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Speakers</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen={true}>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Speakers</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid fixed>
|
||||
<IonRow>
|
||||
{speakers.map((speaker) => (
|
||||
<IonCol size="12" size-md="6" key={speaker.id}>
|
||||
<SpeakerItem
|
||||
key={speaker.id}
|
||||
speaker={speaker}
|
||||
sessions={speakerSessions[speaker.name]}
|
||||
/>
|
||||
</IonCol>
|
||||
))}
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<OwnProps, StateProps, DispatchProps>({
|
||||
mapStateToProps: (state) => ({
|
||||
speakers: selectors.getSpeakers(state),
|
||||
speakerSessions: selectors.getSpeakerSessions(state),
|
||||
}),
|
||||
component: React.memo(SpeakerList),
|
||||
});
|
17
03_source/mobile_baseline.bak/src/pages/Support.scss
Normal file
17
03_source/mobile_baseline.bak/src/pages/Support.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.support-logo {
|
||||
min-height: 200px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.support-logo img {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.support-form {
|
||||
padding: 16px;
|
||||
}
|
93
03_source/mobile_baseline.bak/src/pages/Support.tsx
Normal file
93
03_source/mobile_baseline.bak/src/pages/Support.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonRow,
|
||||
IonCol,
|
||||
IonButton,
|
||||
IonTextarea,
|
||||
useIonToast,
|
||||
useIonViewWillEnter,
|
||||
} from '@ionic/react';
|
||||
import './Support.scss';
|
||||
|
||||
const Support: React.FC = () => {
|
||||
const [present] = useIonToast();
|
||||
const [supportMessage, setSupportMessage] = useState('');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
present({
|
||||
message: 'This does not actually send a support request.',
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
|
||||
if (supportMessage) {
|
||||
setSupportMessage('');
|
||||
setSubmitted(false);
|
||||
|
||||
present({
|
||||
message: 'Your support request has been sent.',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage id="support-page">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton></IonMenuButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Support</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<div className="support-logo">
|
||||
<img src="/assets/img/appicon.svg" alt="Ionic Logo" />
|
||||
</div>
|
||||
|
||||
<div className="support-form">
|
||||
<form onSubmit={submit} noValidate>
|
||||
<IonTextarea
|
||||
label="Enter your support message below"
|
||||
labelPlacement="stacked"
|
||||
fill="solid"
|
||||
value={supportMessage}
|
||||
name="supportQuestion"
|
||||
rows={6}
|
||||
errorText={
|
||||
submitted && !supportMessage
|
||||
? 'Support message is required'
|
||||
: ''
|
||||
}
|
||||
onIonInput={(e) => setSupportMessage(e.detail.value!)}
|
||||
required
|
||||
/>
|
||||
|
||||
<IonRow>
|
||||
<IonCol>
|
||||
<IonButton expand="block" type="submit">
|
||||
Submit
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</form>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Support;
|
56
03_source/mobile_baseline.bak/src/pages/Tutorial.scss
Normal file
56
03_source/mobile_baseline.bak/src/pages/Tutorial.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
#tutorial-page {
|
||||
ion-toolbar {
|
||||
--background: transparent;
|
||||
--border-color: transparent;
|
||||
}
|
||||
|
||||
.slide-title {
|
||||
margin-top: 2.8rem;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 100%);
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
height: 100%;
|
||||
|
||||
overflow: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
.slide-image {
|
||||
max-height: 50%;
|
||||
max-width: 60%;
|
||||
margin: -5vh 0 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);
|
||||
}
|
||||
}
|
||||
}
|
137
03_source/mobile_baseline.bak/src/pages/Tutorial.tsx
Normal file
137
03_source/mobile_baseline.bak/src/pages/Tutorial.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import {
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
useIonViewWillEnter,
|
||||
} from '@ionic/react';
|
||||
import { arrowForward } from 'ionicons/icons';
|
||||
import { setMenuEnabled } from '../data/sessions/sessions.actions';
|
||||
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;
|
||||
setMenuEnabled: typeof setMenuEnabled;
|
||||
}
|
||||
|
||||
interface TutorialProps extends OwnProps, DispatchProps {}
|
||||
|
||||
const Tutorial: React.FC<TutorialProps> = ({
|
||||
history,
|
||||
setHasSeenTutorial,
|
||||
setMenuEnabled,
|
||||
}) => {
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
setMenuEnabled(false);
|
||||
// Scroll to first slide when entering the tutorial
|
||||
if (sliderRef.current) {
|
||||
sliderRef.current.scrollTo({
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const startApp = async () => {
|
||||
await setHasSeenTutorial(true);
|
||||
await setMenuEnabled(true);
|
||||
history.push('/tabs/schedule', { direction: 'none' });
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage id="tutorial-page">
|
||||
<IonHeader className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButtons slot="end">
|
||||
<IonButton color="primary" onClick={startApp}>
|
||||
Skip
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<div className="slider" ref={sliderRef}>
|
||||
<section>
|
||||
<div className="swiper-item">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="swiper-item">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="swiper-item">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="swiper-item">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<OwnProps, {}, DispatchProps>({
|
||||
mapDispatchToProps: {
|
||||
setHasSeenTutorial,
|
||||
setMenuEnabled,
|
||||
},
|
||||
component: Tutorial,
|
||||
});
|
Reference in New Issue
Block a user