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.
 
 

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