"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

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