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

View File

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

View File

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

View File

@@ -0,0 +1,154 @@
import React from 'react';
import { RouteComponentProps, withRouter, useLocation } from 'react-router';
import {
IonContent,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenu,
IonMenuToggle,
IonToggle,
} from '@ionic/react';
import {
calendarOutline,
hammer,
moonOutline,
help,
informationCircleOutline,
logIn,
logOut,
mapOutline,
peopleOutline,
person,
personAdd,
} from 'ionicons/icons';
import { connect } from '../data/connect';
import { setDarkMode } from '../data/user/user.actions';
import './Menu.css';
const routes = {
appPages: [
{ title: 'Schedule', path: '/tabs/schedule', icon: calendarOutline },
{ title: 'Speakers', path: '/tabs/speakers', icon: peopleOutline },
{ title: 'Map', path: '/tabs/map', icon: mapOutline },
{ title: 'About', path: '/tabs/about', icon: informationCircleOutline },
],
loggedInPages: [
{ title: 'Account', path: '/account', icon: person },
{ title: 'Support', path: '/support', icon: help },
{ title: 'Logout', path: '/logout', icon: logOut },
],
loggedOutPages: [
{ title: 'Login', path: '/login', icon: logIn },
{ title: 'Support', path: '/support', icon: help },
{ title: 'Signup', path: '/signup', icon: personAdd },
],
};
interface Pages {
title: string;
path: string;
icon: string;
routerDirection?: string;
}
interface StateProps {
darkMode: boolean;
isAuthenticated: boolean;
menuEnabled: boolean;
}
interface DispatchProps {
setDarkMode: typeof setDarkMode;
}
interface MenuProps extends RouteComponentProps, StateProps, DispatchProps {}
const Menu: React.FC<MenuProps> = ({
darkMode,
history,
isAuthenticated,
setDarkMode,
menuEnabled,
}) => {
const location = useLocation();
function renderlistItems(list: Pages[]) {
return list
.filter((route) => !!route.path)
.map((p) => (
<IonMenuToggle key={p.title} auto-hide="false">
<IonItem
detail={false}
routerLink={p.path}
routerDirection="none"
className={
location.pathname.startsWith(p.path) ? 'selected' : undefined
}
>
<IonIcon slot="start" icon={p.icon} />
<IonLabel>{p.title}</IonLabel>
</IonItem>
</IonMenuToggle>
));
}
return (
<IonMenu type="overlay" disabled={!menuEnabled} contentId="main">
<IonContent forceOverscroll={false}>
<IonList lines="none">
<IonListHeader>Conference</IonListHeader>
{renderlistItems(routes.appPages)}
</IonList>
<IonList lines="none">
<IonListHeader>Account</IonListHeader>
{isAuthenticated
? renderlistItems(routes.loggedInPages)
: renderlistItems(routes.loggedOutPages)}
<IonItem>
<IonIcon
slot="start"
icon={moonOutline}
aria-hidden="true"
></IonIcon>
<IonToggle
checked={darkMode}
onClick={() => setDarkMode(!darkMode)}
>
Dark Mode
</IonToggle>
</IonItem>
</IonList>
<IonList lines="none">
<IonListHeader>Tutorial</IonListHeader>
<IonItem
button
detail={false}
onClick={() => {
history.push('/tutorial');
}}
>
<IonIcon slot="start" icon={hammer} />
<IonLabel>Show Tutorial</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonMenu>
);
};
export default connect<{}, StateProps, {}>({
mapStateToProps: (state) => ({
darkMode: state.user.darkMode,
isAuthenticated: state.user.isLoggedin,
menuEnabled: state.data.menuEnabled,
}),
mapDispatchToProps: {
setDarkMode,
},
component: withRouter(Menu),
});

View File

