feat(web): settings to keep the search open and hide home pickers; collapsible menu sections
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m44s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m43s
ci / ts / ts (packages + web) (push) Successful in 1m16s
ci / api / api (push) Successful in 2m27s
ci / db / db (push) Successful in 2m23s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m44s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m43s
ci / ts / ts (packages + web) (push) Successful in 1m16s
ci / api / api (push) Successful in 2m27s
ci / db / db (push) Successful in 2m23s
- appearance settings get "collapse search" (off keeps the field always open) and separate switches for the activity and mood pickers on the home page; stored in ui_prefs (search_collapse, home_activities, home_moods) - Collection and Admin headers in the sidebar fold their items; the state is remembered in the browser, a folded Admin shows the review queue badge; icon-only and phone layouts always show every item Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
73e642c1ec
commit
b27b915b63
13 files changed
+470
-284
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { unwrap } from '@pipu/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link, Outlet, useNavigate } from '@tanstack/react-router';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuthState, useSession, useUser } from '../app/session';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
dislikeIcon,
|
||||
settingsIcon,
|
||||
androidIcon,
|
||||
chevronRightIcon,
|
||||
storageFillIcon,
|
||||
storageIcon, inboxFillIcon, inboxIcon, myUploadsFillIcon, myUploadsIcon, uploadFillIcon, uploadIcon, tuneFillIcon, tuneIcon} from '../shared/ui/icons';
|
||||
import { Avatar, cx, Icon, Logo, Menu } from '../shared/ui/ui';
|
||||
@@ -76,6 +78,42 @@ function NavLink({
|
||||
);
|
||||
}
|
||||
|
||||
const FOLDED_KEY = 'pipu.nav.folded';
|
||||
|
||||
function readFolded(): string[] {
|
||||
try {
|
||||
const v: unknown = JSON.parse(localStorage.getItem(FOLDED_KEY) ?? '[]');
|
||||
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Раздел меню: заголовок сворачивает пункты; состояние помнит браузер. Свёрнутый раздел показывает сумму бейджей. */
|
||||
function NavSection({ id, title, badge, children }: { id: string; title: string; badge?: number; children: ReactNode }) {
|
||||
const [folded, setFolded] = useState(() => readFolded().includes(id));
|
||||
const toggle = () => {
|
||||
const next = !folded;
|
||||
setFolded(next);
|
||||
try {
|
||||
const rest = readFolded().filter((x) => x !== id);
|
||||
localStorage.setItem(FOLDED_KEY, JSON.stringify(next ? [...rest, id] : rest));
|
||||
} catch {
|
||||
// Без хранилища раздел просто не запомнит состояние.
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<button type="button" className={s.navSection} aria-expanded={!folded} 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, folded && s.navGroupFolded)}>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Между выходом и редиректом на /login каркас не рисуется: пользователя уже нет. */
|
||||
export function AppShell() {
|
||||
return useAuthState().status === 'signed_in' ? (
|
||||
@@ -94,7 +132,7 @@ function Shell() {
|
||||
const staff = user.role !== 'user';
|
||||
const island = user.ui_prefs.web_player === 'island';
|
||||
const searchAt = user.ui_prefs.search_position ?? 'center';
|
||||
const search = <SearchBox align={searchAt} />;
|
||||
const search = <SearchBox align={searchAt} collapsible={user.ui_prefs.search_collapse !== false} />;
|
||||
const counts = useCollectionCounts();
|
||||
const pending = usePendingUploads(user.role === 'admin');
|
||||
|
||||
@@ -138,19 +176,19 @@ function Shell() {
|
||||
|
||||
<nav className={s.side} aria-label={t('nav.main')}>
|
||||
<NavLink to="/" label={t('nav.home')} icon={homeIcon} activeIcon={homeFillIcon} />
|
||||
<div className={s.navSection}>{t('nav.collection')}</div>
|
||||
<NavLink to="/collection/history" label={t('nav.history')} icon={historyIcon} activeIcon={historyFillIcon} />
|
||||
<NavLink to="/collection/likes" label={t('nav.likes')} icon={likeIcon} activeIcon={likeFillIcon} count={counts.likes} />
|
||||
<NavLink
|
||||
to="/collection/dislikes"
|
||||
label={t('nav.dislikes')}
|
||||
icon={dislikeIcon}
|
||||
activeIcon={dislikeFillIcon}
|
||||
count={counts.dislikes}
|
||||
/>
|
||||
<NavSection id="collection" title={t('nav.collection')}>
|
||||
<NavLink to="/collection/history" label={t('nav.history')} icon={historyIcon} activeIcon={historyFillIcon} />
|
||||
<NavLink to="/collection/likes" label={t('nav.likes')} icon={likeIcon} activeIcon={likeFillIcon} count={counts.likes} />
|
||||
<NavLink
|
||||
to="/collection/dislikes"
|
||||
label={t('nav.dislikes')}
|
||||
icon={dislikeIcon}
|
||||
activeIcon={dislikeFillIcon}
|
||||
count={counts.dislikes}
|
||||
/>
|
||||
</NavSection>
|
||||
{staff && (
|
||||
<>
|
||||
<div className={s.navSection}>{t('nav.admin')}</div>
|
||||
<NavSection id="admin" title={t('nav.admin')} badge={pending}>
|
||||
<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} />
|
||||
@@ -171,7 +209,7 @@ function Shell() {
|
||||
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</NavSection>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
|
||||
Reference in new issue
Block a user