Compare commits

...

8 Commits

Author SHA1 Message Date
louiscklaw
030fc1a808 update login requirement for mobile, 2025-05-14 18:13:15 +08:00
louiscklaw
05c69481b5 update check session working, 2025-05-14 18:10:22 +08:00
louiscklaw
0fcc194860 in the middle, working for login and logout test, 2025-05-14 17:19:48 +08:00
louiscklaw
56f0f30ffb ```
replace inline loading text with LoadingScreen component in multiple pages
```
2025-05-14 16:27:30 +08:00
louiscklaw
0aefbfaeae fix typo, 2025-05-14 16:25:57 +08:00
louiscklaw
628c72190b fix typo, 2025-05-14 16:18:39 +08:00
louiscklaw
886a314df7 update settings pages in the middle, 2025-05-14 16:18:04 +08:00
louiscklaw
efc2d31f7c ```
add new student info route and related components, update auth guard implementation and signup success redirect
```
2025-05-14 15:40:59 +08:00
18 changed files with 404 additions and 27 deletions

View File

@@ -2,7 +2,7 @@
tags: cms, login-flow
---
# login flow
# CMS login flow
## description

View File

@@ -0,0 +1,37 @@
---
tags: mobile, login-flow
---
# Mobile login flow
## description
```mermaid
graph TD;
Start-->A;
A-->B;
B-->C;
B-->D;
D-->E;
E-->F;
C-->G;
G-->A
F-->End;
A[greeting, asking username and password]
B[check if username and password is valid]
C[pasword failed]
D[pasword ok]
E[login success]
F[redirect to '/dashboard']
G[prompt user wrong username and password]
Start((start));
End((end))
```
### relations
[REQ0016](../REQ0016/index.md)

View File

@@ -17,7 +17,8 @@
- [REQ0013: cms dashboard](./REQ0013/index.md)
- [REQ0014: mobile client](./REQ0014/index.md)
- [REQ0015: pocketbase json schema to dbml converter](./REQ0015/index.md)
- [REQ0016: login flow](./REQ0016/index.md)
- [REQ0016: CMS login flow](./REQ0016/index.md)
- [REQ0017: lesson page documentation](./REQ0017/index.md)
- [REQ0018: family photo of frameworks](./REQ0018/index.md)
- [REQ0019: System architecture](./REQ0019/index.md)
- [REQ0020: Mobile login flow](./REQ0020/index.md)

View File

@@ -1,3 +1,4 @@
# TODO
- [ ] add login mechanism
- [ ] add task server handle callback tasks

View File

@@ -1,9 +1,14 @@
const Paths = {
AuthHome: `/auth/Home`,
AuthHome: `/auth/home`,
AuthLogin: `/auth/login`,
AuthSignUp: `/auth/signup`,
SignUpSuccess: `/auth/sign_up_success`,
AuthorizedTest: `/auth/authorized_test`,
//
StudentInfo: `/auth/student_info/:id`,
GetStudentInfoLink: (id: string) => `/auth/student_info/${id}`,
//
Setting: `/setting`,
};
export { Paths };

View File