@@ -0,0 +1,22 @@
import React, { useEffect, useContext } from 'react';
import { IonRouterContext } from '@ionic/react';
interface RedirectToLoginProps {
setIsLoggedIn: Function;
setUsername: Function;
}
const RedirectToLogin: React.FC<RedirectToLoginProps> = ({
setIsLoggedIn,
setUsername,
}) => {
const ionRouterContext = useContext(IonRouterContext);
useEffect(() => {
setIsLoggedIn(false);
setUsername(undefined);
ionRouterContext.push('/tabs/schedule');
}, [setIsLoggedIn, setUsername]);
return null;
};
export default RedirectToLogin;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,120 @@
import React, { useRef } from 'react';
import {
IonItemSliding,
IonItem,
IonLabel,
IonItemOptions,
IonItemOption,
AlertButton,
useIonToast,
} from '@ionic/react';
import { Session } from '../models/Schedule';
interface SessionListItemProps {
session: Session;
listType: 'all' | 'favorites';
onAddFavorite: (id: number) => void;
onRemoveFavorite: (id: number) => void;
onShowAlert: (
header: string,
message: string,
buttons: AlertButton[]
) => void;
isFavorite: boolean;
}
const SessionListItem: React.FC<SessionListItemProps> = ({
isFavorite,
onAddFavorite,
onRemoveFavorite,
onShowAlert,
session,
listType,
}) => {
const [presentToast] = useIonToast();
const ionItemSlidingRef = useRef<HTMLIonItemSlidingElement>(null);
const dismissAlert = () => {
ionItemSlidingRef.current && ionItemSlidingRef.current.close();
};
const removeFavoriteSession = (title: string) => {
onAddFavorite(session.id);
onShowAlert(
title,
'Would you like to remove this session from your favorites?',
[
{
text: 'Cancel',
handler: dismissAlert,
},
{
text: 'Remove',
handler: () => {
onRemoveFavorite(session.id);
dismissAlert();
},
},
]
);
};
const addFavoriteSession = async () => {
if (isFavorite) {
// Prompt to remove favorite
removeFavoriteSession('Favorite already added');
} else {
// Add as a favorite
onAddFavorite(session.id);
// Close the open item
ionItemSlidingRef.current && ionItemSlidingRef.current.close();
// Create a toast
presentToast({
message: `${session.name} was successfully added as a favorite.`,
duration: 3000,
buttons: [
{
text: 'Close',
role: 'cancel',
},
],
});
}
};
return (
<IonItemSliding
ref={ionItemSlidingRef}
class={'track-' + session.tracks[0].toLowerCase()}
>
<IonItem routerLink={`/tabs/schedule/${session.id}`}>
<IonLabel>
<h3>{session.name}</h3>
<p>
{session.timeStart} &mdash;&nbsp;
{session.timeEnd}:&nbsp;
{session.location}
</p>
</IonLabel>
</IonItem>
<IonItemOptions>
{listType === 'favorites' ? (
<IonItemOption
color="danger"
onClick={() => removeFavoriteSession('Remove Favorite')}
>
Remove
</IonItemOption>
) : (
<IonItemOption color="favorite" onClick={addFavoriteSession}>
Favorite
</IonItemOption>
)}
</IonItemOptions>
</IonItemSliding>
);
};
export default React.memo(SessionListItem);

View File

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

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { Session } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
import {
IonCard,
IonCardHeader,
IonItem,
IonLabel,
IonAvatar,
IonCardContent,
IonList,
} from '@ionic/react';
interface SpeakerItemProps {
speaker: Speaker;
sessions: Session[];
}
const SpeakerItem: React.FC<SpeakerItemProps> = ({ speaker, sessions }) => {
return (
<>
<IonCard className="speaker-card">
<IonCardHeader>
<IonItem
button
detail={false}
lines="none"
className="speaker-item"
routerLink={`/tabs/speakers/${speaker.id}`}
>
<IonAvatar slot="start">
<img src={speaker.profilePic} alt="Speaker profile pic" />
</IonAvatar>
<IonLabel>
<h2>{speaker.name}</h2>
<p>{speaker.title}</p>
</IonLabel>
</IonItem>
</IonCardHeader>
<IonCardContent>
<IonList lines="none">
{sessions.map((session) => (
<IonItem
detail={false}
routerLink={`/tabs/speakers/sessions/${session.id}`}
key={session.name}
>
<IonLabel>
<h3>{session.name}</h3>
</IonLabel>
</IonItem>
))}
<IonItem detail={false} routerLink={`/tabs/speakers/${speaker.id}`}>
<IonLabel>
<h3>About {speaker.name}</h3>
</IonLabel>
</IonItem>
</IonList>
</IonCardContent>
</IonCard>
</>
);
};
export default SpeakerItem;