Added mobile-friendly menu (#5)

This commit is contained in:
Przemek Wiech
2020-03-31 23:42:31 +02:00
parent 46d0607707
commit d21858547b
2 changed files with 376 additions and 172 deletions

View File

@@ -23,6 +23,7 @@ import {
Search, Search,
SearchProps, SearchProps,
SearchResultProps, SearchResultProps,
Responsive,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
enum WikiTreeLoginState { enum WikiTreeLoginState {
@@ -31,6 +32,11 @@ enum WikiTreeLoginState {
LOGGED_IN, LOGGED_IN,
} }
enum ScreenSize {
LARGE,
SMALL,
}
/** Menus and dialogs state. */ /** Menus and dialogs state. */
interface State { interface State {
loadUrlDialogOpen: boolean; loadUrlDialogOpen: boolean;
@@ -319,126 +325,208 @@ export class TopBar extends React.Component<
); );
} }
private chartMenus() { private search() {
return (
<Search
onSearchChange={debounce(
(_: React.MouseEvent<HTMLElement>, data: SearchProps) =>
this.handleSearch(data.value),
200,
)}
onResultSelect={(_, data) => this.handleResultSelect(data.result.id)}
results={this.state.searchResults}
noResultsMessage={this.context.intl.formatMessage({
id: 'menu.search.no_results',
defaultMessage: 'No results found',
})}
placeholder={this.context.intl.formatMessage({
id: 'menu.search.placeholder',
defaultMessage: 'Search for people',
})}
selectFirstResult={true}
ref={(ref) =>
(this.searchRef = (ref as unknown) as {
setValue(value: string): void;
})
}
/>
);
}
private chartMenus(screenSize: ScreenSize) {
if (!this.props.showingChart) { if (!this.props.showingChart) {
return null; return null;
} }
return ( const chartTypeItems = (
<> <>
<Menu.Item as="a" onClick={() => this.props.eventHandlers.onPrint()}> <Dropdown.Item onClick={() => this.changeView('hourglass')}>
<Icon name="print" /> <Icon name="hourglass" />
<FormattedMessage id="menu.print" defaultMessage="Print" /> <FormattedMessage
</Menu.Item> id="menu.hourglass"
defaultMessage="Hourglass chart"
/>
</Dropdown.Item>
{this.props.allowAllRelativesChart ? (
<Dropdown.Item onClick={() => this.changeView('relatives')}>
<Icon name="users" />
<FormattedMessage
id="menu.relatives"
defaultMessage="All relatives"
/>
</Dropdown.Item>
) : null}
<Dropdown.Item onClick={() => this.changeView('fancy')}>
<Icon name="users" />
<FormattedMessage
id="menu.fancy"
defaultMessage="Fancy tree (experimental)"
/>
</Dropdown.Item>
</>
);
switch (screenSize) {
case ScreenSize.LARGE:
return (
<>
<Menu.Item onClick={() => this.props.eventHandlers.onPrint()}>
<Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" />
</Menu.Item>
<Dropdown
trigger={
<div>
<Icon name="download" />
<FormattedMessage
id="menu.download"
defaultMessage="Download"
/>
</div>
}
className="item"
>
<Dropdown.Menu>
<Dropdown.Item
onClick={() => this.props.eventHandlers.onDownloadPdf()}
>
<FormattedMessage
id="menu.pdf_file"
defaultMessage="PDF file"
/>
</Dropdown.Item>
<Dropdown.Item
onClick={() => this.props.eventHandlers.onDownloadPng()}
>
<FormattedMessage
id="menu.png_file"
defaultMessage="PNG file"
/>
</Dropdown.Item>
<Dropdown.Item
onClick={() => this.props.eventHandlers.onDownloadSvg()}
>
<FormattedMessage
id="menu.svg_file"
defaultMessage="SVG file"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Dropdown
trigger={
<div>
<Icon name="eye" />
<FormattedMessage id="menu.view" defaultMessage="View" />
</div>
}
className="item"
>
<Dropdown.Menu>{chartTypeItems}</Dropdown.Menu>
</Dropdown>
{this.search()}
</>
);
case ScreenSize.SMALL:
return (
<>
<Dropdown.Item onClick={() => this.props.eventHandlers.onPrint()}>
<Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown
trigger={
<div>
<Icon name="download" />
<FormattedMessage id="menu.download" defaultMessage="Download" />
</div>
}
className="item"
>
<Dropdown.Menu>
<Dropdown.Item <Dropdown.Item
onClick={() => this.props.eventHandlers.onDownloadPdf()} onClick={() => this.props.eventHandlers.onDownloadPdf()}
> >
<FormattedMessage id="menu.pdf_file" defaultMessage="PDF file" /> <Icon name="download" />
<FormattedMessage
id="menu.download_pdf"
defaultMessage="Downlod PDF"
/>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
onClick={() => this.props.eventHandlers.onDownloadPng()} onClick={() => this.props.eventHandlers.onDownloadPng()}
> >
<FormattedMessage id="menu.png_file" defaultMessage="PNG file" /> <Icon name="download" />
<FormattedMessage
id="menu.download_png"
defaultMessage="Download PNG"
/>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
onClick={() => this.props.eventHandlers.onDownloadSvg()} onClick={() => this.props.eventHandlers.onDownloadSvg()}
> >
<FormattedMessage id="menu.svg_file" defaultMessage="SVG file" /> <Icon name="download" />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Dropdown
trigger={
<div>
<Icon name="eye" />
<FormattedMessage id="menu.view" defaultMessage="View" />
</div>
}
className="item"
>
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.changeView('hourglass')}>
<Icon name="hourglass" />
<FormattedMessage <FormattedMessage
id="menu.hourglass" id="menu.download_svg"
defaultMessage="Hourglass chart" defaultMessage="Download SVG"
/> />
</Dropdown.Item> </Dropdown.Item>
{this.props.allowAllRelativesChart ? (
<Dropdown.Item onClick={() => this.changeView('relatives')}>
<Icon name="users" />
<FormattedMessage
id="menu.relatives"
defaultMessage="All relatives"
/>
</Dropdown.Item>
) : null}
<Dropdown.Item onClick={() => this.changeView('fancy')}>
<Icon name="users" />
<FormattedMessage
id="menu.fancy"
defaultMessage="Fancy tree (experimental)"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Search <Dropdown.Divider />
onSearchChange={debounce( {chartTypeItems}
(_: React.MouseEvent<HTMLElement>, data: SearchProps) => <Dropdown.Divider />
this.handleSearch(data.value), </>
200, );
)} }
onResultSelect={(_, data) => this.handleResultSelect(data.result.id)} }
results={this.state.searchResults}
noResultsMessage={this.context.intl.formatMessage({ private title() {
id: 'menu.search.no_results', return (
defaultMessage: 'No results found', <Menu.Item>
})} <b>Topola Genealogy</b>
placeholder={this.context.intl.formatMessage({ </Menu.Item>
id: 'menu.search.placeholder',
defaultMessage: 'Search for people',
})}
selectFirstResult={true}
ref={(ref) =>
(this.searchRef = (ref as unknown) as {
setValue(value: string): void;
})
}
/>
</>
); );
} }
private fileMenus() { private fileMenus(screenSize: ScreenSize) {
if (!this.props.standalone) { if (!this.props.standalone) {
return null; return null;
} }
return ( const loadUrlItem = (
<> <>
<Link to="/"> <Icon name="cloud download" />
<Menu.Item> <FormattedMessage
<b>Topola Genealogy</b> id="menu.load_from_url"
</Menu.Item> defaultMessage="Load from URL"
</Link> />
<Menu.Item as="a" onClick={() => this.openLoadUrlDialog()}> </>
<Icon name="cloud download" /> );
<FormattedMessage const loadFileItem = (
id="menu.load_from_url" <>
defaultMessage="Load from URL" <Icon name="folder open" />
/> <FormattedMessage
</Menu.Item> id="menu.load_from_file"
defaultMessage="Load from file"
/>
</>
);
const commonElements = (
<>
{this.loadFromUrlModal()}
<input <input
className="hidden" className="hidden"
type="file" type="file"
@@ -447,53 +535,100 @@ export class TopBar extends React.Component<
multiple multiple
onChange={(e) => this.handleUpload(e)} onChange={(e) => this.handleUpload(e)}
/> />
<label htmlFor="fileInput">
<Menu.Item as="a">
<Icon name="folder open" />
<FormattedMessage
id="menu.load_from_file"
defaultMessage="Load from file"
/>
</Menu.Item>
</label>
</> </>
); );
switch (screenSize) {
case ScreenSize.LARGE:
return (
<>
<Menu.Item onClick={() => this.openLoadUrlDialog()}>
{loadUrlItem}
</Menu.Item>
<label htmlFor="fileInput">
<Menu.Item as="a">{loadFileItem}</Menu.Item>
</label>
{commonElements}
</>
);
case ScreenSize.SMALL:
return (
<>
<Dropdown.Item onClick={() => this.openLoadUrlDialog()}>
{loadUrlItem}
</Dropdown.Item>
<Dropdown.Item as="label" htmlFor="fileInput">
{loadFileItem}
</Dropdown.Item>
<Dropdown.Divider />
{commonElements}
</>
);
}
} }
private wikiTreeLoginMenu() { private wikiTreeLoginMenu(screenSize: ScreenSize) {
if (!this.props.showWikiTreeLogin) { if (!this.props.showWikiTreeLogin) {
return null; return null;
} }
const wikiTreeLogoUrl = const wikiTreeLogoUrl =
'https://www.wikitree.com/photo.php/a/a5/WikiTree_Images.png'; 'https://www.wikitree.com/photo.php/a/a5/WikiTree_Images.png';
switch (this.state.wikiTreeLoginState) { switch (this.state.wikiTreeLoginState) {
case WikiTreeLoginState.NOT_LOGGED_IN: case WikiTreeLoginState.NOT_LOGGED_IN:
return ( const loginForm = (
<Menu.Item as="a" onClick={() => this.wikiTreeLogin()}> <form
<img action="https://apps.wikitree.com/api.php"
src={wikiTreeLogoUrl} method="POST"
alt="WikiTree logo" style={{display: 'hidden'}}
style={{width: '24px', height: '24px'}} ref={this.wikiTreeLoginFormRef}
>
<input type="hidden" name="action" value="clientLogin" />
<input
type="hidden"
name="returnURL"
ref={this.wikiTreeReturnUrlRef}
/> />
<FormattedMessage </form>
id="menu.wikitree_login"
defaultMessage="Log in to WikiTree"
/>
<form
action="https://apps.wikitree.com/api.php"
method="POST"
style={{display: 'hidden'}}
ref={this.wikiTreeLoginFormRef}
>
<input type="hidden" name="action" value="clientLogin" />
<input
type="hidden"
name="returnURL"
ref={this.wikiTreeReturnUrlRef}
/>
</form>
</Menu.Item>
); );
switch (screenSize) {
case ScreenSize.LARGE:
return (
<Menu.Item onClick={() => this.wikiTreeLogin()}>
<img
src={wikiTreeLogoUrl}
alt="WikiTree logo"
style={{width: '24px', height: '24px'}}
/>
<FormattedMessage
id="menu.wikitree_login"
defaultMessage="Log in to WikiTree"
/>
{loginForm}
</Menu.Item>
);
case ScreenSize.SMALL:
return (
<>
<Dropdown.Item onClick={() => this.wikiTreeLogin()}>
<img
src={wikiTreeLogoUrl}
alt="WikiTree logo"
style={{width: '24px', height: '24px'}}
/>
<FormattedMessage
id="menu.wikitree_login"
defaultMessage="Log in to WikiTree"
/>
{loginForm}
</Dropdown.Item>
<Dropdown.Divider />
</>
);
}
break;
case WikiTreeLoginState.LOGGED_IN: case WikiTreeLoginState.LOGGED_IN:
const tooltip = this.state.wikiTreeLoginUsername const tooltip = this.state.wikiTreeLoginUsername
? this.context.intl.formatMessage( ? this.context.intl.formatMessage(
@@ -507,62 +642,129 @@ export class TopBar extends React.Component<
id: 'menu.wikitree_popup', id: 'menu.wikitree_popup',
defaultMessage: 'Logged in to WikiTree', defaultMessage: 'Logged in to WikiTree',
}); });
return ( switch (screenSize) {
<Menu.Item title={tooltip}> case ScreenSize.LARGE:
<img return (
src={wikiTreeLogoUrl} <Menu.Item title={tooltip}>
alt="WikiTree logo" <img
style={{width: '24px', height: '24px'}} src={wikiTreeLogoUrl}
/> alt="WikiTree logo"
<FormattedMessage style={{width: '24px', height: '24px'}}
id="menu.wikitree_logged_in" />
defaultMessage="Logged in" <FormattedMessage
/> id="menu.wikitree_logged_in"
</Menu.Item> defaultMessage="Logged in"
); />
default: </Menu.Item>
return null; );
case ScreenSize.SMALL:
return (
<>
<Menu.Item title={tooltip}>
<img
src={wikiTreeLogoUrl}
alt="WikiTree logo"
style={{width: '24px', height: '24px'}}
/>
<FormattedMessage
id="menu.wikitree_logged_in"
defaultMessage="Logged in"
/>
</Menu.Item>
<Dropdown.Divider />
</>
);
default:
return null;
}
} }
} }
private sourceLink() { private mobileMenus() {
return ( return (
<Menu.Item <>
as="a" <Dropdown
href="https://github.com/PeWu/topola-viewer" trigger={
target="_blank" <div>
> <Icon name="sidebar" />
<FormattedMessage id="menu.github" defaultMessage="Source on GitHub" /> </div>
</Menu.Item> }
className="item"
icon={null}
>
<Dropdown.Menu>
{this.fileMenus(ScreenSize.SMALL)}
{this.chartMenus(ScreenSize.SMALL)}
{this.wikiTreeLoginMenu(ScreenSize.SMALL)}
<Dropdown.Item
href="https://github.com/PeWu/topola-viewer"
target="_blank"
>
<FormattedMessage
id="menu.github"
defaultMessage="Source on GitHub"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
{this.props.standalone ? (
<Link to="/">{this.title()}</Link>
) : (
this.title()
)}
</>
); );
} }
private poweredByLink() { private desktopMenus() {
return ( return (
<Menu.Item <>
as="a" {this.props.standalone ? <Link to="/">{this.title()}</Link> : null}
href="https://pewu.github.com/topola-viewer" {this.fileMenus(ScreenSize.LARGE)}
target="_blank" {this.chartMenus(ScreenSize.LARGE)}
> <Menu.Menu position="right">
<FormattedMessage {this.wikiTreeLoginMenu(ScreenSize.LARGE)}
id="menu.powered_by" <Menu.Item
defaultMessage="Powered by Topola" href="https://github.com/PeWu/topola-viewer"
/> target="_blank"
</Menu.Item> >
<FormattedMessage
id="menu.github"
defaultMessage="GitHub project"
/>
</Menu.Item>
</Menu.Menu>
</>
); );
} }
render() { render() {
return ( return (
<Menu attached="top" inverted color="blue" size="large"> <>
{this.fileMenus()} <Responsive
{this.chartMenus()} as={Menu}
<Menu.Menu position="right"> attached="top"
{this.wikiTreeLoginMenu()} inverted
{this.props.standalone ? this.sourceLink() : this.poweredByLink()} color="blue"
</Menu.Menu> size="large"
{this.loadFromUrlModal()} minWidth={768}
</Menu> >
{this.desktopMenus()}
</Responsive>
<Responsive
as={Menu}
attached="top"
inverted
color="blue"
size="large"
maxWidth={767}
>
{this.mobileMenus()}
</Responsive>
</>
); );
} }
} }

