This is an automated email from the ASF dual-hosted git repository. shuai pushed a commit to branch ui-optimization in repository https://gitbox.apache.org/repos/asf/answer.git
commit 8bc6837b45ba67a0e918064270f94c02670cc160 Author: shuai <[email protected]> AuthorDate: Thu Apr 3 15:59:15 2025 +0800 fix: Optimize sidebar navigation style --- i18n/en_US.yaml | 1 + i18n/zh_CN.yaml | 1 + ui/src/common/constants.ts | 7 + ui/src/common/sideNavLayout.scss | 15 +- ui/src/components/AccordionNav/index.css | 18 ++ ui/src/components/AccordionNav/index.tsx | 11 +- ui/src/components/AdminSideNav/index.scss | 17 ++ ui/src/components/AdminSideNav/index.tsx | 64 ++++++ ui/src/components/Customize/index.tsx | 1 + ui/src/components/Footer/index.tsx | 49 +++-- ui/src/components/Header/index.scss | 7 +- ui/src/components/Header/index.tsx | 311 ++++++++++++++--------------- ui/src/components/MobileSideNav/index.scss | 7 + ui/src/components/MobileSideNav/index.tsx | 26 +++ ui/src/components/SideNav/index.scss | 38 +--- ui/src/components/SideNav/index.tsx | 113 +++++------ ui/src/components/index.ts | 4 + ui/src/pages/Admin/Answers/index.tsx | 6 +- ui/src/pages/Admin/Badges/index.tsx | 20 +- ui/src/pages/Admin/Questions/index.tsx | 6 +- ui/src/pages/Admin/Users/index.tsx | 6 +- ui/src/pages/Admin/index.scss | 5 +- ui/src/pages/Admin/index.tsx | 88 +++----- ui/src/pages/Layout/index.tsx | 3 - ui/src/pages/SideNavLayout/index.tsx | 24 ++- ui/src/stores/sideNav.ts | 8 - ui/template/header.html | 6 +- ui/template/sidenav.html | 2 +- 28 files changed, 470 insertions(+), 394 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 023a4303..12f46a33 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1515,6 +1515,7 @@ ui: display_below: Display below always_display: Always display or: or + back_sites: Back to sites search: title: Search Results keywords: Keywords diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 07c4f658..20c0a24c 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1482,6 +1482,7 @@ ui: display_below: 展示当前 always_display: 一直展示 or: 或者 + back_sites: 返回网站 search: title: 搜索结果 keywords: 关键词 diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 4c2a0168..855d1a24 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -86,20 +86,25 @@ export const ADMIN_LIST_STATUS = { export const ADMIN_NAV_MENUS = [ { name: 'dashboard', + icon: 'speedometer', children: [], }, { name: 'contents', + icon: 'file-earmark-text-fill', children: [{ name: 'questions' }, { name: 'answers' }], }, { name: 'users', + icon: 'people-fill', }, { name: 'badges', + icon: 'award-fill', }, { name: 'customize', + icon: 'palette-fill', children: [ { name: 'themes', @@ -112,6 +117,7 @@ export const ADMIN_NAV_MENUS = [ }, { name: 'settings', + icon: 'gear-fill', children: [ { name: 'general' }, { name: 'interface' }, @@ -127,6 +133,7 @@ export const ADMIN_NAV_MENUS = [ }, { name: 'plugins', + icon: 'plugin', children: [ { name: 'installed_plugins', diff --git a/ui/src/common/sideNavLayout.scss b/ui/src/common/sideNavLayout.scss index 01bab736..db2fd016 100644 --- a/ui/src/common/sideNavLayout.scss +++ b/ui/src/common/sideNavLayout.scss @@ -17,20 +17,29 @@ * under the License. */ -.page-main { - max-width: 70%; +.answer-container { + width: 100%; + min-height: calc(100vh - 95px - 62px); + max-width: 1072px; } + .page-right-side { flex: none; - width: 30%; + width: 300px; + box-sizing: content-box; } // lg @media screen and (max-width: 1199.9px) { + .answer-container { + padding-left: 12px; + padding-right: 12px; + } .page-main { max-width: 100%; } .page-right-side { width: 100%; + box-sizing: border-box; } } diff --git a/ui/src/components/AccordionNav/index.css b/ui/src/components/AccordionNav/index.css index 68e2ba56..524cf0c9 100644 --- a/ui/src/components/AccordionNav/index.css +++ b/ui/src/components/AccordionNav/index.css @@ -24,3 +24,21 @@ .expanding .collapse-indicator { transform: rotate(90deg); } + +#answerAccordion { + max-width: 208px; + .nav-link { + color: var(--an-side-nav-link); + } + .nav-link:focus-visible { + box-shadow: none; + } + .nav-link:hover { + color: var(--an-side-nav-link-hover-color); + background-color: var(--bs-gray-100); + } + .nav-link.active { + color: var(--an-side-nav-link-hover-color); + background-color: var(--bs-gray-200); + } +} diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index d6f84c19..ee081978 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -51,8 +51,10 @@ function MenuNode({ }} className={classNames( 'text-nowrap d-flex flex-nowrap align-items-center w-100', - { expanding, 'link-dark': activeKey !== menu.path }, + { expanding, active: activeKey === menu.path }, )}> + {menu?.icon && <Icon name={menu.icon} className="me-2" />} + <span className="me-auto text-truncate"> {menu.displayName ? menu.displayName : t(menu.name)} </span> @@ -73,8 +75,9 @@ function MenuNode({ }} className={classNames( 'text-nowrap d-flex flex-nowrap align-items-center w-100', - { expanding, 'link-dark': activeKey !== menu.path }, + { expanding, active: activeKey === menu.path }, )}> + {menu?.icon && <Icon name={menu.icon} className="me-2" />} <span className="me-auto text-truncate"> {menu.displayName ? menu.displayName : t(menu.name)} </span> @@ -88,7 +91,7 @@ function MenuNode({ )} {menu.children.length ? ( - <Accordion.Collapse eventKey={menu.path} className="ms-3"> + <Accordion.Collapse eventKey={menu.path} className="ms-4"> <> {menu.children.map((leaf) => { return ( @@ -169,7 +172,7 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => { setOpenKey(getOpenKey()); }, [activeKey, menus]); return ( - <Accordion activeKey={openKey} flush> + <Accordion activeKey={openKey} flush id="answerAccordion"> <Nav variant="pills" className="flex-column" activeKey={activeKey}> {menus.map((li) => { return ( diff --git a/ui/src/components/AdminSideNav/index.scss b/ui/src/components/AdminSideNav/index.scss new file mode 100644 index 00000000..980ffdc9 --- /dev/null +++ b/ui/src/components/AdminSideNav/index.scss @@ -0,0 +1,17 @@ +#sideNav { + max-width: 208px; + .nav-link { + color: var(--an-side-nav-link); + } + .nav-link:focus-visible { + box-shadow: none; + } + .nav-link:hover { + color: var(--an-side-nav-link-hover-color); + background-color: var(--bs-gray-100); + } + .nav-link.active { + color: var(--an-side-nav-link-hover-color); + background-color: var(--bs-gray-200); + } +} diff --git a/ui/src/components/AdminSideNav/index.tsx b/ui/src/components/AdminSideNav/index.tsx new file mode 100644 index 00000000..801a1f16 --- /dev/null +++ b/ui/src/components/AdminSideNav/index.tsx @@ -0,0 +1,64 @@ +import { useEffect } from 'react'; +import { NavLink } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import cloneDeep from 'lodash/cloneDeep'; + +import { AccordionNav, Icon } from '@/components'; +import { ADMIN_NAV_MENUS } from '@/common/constants'; +import { useQueryPlugins } from '@/services'; +import { interfaceStore } from '@/stores'; + +import './index.scss'; + +const AdminSideNav = () => { + const { t } = useTranslation('translation', { keyPrefix: 'btns' }); + const interfaceLang = interfaceStore((_) => _.interface.language); + const { data: configurablePlugins, mutate: updateConfigurablePlugins } = + useQueryPlugins({ + status: 'active', + have_config: true, + }); + + const menus = cloneDeep(ADMIN_NAV_MENUS); + if (configurablePlugins && configurablePlugins.length > 0) { + menus.forEach((item) => { + if (item.name === 'plugins' && item.children) { + item.children = [ + ...item.children, + ...configurablePlugins.map((plugin) => ({ + name: plugin.slug_name, + displayName: plugin.name, + })), + ]; + } + }); + } + + const observePlugins = (evt) => { + if (evt.data.msgType === 'refreshConfigurablePlugins') { + updateConfigurablePlugins(); + } + }; + useEffect(() => { + window.addEventListener('message', observePlugins); + return () => { + window.removeEventListener('message', observePlugins); + }; + }, []); + useEffect(() => { + updateConfigurablePlugins(); + }, [interfaceLang]); + + return ( + <div id="adminSideNav"> + <NavLink to="/" className="pb-3 d-inline-block link-secondary"> + <Icon name="arrow-left" className="me-2" /> + <span>{t('back_sites')}</span> + </NavLink> + <AccordionNav menus={menus} path="/admin/" /> + </div> + ); +}; + +export default AdminSideNav; diff --git a/ui/src/components/Customize/index.tsx b/ui/src/components/Customize/index.tsx index c02db675..0ac4156d 100644 --- a/ui/src/components/Customize/index.tsx +++ b/ui/src/components/Customize/index.tsx @@ -109,6 +109,7 @@ const handleCustomHeader = (content) => { }; const handleCustomFooter = (content) => { + console.log('handleCustomFooter', content); const el = document.body; renderCustomArea(el, CUSTOM_MARK_FOOTER, 'beforeend', content); }; diff --git a/ui/src/components/Footer/index.tsx b/ui/src/components/Footer/index.tsx index a61ebe60..4e4b3aa0 100644 --- a/ui/src/components/Footer/index.tsx +++ b/ui/src/components/Footer/index.tsx @@ -19,7 +19,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Container } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; @@ -33,32 +32,30 @@ const Index = () => { const cc = `${fullYear} ${siteName}`; return ( - <footer className="py-3 bg-light"> - <Container> - <p className="text-center mb-0 small"> - {/* Link to Terms of Service with right margin */} - <Link to="/tos" className="me-3"> - {t('label', { keyPrefix: 'admin.legal.terms_of_service' })} - </Link> + <footer className="py-3 bg-light w-100"> + <p className="text-center mb-0 small"> + {/* Link to Terms of Service with right margin */} + <Link to="/tos" className="me-3"> + {t('label', { keyPrefix: 'admin.legal.terms_of_service' })} + </Link> - {/* Link to Privacy Policy with right margin for spacing */} - <Link to="/privacy"> - {t('label', { keyPrefix: 'admin.legal.privacy_policy' })} - </Link> - </p> - <p className="text-center mb-0 small"> - <Trans i18nKey="footer.build_on" values={{ cc }}> - Powered by - {/* eslint-disable-next-line react/jsx-no-target-blank */} - <a href="https://answer.apache.org" target="_blank"> - Apache Answer - </a> - - the open-source software that powers Q&A communities. - <br /> - Made with love. © 2022 Answer. - </Trans> - </p> - </Container> + {/* Link to Privacy Policy with right margin for spacing */} + <Link to="/privacy"> + {t('label', { keyPrefix: 'admin.legal.privacy_policy' })} + </Link> + </p> + <p className="text-center mb-0 small"> + <Trans i18nKey="footer.build_on" values={{ cc }}> + Powered by + {/* eslint-disable-next-line react/jsx-no-target-blank */} + <a href="https://answer.apache.org" target="_blank"> + Apache Answer + </a> + - the open-source software that powers Q&A communities. + <br /> + Made with love. © 2022 Answer. + </Trans> + </p> </footer> ); }; diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 822c15bd..b6943eb5 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -20,6 +20,7 @@ @import 'bootstrap/scss/functions'; @import 'bootstrap/scss/variables'; #header { + z-index: 1041; transform: translate3d(0, 0, 0); --bs-navbar-padding-y: 0.75rem; background: var(--bs-primary); @@ -46,7 +47,7 @@ box-shadow: none; } - .lg-none { + .xl-none { display: none !important; } @@ -111,13 +112,13 @@ } } -@media (max-width: 991.9px) { +@media (max-width: 1199.9px) { #header { .nav-grow { flex-grow: 1 !important; } - .lg-none { + .xl-none { display: flex !important; } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 28e8370f..a1928b36 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -18,14 +18,7 @@ */ import { FC, memo, useState, useEffect } from 'react'; -import { - Navbar, - Container, - Nav, - Form, - FormControl, - Col, -} from 'react-bootstrap'; +import { Navbar, Nav, Form, FormControl, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSearchParams, @@ -47,7 +40,7 @@ import { sideNavStore, } from '@/stores'; import { logout, useQueryNotificationStatus } from '@/services'; -import { Icon } from '@/components'; +import { Icon, MobileSideNav } from '@/components'; import NavItems from './components/NavItems'; @@ -64,8 +57,9 @@ const Header: FC = () => { const siteInfo = siteInfoStore((state) => state.siteInfo); const brandingInfo = brandingStore((state) => state.branding); const loginSetting = loginSettingStore((state) => state.login); - const { updateReview, updateVisible } = sideNavStore(); + const { updateReview } = sideNavStore(); const { data: redDot } = useQueryNotificationStatus(); + const [showMobileSideNav, setShowMobileSideNav] = useState(false); /** * Automatically append `tag` information when creating a question */ @@ -108,18 +102,11 @@ const Header: FC = () => { }, [q]); useEffect(() => { - const collapse = document.querySelector('#navBarContent'); - if (collapse && collapse.classList.contains('show')) { - const toggle = document.querySelector('#navBarToggle') as HTMLElement; - if (toggle) { - toggle?.click(); - } - } - // clear search input when navigate to other page if (location.pathname !== '/search' && searchStr) { setSearch(''); } + setShowMobileSideNav(false); }, [location.pathname]); let navbarStyle = 'theme-colored'; @@ -128,159 +115,165 @@ const Header: FC = () => { navbarStyle = `theme-${theme_config[theme].navbar_style}`; } + useEffect(() => { + const handleResize = () => { + if (window.innerWidth >= 1199.9) { + setShowMobileSideNav(false); + } + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + return ( - <Navbar - variant={navbarStyle === 'theme-colored' ? 'dark' : ''} - expand="lg" - className={classnames('sticky-top', navbarStyle)} - id="header"> - <Container className="d-flex align-items-center"> - <Navbar.Toggle - aria-controls="navBarContent" - className="answer-navBar me-2" - id="navBarToggle" - onClick={() => { - updateVisible(); - }} - /> + <> + <Navbar + variant={navbarStyle === 'theme-colored' ? 'dark' : ''} + expand="xl" + className={classnames('sticky-top', navbarStyle)} + id="header"> + <div className="w-100 d-flex align-items-center px-3"> + <Navbar.Toggle + className="answer-navBar me-2" + onClick={() => { + setShowMobileSideNav(!showMobileSideNav); + }} + /> - <div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap"> - <Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-5 p-0"> - {brandingInfo.logo ? ( - <> - <img - className="d-none d-lg-block logo me-0" - src={brandingInfo.logo} - alt={siteInfo.name} - /> + <div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap"> + <Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-5 p-0"> + {brandingInfo.logo ? ( + <> + <img + className="d-none d-xl-block logo me-0" + src={brandingInfo.logo} + alt={siteInfo.name} + /> - <img - className="lg-none logo me-0" - src={brandingInfo.mobile_logo || brandingInfo.logo} - alt={siteInfo.name} - /> - </> - ) : ( - <span>{siteInfo.name}</span> - )} - </Navbar.Brand> + <img + className="xl-none logo me-0" + src={brandingInfo.mobile_logo || brandingInfo.logo} + alt={siteInfo.name} + /> + </> + ) : ( + <span>{siteInfo.name}</span> + )} + </Navbar.Brand> - {/* mobile nav */} - <div className="d-flex lg-none align-items-center flex-lg-nowrap"> - {user?.username ? ( - <NavItems - redDot={redDot} - userInfo={user} - logOut={(e) => handleLogout(e)} - /> - ) : ( - <> - <Link - className={classnames('me-2 btn btn-link', { - 'link-light': navbarStyle === 'theme-colored', - 'link-primary': navbarStyle !== 'theme-colored', - })} - onClick={() => floppyNavigation.storageLoginRedirect()} - to={userCenter.getLoginUrl()}> - {t('btns.login')} - </Link> - {loginSetting.allow_new_registrations && ( + {/* mobile nav */} + <div className="d-flex xl-none align-items-center flex-lg-nowrap"> + {user?.username ? ( + <NavItems + redDot={redDot} + userInfo={user} + logOut={(e) => handleLogout(e)} + /> + ) : ( + <> <Link - className={classnames( - 'btn', - navbarStyle === 'theme-colored' - ? 'btn-light' - : 'btn-primary', - )} - to={userCenter.getSignUpUrl()}> - {t('btns.signup')} + className={classnames('me-2 btn btn-link', { + 'link-light': navbarStyle === 'theme-colored', + 'link-primary': navbarStyle !== 'theme-colored', + })} + onClick={() => floppyNavigation.storageLoginRedirect()} + to={userCenter.getLoginUrl()}> + {t('btns.login')} </Link> - )} - </> - )} + {loginSetting.allow_new_registrations && ( + <Link + className={classnames( + 'btn', + navbarStyle === 'theme-colored' + ? 'btn-light' + : 'btn-primary', + )} + to={userCenter.getSignUpUrl()}> + {t('btns.signup')} + </Link> + )} + </> + )} + </div> </div> - </div> - <Navbar.Collapse id="navBarContent" className="me-auto"> - <hr className="hr lg-none mb-3" style={{ marginTop: '12px' }} /> - <Col lg={8} className="ps-0"> - <Form - action="/search" - className="w-100 maxw-400 position-relative" - onSubmit={handleSearch}> - <div className="search-wrap" onClick={handleSearch}> - <Icon name="search" className="search-icon" /> - </div> - <FormControl - type="search" - placeholder={t('header.search.placeholder')} - className="placeholder-search" - value={searchStr} - name="q" - onChange={(e) => handleInput(e.target.value)} - /> - </Form> - </Col> + <div className="d-none d-xl-flex flex-grow-1 me-auto"> + <Col lg={8} className="d-none d-xl-block ps-0"> + <Form + action="/search" + className="w-100 maxw-400 position-relative" + onSubmit={handleSearch}> + <div className="search-wrap" onClick={handleSearch}> + <Icon name="search" className="search-icon" /> + </div> + <FormControl + type="search" + placeholder="sddfsdf" + className="placeholder-search" + value={searchStr} + name="q" + onChange={(e) => handleInput(e.target.value)} + /> + </Form> + </Col> - <Nav.Item className="lg-none mt-3 pb-1"> - <Link - to={askUrl} - className="text-capitalize text-nowrap btn btn-light"> - {t('btns.add_question')} - </Link> - </Nav.Item> - {/* pc nav */} - <Col - lg={4} - className="d-none d-lg-flex justify-content-start justify-content-sm-end"> - {user?.username ? ( - <Nav className="d-flex align-items-center flex-lg-nowrap"> - <Nav.Item className="me-3"> - <Link - to={askUrl} - className={classnames('text-capitalize text-nowrap btn', { - 'btn-light': navbarStyle !== 'theme-light', - 'btn-primary': navbarStyle === 'theme-light', - })}> - {t('btns.add_question')} - </Link> - </Nav.Item> + {/* pc nav */} + <Col + lg={4} + className="d-none d-xl-flex justify-content-start justify-content-sm-end"> + {user?.username ? ( + <Nav className="d-flex align-items-center flex-lg-nowrap"> + <Nav.Item className="me-3"> + <Link + to={askUrl} + className={classnames('text-capitalize text-nowrap btn', { + 'btn-light': navbarStyle !== 'theme-light', + 'btn-primary': navbarStyle === 'theme-light', + })}> + {t('btns.add_question')} + </Link> + </Nav.Item> - <NavItems - redDot={redDot} - userInfo={user} - logOut={handleLogout} - /> - </Nav> - ) : ( - <> - <Link - className={classnames('me-2 btn btn-link', { - 'link-light': navbarStyle === 'theme-colored', - 'link-primary': navbarStyle !== 'theme-colored', - })} - onClick={() => floppyNavigation.storageLoginRedirect()} - to={userCenter.getLoginUrl()}> - {t('btns.login')} - </Link> - {loginSetting.allow_new_registrations && ( + <NavItems + redDot={redDot} + userInfo={user} + logOut={handleLogout} + /> + </Nav> + ) : ( + <> <Link - className={classnames( - 'btn', - navbarStyle === 'theme-colored' - ? 'btn-light' - : 'btn-primary', - )} - to={userCenter.getSignUpUrl()}> - {t('btns.signup')} + className={classnames('me-2 btn btn-link', { + 'link-light': navbarStyle === 'theme-colored', + 'link-primary': navbarStyle !== 'theme-colored', + })} + onClick={() => floppyNavigation.storageLoginRedirect()} + to={userCenter.getLoginUrl()}> + {t('btns.login')} </Link> - )} - </> - )} - </Col> - </Navbar.Collapse> - </Container> - </Navbar> + {loginSetting.allow_new_registrations && ( + <Link + className={classnames( + 'btn', + navbarStyle === 'theme-colored' + ? 'btn-light' + : 'btn-primary', + )} + to={userCenter.getSignUpUrl()}> + {t('btns.signup')} + </Link> + )} + </> + )} + </Col> + </div> + </div> + </Navbar> + <MobileSideNav show={showMobileSideNav} onHide={setShowMobileSideNav} /> + </> ); }; diff --git a/ui/src/components/MobileSideNav/index.scss b/ui/src/components/MobileSideNav/index.scss new file mode 100644 index 00000000..fbb7c945 --- /dev/null +++ b/ui/src/components/MobileSideNav/index.scss @@ -0,0 +1,7 @@ +#mobileSideNav { + top: 60px !important; + width: 240px !important; + height: calc(100vh - 60px) !important; + overflow-y: auto; + flex: none; +} diff --git a/ui/src/components/MobileSideNav/index.tsx b/ui/src/components/MobileSideNav/index.tsx new file mode 100644 index 00000000..b0ff81a8 --- /dev/null +++ b/ui/src/components/MobileSideNav/index.tsx @@ -0,0 +1,26 @@ +import { Offcanvas } from 'react-bootstrap'; +import { useLocation } from 'react-router-dom'; + +import { SideNav, AdminSideNav } from '@/components'; + +import './index.scss'; + +const MobileSideNav = ({ show, onHide }) => { + const { pathname } = useLocation(); + const isAdmin = pathname.includes('/admin'); + return ( + <Offcanvas + show={show} + onHide={() => { + onHide(false); + }} + id="mobileSideNav" + className="px-3 py-4"> + <Offcanvas.Body className="p-0"> + {isAdmin ? <AdminSideNav /> : <SideNav />} + </Offcanvas.Body> + </Offcanvas> + ); +}; + +export default MobileSideNav; diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss index 1fb0e7b1..0f386561 100644 --- a/ui/src/components/SideNav/index.scss +++ b/ui/src/components/SideNav/index.scss @@ -17,19 +17,17 @@ * under the License. */ +#pcSideNav { + width: 240px; + top: 62px; + box-sizing: border-box; + height: calc(100vh - 62px); + overflow-y: auto; + flex: none; +} + #sideNav { - .nav-wrap { - position: sticky; - width: auto; - top: 62px; - box-sizing: border-box; - max-height: calc(100vh - 62px); - overflow-y: auto; - margin-bottom: 8px; - } - .nav { - max-width: calc(100% - 24px); - } + max-width: 208px; .nav-link { color: var(--an-side-nav-link); } @@ -44,26 +42,12 @@ color: var(--an-side-nav-link-hover-color); background-color: var(--bs-gray-200); } - .side-nav-right-line { - position: absolute; - top: 0; - right: 12px; - width: 1px; - height: 100%; - background-color: var(--bs-border-color); - } } @media screen and (max-width: 991.9px) { - #sideNav { - .nav-wrap { - max-height: fit-content; - } + .side-left-nav-wrap { .nav { max-width: 100%; } - .side-nav-right-line { - display: none; - } } } diff --git a/ui/src/components/SideNav/index.tsx b/ui/src/components/SideNav/index.tsx index 4af827f5..e3f04f8f 100644 --- a/ui/src/components/SideNav/index.tsx +++ b/ui/src/components/SideNav/index.tsx @@ -18,12 +18,10 @@ */ import { FC } from 'react'; -import { Col, Nav } from 'react-bootstrap'; +import { Nav } from 'react-bootstrap'; import { NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; - import { loggedUserInfoStore, sideNavStore } from '@/stores'; import { Icon } from '@/components'; import './index.scss'; @@ -32,75 +30,62 @@ const Index: FC = () => { const { t } = useTranslation(); const { pathname } = useLocation(); const { user: userInfo } = loggedUserInfoStore(); - const { visible, can_revision, revision } = sideNavStore(); + const { can_revision, revision } = sideNavStore(); return ( - <Col - xl={2} - lg={3} - md={12} - className={classnames( - 'position-relative', - visible ? '' : 'd-none d-lg-block', - )} - id="sideNav"> - <div className="nav-wrap pt-4"> - <Nav variant="pills" className="flex-column"> - <NavLink - to="/questions" - className={({ isActive }) => - isActive || pathname === '/' ? 'nav-link active' : 'nav-link' - }> - <Icon name="question-circle-fill" className="me-2" /> - <span>{t('header.nav.question')}</span> - </NavLink> + <Nav variant="pills" className="flex-column" id="sideNav"> + <NavLink + to="/questions" + className={({ isActive }) => + isActive || pathname === '/' ? 'nav-link active' : 'nav-link' + }> + <Icon name="question-circle-fill" className="me-2" /> + <span>{t('header.nav.question')}</span> + </NavLink> - <NavLink - to="/tags" - className={() => - pathname === '/tags' ? 'nav-link active' : 'nav-link' - }> - <Icon name="tags-fill" className="me-2" /> - <span>{t('header.nav.tag')}</span> - </NavLink> + <NavLink + to="/tags" + className={() => + pathname === '/tags' ? 'nav-link active' : 'nav-link' + }> + <Icon name="tags-fill" className="me-2" /> + <span>{t('header.nav.tag')}</span> + </NavLink> - <NavLink to="/users" className="nav-link"> - <Icon name="people-fill" className="me-2" /> - <span>{t('header.nav.user')}</span> - </NavLink> + <NavLink to="/users" className="nav-link"> + <Icon name="people-fill" className="me-2" /> + <span>{t('header.nav.user')}</span> + </NavLink> - <NavLink to="/badges" className="nav-link"> - <Icon name="award-fill" className="me-2" /> - <span>{t('header.nav.badges')}</span> - </NavLink> + <NavLink to="/badges" className="nav-link"> + <Icon name="award-fill" className="me-2" /> + <span>{t('header.nav.badges')}</span> + </NavLink> - {can_revision || userInfo?.role_id === 2 ? ( - <> - <div className="py-2 px-3 mt-3 small fw-bold"> - {t('header.nav.moderation')} - </div> - {can_revision && ( - <NavLink to="/review" className="nav-link"> - <Icon name="shield-fill-check" className="me-2" /> - <span>{t('header.nav.review')}</span> - <span className="float-end"> - {revision > 99 ? '99+' : revision > 0 ? revision : ''} - </span> - </NavLink> - )} + {can_revision || userInfo?.role_id === 2 ? ( + <> + <div className="py-2 px-3 mt-3 small fw-bold"> + {t('header.nav.moderation')} + </div> + {can_revision && ( + <NavLink to="/review" className="nav-link"> + <Icon name="shield-fill-check" className="me-2" /> + <span>{t('header.nav.review')}</span> + <span className="float-end"> + {revision > 99 ? '99+' : revision > 0 ? revision : ''} + </span> + </NavLink> + )} - {userInfo?.role_id === 2 ? ( - <NavLink to="/admin" className="nav-link"> - <Icon name="gear-fill" className="me-2" /> - <span>{t('header.nav.admin')}</span> - </NavLink> - ) : null} - </> + {userInfo?.role_id === 2 ? ( + <NavLink to="/admin" className="nav-link"> + <Icon name="gear-fill" className="me-2" /> + <span>{t('header.nav.admin')}</span> + </NavLink> ) : null} - </Nav> - </div> - <div className="side-nav-right-line" /> - </Col> + </> + ) : null} + </Nav> ); }; diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 400c6b6f..68e863d2 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -62,6 +62,8 @@ import PluginRender from './PluginRender'; import HighlightText from './HighlightText'; import CardBadge from './CardBadge'; import PinList from './PinList'; +import MobileSideNav from './MobileSideNav'; +import AdminSideNav from './AdminSideNav'; export { Avatar, @@ -111,5 +113,7 @@ export { HighlightText, CardBadge, PinList, + MobileSideNav, + AdminSideNav, }; export type { EditorRef, JSONSchema, UISchema }; diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 9f8e3cde..07190024 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -97,8 +97,8 @@ const Answers: FC = () => { return ( <> <h3 className="mb-4">{t('page_title')}</h3> - <div className="d-flex flex-wrap justify-content-between align-items-center mb-3"> - <Stack direction="horizontal" gap={3}> + <div className="d-flex flex-wrap justify-content-between align-items-center"> + <Stack direction="horizontal" gap={3} className="mb-3"> <QueryGroup data={answerFilterItems} currentSort={curFilter} @@ -122,7 +122,7 @@ const Answers: FC = () => { type="search" placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} - className="mt-3 mt-sm-0" + className="mb-3" /> </div> <Table responsive="md"> diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 63b8f348..9b99f8eb 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -78,8 +78,8 @@ const Badges: FC = () => { return ( <> <h3 className="mb-4">{t('title')}</h3> - <div className="d-flex flex-wrap justify-content-between align-items-center mb-3"> - <Stack direction="horizontal" gap={3}> + <div className="d-flex flex-wrap justify-content-between align-items-center"> + <Stack direction="horizontal" gap={3} className="mb-3"> <QueryGroup data={BadgeFilterKeys} currentSort={curFilter} @@ -95,19 +95,19 @@ const Badges: FC = () => { onChange={handleFilter} placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} - className="mt-3 mt-sm-0" + className="mb-3" /> </div> <Table responsive="md"> <thead> <tr> - <th>{t('name')}</th> - <th>{t('group')}</th> - <th>{t('awards')}</th> - - <th>{t('status')}</th> - - <th className="text-end">{t('action')}</th> + <th className="min-w-15">{t('name')}</th> + <th style={{ width: '19%' }}>{t('group')}</th> + <th style={{ width: '14%' }}>{t('awards')}</th> + <th style={{ width: '12%' }}>{t('status')}</th> + <th className="text-end" style={{ width: '9%' }}> + {t('action')} + </th> </tr> </thead> <tbody className="align-middle"> diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 58dd0559..494effe6 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -95,8 +95,8 @@ const Questions: FC = () => { return ( <> <h3 className="mb-4">{t('page_title')}</h3> - <div className="d-flex flex-wrap justify-content-between align-items-center mb-3"> - <Stack direction="horizontal" gap={3}> + <div className="d-flex flex-wrap justify-content-between align-items-center"> + <Stack direction="horizontal" gap={3} className="mb-3"> <QueryGroup data={questionFilterItems} currentSort={curFilter} @@ -120,7 +120,7 @@ const Questions: FC = () => { placeholder={t('filter.placeholder')} onChange={handleFilter} style={{ width: '12.25rem' }} - className="mt-3 mt-sm-0" + className="mb-3" /> </div> <Table responsive="md"> diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index fd9c81f3..2402a839 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -190,8 +190,8 @@ const Users: FC = () => { return ( <> <h3 className="mb-4">{t('title')}</h3> - <div className="d-flex flex-wrap justify-content-between align-items-center mb-3"> - <Stack direction="horizontal" gap={3}> + <div className="d-flex flex-wrap justify-content-between align-items-center"> + <Stack direction="horizontal" gap={3} className="mb-3"> <QueryGroup data={UserFilterKeys} currentSort={curFilter} @@ -223,7 +223,7 @@ const Users: FC = () => { onChange={handleFilter} placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} - className="mt-3 mt-sm-0" + className="mb-3" /> </div> <Table responsive="md"> diff --git a/ui/src/pages/Admin/index.scss b/ui/src/pages/Admin/index.scss index 27a9af01..a84961f5 100644 --- a/ui/src/pages/Admin/index.scss +++ b/ui/src/pages/Admin/index.scss @@ -18,8 +18,9 @@ */ .admin-container { - padding-top: 1.5rem; - padding-bottom: 1.5rem; + .page-right-side { + box-sizing: border-box; + } } .min-w-15 { diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 5da0780d..1c89333c 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -17,19 +17,15 @@ * under the License. */ -import { FC, useEffect } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; +import { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { Row, Col } from 'react-bootstrap'; import { Outlet, useMatch } from 'react-router-dom'; -import cloneDeep from 'lodash/cloneDeep'; - import { usePageTags } from '@/hooks'; -import { AccordionNav } from '@/components'; -import { ADMIN_NAV_MENUS } from '@/common/constants'; -import { useQueryPlugins } from '@/services'; -import { interfaceStore } from '@/stores'; +import { AdminSideNav, Footer } from '@/components'; +import '@/common/sideNavLayout.scss'; import './index.scss'; const g10Paths = [ @@ -46,66 +42,32 @@ const Index: FC = () => { const pathMatch = useMatch('/admin/:path'); const curPath = pathMatch?.params.path || 'dashboard'; - const interfaceLang = interfaceStore((_) => _.interface.language); - const { data: configurablePlugins, mutate: updateConfigurablePlugins } = - useQueryPlugins({ - status: 'active', - have_config: true, - }); - - const menus = cloneDeep(ADMIN_NAV_MENUS); - if (configurablePlugins && configurablePlugins.length > 0) { - menus.forEach((item) => { - if (item.name === 'plugins' && item.children) { - item.children = [ - ...item.children, - ...configurablePlugins.map((plugin) => ({ - name: plugin.slug_name, - displayName: plugin.name, - })), - ]; - } - }); - } - - const observePlugins = (evt) => { - if (evt.data.msgType === 'refreshConfigurablePlugins') { - updateConfigurablePlugins(); - } - }; - useEffect(() => { - window.addEventListener('message', observePlugins); - return () => { - window.removeEventListener('message', observePlugins); - }; - }, []); - useEffect(() => { - updateConfigurablePlugins(); - }, [interfaceLang]); - usePageTags({ title: t('admin'), }); return ( - <> - <div className="bg-light py-2"> - <Container className="py-1"> - <h6 className="mb-0 fw-bold lh-base"> - {t('title', { keyPrefix: 'admin.admin_header' })} - </h6> - </Container> + <div className="admin-container d-flex"> + <div + className="position-sticky px-3 border-end pt-4 d-none d-xl-block" + id="pcSideNav"> + <AdminSideNav /> + </div> + <div className="flex-fill w-100"> + <div className="d-flex justify-content-center px-0 px-md-4"> + <div className="answer-container"> + <Row className="py-4"> + <Col className="page-main flex-auto"> + <Outlet /> + </Col> + {g10Paths.find((v) => curPath === v) ? null : ( + <Col className="page-right-side" /> + )} + </Row> + </div> + </div> + <Footer /> </div> - <Container className="admin-container"> - <Row> - <Col lg={2}> - <AccordionNav menus={menus} path="/admin/" /> - </Col> - <Col lg={g10Paths.find((v) => curPath === v) ? 10 : 6}> - <Outlet /> - </Col> - </Row> - </Container> - </> + </div> ); }; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index f3384d9c..984e13f4 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -31,7 +31,6 @@ import { } from '@/stores'; import { Header, - Footer, Toast, Customize, CustomizeTheme, @@ -210,7 +209,6 @@ const Layout: FC = () => { revalidateOnFocus: false, }}> <Header /> - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} <div className="position-relative page-wrap d-flex flex-column flex-fill"> {httpStatusCode ? ( <HttpErrorContent httpCode={httpStatusCode} /> @@ -219,7 +217,6 @@ const Layout: FC = () => { )} </div> <Toast msg={toastMsg} variant={variant} onClose={closeToast} /> - <Footer /> <Customize /> <LoginToContinueModal visible={showLoginToContinueModal} /> <BadgeModal diff --git a/ui/src/pages/SideNavLayout/index.tsx b/ui/src/pages/SideNavLayout/index.tsx index 773d572c..90f3f3b1 100644 --- a/ui/src/pages/SideNavLayout/index.tsx +++ b/ui/src/pages/SideNavLayout/index.tsx @@ -18,23 +18,29 @@ */ import { FC, memo } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; import { Outlet } from 'react-router-dom'; -import { SideNav } from '@/components'; +import { SideNav, Footer } from '@/components'; import '@/common/sideNavLayout.scss'; const Index: FC = () => { return ( - <Container className="d-flex flex-column flex-fill"> - <Row className="flex-fill"> + <div className="d-flex"> + <div + className="position-sticky px-3 border-end pt-4 d-none d-xl-block" + id="pcSideNav"> <SideNav /> - <Col xl={10} lg={9} md={12}> - <Outlet /> - </Col> - </Row> - </Container> + </div> + <div className="flex-fill w-100"> + <div className="d-flex justify-content-center px-0 px-md-4"> + <div className="answer-container"> + <Outlet /> + </div> + </div> + <Footer /> + </div> + </div> ); }; diff --git a/ui/src/stores/sideNav.ts b/ui/src/stores/sideNav.ts index fd164ee1..0cb0e919 100644 --- a/ui/src/stores/sideNav.ts +++ b/ui/src/stores/sideNav.ts @@ -25,22 +25,14 @@ type reviewData = { }; interface ErrorCodeType { - visible: boolean; can_revision: boolean; revision: number; - updateVisible: () => void; updateReview: (params: reviewData) => void; } const Index = create<ErrorCodeType>((set) => ({ - visible: false, can_revision: false, revision: 0, - updateVisible: () => { - set((state) => { - return { visible: !state.visible }; - }); - }, updateReview: (params: reviewData) => { set(() => { return { ...params }; diff --git a/ui/template/header.html b/ui/template/header.html index e3f63509..329365fd 100644 --- a/ui/template/header.html +++ b/ui/template/header.html @@ -169,16 +169,16 @@ {{end}} </div> <div class="me-auto navbar-collapse collapse" id="navBarContent"> - <hr class="hr lg-none mb-2" style="margin-top: 12px" /> + <hr class="hr xl-none mb-2" style="margin-top: 12px" /> <div class="ps-0 col-lg-8"> <form action="/search" class="w-100 maxw-400"> <input placeholder="Search" name="q" type="search" class="placeholder-search form-control" value=""> </form> </div> - <!-- <div class="lg-none mt-3 pb-1 nav-item"> + <!-- <div class="xl-none mt-3 pb-1 nav-item"> <a class="text-capitalize text-nowrap btn btn-light" href="/questions/ask">{{translator $.language "ui.btns.add_question"}}</a> </div> --> - <div class="d-none d-lg-flex justify-content-start justify-content-sm-end col-lg-4"> + <div class="d-none d-xl-flex justify-content-start justify-content-sm-end col-lg-4"> <!-- <a role="button" tabindex="0" href="/users/login" class="me-2 link-light btn btn-link"> {{translator $.language "ui.btns.login"}} </a> diff --git a/ui/template/sidenav.html b/ui/template/sidenav.html index 3191770a..aabf6308 100644 --- a/ui/template/sidenav.html +++ b/ui/template/sidenav.html @@ -19,7 +19,7 @@ --> {{define "sidenav"}} -<div id="sideNav" class="position-relative d-none d-lg-block col-xl-2 col-lg-3 col-md-12"> +<div id="sideNav" class="position-relative d-none d-xl-block col-xl-2 col-lg-3 col-md-12"> <div class="nav-wrap pt-4"> <div class="flex-column nav nav-pills"> <a class="nav-link active" href="{{$.baseURL}}/questions">
