diff --git a/public/favicon.svg b/public/favicon.svg
index 3caf057..dddf9f8 100644
--- a/public/favicon.svg
+++ b/public/favicon.svg
@@ -2,7 +2,7 @@
-
+
diff --git a/public/index.html b/public/index.html
index 0110b01..d7d1875 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,6 +2,47 @@
+
+
+
+
+
+
diff --git a/public/public-sans-latin-400-normal.woff2 b/public/public-sans-latin-400-normal.woff2
new file mode 100644
index 0000000..e303dab
Binary files /dev/null and b/public/public-sans-latin-400-normal.woff2 differ
diff --git a/public/public-sans-latin-500-normal.woff2 b/public/public-sans-latin-500-normal.woff2
new file mode 100644
index 0000000..b763efb
Binary files /dev/null and b/public/public-sans-latin-500-normal.woff2 differ
diff --git a/public/public-sans-latin-600-normal.woff2 b/public/public-sans-latin-600-normal.woff2
new file mode 100644
index 0000000..86089d1
Binary files /dev/null and b/public/public-sans-latin-600-normal.woff2 differ
diff --git a/public/public-sans-latin-700-normal.woff2 b/public/public-sans-latin-700-normal.woff2
new file mode 100644
index 0000000..79c2c45
Binary files /dev/null and b/public/public-sans-latin-700-normal.woff2 differ
diff --git a/src/assets/fonts.css b/src/assets/fonts.css
index 249b9ff..428c819 100644
--- a/src/assets/fonts.css
+++ b/src/assets/fonts.css
@@ -1,19 +1,20 @@
/* Self-hosted fonts (bundled via @fontsource; no Google Fonts CDN — CSP-friendly). */
+/* All @fontsource v5 packages already use font-display: swap by default. */
/* Public Sans — MUI theme (`themeConst.js`), charts, config */
-@import "@fontsource/public-sans/400.css";
-@import "@fontsource/public-sans/500.css";
-@import "@fontsource/public-sans/600.css";
-@import "@fontsource/public-sans/700.css";
+@import url("@fontsource/public-sans/400.css");
+@import url("@fontsource/public-sans/500.css");
+@import url("@fontsource/public-sans/600.css");
+@import url("@fontsource/public-sans/700.css");
-/* Noto Sans HK — `styles.css` / navbar (Latin + Hong Kong glyphs) */
-@import "@fontsource/noto-sans-hk/latin-400.css";
-@import "@fontsource/noto-sans-hk/chinese-hongkong-400.css";
-@import "@fontsource/noto-sans-hk/latin-600.css";
-@import "@fontsource/noto-sans-hk/chinese-hongkong-600.css";
+/* Noto Sans HK — navbar (Latin + Hong Kong glyphs) */
+@import url("@fontsource/noto-sans-hk/latin-400.css");
+@import url("@fontsource/noto-sans-hk/chinese-hongkong-400.css");
+@import url("@fontsource/noto-sans-hk/latin-600.css");
+@import url("@fontsource/noto-sans-hk/chinese-hongkong-600.css");
/* Noto Sans SC — fallback for simplified Chinese */
-@import "@fontsource/noto-sans-sc/latin-400.css";
-@import "@fontsource/noto-sans-sc/chinese-simplified-400.css";
-@import "@fontsource/noto-sans-sc/latin-600.css";
-@import "@fontsource/noto-sans-sc/chinese-simplified-600.css";
+@import url("@fontsource/noto-sans-sc/latin-400.css");
+@import url("@fontsource/noto-sans-sc/chinese-simplified-400.css");
+@import url("@fontsource/noto-sans-sc/latin-600.css");
+@import url("@fontsource/noto-sans-sc/chinese-simplified-600.css");
diff --git a/src/assets/style/navbarStyles.css b/src/assets/style/navbarStyles.css
index dea1992..078a711 100644
--- a/src/assets/style/navbarStyles.css
+++ b/src/assets/style/navbarStyles.css
@@ -11,7 +11,7 @@
top: 0px;
width: 100%;
z-index: 9999;
- border-bottom: 3px solid #0C489E;
+ border-bottom: 3px solid #0c489e;
min-height: 77px;
}
@@ -112,7 +112,7 @@
#navbar div li a:hover,
#navbar div li button.navTrigger:hover{
- color: #0C489E;
+ color: #1565C0;
}
#navbar div li a:hover::after,
@@ -122,7 +122,7 @@
content: "";
width: 80%;
height: 3px;
- background:#0C489E;
+ background:#1565C0;
position: absolute;
bottom: -5px;
left: 20px;
@@ -166,7 +166,7 @@
#navbar a:focus-visible,
#navbar button.navTrigger:focus-visible{
- outline: 3px solid #0C489E;
+ outline: 3px solid #0c489e;
outline-offset: 2px;
border-radius: 10px;
}
@@ -234,7 +234,7 @@
text-decoration: none;
font-size: 1.1rem;
font-weight: 600;
- color: #0C489E;
+ color: #0c489e;
transition: 0.3s ease-in-out;
text-align: center;
}
@@ -242,7 +242,7 @@
text-decoration: none;
font-size: 1.1rem;
font-weight: 600;
- color: #0C489E;
+ color: #0c489e;
transition: 0.3s ease-in-out;
text-align: center;
}
@@ -284,7 +284,7 @@
transition: 0.3s ease-in-out;
}
#sidebar li a:hover{
- color: #0C489E;
+ color: #0c489e;
}
#sidebar div li ul{
background: white;
@@ -344,12 +344,12 @@
/* Hover / focus */
#sidebar li > a:hover,
#sidebar li > button.navTrigger:hover{
- color: #0C489E;
+ color: #0c489e;
}
#sidebar li > a:focus-visible,
#sidebar li > button.navTrigger:focus-visible{
- outline: 3px solid #0C489E;
+ outline: 3px solid #0c489e;
outline-offset: 2px;
border-radius: 10px;
}
diff --git a/src/assets/style/styles.css b/src/assets/style/styles.css
index a675de2..9039950 100644
--- a/src/assets/style/styles.css
+++ b/src/assets/style/styles.css
@@ -72,7 +72,7 @@ a:active {
[role="link"],
[tabindex]:not([tabindex="-1"])
):focus-visible {
- outline: 3px solid #0C489E;
+ outline: 3px solid #0c489e;
outline-offset: 2px;
border-radius: 4px;
}
@@ -95,9 +95,8 @@ a:active {
/* ===== MUI DataGrid keyboard focus (WCAG 2.4.7 / 2.4.11) ===== */
.MuiDataGrid-columnHeader:focus-visible,
.MuiDataGrid-cell:focus-visible {
- outline: 3px solid #0C489E;
- outline-offset: -2px;
- box-shadow: 0 0 0 3px rgba(12, 72, 158, 0.25);
+ outline: none;
+ box-shadow: inset 0 0 0 3px #0c489e;
}
/* Contained buttons only */
@@ -122,7 +121,7 @@ a:active {
/* ===== Outlined button focus ===== */
.MuiButton-outlined:focus-visible {
- outline: 3px solid #0C489E;
+ outline: 3px solid #0c489e;
outline-offset: 2px;
}
diff --git a/src/auth/index.js b/src/auth/index.js
index 1a6a8e9..5776050 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -20,6 +20,16 @@ let axiosInterceptorsSetup = false;
// In-memory guard so only one 401/logout flow shows the alert (avoids race when multiple 401s or interceptors run)
let expiredAlertShownInMemory = false;
+/** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */
+const isPublicAuthRequest = (reqUrl) =>
+ reqUrl.includes('/login') || reqUrl.includes('/ldap-login');
+
+/** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */
+export const clearExpiredSessionAlert = () => {
+ localStorage.removeItem('expiredAlertShown');
+ expiredAlertShownInMemory = false;
+};
+
// ** Handle User Login
export const handleLogin = data => {
return dispatch => {
@@ -152,6 +162,12 @@ export const SetupAxiosInterceptors = () => {
// const { config, response: { status } } = error
const status = error.response?.status
const reqUrl = error.config?.url || ''
+
+ // Let login forms handle their own errors (wrong password, locked account, etc.)
+ if (isPublicAuthRequest(reqUrl)) {
+ return Promise.reject(error)
+ }
+
// iAM Smart registration: HKID already linked — caller must show /iamsmart/pleaseLogin (not refresh/logout)
if (
status === 401 &&
@@ -160,11 +176,15 @@ export const SetupAxiosInterceptors = () => {
) {
return Promise.reject(error)
}
- if (status === 401 && error.config?.url !== apiPath + REFRESH_TOKEN) {
+ if (
+ status === 401 &&
+ !isPublicAuthRequest(reqUrl) &&
+ error.config?.url !== apiPath + REFRESH_TOKEN
+ ) {
// Make a request to refresh the access token
const refreshToken = localStorage.getItem('refreshToken');
if (isRefreshToken) {
- return;
+ return Promise.reject(error);
}
isRefreshToken = true;
return axios
@@ -217,7 +237,7 @@ export const SetupAxiosInterceptors = () => {
// }
// }
- if (localStorage.getItem("expiredAlertShown")) {
+ if (localStorage.getItem("expiredAlertShown") && !isPublicAuthRequest(reqUrl)) {
await dispatch(handleLogoutFunction());
await navigate('/login');
await window.location.reload();
diff --git a/src/auth/jwt/jwtService.js b/src/auth/jwt/jwtService.js
index 0df9dac..884d090 100644
--- a/src/auth/jwt/jwtService.js
+++ b/src/auth/jwt/jwtService.js
@@ -40,29 +40,46 @@ export default class JwtService {
// ** const { config, response: { status } } = error
const {config, response} = error
const originalRequest = config
+ const reqUrl = config?.url || ''
// ** if (status === 401) {
if (response && response.status === 401) {
+ const isPublicAuthRequest =
+ reqUrl.includes('/login') ||
+ reqUrl.includes('/ldap-login') ||
+ reqUrl.includes('/refresh-token')
+ if (isPublicAuthRequest) {
+ return Promise.reject(error)
+ }
+
if (!this.isAlreadyFetchingAccessToken) {
this.isAlreadyFetchingAccessToken = true
- this.refreshToken().then(r => {
- this.isAlreadyFetchingAccessToken = false
-
- // ** Update accessToken in localStorage
- this.setToken(r.data.accessToken)
- this.setRefreshToken(r.data.refreshToken)
-
- this.onAccessTokenFetched(r.data.accessToken)
- })
+ this.refreshToken()
+ .then(r => {
+ this.isAlreadyFetchingAccessToken = false
+
+ // ** Update accessToken in localStorage
+ this.setToken(r.data.accessToken)
+ this.setRefreshToken(r.data.refreshToken)
+
+ this.onAccessTokenFetched(r.data.accessToken)
+ })
+ .catch(refreshError => {
+ this.isAlreadyFetchingAccessToken = false
+ this.onAccessTokenFetchFailed(refreshError)
+ })
}
- const retryOriginalRequest = new Promise(resolve => {
- this.addSubscriber(accessToken => {
- // ** Make sure to assign accessToken according to your response.
- // ** Check: https://pixinvent.ticksy.com/ticket/2413870
- // ** Change Authorization header
- originalRequest.headers.Authorization = `${this.jwtConfig.tokenType} ${accessToken}`
- resolve(this.axios(originalRequest))
- })
+ const retryOriginalRequest = new Promise((resolve, reject) => {
+ this.addSubscriber(
+ accessToken => {
+ // ** Make sure to assign accessToken according to your response.
+ // ** Check: https://pixinvent.ticksy.com/ticket/2413870
+ // ** Change Authorization header
+ originalRequest.headers.Authorization = `${this.jwtConfig.tokenType} ${accessToken}`
+ resolve(axios(originalRequest))
+ },
+ reject
+ )
})
return retryOriginalRequest
}
@@ -72,11 +89,17 @@ export default class JwtService {
}
onAccessTokenFetched(accessToken) {
- this.subscribers = this.subscribers.filter(callback => callback(accessToken))
+ this.subscribers.forEach(({ onSuccess }) => onSuccess(accessToken))
+ this.subscribers = []
+ }
+
+ onAccessTokenFetchFailed(refreshError) {
+ this.subscribers.forEach(({ onFailure }) => onFailure(refreshError))
+ this.subscribers = []
}
- addSubscriber(callback) {
- this.subscribers.push(callback)
+ addSubscriber(onSuccess, onFailure) {
+ this.subscribers.push({ onSuccess, onFailure })
}
getToken() {
diff --git a/src/components/AdminLogo/index.js b/src/components/AdminLogo/index.js
index 4fa7a84..e0b8052 100644
--- a/src/components/AdminLogo/index.js
+++ b/src/components/AdminLogo/index.js
@@ -34,7 +34,7 @@ const LogoSection = ({ sx, to }) => {
...sx,
/* ✅ WCAG 2.4.7 focus indicator */
'&:focus-visible': {
- outline: '3px solid #0C489E',
+ outline: '3px solid #0c489e',
outlineOffset: '2px',
borderRadius: '6px'
}
@@ -42,7 +42,7 @@ const LogoSection = ({ sx, to }) => {
>
- PNSPS
+ PNSPS
diff --git a/src/components/FileDeleteButton.js b/src/components/FileDeleteButton.js
new file mode 100644
index 0000000..21c4f50
--- /dev/null
+++ b/src/components/FileDeleteButton.js
@@ -0,0 +1,27 @@
+import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
+import { IconButton } from '@mui/material';
+import { useIntl } from 'react-intl';
+import { handleActionKeyDown } from 'utils/CommonFunction';
+
+export default function FileDeleteButton({ onDelete, icon: IconComponent = RemoveCircleOutlineIcon, color = 'error' }) {
+ const intl = useIntl();
+ const label = intl.formatMessage({ id: 'ariaDeleteFile' });
+
+ const handleDelete = (event) => {
+ onDelete(event);
+ };
+
+ return (
+ handleActionKeyDown(event, handleDelete)}
+ >
+
+
+ );
+}
diff --git a/src/components/FileList.js b/src/components/FileList.js
index 412649f..48f9156 100644
--- a/src/components/FileList.js
+++ b/src/components/FileList.js
@@ -11,6 +11,8 @@ import { FiDataGrid } from './FiDataGrid';
import {useTheme} from "@emotion/react";
import {useMediaQuery} from "@mui/material";
import {useIntl} from "react-intl";
+import { createDeleteFileColumn } from 'utils/fileListColumns';
+import { CONTAINED_PRIMARY_BLUE } from "themes/colorConst";
// ==============================|| EVENT TABLE ||============================== //
export default function FileList({ refType, refId, allowDelete, sx, dateHideable,lang, ...props }) {
@@ -94,11 +96,10 @@ export default function FileList({ refType, refId, allowDelete, sx, dateHideable
return [
}
+ icon={}
label="Download"
className="textPrimary"
onClick={onDownloadClick(params.id, params.row.skey, params.row.filename)}
- color="primary"
disabled={onDownload}
/>]
},
@@ -143,11 +144,10 @@ export default function FileList({ refType, refId, allowDelete, sx, dateHideable
return [
}
+ icon={}
label="Download"
className="textPrimary"
onClick={onDownloadClick(params.id, params.row.skey, params.row.filename)}
- color="primary"
disabled={onDownload}
/>]
},
@@ -175,24 +175,10 @@ export default function FileList({ refType, refId, allowDelete, sx, dateHideable
if (allowDelete) {
- columns.push({
- field: 'actions',
- type: 'actions',
- headerName: 'Delete',
- width: 100,
- cellClassName: 'actions',
- getActions: (params) => {
- return [
- }
- label="Delete"
- className="textPrimary"
- onClick={onDeleteClick(params.id, params.row.skey, params.row.filename)}
- color="primary"
- />]
- },
- });
+ columns.push(createDeleteFileColumn(
+ (id, row) => onDeleteClick(id, row.skey, row.filename),
+ { icon: Icon.Delete, color: 'primary', width: 100 }
+ ));
}
return (
diff --git a/src/components/MobileLogo/index.js b/src/components/MobileLogo/index.js
index da2ff62..8732c4a 100644
--- a/src/components/MobileLogo/index.js
+++ b/src/components/MobileLogo/index.js
@@ -30,7 +30,7 @@ const LogoSection = ({ sx, to }) => {
/* WCAG 2.4.7 – visible keyboard focus */
'&:focus-visible': {
- outline: '3px solid #0C489E',
+ outline: '3px solid #0c489e',
outlineOffset: '2px',
borderRadius: '6px'
}
diff --git a/src/components/RegisterStepIcon.js b/src/components/RegisterStepIcon.js
new file mode 100644
index 0000000..e143959
--- /dev/null
+++ b/src/components/RegisterStepIcon.js
@@ -0,0 +1,55 @@
+import { Box, useTheme } from '@mui/material';
+
+/** Darker green for completed register steps (matches PAY button hover). */
+export const REGISTER_STEP_COMPLETED_COLOR = '#488F52';
+
+export const registerStepStyle = {
+ width: { lg: '40%', md: '70%', xs: '100%' },
+ boxShadow: 1,
+ backgroundColor: '#FFFFFF',
+ padding: 2,
+ '& .MuiStepLabel-label': { color: '#424242' },
+ '& .MuiStepConnector-line': { borderColor: '#9E9E9E' },
+ '& .MuiStepConnector-root.Mui-completed .MuiStepConnector-line': {
+ borderColor: REGISTER_STEP_COMPLETED_COLOR,
+ },
+ '& .MuiStepConnector-root.Mui-active .MuiStepConnector-line': {
+ borderColor: 'warning.main',
+ },
+};
+
+const RegisterStepIcon = ({ active, completed, className, icon }) => {
+ const theme = useTheme();
+ let circleColor = '#757575';
+ let textColor = '#FFFFFF';
+
+ if (completed) {
+ circleColor = REGISTER_STEP_COMPLETED_COLOR;
+ } else if (active) {
+ circleColor = theme.palette.warning.main;
+ textColor = '#1A1A1A';
+ }
+
+ return (
+
+ {icon}
+
+ );
+};
+
+export default RegisterStepIcon;
diff --git a/src/components/cards/AuthFooter.js b/src/components/cards/AuthFooter.js
index 59e5f8c..ce76960 100644
--- a/src/components/cards/AuthFooter.js
+++ b/src/components/cards/AuthFooter.js
@@ -30,7 +30,7 @@ const AuthFooter = () => {
component="span"
sx={{ color: '#4A4A4A' }}
>
- 2024 ©
+ 2024-{new Date().getFullYear()} ©
doOnClick()} sx={{textTransform: 'none'}} color="iAmSmart" fullWidth={fullWidth} size="large" variant="outlined" startIcon={
}>
-
+
{_label}
diff --git a/src/index.js b/src/index.js
index 44843b0..67e5d9f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -58,7 +58,10 @@ root.render(
-
+
diff --git a/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js b/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js
index 66d0b68..3d3aba6 100644
--- a/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js
+++ b/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js
@@ -64,7 +64,8 @@ const LocaleSelector = () => {
}}
aria-label={intl.formatMessage({id: 'openLanguage'})}
ref={anchorRef}
- aria-controls={open ? 'profile-grow' : undefined}
+ aria-controls={open ? 'locale-selector-menu' : undefined}
+ aria-expanded={open}
aria-haspopup="true"
onClick={handleToggle}
>
@@ -103,6 +104,7 @@ const LocaleSelector = () => {
>