fix: Optimize sidebar navigation style

This commit is contained in:
shuai
2025-04-03 15:59:15 +08:00
parent 1fcf0363e2
commit 8bc6837b45
28 changed files with 472 additions and 396 deletions
+1
View File
@@ -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
+1
View File
@@ -1482,6 +1482,7 @@ ui:
display_below: 展示当前
always_display: 一直展示
or: 或者
back_sites: 返回网站
search:
title: 搜索结果
keywords: 关键词
+7
View File
@@ -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',
+12 -3
View File
@@ -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;
}
}
+18
View File
@@ -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);
}
}
+7 -4
View File
@@ -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 (
+17
View File
@@ -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);
}
}
+64
View File
@@ -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;
+1
View File
@@ -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);
};
+23 -26
View File
@@ -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>
);
};
+4 -3
View File
@@ -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;
}
+154 -161
View File
@@ -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>
{/* 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 && (
<Link
className={classnames(
'btn',
navbarStyle === 'theme-colored'
? 'btn-light'
: 'btn-primary',
)}
to={userCenter.getSignUpUrl()}>
{t('btns.signup')}
</Link>
)}
</>
)}
</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>
<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>
<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 xl-none align-items-center flex-lg-nowrap">
{user?.username ? (
<NavItems
redDot={redDot}
userInfo={user}
logOut={handleLogout}
logOut={(e) => handleLogout(e)}
/>
</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 && (
) : (
<>
<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>
)}
</>
)}
</div>
</div>
<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>
{/* 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 && (
<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} />
</>
);
};
@@ -0,0 +1,7 @@
#mobileSideNav {
top: 60px !important;
width: 240px !important;
height: calc(100vh - 60px) !important;
overflow-y: auto;
flex: none;
}
+26
View File
@@ -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;
+11 -27
View File
@@ -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;
}
}
}
+49 -64
View File
@@ -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>
);
};
+4
View File
@@ -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 };
+3 -3
View File
@@ -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">
+10 -10
View File
@@ -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">
+3 -3
View File
@@ -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">
+3 -3
View File
@@ -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">
+3 -2
View File
@@ -18,8 +18,9 @@
*/
.admin-container {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
.page-right-side {
box-sizing: border-box;
}
}
.min-w-15 {
+25 -63
View File
@@ -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>
<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 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>
</div>
);
};
-3
View File
@@ -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
+15 -9
View File
@@ -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>
);
};
-8
View File
@@ -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 };
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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">