"chore: update frontend dev script to include lint checks and add ESLint config file"
This commit is contained in:
27
03_source/mobile/src/pages/Helloworld/index.tsx
Normal file
27
03_source/mobile/src/pages/Helloworld/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// REQ0041/home_discover_event_tab
|
||||
|
||||
import { IonPage, IonHeader, IonToolbar, IonButtons, IonButton, IonIcon, IonTitle, IonContent } from '@ionic/react';
|
||||
import { menuOutline } from 'ionicons/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const Helloworld: React.FC = () => {
|
||||
return (
|
||||
<IonPage id="speaker-list">
|
||||
<IonHeader translucent={true} className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButtons slot="end">
|
||||
{/* <IonMenuButton /> */}
|
||||
<IonButton shape="round" id="events-open-modal" expand="block">
|
||||
<IonIcon slot="icon-only" icon={menuOutline}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Discover Events</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen={true}>Helloworld</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Helloworld;
|
@@ -26,7 +26,7 @@ const MainTabs: React.FC<MainTabsProps> = () => {
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
{/* REQ0117/default-route */}
|
||||
<Redirect exact path="/tabs" to="/tabs/schedule" />
|
||||
<Redirect exact path="/tabs" to="/tabs/events" />
|
||||
{/*
|
||||
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.
|
||||
@@ -68,7 +68,7 @@ const MainTabs: React.FC<MainTabsProps> = () => {
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Message</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="my_profile" href="/tabs/my_profile">
|
||||
<IonTabButton tab="my_profile" href={paths.PROFILE}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Profile</IonLabel>
|
||||
</IonTabButton>
|
||||
|
12
03_source/mobile/src/pages/MyLogin/endpoints.ts
Normal file
12
03_source/mobile/src/pages/MyLogin/endpoints.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
const CMS_BACKEND_URL = 'http://192.168.10.75:7272';
|
||||
|
||||
const endpoints = {
|
||||
auth: {
|
||||
me: `http://localhost:7272/api/auth/me`,
|
||||
signIn: `${CMS_BACKEND_URL}/api/auth/sign-in`,
|
||||
signUp: `${CMS_BACKEND_URL}/api/auth/sign-up`,
|
||||
//
|
||||
},
|
||||
};
|
||||
|
||||
export { endpoints };
|
269
03_source/mobile/src/pages/MyLogin/index.tsx
Normal file
269
03_source/mobile/src/pages/MyLogin/index.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { z as zod } from 'zod';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import {
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButtons,
|
||||
IonMenuButton,
|
||||
IonRow,
|
||||
IonCol,
|
||||
IonButton,
|
||||
IonInput,
|
||||
IonIcon,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { useHistory } from 'react-router';
|
||||
import './style.scss';
|
||||
|
||||
import {
|
||||
setAccessToken,
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
checkUserSession,
|
||||
} from '../../data/user/user.actions';
|
||||
import { connect } from '../../data/connect';
|
||||
|
||||
import { chevronBack, chevronBackOutline } from 'ionicons/icons';
|
||||
|
||||
import { signInWithPassword } from '../../context/jwt/action';
|
||||
import axios from 'axios';
|
||||
import { endpoints } from './endpoints';
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface DispatchProps {
|
||||
setIsLoggedIn: typeof setIsLoggedIn;
|
||||
setUsername: typeof setUsername;
|
||||
setAccessToken: typeof setAccessToken;
|
||||
checkUserSession: typeof checkUserSession;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
isSessionValid: boolean;
|
||||
}
|
||||
|
||||
interface LoginProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
type UserType = Record<string, any> | null;
|
||||
|
||||
const Login: React.FC<LoginProps> = (props) => {
|
||||
const {
|
||||
setAccessToken,
|
||||
setIsLoggedIn,
|
||||
setUsername: setUsernameAction,
|
||||
checkUserSession,
|
||||
isSessionValid,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// TODO: delete unused code
|
||||
// const [login, setLogin] = useState({ email: '', password: '' });
|
||||
// const [submitted, setSubmitted] = useState(false);
|
||||
// const onLogin = async (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
// setSubmitted(true);
|
||||
|
||||
// if (login.email && login.password) {
|
||||
// await setIsLoggedIn(true);
|
||||
// await login.email;
|
||||
// history.push('/tabs/events');
|
||||
// }
|
||||
// };
|
||||
|
||||
const router = useIonRouter();
|
||||
function handleBackButtonClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
const onSignup = () => {
|
||||
history.push('/signup');
|
||||
};
|
||||
|
||||
// ----------
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isDirty, dirtyFields, errors, isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const values = watch();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
console.log({ isSessionValid });
|
||||
if (isSessionValid) {
|
||||
await setIsLoggedIn(true);
|
||||
router.push('/tabs');
|
||||
// reset();
|
||||
reset();
|
||||
}
|
||||
})();
|
||||
}, [isSessionValid]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
console.log({ data });
|
||||
try {
|
||||
let token = await signInWithPassword({ email: values.email, password: values.password });
|
||||
console.log({ token });
|
||||
if (token) setAccessToken(token);
|
||||
|
||||
await checkUserSession();
|
||||
|
||||
// NOTE: page forward handled by changing of state `isSessionValid`
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// const feedbackMessage = getErrorMessage(error);
|
||||
// setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
onChange: emailOnChange,
|
||||
onBlur: emailOnBlur,
|
||||
name: emailName,
|
||||
ref: emailRef,
|
||||
} = register('email');
|
||||
const {
|
||||
onChange: passwordOnChange,
|
||||
onBlur: passwordOnBlur,
|
||||
name: passwordName,
|
||||
ref: passwordRef,
|
||||
} = register('password');
|
||||
|
||||
return (
|
||||
<IonPage id="login-page">
|
||||
<IonHeader translucent={true} className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButton
|
||||
slot="start"
|
||||
fill="clear"
|
||||
shape="round"
|
||||
onClick={() => handleBackButtonClick()}
|
||||
>
|
||||
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
|
||||
</IonButton>
|
||||
<IonTitle>Login</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
{/* */}
|
||||
<IonContent>
|
||||
<div className="login-logo">
|
||||
<img src="/assets/img/appicon.svg" alt="Ionic logo" />
|
||||
</div>
|
||||
|
||||
<div className="login-form">
|
||||
<form onSubmit={onSubmit} noValidate autoComplete="off">
|
||||
{/* // ${!errors.email && 'ion-valid'} */}
|
||||
<IonInput
|
||||
className={`
|
||||
${errors.email && 'ion-invalid'}
|
||||
${dirtyFields.email && 'ion-touched'}
|
||||
`}
|
||||
label="Email"
|
||||
labelPlacement="floating"
|
||||
placeholder="Email"
|
||||
//
|
||||
fill="outline"
|
||||
// name="email"
|
||||
type="email"
|
||||
spellCheck={false}
|
||||
autocapitalize="off"
|
||||
//
|
||||
helperText="Enter a valid email"
|
||||
errorText={errors.email?.message}
|
||||
//
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
// value={values.email}
|
||||
// onIonInput={(e) => setValue('email', e.detail.value!)}
|
||||
{...register('email')}
|
||||
/>
|
||||
|
||||
{/* // ${!errors.password && 'ion-valid'} */}
|
||||
<IonInput
|
||||
className={`
|
||||
${errors.password && 'ion-invalid'}
|
||||
${dirtyFields.password && 'ion-touched'}
|
||||
`}
|
||||
// label="Password"
|
||||
// labelPlacement={values.password == '' ? 'start' : 'stacked'}
|
||||
// name="password"
|
||||
placeholder="Password"
|
||||
//
|
||||
fill="outline"
|
||||
type="password"
|
||||
errorText={errors.password?.message}
|
||||
value={values.password}
|
||||
onIonInput={(e) => setValue('password', e.detail.value!)}
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
{...register('password')}
|
||||
/>
|
||||
|
||||
<IonRow>
|
||||
<IonCol>
|
||||
<IonButton disabled={isSubmitting || !isDirty} type="submit" expand="block">
|
||||
{isSubmitting ? 'logging in' : 'Login'}
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
<IonCol>
|
||||
<IonButton onClick={onSignup} color="light" expand="block">
|
||||
Signup
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</form>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect<{}, StateProps, DispatchProps>({
|
||||
mapStateToProps: (state) => ({
|
||||
isSessionValid: state.user.isSessionValid,
|
||||
}),
|
||||
mapDispatchToProps: {
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
setAccessToken,
|
||||
checkUserSession,
|
||||
},
|
||||
component: Login,
|
||||
});
|
22
03_source/mobile/src/pages/MyLogin/isValidToken.tsx
Normal file
22
03_source/mobile/src/pages/MyLogin/isValidToken.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { jwtDecode } from './jwtDecode';
|
||||
|
||||
function isValidToken(accessToken: string) {
|
||||
if (!accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwtDecode(accessToken);
|
||||
|
||||
if (!decoded || !('exp' in decoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTime = Date.now() / 1000;
|
||||
|
||||
return decoded.exp > currentTime;
|
||||
} catch (error) {
|
||||
console.error('Error during token validation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
19
03_source/mobile/src/pages/MyLogin/jwtDecode.tsx
Normal file
19
03_source/mobile/src/pages/MyLogin/jwtDecode.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
export function jwtDecode(token: string) {
|
||||
try {
|
||||
if (!token) return null;
|
||||
|
||||
const parts = token.split('.');
|
||||
if (parts.length < 2) {
|
||||
throw new Error('Invalid token!');
|
||||
}
|
||||
|
||||
const base64Url = parts[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const decoded = JSON.parse(atob(base64));
|
||||
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
console.error('Error decoding token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
23
03_source/mobile/src/pages/MyLogin/style.scss
Normal file
23
03_source/mobile/src/pages/MyLogin/style.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
#login-page, #signup-page, #support-page {
|
||||
.login-logo {
|
||||
min-height: 200px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-logo img {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ion-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
@@ -44,7 +44,14 @@ import '../../SpeakerList.scss';
|
||||
import { getEvents } from '../../../api/getEvents';
|
||||
import { format } from 'date-fns';
|
||||
import { Event } from '../types';
|
||||
import { alertOutline, chevronDownCircleOutline, createOutline, heart, menuOutline, settingsOutline } from 'ionicons/icons';
|
||||
import {
|
||||
alertOutline,
|
||||
chevronDownCircleOutline,
|
||||
createOutline,
|
||||
heart,
|
||||
menuOutline,
|
||||
settingsOutline,
|
||||
} from 'ionicons/icons';
|
||||
import AboutPopover from '../../../components/AboutPopover';
|
||||
import paths from '../../../paths';
|
||||
import { getProfileById } from '../../../api/getProfileById';
|
||||
@@ -89,7 +96,7 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
|
||||
function handleForwardLoginPage() {
|
||||
try {
|
||||
setDisableForwardLoginButton(true);
|
||||
router.push(paths.login);
|
||||
router.push(paths.SIGN_IN);
|
||||
setDisableForwardLoginButton(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -121,7 +128,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
|
||||
|
||||
<IonContent fullscreen={true}>
|
||||
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
|
||||
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent>
|
||||
<IonRefresherContent
|
||||
pullingIcon={chevronDownCircleOutline}
|
||||
pullingText="Pull to refresh"
|
||||
refreshingSpinner="circles"
|
||||
refreshingText="Refreshing..."
|
||||
></IonRefresherContent>
|
||||
</IonRefresher>
|
||||
|
||||
<IonHeader collapse="condense" className="ion-no-border">
|
||||
@@ -130,12 +142,28 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<div style={{ height: '50vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
height: '50vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
not login yet, <br />
|
||||
please login or sign up
|
||||
</div>
|
||||
<div style={{ height: '50vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
height: '50vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IonButton disabled={disableForwardLoginButton} onClick={handleForwardLoginPage}>
|
||||
Login
|
||||
</IonButton>
|
||||
|
@@ -44,9 +44,7 @@ interface SpeakerDetailProps extends OwnProps, StateProps, DispatchProps {}
|
||||
|
||||
const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
|
||||
const [showActionSheet, setShowActionSheet] = useState(false);
|
||||
const [actionSheetButtons, setActionSheetButtons] = useState<
|
||||
ActionSheetButton[]
|
||||
>([]);
|
||||
const [actionSheetButtons, setActionSheetButtons] = useState<ActionSheetButton[]>([]);
|
||||
const [actionSheetHeader, setActionSheetHeader] = useState('');
|
||||
|
||||
function openSpeakerShare(speaker: Speaker) {
|
||||
@@ -112,18 +110,10 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
|
||||
</IonButtons>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={() => openContact(speaker)}>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
ios={callOutline}
|
||||
md={callSharp}
|
||||
></IonIcon>
|
||||
<IonIcon slot="icon-only" ios={callOutline} md={callSharp}></IonIcon>
|
||||
</IonButton>
|
||||
<IonButton onClick={() => openSpeakerShare(speaker)}>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
ios={shareOutline}
|
||||
md={shareSharp}
|
||||
></IonIcon>
|
||||
<IonIcon slot="icon-only" ios={shareOutline} md={shareSharp}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
@@ -141,9 +131,7 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
|
||||
|
||||
<IonChip
|
||||
color="twitter"
|
||||
onClick={() =>
|
||||
openExternalUrl(`https://twitter.com/${speaker.twitter}`)
|
||||
}
|
||||
onClick={() => openExternalUrl(`https://twitter.com/${speaker.twitter}`)}
|
||||
>
|
||||
<IonIcon icon={logoTwitter}></IonIcon>
|
||||
<IonLabel>Twitter</IonLabel>
|
||||
@@ -151,9 +139,7 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
|
||||
|
||||
<IonChip
|
||||
color="dark"
|
||||
onClick={() =>
|
||||
openExternalUrl('https://github.com/ionic-team/ionic-framework')
|
||||
}
|
||||
onClick={() => openExternalUrl('https://github.com/ionic-team/ionic-framework')}
|
||||
>
|
||||
<IonIcon icon={logoGithub}></IonIcon>
|
||||
<IonLabel>GitHub</IonLabel>
|
||||
@@ -161,9 +147,7 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
|
||||
|
||||
<IonChip
|
||||
color="instagram"
|
||||
onClick={() =>
|
||||
openExternalUrl('https://instagram.com/ionicframework')
|
||||
}
|
||||
onClick={() => openExternalUrl('https://instagram.com/ionicframework')}
|
||||
>
|
||||
<IonIcon icon={logoInstagram}></IonIcon>
|
||||
<IonLabel>Instagram</IonLabel>
|
||||
|
Reference in New Issue
Block a user