feat(admin): group navigation into media, uploads and system
ci / ts / ts (packages + web) (push) Failing after 4m33s
release / android (APK в релиз) (push) Successful in 4m45s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m35s
release / release (версия и заметки) (push) Successful in 8s
release / desktop (Linux и Windows в релиз) (push) Successful in 12m14s
release / prune (старые файлы и прогоны) (push) Successful in 12s
ci / ts / ts (packages + web) (push) Failing after 4m33s
release / android (APK в релиз) (push) Successful in 4m45s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m35s
release / release (версия и заметки) (push) Successful in 8s
release / desktop (Linux и Windows в релиз) (push) Successful in 12m14s
release / prune (старые файлы и прогоны) (push) Successful in 12s
This commit is contained in:
1 parent
17bb0e1846
commit
66d3897f66
6 files changed
+156
-27
No files matched your search
@@ -170,6 +170,22 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navSubsection {
|
||||
min-height: 40px;
|
||||
margin: 2px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.navSubgroup > .navItem {
|
||||
margin-left: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -295,6 +311,9 @@
|
||||
.navGroupFolded {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.navSubsection { min-height: 0; }
|
||||
.navSubgroup > .navItem { margin-left: 0; padding-left: 0; border-left: 0; }
|
||||
}
|
||||
|
||||
.mobileBottom { display: none; }
|
||||
@@ -358,6 +377,8 @@
|
||||
border: 0; pointer-events: none;
|
||||
}
|
||||
.mobileMenu .navSectionTitle { display: block; }
|
||||
.mobileMenu .navSubsection { color: var(--text2); font-size: 13px; }
|
||||
.mobileMenu .navSubgroup > .navItem { margin-left: 12px; border-left: 1px solid var(--line); }
|
||||
.mobileMenu .navChevron { display: none; }
|
||||
.content, .island .content { padding: 20px 16px 24px; }
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { queueIcon } from '../shared/ui/icons';
|
||||
import { unwrap } from '@pipu/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link, Outlet, useNavigate, useRouterState } from '@tanstack/react-router';
|
||||
import { Suspense, useCallback, useState, type ReactNode } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useId, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuthState, useSession, useUser } from '../app/session';
|
||||
@@ -110,6 +110,8 @@ function NavSection({
|
||||
children,
|
||||
defaultFolded = false,
|
||||
mobile = false,
|
||||
nested = false,
|
||||
paths,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -117,12 +119,21 @@ function NavSection({
|
||||
children: ReactNode;
|
||||
defaultFolded?: boolean;
|
||||
mobile?: boolean;
|
||||
nested?: boolean;
|
||||
paths?: readonly string[];
|
||||
}) {
|
||||
const groupId = useId();
|
||||
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
||||
const active = paths?.some((path) => pathname === path || pathname.startsWith(`${path}/`)) ?? false;
|
||||
const [folded, setFolded] = useState(() => {
|
||||
if (active) return false;
|
||||
if (readIds(FOLDED_KEY).includes(id)) return true;
|
||||
if (readIds(TOUCHED_KEY).includes(id)) return false;
|
||||
return defaultFolded;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (active) setFolded(false);
|
||||
}, [active, pathname]);
|
||||
const toggle = () => {
|
||||
const next = !folded;
|
||||
setFolded(next);
|
||||
@@ -138,15 +149,15 @@ function NavSection({
|
||||
return (
|
||||
<>
|
||||
{mobile ? (
|
||||
<h3 className={s.navSection}><span className={s.navSectionTitle}>{title}</span></h3>
|
||||
<h3 className={cx(s.navSection, nested && s.navSubsection)}><span className={s.navSectionTitle}>{title}</span></h3>
|
||||
) : (
|
||||
<button type="button" className={s.navSection} aria-expanded={!folded} onClick={toggle}>
|
||||
<button type="button" className={cx(s.navSection, nested && s.navSubsection)} aria-expanded={!folded} aria-controls={groupId} onClick={toggle}>
|
||||
<span className={s.navSectionTitle}>{title}</span>
|
||||
{folded && badge ? <span className={s.navBadge}>{badge > 99 ? '99+' : badge}</span> : null}
|
||||
<Icon src={chevronRightIcon} size={16} className={cx(s.navChevron, !folded && s.navChevronOpen)} />
|
||||
</button>
|
||||
)}
|
||||
<div className={cx(s.navGroup, !mobile && folded && s.navGroupFolded)}>{children}</div>
|
||||
<div id={groupId} role="group" aria-label={title} className={cx(s.navGroup, nested && s.navSubgroup, !mobile && folded && s.navGroupFolded)}>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -178,6 +189,7 @@ function Shell() {
|
||||
const reviews = user.reviews_uploads;
|
||||
const pending = usePendingUploads(reviews);
|
||||
const unseen = useUnseenUploads(staff);
|
||||
const uploadBadge = (reviews ? pending ?? 0 : 0) + (unseen ?? 0);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const closeMobile = useCallback(() => setMobileOpen(false), []);
|
||||
const navigation = (mobile = false) => (
|
||||
@@ -208,30 +220,38 @@ function Shell() {
|
||||
)}
|
||||
{!staff && user.manages_updates && <NavLink to="/admin/updates" label={t('nav.server_updates')} icon={downloadIcon} activeIcon={downloadIcon} />}
|
||||
{staff && (
|
||||
<NavSection mobile={mobile} id="admin" title={t('nav.admin')} badge={reviews ? pending : undefined} defaultFolded>
|
||||
<NavSection mobile={mobile} id="admin" title={t('nav.admin')} badge={uploadBadge} paths={['/admin', '/uploads']} defaultFolded>
|
||||
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
||||
{user.role === 'admin' && (
|
||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||
)}
|
||||
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
|
||||
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
|
||||
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
|
||||
{reviews && (
|
||||
<NavLink
|
||||
to="/admin/uploads"
|
||||
label={t('nav.upload_queue')}
|
||||
icon={inboxIcon}
|
||||
activeIcon={inboxFillIcon}
|
||||
badge={pending}
|
||||
/>
|
||||
)}
|
||||
{(user.role === 'admin' || user.manages_updates) && <NavLink to="/admin/updates" label={t('nav.server_updates')} icon={downloadIcon} activeIcon={downloadIcon} />}
|
||||
{user.role === 'admin' && (
|
||||
<>
|
||||
<NavLink to="/admin/config" label={t('nav.server_config')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
||||
</>
|
||||
<NavSection mobile={mobile} nested id="admin-media" title={t('nav.admin_media')} paths={['/admin/library', '/admin/sources']} defaultFolded>
|
||||
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
|
||||
{user.role === 'admin' && (
|
||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||
)}
|
||||
</NavSection>
|
||||
<NavSection mobile={mobile} nested id="admin-uploads" title={t('nav.admin_uploads')} badge={uploadBadge} paths={['/uploads', '/admin/uploads']} defaultFolded>
|
||||
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
|
||||
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
|
||||
{reviews && (
|
||||
<NavLink
|
||||
to="/admin/uploads"
|
||||
label={t('nav.upload_queue')}
|
||||
icon={inboxIcon}
|
||||
activeIcon={inboxFillIcon}
|
||||
badge={pending}
|
||||
/>
|
||||
)}
|
||||
</NavSection>
|
||||
{(user.role === 'admin' || user.manages_updates) && (
|
||||
<NavSection mobile={mobile} nested id="admin-system" title={t('nav.admin_system')} paths={['/admin/updates', '/admin/config', '/admin/settings', '/admin/audit']} defaultFolded>
|
||||
<NavLink to="/admin/updates" label={t('nav.server_updates')} icon={downloadIcon} activeIcon={downloadIcon} />
|
||||
{user.role === 'admin' && (
|
||||
<>
|
||||
<NavLink to="/admin/config" label={t('nav.server_config')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
||||
</>
|
||||
)}
|
||||
</NavSection>
|
||||
)}
|
||||
</NavSection>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user