Feature/history toolbar (#44)

Add the history bar
This commit is contained in:
mrdrogdrog 2020-05-21 22:01:23 +02:00 committed by GitHub
parent 4c785b345b
commit d03e000bd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 681 additions and 303 deletions

View file

@ -0,0 +1,20 @@
.btn-icon {
padding: 0.375rem 0.375rem;
border-right: 1px solid rgba(0, 0, 0, 0.2);
display: flex;
.icon-part {
padding: 0.375rem 0.375rem;
border-right: 1px solid rgba(0, 0, 0, 0.2);
display: flex;
.social-icon {
font-size: 1.5em;
}
}
.text-part {
padding: 0.375rem 0.75rem;
}
}

View file

@ -0,0 +1,24 @@
import React from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import "./icon-button.scss";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import {Button, ButtonProps} from "react-bootstrap";
export interface SocialButtonProps extends ButtonProps {
icon: IconProp
onClick?: () => void
}
export const IconButton: React.FC<SocialButtonProps> = ({icon, children, variant, onClick}) => {
return (
<Button variant={variant} className={"btn-icon p-0 d-inline-flex align-items-stretch"}
onClick={() => onClick?.()}>
<span className="icon-part d-flex align-items-center">
<FontAwesomeIcon icon={icon} className={"icon"}/>
</span>
<span className="text-part d-flex align-items-center">
{children}
</span>
</Button>
)
}

View file

@ -1,4 +1,5 @@
@import "../../../../../node_modules/bootstrap/scss/bootstrap";
@import '../../../../../node_modules/react-bootstrap-typeahead/css/Typeahead';
@import "font-pack";
//@import "cover.scss";

View file

@ -1,14 +1,21 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import "./close-button.scss"
import {Button} from "react-bootstrap";
const CloseButton: React.FC = () => {
export interface CloseButtonProps {
isDark: boolean;
}
const CloseButton: React.FC<CloseButtonProps> = ({isDark}) => {
return (
<FontAwesomeIcon
className="history-close"
icon="times"
/>
<Button variant={isDark ? "secondary" : "light"}>
<FontAwesomeIcon
className="history-close"
icon="times"
/>
</Button>
);
}
export { CloseButton }
export {CloseButton}

View file

@ -1,20 +1,22 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import "./pin-button.scss"
import {Button} from "react-bootstrap";
export interface PinButtonProps {
pin: boolean;
isPinned: boolean;
onPinClick: () => void;
isDark: boolean;
}
const PinButton: React.FC<PinButtonProps> = ({pin, onPinClick}) => {
export const PinButton: React.FC<PinButtonProps> = ({isPinned, onPinClick, isDark}) => {
return (
<FontAwesomeIcon
icon="thumbtack"
className={`history-pin ${pin ? 'active' : ''}`}
onClick={onPinClick}
/>
<Button variant={isDark ? "secondary" : "light"}
onClick={onPinClick}>
<FontAwesomeIcon
icon="thumbtack"
className={`history-pin ${isPinned ? 'active' : ''}`}
/>
</Button>
);
}
export { PinButton }

View file

@ -13,12 +13,12 @@ export const HistoryCard: React.FC<HistoryEntryProps> = ({entry, onPinClick}) =>
return (
<div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4">
<Card className="p-0" text={"dark"} bg={"light"}>
<div className="d-flex justify-content-between p-2">
<PinButton pin={entry.pinned} onPinClick={() => {
<div className="d-flex justify-content-between p-2 align-items-start">
<PinButton isDark={false} isPinned={entry.pinned} onPinClick={() => {
onPinClick(entry.id)
}}/>
<Card.Title className="m-0 mt-3">{entry.title}</Card.Title>
<CloseButton/>
<CloseButton isDark={false}/>
</div>
<Card.Body>
<div className="text-black-50">
@ -26,7 +26,8 @@ export const HistoryCard: React.FC<HistoryEntryProps> = ({entry, onPinClick}) =>
{formatHistoryDate(entry.lastVisited)}
<div>
{
entry.tags.map((tag) => <Badge variant={"dark"} key={tag}>{tag}</Badge>)
entry.tags.map((tag) => <Badge variant={"dark"} className={"mr-1 mb-1"}
key={tag}>{tag}</Badge>)
}
</div>
</div>

View file

@ -1,9 +1,10 @@
import React from "react";
import {HistoryEntry, pinClick, ViewStateEnum} from "../history";
import {HistoryEntry, pinClick} from "../history";
import {HistoryTable} from "../history-table/history-table";
import {Alert} from "react-bootstrap";
import {Trans} from "react-i18next";
import {HistoryCardList} from "../history-card/history-card-list";
import {ViewStateEnum} from "../history-toolbar/history-toolbar";
export interface HistoryContentProps {
viewState: ViewStateEnum

View file

@ -12,11 +12,11 @@ export const HistoryTableRow: React.FC<HistoryEntryProps> = ({entry, onPinClick}
<td>{entry.title}</td>
<td>{formatHistoryDate(entry.lastVisited)}</td>
<td>
<PinButton pin={entry.pinned} onPinClick={() => {
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => {
onPinClick(entry.id)
}}/>
&nbsp;
<CloseButton/>
<CloseButton isDark={true}/>
</td>
</tr>
)

View file

@ -2,15 +2,16 @@ import React from "react";
import {Table} from "react-bootstrap"
import {HistoryTableRow} from "./history-table-row";
import {HistoryEntriesProps} from "../history-content/history-content";
import {Trans} from "react-i18next";
const HistoryTable: React.FC<HistoryEntriesProps> = ({entries, onPinClick}) => {
return (
<Table striped bordered hover size="sm" variant="dark">
<thead>
<tr>
<th>Title</th>
<th>Last visited</th>
<th>Actions</th>
<th><Trans i18nKey={"title"}/></th>
<th><Trans i18nKey={"lastVisit"}/></th>
<th><Trans i18nKey={"actions"}/></th>
</tr>
</thead>
<tbody>

View file

@ -0,0 +1,128 @@
import {Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup} from "react-bootstrap";
import React, {ChangeEvent, useEffect, useState} from "react";
import {Trans, useTranslation} from "react-i18next";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SortButton, SortModeEnum} from "../../../../sort-button/sort-button";
import {Typeahead} from 'react-bootstrap-typeahead';
import "./typeahead-hacks.scss";
export type HistoryToolbarChange = (settings: HistoryToolbarState) => void;
export interface HistoryToolbarState {
viewState: ViewStateEnum
titleSortDirection: SortModeEnum
lastVisitedSortDirection: SortModeEnum
keywordSearch: string
selectedTags: string[]
}
export enum ViewStateEnum {
card,
table
}
export interface HistoryToolbarProps {
onSettingsChange: HistoryToolbarChange
tags: string[]
}
export const initState: HistoryToolbarState = {
viewState: ViewStateEnum.card,
titleSortDirection: SortModeEnum.no,
lastVisitedSortDirection: SortModeEnum.no,
keywordSearch: "",
selectedTags: []
}
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({onSettingsChange, tags}) => {
const [t] = useTranslation()
const [state, setState] = useState<HistoryToolbarState>(initState);
const titleSortChanged = (direction: SortModeEnum) => {
setState(prevState => ({
...prevState,
titleSortDirection: direction,
lastVisitedSortDirection: SortModeEnum.no
}))
}
const lastVisitedSortChanged = (direction: SortModeEnum) => {
setState(prevState => ({
...prevState,
lastVisitedSortDirection: direction,
titleSortDirection: SortModeEnum.no
}))
}
const keywordSearchChanged = (event: ChangeEvent<HTMLInputElement>) => {
setState(prevState => ({...prevState, keywordSearch: event.currentTarget.value}));
}
const toggleViewChanged = (newViewState: ViewStateEnum) => {
setState((prevState) => ({...prevState, viewState: newViewState}))
}
const selectedTagsChanged = (selected: string[]) => {
setState((prevState => ({...prevState, selectedTags: selected})))
}
useEffect(() => {
onSettingsChange(state);
}, [onSettingsChange, state])
return (
<Form inline={true}>
<InputGroup className={"mr-1"}>
<Typeahead id={"tagsSelection"} options={tags} multiple={true} placeholder={t("selectTags")}
onChange={selectedTagsChanged}/>
</InputGroup>
<InputGroup className={"mr-1"}>
<FormControl
placeholder={t("searchKeywords")}
aria-label={t("searchKeywords")}
onChange={keywordSearchChanged}
/>
</InputGroup>
<InputGroup className={"mr-1"}>
<SortButton onChange={titleSortChanged} direction={state.titleSortDirection} variant={"light"}><Trans
i18nKey={"sortByTitle"}/></SortButton>
</InputGroup>
<InputGroup className={"mr-1"}>
<SortButton onChange={lastVisitedSortChanged} direction={state.lastVisitedSortDirection}
variant={"light"}><Trans i18nKey={"sortByLastVisited"}/></SortButton>
</InputGroup>
<InputGroup className={"mr-1"}>
<Button variant={"light"} title={t("exportHistory")}>
<FontAwesomeIcon icon={"download"}/>
</Button>
</InputGroup>
<InputGroup className={"mr-1"}>
<Button variant={"light"} title={t("importHistory")}>
<FontAwesomeIcon icon={"upload"}/>
</Button>
</InputGroup>
<InputGroup className={"mr-1"}>
<Button variant={"light"} title={t("clearHistory")}>
<FontAwesomeIcon icon={"trash"}/>
</Button>
</InputGroup>
<InputGroup className={"mr-1"}>
<Button variant={"light"} title={t("refreshHistory")}>
<FontAwesomeIcon icon={"sync"}/>
</Button>
</InputGroup>
<InputGroup className={"mr-1"}>
<ToggleButtonGroup type="radio" name="options" value={state.viewState}
onChange={(newViewState: ViewStateEnum) => {
toggleViewChanged(newViewState)
}}>
<ToggleButton className={"btn-light"} value={ViewStateEnum.card}><Trans
i18nKey={"cards"}/></ToggleButton>
<ToggleButton className={"btn-light"} value={ViewStateEnum.table}><Trans
i18nKey={"table"}/></ToggleButton>
</ToggleButtonGroup>
</InputGroup>
</Form>
)
}

View file

@ -0,0 +1,11 @@
.rbt-input-multi {
min-width: 200px !important;
.rbt-input-main {
&[placeholder=""] {
width: 10px !important;
}
width: 100%;
}
}

View file

@ -1,12 +1,8 @@
import React, {Fragment, useEffect, useState} from 'react'
import {ToggleButton, ToggleButtonGroup} from 'react-bootstrap';
import {HistoryContent} from './history-content/history-content';
import {HistoryToolbar, HistoryToolbarState, initState as toolbarInitState} from './history-toolbar/history-toolbar';
import {loadHistoryFromLocalStore, sortAndFilterEntries} from "../../../../utils/historyUtils";
export enum ViewStateEnum {
card,
table
}
import {Row} from 'react-bootstrap';
export interface HistoryEntry {
id: string,
@ -20,7 +16,7 @@ export type pinClick = (entryId: string) => void;
export const History: React.FC = () => {
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([])
const [viewState, setViewState] = useState<ViewStateEnum>(ViewStateEnum.card)
const [viewState, setViewState] = useState<HistoryToolbarState>(toolbarInitState)
useEffect(() => {
const history = loadHistoryFromLocalStore();
@ -28,6 +24,9 @@ export const History: React.FC = () => {
}, [])
useEffect(() => {
if (historyEntries === []) {
return;
}
window.localStorage.setItem("history", JSON.stringify(historyEntries));
}, [historyEntries])
@ -42,16 +41,25 @@ export const History: React.FC = () => {
})
}
const tags = historyEntries.map(entry => entry.tags)
.reduce((a, b) => ([...a, ...b]), [])
.filter((value, index, array) => {
if (index === 0) {
return true;
}
return (value !== array[index - 1])
})
const entriesToShow = sortAndFilterEntries(historyEntries, viewState);
return (
<Fragment>
<h1>History</h1>
<ToggleButtonGroup type="radio" name="options" defaultValue={ViewStateEnum.card} className="mb-2"
onChange={(newState: ViewStateEnum) => setViewState(newState)}>
<ToggleButton value={ViewStateEnum.card}>Card</ToggleButton>
<ToggleButton value={ViewStateEnum.table}>Table</ToggleButton>
</ToggleButtonGroup>
<Row className={"justify-content-center mb-3"}>
<HistoryToolbar onSettingsChange={setViewState} tags={tags}/>
</Row>
<div className="d-flex flex-wrap justify-content-center">
<HistoryContent viewState={viewState} entries={sortAndFilterEntries(historyEntries)}
<HistoryContent viewState={viewState.viewState}
entries={entriesToShow}
onPinClick={pinClick}/>
</div>
</Fragment>

View file

@ -1,4 +1,4 @@
.btn.btn-icon {
.btn.social-link-button {
color: #FFFFFF;
@mixin button($color) {
@ -8,6 +8,20 @@
}
}
.icon-part {
padding: 0.375rem 0.375rem;
border-right: 1px solid rgba(0, 0, 0, 0.2);
display: flex;
.social-icon {
font-size: 1.5em;
}
}
.text-part {
padding: 0.375rem 0.75rem;
}
&.btn-social-dropbox {
@include button(#1087DD);
}
@ -33,16 +47,3 @@
}
}
.btn-social-button {
padding: 0.375rem 0.375rem;
border-right: 1px solid rgba(0, 0, 0, 0.2);
display: flex;
.social-icon {
font-size: 1.5em;
}
}
.btn-social-text {
padding: 0.375rem 0.75rem;
}

View file

@ -1,6 +1,6 @@
import React from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import "./icon-button.scss";
import "./social-link-button.scss";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
export interface SocialButtonProps {
@ -10,14 +10,14 @@ export interface SocialButtonProps {
title?: string
}
export const IconButton: React.FC<SocialButtonProps> = ({title, backgroundClass, href, icon, children}) => {
export const SocialLinkButton: React.FC<SocialButtonProps> = ({title, backgroundClass, href, icon, children}) => {
return (
<a href={href} title={title}
className={"btn btn-icon p-0 d-inline-flex align-items-stretch " + backgroundClass}>
<span className="btn-social-button d-flex align-items-center">
className={"btn social-link-button p-0 d-inline-flex align-items-stretch " + backgroundClass}>
<span className="icon-part d-flex align-items-center">
<FontAwesomeIcon icon={icon} className={"social-icon"}/>
</span>
<span className="btn-social-text d-flex align-items-center">
<span className="text-part d-flex align-items-center">
{children}
</span>
</a>

View file

@ -1,6 +1,6 @@
import React from "react";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import {IconButton} from "./icon-button/icon-button";
import {SocialLinkButton} from "./social-link-button/social-link-button";
export enum OneClickType {
'DROPBOX'="dropbox",
@ -101,14 +101,14 @@ const ViaOneClick: React.FC<ViaOneClickProps> = ({oneClickType, optionalName}) =
const {name, icon, className, url} = getMetadata(oneClickType);
const text = !!optionalName ? optionalName : name;
return (
<IconButton
<SocialLinkButton
backgroundClass={className}
icon={icon}
href={url}
title={text}
>
{text}
</IconButton>
</SocialLinkButton>
)
}

View file

@ -0,0 +1,47 @@
import React from "react";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import {ButtonProps} from "react-bootstrap";
import {IconButton} from "../icon-button/icon-button";
export enum SortModeEnum {
up = 1,
down = -1,
no = 0
}
const getIcon = (direction: SortModeEnum): IconProp => {
switch (direction) {
default:
case SortModeEnum.no:
return "sort";
case SortModeEnum.up:
return "sort-up";
case SortModeEnum.down:
return "sort-down";
}
}
export interface SortButtonProps extends ButtonProps {
onChange: (direction: SortModeEnum) => void
direction: SortModeEnum
}
const toggleDirection = (direction: SortModeEnum) => {
switch (direction) {
case SortModeEnum.no:
return SortModeEnum.up;
case SortModeEnum.up:
return SortModeEnum.down;
default:
case SortModeEnum.down:
return SortModeEnum.no;
}
}
export const SortButton: React.FC<SortButtonProps> = ({children, variant, onChange, direction}) => {
const toggleSort = () => {
onChange(toggleDirection(direction));
}
return <IconButton onClick={toggleSort} variant={variant} icon={getIcon(direction)}>{children}</IconButton>;
}