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

@ -25,7 +25,7 @@
"Import history": "استيراد التاريخ",
"Clear history": "مسح التاريخ",
"Refresh history": "حدث التاريخ",
"No history": "ليس هناك سِجِل",
"noHistory": "ليس هناك سِجِل",
"Import from browser": "استيراد من المتصفح",
"releases": "إصدارات",
"Are you sure?": "هل أنت واثق؟",

View file

@ -25,7 +25,7 @@
"Import history": "Importar historial",
"Clear history": "Borrar historial",
"Refresh history": "Actualitzar historial",
"No history": "Cap historial",
"noHistory": "Cap historial",
"Import from browser": "Importar del navegador",
"releases": "Versions",
"Are you sure?": "Estas segur?",

View file

@ -25,7 +25,7 @@
"Import history": "Importovat historii",
"Clear history": "Odstranit historii",
"Refresh history": "Aktualizovat historii",
"No history": "Žádná historie",
"noHistory": "Žádná historie",
"Import from browser": "Importovat z prohlížeče",
"releases": "Vydání",
"Are you sure?": "Jste si jisti?",

View file

@ -25,7 +25,7 @@
"Import history": "Importér historik",
"Clear history": "Ryd hsitorik",
"Refresh history": "Genindlæs historik",
"No history": "Ingen historik",
"noHistory": "Ingen historik",
"Import from browser": "Importér fra browser",
"releases": "Releases",
"Are you sure?": "Er du sikker?",

View file

@ -25,7 +25,7 @@
"Import history": "Verlauf importieren",
"Clear history": "Verlauf löschen",
"Refresh history": "Verlauf aktualisieren",
"No history": "Kein Verlauf",
"noHistory": "Kein Verlauf",
"Import from browser": "Vom Browser importieren",
"releases": "Versionen",
"Are you sure?": "Sind Sie sicher?",

View file

@ -25,7 +25,7 @@
"Import history": "Εισαγωγή ιστορίας",
"Clear history": "Καθαρισμός Ιστορίας",
"Refresh history": "Ανανέωση ιστορίας",
"No history": "Δεν υπάρχει ιστορία",
"noHistory": "Δεν υπάρχει ιστορία",
"Import from browser": "Εισαγωγή απο τον περιηγητή",
"releases": "Κυκλοφορίες",
"Are you sure?": "Είστε σίγουρος?",

View file

@ -25,7 +25,7 @@
"Import history": "Import history",
"Clear history": "Clear history",
"Refresh history": "Refresh history",
"No history": "No history",
"noHistory": "No history",
"Import from browser": "Import from browser",
"releases": "Releases",
"Are you sure?": "Are you sure?",

View file

@ -25,7 +25,7 @@
"Import history": "Alportu historion",
"Clear history": "Malplenigu historion",
"Refresh history": "Refreŝigu historion",
"No history": "Neniu historio",
"noHistory": "Neniu historio",
"Import from browser": "Alportu de retumilo",
"releases": "Eldonoj",
"Are you sure?": "Ĉu vi certas?",

View file

@ -25,7 +25,7 @@
"Import history": "Importar historial",
"Clear history": "Borrar historial",
"Refresh history": "Actualizar historial",
"No history": "Ningún historial",
"noHistory": "Ningún historial",
"Import from browser": "Importar del navegador",
"releases": "Versiones",
"Are you sure?": "¿Estás seguro?",

View file

@ -25,7 +25,7 @@
"Import history": "Importer l'historique",
"Clear history": "Effacer l'historique",
"Refresh history": "Actualiser l'historique",
"No history": "Pas d'historique",
"noHistory": "Pas d'historique",
"Import from browser": "Importer depuis le navigateur",
"releases": "Versions",
"Are you sure?": "Ëtes-vous sûr ?",

View file

@ -25,7 +25,7 @@
"Import history": "इतिहास को आयात करें",
"Clear history": "इतिहास मिटा दें",
"Refresh history": "इतिहास ताज़ा करे",
"No history": "इतिहास न रखें",
"noHistory": "इतिहास न रखें",
"Import from browser": "ब्राउज़र से आयात",
"releases": "विज्ञप्ति",
"Are you sure?": "क्या आपको यकीन है?",

