You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

297 lines
9.8 KiB

  1. import PropTypes from 'prop-types';
  2. import React, { useContext, useEffect, useMemo, useState } from 'react';
  3. import { Box, Grid, Typography, Dialog, DialogContent, IconButton } from '@mui/material';
  4. import CloseIcon from '@mui/icons-material/Close';
  5. import Loadable from 'components/Loadable';
  6. import { lazy } from 'react';
  7. import { FormattedMessage, useIntl } from 'react-intl';
  8. import { checkSysEnv, checkPaymentSuspension, checkIsOnlyOnlinePayment } from 'utils/Utils';
  9. import backbroundImg from 'assets/images/bg_ml.jpg';
  10. import lgceImg from 'assets/images/2025_lgce.jpg';
  11. import 'assets/style/loginStyles.css';
  12. import { SysContext } from 'components/SysSettingProvider';
  13. import SafeHtml from 'components/SafeHtml';
  14. const AuthCard = Loadable(lazy(() => import('./AuthCardCustom')));
  15. const BackgroundHead = {
  16. backgroundImage: `url(${backbroundImg})`,
  17. width: '100%',
  18. height: '100%',
  19. backgroundSize: 'cover'
  20. };
  21. const parseToDate = (v) => {
  22. if (v == null || v === "") return null;
  23. // timestamp: 10 digits (sec) or 13 digits (ms)
  24. const s = String(v).trim();
  25. if (/^\d{10,13}$/.test(s)) {
  26. const n = Number(s);
  27. return new Date(s.length === 10 ? n * 1000 : n);
  28. }
  29. // date-only: YYYY-MM-DD -> treat as LOCAL midnight
  30. const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
  31. if (m) {
  32. const y = Number(m[1]);
  33. const mo = Number(m[2]) - 1; // 0-based
  34. const d = Number(m[3]);
  35. return new Date(y, mo, d, 0, 0, 0, 0);
  36. }
  37. // otherwise: ISO datetime or other formats
  38. const d = new Date(s);
  39. return Number.isNaN(d.getTime()) ? null : d;
  40. };
  41. const AuthWrapper = ({ children }) => {
  42. const intl = useIntl();
  43. const { sysSetting } = useContext(SysContext);
  44. // ===== Popup #1 (image popup) =====
  45. const today = new Date();
  46. const showUntil = new Date('2025-11-27T00:00:00');
  47. const [openImagePopup, setOpenImagePopup] = useState(today < showUntil);
  48. const handleCloseImagePopup = () => setOpenImagePopup(false);
  49. // ===== Popup #2 (system popup) =====
  50. const [openNoticePopup, setOpenNoticePopup] = useState(false);
  51. const popupStart = useMemo(() => parseToDate(sysSetting?.loginPopupStart), [sysSetting?.loginPopupStart]);
  52. const popupEnd = useMemo(() => parseToDate(sysSetting?.loginPopupEnd), [sysSetting?.loginPopupEnd]);
  53. const popupHtml = useMemo(() => {
  54. return intl.formatMessage({ id: 'loginPopupHtml', defaultMessage: '' }) || '';
  55. }, [intl]);
  56. useEffect(() => {
  57. const now = new Date();
  58. const inRange =
  59. (popupStart ? now >= popupStart : true) &&
  60. (popupEnd ? now <= popupEnd : true);
  61. const hasMessage = popupHtml && popupHtml.trim().length > 0;
  62. setOpenNoticePopup(Boolean(hasMessage && inRange));
  63. }, [popupHtml, popupStart, popupEnd]);
  64. const handleCloseNoticePopup = () => {
  65. setOpenNoticePopup(false);
  66. };
  67. const isOnlyOnline = checkIsOnlyOnlinePayment();
  68. return (
  69. <Box sx={{ minHeight: '87vh' }}>
  70. {/* =========================
  71. Popup #1: Image popup
  72. ========================= */}
  73. <Dialog
  74. open={openImagePopup}
  75. onClose={handleCloseImagePopup}
  76. aria-labelledby="election-promo-title"
  77. maxWidth="md"
  78. PaperProps={{
  79. sx: {
  80. borderRadius: 2,
  81. overflow: 'hidden',
  82. boxShadow: 6
  83. }
  84. }}
  85. >
  86. <Box sx={{ position: 'relative' }}>
  87. <IconButton
  88. aria-label={intl.formatMessage({ id: 'close' })}
  89. onClick={handleCloseImagePopup}
  90. sx={{
  91. position: 'absolute',
  92. top: 6,
  93. right: 6,
  94. zIndex: 1,
  95. bgcolor: 'rgba(255,255,255,0.8)',
  96. '&:hover': { bgcolor: 'rgba(255,255,255,1)' }
  97. }}
  98. >
  99. <CloseIcon />
  100. </IconButton>
  101. <DialogContent sx={{ p: 0 }}>
  102. <Box
  103. component="img"
  104. src={lgceImg}
  105. alt={intl.formatMessage({ id: 'lgce_alt', defaultMessage: '2025 Legislative Council General Election' })}
  106. title={intl.formatMessage({ id: 'lgce_title', defaultMessage: '2025 Legislative Council General Election' })}
  107. sx={{ display: 'block', width: '100%', maxWidth: '720px', height: 'auto' }}
  108. />
  109. </DialogContent>
  110. </Box>
  111. </Dialog>
  112. {/* =========================
  113. Popup #2: system popup
  114. ========================= */}
  115. <Dialog
  116. open={openNoticePopup}
  117. onClose={handleCloseNoticePopup}
  118. maxWidth="md"
  119. PaperProps={{
  120. sx: {
  121. borderRadius: 1,
  122. overflow: 'hidden',
  123. width: { xs: '95vw', md: 900 },
  124. maxWidth: '95vw',
  125. boxShadow: 10,
  126. border: '1px solid #0c489e'
  127. }
  128. }}
  129. >
  130. <IconButton
  131. aria-label={intl.formatMessage({ id: 'close' })}
  132. onClick={handleCloseNoticePopup}
  133. sx={{
  134. position: 'absolute',
  135. top: 8,
  136. right: 8,
  137. zIndex: 10,
  138. width: 34,
  139. height: 34,
  140. border: '1px solid rgba(0,0,0,0.5)',
  141. borderRadius: 0,
  142. bgcolor: '#fff'
  143. }}
  144. >
  145. <CloseIcon />
  146. </IconButton>
  147. <DialogContent
  148. sx={{
  149. bgcolor: '#d9ecff',
  150. p: 3,
  151. maxHeight: { xs: '70vh', md: '60vh' },
  152. overflowY: 'auto',
  153. '& h1, & h2, & h3': { marginTop: 0, marginBottom: '12px' },
  154. '& p': { marginBottom: '12px', lineHeight: 1.7 }
  155. }}
  156. >
  157. <Typography component="div" sx={{ fontSize: 18, lineHeight: 1.8 }}>
  158. <SafeHtml html={popupHtml} />
  159. </Typography>
  160. </DialogContent>
  161. </Dialog>
  162. {/* =========================
  163. Page content
  164. ========================= */}
  165. <div style={BackgroundHead}>
  166. <Grid
  167. container
  168. direction="row"
  169. justifyContent="space-between"
  170. alignItems="center"
  171. sx={{
  172. minHeight: { xs: '100vh', md: '87vh' },
  173. flexDirection: { xs: 'column', md: 'row' }
  174. }}
  175. >
  176. <Grid item xs={12} md={8} lg={8} xl={8} sx={{ flexShrink: { xs: 0, md: 1 } }}>
  177. <Grid
  178. container
  179. direction="column"
  180. justifyContent={{ xs: 'center', md: 'flex-start' }}
  181. alignItems={{ xs: 'center', md: 'flex-start' }}
  182. sx={{ minHeight: { xs: '45vh', md: 'calc(87vh)' } }}
  183. >
  184. <Grid item xs={12} sx={{ px: { xs: 2 }, pl: { md: 4 }, pt: { xs: 4, sm: 2, md: 1.5 }, pb: { xs: 4, sm: 0 } }}>
  185. {checkPaymentSuspension() ? (
  186. <Typography component="div" sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }}>
  187. <SafeHtml html={intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' })} />
  188. </Typography>
  189. ) : (
  190. <Typography component="div" sx={{ textAlign: 'start', p: 1.5 }}>
  191. <SafeHtml
  192. html={intl.formatMessage({
  193. id: isOnlyOnline ? 'landingMessage' : 'homePageHeaderMessage',
  194. defaultMessage: ''
  195. })}
  196. />
  197. </Typography>
  198. )}
  199. </Grid>
  200. <Grid
  201. container
  202. direction="column"
  203. justifyContent="flex-start"
  204. alignItems="center"
  205. spacing={2}
  206. sx={{ minHeight: { md: 'calc(50vh)' } }}
  207. >
  208. <Grid item xs={12} sx={{ mt: { sm: 2, md: 12 }, display: { xs: 'none', sm: 'block' } }}>
  209. <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}>
  210. <FormattedMessage id="HKSARGOV" />
  211. </Typography>
  212. <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}>
  213. <FormattedMessage id="Gazette" />
  214. </Typography>
  215. <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}>
  216. <FormattedMessage id="PNSPS_fullname" />
  217. </Typography>
  218. {checkSysEnv() === 'uat' ? (
  219. <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}>
  220. User Acceptance Test Environment
  221. </Typography>
  222. ) : checkSysEnv() === 'dev' ? (
  223. <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}>
  224. Development Environment
  225. </Typography>
  226. ) : (
  227. ''
  228. )}
  229. </Grid>
  230. </Grid>
  231. </Grid>
  232. </Grid>
  233. <Grid
  234. item
  235. xs={12}
  236. md={4}
  237. lg={4}
  238. xl={4}
  239. sx={{
  240. flex: { xs: '1 1 0%', md: '0 0 auto' },
  241. minHeight: { xs: 0, md: 'auto' },
  242. display: { xs: 'flex', md: 'block' },
  243. alignItems: { xs: 'center', md: 'stretch' },
  244. justifyContent: { xs: 'center', md: 'flex-start' }
  245. }}
  246. >
  247. <Grid
  248. container
  249. justifyContent="center"
  250. alignItems="center"
  251. sx={{
  252. minHeight: { xs: '100%', md: 'calc(90vh - 112px)' },
  253. height: { xs: '100%', md: 'auto' },
  254. width: { xs: '100%', md: 'auto' }
  255. }}
  256. >
  257. <Grid item xs={12} md={11} lg={11} xl={11}>
  258. <AuthCard>{children}</AuthCard>
  259. </Grid>
  260. </Grid>
  261. </Grid>
  262. </Grid>
  263. </div>
  264. </Box>
  265. );
  266. };
  267. AuthWrapper.propTypes = {
  268. children: PropTypes.node
  269. };
  270. export default AuthWrapper;