Settings Page Side Navigation

All <Setting*> components will have an ID that corresponds to the sidebar link. When clicked, we scroll to the position of the card by searching for the element with the ID. It behaves simiar to # anchor navigation.

I like this model mainly because in Mobile, users don't need to keep opening the drawer to navigate -- they just need to scroll. This allows us to use the same design for Mobile and App.
This commit is contained in:
infinite-persistence 2021-08-08 16:13:35 +08:00
parent d3fde729f5
commit b3b4e54975
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
13 changed files with 265 additions and 30 deletions

View file

@ -10,6 +10,7 @@ type Props = {
title?: string | Node,
subtitle?: string | Node,
titleActions?: string | Node,
id?: string,
body?: string | Node,
actions?: string | Node,
icon?: string,
@ -30,6 +31,7 @@ export default function Card(props: Props) {
title,
subtitle,
titleActions,
id,
body,
actions,
icon,
@ -53,6 +55,7 @@ export default function Card(props: Props) {
className={classnames(className, 'card', {
'card__multi-pane': Boolean(secondPane),
})}
id={id}
onClick={(e) => {
if (onClick) {
onClick();

View file

@ -2318,6 +2318,23 @@ export const icons = {
<path d="M.75 19.497a3.75 3.75 0 107.5 0 3.75 3.75 0 10-7.5 0zM.75 8.844a11.328 11.328 0 0114.4 14.4M.75 1.113a18.777 18.777 0 0122.139 22.123" />
</g>
),
[ICONS.APPEARANCE]: buildIcon(
<g>
<path d="M16.022,15.624c.3,3.856,6.014,1.2,5.562,2.54-2.525,7.481-12.648,5.685-16.966,1.165A10.9,10.9,0,0,1,4.64,4.04C8.868-.188,16.032-.495,19.928,4.018,27.56,12.858,15.758,12.183,16.022,15.624Z" />
<path d="M5.670 13.309 A1.520 1.520 0 1 0 8.710 13.309 A1.520 1.520 0 1 0 5.670 13.309 Z" />
<path d="M9.430 18.144 A1.520 1.520 0 1 0 12.470 18.144 A1.520 1.520 0 1 0 9.430 18.144 Z" />
<path d="M13.066 5.912 A1.520 1.520 0 1 0 16.106 5.912 A1.520 1.520 0 1 0 13.066 5.912 Z" />
<path d="M6.620 7.524 A1.520 1.520 0 1 0 9.660 7.524 A1.520 1.520 0 1 0 6.620 7.524 Z" />
</g>
),
[ICONS.CONTENT]: buildIcon(
<g>
<path d="M15.750 16.500 A1.500 1.500 0 1 0 18.750 16.500 A1.500 1.500 0 1 0 15.750 16.500 Z" />
<path d="M18.524,10.7l.442,1.453a.994.994,0,0,0,1.174.681l1.472-.341a1.339,1.339,0,0,1,1.275,2.218l-1.031,1.111a1,1,0,0,0,0,1.362l1.031,1.111a1.339,1.339,0,0,1-1.275,2.218l-1.472-.341a.994.994,0,0,0-1.174.681L18.524,22.3a1.33,1.33,0,0,1-2.548,0l-.442-1.453a.994.994,0,0,0-1.174-.681l-1.472.341a1.339,1.339,0,0,1-1.275-2.218l1.031-1.111a1,1,0,0,0,0-1.362l-1.031-1.111a1.339,1.339,0,0,1,1.275-2.218l1.472.341a.994.994,0,0,0,1.174-.681l.442-1.453A1.33,1.33,0,0,1,18.524,10.7Z" />
<path d="M8.25,20.25h-6a1.5,1.5,0,0,1-1.5-1.5V2.25A1.5,1.5,0,0,1,2.25.75H12.879a1.5,1.5,0,0,1,1.06.439l2.872,2.872a1.5,1.5,0,0,1,.439,1.06V6.75" />
<path d="M6.241,12.678a.685.685,0,0,1-.991-.613V7.435a.685.685,0,0,1,.991-.613l4.631,2.316a.684.684,0,0,1,0,1.224Z" />
</g>
),
[ICONS.STAR]: buildIcon(
<g>
<path d="M12.729 1.2l3.346 6.629 6.44.638a.805.805 0 01.5 1.374l-5.3 5.253 1.965 7.138a.813.813 0 01-1.151.935L12 19.934l-6.52 3.229a.813.813 0 01-1.151-.935l1.965-7.138L.99 9.837a.805.805 0 01.5-1.374l6.44-.638L11.271 1.2a.819.819 0 011.458 0z" />

View file

@ -23,6 +23,7 @@ type Props = {
isUpgradeAvailable: boolean,
authPage: boolean,
filePage: boolean,
settingsPage?: boolean,
noHeader: boolean,
noFooter: boolean,
noSideNavigation: boolean,
@ -45,6 +46,7 @@ function Page(props: Props) {
children,
className,
filePage = false,
settingsPage,
authPage = false,
fullWidthPage = false,
noHeader = false,
@ -114,6 +116,7 @@ function Page(props: Props) {
'main--full-width': fullWidthPage,
'main--auth-page': authPage,
'main--file-page': filePage,
'main--settings-page': settingsPage,
'main--markdown': isMarkdown,
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream,
'main--livestream': livestream && !chatDisabled,

View file

@ -1,6 +1,7 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import { SETTINGS_GRP } from 'constants/settings';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
@ -38,6 +39,7 @@ export default function SettingAccount(props: Props) {
return (
<Card
id={SETTINGS_GRP.ACCOUNT}
title={__('Account')}
subtitle=""
isBodyList

View file

@ -1,4 +1,5 @@
// @flow
import { SETTINGS_GRP } from 'constants/settings';
import React from 'react';
import { SETTINGS } from 'lbry-redux';
import Card from 'component/common/card';
@ -24,6 +25,7 @@ export default function SettingAppearance(props: Props) {
return (
<Card
id={SETTINGS_GRP.APPEARANCE}
title={__('Appearance')}
subtitle=""
isBodyList

View file

@ -6,6 +6,7 @@ import { SETTINGS } from 'lbry-redux';
import { Lbryio } from 'lbryinc';
import { SIMPLE_SITE } from 'config';
import * as MODALS from 'constants/modal_types';
import { SETTINGS_GRP } from 'constants/settings';
import Button from 'component/button';
import Card from 'component/common/card';
import { FormField, FormFieldPrice } from 'component/common/form';
@ -52,6 +53,7 @@ export default function SettingContent(props: Props) {
return (
<Card
id={SETTINGS_GRP.CONTENT}
title={__('Content settings')}
subtitle=""
isBodyList

View file

@ -1,5 +1,6 @@
// @flow
import { ALERT } from 'constants/icons';
import { SETTINGS_GRP } from 'constants/settings';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
@ -121,6 +122,7 @@ export default function SettingSystem(props: Props) {
return (
<Card
id={SETTINGS_GRP.SYSTEM}
title={__('System')}
subtitle=""
isBodyList

View file

@ -0,0 +1,3 @@
import SettingsSideNavigation from './view';
export default SettingsSideNavigation;

View file

@ -0,0 +1,158 @@
// @flow
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import { SETTINGS_GRP } from 'constants/settings';
import type { Node } from 'react';
import React from 'react';
import { useHistory } from 'react-router-dom';
import classnames from 'classnames';
import Button from 'component/button';
// @if TARGET='app'
import { IS_MAC } from 'component/app/view';
// @endif
import { useIsMediumScreen } from 'effects/use-screensize';
type SideNavLink = {
title: string,
link?: string,
route?: string,
section?: string,
onClick?: () => any,
icon: string,
extra?: Node,
};
const SIDE_LINKS: Array<SideNavLink> = [
{
title: 'Appearance',
link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.APPEARANCE}`,
section: SETTINGS_GRP.APPEARANCE,
icon: ICONS.APPEARANCE,
},
{
title: 'Account',
link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.ACCOUNT}`,
section: SETTINGS_GRP.ACCOUNT,
icon: ICONS.ACCOUNT,
},
{
title: 'Content settings',
link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.CONTENT}`,
section: SETTINGS_GRP.CONTENT,
icon: ICONS.CONTENT,
},
{
title: 'System',
link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.SYSTEM}`,
section: SETTINGS_GRP.SYSTEM,
icon: ICONS.SETTINGS,
},
];
export default function SettingsSideNavigation() {
const sidebarOpen = true;
const isMediumScreen = useIsMediumScreen();
const isAbsolute = isMediumScreen;
const microNavigation = !sidebarOpen || isMediumScreen;
const { location } = useHistory();
// This sidebar could be called from Settings or from a Settings Sub Page.
// - "#" navigation = don't record to history, just scroll.
// - "/" navigation = record sub-page navigation to history.
const scrollInstead = location.pathname === `/$/${PAGES.SETTINGS}`;
function scrollToSection(section: string) {
const TOP_MARGIN_PX = 20;
const element = document.getElementById(section);
if (element) {
window.scrollTo(0, element.offsetTop - TOP_MARGIN_PX);
}
}
if (isMediumScreen) {
// I think it's ok to hide it for now on medium/small screens given that
// we are using a scrolling Settings Page that displays everything. If we
// really need this, most likely we can display it as a Tab at the top
// of the page.
return null;
}
return (
<div
className={classnames('navigation__wrapper', {
'navigation__wrapper--micro': microNavigation,
'navigation__wrapper--absolute': isAbsolute,
})}
>
<nav
aria-label={'Sidebar'}
className={classnames('navigation', {
'navigation--micro': microNavigation,
// @if TARGET='app'
'navigation--mac': IS_MAC,
// @endif
})}
>
<div>
<ul className={classnames('navigation-links', { 'navigation-links--micro': !sidebarOpen })}>
{SIDE_LINKS.map((linkProps) => {
return (
<li key={linkProps.route || linkProps.link}>
<Button
{...linkProps}
label={__(linkProps.title)}
title={__(linkProps.title)}
navigate={scrollInstead ? undefined : linkProps.route || linkProps.link}
icon={linkProps.icon}
className={classnames('navigation-link', {})}
// $FlowFixMe
onClick={scrollInstead && linkProps.section ? () => scrollToSection(linkProps.section) : undefined}
/>
{linkProps.extra && linkProps.extra}
</li>
);
})}
</ul>
</div>
</nav>
{isMediumScreen && sidebarOpen && (
<>
<nav
className={classnames('navigation--absolute', {
// @if TARGET='app'
'navigation--mac': IS_MAC,
// @endif
})}
>
<div>
<ul className="navigation-links--absolute">
{SIDE_LINKS.map((linkProps) => {
// $FlowFixMe
const { link, route, ...passedProps } = linkProps;
return (
<li key={route || link}>
<Button
{...passedProps}
navigate={scrollInstead ? undefined : route || link}
label={__(linkProps.title)}
title={__(linkProps.title)}
icon={linkProps.icon}
className={classnames('navigation-link', {})}
onClick={
// $FlowFixMe
scrollInstead && linkProps.section ? () => scrollToSection(linkProps.section) : undefined
}
/>
{linkProps.extra && linkProps.extra}
</li>
);
})}
</ul>
</div>
</nav>
</>
)}
</div>
);
}

View file

@ -163,6 +163,8 @@ export const STACK = 'stack';
export const TIME = 'time';
export const GLOBE = 'globe';
export const RSS = 'rss';
export const APPEARANCE = 'Appearance';
export const CONTENT = 'Content';
export const STAR = 'star';
export const MUSIC = 'MusicCategory';
export const BADGE_MOD = 'BadgeMod';

View file

@ -26,3 +26,10 @@ export const ENABLE_SYNC = 'enable_sync';
export const TO_TRAY_WHEN_CLOSED = 'to_tray_when_closed';
export const ENABLE_PUBLISH_PREVIEW = 'enable-publish-preview';
export const DESKTOP_WINDOW_ZOOM = 'desktop_window_zoom';
export const SETTINGS_GRP = {
APPEARANCE: 'appearance',
ACCOUNT: 'account',
CONTENT: 'content',
SYSTEM: 'system',
};

View file

@ -8,6 +8,7 @@ import Page from 'component/page';
import SettingAccount from 'component/settingAccount';
import SettingAppearance from 'component/settingAppearance';
import SettingContent from 'component/settingContent';
import SettingsSideNavigation from 'component/settingsSideNavigation';
import SettingSystem from 'component/settingSystem';
import SettingUnauthenticated from 'component/settingUnauthenticated';
import Yrbl from 'component/yrbl';
@ -40,37 +41,52 @@ class SettingsPage extends React.PureComponent<Props> {
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
return (
<Page noFooter noSideNavigation backout={{ title: __('Settings'), backLabel: __('Done') }} className="card-stack">
{!isAuthenticated && IS_WEB && (
<>
<SettingUnauthenticated />
<div className="main--empty">
<Yrbl
type="happy"
title={__('Sign up for full control')}
subtitle={__('Unlock new buttons that change things.')}
actions={
<div className="section__actions">
<Button button="primary" icon={ICONS.SIGN_UP} label={__('Sign Up')} navigate={`/$/${PAGES.AUTH}`} />
</div>
}
/>
</div>
</>
)}
<Page
noFooter
settingsPage
noSideNavigation
backout={{ title: __('Settings'), backLabel: __('Done') }}
className="card-stack"
>
<SettingsSideNavigation />
{!IS_WEB && noDaemonSettings ? (
<section className="card card--section">
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
</section>
) : (
<div className={classnames('card-stack', { 'card--disabled': IS_WEB && !isAuthenticated })}>
<SettingAppearance />
<SettingAccount />
<SettingContent />
<SettingSystem />
</div>
)}
<div>
{!isAuthenticated && IS_WEB && (
<>
<SettingUnauthenticated />
<div className="main--empty">
<Yrbl
type="happy"
title={__('Sign up for full control')}
subtitle={__('Unlock new buttons that change things.')}
actions={
<div className="section__actions">
<Button
button="primary"
icon={ICONS.SIGN_UP}
label={__('Sign Up')}
navigate={`/$/${PAGES.AUTH}`}
/>
</div>
}
/>
</div>
</>
)}
{!IS_WEB && noDaemonSettings ? (
<section className="card card--section">
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
</section>
) : (
<div className={classnames('card-stack', { 'card--disabled': IS_WEB && !isAuthenticated })}>
<SettingAppearance />
<SettingAccount />
<SettingContent />
<SettingSystem />
</div>
)}
</div>
</Page>
);
}

View file

@ -221,6 +221,24 @@
}
}
.main--settings-page {
width: 100%;
max-width: 70rem;
margin-left: auto;
margin-right: auto;
margin-top: var(--spacing-m);
padding: 0 var(--spacing-m);
.card__subtitle {
margin: 0 0 var(--spacing-s) 0;
font-size: var(--font-small);
}
.button--inverse {
color: var(--color-primary);
}
}
.main--markdown {
flex-direction: column;
}