@@ -45,14 +45,16 @@ import Page from './pages/Page';
import QuizzesMainMenu from './pages/QuizzesMainMenu';
//
import MyAchievementPage from './pages/Record/index';
import Setting from './pages/Setting/indx';
import Setting from './pages/Setting';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';
import { Paths } from './Paths';
import SignUpSuccess from './pages/auth/SignUpSuccess';
import AuthorizedTest from './pages/auth/AuthorizedTest';
import { AuthGuard } from './pages/auth/AuthorizedTest/auth-guard';
import { AuthGuard } from './components/auth/auth-guard';
import StudentInfo from './pages/auth/StudentInfo';
// import { AuthGuard } from './pages/auth/AuthorizedTest/auth-guard';
// import WordPageWithLayout from './pages/Lesson/WordPageWithLayout.del';
function RouteConfig() {
@@ -180,11 +182,15 @@ function RouteConfig() {
<SignUpSuccess />
</Route>
<Route exact path={Paths.AuthorizedTest}>
<AuthGuard>
{/* protected page */}
<AuthGuard>
<Route exact path={Paths.AuthorizedTest}>
<AuthorizedTest />
</AuthGuard>
</Route>
</Route>
<Route exact path={Paths.StudentInfo}>
<StudentInfo />
</Route>
</AuthGuard>
{/* TODO: remove below */}
<Route exact path="/tab1">
@@ -196,7 +202,7 @@ function RouteConfig() {
<Route path="/tab3">
<Tab3 />
</Route>
<Route path="/setting">
<Route path={Paths.Setting}>
<Setting />
</Route>
<Route path="/page/:name" exact={true}>

View File

@@ -1,8 +1,8 @@
import { useIonRouter } from '@ionic/react';
import * as React from 'react';
import { IonAlert, IonButton } from '@ionic/react';
import { useUser } from '../../../hooks/use-user';
import { Paths } from '../../../Paths';
import { useUser } from '../../hooks/use-user';
import { Paths } from '../../Paths';
export interface AuthGuardProps {
children: React.ReactNode;

View File

@@ -4,5 +4,8 @@ import { COL_USER_METAS } from '../../constants';
import { pb } from '../../lib/pb';
export async function getUserMetaById(id: string): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).getOne(id);
return pb.collection(COL_USER_METAS).getOne(id, {
expand: 'billingAddress',
requestKey: null,
});
}

View File

@@ -1,5 +1,19 @@
import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
// DBUserMeta type definitions
export interface DBUserMeta {
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
//
collectionId: string;
id: string;
createdAt: Date;
}
// UserMeta type definitions
export interface UserMeta {
id: string;

View File

@@ -0,0 +1,5 @@
import { DBUserMeta } from '../db/UserMetas/type';
export function getStudentAvatar(studentMeta: DBUserMeta) {
return `url(http://localhost:8090/api/files/${studentMeta.collectionId}/${studentMeta.id}/${studentMeta.avatar})`;
}

View File

@@ -35,7 +35,7 @@ const LessonContainer: React.FC<ContainerProps> = ({ lesson_type_id: lesson_type
if (loading) return <LoadingScreen />;
if (!selected_content) return <LoadingScreen />;
if (selected_content.length == 0) return <>loading</>;
if (selected_content.length == 0) return <LoadingScreen />;
return (
<>

View File

@@ -22,12 +22,25 @@ import _ from 'lodash';
import { Router, useParams } from 'react-router';
import { Wave } from '../../../components/Wave';
import { Paths } from '../../../Paths';
import { useTransition } from 'react';
import { useTranslation } from 'react-i18next';
import { useUser } from '../../../hooks/use-user';
function AuthorizedTest(): React.JSX.Element {
const router = useIonRouter();
const { t } = useTranslation();
const { user } = useUser();
function handleBackToLogin() {
router.push(Paths.AuthLogin);
}
function handleViewStudentInfoOnClick() {
if (user?.id) {
router.push(Paths.GetStudentInfoLink(user.id));
}
}
return (
<IonPage className={styles.loginPage}>
<IonHeader>{/* */}</IonHeader>
@@ -36,6 +49,12 @@ function AuthorizedTest(): React.JSX.Element {
<IonGrid className="ion-padding">
<IonCol>
<IonRow>Authorized page test</IonRow>
{JSON.stringify({ user })}
{/* */}
<IonRow>
<IonButton onClick={handleViewStudentInfoOnClick}>{t('view-student-info')}</IonButton>
</IonRow>
{/* */}
<IonRow>
<IonButton onClick={handleBackToLogin}>Back to login</IonButton>
</IonRow>

View File

@@ -0,0 +1,107 @@
import {
IonButton,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonImg,
IonPage,
IonRouterLink,
IonRow,
IonToolbar,
useIonRouter,
} from '@ionic/react';
// import { Action } from '../components/Action';
import styles from './style.module.scss';
import { Action } from '../../../components/Action';
import { Paths } from '../../../Paths';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import { useUser } from '../../../hooks/use-user';
import { LoadingScreen } from '../../../components/LoadingScreen';
const AuthHome = () => {
const { t } = useTranslation();
const { user, checkSession, isLoading } = useUser();
const router = useIonRouter();
const [showLoading, setShowLoading] = useState<boolean>(true);
const [showError, setShowErrr] = useState<{ show: boolean; message: string }>({
show: false,
message: '',
});
const [checkingSession, setCheckingSession] = useState<boolean>(true);
useEffect(() => {
if (!checkingSession) {
if (!user) {
router.push(Paths.AuthLogin);
} else {
router.push(Paths.AuthorizedTest);
}
setShowLoading(false);
}
}, [user, checkingSession]);
useEffect(() => {
checkSession?.()
.then(() => {
setCheckingSession(false);
})
.catch((err) => console.error(err));
}, [checkSession]);
if (showLoading) return <LoadingScreen />;
// if (showError) return <>{showError.message}</>;
return (
<IonPage className={'styles.homePage'}>
<IonHeader>
{/* <IonToolbar className="ion-no-margin ion-no-padding"> */}
<IonImg src="/assets/login2.jpeg" />
{/* </IonToolbar> */}
</IonHeader>
<IonContent fullscreen>
<div className={styles.getStarted}>
<IonGrid>
<IonRow className={`ion-text-center ion-justify-content-center ${styles.heading}`}>
<IonCol size="11" className={styles.headingText}>
<IonCardTitle>
{/* */}
Join millions of other people discovering their creative side
</IonCardTitle>
</IonCol>
</IonRow>
<IonRow className={`ion-text-center ion-justify-content-center`}>
<IonRouterLink routerLink={Paths.AuthSignUp} className="custom-link">
<IonCol size="11">
<IonButton className={`${styles.getStartedButton} custom-button`}>
{/* */}
Get started &rarr;
</IonButton>
</IonCol>
</IonRouterLink>
</IonRow>
</IonGrid>
</div>
</IonContent>
<IonFooter>
<IonGrid style={{ marginBottom: '1rem' }}>
<Action
message={t('already-got-an-account')}
text={t('login')}
link={Paths.AuthLogin}
//
/>
</IonGrid>
</IonFooter>
</IonPage>
);
};
export default AuthHome;

View File

@@ -11,16 +11,33 @@ import {
IonRouterLink,
IonRow,
IonToolbar,
useIonRouter,
} from '@ionic/react';
// import { Action } from '../components/Action';
import styles from './style.module.scss';
import { Action } from '../../../components/Action';
import { Paths } from '../../../Paths';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import { useUser } from '../../../hooks/use-user';
import { LoadingScreen } from '../../../components/LoadingScreen';
const AuthHome = () => {
const { t } = useTranslation();
const [showLoading, setShowLoading] = useState<boolean>(true);
const [showError, setShowErrr] = useState<{ show: boolean; message: string }>({
show: false,
message: '',
});
useEffect(() => {
setShowLoading(false);
}, []);
if (showLoading) return <LoadingScreen />;
if (showError.show) return <>{showError.message}</>;
return (
<IonPage className={'styles.homePage'}>
<IonHeader>

View File

@@ -38,6 +38,7 @@ import { z as zod } from 'zod';
import { pb } from '../../../lib/pb';
import { ClientResponseError } from 'pocketbase';
import { COL_USER_METAS, COL_USERS } from '../../../constants';
import { Paths } from '../../../Paths';
function AuthSignUp(): React.JSX.Element {
const params = useParams();
@@ -109,6 +110,8 @@ function AuthSignUp(): React.JSX.Element {
};
const userMetaRecord = await pb.collection(COL_USER_METAS).create(userMeta);
await pb.collection('users').requestVerification(user.email);
router.push(Paths.SignUpSuccess);
} catch (err: any) {
const res_err = err as unknown as ClientResponseError;
const {
@@ -133,19 +136,7 @@ function AuthSignUp(): React.JSX.Element {
return (
<IonPage className={styles.loginPage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton icon={arrowBack} text="" className="custom-back" />
</IonButtons>
<IonButtons slot="end">
<IonButton className="custom-button">
<IonIcon icon={shapesOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonHeader></IonHeader>
{/* */}
<IonContent fullscreen>
<form onSubmit={handleSubmit(onSubmit)}>

View File

@@ -0,0 +1,154 @@
import {
IonButton,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonPage,
IonRow,
IonText,
useIonRouter,
} from '@ionic/react';
import styles from './style.module.scss';
import { useParams } from 'react-router';
import { Wave } from '../../../components/Wave';
import { Paths } from '../../../Paths';
import { useEffect, useState } from 'react';
import { getUserMetaById } from '../../../db/UserMetas/GetById';
import { useTranslation } from 'react-i18next';
import { DBUserMeta } from '../../../db/UserMetas/type';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { getStudentAvatar } from '../../../lib/getStudentAvatar';
import { authClient } from '../../../lib/auth/custom/client';
import { useUser } from '../../../hooks/use-user';
function StudentInfo(): React.JSX.Element {
const router = useIonRouter();
const { id } = useParams<{ id: string }>();
const { t } = useTranslation();
const [studentMeta, setStudentMeta] = useState<DBUserMeta>();
const test = useUser();
const [showLoading, setShowLoading] = useState<boolean>(true);
const [showError, setShowError] = useState<{ show: boolean; message: string }>({ show: false, message: '' });
function handleBackToLogin() {
router.push(Paths.AuthLogin);
}
function handleBackOnClick() {
router.push(Paths.Setting);
}
async function handleFetchUserMeta() {
try {
const result = await getUserMetaById(id);
const tempStudentMeta = result as unknown as DBUserMeta;
setStudentMeta(tempStudentMeta);
setShowLoading(false);
} catch (error) {
setShowError({ show: true, message: JSON.stringify({ error }, null, 2) });
setShowLoading(false);
}
}
async function handleLogoutOnClick() {
try {
await authClient.signOut();
router.push(Paths.AuthLogin);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
void handleFetchUserMeta();
}, []);
if (showLoading) return <LoadingScreen />;
if (!studentMeta) return <LoadingScreen />;
if (showError.show) return <>{showError.message}</>;
return (
<IonPage className={styles.loginPage}>
<IonHeader>{/* */}</IonHeader>
{/* */}
<IonContent fullscreen>
<IonGrid className="ion-padding">
<IonRow className="ion-justify-content-center">
<IonCol size={'3'}>
<div
style={{
backgroundImage: getStudentAvatar(studentMeta),
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
//
width: '25vw',
height: '25vw',
//
borderRadius: 'calc( 25vw / 2 )',
}}
></div>
</IonCol>
</IonRow>
{/* */}
<IonRow
className="ion-justify-content-between"
style={{
marginTop: '1rem',
marginBottom: '1rem',
//
}}
>
<IonText>{t('student-name')}</IonText>
<IonText>{studentMeta.name}</IonText>
</IonRow>
{/* */}
<IonRow
className="ion-justify-content-between"
style={{
marginTop: '1rem',
marginBottom: '1rem',
//
}}
>
<IonText>{t('student-email')}</IonText>
<IonText>{studentMeta.email}</IonText>
</IonRow>
{/* */}
<IonRow
className="ion-justify-content-between"
style={{
marginTop: '1rem',
marginBottom: '1rem',
//
}}
>
<IonText>{t('student-phone')}</IonText>
<IonText>{studentMeta.phone}</IonText>
</IonRow>
{/* */}
<IonRow className="ion-justify-content-center">
<IonButton onClick={handleBackOnClick}>{t('back')}</IonButton>
</IonRow>
<IonRow className="ion-justify-content-center">
<IonButton onClick={handleLogoutOnClick}>{t('logout')}</IonButton>
</IonRow>
</IonGrid>
</IonContent>
{/* */}
<IonFooter>
<IonGrid className="ion-no-margin ion-no-padding">
<Wave />
</IonGrid>
</IonFooter>
</IonPage>
);
}
export default StudentInfo;

View File

@@ -0,0 +1,17 @@
.signupPage {
ion-toolbar {
--border-style: none;
--border-color: transparent;
--padding-top: 1rem;
--padding-bottom: 1rem;
--padding-start: 1rem;
--padding-end: 1rem;
}
}
.headingText {
h5 {
margin-top: 0.2rem;
// color: #d3a6c7;
}
}