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:
parent
d3fde729f5
commit
b3b4e54975
13 changed files with 265 additions and 30 deletions
|
@ -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();
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
3
ui/component/settingsSideNavigation/index.js
Normal file
3
ui/component/settingsSideNavigation/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import SettingsSideNavigation from './view';
|
||||
|
||||
export default SettingsSideNavigation;
|
158
ui/component/settingsSideNavigation/view.jsx
Normal file
158
ui/component/settingsSideNavigation/view.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue