"chore: update frontend dev script to include lint checks and add ESLint config file"
This commit is contained in:
@@ -47,6 +47,7 @@ import { loadConfData } from './data/sessions/sessions.actions';
|
||||
import { setIsLoggedIn, setUsername, loadUserData } from './data/user/user.actions';
|
||||
import Account from './pages/Account';
|
||||
import Login from './pages/Login';
|
||||
import MyLogin from './pages/MyLogin';
|
||||
import Signup from './pages/Signup';
|
||||
import Support from './pages/Support';
|
||||
import Tutorial from './pages/Tutorial';
|
||||
@@ -88,10 +89,18 @@ interface DispatchProps {
|
||||
|
||||
interface IonicAppProps extends StateProps, DispatchProps {}
|
||||
|
||||
const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => {
|
||||
const IonicApp: React.FC<IonicAppProps> = ({
|
||||
darkMode,
|
||||
schedule,
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
loadConfData,
|
||||
loadUserData,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
loadUserData();
|
||||
loadConfData();
|
||||
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
@@ -112,18 +121,15 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
|
||||
|
||||
{/* */}
|
||||
<Route path="/tabs" render={() => <MainTabs />} />
|
||||
|
||||
{/* */}
|
||||
<Route path="/account" component={Account} />
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/mylogin" component={MyLogin} />
|
||||
|
||||
<Route path="/signup" component={Signup} />
|
||||
<Route path="/support" component={Support} />
|
||||
<Route path="/tutorial" component={Tutorial} />
|
||||
{/* */}
|
||||
|
||||
{/* */}
|
||||
<Route path={paths.SETTINGS} component={Settings} />
|
||||
<Route path={paths.CHANGE_LANGUAGE} component={ChangeLanguage} />
|
||||
<Route path={paths.SERVICE_AGREEMENT} component={ServiceAgreement} />
|
||||
<Route path={paths.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
|
||||
|
||||
{/* */}
|
||||
<Route
|
||||
|
@@ -1,7 +1,20 @@
|
||||
//
|
||||
// pages without bottom tab bar
|
||||
//
|
||||
|
||||
import { Route } from 'react-router';
|
||||
import NotImplemented from './pages/NotImplemented';
|
||||
import EventDetail from './pages/EventDetail';
|
||||
import MemberProfile from './pages/MemberProfile';
|
||||
import paths from './paths';
|
||||
import Helloworld from './pages/Helloworld';
|
||||
import Settings from './pages/Settings';
|
||||
import ChangeLanguage from './pages/ChangeLanguage';
|
||||
import ServiceAgreement from './pages/ServiceAgreement';
|
||||
import PrivacyAgreement from './pages/PrivacyAgreement';
|
||||
// import OrderDetails from './pages/OrderDetail';
|
||||
import OrderDetail from './pages/OrderDetail';
|
||||
import SpeakerDetail from './pages/SpeakerDetail';
|
||||
|
||||
const AppRoute: React.FC = () => {
|
||||
return (
|
||||
@@ -9,8 +22,18 @@ const AppRoute: React.FC = () => {
|
||||
<Route path="/not_implemented" component={NotImplemented} />
|
||||
|
||||
{/* */}
|
||||
<Route path="/event_detail/:id" component={EventDetail} />
|
||||
<Route path="/profile/:id" component={MemberProfile} />
|
||||
<Route exact={true} path="/event_detail/:id" component={EventDetail} />
|
||||
<Route exact={true} path="/profile/:id" component={MemberProfile} />
|
||||
|
||||
{/* component make the ":id" available in the "OrderDetail" */}
|
||||
<Route exact={true} path="/order_detail/:id" component={OrderDetail} />
|
||||
{/* <Route path="/tabs/speakers/:id" component={SpeakerDetail} exact={true} /> */}
|
||||
|
||||
{/* */}
|
||||
<Route exact={true} path={paths.SETTINGS} component={Settings} />
|
||||
<Route exact={true} path={paths.CHANGE_LANGUAGE} component={ChangeLanguage} />
|
||||
<Route exact={true} path={paths.SERVICE_AGREEMENT} component={ServiceAgreement} />
|
||||
<Route exact={true} path={paths.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -9,11 +9,12 @@ import MessageList from './pages/MessageList';
|
||||
import Favourites from './pages/Favourites';
|
||||
import MyProfile from './pages/MyProfile';
|
||||
import EventList from './pages/EventList';
|
||||
import Helloworld from './pages/Helloworld';
|
||||
|
||||
const TabAppRoute: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Route path="/tabs/not_implemented" component={NotImplemented} />
|
||||
<Route path={paths.TAB_NOT_IMPLEMENTED} component={NotImplemented} />
|
||||
|
||||
{/* */}
|
||||
<Route path={paths.NEARBY_LIST} render={() => <MembersNearByList />} exact={true} />
|
||||
@@ -28,10 +29,10 @@ const TabAppRoute: React.FC = () => {
|
||||
<Route path={paths.FAVOURITES_LIST} render={() => <Favourites />} exact={true} />
|
||||
|
||||
{/* */}
|
||||
<Route path="/tabs/events" render={() => <EventList />} exact={true} />
|
||||
<Route path={paths.EVENT_LIST} render={() => <EventList />} exact={true} />
|
||||
|
||||
{/* */}
|
||||
<Route path="/tabs/my_profile" render={() => <MyProfile />} exact={true} />
|
||||
<Route path={paths.PROFILE} render={() => <MyProfile />} exact={true} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -7,11 +7,7 @@ interface StateProps {
|
||||
}
|
||||
|
||||
const HomeOrTutorial: React.FC<StateProps> = ({ hasSeenTutorial }) => {
|
||||
return hasSeenTutorial ? (
|
||||
<Redirect to="/tabs/schedule" />
|
||||
) : (
|
||||
<Redirect to="/tutorial" />
|
||||
);
|
||||
return hasSeenTutorial ? <Redirect to="/tabs/events" /> : <Redirect to="/tutorial" />;
|
||||
};
|
||||
|
||||
export default connect<{}, StateProps, {}>({
|
||||
|
@@ -6,15 +6,12 @@ interface RedirectToLoginProps {
|
||||
setUsername: Function;
|
||||
}
|
||||
|
||||
const RedirectToLogin: React.FC<RedirectToLoginProps> = ({
|
||||
setIsLoggedIn,
|
||||
setUsername,
|
||||
}) => {
|
||||
const RedirectToLogin: React.FC<RedirectToLoginProps> = ({ setIsLoggedIn, setUsername }) => {
|
||||
const ionRouterContext = useContext(IonRouterContext);
|
||||
useEffect(() => {
|
||||
setIsLoggedIn(false);
|
||||
setUsername(undefined);
|
||||
ionRouterContext.push('/tabs/schedule');
|
||||
ionRouterContext.push('/tabs/events');
|
||||
}, [setIsLoggedIn, setUsername]);
|
||||
return null;
|
||||
};
|
||||
|
0
03_source/mobile/src/context/action.tsx
Normal file
0
03_source/mobile/src/context/action.tsx
Normal file
0
03_source/mobile/src/context/index.ts
Normal file
0
03_source/mobile/src/context/index.ts
Normal file
92
03_source/mobile/src/context/jwt/action.tsx
Normal file
92
03_source/mobile/src/context/jwt/action.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import axios, { endpoints } from '../../lib/axios';
|
||||
|
||||
import { setSession } from './utils';
|
||||
import { JWT_STORAGE_KEY } from './constant';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SignUpParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
const ERR_ACCESS_TOKEN_NOT_FOUND = `Access token not found in response`;
|
||||
|
||||
/** **************************************
|
||||
* Sign in
|
||||
*************************************** */
|
||||
export const signInWithPassword = async ({
|
||||
email,
|
||||
password,
|
||||
}: SignInParams): Promise<string | null> => {
|
||||
try {
|
||||
const params = { email, password };
|
||||
|
||||
const res = await axios.post(endpoints.auth.signIn, params);
|
||||
|
||||
const { accessToken } = res.data;
|
||||
|
||||
console.log({ t: res.data });
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error(ERR_ACCESS_TOKEN_NOT_FOUND);
|
||||
}
|
||||
|
||||
// setSession(accessToken);
|
||||
return accessToken;
|
||||
} catch (error) {
|
||||
console.error('Error during sign in:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign up
|
||||
*************************************** */
|
||||
export const signUp = async ({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: SignUpParams): Promise<void> => {
|
||||
const params = {
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios.post(endpoints.auth.signUp, params);
|
||||
|
||||
const { accessToken } = res.data;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('Access token not found in response');
|
||||
}
|
||||
|
||||
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
|
||||
} catch (error) {
|
||||
console.error('Error during sign up:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign out
|
||||
*************************************** */
|
||||
export const signOut = async (): Promise<void> => {
|
||||
try {
|
||||
await setSession(null);
|
||||
} catch (error) {
|
||||
console.error('Error during sign out:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
1
03_source/mobile/src/context/jwt/constant.ts
Normal file
1
03_source/mobile/src/context/jwt/constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const JWT_STORAGE_KEY = 'jwt_access_token';
|
97
03_source/mobile/src/context/jwt/utils.tsx
Normal file
97
03_source/mobile/src/context/jwt/utils.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
// import { paths } from 'src/routes/paths';
|
||||
|
||||
import axios from '../../lib/axios';
|
||||
|
||||
import { JWT_STORAGE_KEY } from './constant.js';
|
||||
import paths from '../../paths.js';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function tokenExpired(exp: number) {
|
||||
const currentTime = Date.now();
|
||||
const timeLeft = exp * 1000 - currentTime;
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
alert('Token expired!');
|
||||
sessionStorage.removeItem(JWT_STORAGE_KEY);
|
||||
window.location.href = paths.SIGN_IN;
|
||||
} catch (error) {
|
||||
console.error('Error during token expiration:', error);
|
||||
throw error;
|
||||
}
|
||||
}, timeLeft);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const INVALID_ACCESS_TOKEN = 'Invalid access token!';
|
||||
|
||||
export async function setSession(accessToken: string | null) {
|
||||
try {
|
||||
if (accessToken) {
|
||||
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
|
||||
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
|
||||
|
||||
const decodedToken = jwtDecode(accessToken); // ~3 days by minimals server
|
||||
|
||||
if (decodedToken && 'exp' in decodedToken) {
|
||||
tokenExpired(decodedToken.exp);
|
||||
} else {
|
||||
throw new Error(INVALID_ACCESS_TOKEN);
|
||||
}
|
||||
} else {
|
||||
sessionStorage.removeItem(JWT_STORAGE_KEY);
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during set session:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
@@ -11,9 +11,7 @@ export const AppContext = createContext<AppContextState>({
|
||||
dispatch: () => undefined,
|
||||
});
|
||||
|
||||
export const AppContextProvider: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
export const AppContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [store, dispatch] = useReducer(reducers, initialState);
|
||||
|
||||
return (
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Preferences as Storage } from '@capacitor/preferences';
|
||||
import { GetResult, Preferences as Storage } from '@capacitor/preferences';
|
||||
import { Schedule, Session } from '../models/Schedule';
|
||||
import { Speaker } from '../models/Speaker';
|
||||
import { Location } from '../models/Location';
|
||||
@@ -13,6 +13,8 @@ const locationsUrl = '/assets/data/locations.json';
|
||||
const HAS_LOGGED_IN = 'hasLoggedIn';
|
||||
const HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
|
||||
const USERNAME = 'username';
|
||||
const ACCESS_TOKEN = 'a_token';
|
||||
const ACTIVE_SESSION = 'a_session';
|
||||
|
||||
export const getConfData = async () => {
|
||||
const response = await Promise.all([
|
||||
@@ -116,6 +118,18 @@ export const setUsernameData = async (username?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setAccessTokenData = async (accessToken?: string) => {
|
||||
if (!accessToken) {
|
||||
await Storage.remove({ key: ACCESS_TOKEN });
|
||||
} else {
|
||||
await Storage.set({ key: ACCESS_TOKEN, value: accessToken });
|
||||
}
|
||||
};
|
||||
|
||||
export const getAccessTokenData = async (): Promise<GetResult> => {
|
||||
return Storage.get({ key: ACCESS_TOKEN });
|
||||
};
|
||||
|
||||
function parseSessions(schedule: Schedule) {
|
||||
const sessions: Session[] = [];
|
||||
schedule.groups.forEach((g) => {
|
||||
@@ -123,3 +137,15 @@ function parseSessions(schedule: Schedule) {
|
||||
});
|
||||
return sessions;
|
||||
}
|
||||
|
||||
export const setActiveSessionData = async (activeSession: any) => {
|
||||
if (!activeSession) {
|
||||
await Storage.remove({ key: ACTIVE_SESSION });
|
||||
} else {
|
||||
await Storage.set({ key: ACTIVE_SESSION, value: JSON.stringify(activeSession) });
|
||||
}
|
||||
};
|
||||
|
||||
export const getActiveSessionData = async (): Promise<GetResult> => {
|
||||
return Storage.get({ key: JSON.parse(ACTIVE_SESSION) });
|
||||
};
|
||||
|
@@ -28,6 +28,8 @@ export const initialState: AppState = {
|
||||
darkMode: false,
|
||||
isLoggedin: false,
|
||||
loading: false,
|
||||
//
|
||||
isSessionValid: false,
|
||||
},
|
||||
locations: {
|
||||
locations: [],
|
||||
|
@@ -3,9 +3,15 @@ import {
|
||||
setIsLoggedInData,
|
||||
setUsernameData,
|
||||
setHasSeenTutorialData,
|
||||
setAccessTokenData,
|
||||
getAccessTokenData,
|
||||
setActiveSessionData,
|
||||
} from '../dataApi';
|
||||
import { ActionType } from '../../util/types';
|
||||
import { UserState } from './user.state';
|
||||
import { isValidToken } from '../../context/jwt/utils';
|
||||
import axios from 'axios';
|
||||
import { endpoints } from '../../pages/MyLogin/endpoints';
|
||||
|
||||
export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
|
||||
dispatch(setLoading(true));
|
||||
@@ -15,10 +21,7 @@ export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
|
||||
};
|
||||
|
||||
export const setLoading = (isLoading: boolean) =>
|
||||
({
|
||||
type: 'set-user-loading',
|
||||
isLoading,
|
||||
} as const);
|
||||
({ type: 'set-user-loading', isLoading } as const);
|
||||
|
||||
export const setData = (data: Partial<UserState>) =>
|
||||
({
|
||||
@@ -47,6 +50,53 @@ export const setUsername = (username?: string) => async (dispatch: React.Dispatc
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const setAccessToken = (token?: string) => async (dispatch: React.Dispatch<any>) => {
|
||||
await setAccessTokenData(token);
|
||||
return {
|
||||
type: 'set-access-token',
|
||||
token,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const setActiveSession = (session: any) => async (dispatch: React.Dispatch<any>) => {
|
||||
await setActiveSessionData(session);
|
||||
return {
|
||||
type: 'set-active-session',
|
||||
session,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const checkUserSession = () => async (dispatch: React.Dispatch<any>) => {
|
||||
let accessToken = (await getAccessTokenData()).value;
|
||||
console.log('check user session');
|
||||
let sessionValid = false;
|
||||
|
||||
try {
|
||||
if (accessToken && isValidToken(accessToken)) {
|
||||
const res = await axios.get(endpoints.auth.me, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
const { user } = res.data;
|
||||
|
||||
setActiveSession({ user: { ...user, accessToken }, loading: false });
|
||||
sessionValid = true;
|
||||
console.log('session valid');
|
||||
} else {
|
||||
setActiveSession({ user: null, loading: false });
|
||||
console.log('session not valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setActiveSession({ user: null, loading: false });
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'check-user-session',
|
||||
sessionValid,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const setHasSeenTutorial =
|
||||
(hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => {
|
||||
await setHasSeenTutorialData(hasSeenTutorial);
|
||||
@@ -68,4 +118,7 @@ export type UserActions =
|
||||
| ActionType<typeof setIsLoggedIn>
|
||||
| ActionType<typeof setUsername>
|
||||
| ActionType<typeof setHasSeenTutorial>
|
||||
| ActionType<typeof setDarkMode>;
|
||||
| ActionType<typeof setDarkMode>
|
||||
| ActionType<typeof setAccessToken>
|
||||
// | ActionType<typeof setSession>
|
||||
| ActionType<typeof checkUserSession>;
|
||||
|
@@ -15,5 +15,11 @@ export function userReducer(state: UserState, action: UserActions): UserState {
|
||||
return { ...state, darkMode: action.darkMode };
|
||||
case 'set-is-loggedin':
|
||||
return { ...state, isLoggedin: action.loggedIn };
|
||||
case 'check-user-session':
|
||||
return { ...state, isSessionValid: action.sessionValid };
|
||||
// case 'set-active-session':
|
||||
// return { ...state, session: action.session };
|
||||
// case 'set-access-token':
|
||||
// return { ...state, token: action.token };
|
||||
}
|
||||
}
|
||||
|
@@ -4,4 +4,7 @@ export interface UserState {
|
||||
darkMode: boolean;
|
||||
hasSeenTutorial: boolean;
|
||||
loading: boolean;
|
||||
isSessionValid: boolean;
|
||||
session?: any;
|
||||
token?: string;
|
||||
}
|
||||
|
3
03_source/mobile/src/global-config.ts
Normal file
3
03_source/mobile/src/global-config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const CONFIG = {
|
||||
serverUrl: '',
|
||||
};
|
32
03_source/mobile/src/hooks/use-set-state.ts
Normal file
32
03_source/mobile/src/hooks/use-set-state.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Custom hook to manage state with utility functions to set state, set a specific field, and reset state.
|
||||
*
|
||||
* @param {T} initialState - The initial state value.
|
||||
*
|
||||
* @returns {UseSetStateReturn<T>} - An object containing:
|
||||
* - `state`: The current state.
|
||||
* - `resetState`: A function to reset the state to the initial value.
|
||||
* - `setState`: A function to update the state.
|
||||
* - `setField`: A function to update a specific field in the state.
|
||||
*
|
||||
* @example
|
||||
* const { state, setState, setField, resetState } = useSetState({ name: '', age: 0 });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <p>Name: {state.name}</p>
|
||||
* <p>Age: {state.age}</p>
|
||||
* <button onClick={() => setField('name', 'John')}>Set Name</button>
|
||||
* <button onClick={resetState}>Reset</button>
|
||||
* </div>
|
||||
* );
|
||||
*/
|
||||
type UseSetStateReturn<T> = {
|
||||
state: T;
|
||||
resetState: (defaultState?: T) => void;
|
||||
setState: (updateState: T | Partial<T>) => void;
|
||||
setField: (name: keyof T, updateValue: T[keyof T]) => void;
|
||||
};
|
||||
declare function useSetState<T>(initialState?: T): UseSetStateReturn<T>;
|
||||
|
||||
export { type UseSetStateReturn, useSetState };
|
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>
|
||||
|
@@ -1,17 +1,25 @@
|
||||
const paths = {
|
||||
NOT_IMPLEMENTED: '/not_implemented',
|
||||
TAB_NOT_IMPLEMENTED: '/tabs/not_implemented',
|
||||
//
|
||||
SETTINGS: '/settings',
|
||||
//
|
||||
EVENT_LIST: `/tabs/events`,
|
||||
MESSAGE_LIST: `/tabs/messages`,
|
||||
NEARBY_LIST: '/tabs/nearby',
|
||||
//
|
||||
ORDERS_LIST: '/tabs/orders',
|
||||
FAVOURITES_LIST: `/tabs/favourites`,
|
||||
//
|
||||
ORDER_DETAIL: '/order_detail/:id',
|
||||
getOrderDetail: (id: string) => `/order_detail/${id}`,
|
||||
//
|
||||
FAVOURITES_LIST: `/tabs/favourites`,
|
||||
CHANGE_LANGUAGE: '/change_language',
|
||||
SERVICE_AGREEMENT: '/service_agreement',
|
||||
PRIVACY_AGREEMENT: '/privacy_agreement',
|
||||
//
|
||||
login: '/login',
|
||||
PROFILE: '/tabs/my_profile',
|
||||
//
|
||||
SIGN_IN: '/mylogin',
|
||||
};
|
||||
export default paths;
|
||||
|
Reference in New Issue
Block a user