View file

@ -25,7 +25,7 @@
"Import history": "Uvezi povijest",
"Clear history": "Očisti povijest",
"Refresh history": "Osvježi povijest",
"No history": "Nema povijesti",
"noHistory": "Nema povijesti",
"Import from browser": "Uvezi iz preglednika",
"releases": "Izdanja",
"Are you sure?": "Jeste li sigurni?",

View file

@ -25,7 +25,7 @@
"Import history": "Impor Riwayat",
"Clear history": "Bersihkan Riwayat",
"Refresh history": "Muat-ulang Riwayat",
"No history": "Tidak ada riwayat",
"noHistory": "Tidak ada riwayat",
"Import from browser": "Impor dari browser",
"releases": "Penerbitan",
"Are you sure?": "Apakah anda yakin?",

View file

@ -25,7 +25,7 @@
"Import history": "Importa cronologia",
"Clear history": "Cancella cronologia",
"Refresh history": "Aggiorna cronologia",
"No history": "Nessuna cronologia",
"noHistory": "Nessuna cronologia",
"Import from browser": "Importa da browser",
"releases": "Versioni",
"Are you sure?": "Sei sicuro?",

View file

@ -25,7 +25,7 @@
"Import history": "履歴をインポート",
"Clear history": "履歴をクリア",
"Refresh history": "履歴を更新",
"No history": "履歴はありません",
"noHistory": "履歴はありません",
"Import from browser": "ブラウザからインポート",
"releases": "リリース",
"Are you sure?": "本当にいいですか?",

View file

@ -25,7 +25,7 @@
"Import history": "기록 불러오기",
"Clear history": "기록 초기화",
"Refresh history": "기록 새로고침",
"No history": "기록 없음",
"noHistory": "기록 없음",
"Import from browser": "브라우저에서 불러오기",
"releases": "릴리즈",
"Are you sure?": "확실합니까?",

View file

@ -25,7 +25,7 @@
"Import history": "Importeer geschiedenis",
"Clear history": "Verwijder geschiedenis",
"Refresh history": "Ververs geschiedenis",
"No history": "Geen geschidenis gevonden",
"noHistory": "Geen geschidenis gevonden",
"Import from browser": "Importeer van browser",
"releases": "Versies",
"Are you sure?": "Weet je het zeker?",

View file

@ -25,7 +25,7 @@
"Import history": "Importuj historię",
"Clear history": "Wyczyść historię",
"Refresh history": "Odśwież historię",
"No history": "Brak historii",
"noHistory": "Brak historii",
"Import from browser": "Importuj z przeglądarki",
"releases": "Wydania",
"Are you sure?": "Jesteś pewny?",

View file

@ -25,7 +25,7 @@
"Import history": "Importar histórico",
"Clear history": "Apagar histórico",
"Refresh history": "Atualizar histórico",
"No history": "Nenhum histórico",
"noHistory": "Nenhum histórico",
"Import from browser": "Importar do navegador",
"releases": "Lançamentos",
"Are you sure?": "Tem certeza?",

View file

@ -25,7 +25,7 @@
"Import history": "Импорт истории",
"Clear history": "Очистить историю",
"Refresh history": "Обновить историю",
"No history": "Нет истории",
"noHistory": "Нет истории",
"Import from browser": "Импорт из браузера",
"releases": "Релизы",
"Are you sure?": "Вы уверены?",

View file

@ -25,7 +25,7 @@
"Import history": "Importovať históriu",
"Clear history": "Odstrániť históriu",
"Refresh history": "Aktualizovať históriu",
"No history": "Žiadna história",
"noHistory": "Žiadna história",
"Import from browser": "Importovať z prehliadača",
"releases": "Vydania",
"Are you sure?": "Ste si istý?",

View file

