init commit,
This commit is contained in:
6
03_source/mobile_notworking.del/src/App.scss
Normal file
6
03_source/mobile_notworking.del/src/App.scss
Normal 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.
|
||||
*/
|
7
03_source/mobile_notworking.del/src/App.test.tsx
Normal file
7
03_source/mobile_notworking.del/src/App.test.tsx
Normal 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();
|
||||
});
|
273
03_source/mobile_notworking.del/src/App.tsx
Normal file
273
03_source/mobile_notworking.del/src/App.tsx
Normal 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,
|
||||
});
|
@@ -0,0 +1,5 @@
|
||||
const SBLoginError = () => {
|
||||
return <>Login Error</>;
|
||||
};
|
||||
|
||||
export default SBLoginError;
|
@@ -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>
|
||||
`;
|
@@ -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;
|
27
03_source/mobile_notworking.del/src/components/Avatar.css
Normal file
27
03_source/mobile_notworking.del/src/components/Avatar.css
Normal 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%;
|
||||
}
|
56
03_source/mobile_notworking.del/src/components/Avatar.tsx
Normal file
56
03_source/mobile_notworking.del/src/components/Avatar.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
@@ -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;
|
@@ -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;
|
||||
// }
|
||||
// }
|
||||
}
|
@@ -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 '';
|
||||
}
|
||||
};
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -0,0 +1,13 @@
|
||||
import { IonPage } from '@ionic/react';
|
||||
|
||||
const HKPartyIonPage = ({ children, ...props }) => {
|
||||
return (
|
||||
<IonPage {...props}>
|
||||
{/* */}
|
||||
{children}
|
||||
{/* */}
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default HKPartyIonPage;
|
@@ -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;
|
@@ -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,
|
||||
});
|
@@ -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;
|
52
03_source/mobile_notworking.del/src/components/Map.tsx
Normal file
52
03_source/mobile_notworking.del/src/components/Map.tsx
Normal 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;
|
88
03_source/mobile_notworking.del/src/components/Menu.css
Normal file
88
03_source/mobile_notworking.del/src/components/Menu.css
Normal 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);
|
||||
}
|
138
03_source/mobile_notworking.del/src/components/Menu.tsx
Normal file
138
03_source/mobile_notworking.del/src/components/Menu.tsx
Normal 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),
|
||||
});
|
@@ -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;
|
@@ -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;
|
290
03_source/mobile_notworking.del/src/components/ReplyTo/style.css
Normal file
290
03_source/mobile_notworking.del/src/components/ReplyTo/style.css
Normal 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%;
|
||||
}
|
102
03_source/mobile_notworking.del/src/components/SessionList.tsx
Normal file
102
03_source/mobile_notworking.del/src/components/SessionList.tsx
Normal 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,
|
||||
});
|
@@ -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);
|
||||
}
|
@@ -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,
|
||||
});
|
@@ -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}—
|
||||
{session.timeStart}—
|
||||
{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);
|
@@ -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;
|
@@ -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;
|
3
03_source/mobile_notworking.del/src/constants.ts
Normal file
3
03_source/mobile_notworking.del/src/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const IONIC_DEFAULT_AVATAR = 'https://ionicframework.com/docs/img/demos/avatar.svg';
|
||||
|
||||
export default { IONIC_DEFAULT_AVATAR };
|
@@ -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;
|
||||
};
|
79
03_source/mobile_notworking.del/src/data/AppContext.tsx
Normal file
79
03_source/mobile_notworking.del/src/data/AppContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
18
03_source/mobile_notworking.del/src/data/combineReducers.ts
Normal file
18
03_source/mobile_notworking.del/src/data/combineReducers.ts
Normal 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;
|
||||
}
|
55
03_source/mobile_notworking.del/src/data/connect.tsx
Normal file
55
03_source/mobile_notworking.del/src/data/connect.tsx
Normal 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);
|
||||
}
|
78
03_source/mobile_notworking.del/src/data/dataApi.ts
Normal file
78
03_source/mobile_notworking.del/src/data/dataApi.ts
Normal 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;
|
||||
}
|
122
03_source/mobile_notworking.del/src/data/selectors.ts
Normal file
122
03_source/mobile_notworking.del/src/data/selectors.ts
Normal 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;
|
||||
};
|
@@ -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;
|
||||
}
|
@@ -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>;
|
@@ -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 };
|
||||
}
|
||||
}
|
||||
};
|
31
03_source/mobile_notworking.del/src/data/state.ts
Normal file
31
03_source/mobile_notworking.del/src/data/state.ts
Normal 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>;
|
@@ -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>;
|
@@ -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 };
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
export interface UserState {
|
||||
isLoggedin: boolean;
|
||||
username?: string;
|
||||
darkMode: boolean;
|
||||
hasSeenTutorial: boolean;
|
||||
loading: boolean;
|
||||
}
|
5
03_source/mobile_notworking.del/src/declarations.ts
Normal file
5
03_source/mobile_notworking.del/src/declarations.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface AppPage {
|
||||
url: string;
|
||||
icon: object;
|
||||
title: string;
|
||||
}
|
18
03_source/mobile_notworking.del/src/hooks/useCamera.js
Normal file
18
03_source/mobile_notworking.del/src/hooks/useCamera.js
Normal 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,
|
||||
};
|
||||
};
|
@@ -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;
|
18
03_source/mobile_notworking.del/src/hooks/useGallery.js
Normal file
18
03_source/mobile_notworking.del/src/hooks/useGallery.js
Normal 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,
|
||||
};
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
25
03_source/mobile_notworking.del/src/hooks/useJoinTest.ts
Normal file
25
03_source/mobile_notworking.del/src/hooks/useJoinTest.ts
Normal 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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
@@ -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];
|
||||
};
|
21
03_source/mobile_notworking.del/src/i18n.ts
Normal file
21
03_source/mobile_notworking.del/src/i18n.ts
Normal 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 };
|
1634
03_source/mobile_notworking.del/src/locales/en/en.json
Normal file
1634
03_source/mobile_notworking.del/src/locales/en/en.json
Normal file
File diff suppressed because it is too large
Load Diff
1634
03_source/mobile_notworking.del/src/locales/hk/hk.json
Normal file
1634
03_source/mobile_notworking.del/src/locales/hk/hk.json
Normal file
File diff suppressed because it is too large
Load Diff
15
03_source/mobile_notworking.del/src/main.tsx
Normal file
15
03_source/mobile_notworking.del/src/main.tsx
Normal 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();
|
6
03_source/mobile_notworking.del/src/models/Location.ts
Normal file
6
03_source/mobile_notworking.del/src/models/Location.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Location {
|
||||
id: number;
|
||||
name?: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
20
03_source/mobile_notworking.del/src/models/Schedule.ts
Normal file
20
03_source/mobile_notworking.del/src/models/Schedule.ts
Normal 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[];
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { Session } from './Schedule';
|
||||
export interface SessionGroup {
|
||||
startTime: string;
|
||||
sessions: Session[];
|
||||
}
|
12
03_source/mobile_notworking.del/src/models/Speaker.ts
Normal file
12
03_source/mobile_notworking.del/src/models/Speaker.ts
Normal 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;
|
||||
}
|
138
03_source/mobile_notworking.del/src/pages/About/index.tsx
Normal file
138
03_source/mobile_notworking.del/src/pages/About/index.tsx
Normal 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 we’re 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);
|
103
03_source/mobile_notworking.del/src/pages/About/style.scss
Normal file
103
03_source/mobile_notworking.del/src/pages/About/style.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
#about-page {
|
||||
ion-toolbar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
--background: transparent;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
ion-toolbar ion-back-button,
|
||||
ion-toolbar ion-button,
|
||||
ion-toolbar ion-menu-button {
|
||||
--color: white;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.about-header .about-image {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.about-header .madison {
|
||||
background-image: url('/assets/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/img/about/seattle.jpg');
|
||||
}
|
||||
|
||||
.about-info {
|
||||
position: relative;
|
||||
margin-top: -10px;
|
||||
border-radius: 10px;
|
||||
background: var(--ion-background-color, #fff);
|
||||
z-index: 2; // display rounded border above header image
|
||||
}
|
||||
|
||||
.about-info h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.about-info ion-list {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.about-info p {
|
||||
line-height: 130%;
|
||||
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
.about-info ion-icon {
|
||||
margin-inline-end: 32px;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Only
|
||||
*/
|
||||
|
||||
.ios .about-info {
|
||||
--ion-padding: 19px;
|
||||
}
|
||||
|
||||
.ios .about-info h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
#date-input-popover {
|
||||
--offset-y: -var(--ion-safe-area-bottom);
|
||||
|
||||
--max-width: 90%;
|
||||
--width: 336px;
|
||||
}
|
6
03_source/mobile_notworking.del/src/pages/Account.scss
Normal file
6
03_source/mobile_notworking.del/src/pages/Account.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
#account-page {
|
||||
img {
|
||||
max-width: 140px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
101
03_source/mobile_notworking.del/src/pages/Account.tsx
Normal file
101
03_source/mobile_notworking.del/src/pages/Account.tsx
Normal 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,
|
||||
});
|
17
03_source/mobile_notworking.del/src/pages/Login.scss
Normal file
17
03_source/mobile_notworking.del/src/pages/Login.scss
Normal 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;
|
||||
}
|
||||
}
|
137
03_source/mobile_notworking.del/src/pages/Login.tsx
Normal file
137
03_source/mobile_notworking.del/src/pages/Login.tsx
Normal 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,
|
||||
});
|
94
03_source/mobile_notworking.del/src/pages/MainTabs/index.tsx
Normal file
94
03_source/mobile_notworking.del/src/pages/MainTabs/index.tsx
Normal 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;
|
17
03_source/mobile_notworking.del/src/pages/MapView.scss
Normal file
17
03_source/mobile_notworking.del/src/pages/MapView.scss
Normal 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;
|
||||
}
|
||||
}
|
45
03_source/mobile_notworking.del/src/pages/MapView.tsx
Normal file
45
03_source/mobile_notworking.del/src/pages/MapView.tsx
Normal 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,
|
||||
});
|
161
03_source/mobile_notworking.del/src/pages/SBAccount/index.tsx
Normal file
161
03_source/mobile_notworking.del/src/pages/SBAccount/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
93
03_source/mobile_notworking.del/src/pages/SBLogin/index.tsx
Normal file
93
03_source/mobile_notworking.del/src/pages/SBLogin/index.tsx
Normal 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);
|
18
03_source/mobile_notworking.del/src/pages/SBLogout/index.tsx
Normal file
18
03_source/mobile_notworking.del/src/pages/SBLogout/index.tsx
Normal 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;
|
58
03_source/mobile_notworking.del/src/pages/SchedulePage.scss
Normal file
58
03_source/mobile_notworking.del/src/pages/SchedulePage.scss
Normal file
@@ -0,0 +1,58 @@
|
||||
#schedule-page {
|
||||
ion-fab-button {
|
||||
--background: var(--ion-color-step-150, #fff);
|
||||
--background-hover: var(--ion-color-step-200, #f2f2f2);
|
||||
--background-focused: var(--ion-color-step-250, #d9d9d9);
|
||||
|
||||
--color: var(--ion-color-primary, #3880ff);
|
||||
}
|
||||
|
||||
/*
|
||||
* Material Design uses the ripple for activated
|
||||
* so only style the iOS activated background
|
||||
*/
|
||||
.ios ion-fab-button {
|
||||
--background-activated: var(--ion-color-step-250, #d9d9d9);
|
||||
}
|
||||
|
||||
ion-item-sliding.track-ionic ion-label {
|
||||
border-left: 2px solid var(--ion-color-primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
ion-item-sliding.track-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;
|
||||
}
|
||||
}
|
167
03_source/mobile_notworking.del/src/pages/SchedulePage.tsx
Normal file
167
03_source/mobile_notworking.del/src/pages/SchedulePage.tsx
Normal 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),
|
||||
});
|
73
03_source/mobile_notworking.del/src/pages/SessionDetail.scss
Normal file
73
03_source/mobile_notworking.del/src/pages/SessionDetail.scss
Normal 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;
|
||||
}
|
||||
}
|
122
03_source/mobile_notworking.del/src/pages/SessionDetail.tsx
Normal file
122
03_source/mobile_notworking.del/src/pages/SessionDetail.tsx
Normal 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} – {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),
|
||||
});
|
138
03_source/mobile_notworking.del/src/pages/Signup.tsx
Normal file
138
03_source/mobile_notworking.del/src/pages/Signup.tsx
Normal 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,
|
||||
});
|
77
03_source/mobile_notworking.del/src/pages/SpeakerDetail.scss
Normal file
77
03_source/mobile_notworking.del/src/pages/SpeakerDetail.scss
Normal 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);
|
||||
}
|
||||
}
|
163
03_source/mobile_notworking.del/src/pages/SpeakerDetail.tsx
Normal file
163
03_source/mobile_notworking.del/src/pages/SpeakerDetail.tsx
Normal 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,
|
||||
});
|
48
03_source/mobile_notworking.del/src/pages/SpeakerList.scss
Normal file
48
03_source/mobile_notworking.del/src/pages/SpeakerList.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
#speaker-list {
|
||||
.speaker-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Due to the fact the cards are inside of columns the margins don't overlap
|
||||
* properly so we want to remove the extra margin between cards
|
||||
*/
|
||||
ion-col:not(:last-of-type) .speaker-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.speaker-card .speaker-item {
|
||||
--min-height: 85px;
|
||||
}
|
||||
|
||||
.speaker-card .speaker-item h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.speaker-card .speaker-item p {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.speaker-card ion-card-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.speaker-card ion-card-content {
|
||||
flex: 1 1 auto;
|
||||
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ios ion-list {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.md ion-list {
|
||||
border-top: 1px solid var(--ion-color-step-150, #d7d8da);
|
||||
|
||||
padding: 0;
|
||||
}
|
||||
}
|
71
03_source/mobile_notworking.del/src/pages/SpeakerList.tsx
Normal file
71
03_source/mobile_notworking.del/src/pages/SpeakerList.tsx
Normal 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),
|
||||
});
|
107
03_source/mobile_notworking.del/src/pages/Support.tsx
Normal file
107
03_source/mobile_notworking.del/src/pages/Support.tsx
Normal 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,
|
||||
});
|
56
03_source/mobile_notworking.del/src/pages/Tutorial.scss
Normal file
56
03_source/mobile_notworking.del/src/pages/Tutorial.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
#tutorial-page {
|
||||
ion-toolbar {
|
||||
--background: transparent;
|
||||
--border-color: transparent;
|
||||
}
|
||||
|
||||
.slide-title {
|
||||
margin-top: 2.8rem;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 100%);
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
height: 100%;
|
||||
|
||||
overflow: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
.slide-image {
|
||||
max-height: 50%;
|
||||
max-width: 60%;
|
||||
margin: -5vh 0 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0 40px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--ion-color-step-600, #60646b);
|
||||
|
||||
b {
|
||||
color: var(--ion-text-color, #000000);
|
||||
}
|
||||
}
|
||||
}
|
105
03_source/mobile_notworking.del/src/pages/Tutorial.tsx
Normal file
105
03_source/mobile_notworking.del/src/pages/Tutorial.tsx
Normal 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,
|
||||
});
|
@@ -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;
|
@@ -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);
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
@@ -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
Reference in New Issue
Block a user