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,6 @@
/*
* App Global CSS
* ----------------------------------------------------------------------------
* Put style rules here that you want to apply globally. These styles are for
* the entire app and not just one component.
*/

View File

@@ -0,0 +1,7 @@
import { render } from '@testing-library/react';
import App from './App';
it('renders without crashing', () => {
const { asFragment, container } = render(<App />);
expect(asFragment()).toMatchSnapshot();
});

View File

@@ -0,0 +1,273 @@
import { IonApp, IonNav, IonRouterOutlet, IonSplitPane, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import React, { useContext, useEffect } from 'react';
import { Route } from 'react-router-dom';
import Menu from './components/Menu';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/display.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/padding.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
/**
* Ionic Dark Mode
* -----------------------------------------------------
* For more info, please see:
* https://ionicframework.com/docs/theming/dark-mode
*/
// import "@ionic/react/css/palettes/dark.always.css";
// import "@ionic/react/css/palettes/dark.system.css";
import '@ionic/react/css/palettes/dark.class.css';
/* Theme variables */
import './theme/variables.css';
/* Global styles */
import './App.scss';
import RedirectToLogin from './components/RedirectToLogin';
import { AppContext, AppContextProvider } from './data/AppContext';
import { connect } from './data/connect';
import { loadConfData } from './data/sessions/sessions.actions';
import { loadUserData, setIsLoggedIn, setUsername } from './data/user/user.actions';
import { Schedule } from './models/Schedule';
import Account from './pages/Account';
import Login from './pages/Login';
import MainTabs from './pages/MainTabs';
import Signup from './pages/Signup';
import Support from './pages/Support';
import Tutorial from './pages/Tutorial';
//
import { Redirect } from 'react-router';
import { AccountPage } from './pages/SBAccount';
import LoginPage from './pages/SBLogin';
//
import SBLogout from './pages/SBLogout';
import StartupLoading from './pages/debug/StartupLoading';
import WelcomePage from './pages/debug/WelcomePage';
import Helloworld from './pages/debug/helloworld';
//
import SBLoginError from './SBLoginError';
import './i18n';
import UnlockMemberShip from './pages/UnlockMembership';
import EventDetail from './pages/event_detail';
//
// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function PrivateRoute({ children, ...rest }) {
const { session } = useContext(AppContext);
return <Route {...rest} render={({ location }) => (session ? children : <Redirect to="/sblogin" />)} />;
}
setupIonicReact();
const App: React.FC = () => {
return (
<AppContextProvider>
<IonicAppConnected />
</AppContextProvider>
);
};
interface StateProps {
darkMode: boolean;
schedule: Schedule;
}
interface DispatchProps {
loadConfData: typeof loadConfData;
loadUserData: typeof loadUserData;
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface IonicAppProps extends StateProps, DispatchProps {}
function AppOutlet() {
return (
<IonRouterOutlet id="main">
{/*
We use IonRoute here to keep the tabs state intact,
which makes transitions between tabs and non tab pages smooth
*/}
{/* <Route path="/tabs" render={() => <MainTabs />} /> */}
<Route path="/account" component={Account} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/support" component={Support} />
<Route path="/tutorial" component={Tutorial} />
<Route path="/login_error" component={SBLoginError} />
<Route
path="/logout"
render={() => {
return <RedirectToLogin setIsLoggedIn={setIsLoggedIn} setUsername={setUsername} />;
}}
/>
<Route path="/debug">
<Route path="/debug/helloworld" component={Helloworld} />
<Route path="/debug/StartupLoading" component={StartupLoading} />
<Route path="/debug/WelcomePage" component={WelcomePage} />
</Route>
<Route path="/helloworld" component={Helloworld} />
<Route path="/event_detail/1" render={() => <EventDetail />} exact={true} />
{/* */}
<Route path="/privacy/en" render={() => <PrivacyPage />} exact={true} />
{/* nested route / cascade route example */}
<Route path="/tabs">
<MainTabs />
</Route>
<Route exact path="/unlock-membership" render={() => <UnlockMemberShip />} />
<Route exact path="/sblogout" render={() => <SBLogout />} />
<Route exact path="/sblogin" render={() => <LoginPage />} />
<PrivateRoute path="/sbaccount">
<AccountPage />
</PrivateRoute>
{/* <Route path="/" component={HomeOrTutorial} exact /> */}
<Route
exact
path="/"
render={() => <RedirectToLogin setIsLoggedIn={setIsLoggedIn} setUsername={setUsername} />}
/>
</IonRouterOutlet>
);
}
const IonicApp: React.FC<IonicAppProps> = ({
darkMode,
schedule,
setIsLoggedIn,
setUsername,
loadConfData,
loadUserData,
}) => {
useEffect(() => {
loadUserData();
loadConfData();
// eslint-disable-next-line
}, []);
return schedule.groups.length === 0 ? (
<div></div>
) : (
<IonApp className={`${darkMode ? 'ion-palette-dark' : ''}`}>
<IonReactRouter>
<IonSplitPane contentId="main">
{/* */}
<Menu />
{/* <AppOutlet /> */}
<IonNav root={() => <AppOutlet />}></IonNav>;
</IonSplitPane>
</IonReactRouter>
</IonApp>
);
// TODO: obsoleted, delete this
// return schedule.groups.length === 0 ? (
// <div></div>
// ) : (
// <IonApp className={`${darkMode ? 'ion-palette-dark' : ''}`}>
// <IonReactRouter>
// <IonSplitPane contentId="main">
// {/* */}
// <Menu />
// <IonRouterOutlet id="main">
// {/*
// We use IonRoute here to keep the tabs state intact,
// which makes transitions between tabs and non tab pages smooth
// */}
// {/* <Route path="/tabs" render={() => <MainTabs />} /> */}
// <Route path="/account" component={Account} />
// <Route path="/login" component={Login} />
// <Route path="/signup" component={Signup} />
// <Route path="/support" component={Support} />
// <Route path="/tutorial" component={Tutorial} />
// <Route
// path="/logout"
// render={() => {
// return <RedirectToLogin setIsLoggedIn={setIsLoggedIn} setUsername={setUsername} />;
// }}
// />
// <Route path="/debug">
// <Route path="/debug/helloworld" component={Helloworld} />
// <Route path="/debug/StartupLoading" component={StartupLoading} />
// <Route path="/debug/WelcomePage" component={WelcomePage} />
// </Route>
// <Route path="/helloworld" component={Helloworld} />
// <Route path="/event_detail/1" render={() => <EventDetail />} exact={true} />
// {/* nested route / cascade route example */}
// <Route path="/tabs">
// <MainTabs />
// </Route>
// <PrivateRoute path="/sbaccount">
// <AccountPage />
// </PrivateRoute>
// <Route
// path="/sblogout"
// render={() => {
// return <SBLogout />;
// }}
// />
// <Route
// exact
// path="/sblogin"
// render={() => {
// return <LoginPage />;
// }}
// />
// <Route path="/" component={HomeOrTutorial} exact />
// </IonRouterOutlet>
// </IonSplitPane>
// </IonReactRouter>
// </IonApp>
// );
};
export default App;
const IonicAppConnected = connect<{}, StateProps, DispatchProps>({
mapStateToProps: state => ({
darkMode: state.user.darkMode,
schedule: state.data.schedule,
}),
mapDispatchToProps: {
loadConfData,
loadUserData,
setIsLoggedIn,
setUsername,
},
component: IonicApp,
});

View File

@@ -0,0 +1,5 @@
const SBLoginError = () => {
return <>Login Error</>;
};
export default SBLoginError;

View File

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

View File

@@ -0,0 +1,35 @@
import { IonItem, IonLabel, IonList } from '@ionic/react';
import React from '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,27 @@
.avatar {
display: block;
margin: auto;
min-height: 150px;
}
.avatar .avatar_wrapper {
margin: 16px auto 16px;
border-radius: 50%;
overflow: hidden;
height: 150px;
aspect-ratio: 1;
background: var(--ion-color-step-50);
border: thick solid var(--ion-color-step-200);
}
.avatar .avatar_wrapper:hover {
cursor: pointer;
}
.avatar .avatar_wrapper ion-icon.no-avatar {
width: 100%;
height: 115%;
}
.avatar img {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,56 @@
import { Camera, CameraResultType } from '@capacitor/camera';
import { IonIcon } from '@ionic/react';
import { person } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import './Avatar.css';
export function Avatar({ url, onUpload }: { url: string; onUpload: (e: any, file: string) => Promise<void> }) {
const [avatarUrl, setAvatarUrl] = useState<string | undefined>();
useEffect(() => {
if (url) {
downloadImage(url);
}
}, [url]);
const uploadAvatar = async () => {
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
});
const file = await fetch(photo.dataUrl!)
.then(res => res.blob())
.then(blob => new File([blob], 'my-file', { type: `image/${photo.format}` }));
const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`;
let { error: uploadError } = await supabase.storage.from('avatars').upload(fileName, file);
if (uploadError) {
throw uploadError;
}
onUpload(null, fileName);
} catch (error) {
console.log(error);
}
};
const downloadImage = async (path: string) => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path);
if (error) {
throw error;
}
const url = URL.createObjectURL(data!);
setAvatarUrl(url);
} catch (error: any) {
console.log('Error downloading image: ', error.message);
}
};
return (
<div className="avatar">
<div className="avatar_wrapper" onClick={uploadAvatar}>
{avatarUrl ? <img src={avatarUrl} /> : <IonIcon icon={person} className="no-avatar" />}
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { IonIcon } from '@ionic/react';
import { checkmarkDone, star } from 'ionicons/icons';
export const ChatBottomDetails = ({ message }) => (
<span className="chat-bottom-details" id={`chatTime_${message.id}`}>
<span>{message.date}</span>
{message.sent && <IonIcon icon={checkmarkDone} color="primary" style={{ fontSize: '0.8rem' }} />}
{message.starred && <IonIcon icon={star} />}
</span>
);

View File

@@ -0,0 +1,73 @@
import {
IonIcon,
IonItem,
IonItemOption,
IonItemOptions,
IonItemSliding,
IonNavLink,
IonText,
IonThumbnail,
} from '@ionic/react';
import { checkmarkDone } from 'ionicons/icons';
import ChatHelloworld from '../../pages/chat';
import { ContactStore } from '../../store';
import { getContacts } from '../../store/Selectors';
import HKPartyIonDeleteIcon from '../HKPartyIonDeleteIcon';
import './style.scss';
const ChatItem = ({ chat }) => {
const contacts = ContactStore.useState(getContacts);
const { chats, contact_id } = chat;
const { read, date, preview, received } = chats[chats.length - 1];
const contact = contacts.filter(c => c.id === contact_id)[0];
const notificationCount = chats.filter(chat => chat.read === false).length;
return (
<>
<IonItemSliding>
{/*
<IonItemOptions side="start">
<IonItemOption color="success">Archive</IonItemOption>
</IonItemOptions>
*/}
{/* */}
<IonNavLink routerDirection="forward" component={() => <ChatHelloworld />}>
<IonItem
className="chat-row"
// routerLink={`/tabs/messages/${contact.id}`}
lines="full"
detail={false}
>
<IonThumbnail slot="start" style={{ '--border-radius': '50%' }}>
<img alt="Silhouette of mountains" src="https://ionicframework.com/docs/img/demos/thumbnail.svg" />
</IonThumbnail>
<div className="chat-row-content">
<IonText>{contact.name}</IonText>
<IonText>{read && received && <IonIcon icon={checkmarkDone} color="primary" />}</IonText>
<IonText>{preview}</IonText>
</div>
<div slot="end">
<div>
<IonText>{date}</IonText>
<IonText>
{notificationCount > 0 && <IonText className="chat-notification">{notificationCount}</IonText>}
</IonText>
</div>
</div>
</IonItem>
</IonNavLink>
{/* */}
<IonItemOptions side="end">
<IonItemOption color="danger">
<HKPartyIonDeleteIcon size={'large'} />
</IonItemOption>
</IonItemOptions>
</IonItemSliding>
</>
);
};
export default ChatItem;

View File

@@ -0,0 +1,20 @@
.chat-row {
.chat-row-content {
padding: 1rem;
}
.chat-content {
background-color: pink;
}
// img {
// border-radius: 500px;
// height: 2.5rem;
// width: 2.5rem;
// margin-right: 1.5rem;
// }
// ion-label {
// h1 {
// font-size: 1rem;
// }
// }
}

View File

@@ -0,0 +1,14 @@
const Quote = ({ message, contact, repliedMessage }) => (
<div className="in-chat-reply-to-container">
<h1>{contact.name}</h1>
<p>{repliedMessage.preview}</p>
</div>
);
export const ChatRepliedQuote = ({ message, contact, repliedMessage }) => {
if (message.reply && repliedMessage) {
return <Quote message={message} contact={contact} repliedMessage={repliedMessage} />;
} else {
return '';
}
};

View File

@@ -0,0 +1,52 @@
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonList,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { ContactStore } from '../store';
import { getContacts } from '../store/Selectors';
import './ContactModal.scss';
const ContactModal = ({ close }) => {
const contacts = ContactStore.useState(getContacts);
return (
<div style={{ height: '100%' }}>
<IonHeader>
<IonToolbar>
<IonTitle>New Chat</IonTitle>
<IonButtons slot="end">
<IonButton fill="clear" onClick={close}>
Cancel
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
{contacts.map(contact => {
return (
<IonItem key={`contact_${contact.id}`} lines="full" className="contact-item">
<img src={contact.avatar} alt="contact avatar" />
<IonLabel>
<h1>{contact.name}</h1>
<p>Available</p>
</IonLabel>
</IonItem>
);
})}
</IonList>
</IonContent>
</div>
);
};
export default ContactModal;

View File

@@ -0,0 +1,14 @@
.contact-item {
img {
border-radius: 500px;
height: 2.5rem;
width: 2.5rem;
margin-right: 1.5rem;
}
ion-label {
h1 {
font-size: 1rem;
}
}
}

View File

@@ -0,0 +1,8 @@
import { IonIcon } from '@ionic/react';
import { trashOutline } from 'ionicons/icons';
const HKPartyIonDeleteIcon = ({ ...props }) => {
return <IonIcon icon={trashOutline} {...props} />;
};
export default HKPartyIonDeleteIcon;

View File

@@ -0,0 +1,8 @@
import { IonIcon } from '@ionic/react';
import { trashOutline } from 'ionicons/icons';
const HKPartyIonDeleteIcon = ({ ...props }) => {
return <IonIcon icon={trashOutline} {...props} />;
};
export default HKPartyIonDeleteIcon;

View File

@@ -0,0 +1,13 @@
import { IonHeader } from '@ionic/react';
const HKPartyIonHeader = ({ children, ...props }) => {
return (
<IonHeader translucent={true} className="ion-no-border" {...props}>
{/* */}
{children}
{/* */}
</IonHeader>
);
};
export default HKPartyIonHeader;

View File

@@ -0,0 +1,13 @@
import { IonPage } from '@ionic/react';
const HKPartyIonPage = ({ children, ...props }) => {
return (
<IonPage {...props}>
{/* */}
{children}
{/* */}
</IonPage>
);
};
export default HKPartyIonPage;

View File

@@ -0,0 +1,28 @@
import { IonToolbar } from '@ionic/react';
const HKPartyIonToolbar = ({ children, ...props }) => {
return (
<IonToolbar {...props}>
{/* */}
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
color: 'black',
minHeight: '30px',
margin: '0 10px',
//
fontWeight: 'bold',
fontSize: '1.2rem',
}}
>
{children}
</div>
{/* */}
</IonToolbar>
);
};
export default HKPartyIonToolbar;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Redirect } from 'react-router';
import { connect } from '../data/connect';
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,24 @@
import { IonContent, IonPage, IonSpinner, IonText } from '@ionic/react';
function Loading() {
return (
<IonPage>
<IonContent fullscreen={true} className="ion-padding">
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<IonSpinner name="dots"></IonSpinner>
<IonText>{'Loading'}</IonText>
</div>
</IonContent>
</IonPage>
);
}
export default Loading;

View File

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

View File

@@ -0,0 +1,88 @@
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,138 @@
import React from 'react';
import { RouteComponentProps, useLocation, withRouter } from 'react-router';
import {
IonContent,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenu,
IonMenuToggle,
IonToggle,
} from '@ionic/react';
import {
calendarOutline,
hammer,
help,
informationCircleOutline,
logIn,
logOut,
mapOutline,
moonOutline,
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 },
{ title: 'Message', path: '/tabs/message', icon: informationCircleOutline },
{ title: 'Profile', path: '/tabs/profile', 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
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,23 @@
import { IonRouterContext } from '@ionic/react';
import React, { useContext, useEffect } from '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');
console.error('who call this ?');
}, [setIsLoggedIn, setUsername]);
return null;
};
export default RedirectToLogin;

View File

@@ -0,0 +1,54 @@
import { CreateAnimation, IonButton, IonCol, IonIcon, IonLabel, IonRow } from '@ionic/react';
import { closeCircleOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
// import './style.css';
const ReplyTo = ({ contact, replyToMessage = false, replyToAnimationRef, setReplyToMessage, messageSent }) => {
const [cancellingReplyTo, setCancellingReplyTo] = useState(false);
useEffect(() => {
messageSent && cancelReplyTo();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messageSent]);
const slideAnimation = {
property: 'transform',
fromValue: 'translateY(100px)',
toValue: 'translateY(0px)',
};
const replyToAnimation = {
duration: 300,
direction: !cancellingReplyTo ? 'normal' : 'reverse',
iterations: '1',
fromTo: [slideAnimation],
easing: 'ease-in-out',
};
// Cancel the reply-to
const cancelReplyTo = async () => {
setCancellingReplyTo(true);
await replyToAnimationRef.current.animation.play();
setCancellingReplyTo(false);
setReplyToMessage(false);
};
return (
<CreateAnimation ref={replyToAnimationRef} {...replyToAnimation}>
<IonRow className="ion-align-items-center chat-reply-to-row" id="replyTo">
<IonCol size="10" className="chat-reply-to-container">
<IonLabel className="chat-reply-to-name">{contact}</IonLabel>
<IonLabel className="chat-reply-to-message">{replyToMessage.preview}</IonLabel>
</IonCol>
<IonCol size="1">
<IonButton fill="clear" onClick={cancelReplyTo}>
<IonIcon size="large" icon={closeCircleOutline} color="primary" />
</IonButton>
</IonCol>
</IonRow>
</CreateAnimation>
);
};
export default ReplyTo;

View File

@@ -0,0 +1,290 @@
.chat-page ion-header,
.chat-page ion-toolbar {
--min-height: 3.5rem;
}
.chat-page ion-title {
margin-left: -3.5rem;
}
.chat-page ion-title p {
padding: 0;
margin: 0;
}
.chat-contact {
display: flex;
flex-direction: row;
align-content: center;
justify-content: center;
align-items: center;
}
.chat-contact img {
height: 2rem;
width: 2rem;
border-radius: 500px;
}
.chat-contact-details {
display: flex;
flex-direction: column;
margin-left: 0.5rem;
text-align: left;
}
.chat-contact-details p {
font-size: 0.9rem;
}
.chat-contact-details ion-text {
font-size: 0.7rem;
font-weight: 400;
}
.chat-bubble {
border-radius: 5px;
margin-left: 1rem;
margin-right: 1rem;
margin-top: 0.8rem;
padding: 0.5rem;
max-width: 80%;
clear: both;
display: flex;
flex-direction: row;
transition: 0.2s all linear;
}
.chat-bubble:last-child {
margin-bottom: 0.8rem;
}
.bubble-sent {
background-color: var(--chat-bubble-sent-color);
float: right;
}
.bubble-received {
background-color: var(--chat-bubble-received-color);
float: left;
}
.chat-bubble p {
padding: 0;
margin: 0;
}
.chat-footer {
/* background-color: rgb(22, 22, 22); */
border-top: 1px solid rgb(47, 47, 47);
padding-top: 0.2rem;
padding-bottom: 1rem;
}
.chat-footer ion-textarea {
/* background-color: rgb(31, 31, 31); */
background-color: rgba(32, 32, 32, 0.1);
/* border: 1px solid rgb(36, 36, 36); */
color: white;
border-radius: 25px;
padding-left: 0.5rem;
caret-color: var(--ion-color-primary);
}
.chat-footer ion-icon {
font-size: 1.5rem;
margin-top: 0.2rem;
}
.chat-input-container {
width: 70%;
margin-right: 0.75rem;
}
.chat-send-button {
margin: 0 !important;
padding: 0 !important;
position: absolute;
right: 17px;
margin-top: -0.2rem !important;
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
justify-content: center;
}
.chat-send-button ion-icon {
color: white;
background-color: var(--ion-color-primary);
font-size: 1.1rem;
border-radius: 500px;
padding: 0.5rem;
}
.chat-time {
color: rgb(165, 165, 165);
font-size: 0.75rem;
right: 0;
bottom: 0 !important;
margin: 0;
padding: 0;
margin-top: 5px;
}
.bubble-arrow {
position: absolute;
float: left;
left: 6px;
margin-top: -8px;
/* top: 0px; */
}
.bubble-arrow.alt {
position: relative;
bottom: 0px;
left: auto;
right: -3px;
float: right;
}
.bubble-arrow:after {
content: "";
position: absolute;
border-top: 15px solid var(--chat-bubble-received-color);
border-left: 15px solid transparent;
border-radius: 4px 0 0 0px;
width: 0;
height: 0;
}
.bubble-arrow.alt:after {
border-top: 15px solid var(--chat-bubble-sent-color);
transform: scaleX(-1);
}
.chat-reply-to-row {
bottom: 70px !important;
position: absolute;
border-left: 4px solid rgb(224, 176, 18);
width: 100%;
background-color: rgb(22, 22, 22);
border-top: 1px solid rgb(47, 47, 47);
padding: 0.5rem;
padding-bottom: 0.8rem;
}
.chat-reply-to-container {
display: flex;
flex-direction: column;
}
.chat-reply-to-name {
color: rgb(224, 176, 18);
font-weight: 500;
margin-bottom: 0.5rem;
}
.chat-reply-to-message {
font-size: 0.8rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.all-chats {
}
.chat-bottom-details {
display: flex;
flex-direction: row;
width: 100%;
align-content: center;
align-items: center;
justify-content: flex-end;
margin-top: 0.4rem;
}
.chat-bottom-details ion-icon {
font-size: 0.6rem;
color: grey;
margin-left: 0.5rem;
margin-top: 0.05rem;
}
.chat-bottom-details span {
margin: 0;
padding: 0;
font-size: 0.75rem;
color: rgb(190, 190, 190);
}
.in-chat-reply-to-container {
/* background-color: rgba(0, 0, 0, 0.2); */
border-left: 3px solid rgb(224, 176, 18);
height: fit-content;
padding: 0.5rem;
border-radius: 5px;
margin-bottom: 0.5rem;
}
.in-chat-reply-to-container h1 {
margin: 0;
padding: 0;
color: rgb(224, 176, 18);
font-size: 0.8rem;
}
.in-chat-reply-to-container p {
color: rgb(167, 167, 167);
font-size: 0.8rem;
}
.bottom-container {
position: absolute;
bottom: 4.5rem;
height: 5rem;
background-color: red;
width: 100%;
}

View File

@@ -0,0 +1,102 @@
import { AlertButton, IonAlert, IonItemDivider, IonItemGroup, IonLabel, IonList, IonListHeader } from '@ionic/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { connect } from '../data/connect';
import { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
import { Schedule, Session } from '../models/Schedule';
import SessionListItem from './SessionListItem';
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,152 @@
import React from 'react';
import { getMode } from '@ionic/core';
import {
IonButton,
IonButtons,
IonCheckbox,
IonContent,
IonFooter,
IonHeader,
IonIcon,
IonItem,
IonList,
IonListHeader,
IonTitle,
IonToolbar,
} from '@ionic/react';
import {
call,
cog,
colorPalette,
compass,
construct,
document,
hammer,
logoAngular,
logoIonic,
restaurant,
} 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 } = {
Angular: logoAngular,
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,106 @@
import {
AlertButton,
IonItem,
IonItemOption,
IonItemOptions,
IonItemSliding,
IonLabel,
useIonToast,
} from '@ionic/react';
import React, { useRef } from '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.timeStart}&mdash;&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,46 @@
import { IonFab, IonFabButton, IonFabList, IonIcon, IonLoading } from '@ionic/react';
import { logoFacebook, logoInstagram, logoTwitter, logoVimeo, shareSocial } 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,54 @@
import { IonAvatar, IonCard, IonCardContent, IonCardHeader, IonItem, IonLabel, IonList } from '@ionic/react';
import React from 'react';
import { Session } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
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;

View File

@@ -0,0 +1,3 @@
const IONIC_DEFAULT_AVATAR = 'https://ionicframework.com/docs/img/demos/avatar.svg';
export default { IONIC_DEFAULT_AVATAR };

View File

@@ -0,0 +1,26 @@
import { Session } from '@supabase/supabase-js';
import React, { createContext, useContext, useState } from 'react';
import { supabase } from '../supabaseClient';
export const SessionContext = createContext<{
session: Session | null;
} | null>(null);
export const SessionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [session] = useState(() => supabase.auth.session());
// const value = useMemo(() => ({ session, setSession }), [session, setSession]);
const value = { session };
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
};
export const useSession = () => {
const context = useContext(SessionContext);
if (!context) {
throw new Error('useSession must be used within a SessionProvider');
}
return context;
};

View File

@@ -0,0 +1,79 @@
import { Session } from '@supabase/supabase-js';
import React, { createContext, PropsWithChildren, useEffect, useReducer, useState } from 'react';
import { useListPartyEventSummaries } from '../hooks/useListPartyEventSummaries';
import { supabase } from '../supabaseClient';
import { AppState, initialState, reducers } from './state';
export interface AppContextState {
state: AppState;
dispatch: React.Dispatch<any>;
session: Session | null;
}
export const AppContext = createContext<AppContextState>({
state: initialState,
dispatch: () => undefined,
session: null,
});
export const AppContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [store, dispatch] = useReducer(reducers, initialState);
const [party_event_summaries] = useListPartyEventSummaries();
const [session, setSession] = useState<Session | null>(null);
const [profile, setProfile] = useState<{} | null>(null);
const [showBottomTabBar, setShowBottomTabBar] = useState(true);
const [helloworld, setHelloworld] = useState('');
function getProfileById(id) {
return supabase.from('profiles').select('*').filter('id', 'in', `("${id}")`);
}
useEffect(() => {
console.log({ profile });
}, [profile]);
useEffect(() => {
const run = async () => {
const { data } = await supabase.auth.getSession();
setSession(data.session);
};
run();
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session && session.user) {
const { id } = session.user;
getProfileById(id).then(({ data }) => {
setProfile(data[0]);
});
}
});
}, []);
useEffect(() => {
console.log('helloworld from appcontext');
}, [helloworld]);
return (
<AppContext.Provider
value={{
state: store,
dispatch,
session,
//
party_event_summaries,
profile,
showBottomTabBar,
setShowBottomTabBar,
helloworld,
setHelloworld,
}}
>
{children}
</AppContext.Provider>
);
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,122 @@
import { createSelector } from 'reselect';
import { Location } from '../models/Location';
import { Schedule, ScheduleGroup, Session } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
import { AppState } from './state';
const getSchedule = (state: AppState) => {
return state.data.schedule;
};
export const getSpeakers = (state: AppState) => state.data.speakers;
const getSessions = (state: AppState) => state.data.sessions;
const getFilteredTracks = (state: AppState) => state.data.filteredTracks;
const getFavoriteIds = (state: AppState) => state.data.favorites;
const getSearchText = (state: AppState) => state.data.searchText;
export const getFilteredSchedule = createSelector(getSchedule, getFilteredTracks, (schedule, filteredTracks) => {
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group: ScheduleGroup) => {
const sessions: Session[] = [];
group.sessions.forEach(session => {
session.tracks.forEach(track => {
if (filteredTracks.indexOf(track) > -1) {
sessions.push(session);
}
});
});
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
export const getSearchedSchedule = createSelector(getFilteredSchedule, getSearchText, (schedule, searchText) => {
if (!searchText) {
return schedule;
}
const groups: ScheduleGroup[] = [];
schedule.groups.forEach(group => {
const sessions = group.sessions.filter(s => s.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
export const getScheduleList = createSelector(getSearchedSchedule, schedule => schedule);
export const getGroupedFavorites = createSelector(getScheduleList, getFavoriteIds, (schedule, favoriteIds) => {
const groups: ScheduleGroup[] = [];
schedule.groups.forEach(group => {
const sessions = group.sessions.filter(s => favoriteIds.indexOf(s.id) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
const getIdParam = (_state: AppState, props: any) => {
return props.match.params['id'];
};
export const getSession = createSelector(getSessions, getIdParam, (sessions, id) => {
return sessions.find((s: Session) => s.id === id);
});
export const getSpeaker = createSelector(getSpeakers, getIdParam, (speakers, id) =>
speakers.find((x: Speaker) => x.id === id),
);
export const getSpeakerSessions = createSelector(getSessions, sessions => {
const speakerSessions: { [key: string]: Session[] } = {};
sessions.forEach((session: Session) => {
session.speakerNames &&
session.speakerNames.forEach(name => {
if (speakerSessions[name]) {
speakerSessions[name].push(session);
} else {
speakerSessions[name] = [session];
}
});
});
return speakerSessions;
});
export const mapCenter = (state: AppState) => {
const item = state.data.locations.find((l: Location) => l.id === state.data.mapCenterId);
if (item == null) {
return {
id: 1,
name: 'Map Center',
lat: 43.071584,
lng: -89.38012,
};
}
return item;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';
export const useCamera = () => {
const takePhoto = async () => {
const options = {
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
};
const cameraPhoto = await Camera.getPhoto(options);
return Capacitor.convertFileSrc(cameraPhoto.webPath);
};
return {
takePhoto,
};
};

View File

@@ -0,0 +1,20 @@
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
export default useFriendStatus;

View File

@@ -0,0 +1,18 @@
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';
export const useGallery = () => {
const prompt = async () => {
const options = {
resultType: CameraResultType.Uri,
source: CameraSource.Prompt,
quality: 100,
};
const cameraPhoto = await Camera.getPhoto(options);
return Capacitor.convertFileSrc(cameraPhoto.webPath);
};
return {
prompt,
};
};

View File

@@ -0,0 +1,20 @@
// REQ0041/home_discover_event_tab
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useGetPartyEventDetail = ({ party_event_id }) => {
const [party_event_detail, setPartyEventDetail] = useState(null);
async function getPartyEvents() {
let { data } = await supabase.from('view_party_event_summaries').select('*').filter('id', '=', party_event_id);
setPartyEventDetail(data);
}
useEffect(() => {
getPartyEvents();
}, [party_event_id]);
return [party_event_detail];
};

View File

@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useGetPartyEventOrder = ({ event_id }) => {
const [party_event_order, setPartyEventOrder] = useState(null);
async function getPartyEventOrder() {
let { data } = await supabase.from('party_event_orders').select(`
id, profiles(id, full_name, gender, remarks)
`);
// status == 2 , paid
// tidy up
// .filter('party_event_id', 'in', `("${event_id}")`);
// setPartyEventOrder(data);
// console.log({data})
}
useEffect(() => {
getPartyEventOrder();
}, []);
return [party_event_order];
};

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useGetProfileById = ({ id }) => {
const [party_event_order, setPartyEventOrder] = useState(null);
async function getPartyEventOrder() {
let { data } = await supabase
.from('profiles')
.select('*')
.filter('id', 'in', `("33a1f462-7085-4655-b42e-a0f1e8395f6c")`);
}
useEffect(() => {
getPartyEventOrder();
}, []);
return [party_event_order];
};

View File

@@ -0,0 +1,25 @@
// REQ0044/near_by_page
// what to do:
// list users near by except current user
import { useContext, useEffect, useState } from 'react';
import { AppContext } from '../data/AppContext';
import { supabase } from '../supabaseClient';
export const useGetUserProfileById = ({ user_id }) => {
const { profile } = useContext(AppContext);
const [other_user_profiles, setOtherUserProfiles] = useState([]);
async function getPartyEvents() {
let { data } = await supabase.from('profiles').select('*').filter('user_id', 'in', `(${user_id})`).limit(1);
setOtherUserProfiles(data[0]);
}
useEffect(() => {
getPartyEvents();
}, []);
return [other_user_profiles];
};

View File

@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useJoinTest = () => {
const [test_data, setTestData] = useState(null);
async function getPartyEvents() {
const { data, error } = await supabase.from('countries').select(`
id,
name,
cities ( id, name )
`);
console.log({ data });
setTestData(data);
}
useEffect(() => {
getPartyEvents();
}, []);
return [test_data];
};

View File

@@ -0,0 +1,26 @@
// REQ0044/near_by_page
// what to do:
// list users near by except current user
import { useContext, useEffect, useState } from 'react';
import { AppContext } from '../data/AppContext';
import { supabase } from '../supabaseClient';
export const useListOtherUserProfiles = () => {
const { profile } = useContext(AppContext);
const [other_user_profiles, setOtherUserProfiles] = useState([]);
const user_id = '1';
async function getPartyEvents() {
console.log({ profile });
let { data } = await supabase.from('profiles').select('*').not('user_id', 'in', `(${user_id})`).limit(10);
setOtherUserProfiles(data);
}
useEffect(() => {
getPartyEvents();
}, []);
return [other_user_profiles];
};

View File

@@ -0,0 +1,19 @@
// REQ0041/home_discover_event_tab
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useListPartyEventOrderSummary = ({ user_id }) => {
const [party_event_summaries, setPartyEventSummaries] = useState(null);
async function getPartyEvents() {
let { data } = await supabase.from('view_party_event_orders_summary').select('*').filter('user_id', 'eq', '1');
setPartyEventSummaries(data);
}
useEffect(() => {
getPartyEvents();
}, []);
return [party_event_summaries];
};

View File

@@ -0,0 +1,19 @@
// REQ0041/home_discover_event_tab
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useListPartyEventOrders = ({ user_id }) => {
const [party_event_summaries, setPartyEventSummaries] = useState(null);
async function getPartyEvents() {
let { data } = await supabase.from('view_party_event_orders').select('*').filter('user_id', 'eq', '1');
setPartyEventSummaries(data);
}
useEffect(() => {
getPartyEvents();
}, []);
return [party_event_summaries];
};

View File

@@ -0,0 +1,21 @@
// REQ0041/home_discover_event_tab
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useListPartyEventSummaries = () => {
const [party_event_summaries, setPartyEventSummaries] = useState(null);
async function getPartyEvents() {
let { data } = await supabase.from('view_party_event_summaries').select('*');
setPartyEventSummaries(data);
console.log({ party_event_summaries });
}
useEffect(() => {
getPartyEvents();
}, []);
return [party_event_summaries];
};

View File

@@ -0,0 +1,21 @@
// REQ0041/home_discover_event_tab
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useListPartyEvents = () => {
const [party_event_summaries, setPartyEventSummaries] = useState(null);
async function getPartyEvents() {
let { data } = await supabase.from('view_party_event_summaries').select('*');
setPartyEventSummaries(data);
console.log({ data });
}
useEffect(() => {
getPartyEvents();
}, []);
return [party_event_summaries];
};

View File

@@ -0,0 +1,24 @@
// REQ0041/home_discover_event_tab
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
export const useViewPartyEventParticipants = ({ party_event_id, limit }) => {
const [participants, setParticipants] = useState(null);
async function getPartyEvents() {
let { data } = await supabase
.from('view_party_event_participants')
.select('*')
.filter('id', '=', party_event_id)
.limit(limit);
setParticipants(data);
}
useEffect(() => {
getPartyEvents();
}, [party_event_id]);
return [participants];
};

View File

@@ -0,0 +1,21 @@
import i18n from 'i18next';
import Languagedetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import en from './locales/en/en.json';
import hk from './locales/hk/hk.json';
i18n
.use(initReactI18next)
.use(Languagedetector)
.init({
resources: {
en: { translation: en },
zhHk: { translation: hk },
},
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});
export { i18n };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.register();

View File

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

View File

@@ -0,0 +1,20 @@
export interface Schedule {
date: string;
groups: ScheduleGroup[];
}
export interface ScheduleGroup {
time: string;
sessions: Session[];
}
export interface Session {
id: number;
timeStart: string;
timeEnd: string;
name: string;
location: string;
description: string;
speakerNames: string[];
tracks: string[];
}

View File

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

View File

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

View File

@@ -0,0 +1,138 @@
import {
IonButton,
IonButtons,
IonContent,
IonDatetime,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonMenuButton,
IonPage,
IonPopover,
IonSelect,
IonSelectOption,
IonText,
IonToolbar,
} from '@ionic/react';
import { format, parseISO } from 'date-fns';
import { ellipsisHorizontal, ellipsisVertical } from 'ionicons/icons';
import React, { useState } from 'react';
import AboutPopover from '../../components/AboutPopover';
import './style.scss';
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 conference on {displayDate(conferenceDate, 'MMM dd, yyyy')} featuring
talks from the Ionic team. It is focused on Ionic applications being built with Ionic Framework. This
includes migrating apps to the latest version of the framework, Angular concepts, Webpack, Sass, and many
other technologies used in Ionic 2. Tickets are completely sold out, and were expecting more than 1000
developers making this the largest Ionic conference ever!
</p>
<h3 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,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,6 @@
#account-page {
img {
max-width: 140px;
border-radius: 50%;
}
}

View File

@@ -0,0 +1,101 @@
import {
IonAlert,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonList,
IonMenuButton,
IonPage,
IonTitle,
IonToolbar,
} from '@ionic/react';
import React, { useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { connect } from '../data/connect';
import { setUsername } from '../data/user/user.actions';
import './Account.scss';
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,17 @@
#login-page,
#signup-page,
#support-page {
.login-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.login-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}
}

View File

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

View File

@@ -0,0 +1,94 @@
// REQ102-navigation-bar
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { calendar, informationCircle, location, people } from 'ionicons/icons';
import React, { useContext } from 'react';
import { Redirect, Route } from 'react-router';
import About from '../About';
import MapView from '../MapView';
import SchedulePage from '../SchedulePage';
import SessionDetail from '../SessionDetail';
import SpeakerDetail from '../SpeakerDetail';
import SpeakerList from '../SpeakerList';
//
import { AppContext } from '../../data/AppContext';
// import Chat from '../chat.del.3';
import Events from '../events';
import FavouriteEvents from '../favourite_events';
import Messages from '../messages';
import NearBy from '../near_by';
import Orders from '../orders';
import Profile from '../profile';
import UserProfile from '../user_profile';
interface MainTabsProps {}
const MainTabs: React.FC<MainTabsProps> = () => {
const { showBottomTabBar } = useContext(AppContext);
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/events" render={() => <Events />} exact={true} />
<Route path="/tabs/nearby" render={() => <NearBy />} exact={true} />
<Route path="/tabs/orders" render={() => <Orders />} exact={true} />
<Route path="/tabs/messages" render={() => <Messages />} exact={true} />
<Route path="/tabs/profile" render={() => <Profile />} exact={true} />
<Route path="/tabs/about" render={() => <About />} exact={true} />
{/* FIXME: chat room missing */}
{/* <Route path="/tabs/chat_history/:id" component={Chat} exact={true} /> */}
<Route path="/tabs/favourite_events" render={() => <FavouriteEvents />} exact={true} />
<Route path="/tabs/map" render={() => <MapView />} exact={true} />
<Route path="/tabs/schedule" render={() => <SchedulePage />} exact={true} />
<Route path="/tabs/schedule/:id" component={SessionDetail} />
<Route path="/tabs/speakers" render={() => <SpeakerList />} exact={true} />
<Route path="/tabs/speakers/:id" component={SpeakerDetail} exact={true} />
<Route path="/tabs/speakers/sessions/:id" component={SessionDetail} />
<Route path="/tabs/user_profile/:user_id" component={UserProfile} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="events" href="/tabs/events">
<IonIcon icon={calendar} />
<IonLabel>{'Events'}</IonLabel>
</IonTabButton>
<IonTabButton tab="nearby" href="/tabs/nearby">
<IonIcon icon={people} />
<IonLabel>{'NearBy'}</IonLabel>
</IonTabButton>
<IonTabButton tab="orders" href="/tabs/orders">
<IonIcon icon={people} />
<IonLabel>{'Orders'}</IonLabel>
</IonTabButton>
<IonTabButton tab="message" href="/tabs/messages">
<IonIcon icon={location} />
<IonLabel>{'Message'}</IonLabel>
</IonTabButton>
<IonTabButton tab="profile" href="/tabs/profile">
<IonIcon icon={informationCircle} />
<IonLabel>{'Profile'}</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default MainTabs;

View File

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

View File

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

View File

@@ -0,0 +1,161 @@
import {
IonButton,
IonContent,
IonHeader,
IonInput,
IonItem,
IonLabel,
IonPage,
IonTitle,
IonToolbar,
useIonLoading,
useIonRouter,
useIonToast,
} from '@ionic/react';
import { useEffect, useState } from 'react';
import { Avatar } from '../../components/Avatar';
import { supabase } from '../../supabaseClient';
import './style.scss';
export function AccountPage() {
const [showLoading, hideLoading] = useIonLoading();
const [showToast] = useIonToast();
const [session] = useState(() => supabase.auth.session());
const router = useIonRouter();
const [profile, setProfile] = useState({
username: '',
website: '',
avatar_url: '',
});
useEffect(() => {
getProfile();
}, [session]);
const getProfile = async () => {
console.log('get');
// await showLoading();
try {
const user = supabase.auth.user();
let { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user!.id)
.single();
if (error && status !== 406) {
throw error;
}
if (data) {
setProfile({
username: data.username,
website: data.website,
avatar_url: data.avatar_url,
});
}
} catch (error: any) {
showToast({ message: error.message, duration: 5000 });
} finally {
// await hideLoading();
}
};
// const signOut = async () => {
// await supabase.auth.signOut();
// router.push('/', 'forward', 'replace');
// };
const updateProfile = async (e?: any, avatar_url: string = '') => {
e?.preventDefault();
console.log('update ');
console.log('show loading here');
// await showLoading();
try {
const user = supabase.auth.user();
const updates = {
id: user!.id,
...profile,
avatar_url: avatar_url,
updated_at: new Date(),
};
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
});
if (error) {
throw error;
}
} catch (error: any) {
showToast({ message: error.message, duration: 5000 });
} finally {
console.log('hide loading here');
// await hideLoading();
}
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Account</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<Avatar url={profile.avatar_url} onUpload={updateProfile}></Avatar>
<form onSubmit={updateProfile}>
<IonItem>
<IonLabel>
<p>Email</p>
<p>{session?.user?.email}</p>
</IonLabel>
</IonItem>
<IonItem>
<IonLabel position="stacked">Name</IonLabel>
<IonInput
type="text"
name="username"
value={profile.username}
onIonChange={e => setProfile({ ...profile, username: e.detail.value ?? '' })}
></IonInput>
</IonItem>
<IonItem>
<IonLabel position="stacked">Website</IonLabel>
<IonInput
type="url"
name="website"
value={profile.website}
onIonChange={e => setProfile({ ...profile, website: e.detail.value ?? '' })}
></IonInput>
</IonItem>
<div className="ion-text-center">
<IonButton fill="clear" type="submit">
Update Profile
</IonButton>
</div>
</form>
<div className="ion-text-center">
<IonButton
fill="clear"
onClick={() => {
router.push('/sblogout', 'forward', 'replace');
}}
>
Log Out
</IonButton>
</div>
</IonContent>
</IonPage>
);
}

View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import {
IonButton,
IonContent,
IonInput,
IonItem,
IonLabel,
IonList,
IonPage,
useIonLoading,
useIonRouter,
useIonToast,
} from '@ionic/react';
import { supabase } from '../../supabaseClient';
function LoginPage() {
const [email, setEmail] = useState('user1@example.com');
const [password, setPassword] = useState('Aa1234567');
const [showLoading, hideLoading] = useIonLoading();
const [showToast] = useIonToast();
const router = useIonRouter();
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await showLoading();
try {
// // OPT
// // await supabase.auth.signIn({ email });
console.log({ email, password });
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
console.error(error);
router.push('/login_error', 'forward', 'replace');
// return redirect('/login?message=Could not authenticate user');
}
router.push('/tabs/events', 'forward', 'replace');
// return redirect('/protected');
} catch (e: any) {
await showToast({
message: e.error_description || e.message,
duration: 5000,
});
} finally {
await hideLoading();
}
};
return (
<IonPage>
<IonContent>
<IonList inset={true}>
<form onSubmit={handleLogin}>
<IonItem>
<IonLabel position="stacked">{'Email'}</IonLabel>
<IonInput
type="email"
value={email}
name="email"
onIonChange={e => setEmail(e.detail.value ?? '')}
></IonInput>
</IonItem>
<IonItem>
<IonLabel position="stacked">{'Password'}</IonLabel>
<IonInput
type="password"
value={password}
name="password"
onIonChange={e => setPassword(e.detail.value ?? '')}
></IonInput>
</IonItem>
<div className="ion-text-center">
<IonButton type="submit" fill="clear">
{'Login'}
</IonButton>
</div>
</form>
</IonList>
</IonContent>
</IonPage>
);
}
export default React.memo(LoginPage);

View File

@@ -0,0 +1,18 @@
import { useIonRouter } from '@ionic/react';
import { useContext, useEffect } from 'react';
import { AppContext } from '../../data/AppContext';
import { supabase } from '../../supabaseClient';
function SBLogout() {
const router = useIonRouter();
const { session } = useContext(AppContext);
useEffect(() => {
supabase.auth.signOut();
router.push('/sblogin');
}, []);
return <>SBLogout</>;
}
export default SBLogout;

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-angular ion-label {
border-left: 2px solid var(--ion-color-angular);
padding-left: 10px;
}
ion-item-sliding.track-communication ion-label {
border-left: 2px solid var(--ion-color-communication);
padding-left: 10px;
}
ion-item-sliding.track-tooling ion-label {
border-left: 2px solid var(--ion-color-tooling);
padding-left: 10px;
}
ion-item-sliding.track-services ion-label {
border-left: 2px solid var(--ion-color-services);
padding-left: 10px;
}
ion-item-sliding.track-design ion-label {
border-left: 2px solid var(--ion-color-design);
padding-left: 10px;
}
ion-item-sliding.track-workshop ion-label {
border-left: 2px solid var(--ion-color-workshop);
padding-left: 10px;
}
ion-item-sliding.track-food ion-label {
border-left: 2px solid var(--ion-color-food);
padding-left: 10px;
}
ion-item-sliding.track-documentation ion-label {
border-left: 2px solid var(--ion-color-documentation);
padding-left: 10px;
}
ion-item-sliding.track-navigation ion-label {
border-left: 2px solid var(--ion-color-navigation);
padding-left: 10px;
}
}

View File

@@ -0,0 +1,167 @@
import React, { useRef, useState } from 'react';
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonMenuButton,
IonModal,
IonPage,
IonRefresher,
IonRefresherContent,
IonSearchbar,
IonSegment,
IonSegmentButton,
IonTitle,
IonToast,
IonToolbar,
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 { connect } from '../data/connect';
import * as selectors from '../data/selectors';
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-angular {
color: var(--ion-color-angular);
}
.session-track-communication {
color: var(--ion-color-communication);
}
.session-track-tooling {
color: var(--ion-color-tooling);
}
.session-track-services {
color: var(--ion-color-services);
}
.session-track-design {
color: var(--ion-color-design);
}
.session-track-workshop {
color: var(--ion-color-workshop);
}
.session-track-food {
color: var(--ion-color-food);
}
.session-track-documentation {
color: var(--ion-color-documentation);
}
.session-track-navigation {
color: var(--ion-color-navigation);
}
.show-favorite {
position: relative;
}
.icon-heart-empty {
position: absolute;
top: 5px;
right: 5px;
transform: scale(1);
transition: transform 0.3s ease;
}
.icon-heart {
position: absolute;
top: 5px;
right: 5px;
transform: scale(0);
transition: transform 0.3s ease;
}
.show-favorite .icon-heart {
transform: scale(1);
}
.show-favorite .icon-heart-empty {
transform: scale(0);
}
h1 {
margin: 0;
}
}

View File

@@ -0,0 +1,122 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonPage,
IonText,
IonToolbar,
} from '@ionic/react';
import { cloudDownload, share, star, starOutline } from 'ionicons/icons';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import { addFavorite, removeFavorite } from '../data/sessions/sessions.actions';
import { Session } from '../models/Schedule';
import './SessionDetail.scss';
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,138 @@
import {
IonButton,
IonButtons,
IonCol,
IonContent,
IonHeader,
IonInput,
IonItem,
IonList,
IonMenuButton,
IonPage,
IonRow,
IonText,
IonTitle,
IonToolbar,
} from '@ionic/react';
import React, { useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { connect } from '../data/connect';
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
import './Login.scss';
interface OwnProps extends RouteComponentProps {}
interface DispatchProps {
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
}
interface LoginProps extends OwnProps, DispatchProps {}
const Login: React.FC<LoginProps> = ({ setIsLoggedIn, history, setUsername: setUsernameAction }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [usernameError, setUsernameError] = useState(false);
const [passwordError, setPasswordError] = useState(false);
const login = async (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if (!username) {
setUsernameError(true);
}
if (!password) {
setPasswordError(true);
}
if (username && password) {
await setIsLoggedIn(true);
await setUsernameAction(username);
history.push('/tabs/schedule', { direction: 'none' });
}
};
return (
<IonPage id="signup-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Signup</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={login}>
<IonList>
<IonItem>
<IonInput
label="Username"
labelPlacement="stacked"
color="primary"
name="username"
type="text"
value={username}
spellCheck={false}
autocapitalize="off"
onIonInput={e => {
setUsername(e.detail.value as string);
setUsernameError(false);
}}
required
>
{formSubmitted && usernameError && (
<IonText color="danger" slot="error">
<p>Username is required</p>
</IonText>
)}
</IonInput>
</IonItem>
<IonItem>
<IonInput
label="Password"
labelPlacement="stacked"
color="primary"
name="password"
type="password"
value={password}
onIonInput={e => {
setPassword(e.detail.value as string);
setPasswordError(false);
}}
>
{formSubmitted && passwordError && (
<IonText color="danger" slot="error">
<p>Password is required</p>
</IonText>
)}
</IonInput>
</IonItem>
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">
Create
</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
mapDispatchToProps: {
setIsLoggedIn,
setUsername,
},
component: Login,
});

View File

@@ -0,0 +1,77 @@
#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,163 @@
import React, { useState } from 'react';
import { RouteComponentProps } from 'react-router';
import './SpeakerDetail.scss';
import { ActionSheetButton } from '@ionic/core';
import {
IonActionSheet,
IonBackButton,
IonButton,
IonButtons,
IonChip,
IonContent,
IonHeader,
IonIcon,
IonLabel,
IonPage,
IonToolbar,
} from '@ionic/react';
import {
callOutline,
callSharp,
logoGithub,
logoInstagram,
logoTwitter,
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,71 @@
import {
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonMenuButton,
IonPage,
IonRow,
IonTitle,
IonToolbar,
} from '@ionic/react';
import React from 'react';
import SpeakerItem from '../components/SpeakerItem';
import { connect } from '../data/connect';
import * as selectors from '../data/selectors';
import { Session } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
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,107 @@
import {
IonButton,
IonButtons,
IonCol,
IonContent,
IonHeader,
IonItem,
IonList,
IonMenuButton,
IonPage,
IonRow,
IonText,
IonTextarea,
IonTitle,
IonToast,
IonToolbar,
} from '@ionic/react';
import React, { useState } from 'react';
import { connect } from '../data/connect';
import './Login.scss';
interface OwnProps {}
interface DispatchProps {}
interface SupportProps extends OwnProps, DispatchProps {}
const Support: React.FC<SupportProps> = () => {
const [message, setMessage] = useState('');
const [formSubmitted, setFormSubmitted] = useState(false);
const [messageError, setMessageError] = useState(false);
const [showToast, setShowToast] = useState(false);
const send = (e: React.FormEvent) => {
e.preventDefault();
setFormSubmitted(true);
if (!message) {
setMessageError(true);
}
if (message) {
setMessage('');
setShowToast(true);
}
};
return (
<IonPage id="support-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton></IonMenuButton>
</IonButtons>
<IonTitle>Support</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="login-logo">
<img src="assets/img/appicon.svg" alt="Ionic logo" />
</div>
<form noValidate onSubmit={send}>
<IonList>
<IonItem>
<IonTextarea
label="Enter your support message below"
labelPlacement="stacked"
color="primary"
name="message"
value={message}
spellCheck={false}
autocapitalize="off"
rows={6}
onIonInput={e => setMessage(e.detail.value!)}
required
>
{formSubmitted && messageError && (
<IonText color="danger" slot="error">
<p>Support message is required</p>
</IonText>
)}
</IonTextarea>
</IonItem>
</IonList>
<IonRow>
<IonCol>
<IonButton type="submit" expand="block">
Submit
</IonButton>
</IonCol>
</IonRow>
</form>
</IonContent>
<IonToast
isOpen={showToast}
duration={3000}
message="Your support request has been sent"
onDidDismiss={() => setShowToast(false)}
/>
</IonPage>
);
};
export default connect<OwnProps, {}, DispatchProps>({
component: Support,
});

View File

@@ -0,0 +1,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,105 @@
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonPage,
IonToolbar,
useIonViewWillEnter,
} from '@ionic/react';
import { arrowForward } from 'ionicons/icons';
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { connect } from '../data/connect';
import { setMenuEnabled } from '../data/sessions/sessions.actions';
import { setHasSeenTutorial } from '../data/user/user.actions';
import './Tutorial.scss';
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 }) => {
useIonViewWillEnter(() => {
setMenuEnabled(false);
});
const startApp = async () => {
await setHasSeenTutorial(true);
await setMenuEnabled(true);
history.push('/tabs/schedule', { direction: 'none' });
};
return (
<IonPage id="tutorial-page">
<IonHeader no-border>
<IonToolbar>
<IonButtons slot="end">
<IonButton color="primary" onClick={startApp}>
Skip
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<div className="slider">
<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,
});

View File

@@ -0,0 +1,30 @@
// REQ0114/Unlock-membership
import { IonButton } from '@ionic/react';
import React from 'react';
interface UnlockMemberShipProps {
setIsLoggedIn: Function;
setUsername: Function;
}
const UnlockMemberShip: React.FC<UnlockMemberShipProps> = () => {
return (
<>
<div>{'Unlock membership place holder'}</div>
<div>
<IonButton
fill="white"
shape="round"
onClick={() => {
window.history.back();
}}
>
{'Back'}
</IonButton>
</div>
</>
);
};
export default UnlockMemberShip;

View File

@@ -0,0 +1,173 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonChip,
IonContent,
IonFooter,
IonIcon,
IonItem,
IonLabel,
IonList,
IonPopover,
IonText,
IonTitle,
} from '@ionic/react';
import React, { useEffect } from 'react';
import HKPartyIonHeader from '../../../components/HKPartyIonHeader';
import HKPartyIonToolbar from '../../../components/HKPartyIonToolbar';
import { useGetUserProfileById } from '../../../hooks/useGetUserProfileById';
import { chevronBackOutline, ellipsisHorizontal, ellipsisVertical, helpCircleOutline } from 'ionicons/icons';
import Loading from '../../../components/Loading';
import constants from '../../../constants';
import './style.scss';
function ParticipantDetail({ user_id }) {
const [user_profile] = useGetUserProfileById({ user_id });
const [page_content, setPageContent] = React.useState(null);
useEffect(() => {
setPageContent({
about_user: user_profile?.about_user || 'No About',
gender: user_profile?.gender || -1,
height_cm: user_profile?.height_cm || -1,
weight_kg: user_profile?.weight_kg || -1,
avatar_url: user_profile?.avatar_urls?.length > 0 ? user_profile?.avatar_urls[0] : constants.IONIC_DEFAULT_AVATAR,
other_tags: user_profile?.other_tags || [],
career: user_profile?.career || [],
spoken_language: user_profile?.spoken_language || [],
education: user_profile?.education || [],
});
}, [user_profile]);
if (!page_content) return <Loading />;
return (
<>
<IonPopover trigger="click-trigger" triggerAction="click">
<IonContent class="ion-padding">
<IonList>
<IonItem lines="none" button onClick={() => close('https://ionicframework.com/docs')}>
<IonLabel>{'Block'}</IonLabel>
</IonItem>
<IonItem lines="none" button onClick={() => close('https://ionicframework.com/docs')}>
<IonLabel>{'Report and Block'}</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPopover>
<HKPartyIonHeader className="ion-no-border">
<HKPartyIonToolbar>
<IonButtons slot="start">
<IonBackButton icon={chevronBackOutline}></IonBackButton>
</IonButtons>
<IonTitle>{'Profiles'}</IonTitle>
<IonButtons>
<IonButton id="click-trigger">
<IonIcon slot="icon-only" ios={ellipsisHorizontal} md={ellipsisVertical}></IonIcon>
</IonButton>
</IonButtons>
</HKPartyIonToolbar>
</HKPartyIonHeader>
<IonContent>
<div>
<div
style={{
backgroundImage: `url(${page_content.avatar_url})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '100vw',
height: 'calc( 100vw * 3 / 4 )',
}}
></div>
<div>
<div style={{ margin: '0.5rem', padding: '0.5rem', color: 'black' }}>
<div style={{ fontSize: '1.5rem' }}>
<span>{page_content.about_user}</span>
</div>
<div className="detail_table">
<div className="detail_row">
<div className="detail_cell">
<IonIcon icon={helpCircleOutline} />
<IonText>{page_content.gender == 1 ? 'Male' : 'Female'}</IonText>
</div>
<div className="detail_cell">
<IonIcon icon={helpCircleOutline} />
<IonText>{page_content.height_cm} cm</IonText>
</div>
<div className="detail_cell right_most_cell">
<IonIcon icon={helpCircleOutline} />
<IonText>{page_content.weight_kg} kg</IonText>
</div>
</div>
<div className="detail_row">
<div className="detail_cell full_row right_most_cell">
<IonIcon icon={helpCircleOutline} className="no-avatar" />
<IonText>Doctor</IonText>
</div>
</div>
<div className="detail_row">
<div className="detail_cell full_row right_most_cell">
<IonIcon icon={helpCircleOutline} className="no-avatar" />
<IonText>{page_content.spoken_language.map((t, i) => (i > 0 ? ', ' + t : t))}</IonText>
</div>
</div>
<div className="detail_row last_detail_row">
<div className="detail_cell full_row right_most_cell">
<IonIcon icon={helpCircleOutline} className="no-avatar" />
<IonText>{page_content.education}</IonText>
</div>
</div>
</div>
<div>
<h3>{'About me'}</h3>
<IonText>{page_content.about_user}</IonText>
</div>
<div>
<h3>{'Career'}</h3>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{page_content.career.map((t, i) => (
<IonChip key={i}>{t}</IonChip>
))}
</div>
</div>
<div>
<h3>{'Spoken Language'}</h3>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{page_content.spoken_language.map((t, i) => (
<IonChip key={i}>{t}</IonChip>
))}
</div>
</div>
<div>
<h3>{'Other Tags'}</h3>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{page_content.other_tags.map((t, i) => (
<IonChip key={i}>{t}</IonChip>
))}
</div>
</div>
</div>
</div>
</div>
</IonContent>
<IonFooter className="ion-no-border">
<IonButton style={{ margin: '1rem 1rem' }} expand="block" shape="round">
<IonText style={{ padding: '1rem' }}>{'chat'}</IonText>
</IonButton>
</IonFooter>
</>
);
}
export default React.memo(ParticipantDetail);

