diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 38c57658..4612e6e0 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1020,9 +1020,11 @@ ui: answers: answers accepted: Accepted page_404: + http_error: HTTP Error 404 desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: + http_error: HTTP Error 500 desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: diff --git a/ui/src/pages/404/index.jsx b/ui/src/pages/404/index.jsx index d2aa8a85..6cdb4df4 100644 --- a/ui/src/pages/404/index.jsx +++ b/ui/src/pages/404/index.jsx @@ -1,17 +1,32 @@ +import { useEffect } from 'react'; import { Container, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import './index.scss'; + const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_404' }); + useEffect(() => { + // auto height of container + const pageWrap = document.querySelector('.page-wrap'); + pageWrap.style.display = 'contents'; + + return () => { + pageWrap.style.display = 'block'; + }; + }, []); return ( - <Container className="d-flex flex-column justify-content-center align-items-center page-wrap"> + <Container + className="d-flex flex-column justify-content-center align-items-center" + style={{ flex: 1 }}> <div className="mb-4 text-secondary" style={{ fontSize: '120px', lineHeight: 1.2 }}> (=‘x‘=) </div> - <div className="text-center mb-4">{t('desc')}</div> + <h4 className="text-center mb-2">{t('http_error')}</h4> + <div className="text-center mb-3">{t('desc')}</div> <div className="text-center"> <Button as={Link} to="/" variant="link"> {t('back_home')} diff --git a/ui/src/pages/404/index.scss b/ui/src/pages/404/index.scss new file mode 100644 index 00000000..b0d8dd2c --- /dev/null +++ b/ui/src/pages/404/index.scss @@ -0,0 +1,4 @@ +.page-wrap { + display: flex; + flex: 1; +} diff --git a/ui/src/pages/50X/index.jsx b/ui/src/pages/50X/index.jsx index 744c403c..50e1f235 100644 --- a/ui/src/pages/50X/index.jsx +++ b/ui/src/pages/50X/index.jsx @@ -1,9 +1,19 @@ +import { useEffect } from 'react'; import { Container, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_50X' }); + useEffect(() => { + // auto height of container + const pageWrap = document.querySelector('.page-wrap'); + pageWrap.style.display = 'contents'; + + return () => { + pageWrap.style.display = 'block'; + }; + }, []); return ( <Container className="d-flex flex-column justify-content-center align-items-center page-wrap"> <div @@ -11,6 +21,8 @@ const Index = () => { style={{ fontSize: '120px', lineHeight: 1.2 }}> (=T^T=) </div> + + <h4 className="text-center mb-2">{t('http_error')}</h4> <div className="text-center mb-3">{t('desc')}</div> <div className="text-center"> <Button as={Link} to="/" variant="link"> diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index dd26db2d..88e0120c 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -1,10 +1,10 @@ -import { FC, memo } from 'react'; -import { Outlet } from 'react-router-dom'; +import { FC, memo, useEffect } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { toastStore, loginToContinueStore } from '@/stores'; +import { toastStore, loginToContinueStore, notFoundStore } from '@/stores'; import { Header, Footer, @@ -15,14 +15,22 @@ import { } from '@/components'; import { LoginToContinueModal } from '@/components/Modal'; import { useImgViewer } from '@/hooks'; +import Component404 from '@/pages/404'; const Layout: FC = () => { + const location = useLocation(); const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const closeToast = () => { toastClear(); }; + const { visible: show404, hide: notFoundHide } = notFoundStore(); + const imgViewer = useImgViewer(); const { show: showLoginToContinueModal } = loginToContinueStore(); + + useEffect(() => { + notFoundHide(); + }, [location]); return ( <HelmetProvider> <PageTags /> @@ -36,7 +44,7 @@ const Layout: FC = () => { <div className="position-relative page-wrap" onClick={imgViewer.checkClickForImgView}> - <Outlet /> + {show404 ? <Component404 /> : <Outlet />} </div> <Toast msg={toastMsg} variant={variant} onClose={closeToast} /> <Footer /> diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index 8d40afec..662e0e7a 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -11,6 +11,7 @@ import { usePersonalTop, usePersonalListByTabName, } from '@/services'; +import type { UserInfoRes } from '@/common/interface'; import { UserInfo, @@ -47,8 +48,8 @@ const Personal: FC = () => { tabName, ); let pageTitle = ''; - if (userInfo && userInfo.info && userInfo.has) { - pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`; + if (userInfo?.username) { + pageTitle = `${userInfo?.display_name} (${userInfo?.username})`; } const { count = 0, list = [] } = listData?.[tabName] || {}; usePageTags({ @@ -57,11 +58,11 @@ const Personal: FC = () => { return ( <Container className="pt-4 mt-2 mb-5"> <Row className="justify-content-center"> - {userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && ( - <Alert data={userInfo?.info.status_msg} /> + {userInfo?.status !== 'normal' && userInfo?.status_msg && ( + <Alert data={userInfo?.status_msg} /> )} <Col xxl={7} lg={8} sm={12}> - <UserInfo data={userInfo?.info} /> + <UserInfo data={userInfo as UserInfoRes} /> </Col> <Col xxl={3} @@ -88,11 +89,11 @@ const Personal: FC = () => { <Col xxl={7} lg={8} sm={12}> <Overview visible={tabName === 'overview'} - introduction={userInfo?.info?.bio_html} + introduction={userInfo?.bio_html || ''} data={topData} /> <ListHead - count={tabName === 'reputation' ? userInfo?.info?.rank : count} + count={tabName === 'reputation' ? Number(userInfo?.rank) : count} sort={order} visible={tabName !== 'overview'} tabName={tabName} @@ -120,17 +121,14 @@ const Personal: FC = () => { </Col> <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <h5 className="mb-3">{t('stats')}</h5> - {userInfo?.info && ( + {userInfo?.created_at && ( <> <div className="text-secondary"> - <FormatTime - time={userInfo.info.created_at} - preFix={t('joined')} - /> + <FormatTime time={userInfo.created_at} preFix={t('joined')} /> </div> <div className="text-secondary"> <FormatTime - time={userInfo.info.last_login_date} + time={userInfo.last_login_date} preFix={t('last_login')} /> </div> diff --git a/ui/src/services/client/personal.ts b/ui/src/services/client/personal.ts index 6b61aaba..d6b978c8 100644 --- a/ui/src/services/client/personal.ts +++ b/ui/src/services/client/personal.ts @@ -8,7 +8,10 @@ export const usePersonalInfoByName = (username: string) => { const apiUrl = '/answer/api/v1/personal/user/info'; const { data, error, mutate } = useSWR<Type.UserInfoRes, Error>( username ? `${apiUrl}?username=${username}` : null, - request.instance.get, + (url) => + request.get(url, { + allow404: true, + }), ); return { data, diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index b8540aee..af92140e 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -47,7 +47,9 @@ export const useTagInfo = ({ id = '', name = '' }) => { name = encodeURIComponent(name); apiUrl = `/answer/api/v1/tag?name=${name}`; } - const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get); + const { data, error } = useSWR<Type.TagInfo>(apiUrl, (url) => + request.get(url, { allow404: true }), + ); return { data, isLoading: !data && !error, diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 6d013aad..a7b41552 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -171,6 +171,7 @@ export const saveQuestion = (params: Type.QuestionParams) => { export const questionDetail = (id: string) => { return request.get<Type.QuestionDetailRes>( `/answer/api/v1/question/info?id=${id}`, + { allow404: true }, ); }; diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index 91b70f52..47dc0712 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -10,6 +10,7 @@ import pageTagStore from './pageTags'; import customizeStore from './customize'; import themeSettingStore from './themeSetting'; import loginToContinueStore from './loginToContinue'; +import notFoundStore from './notFound'; export { toastStore, @@ -23,4 +24,5 @@ export { themeSettingStore, seoSettingStore, loginToContinueStore, + notFoundStore, }; diff --git a/ui/src/stores/notFound.ts b/ui/src/stores/notFound.ts new file mode 100644 index 00000000..35c361af --- /dev/null +++ b/ui/src/stores/notFound.ts @@ -0,0 +1,23 @@ +import create from 'zustand'; + +interface NotFoundType { + visible: boolean; + show: () => void; + hide: () => void; +} + +const notFound = create<NotFoundType>((set) => ({ + visible: false, + show: () => { + set(() => { + return { visible: true }; + }); + }, + hide: () => { + set(() => { + return { visible: false }; + }); + }, +})); + +export default notFound; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 615cc8a3..df411410 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@/components'; -import { loggedUserInfoStore, toastStore } from '@/stores'; +import { loggedUserInfoStore, toastStore, notFoundStore } from '@/stores'; import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; import { getCurrentLang } from '@/utils/localize'; @@ -15,6 +15,10 @@ const baseConfig = { withCredentials: true, }; +interface APIconfig extends AxiosRequestConfig { + allow404: boolean; +} + class Request { instance: AxiosInstance; @@ -49,6 +53,9 @@ class Request { (error) => { const { status, data: respData } = error.response || {}; const { data = {}, msg = '', reason = '' } = respData || {}; + + console.log('response error:', error); + if (status === 400) { // show error message if (data instanceof Object && data.err_type) { @@ -135,6 +142,11 @@ class Request { } return Promise.reject(false); } + + if (status === 404 && error.config?.allow404) { + notFoundStore.getState().show(); + return Promise.reject(false); + } if (status >= 500) { console.error( `Request failed with status code ${status}, ${msg || ''}`, @@ -149,7 +161,7 @@ class Request { return this.instance.request(config); } - public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> { + public get<T = any>(url: string, config?: APIconfig): Promise<T> { return this.instance.get(url, config); }