Feature/history page (#28)

* add alert message and use only entry for card and table

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Refresh table view when translation was changed

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add sort by date and pinning

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* save history to localstorage

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* improve card and table history

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* extract functions

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Sort in history component

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix i18n key

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Move scss imports

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* fix scss import

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* modify state with setState

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* fix import

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* add sortAndFilterEntries function

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
mrdrogdrog 2020-05-16 19:54:08 +02:00 committed by GitHub
parent 5eb8ab7517
commit 83ab0bbe7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 226 additions and 163 deletions

View file

@ -0,0 +1,7 @@
.history-close {
opacity: 0.5;
&:hover {
opacity: 1;
}
}

View file

@ -1,5 +1,6 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import "./close-button.scss"
const CloseButton: React.FC = () => {
return (

View file

@ -11,11 +11,3 @@
opacity: 1;
}
}
.history-close {
opacity: 0.5;
&:hover {
opacity: 1;
}
}

View file

@ -1,17 +1,18 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import "./pin-button.scss"
export interface PinButtonProps {
pin: boolean;
onPinChange: () => void;
onPinClick: () => void;
}
const PinButton: React.FC<PinButtonProps> = ({pin, onPinChange}) => {
const PinButton: React.FC<PinButtonProps> = ({pin, onPinClick}) => {
return (
<FontAwesomeIcon
icon="thumbtack"
className={`history-pin ${pin? 'active' : ''}`}
onClick={onPinChange}
className={`history-pin ${pin ? 'active' : ''}`}
onClick={onPinClick}
/>
);
}

View file

@ -0,0 +1,18 @@
import React, {Fragment} from 'react'
import {HistoryEntriesProps} from "../history-content/history-content";
import {HistoryCard} from "./history-card";
export const HistoryCardList: React.FC<HistoryEntriesProps> = ({entries, onPinClick}) => {
return (
<Fragment>
{
entries.map((entry) => (
<HistoryCard
key={entry.id}
entry={entry}
onPinClick={onPinClick}
/>))
}
</Fragment>
)
}

View file

@ -1,32 +1,34 @@
import React from 'react'
import {HistoryInput} from '../history'
import {Badge, Card} from 'react-bootstrap'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import "../common/button.scss"
import {PinButton} from "../common/pin-button";
import {CloseButton} from "../common/close-button";
import moment from "moment";
import {useTranslation} from "react-i18next";
import {HistoryEntryProps} from "../history-content/history-content";
import {formatHistoryDate} from "../../../../../utils/historyUtils";
export const HistoryCard: React.FC<HistoryInput> = ({pinned, title, lastVisited, tags, onPinChange}) => {
export const HistoryCard: React.FC<HistoryEntryProps> = ({entry, onPinClick}) => {
useTranslation()
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={pinned} onPinChange={onPinChange}/>
<Card.Title className="m-0 mt-3">{title}</Card.Title>
<PinButton pin={entry.pinned} onPinClick={() => {
onPinClick(entry.id)
}}/>
<Card.Title className="m-0 mt-3">{entry.title}</Card.Title>
<CloseButton/>
</div>
<Card.Body>
<div className="text-black-50">
<FontAwesomeIcon icon="clock"/> {moment(lastVisited).fromNow()}<br/>
{moment(lastVisited).format("llll")}
<div children=
{
tags.map((tag) => <Badge variant={"dark"} key={tag}>{tag}</Badge>)
}
/>
<FontAwesomeIcon icon="clock"/> {moment(entry.lastVisited).fromNow()}<br/>
{formatHistoryDate(entry.lastVisited)}
<div>
{
entry.tags.map((tag) => <Badge variant={"dark"} key={tag}>{tag}</Badge>)
}
</div>
</div>
</Card.Body>
</Card>

View file

@ -0,0 +1,41 @@
import React from "react";
import {HistoryEntry, pinClick, ViewStateEnum} 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";
export interface HistoryContentProps {
viewState: ViewStateEnum
entries: HistoryEntry[]
onPinClick: pinClick
}
export interface HistoryEntryProps {
entry: HistoryEntry,
onPinClick: pinClick
}
export interface HistoryEntriesProps {
entries: HistoryEntry[]
onPinClick: pinClick
}
export const HistoryContent: React.FC<HistoryContentProps> = ({viewState, entries, onPinClick}) => {
if (entries.length === 0) {
return (
<Alert variant={"secondary"}>
<Trans i18nKey={"noHistory"}/>
</Alert>
);
}
switch (viewState) {
default:
case ViewStateEnum.card:
return <HistoryCardList entries={entries} onPinClick={onPinClick}/>
case ViewStateEnum.table:
return <HistoryTable entries={entries} onPinClick={onPinClick}/>;
}
}

View file

@ -1,16 +1,20 @@
import React from "react";
import {HistoryInput} from "../history";
import {PinButton} from "../common/pin-button";
import {CloseButton} from "../common/close-button";
import moment from "moment";
import {useTranslation} from "react-i18next";
import {HistoryEntryProps} from "../history-content/history-content";
import {formatHistoryDate} from "../../../../../utils/historyUtils";
export const HistoryTableRow: React.FC<HistoryInput> = ({pinned, title, lastVisited, onPinChange}) => {
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({entry, onPinClick}) => {
useTranslation()
return (
<tr>
<td>{title}</td>
<td>{moment(lastVisited).format("llll")}</td>
<td>{entry.title}</td>
<td>{formatHistoryDate(entry.lastVisited)}</td>
<td>
<PinButton pin={pinned} onPinChange={onPinChange}/>
<PinButton pin={entry.pinned} onPinClick={() => {
onPinClick(entry.id)
}}/>
&nbsp;
<CloseButton/>
</td>

View file

@ -1,21 +1,30 @@
import React from "react";
import {Table} from "react-bootstrap"
import {HistoryTableRow} from "./history-table-row";
import {HistoryEntriesProps} from "../history-content/history-content";
const HistoryTable: React.FC = ({children}) => {
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>
</tr>
<tr>
<th>Title</th>
<th>Last visited</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{children}
{
entries.map((entry) =>
<HistoryTableRow
key={entry.id}
entry={entry}
onPinClick={onPinClick}
/>)
}
</tbody>
</Table>
)
}
export { HistoryTable }
export {HistoryTable}

View file

@ -1,26 +1,14 @@
import React, {Fragment, useEffect, useState} from 'react'
import {HistoryCard} from "./history-card/history-card";
import {HistoryTable} from "./history-table/history-table";
import {HistoryTableRow} from './history-table/history-table-row';
import {ToggleButton, ToggleButtonGroup} from 'react-bootstrap';
import moment from "moment";
import {HistoryContent} from './history-content/history-content';
import {loadHistoryFromLocalStore, sortAndFilterEntries} from "../../../../utils/historyUtils";
interface HistoryChange {
onPinChange: () => void,
}
interface ViewState {
viewState: ViewStateEnum
}
enum ViewStateEnum {
export enum ViewStateEnum {
card,
table
}
export type HistoryInput = HistoryEntry & HistoryChange
interface HistoryEntry {
export interface HistoryEntry {
id: string,
title: string,
lastVisited: Date,
@ -28,101 +16,44 @@ interface HistoryEntry {
pinned: boolean
}
interface OldHistoryEntry {
id: string;
text: string;
time: number;
tags: string[];
pinned: boolean;
}
export type pinClick = (entryId: string) => void;
function loadHistoryFromLocalStore() {
const historyJsonString = window.localStorage.getItem("history");
if (historyJsonString === null) {
// if localStorage["history"] is empty we check the old localStorage["notehistory"]
// and convert it to the new format
const oldHistoryJsonString = window.localStorage.getItem("notehistory")
const oldHistory = oldHistoryJsonString ? JSON.parse(JSON.parse(oldHistoryJsonString)) : [];
return oldHistory.map((entry: OldHistoryEntry) => {
return {
id: entry.id,
title: entry.text,
lastVisited: moment(entry.time).toDate(),
tags: entry.tags,
pinned: entry.pinned,
}
})
} else {
return JSON.parse(historyJsonString)
}
}
const History: React.FC = () => {
export const History: React.FC = () => {
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([])
const [viewState, setViewState] = useState<ViewState>({
viewState: ViewStateEnum.card
})
const [viewState, setViewState] = useState<ViewStateEnum>(ViewStateEnum.card)
useEffect(() => {
const history = loadHistoryFromLocalStore();
setHistoryEntries(history);
}, [])
useEffect(() => {
window.localStorage.setItem("history", JSON.stringify(historyEntries));
}, [historyEntries])
const pinClick: pinClick = (entryId: string) => {
setHistoryEntries((entries) => {
return entries.map((entry) => {
if (entry.id === entryId) {
entry.pinned = !entry.pinned;
}
return entry;
});
})
}
return (
<Fragment>
<h1>History</h1>
<ToggleButtonGroup type="radio" name="options" defaultValue={ViewStateEnum.card} className="mb-2"
onChange={(newState: ViewStateEnum) => setViewState(() => ({viewState: newState}))}>
onChange={(newState: ViewStateEnum) => setViewState(newState)}>
<ToggleButton value={ViewStateEnum.card}>Card</ToggleButton>
<ToggleButton value={ViewStateEnum.table}>Table</ToggleButton>
</ToggleButtonGroup>
{
viewState.viewState === ViewStateEnum.card ? (
<div className="d-flex flex-wrap">
{
historyEntries.length === 0 ?
''
:
historyEntries.map((entry) =>
<HistoryCard
id={entry.id}
tags={entry.tags}
pinned={entry.pinned}
title={entry.title}
lastVisited={entry.lastVisited}
onPinChange={() => {
// setHistoryEntries((prev: HistoryEntry) => {
// return {...prev, pinned: !prev.pinned};
// });
}}
/>)
}
</div>
) : (
<HistoryTable>
{
historyEntries.length === 0 ?
''
:
historyEntries.map((entry) =>
<HistoryTableRow
id={entry.id}
tags={entry.tags}
pinned={entry.pinned}
title={entry.title}
lastVisited={entry.lastVisited}
onPinChange={() => {
// setEntry((prev: HistoryEntry) => {
// return {...prev, pinned: !prev.pinned};
// });
}}
/>)
}
</HistoryTable>
)
}
<div className="d-flex flex-wrap justify-content-center">
<HistoryContent viewState={viewState} entries={sortAndFilterEntries(historyEntries)}
onPinClick={pinClick}/>
</div>
</Fragment>
)
}
export {History}
}