@ -25,7 +25,7 @@
"Import history": "Увези историјат",
"Clear history": "Очисти историју",
"Refresh history": "Освежи историју",
"No history": "Нема историје",
"noHistory": "Нема историје",
"Import from browser": "Увези из прегледача",
"releases": "Издања",
"Are you sure?": "Јесте ли сигурни?",

View file

@ -25,7 +25,7 @@
"Import history": "Importhistorik",
"Clear history": "Rensa historik",
"Refresh history": "Uppdatera historik",
"No history": "Ingen historik",
"noHistory": "Ingen historik",
"Import from browser": "Importera från webbläsare",
"releases": "Lanseringar",
"Are you sure?": "Är du säker?",

View file

@ -25,7 +25,7 @@
"Import history": "Geçmişi içe aktar",
"Clear history": "Geçmişi temizle",
"Refresh history": "Geçmişi yenile",
"No history": "Geçmiş yok",
"noHistory": "Geçmiş yok",
"Import from browser": "Tarayıcıdan içe aktar",
"releases": "Sürümler",
"Are you sure?": "Emin misiniz?",

View file

@ -25,7 +25,7 @@
"Import history": "Імпортувати історію",
"Clear history": "Очистити історію",
"Refresh history": "Оновити історію",
"No history": "Історія відсутня",
"noHistory": "Історія відсутня",
"Import from browser": "Імпортувати з браузера",
"releases": "Релізи",
"Are you sure?": "Ви впевнені?",

View file

@ -25,7 +25,7 @@
"Import history": "Nhập lịch sử",
"Clear history": "Xóa lịch sử",
"Refresh history": "Làm mới lịch sử",
"No history": "Không có lịch sử",
"noHistory": "Không có lịch sử",
"Import from browser": "Nhập từ trình duyệt",
"releases": "Xuất bản",
"Are you sure?": "Bạn có chắc chắn không ?",

View file

@ -25,7 +25,7 @@
"Import history": "导入历史",
"Clear history": "清空历史",
"Refresh history": "刷新历史",
"No history": "无历史记录",
"noHistory": "无历史记录",
"Import from browser": "从浏览器导入",
"releases": "版本",
"Are you sure?": "您确定吗?",

View file

@ -25,7 +25,7 @@
"Import history": "匯入紀錄",
"Clear history": "清空紀錄",
"Refresh history": "更新紀錄",
"No history": "沒有紀錄",
"noHistory": "沒有紀錄",
"Import from browser": "從瀏覽器匯入",
"releases": "版本",
"Are you sure?": "你確定嗎?",

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=
<FontAwesomeIcon icon="clock"/> {moment(entry.lastVisited).fromNow()}<br/>
{formatHistoryDate(entry.lastVisited)}
<div>
{
tags.map((tag) => <Badge variant={"dark"} key={tag}>{tag}</Badge>)
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,7 +1,9 @@
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>
@ -12,10 +14,17 @@ const HistoryTable: React.FC = ({children}) => {
</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 className="d-flex flex-wrap justify-content-center">
<HistoryContent viewState={viewState} entries={sortAndFilterEntries(historyEntries)}
onPinClick={pinClick}/>
</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>
)
}
</Fragment>
)
}
export {History}

57
src/utils/historyUtils.ts Normal file
View file

@ -0,0 +1,57 @@
import {HistoryEntry} from "../components/landing/pages/history/history";
import moment from "moment";
export function sortAndFilterEntries(entries: HistoryEntry[]): HistoryEntry[] {
return sortEntries(entries);
}
function sortEntries(entries: HistoryEntry[]): HistoryEntry[] {
return entries.sort((a, b) => {
if (a.pinned && !b.pinned) {
return -1;
}
if (!a.pinned && b.pinned) {
return 1;
}
if (a.lastVisited < b.lastVisited) {
return -1;
}
if (a.lastVisited > b.lastVisited) {
return 1;
}
return 0;
})
}
export function formatHistoryDate(date: Date) {
return moment(date).format("llll")
}
export interface OldHistoryEntry {
id: string;
text: string;
time: number;
tags: string[];
pinned: boolean;
}
export function loadHistoryFromLocalStore(): HistoryEntry[] {
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)
}
}