"chore: update frontend dev script to include lint checks and add ESLint config file"

This commit is contained in:
louiscklaw
2025-06-04 02:35:32 +08:00
parent c0fad42f0a
commit 22fb620eef
48 changed files with 3315 additions and 97 deletions

View File

@@ -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

View File

@@ -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} />
</>
);
};

View File

@@ -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} />
</>
);
};

View File

@@ -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, {}>({

View File

@@ -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;
};

View File

View File

View 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;
}
};

View File

@@ -0,0 +1 @@
export const JWT_STORAGE_KEY = 'jwt_access_token';

View 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;
}
}

View File

@@ -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 (

View File

@@ -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) });
};

View File

@@ -28,6 +28,8 @@ export const initialState: AppState = {
darkMode: false,
isLoggedin: false,
loading: false,
//
isSessionValid: false,
},
locations: {
locations: [],

View File

@@ -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>;

View File

@@ -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 };
}
}

View File

@@ -4,4 +4,7 @@ export interface UserState {
darkMode: boolean;
hasSeenTutorial: boolean;
loading: boolean;
isSessionValid: boolean;
session?: any;
token?: string;
}

View File

@@ -0,0 +1,3 @@
export const CONFIG = {
serverUrl: '',
};

View 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 };

View 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;

View File

@@ -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>

View 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 };

View 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,
});

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;