View File

@@ -6,6 +6,9 @@
"menu.pdf_file": "Plik PDF", "menu.pdf_file": "Plik PDF",
"menu.png_file": "Plik PNG", "menu.png_file": "Plik PNG",
"menu.svg_file": "Plik SVG", "menu.svg_file": "Plik SVG",
"menu.download_pdf": "Pobierz PDF",
"menu.download_png": "Pobierz PNG",
"menu.download_svg": "Pobierz SVG",
"menu.view": "Widok", "menu.view": "Widok",
"menu.hourglass": "Wykres klepsydrowy", "menu.hourglass": "Wykres klepsydrowy",
"menu.relatives": "Wszyscy krewni", "menu.relatives": "Wszyscy krewni",
@@ -14,8 +17,7 @@
"menu.wikitree_logged_in": "Zalogowano", "menu.wikitree_logged_in": "Zalogowano",
"menu.wikitree_popup_username": "Zalogowano do WikiTree jako {username}", "menu.wikitree_popup_username": "Zalogowano do WikiTree jako {username}",
"menu.wikitree_popup": "Zalogowano do WikiTree", "menu.wikitree_popup": "Zalogowano do WikiTree",
"menu.github": "Źródła na GitHub", "menu.github": "Projekt na stronie GitHub",
"menu.powered_by": "Topola Genealogy",
"menu.search.placeholder": "Szukaj osoby", "menu.search.placeholder": "Szukaj osoby",
"menu.search.no_results": "Brak wyników", "menu.search.no_results": "Brak wyników",
"intro.title": "Topola Genealogy", "intro.title": "Topola Genealogy",