View File

@@ -0,0 +1,49 @@
ion-chip {
--background: #00213f;
--color: #adefd1;
}
div.detail_table {
margin: 0.25rem;
border-radius: 0.5rem;
border: 1px solid gray;
.detail_row {
border-bottom: 1px solid gray;
display: flex;
}
.last_detail_row {
border-bottom: unset;
}
.detail_cell {
border-right: 1px solid gray;
width: 33%;
padding: 10px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
ion-icon {
margin-right: 0.5rem;
}
}
.right_most_cell {
border-right: unset;
}
.full_row {
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
ion-icon {
margin-right: 0.5rem;
}
}
}

View File

@@ -0,0 +1,91 @@
// REQ0080/party_participants
import { IonBackButton, IonButton, IonButtons, IonContent, IonNavLink, IonTitle } from '@ionic/react';
import { chevronBackOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import HKPartyIonHeader from '../../components/HKPartyIonHeader';
import HKPartyIonToolbar from '../../components/HKPartyIonToolbar';
import Loading from '../../components/Loading';
import constants from '../../constants';
import ParticipantDetail from './ParticipantDetail';
import './style.scss';
function ParticipantsPhoto({ participant }) {
const [loading, setLoading] = useState(true);
const [content, setContent] = useState({
avatar_url: participant?.acatar_urls?.length > 0 ? participant?.acatar_urls[0] : constants.IONIC_DEFAULT_AVATAR,
user_id: participant.user_id || 0,
});
useEffect(() => {
if (participant.avatar_urls?.length > 0) {
setContent({ ...content, avatar_url: participant.avatar_urls[0] });
}
console.log({ participant });
setLoading(false);
}, []);
if (loading) return <Loading />;
return (
<IonNavLink routerDirection="forward" component={() => <ParticipantDetail user_id={content.user_id} />}>
<div
style={{
width: 'calc( 30vw - 10px )',
height: 'calc( 30vw - 10px )',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '10px',
margin: '5px',
//
backgroundImage: `url(${content.avatar_url})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
></div>
</IonNavLink>
);
}
function ViewParticipants({ participants }) {
return (
<>
<HKPartyIonHeader className="ion-no-border">
<HKPartyIonToolbar>
<IonButtons slot="start">
<IonBackButton icon={chevronBackOutline}></IonBackButton>
</IonButtons>
<IonTitle>{'View Participants'}</IonTitle>
{/*
<IonButtons>
<IonButton id="click-trigger">
<IonIcon slot="icon-only" ios={ellipsisHorizontal} md={ellipsisVertical}></IonIcon>
</IonButton>
</IonButtons>
*/}
</HKPartyIonToolbar>
</HKPartyIonHeader>
<IonContent className="ion-padding">
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{participants.map((u, i) => (
<ParticipantsPhoto key={i} participant={u} />
))}
</div>
</IonContent>
<div style={{ margin: '10px 30px' }}>
<IonButton shape="round" size="large" expand="block">
{'Unlock'}
</IonButton>
</div>
</>
);
}
export default ViewParticipants;

View File

@@ -0,0 +1,3 @@
.ion-padding {
background-color: gold;
}

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