init commit,

This commit is contained in:
louiscklaw
2025-05-28 09:55:51 +08:00
commit efe70ceb69
8042 changed files with 951668 additions and 0 deletions

View 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;
}

View 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);

View File

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

View 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,
});

View 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;
}
}

View 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,
});

View 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;

View 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;
}

View 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,
});

View 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;
}
}

View 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),
});

View 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;
}
}

View 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} &ndash; {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),
});

View 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;
}

View 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,
});

View 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);
}
}

View 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,
});

View 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;
}
}

View 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),
});

View 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;
}

View 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;

View 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);
}
}
}

View 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,
});