mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-19 01:35:18 -04:00
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:
parent
5eb8ab7517
commit
83ab0bbe7e
39 changed files with 226 additions and 163 deletions
|
@ -0,0 +1,7 @@
|
|||
.history-close {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import "./close-button.scss"
|
||||
|
||||
const CloseButton: React.FC = () => {
|
||||
return (
|
||||
|
|
|
@ -11,11 +11,3 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.history-close {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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}/>;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}}/>
|
||||
|
||||
<CloseButton/>
|
||||
</td>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue