better linting (#72)

Improve linting and fix linting errors

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Philip Molares 2020-05-27 15:43:28 +02:00 committed by GitHub
parent efb6513205
commit eba59ae622
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 2413 additions and 1867 deletions

View file

@ -1,15 +1,15 @@
import {FrontendConfigState} from "../redux/frontend-config/types";
import {BackendConfigState} from "../redux/backend-config/types";
import {expectResponseCode, getBackendUrl} from "../utils/apiUtils";
import { FrontendConfigState } from '../redux/frontend-config/types'
import { BackendConfigState } from '../redux/backend-config/types'
import { expectResponseCode, getBackendUrl } from '../utils/apiUtils'
export const getBackendConfig = async () => {
const response = await fetch(getBackendUrl() + '/backend-config.json');
expectResponseCode(response);
return await response.json() as Promise<BackendConfigState>;
export const getBackendConfig: () => Promise<BackendConfigState> = async () => {
const response = await fetch(getBackendUrl() + '/backend-config.json')
expectResponseCode(response)
return await response.json() as Promise<BackendConfigState>
}
export const getFrontendConfig = async () => {
const response = await fetch('config.json');
expectResponseCode(response)
return await response.json() as Promise<FrontendConfigState>;
}
export const getFrontendConfig: () => Promise<FrontendConfigState> = async () => {
const response = await fetch('config.json')
expectResponseCode(response)
return await response.json() as Promise<FrontendConfigState>
}

View file

@ -1,67 +1,72 @@
import {expectResponseCode, getBackendUrl} from "../utils/apiUtils";
import { expectResponseCode, getBackendUrl } from '../utils/apiUtils'
export const getMe = async () => {
return fetch('/me');
export const getMe: (() => Promise<meResponse>) = async () => {
const response = await fetch('/me')
expectResponseCode(response)
return (await response.json()) as meResponse
}
export const postEmailLogin = async (email: string, password: string) => {
const response = await fetch(getBackendUrl() + "/auth/email", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
email: email,
password: password,
})
});
expectResponseCode(response);
return await response.json();
export interface meResponse {
id: string
name: string
photo: string
}
export const postLdapLogin = async (username: string, password: string) => {
const response = await fetch(getBackendUrl() + "/auth/ldap", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
username: username,
password: password,
})
export const postEmailLogin: ((email: string, password: string) => Promise<void>) = async (email, password) => {
const response = await fetch(getBackendUrl() + '/auth/email', {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
email: email,
password: password
})
})
expectResponseCode(response)
return await response.json();
expectResponseCode(response)
}
export const postOpenIdLogin = async (openId: string) => {
const response = await fetch(getBackendUrl() + "/auth/openid", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
openId: openId
})
export const postLdapLogin: ((email: string, password: string) => Promise<void>) = async (username, password) => {
const response = await fetch(getBackendUrl() + '/auth/ldap', {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
username: username,
password: password
})
})
expectResponseCode(response)
return await response.json();
expectResponseCode(response)
}
export const postOpenIdLogin: ((openid: string) => Promise<void>) = async (openId: string) => {
const response = await fetch(getBackendUrl() + '/auth/openid', {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
openId: openId
})
})
expectResponseCode(response)
}

View file

@ -1,35 +1,35 @@
import React, {Fragment, useEffect, useState} from "react";
import "./application-loader.scss";
import {LoadingScreen} from "./loading-screen";
import React, { Fragment, useEffect, useState } from 'react'
import './application-loader.scss'
import { LoadingScreen } from './loading-screen'
interface ApplicationLoaderProps {
initTasks: Promise<any>[]
initTasks: Promise<void>[]
}
export const ApplicationLoader: React.FC<ApplicationLoaderProps> = ({children, initTasks}) => {
const [failed, setFailed] = useState<boolean>(false);
const [doneTasks, setDoneTasks] = useState<number>(0);
export const ApplicationLoader: React.FC<ApplicationLoaderProps> = ({ children, initTasks }) => {
const [failed, setFailed] = useState<boolean>(false)
const [doneTasks, setDoneTasks] = useState<number>(0)
useEffect(() => {
setDoneTasks(0);
initTasks.forEach(task => {
(async () => {
try {
await task;
setDoneTasks(prevDoneTasks => {
return prevDoneTasks + 1;
})
} catch (reason) {
setFailed(true);
console.error(reason);
}
})();
})
}, [initTasks]);
const runTask:((task: Promise<void>) => (Promise<void>)) = async (task) => {
await task
setDoneTasks(prevDoneTasks => {
return prevDoneTasks + 1
})
}
return (
doneTasks < initTasks.length || initTasks.length === 0 ?
<LoadingScreen failed={failed}/> :
<Fragment>{children}</Fragment>
);
}
useEffect(() => {
setDoneTasks(0)
for (const task of initTasks) {
runTask(task).catch(reason => {
setFailed(true)
console.error(reason)
})
}
}, [initTasks])
return (
doneTasks < initTasks.length || initTasks.length === 0
? <LoadingScreen failed={failed}/>
: <Fragment>{children}</Fragment>
)
}

View file

@ -1,21 +1,21 @@
import React from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Alert} from "react-bootstrap";
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Alert } from 'react-bootstrap'
export interface LoadingScreenProps {
failed: boolean
failed: boolean
}
export const LoadingScreen: React.FC<LoadingScreenProps> = ({failed}) => {
return (
<div className="loader middle">
<div className="icon">
<FontAwesomeIcon icon="file-alt" size="6x"
className={failed ? "animation-shake" : "animation-pulse"}/>
</div>
{
failed ? <Alert variant={"danger"}>An error occured while loading the application!</Alert> : null
}
</div>
)
}
export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failed }) => {
return (
<div className="loader middle">
<div className="icon">
<FontAwesomeIcon icon="file-alt" size="6x"
className={failed ? 'animation-shake' : 'animation-pulse'}/>
</div>
{
failed ? <Alert variant={'danger'}>An error occurred while loading the application!</Alert> : null
}
</div>
)
}

View file

@ -1,27 +1,27 @@
import React, {Fragment} from "react";
import React, { Fragment } from 'react'
export interface ElementSeparatorProps {
separator: React.ReactElement
}
export const ElementSeparator: React.FC<ElementSeparatorProps> = ({children, separator}) => {
return (
<Fragment>
{
React.Children
.toArray(children)
.filter(child => child !== null)
.map((child, index) => {
return (
<Fragment>
{
(index > 0 ) ? separator : null
}
{child}
</Fragment>
)
})
}
</Fragment>
)
}
export const ElementSeparator: React.FC<ElementSeparatorProps> = ({ children, separator }) => {
return (
<Fragment>
{
React.Children
.toArray(children)
.filter(child => child !== null)
.map((child, index) => {
return (
<Fragment>
{
(index > 0) ? separator : null
}
{child}
</Fragment>
)
})
}
</Fragment>
)
}

View file

@ -1,24 +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";
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './icon-button.scss'
import { Button, ButtonProps } from 'react-bootstrap'
import { IconProp } from '../../utils/iconProp'
export interface SocialButtonProps extends ButtonProps {
icon: IconProp
onClick?: () => void
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>
)
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,28 +1,27 @@
import React, {Fragment} from "react";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React, { Fragment } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconProp } from '../../../../utils/iconProp'
export interface ExternalLinkProp {
href: string;
text: string;
icon?: IconProp;
href: string;
text: string;
icon?: IconProp;
}
export const ExternalLink: React.FC<ExternalLinkProp> = ({href, text, icon}) => {
return (
<a href={href}
target="_blank"
rel="noopener noreferrer"
className="text-light">
{
icon ?
<Fragment>
<FontAwesomeIcon icon={icon}/>&nbsp;
</Fragment>
:
null
}
{text}
</a>
)
export const ExternalLink: React.FC<ExternalLinkProp> = ({ href, text, icon }) => {
return (
<a href={href}
target="_blank"
rel="noopener noreferrer"
className="text-light">
{
icon
? <Fragment>
<FontAwesomeIcon icon={icon}/>&nbsp;
</Fragment>
: null
}
{text}
</a>
)
}

View file

@ -1,14 +1,14 @@
import React from "react";
import {LanguagePicker} from "./language-picker";
import {PoweredByLinks} from "./powered-by-links";
import {SocialLink} from "./social-links";
import React from 'react'
import { LanguagePicker } from './language-picker'
import { PoweredByLinks } from './powered-by-links'
import { SocialLink } from './social-links'
export const Footer: React.FC = () => {
return (
<footer className="text-white-50 small">
<LanguagePicker/>
<PoweredByLinks/>
<SocialLink/>
</footer>
);
return (
<footer className="text-white-50 small">
<LanguagePicker/>
<PoweredByLinks/>
<SocialLink/>
</footer>
)
}

View file

@ -1,54 +1,54 @@
import React from "react";
import {useTranslation} from "react-i18next";
import moment from "moment";
import { Form } from "react-bootstrap";
import React from 'react'
import { useTranslation } from 'react-i18next'
import moment from 'moment'
import { Form } from 'react-bootstrap'
const LanguagePicker: React.FC = () => {
const {i18n} = useTranslation();
const { i18n } = useTranslation()
const onChangeLang = (event: React.ChangeEvent<HTMLSelectElement>) => {
moment.locale(event.currentTarget.value);
i18n.changeLanguage(event.currentTarget.value);
}
const onChangeLang = async (event: React.ChangeEvent<HTMLSelectElement>) => {
moment.locale(event.currentTarget.value)
await i18n.changeLanguage(event.currentTarget.value)
}
return (
<Form.Control
as="select"
size="sm"
className="mb-2 mx-auto w-auto"
value={i18n.language}
onChange={onChangeLang}
>
<option value="en">English</option>
<option value="zh-CN"></option>
<option value="zh-TW"></option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ja"></option>
<option value="es">Español</option>
<option value="ca">Català</option>
<option value="el">Ελληνικά</option>
<option value="pt">Português</option>
<option value="it">Italiano</option>
<option value="tr">Türkçe</option>
<option value="ru">Русский</option>
<option value="nl">Nederlands</option>
<option value="hr">Hrvatski</option>
<option value="pl">Polski</option>
<option value="uk">Українська</option>
<option value="hi">ि</option>
<option value="sv">Svenska</option>
<option value="eo">Esperanto</option>
<option value="da">Dansk</option>
<option value="ko"></option>
<option value="id">Bahasa Indonesia</option>
<option value="sr">Cрпски</option>
<option value="vi">Tiếng Việt</option>
<option value="ar">العربية</option>
<option value="cs">Česky</option>
<option value="sk">Slovensky</option>
</Form.Control>
)
return (
<Form.Control
as="select"
size="sm"
className="mb-2 mx-auto w-auto"
value={i18n.language}
onChange={onChangeLang}
>
<option value="en">English</option>
<option value="zh-CN"></option>
<option value="zh-TW"></option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ja"></option>
<option value="es">Español</option>
<option value="ca">Català</option>
<option value="el">Ελληνικά</option>
<option value="pt">Português</option>
<option value="it">Italiano</option>
<option value="tr">Türkçe</option>
<option value="ru">Русский</option>
<option value="nl">Nederlands</option>
<option value="hr">Hrvatski</option>
<option value="pl">Polski</option>
<option value="uk">Українська</option>
<option value="hi">ि</option>
<option value="sv">Svenska</option>
<option value="eo">Esperanto</option>
<option value="da">Dansk</option>
<option value="ko"></option>
<option value="id">Bahasa Indonesia</option>
<option value="sr">Cрпски</option>
<option value="vi">Tiếng Việt</option>
<option value="ar">العربية</option>
<option value="cs">Česky</option>
<option value="sk">Slovensky</option>
</Form.Control>
)
}
export { LanguagePicker }

View file

@ -1,51 +1,35 @@
import {Trans, useTranslation} from "react-i18next";
import {TranslatedLink} from "./translated-link";
import React, {Fragment} from "react";
import {ExternalLink} from "./external-link";
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../redux";
import { Trans, useTranslation } from 'react-i18next'
import { TranslatedLink } from './translated-link'
import React, { Fragment } from 'react'
import { ExternalLink } from './external-link'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux'
const PoweredByLinks: React.FC = () => {
useTranslation();
const defaultLinks = [
{
href: '/s/release-notes',
i18nKey: 'releases'
},
{
href: 'https://github.com/codimd/server/tree/41b13e71b6b1d499238c04b15d65e3bd76442f1d',
i18nKey: 'sourceCode'
}
]
export const PoweredByLinks: React.FC = () => {
useTranslation()
const config = useSelector((state: ApplicationState) => state.backendConfig);
const defaultLinks =
{
releases: '/s/release-notes',
sourceCode: 'https://github.com/codimd/server/tree/41b13e71b6b1d499238c04b15d65e3bd76442f1d'
}
const specialLinks = Object.entries(config.specialLinks)
.filter(([_, value]) => value !== "")
.map(([key, value]) => {
return {
href: value,
i18nKey: key
}
})
const config = useSelector((state: ApplicationState) => state.backendConfig)
return (
<p>
<Trans i18nKey="poweredBy" components={[<ExternalLink href="https://codimd.org" text="CodiMD"/>]}/>
{
(defaultLinks.concat(specialLinks)).map(({href, i18nKey}) =>
<Fragment key={i18nKey}>
&nbsp;|&nbsp;
<TranslatedLink
href={href}
i18nKey={i18nKey}
/>
</Fragment>
)
}
</p>
)
return (
<p>
<Trans i18nKey="poweredBy" components={[<ExternalLink href="https://codimd.org" text="CodiMD"/>]}/>
{
Object.entries({ ...defaultLinks, ...(config.specialLinks) }).map(([i18nKey, href]) =>
<Fragment key={i18nKey}>
&nbsp;|&nbsp;
<TranslatedLink
href={href}
i18nKey={i18nKey}
/>
</Fragment>
)
}
</p>
)
}
export { PoweredByLinks }

View file

@ -1,20 +1,20 @@
import React from "react";
import {Trans, useTranslation} from "react-i18next";
import {ExternalLink} from "./external-link";
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { ExternalLink } from './external-link'
const SocialLink: React.FC = () => {
useTranslation();
return (
<p>
<Trans i18nKey="followUs" components={[
<ExternalLink href="https://github.com/codimd/server" icon={['fab', 'github']} text="GitHub"/>,
<ExternalLink href="https://community.codimd.org" icon={['fab', 'discourse']} text="Discourse"/>,
<ExternalLink href="https://riot.im/app/#/room/#codimd:matrix.org" icon="comment" text="Riot"/>,
<ExternalLink href="https://social.codimd.org/mastodon" icon={['fab', 'mastodon']} text="Mastodon"/>,
<ExternalLink href="https://translate.codimd.org" icon="globe" text="POEditor"/>
]}/>
</p>
)
useTranslation()
return (
<p>
<Trans i18nKey="followUs" components={[
<ExternalLink href="https://github.com/codimd/server" icon={['fab', 'github']} text="GitHub"/>,
<ExternalLink href="https://community.codimd.org" icon={['fab', 'discourse']} text="Discourse"/>,
<ExternalLink href="https://riot.im/app/#/room/#codimd:matrix.org" icon="comment" text="Riot"/>,
<ExternalLink href="https://social.codimd.org/mastodon" icon={['fab', 'mastodon']} text="Mastodon"/>,
<ExternalLink href="https://translate.codimd.org" icon="globe" text="POEditor"/>
]}/>
</p>
)
}
export { SocialLink }

View file

@ -1,21 +1,21 @@
import React from "react";
import {Trans, useTranslation} from "react-i18next";
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
export interface TranslatedLinkProps {
href: string;
i18nKey: string;
href: string;
i18nKey: string;
}
const TranslatedLink: React.FC<TranslatedLinkProps> = ({href, i18nKey}) => {
useTranslation();
return (
<a href={href}
target="_blank"
rel="noopener noreferrer"
className="text-light">
<Trans i18nKey={i18nKey}/>
</a>
)
const TranslatedLink: React.FC<TranslatedLinkProps> = ({ href, i18nKey }) => {
useTranslation()
return (
<a href={href}
target="_blank"
rel="noopener noreferrer"
className="text-light">
<Trans i18nKey={i18nKey}/>
</a>
)
}
export {TranslatedLink}
export { TranslatedLink }

View file

@ -1,30 +1,30 @@
import React from "react";
import {Redirect, Route, Switch} from "react-router-dom";
import {History} from "../pages/history/history";
import {Intro} from "../pages/intro/intro";
import {Container} from "react-bootstrap";
import {HeaderBar} from "./navigation/header-bar/header-bar";
import {Footer} from "./footer/footer";
import "./style/index.scss";
import {Login} from "../pages/login/login";
import React from 'react'
import { Redirect, Route, Switch } from 'react-router-dom'
import { History } from '../pages/history/history'
import { Intro } from '../pages/intro/intro'
import { Container } from 'react-bootstrap'
import { HeaderBar } from './navigation/header-bar/header-bar'
import { Footer } from './footer/footer'
import './style/index.scss'
import { Login } from '../pages/login/login'
export const Landing: React.FC = () => {
return (<Container>
<HeaderBar/>
<Switch>
<Route path="/history">
<History/>
</Route>
<Route path="/intro">
<Intro/>
</Route>
<Route path="/login">
<Login/>
</Route>
<Route path="/">
<Redirect to="/intro"/>
</Route>
</Switch>
<Footer/>
</Container>)
return (<Container>
<HeaderBar/>
<Switch>
<Route path="/history">
<History/>
</Route>
<Route path="/intro">
<Intro/>
</Route>
<Route path="/login">
<Login/>
</Route>
<Route path="/">
<Redirect to="/intro"/>
</Route>
</Switch>
<Footer/>
</Container>)
}

View file

@ -1,50 +1,48 @@
import React, {Fragment} from 'react'
import {Navbar} from 'react-bootstrap';
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../../redux";
import {NewUserNoteButton} from "../new-user-note-button";
import {UserDropdown} from "../user-dropdown/user-dropdown";
import {SignInButton} from "../sign-in-button";
import {NewGuestNoteButton} from "../new-guest-note-button";
import {LoginStatus} from "../../../../../redux/user/types";
import {HeaderNavLink} from "../header-nav-link";
import "./header-bar.scss";
import {Trans, useTranslation} from "react-i18next";
import React, { Fragment } from 'react'
import { Navbar } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { NewUserNoteButton } from '../new-user-note-button'
import { UserDropdown } from '../user-dropdown/user-dropdown'
import { SignInButton } from '../sign-in-button'
import { NewGuestNoteButton } from '../new-guest-note-button'
import { LoginStatus } from '../../../../../redux/user/types'
import { HeaderNavLink } from '../header-nav-link'
import './header-bar.scss'
import { Trans, useTranslation } from 'react-i18next'
const HeaderBar: React.FC = () => {
useTranslation()
const user = useSelector((state: ApplicationState) => state.user);
useTranslation()
const user = useSelector((state: ApplicationState) => state.user)
return (
<Navbar className="justify-content-between">
<div className="nav header-nav">
<HeaderNavLink to="/intro">
<Trans i18nKey="intro"/>
</HeaderNavLink>
<HeaderNavLink to="/history">
<Trans i18nKey="history"/>
</HeaderNavLink>
</div>
<div className="d-inline-flex">
{user.status === LoginStatus.forbidden ?
<Fragment>
<span className={"mr-1"}>
<NewGuestNoteButton/>
</span>
<SignInButton/>
</Fragment>
:
<Fragment>
<span className={"mr-1"}>
<NewUserNoteButton/>
</span>
<UserDropdown/>
</Fragment>
}
</div>
</Navbar>
);
return (
<Navbar className="justify-content-between">
<div className="nav header-nav">
<HeaderNavLink to="/intro">
<Trans i18nKey="intro"/>
</HeaderNavLink>
<HeaderNavLink to="/history">
<Trans i18nKey="history"/>
</HeaderNavLink>
</div>
<div className="d-inline-flex">
{user.status === LoginStatus.forbidden
? <Fragment>
<span className={'mr-1'}>
<NewGuestNoteButton/>
</span>
<SignInButton/>
</Fragment>
: <Fragment>
<span className={'mr-1'}>
<NewUserNoteButton/>
</span>
<UserDropdown/>
</Fragment>
}
</div>
</Navbar>
)
}
export {HeaderBar}
export { HeaderBar }

View file

@ -1,17 +1,17 @@
import {Nav} from "react-bootstrap";
import {LinkContainer} from "react-router-bootstrap";
import React from "react";
import { Nav } from 'react-bootstrap'
import { LinkContainer } from 'react-router-bootstrap'
import React from 'react'
export interface HeaderNavLinkProps {
to: string
to: string
}
export const HeaderNavLink: React.FC<HeaderNavLinkProps> = (props) => {
return (
<Nav.Item>
<LinkContainer to={props.to}>
<Nav.Link className="text-light" href={props.to}>{props.children}</Nav.Link>
</LinkContainer>
</Nav.Item>
);
}
return (
<Nav.Item>
<LinkContainer to={props.to}>
<Nav.Link className="text-light" href={props.to}>{props.children}</Nav.Link>
</LinkContainer>
</Nav.Item>
)
}

View file

@ -1,21 +1,21 @@
import React from "react";
import {LinkContainer} from "react-router-bootstrap";
import {Button} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Trans, useTranslation} from "react-i18next";
import React from 'react'
import { LinkContainer } from 'react-router-bootstrap'
import { Button } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Trans, useTranslation } from 'react-i18next'
export const NewGuestNoteButton: React.FC = () => {
const {i18n} = useTranslation();
return (
<LinkContainer to={'/new'} title={i18n.t("newGuestNote")}>
<Button
variant="primary"
size="sm"
className="d-inline-flex align-items-center">
<FontAwesomeIcon icon="plus" className="mr-1"/>
<span>
<Trans i18nKey='newGuestNote'/>
</span>
</Button>
</LinkContainer>)
const { i18n } = useTranslation()
return (
<LinkContainer to={'/new'} title={i18n.t('newGuestNote')}>
<Button
variant="primary"
size="sm"
className="d-inline-flex align-items-center">
<FontAwesomeIcon icon="plus" className="mr-1"/>
<span>
<Trans i18nKey='newGuestNote'/>
</span>
</Button>
</LinkContainer>)
}

View file

@ -1,22 +1,22 @@
import {LinkContainer} from "react-router-bootstrap";
import {Button} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {Trans, useTranslation} from "react-i18next";
import { LinkContainer } from 'react-router-bootstrap'
import { Button } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
export const NewUserNoteButton: React.FC = () => {
const {i18n} = useTranslation()
return (
<LinkContainer to={'/new'} title={i18n.t("newNote")}>
<Button
variant="primary"
size="sm"
className="d-inline-flex align-items-center">
<FontAwesomeIcon icon="plus" className="mr-1"/>
<span>
<Trans i18nKey='newNote'/>
</span>
</Button>
</LinkContainer>
)
const { i18n } = useTranslation()
return (
<LinkContainer to={'/new'} title={i18n.t('newNote')}>
<Button
variant="primary"
size="sm"
className="d-inline-flex align-items-center">
<FontAwesomeIcon icon="plus" className="mr-1"/>
<span>
<Trans i18nKey='newNote'/>
</span>
</Button>
</LinkContainer>
)
}

View file

@ -1,19 +1,19 @@
import React from "react";
import {Button} from "react-bootstrap";
import {Trans, useTranslation} from "react-i18next";
import {LinkContainer} from "react-router-bootstrap";
import React from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { LinkContainer } from 'react-router-bootstrap'
export const SignInButton: React.FC = () => {
const {i18n} = useTranslation();
const { i18n } = useTranslation()
return (
<LinkContainer to="/login" title={i18n.t("signIn")}>
<Button
variant="success"
size="sm"
>
<Trans i18nKey="signIn"/>
</Button>
</LinkContainer>
)
return (
<LinkContainer to="/login" title={i18n.t('signIn')}>
<Button
variant="success"
size="sm"
>
<Trans i18nKey="signIn"/>
</Button>
</LinkContainer>
)
}

View file

@ -1,52 +1,52 @@
import {Dropdown} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../../redux";
import {LinkContainer} from "react-router-bootstrap";
import {clearUser} from "../../../../../redux/user/methods";
import "./user-dropdown.scss";
import {Trans} from "react-i18next";
import { Dropdown } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { LinkContainer } from 'react-router-bootstrap'
import { clearUser } from '../../../../../redux/user/methods'
import './user-dropdown.scss'
import { Trans } from 'react-i18next'
export const UserDropdown: React.FC = () => {
const user = useSelector((state: ApplicationState) => state.user);
const user = useSelector((state: ApplicationState) => state.user)
return (
<Dropdown alignRight>
<Dropdown.Toggle size="sm" variant="dark" id="dropdown-basic">
<div className='d-inline-flex align-items-baseline'>
<img
src={user.photo}
className="user-avatar"
alt={`Avatar of ${user.name}`}
/><span>{user.name}</span>
</div>
</Dropdown.Toggle>
return (
<Dropdown alignRight>
<Dropdown.Toggle size="sm" variant="dark" id="dropdown-basic">
<div className='d-inline-flex align-items-baseline'>
<img
src={user.photo}
className="user-avatar"
alt={`Avatar of ${user.name}`}
/><span>{user.name}</span>
</div>
</Dropdown.Toggle>
<Dropdown.Menu>
<LinkContainer to={`/features`}>
<Dropdown.Item>
<FontAwesomeIcon icon="bolt" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="features"/>
</Dropdown.Item>
</LinkContainer>
<LinkContainer to={`/me/export`}>
<Dropdown.Item>
<FontAwesomeIcon icon="cloud-download-alt" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="exportUserData"/>
</Dropdown.Item>
</LinkContainer>
<Dropdown.Item href="#">
<FontAwesomeIcon icon="trash" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="deleteUser"/>
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
clearUser();
}}>
<FontAwesomeIcon icon="sign-out-alt" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="signOut"/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>)
};
<Dropdown.Menu>
<LinkContainer to={'/features'}>
<Dropdown.Item>
<FontAwesomeIcon icon="bolt" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="features"/>
</Dropdown.Item>
</LinkContainer>
<LinkContainer to={'/me/export'}>
<Dropdown.Item>
<FontAwesomeIcon icon="cloud-download-alt" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="exportUserData"/>
</Dropdown.Item>
</LinkContainer>
<Dropdown.Item href="#">
<FontAwesomeIcon icon="trash" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="deleteUser"/>
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
clearUser()
}}>
<FontAwesomeIcon icon="sign-out-alt" fixedWidth={true} className="mr-2"/>
<Trans i18nKey="signOut"/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>)
}

View file

@ -67,7 +67,7 @@
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('/src/components/indexnents/index/fonts/SourceSansPro-Light.woff') format('woff');
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('fonts/SourceSansPro-Light.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
/* vietnamese */

View file

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

View file

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

View file

@ -1,23 +1,22 @@
import React from 'react'
import {HistoryEntriesProps} from "../history-content/history-content";
import {HistoryCard} from "./history-card";
import {Pager} from '../../../../pagination/pager';
import {Row} from 'react-bootstrap';
import { HistoryEntriesProps } from '../history-content/history-content'
import { HistoryCard } from './history-card'
import { Pager } from '../../../../pagination/pager'
import { Row } from 'react-bootstrap'
export const HistoryCardList: React.FC<HistoryEntriesProps> = ({entries, onPinClick, pageIndex, onLastPageIndexChange}) => {
return (
<Row className="justify-content-center">
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
{
entries.map((entry) => (
<HistoryCard
key={entry.id}
entry={entry}
onPinClick={onPinClick}
/>))
}
</Pager>
</Row>
);
export const HistoryCardList: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, pageIndex, onLastPageIndexChange }) => {
return (
<Row className="justify-content-center">
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
{
entries.map((entry) => (
<HistoryCard
key={entry.id}
entry={entry}
onPinClick={onPinClick}
/>))
}
</Pager>
</Row>
)
}

View file

@ -1,38 +1,38 @@
import React from 'react'
import {Badge, Card} from 'react-bootstrap'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
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";
import { Badge, Card } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
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<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 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 isDark={false}/>
</div>
<Card.Body>
<div className="text-black-50">
<FontAwesomeIcon icon="clock"/> {moment(entry.lastVisited).fromNow()}<br/>
{formatHistoryDate(entry.lastVisited)}
<div>
{
entry.tags.map((tag) => <Badge variant={"dark"} className={"mr-1 mb-1"}
key={tag}>{tag}</Badge>)
}
</div>
</div>
</Card.Body>
</Card>
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 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 isDark={false}/>
</div>
)
<Card.Body>
<div className="text-black-50">
<FontAwesomeIcon icon="clock"/> {moment(entry.lastVisited).fromNow()}<br/>
{formatHistoryDate(entry.lastVisited)}
<div>
{
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'}
key={tag}>{tag}</Badge>)
}
</div>
</div>
</Card.Body>
</Card>
</div>
)
}

View file

@ -1,21 +1,21 @@
import React, {Fragment, useState} from "react";
import {HistoryEntry, pinClick} from "../history";
import {HistoryTable} from "../history-table/history-table";
import {Alert, Row} from "react-bootstrap";
import {Trans} from "react-i18next";
import {HistoryCardList} from "../history-card/history-card-list";
import {ViewStateEnum} from "../history-toolbar/history-toolbar";
import {PagerPagination} from "../../../../pagination/pager-pagination";
import React, { Fragment, useState } from 'react'
import { HistoryEntry, pinClick } from '../history'
import { HistoryTable } from '../history-table/history-table'
import { Alert, Row } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { HistoryCardList } from '../history-card/history-card-list'
import { ViewStateEnum } from '../history-toolbar/history-toolbar'
import { PagerPagination } from '../../../../pagination/pager-pagination'
export interface HistoryContentProps {
viewState: ViewStateEnum
entries: HistoryEntry[]
onPinClick: pinClick
viewState: ViewStateEnum
entries: HistoryEntry[]
onPinClick: pinClick
}
export interface HistoryEntryProps {
entry: HistoryEntry,
onPinClick: pinClick
entry: HistoryEntry,
onPinClick: pinClick
}
export interface HistoryEntriesProps {
@ -25,39 +25,38 @@ export interface HistoryEntriesProps {
onLastPageIndexChange: (lastPageIndex: number) => void
}
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries, onPinClick }) => {
const [pageIndex, setPageIndex] = useState(0)
const [lastPageIndex, setLastPageIndex] = useState(0)
export const HistoryContent: React.FC<HistoryContentProps> = ({viewState, entries, onPinClick}) => {
const [pageIndex, setPageIndex] = useState(0);
const [lastPageIndex, setLastPageIndex] = useState(0);
if (entries.length === 0) {
return (
<Row className={"justify-content-center"}>
<Alert variant={"secondary"}>
<Trans i18nKey={"noHistory"}/>
</Alert>
</Row>
);
}
const mapViewStateToComponent = (viewState: ViewStateEnum) => {
switch (viewState) {
default:
case ViewStateEnum.card:
return <HistoryCardList entries={entries} onPinClick={onPinClick} pageIndex={pageIndex}
onLastPageIndexChange={setLastPageIndex}/>
case ViewStateEnum.table:
return <HistoryTable entries={entries} onPinClick={onPinClick} pageIndex={pageIndex}
onLastPageIndexChange={setLastPageIndex}/>
}
}
if (entries.length === 0) {
return (
<Fragment>
{mapViewStateToComponent(viewState)}
<Row className="justify-content-center">
<PagerPagination numberOfPageButtonsToShowAfterAndBeforeCurrent={2} lastPageIndex={lastPageIndex}
onPageChange={setPageIndex}/>
</Row>
</Fragment>);
}
<Row className={'justify-content-center'}>
<Alert variant={'secondary'}>
<Trans i18nKey={'noHistory'}/>
</Alert>
</Row>
)
}
const mapViewStateToComponent = (viewState: ViewStateEnum) => {
switch (viewState) {
default:
case ViewStateEnum.card:
return <HistoryCardList entries={entries} onPinClick={onPinClick} pageIndex={pageIndex}
onLastPageIndexChange={setLastPageIndex}/>
case ViewStateEnum.table:
return <HistoryTable entries={entries} onPinClick={onPinClick} pageIndex={pageIndex}
onLastPageIndexChange={setLastPageIndex}/>
}
}
return (
<Fragment>
{mapViewStateToComponent(viewState)}
<Row className="justify-content-center">
<PagerPagination numberOfPageButtonsToShowAfterAndBeforeCurrent={2} lastPageIndex={lastPageIndex}
onPageChange={setPageIndex}/>
</Row>
</Fragment>)
}

View file

@ -1,30 +1,30 @@
import React from "react";
import {PinButton} from "../common/pin-button";
import {CloseButton} from "../common/close-button";
import {useTranslation} from "react-i18next";
import {HistoryEntryProps} from "../history-content/history-content";
import {formatHistoryDate} from "../../../../../utils/historyUtils";
import {Badge} from "react-bootstrap";
import React from 'react'
import { PinButton } from '../common/pin-button'
import { CloseButton } from '../common/close-button'
import { useTranslation } from 'react-i18next'
import { HistoryEntryProps } from '../history-content/history-content'
import { formatHistoryDate } from '../../../../../utils/historyUtils'
import { Badge } from 'react-bootstrap'
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({entry, onPinClick}) => {
useTranslation()
return (
<tr>
<td>{entry.title}</td>
<td>{formatHistoryDate(entry.lastVisited)}</td>
<td>
{
entry.tags.map((tag) => <Badge variant={"dark"} className={"mr-1 mb-1"}
key={tag}>{tag}</Badge>)
}
</td>
<td>
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => {
onPinClick(entry.id)
}}/>
&nbsp;
<CloseButton isDark={true}/>
</td>
</tr>
)
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick }) => {
useTranslation()
return (
<tr>
<td>{entry.title}</td>
<td>{formatHistoryDate(entry.lastVisited)}</td>
<td>
{
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'}
key={tag}>{tag}</Badge>)
}
</td>
<td>
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => {
onPinClick(entry.id)
}}/>
&nbsp;
<CloseButton isDark={true}/>
</td>
</tr>
)
}

View file

@ -1,33 +1,33 @@
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";
import {Pager} from "../../../../pagination/pager";
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'
import { Pager } from '../../../../pagination/pager'
export const HistoryTable: React.FC<HistoryEntriesProps> = ({entries, onPinClick, pageIndex, onLastPageIndexChange}) => {
return (
<Table striped bordered hover size="sm" variant="dark">
<thead>
<tr>
<th><Trans i18nKey={"title"}/></th>
<th><Trans i18nKey={"lastVisit"}/></th>
<th><Trans i18nKey={"tags"}/></th>
<th><Trans i18nKey={"actions"}/></th>
</tr>
</thead>
<tbody>
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
{
entries.map((entry) =>
<HistoryTableRow
key={entry.id}
entry={entry}
onPinClick={onPinClick}
/>)
}
</Pager>
</tbody>
</Table>
)
export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, pageIndex, onLastPageIndexChange }) => {
return (
<Table striped bordered hover size="sm" variant="dark">
<thead>
<tr>
<th><Trans i18nKey={'title'}/></th>
<th><Trans i18nKey={'lastVisit'}/></th>
<th><Trans i18nKey={'tags'}/></th>
<th><Trans i18nKey={'actions'}/></th>
</tr>
</thead>
<tbody>
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
{
entries.map((entry) =>
<HistoryTableRow
key={entry.id}
entry={entry}
onPinClick={onPinClick}
/>)
}
</Pager>
</tbody>
</Table>
)
}

View file

@ -1,19 +1,19 @@
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";
import React, { ChangeEvent, useEffect, useState } from 'react'
import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
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[]
viewState: ViewStateEnum
titleSortDirection: SortModeEnum
lastVisitedSortDirection: SortModeEnum
keywordSearch: string
selectedTags: string[]
}
export enum ViewStateEnum {
@ -27,102 +27,101 @@ export interface HistoryToolbarProps {
}
export const initState: HistoryToolbarState = {
viewState: ViewStateEnum.card,
titleSortDirection: SortModeEnum.no,
lastVisitedSortDirection: SortModeEnum.no,
keywordSearch: "",
selectedTags: []
viewState: ViewStateEnum.card,
titleSortDirection: SortModeEnum.no,
lastVisitedSortDirection: SortModeEnum.no,
keywordSearch: '',
selectedTags: []
}
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({onSettingsChange, tags}) => {
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange, tags }) => {
const [t] = useTranslation()
const [state, setState] = useState<HistoryToolbarState>(initState)
const [t] = useTranslation()
const [state, setState] = useState<HistoryToolbarState>(initState);
const titleSortChanged = (direction: SortModeEnum) => {
setState(prevState => ({
...prevState,
titleSortDirection: direction,
lastVisitedSortDirection: SortModeEnum.no
}))
}
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 lastVisitedSortChanged = (direction: SortModeEnum) => {
setState(prevState => ({
...prevState,
lastVisitedSortDirection: direction,
titleSortDirection: SortModeEnum.no
}))
}
const keywordSearchChanged = (event: ChangeEvent<HTMLInputElement>) => {
setState(prevState => ({ ...prevState, keywordSearch: event.currentTarget.value }))
}
const keywordSearchChanged = (event: ChangeEvent<HTMLInputElement>) => {
setState(prevState => ({...prevState, keywordSearch: event.currentTarget.value}));
}
const toggleViewChanged = (newViewState: ViewStateEnum) => {
setState((prevState) => ({ ...prevState, viewState: newViewState }))
}
const toggleViewChanged = (newViewState: ViewStateEnum) => {
setState((prevState) => ({...prevState, viewState: newViewState}))
}
const selectedTagsChanged = (selected: string[]) => {
setState(prevState => ({ ...prevState, selectedTags: selected }))
}
const selectedTagsChanged = (selected: string[]) => {
setState((prevState => ({...prevState, selectedTags: selected})))
}
useEffect(() => {
onSettingsChange(state)
}, [onSettingsChange, state])
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>
)
}
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

@ -1,67 +1,67 @@
import React, {Fragment, useEffect, useState} from 'react'
import {HistoryContent} from './history-content/history-content';
import {HistoryToolbar, HistoryToolbarState, initState as toolbarInitState} from './history-toolbar/history-toolbar';
import {loadHistoryFromLocalStore, sortAndFilterEntries} from "../../../../utils/historyUtils";
import {Row} from 'react-bootstrap';
import {Trans, useTranslation} from "react-i18next";
import React, { Fragment, useEffect, useState } from 'react'
import { Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { loadHistoryFromLocalStore, sortAndFilterEntries } from '../../../../utils/historyUtils'
import { HistoryContent } from './history-content/history-content'
import { HistoryToolbar, HistoryToolbarState, initState as toolbarInitState } from './history-toolbar/history-toolbar'
export interface HistoryEntry {
id: string,
title: string,
lastVisited: Date,
tags: string[],
pinned: boolean
id: string,
title: string,
lastVisited: Date,
tags: string[],
pinned: boolean
}
export type pinClick = (entryId: string) => void;
export const History: React.FC = () => {
useTranslation();
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([])
const [viewState, setViewState] = useState<HistoryToolbarState>(toolbarInitState)
useTranslation()
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([])
const [viewState, setViewState] = useState<HistoryToolbarState>(toolbarInitState)
useEffect(() => {
const history = loadHistoryFromLocalStore();
setHistoryEntries(history);
}, [])
useEffect(() => {
const history = loadHistoryFromLocalStore()
setHistoryEntries(history)
}, [])
useEffect(() => {
if (historyEntries === []) {
return;
}
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;
});
})
useEffect(() => {
if (historyEntries === []) {
return
}
window.localStorage.setItem('history', JSON.stringify(historyEntries))
}, [historyEntries])
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);
const pinClick: pinClick = (entryId: string) => {
setHistoryEntries((entries) => {
return entries.map((entry) => {
if (entry.id === entryId) {
entry.pinned = !entry.pinned
}
return entry
})
})
}
return (
<Fragment>
<h1 className="mb-4"><Trans i18nKey="history"/></h1>
<Row className={"justify-content-center mb-3"}>
<HistoryToolbar onSettingsChange={setViewState} tags={tags}/>
</Row>
<HistoryContent viewState={viewState.viewState}
entries={entriesToShow}
onPinClick={pinClick}/>
</Fragment>
)
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 className="mb-4"><Trans i18nKey="history"/></h1>
<Row className={'justify-content-center mb-3'}>
<HistoryToolbar onSettingsChange={setViewState} tags={tags}/>
</Row>
<HistoryContent viewState={viewState.viewState}
entries={entriesToShow}
onPinClick={pinClick}/>
</Fragment>
)
}

View file

@ -1,46 +1,45 @@
import {LoginStatus} from "../../../../../redux/user/types";
import {Link} from "react-router-dom";
import {Button} from "react-bootstrap";
import {Trans, useTranslation} from "react-i18next";
import React from "react";
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../../redux";
import "./cover-buttons.scss";
import React from 'react'
import { Link } from 'react-router-dom'
import { Button } from 'react-bootstrap'
import { LoginStatus } from '../../../../../redux/user/types'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import './cover-buttons.scss'
export const CoverButtons: React.FC = () => {
useTranslation();
const user = useSelector((state: ApplicationState) => state.user);
useTranslation()
const user = useSelector((state: ApplicationState) => state.user)
if (user.status === LoginStatus.ok) {
return null;
}
if (user.status === LoginStatus.ok) {
return null
}
return (
<div className="mb-5">
<Link to="/login">
<Button
className="cover-button"
variant="success"
size="lg"
>
<Trans i18nKey="signIn"/>
</Button>
</Link>
return (
<div className="mb-5">
<Link to="/login">
<Button
className="cover-button"
variant="success"
size="lg"
>
<Trans i18nKey="signIn"/>
</Button>
</Link>
<span className="m-2">
<Trans i18nKey="or"/>
</span>
<Link to="/features">
<Button
className="cover-button"
variant="primary"
size="lg"
>
<Trans i18nKey="exploreFeatures"/>
</Button>
</Link>
</div>
);
<span className="m-2">
<Trans i18nKey="or"/>
</span>
<Link to="/features">
<Button
className="cover-button"
variant="primary"
size="lg"
>
<Trans i18nKey="exploreFeatures"/>
</Button>
</Link>
</div>
)
}

View file

@ -1,37 +1,37 @@
import {Col, Row} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Trans, useTranslation} from "react-i18next";
import React from "react";
import { Link } from "react-router-dom";
import React from 'react'
import { Link } from 'react-router-dom'
import { Col, Row } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Trans, useTranslation } from 'react-i18next'
export const FeatureLinks: React.FC = () => {
useTranslation();
return (
<Row className="mb-5">
<Col md={4}>
<Link to={"/features#Share-Notes"} className="text-light">
<FontAwesomeIcon icon="bolt" size="3x"/>
<h5>
<Trans i18nKey="featureCollaboration"/>
</h5>
</Link>
</Col>
<Col md={4}>
<Link to={"/features#MathJax"} className="text-light">
<FontAwesomeIcon icon="chart-bar" size="3x"/>
<h5>
<Trans i18nKey="featureMathJax"/>
</h5>
</Link>
</Col>
<Col md={4}>
<Link to={"/features#Slide-Mode"} className="text-light">
<FontAwesomeIcon icon="tv" size="3x"/>
<h5>
<Trans i18nKey="featureSlides"/>
</h5>
</Link>
</Col>
</Row>
);
useTranslation()
return (
<Row className="mb-5">
<Col md={4}>
<Link to={'/features#Share-Notes'} className="text-light">
<FontAwesomeIcon icon="bolt" size="3x"/>
<h5>
<Trans i18nKey="featureCollaboration"/>
</h5>
</Link>
</Col>
<Col md={4}>
<Link to={'/features#MathJax'} className="text-light">
<FontAwesomeIcon icon="chart-bar" size="3x"/>
<h5>
<Trans i18nKey="featureMathJax"/>
</h5>
</Link>
</Col>
<Col md={4}>
<Link to={'/features#Slide-Mode'} className="text-light">
<FontAwesomeIcon icon="tv" size="3x"/>
<h5>
<Trans i18nKey="featureSlides"/>
</h5>
</Link>
</Col>
</Row>
)
}

View file

@ -1,29 +1,28 @@
import React from 'react'
import screenshot from './img/screenshot.png';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Trans, useTranslation} from "react-i18next";
import {FeatureLinks} from "./feature-links";
import {CoverButtons} from "./cover-buttons/cover-buttons";
import screenshot from './img/screenshot.png'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Trans, useTranslation } from 'react-i18next'
import { FeatureLinks } from './feature-links'
import { CoverButtons } from './cover-buttons/cover-buttons'
const Intro: React.FC = () => {
useTranslation();
useTranslation()
return (
<div>
<h1>
<FontAwesomeIcon icon="file-alt"/> CodiMD
</h1>
<p className="lead mb-5">
<Trans i18nKey="coverSlogan"/>
</p>
return (
<div>
<h1>
<FontAwesomeIcon icon="file-alt"/> CodiMD
</h1>
<p className="lead mb-5">
<Trans i18nKey="coverSlogan"/>
</p>
<CoverButtons/>
<CoverButtons/>
<img alt="CodiMD Screenshot" src={screenshot} className="img-fluid mb-5"/>
<FeatureLinks/>
</div>
)
<img alt="CodiMD Screenshot" src={screenshot} className="img-fluid mb-5"/>
<FeatureLinks/>
</div>
)
}
export {Intro}
export { Intro }

View file

@ -1,25 +1,25 @@
import React from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import "./social-link-button.scss";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './social-link-button.scss'
import { IconProp } from '../../../../../../utils/iconProp'
export interface SocialButtonProps {
backgroundClass: string,
href: string
icon: IconProp
title?: string
backgroundClass: string,
href: string
icon: IconProp
title?: string
}
export const SocialLinkButton: React.FC<SocialButtonProps> = ({title, backgroundClass, href, icon, children}) => {
return (
<a href={href} title={title}
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"} fixedWidth={true}/>
</span>
<span className="text-part d-flex align-items-center mx-auto">
{children}
</span>
</a>
)
export const SocialLinkButton: React.FC<SocialButtonProps> = ({ title, backgroundClass, href, icon, children }) => {
return (
<a href={href} title={title}
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'} fixedWidth={true}/>
</span>
<span className="text-part d-flex align-items-center mx-auto">
{children}
</span>
</a>
)
}

View file

@ -1,72 +1,64 @@
import {Trans, useTranslation} from "react-i18next";
import {Alert, Button, Card, Form} from "react-bootstrap";
import React, {useState} from "react";
import {postEmailLogin} from "../../../../../api/user";
import {getAndSetUser} from "../../../../../utils/apiUtils";
import { Trans, useTranslation } from 'react-i18next'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import React, { FormEvent, useState } from 'react'
import { postEmailLogin } from '../../../../../api/user'
import { getAndSetUser } from '../../../../../utils/apiUtils'
export const ViaEMail: React.FC = () => {
const {t} = useTranslation();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(false);
const { t } = useTranslation()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(false)
const doAsyncLogin = () => {
(async () => {
try {
await postEmailLogin(email, password);
await getAndSetUser();
} catch {
setError(true);
}
})();
}
const doAsyncLogin = async () => {
await postEmailLogin(email, password)
await getAndSetUser()
}
const onFormSubmit = (event: any) => {
doAsyncLogin();
event.preventDefault();
};
const onFormSubmit = (event: FormEvent) => {
doAsyncLogin().catch(() => setError(true))
event.preventDefault()
}
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{service: "E-Mail"}}/>
</Card.Title>
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{ service: 'E-Mail' }}/>
</Card.Title>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="email">
<Form.Control
isInvalid={error}
type="email"
size="sm"
placeholder={t('email')}
onChange={(event) => setEmail(event.currentTarget.value)} className="bg-dark text-white"
/>
</Form.Group>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="email">
<Form.Control
isInvalid={error}
type="email"
size="sm"
placeholder={t("email")}
onChange={(event) => setEmail(event.currentTarget.value)}
className="bg-dark text-white"
/>
</Form.Group>
<Form.Group controlId="password">
<Form.Control
isInvalid={error}
type="password"
size="sm"
placeholder={t('password')}
onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"/>
</Form.Group>
<Form.Group controlId="password">
<Form.Control
isInvalid={error}
type="password"
size="sm"
placeholder={t("password")}
onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"
/>
</Form.Group>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="errorEmailLogin"/>
</Alert>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="errorEmailLogin"/>
</Alert>
<Button
type="submit"
<Button
type="submit"
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
);
}
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
)
}

View file

@ -1,80 +1,74 @@
import React, {useState} from "react";
import {Trans, useTranslation} from "react-i18next";
import {Alert, Button, Card, Form} from "react-bootstrap";
import {postLdapLogin} from "../../../../../api/user";
import {getAndSetUser} from "../../../../../utils/apiUtils";
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../../redux";
import React, { FormEvent, useState } from 'react'
const ViaLdap: React.FC = () => {
const {t} = useTranslation();
const ldapCustomName = useSelector((state: ApplicationState) => state.backendConfig.customAuthNames.ldap);
import { Trans, useTranslation } from 'react-i18next'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { postLdapLogin } from '../../../../../api/user'
import { getAndSetUser } from '../../../../../utils/apiUtils'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(false);
export const ViaLdap: React.FC = () => {
const { t } = useTranslation()
const ldapCustomName = useSelector((state: ApplicationState) => state.backendConfig.customAuthNames.ldap)
const name = ldapCustomName ? `${ldapCustomName} (LDAP)` : "LDAP";
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(false)
const doAsyncLogin = () => {
(async () => {
try {
await postLdapLogin(username, password);
await getAndSetUser();
} catch {
setError(true);
}
})();
const name = ldapCustomName ? `${ldapCustomName} (LDAP)` : 'LDAP'
const doAsyncLogin = async () => {
try {
await postLdapLogin(username, password)
await getAndSetUser()
} catch {
setError(true)
}
}
const onFormSubmit = (event: any) => {
doAsyncLogin();
event.preventDefault();
}
const onFormSubmit = (event: FormEvent) => {
doAsyncLogin().catch(() => setError(true))
event.preventDefault()
}
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{service: name}}/>
</Card.Title>
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{ service: name }}/>
</Card.Title>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="username">
<Form.Control
isInvalid={error}
type="text"
size="sm"
placeholder={t('username')}
onChange={(event) => setUsername(event.currentTarget.value)} className="bg-dark text-white"
/>
</Form.Group>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="username">
<Form.Control
isInvalid={error}
type="text"
size="sm"
placeholder={t("username")}
onChange={(event) => setUsername(event.currentTarget.value)}
className="bg-dark text-white"
/>
</Form.Group>
<Form.Group controlId="password">
<Form.Control
isInvalid={error}
type="password"
size="sm"
placeholder={t('password')}
onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"/>
</Form.Group>
<Form.Group controlId="password">
<Form.Control
isInvalid={error}
type="password"
size="sm"
placeholder={t("password")}
onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"
/>
</Form.Group>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="errorLdapLogin"/>
</Alert>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="errorLdapLogin"/>
</Alert>
<Button
type="submit"
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
);
};
export { ViaLdap }
<Button
type="submit"
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
)
}

View file

@ -1,115 +1,115 @@
import React from "react";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import {SocialLinkButton} from "./social-link-button/social-link-button";
import React from 'react'
import { IconProp } from '../../../../../utils/iconProp'
import { SocialLinkButton } from './social-link-button/social-link-button'
export enum OneClickType {
'DROPBOX'="dropbox",
'FACEBOOK'="facebook",
'GITHUB'="github",
'GITLAB'="gitlab",
'GOOGLE'="google",
'OAUTH2'="oauth2",
'SAML'="saml",
'TWITTER'="twitter"
'DROPBOX' = 'dropbox',
'FACEBOOK' = 'facebook',
'GITHUB' = 'github',
'GITLAB' = 'gitlab',
'GOOGLE' = 'google',
'OAUTH2' = 'oauth2',
'SAML' = 'saml',
'TWITTER' = 'twitter'
}
type OneClick2Map = (oneClickType: OneClickType) => {
name: string,
icon: IconProp,
className: string,
url: string
name: string,
icon: IconProp,
className: string,
url: string
};
const buildBackendAuthUrl = (backendName: string) => {
return `https://localhost:3000/auth/${backendName}`
};
return `https://localhost:3000/auth/${backendName}`
}
const getMetadata: OneClick2Map = (oneClickType: OneClickType) => {
switch (oneClickType) {
case OneClickType.DROPBOX:
return {
name: "Dropbox",
icon: ["fab", "dropbox"],
className: "btn-social-dropbox",
url: buildBackendAuthUrl("dropbox")
}
case OneClickType.FACEBOOK:
return {
name: "Facebook",
icon: ["fab", "facebook"],
className: "btn-social-facebook",
url: buildBackendAuthUrl("facebook")
}
case OneClickType.GITHUB:
return {
name: "GitHub",
icon: ["fab", "github"],
className: "btn-social-github",
url: buildBackendAuthUrl("github")
}
case OneClickType.GITLAB:
return {
name: "GitLab",
icon: ["fab", "gitlab"],
className: "btn-social-gitlab",
url: buildBackendAuthUrl("gitlab")
}
case OneClickType.GOOGLE:
return {
name: "Google",
icon: ["fab", "google"],
className: "btn-social-google",
url: buildBackendAuthUrl("google")
}
case OneClickType.OAUTH2:
return {
name: "OAuth2",
icon: "address-card",
className: "btn-primary",
url: buildBackendAuthUrl("oauth2")
}
case OneClickType.SAML:
return {
name: "SAML",
icon: "users",
className: "btn-success",
url: buildBackendAuthUrl("saml")
}
case OneClickType.TWITTER:
return {
name: "Twitter",
icon: ["fab", "twitter"],
className: "btn-social-twitter",
url: buildBackendAuthUrl("twitter")
}
default:
return {
name: "",
icon: "exclamation",
className: "",
url: "#"
}
}
switch (oneClickType) {
case OneClickType.DROPBOX:
return {
name: 'Dropbox',
icon: ['fab', 'dropbox'],
className: 'btn-social-dropbox',
url: buildBackendAuthUrl('dropbox')
}
case OneClickType.FACEBOOK:
return {
name: 'Facebook',
icon: ['fab', 'facebook'],
className: 'btn-social-facebook',
url: buildBackendAuthUrl('facebook')
}
case OneClickType.GITHUB:
return {
name: 'GitHub',
icon: ['fab', 'github'],
className: 'btn-social-github',
url: buildBackendAuthUrl('github')
}
case OneClickType.GITLAB:
return {
name: 'GitLab',
icon: ['fab', 'gitlab'],
className: 'btn-social-gitlab',
url: buildBackendAuthUrl('gitlab')
}
case OneClickType.GOOGLE:
return {
name: 'Google',
icon: ['fab', 'google'],
className: 'btn-social-google',
url: buildBackendAuthUrl('google')
}
case OneClickType.OAUTH2:
return {
name: 'OAuth2',
icon: 'address-card',
className: 'btn-primary',
url: buildBackendAuthUrl('oauth2')
}
case OneClickType.SAML:
return {
name: 'SAML',
icon: 'users',
className: 'btn-success',
url: buildBackendAuthUrl('saml')
}
case OneClickType.TWITTER:
return {
name: 'Twitter',
icon: ['fab', 'twitter'],
className: 'btn-social-twitter',
url: buildBackendAuthUrl('twitter')
}
default:
return {
name: '',
icon: 'exclamation',
className: '',
url: '#'
}
}
}
export interface ViaOneClickProps {
oneClickType: OneClickType;
optionalName?: string;
oneClickType: OneClickType;
optionalName?: string;
}
const ViaOneClick: React.FC<ViaOneClickProps> = ({oneClickType, optionalName}) => {
const {name, icon, className, url} = getMetadata(oneClickType);
const text = !!optionalName ? optionalName : name;
return (
<SocialLinkButton
backgroundClass={className}
icon={icon}
href={url}
title={text}
>
{text}
</SocialLinkButton>
)
const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName }) => {
const { name, icon, className, url } = getMetadata(oneClickType)
const text = optionalName || name
return (
<SocialLinkButton
backgroundClass={className}
icon={icon}
href={url}
title={text}
>
{text}
</SocialLinkButton>
)
}
export {ViaOneClick}
export { ViaOneClick }

View file

@ -1,61 +1,53 @@
import React, {useState} from "react";
import {Trans, useTranslation} from "react-i18next";
import {Alert, Button, Card, Form} from "react-bootstrap";
import {postOpenIdLogin} from "../../../../../api/user";
import {getAndSetUser} from "../../../../../utils/apiUtils";
import React, { FormEvent, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { postOpenIdLogin } from '../../../../../api/user'
import { getAndSetUser } from '../../../../../utils/apiUtils'
const ViaOpenId: React.FC = () => {
useTranslation();
const [openId, setOpenId] = useState("");
const [error, setError] = useState(false);
const doAsyncLogin = () => {
(async () => {
try {
await postOpenIdLogin(openId);
await getAndSetUser();
} catch {
setError(true);
}
})();
}
export const ViaOpenId: React.FC = () => {
useTranslation()
const [openId, setOpenId] = useState('')
const [error, setError] = useState(false)
const doAsyncLogin: (() => Promise<void>) = async () => {
await postOpenIdLogin(openId)
await getAndSetUser()
}
const onFormSubmit = (event: any) => {
doAsyncLogin();
event.preventDefault();
}
const onFormSubmit = (event: FormEvent) => {
doAsyncLogin().catch(() => setError(true))
event.preventDefault()
}
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{service: "OpenID"}}/>
</Card.Title>
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{ service: 'OpenID' }}/>
</Card.Title>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="openid">
<Form.Control
isInvalid={error}
type="text"
size="sm"
placeholder={"OpenID"}
onChange={(event) => setOpenId(event.currentTarget.value)}
className="bg-dark text-white"
/>
</Form.Group>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="openid">
<Form.Control
isInvalid={error}
type="text"
size="sm"
placeholder={'OpenID'}
onChange={(event) => setOpenId(event.currentTarget.value)}
className="bg-dark text-white"
/>
</Form.Group>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="errorOpenIdLogin"/>
</Alert>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="errorOpenIdLogin"/>
</Alert>
<Button
type="submit"
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
);
};
export { ViaOpenId }
<Button
type="submit"
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
)
}

View file

@ -1,83 +1,81 @@
import React from "react"
import {Card, Col, Row} from "react-bootstrap"
import {Trans, useTranslation} from "react-i18next";
import {ViaEMail} from "./auth/via-email";
import {OneClickType, ViaOneClick} from "./auth/via-one-click";
import {ViaLdap} from "./auth/via-ldap";
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../redux";
import {ViaOpenId} from "./auth/via-openid";
import {Redirect} from "react-router";
import {LoginStatus} from "../../../../redux/user/types";
import React from 'react'
import { Card, Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ViaEMail } from './auth/via-email'
import { OneClickType, ViaOneClick } from './auth/via-one-click'
import { ViaLdap } from './auth/via-ldap'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux'
import { ViaOpenId } from './auth/via-openid'
import { Redirect } from 'react-router'
import { LoginStatus } from '../../../../redux/user/types'
const Login: React.FC = () => {
useTranslation();
const authProviders = useSelector((state: ApplicationState) => state.backendConfig.authProviders);
const customAuthNames = useSelector((state: ApplicationState) => state.backendConfig.customAuthNames);
const userLoginState = useSelector((state: ApplicationState) => state.user.status);
const emailForm = authProviders.email ? <ViaEMail/> : null
const ldapForm = authProviders.ldap ? <ViaLdap/> : null
const openIdForm = authProviders.openid ? <ViaOpenId/> : null
export const Login: React.FC = () => {
useTranslation()
const authProviders = useSelector((state: ApplicationState) => state.backendConfig.authProviders)
const customAuthNames = useSelector((state: ApplicationState) => state.backendConfig.customAuthNames)
const userLoginState = useSelector((state: ApplicationState) => state.user.status)
const emailForm = authProviders.email ? <ViaEMail/> : null
const ldapForm = authProviders.ldap ? <ViaLdap/> : null
const openIdForm = authProviders.openid ? <ViaOpenId/> : null
const oneClickCustomName: (type: OneClickType) => string | undefined = (type) => {
switch (type) {
case OneClickType.SAML:
return customAuthNames.saml;
case OneClickType.OAUTH2:
return customAuthNames.oauth2;
default:
return undefined;
}
}
if (userLoginState === LoginStatus.ok) {
// TODO Redirect to previous page?
return (
<Redirect to='/history' />
)
const oneClickCustomName: (type: OneClickType) => string | undefined = (type) => {
switch (type) {
case OneClickType.SAML:
return customAuthNames.saml
case OneClickType.OAUTH2:
return customAuthNames.oauth2
default:
return undefined
}
}
if (userLoginState === LoginStatus.ok) {
// TODO Redirect to previous page?
return (
<div className="my-3">
<Row className="h-100 flex justify-content-center">
{
authProviders.email || authProviders.ldap || authProviders.openid ?
<Col xs={12} sm={10} lg={4}>
{emailForm}
{ldapForm}
{openIdForm}
</Col>
: null
}
<Col xs={12} sm={10} lg={4}>
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{service: ""}}/>
</Card.Title>
{
Object.values(OneClickType)
.filter((value) => authProviders[value])
.map((value) => {
return (
<div
className="p-2 d-flex flex-column social-button-container"
key={value}
>
<ViaOneClick
oneClickType={value}
optionalName={oneClickCustomName(value)}
/>
</div>
)
})
}
</Card.Body>
</Card>
</Col>
</Row>
</div>
<Redirect to='/history'/>
)
}
}
export {Login}
return (
<div className="my-3">
<Row className="h-100 flex justify-content-center">
{
authProviders.email || authProviders.ldap || authProviders.openid
? <Col xs={12} sm={10} lg={4}>
{emailForm}
{ldapForm}
{openIdForm}
</Col>
: null
}
<Col xs={12} sm={10} lg={4}>
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="signInVia" values={{ service: '' }}/>
</Card.Title>
{
Object.values(OneClickType)
.filter((value) => authProviders[value])
.map((value) => {
return (
<div
className="p-2 d-flex flex-column social-button-container"
key={value}
>
<ViaOneClick
oneClickType={value}
optionalName={oneClickCustomName(value)}
/>
</div>
)
})
}
</Card.Body>
</Card>
</Col>
</Row>
</div>
)
}

View file

@ -1,17 +1,16 @@
import React from "react";
import React from 'react'
export interface PageItemProps {
onClick: (index: number) => void
index: number
}
export const PagerItem: React.FC<PageItemProps> = ({index, onClick}) => {
return (
<li className="page-item">
<span className="page-link" role="button" onClick={() => onClick(index)}>
{index + 1}
</span>
</li>
);
}
export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => {
return (
<li className="page-item">
<span className="page-link" role="button" onClick={() => onClick(index)}>
{index + 1}
</span>
</li>
)
}

View file

@ -1,6 +1,6 @@
import React, {Fragment, useEffect, useState} from "react";
import {Pagination} from "react-bootstrap";
import {PagerItem} from "./pager-item";
import React, { Fragment, useEffect, useState } from 'react'
import { Pagination } from 'react-bootstrap'
import { PagerItem } from './pager-item'
export interface PaginationProps {
numberOfPageButtonsToShowAfterAndBeforeCurrent: number
@ -8,75 +8,75 @@ export interface PaginationProps {
lastPageIndex: number
}
export const PagerPagination: React.FC<PaginationProps> = ({numberOfPageButtonsToShowAfterAndBeforeCurrent, onPageChange, lastPageIndex}) => {
if (numberOfPageButtonsToShowAfterAndBeforeCurrent % 2 !== 0) {
throw new Error("number of pages to show must be even!")
}
export const PagerPagination: React.FC<PaginationProps> = ({ numberOfPageButtonsToShowAfterAndBeforeCurrent, onPageChange, lastPageIndex }) => {
if (numberOfPageButtonsToShowAfterAndBeforeCurrent % 2 !== 0) {
throw new Error('number of pages to show must be even!')
}
const [pageIndex, setPageIndex] = useState(0);
const correctedPageIndex = Math.min(pageIndex, lastPageIndex);
const wantedUpperPageIndex = correctedPageIndex + numberOfPageButtonsToShowAfterAndBeforeCurrent;
const wantedLowerPageIndex = correctedPageIndex - numberOfPageButtonsToShowAfterAndBeforeCurrent;
const [pageIndex, setPageIndex] = useState(0)
const correctedPageIndex = Math.min(pageIndex, lastPageIndex)
const wantedUpperPageIndex = correctedPageIndex + numberOfPageButtonsToShowAfterAndBeforeCurrent
const wantedLowerPageIndex = correctedPageIndex - numberOfPageButtonsToShowAfterAndBeforeCurrent
useEffect(() => {
onPageChange(pageIndex)
}, [onPageChange, pageIndex])
useEffect(() => {
onPageChange(pageIndex)
}, [onPageChange, pageIndex])
const correctedLowerPageIndex =
Math.min(
Math.max(
Math.min(
wantedLowerPageIndex,
wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex
),
0
),
lastPageIndex
);
const correctedUpperPageIndex =
const correctedLowerPageIndex =
Math.min(
Math.max(
Math.min(
Math.max(
wantedUpperPageIndex,
wantedUpperPageIndex - wantedLowerPageIndex
),
lastPageIndex
),
0
);
Math.min(
wantedLowerPageIndex,
wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex
),
0
),
lastPageIndex
)
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex)).map((k, index) => {
const itemIndex = correctedLowerPageIndex + index;
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex}/>
});
const correctedUpperPageIndex =
Math.max(
Math.min(
Math.max(
wantedUpperPageIndex,
wantedUpperPageIndex - wantedLowerPageIndex
),
lastPageIndex
),
0
)
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex)).map((k, index) => {
const itemIndex = correctedPageIndex + index + 1;
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex}/>
});
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex)).map((k, index) => {
const itemIndex = correctedLowerPageIndex + index
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex}/>
})
return (
<Pagination>
{
correctedLowerPageIndex > 0 ?
<Fragment>
<PagerItem key={0} index={0} onClick={setPageIndex}/>
<Pagination.Ellipsis disabled/>
</Fragment>
: null
}
{paginationItemsBefore}
<Pagination.Item active>{correctedPageIndex + 1}</Pagination.Item>
{paginationItemsAfter}
{
correctedUpperPageIndex < lastPageIndex ?
<Fragment>
<Pagination.Ellipsis disabled/>
<PagerItem key={lastPageIndex} index={lastPageIndex} onClick={setPageIndex}/>
</Fragment>
: null
}
</Pagination>
);
}
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex)).map((k, index) => {
const itemIndex = correctedPageIndex + index + 1
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex}/>
})
return (
<Pagination>
{
correctedLowerPageIndex > 0
? <Fragment>
<PagerItem key={0} index={0} onClick={setPageIndex}/>
<Pagination.Ellipsis disabled/>
</Fragment>
: null
}
{paginationItemsBefore}
<Pagination.Item active>{correctedPageIndex + 1}</Pagination.Item>
{paginationItemsAfter}
{
correctedUpperPageIndex < lastPageIndex
? <Fragment>
<Pagination.Ellipsis disabled/>
<PagerItem key={lastPageIndex} index={lastPageIndex} onClick={setPageIndex}/>
</Fragment>
: null
}
</Pagination>
)
}

View file

@ -1,4 +1,4 @@
import React, {Fragment, useEffect} from "react";
import React, { Fragment, useEffect } from 'react'
export interface PagerPageProps {
pageIndex: number
@ -6,19 +6,18 @@ export interface PagerPageProps {
onLastPageIndexChange: (lastPageIndex: number) => void
}
export const Pager: React.FC<PagerPageProps> = ({children, numberOfElementsPerPage, pageIndex, onLastPageIndexChange}) => {
export const Pager: React.FC<PagerPageProps> = ({ children, numberOfElementsPerPage, pageIndex, onLastPageIndexChange }) => {
useEffect(() => {
const lastPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1
onLastPageIndexChange(lastPageIndex)
}, [children, numberOfElementsPerPage, onLastPageIndexChange])
useEffect(() => {
const lastPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1;
onLastPageIndexChange(lastPageIndex)
}, [children, numberOfElementsPerPage, onLastPageIndexChange])
return <Fragment>
{
React.Children.toArray(children).filter((value, index) => {
const pageOfElement = Math.floor((index) / numberOfElementsPerPage);
return (pageOfElement === pageIndex);
})
}
</Fragment>
}
return <Fragment>
{
React.Children.toArray(children).filter((value, index) => {
const pageOfElement = Math.floor((index) / numberOfElementsPerPage)
return (pageOfElement === pageIndex)
})
}
</Fragment>
}

View file

@ -1,7 +1,7 @@
import React from "react";
import {IconProp} from "@fortawesome/fontawesome-svg-core";
import {ButtonProps} from "react-bootstrap";
import {IconButton} from "../icon-button/icon-button";
import React from 'react'
import { ButtonProps } from 'react-bootstrap'
import { IconProp } from '../../utils/iconProp'
import { IconButton } from '../icon-button/icon-button'
export enum SortModeEnum {
up = 1,
@ -10,15 +10,15 @@ export enum SortModeEnum {
}
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";
}
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 {
@ -27,21 +27,21 @@ export interface SortButtonProps extends ButtonProps {
}
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;
}
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));
}
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>;
return <IconButton onClick={toggleSort} variant={variant} icon={getIcon(direction)}>{children}</IconButton>
}

View file

@ -1,27 +1,27 @@
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router} from 'react-router-dom'
import * as serviceWorker from './service-worker';
import {Landing} from "./components/landing/layout";
import {ApplicationLoader} from "./components/application-loader/application-loader";
import {Provider} from "react-redux";
import {store} from "./utils/store";
import {setUp} from "./initializers";
import { BrowserRouter as Router } from 'react-router-dom'
import * as serviceWorker from './service-worker'
import { Landing } from './components/landing/layout'
import { ApplicationLoader } from './components/application-loader/application-loader'
import { Provider } from 'react-redux'
import { store } from './utils/store'
import { setUp } from './initializers'
const initTasks = setUp();
const initTasks = setUp()
ReactDOM.render(
<Provider store={store}>
<ApplicationLoader initTasks={initTasks}>
<Router>
<Landing/>
</Router>
</ApplicationLoader>
</Provider>
, document.getElementById('root')
);
<Provider store={store}>
<ApplicationLoader initTasks={initTasks}>
<Router>
<Landing/>
</Router>
</ApplicationLoader>
</Provider>
, document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
serviceWorker.unregister()

View file

@ -1,20 +1,20 @@
import {getBackendConfig, getFrontendConfig} from "../api/config";
import {setFrontendConfig} from "../redux/frontend-config/methods";
import {setBackendConfig} from "../redux/backend-config/methods";
import {getAndSetUser} from "../utils/apiUtils";
import { getBackendConfig, getFrontendConfig } from '../api/config'
import { setFrontendConfig } from '../redux/frontend-config/methods'
import { setBackendConfig } from '../redux/backend-config/methods'
import { getAndSetUser } from '../utils/apiUtils'
export async function loadAllConfig() {
const frontendConfig = await getFrontendConfig();
if (!frontendConfig) {
return Promise.reject("Frontend config empty!");
}
setFrontendConfig(frontendConfig);
export const loadAllConfig: () => Promise<void> = async () => {
const frontendConfig = await getFrontendConfig()
if (!frontendConfig) {
return Promise.reject(new Error('Frontend config empty!'))
}
setFrontendConfig(frontendConfig)
const backendConfig = await getBackendConfig()
if (!backendConfig) {
return Promise.reject("Backend config empty!");
}
setBackendConfig(backendConfig)
const backendConfig = await getBackendConfig()
if (!backendConfig) {
return Promise.reject(new Error('Backend config empty!'))
}
setBackendConfig(backendConfig)
await getAndSetUser();
}
await getAndSetUser()
}

View file

@ -1,41 +1,41 @@
import {library} from "@fortawesome/fontawesome-svg-core";
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAddressCard,
faBolt,
faChartBar,
faClock,
faCloudDownloadAlt,
faComment,
faDownload,
faFileAlt,
faGlobe,
faPlus,
faSignOutAlt,
faSort,
faSortDown,
faSortUp,
faSync,
faThumbtack,
faTimes,
faTrash,
faTv,
faUpload,
faUsers,
} from "@fortawesome/free-solid-svg-icons";
faAddressCard,
faBolt,
faChartBar,
faClock,
faCloudDownloadAlt,
faComment,
faDownload,
faFileAlt,
faGlobe,
faPlus,
faSignOutAlt,
faSort,
faSortDown,
faSortUp,
faSync,
faThumbtack,
faTimes,
faTrash,
faTv,
faUpload,
faUsers
} from '@fortawesome/free-solid-svg-icons'
import {
faDiscourse,
faDropbox,
faFacebook,
faGithub,
faGitlab,
faGoogle,
faMastodon,
faTwitter
} from "@fortawesome/free-brands-svg-icons";
faDiscourse,
faDropbox,
faFacebook,
faGithub,
faGitlab,
faGoogle,
faMastodon,
faTwitter
} from '@fortawesome/free-brands-svg-icons'
export function setUpFontAwesome() {
library.add(faBolt, faPlus, faChartBar, faTv, faFileAlt, faCloudDownloadAlt,
faTrash, faSignOutAlt, faComment, faDiscourse, faMastodon, faGlobe,
faThumbtack, faClock, faTimes, faGithub, faGitlab, faGoogle, faFacebook,
faDropbox, faTwitter, faUsers, faAddressCard, faSort, faDownload, faUpload, faTrash, faSync, faSortUp, faSortDown)
export const setUpFontAwesome: () => void = () => {
library.add(faBolt, faPlus, faChartBar, faTv, faFileAlt, faCloudDownloadAlt,
faTrash, faSignOutAlt, faComment, faDiscourse, faMastodon, faGlobe,
faThumbtack, faClock, faTimes, faGithub, faGitlab, faGoogle, faFacebook,
faDropbox, faTwitter, faUsers, faAddressCard, faSort, faDownload, faUpload, faTrash, faSync, faSortUp, faSortDown)
}

View file

@ -1,54 +1,52 @@
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import {initReactI18next} from 'react-i18next';
import moment from "moment";
import "moment/locale/ar";
import "moment/locale/ca";
import "moment/locale/cs";
import "moment/locale/da";
import "moment/locale/de";
import "moment/locale/el";
import "moment/locale/eo";
import "moment/locale/es";
import "moment/locale/fr";
import "moment/locale/hi";
import "moment/locale/hr";
import "moment/locale/id";
import "moment/locale/it";
import "moment/locale/ja";
import "moment/locale/ko";
import "moment/locale/nl";
import "moment/locale/pl";
import "moment/locale/pt";
import "moment/locale/ru";
import "moment/locale/sk";
import "moment/locale/sr";
import "moment/locale/sv";
import "moment/locale/tr";
import "moment/locale/uk";
import "moment/locale/vi";
import "moment/locale/zh-cn";
import "moment/locale/zh-tw";
import i18n from 'i18next'
import Backend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import moment from 'moment'
import 'moment/locale/ar'
import 'moment/locale/ca'
import 'moment/locale/cs'
import 'moment/locale/da'
import 'moment/locale/de'
import 'moment/locale/el'
import 'moment/locale/eo'
import 'moment/locale/es'
import 'moment/locale/fr'
import 'moment/locale/hi'
import 'moment/locale/hr'
import 'moment/locale/id'
import 'moment/locale/it'
import 'moment/locale/ja'
import 'moment/locale/ko'
import 'moment/locale/nl'
import 'moment/locale/pl'
import 'moment/locale/pt'
import 'moment/locale/ru'
import 'moment/locale/sk'
import 'moment/locale/sr'
import 'moment/locale/sv'
import 'moment/locale/tr'
import 'moment/locale/uk'
import 'moment/locale/vi'
import 'moment/locale/zh-cn'
import 'moment/locale/zh-tw'
export async function setUpI18n() {
await i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
backend: {
loadPath: '/locales/{{lng}}.json',
},
export const setUpI18n: () => Promise<void> = async () => {
await i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
backend: {
loadPath: '/locales/{{lng}}.json'
},
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
})
interpolation: {
escapeValue: false // not needed for react as it escapes by default
}
})
moment.locale(i18n.language);
moment.locale(i18n.language)
}

View file

@ -1,16 +1,16 @@
import {setUpFontAwesome} from "./fontAwesome";
import {setUpI18n} from "./i18n";
import {loadAllConfig} from "./configLoader";
import { setUpFontAwesome } from './fontAwesome'
import { setUpI18n } from './i18n'
import { loadAllConfig } from './configLoader'
function customDelay() {
if (!!window.localStorage.getItem("customDelay")) {
return new Promise((resolve => setTimeout(resolve, 5000)));
} else {
return Promise.resolve()
}
const customDelay: () => Promise<void> = async () => {
if (window.localStorage.getItem('customDelay')) {
return new Promise(resolve => setTimeout(resolve, 5000))
} else {
return Promise.resolve()
}
}
export function setUp() {
setUpFontAwesome();
return [setUpI18n(), loadAllConfig(), customDelay()]
}
export const setUp: () => Promise<void>[] = () => {
setUpFontAwesome()
return [setUpI18n(), loadAllConfig(), customDelay()]
}

View file

@ -1,12 +1,12 @@
import {BackendConfigState, SET_BACKEND_CONFIG_ACTION_TYPE, SetBackendConfigAction} from "./types";
import {store} from "../../utils/store";
import { BackendConfigState, SET_BACKEND_CONFIG_ACTION_TYPE, SetBackendConfigAction } from './types'
import { store } from '../../utils/store'
export const setBackendConfig = (state: BackendConfigState) => {
const action: SetBackendConfigAction = {
type: SET_BACKEND_CONFIG_ACTION_TYPE,
payload: {
state
}
};
store.dispatch(action)
export const setBackendConfig: (state: BackendConfigState) => void = (state: BackendConfigState) => {
const action: SetBackendConfigAction = {
type: SET_BACKEND_CONFIG_ACTION_TYPE,
payload: {
state
}
}
store.dispatch(action)
}

View file

@ -1,38 +1,38 @@
import {Reducer} from 'redux';
import {BackendConfigActions, BackendConfigState, SET_BACKEND_CONFIG_ACTION_TYPE} from './types';
import { Reducer } from 'redux'
import { BackendConfigActions, BackendConfigState, SET_BACKEND_CONFIG_ACTION_TYPE } from './types'
export const initialState: BackendConfigState = {
allowAnonymous: true,
authProviders: {
facebook: false,
github: false,
twitter: false,
gitlab: false,
dropbox: false,
ldap: false,
google: false,
saml: false,
oauth2: false,
email: false,
openid: false
},
customAuthNames: {
ldap: "",
oauth2: "",
saml: ""
},
specialLinks: {
privacy: "",
termsOfUse: "",
imprint: "",
}
};
allowAnonymous: true,
authProviders: {
facebook: false,
github: false,
twitter: false,
gitlab: false,
dropbox: false,
ldap: false,
google: false,
saml: false,
oauth2: false,
email: false,
openid: false
},
customAuthNames: {
ldap: '',
oauth2: '',
saml: ''
},
specialLinks: {
privacy: '',
termsOfUse: '',
imprint: ''
}
}
export const BackendConfigReducer: Reducer<BackendConfigState, BackendConfigActions> = (state: BackendConfigState = initialState, action: BackendConfigActions) => {
switch (action.type) {
case SET_BACKEND_CONFIG_ACTION_TYPE:
return action.payload.state;
default:
return state;
}
};
switch (action.type) {
case SET_BACKEND_CONFIG_ACTION_TYPE:
return action.payload.state
default:
return state
}
}

View file

@ -1,6 +1,6 @@
import {Action} from "redux";
import { Action } from 'redux'
export const SET_BACKEND_CONFIG_ACTION_TYPE = 'backend-config/set';
export const SET_BACKEND_CONFIG_ACTION_TYPE = 'backend-config/set'
export interface BackendConfigState {
allowAnonymous: boolean,

View file

@ -1,12 +1,12 @@
import {FrontendConfigState, SET_FRONTEND_CONFIG_ACTION_TYPE, SetFrontendConfigAction} from "./types";
import {store} from "../../utils/store";
import { FrontendConfigState, SET_FRONTEND_CONFIG_ACTION_TYPE, SetFrontendConfigAction } from './types'
import { store } from '../../utils/store'
export const setFrontendConfig = (state: FrontendConfigState) => {
const action: SetFrontendConfigAction = {
type: SET_FRONTEND_CONFIG_ACTION_TYPE,
payload: {
state
}
export const setFrontendConfig: (state: FrontendConfigState) => void = (state: FrontendConfigState) => {
const action: SetFrontendConfigAction = {
type: SET_FRONTEND_CONFIG_ACTION_TYPE,
payload: {
state
}
store.dispatch(action);
}
store.dispatch(action)
}

View file

@ -1,15 +1,15 @@
import {Reducer} from 'redux';
import {FrontendConfigActions, FrontendConfigState, SET_FRONTEND_CONFIG_ACTION_TYPE} from './types';
import { Reducer } from 'redux'
import { FrontendConfigActions, FrontendConfigState, SET_FRONTEND_CONFIG_ACTION_TYPE } from './types'
export const initialState: FrontendConfigState = {
backendUrl: ""
};
backendUrl: ''
}
export const FrontendConfigReducer: Reducer<FrontendConfigState, FrontendConfigActions> = (state: FrontendConfigState = initialState, action: FrontendConfigActions) => {
switch (action.type) {
case SET_FRONTEND_CONFIG_ACTION_TYPE:
return action.payload.state;
default:
return state;
}
};
switch (action.type) {
case SET_FRONTEND_CONFIG_ACTION_TYPE:
return action.payload.state
default:
return state
}
}

View file

@ -1,6 +1,6 @@
import {Action} from "redux";
import { Action } from 'redux'
export const SET_FRONTEND_CONFIG_ACTION_TYPE = 'frontend-config/set';
export const SET_FRONTEND_CONFIG_ACTION_TYPE = 'frontend-config/set'
export interface SetFrontendConfigAction extends Action {
type: string;

View file

@ -1,12 +1,12 @@
import {combineReducers, Reducer} from 'redux';
import {UserState} from "./user/types";
import {UserReducer} from "./user/reducers";
import {ModalShowReducer} from "./modal/reducers";
import {ModalShowState} from "./modal/types";
import {BackendConfigState} from "./backend-config/types";
import {FrontendConfigState} from "./frontend-config/types";
import {BackendConfigReducer} from "./backend-config/reducers";
import {FrontendConfigReducer} from "./frontend-config/reducers";
import { combineReducers, Reducer } from 'redux'
import { UserState } from './user/types'
import { UserReducer } from './user/reducers'
import { ModalShowReducer } from './modal/reducers'
import { ModalShowState } from './modal/types'
import { BackendConfigState } from './backend-config/types'
import { FrontendConfigState } from './frontend-config/types'
import { BackendConfigReducer } from './backend-config/reducers'
import { FrontendConfigReducer } from './frontend-config/reducers'
export interface ApplicationState {
user: UserState;
@ -16,8 +16,8 @@ export interface ApplicationState {
}
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
user: UserReducer,
modalShow: ModalShowReducer,
backendConfig: BackendConfigReducer,
frontendConfig: FrontendConfigReducer
});
user: UserReducer,
modalShow: ModalShowReducer,
backendConfig: BackendConfigReducer,
frontendConfig: FrontendConfigReducer
})

View file

@ -1,7 +1,7 @@
import {ActionCreator} from "redux";
import {SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE, SetHistoryDeleteModalShowAction} from "./types";
import { ActionCreator } from 'redux'
import { SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE, SetHistoryDeleteModalShowAction } from './types'
export const setSignInModalShow: ActionCreator<SetHistoryDeleteModalShowAction> = (historyDelete: boolean) => ({
type: SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE,
payload: historyDelete,
type: SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE,
payload: historyDelete
})

View file

@ -1,18 +1,18 @@
import {Reducer} from 'redux';
import {ModalShowActions, ModalShowState, SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE} from './types';
import { Reducer } from 'redux'
import { ModalShowActions, ModalShowState, SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE } from './types'
export const initialState: ModalShowState = {
historyDelete: false
};
historyDelete: false
}
export const ModalShowReducer: Reducer<ModalShowState, ModalShowActions> = (state: ModalShowState = initialState, action: ModalShowActions) => {
switch (action.type) {
case SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE:
return {
...state,
historyDelete: action.payload
};
default:
return state;
}
};
switch (action.type) {
case SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE:
return {
...state,
historyDelete: action.payload
}
default:
return state
}
}

View file

@ -1,6 +1,6 @@
import {Action} from "redux";
import { Action } from 'redux'
export const SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE = 'modal/history-delete/set';
export const SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE = 'modal/history-delete/set'
export interface ModalShowState {
historyDelete: boolean

View file

@ -1,20 +1,20 @@
import {CLEAR_USER_ACTION_TYPE, ClearUserAction, SET_USER_ACTION_TYPE, SetUserAction, UserState} from "./types";
import {store} from "../../utils/store";
import { CLEAR_USER_ACTION_TYPE, ClearUserAction, SET_USER_ACTION_TYPE, SetUserAction, UserState } from './types'
import { store } from '../../utils/store'
export const setUser = (state: UserState) => {
const action: SetUserAction = {
type: SET_USER_ACTION_TYPE,
payload: {
state
}
export const setUser: (state: UserState) => void = (state: UserState) => {
const action: SetUserAction = {
type: SET_USER_ACTION_TYPE,
payload: {
state
}
store.dispatch(action);
}
store.dispatch(action)
}
export const clearUser = () => {
const action: ClearUserAction = {
type: CLEAR_USER_ACTION_TYPE,
payload: {},
}
store.dispatch(action);
}
export const clearUser: () => void = () => {
const action: ClearUserAction = {
type: CLEAR_USER_ACTION_TYPE,
payload: null
}
store.dispatch(action)
}

View file

@ -1,27 +1,20 @@
import {Reducer} from 'redux';
import {
CLEAR_USER_ACTION_TYPE,
LoginStatus,
SET_USER_ACTION_TYPE,
SetUserAction,
UserActions,
UserState
} from './types';
import { Reducer } from 'redux'
import { CLEAR_USER_ACTION_TYPE, LoginStatus, SET_USER_ACTION_TYPE, SetUserAction, UserActions, UserState } from './types'
export const initialState: UserState = {
id: "",
name: "",
photo: "",
status: LoginStatus.forbidden
};
id: '',
name: '',
photo: '',
status: LoginStatus.forbidden
}
export const UserReducer: Reducer<UserState, UserActions> = (state: UserState = initialState, action: UserActions) => {
switch (action.type) {
case SET_USER_ACTION_TYPE:
return (action as SetUserAction).payload.state;
case CLEAR_USER_ACTION_TYPE:
return initialState;
default:
return state;
}
};
switch (action.type) {
case SET_USER_ACTION_TYPE:
return (action as SetUserAction).payload.state
case CLEAR_USER_ACTION_TYPE:
return initialState
default:
return state
}
}

View file

@ -1,7 +1,7 @@
import {Action} from "redux";
import { Action } from 'redux'
export const SET_USER_ACTION_TYPE = 'user/set';
export const CLEAR_USER_ACTION_TYPE = 'user/clear';
export const SET_USER_ACTION_TYPE = 'user/set'
export const CLEAR_USER_ACTION_TYPE = 'user/clear'
export interface SetUserAction extends Action {
type: string;
@ -12,7 +12,7 @@ export interface SetUserAction extends Action {
export interface ClearUserAction extends Action {
type: string;
payload: {};
payload: null;
}
export interface UserState {
@ -23,8 +23,8 @@ export interface UserState {
}
export enum LoginStatus {
forbidden = "forbidden",
ok = "ok"
forbidden = 'forbidden',
ok = 'ok'
}
export type UserActions = SetUserAction | ClearUserAction;

View file

@ -15,61 +15,50 @@ const isLocalhost = Boolean(
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
new RegExp(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/).exec(window.location.hostname)
)
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
export function register (config?: Config):void {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
checkValidServiceWorker(swUrl, config)
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
registerValidSW(swUrl, config)
}
});
})
}
}
function registerValidSW(swUrl: string, config?: Config) {
function registerValidSW (swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
const installingWorker = registration.installing
if (installingWorker == null) {
return;
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
@ -80,70 +69,70 @@ function registerValidSW(swUrl: string, config?: Config) {
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
console.log('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
config.onSuccess(registration)
}
}
}
};
};
}
}
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
function checkValidServiceWorker (swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
const contentType = response.headers.get('content-type')
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
return registration.unregister().then(() => {
window.location.reload()
})
}).catch(() => console.log('Service worker not ready'))
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
)
})
}
export function unregister() {
export function unregister ():void {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
return registration.unregister()
})
.catch((error:Error) => {
console.error(error.message)
})
.catch(error => {
console.error(error.message);
});
}
}

View file

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom/extend-expect'

View file

@ -1,27 +1,22 @@
import {getMe} from "../api/user";
import {LoginStatus} from "../redux/user/types";
import {setUser} from "../redux/user/methods";
import {store} from "./store";
import { getMe } from '../api/user'
import { LoginStatus } from '../redux/user/types'
import { setUser } from '../redux/user/methods'
import { store } from './store'
export const getAndSetUser = async () => {
const meResponse = await getMe();
expectResponseCode(meResponse);
const me = await meResponse.json();
if (!me) {
return;
}
setUser({
status: LoginStatus.ok,
id: me.id,
name: me.name,
photo: me.photo,
});
export const getAndSetUser: () => (Promise<void>) = async () => {
const me = await getMe()
setUser({
status: LoginStatus.ok,
id: me.id,
name: me.name,
photo: me.photo
})
}
export const getBackendUrl = () => {
return store.getState().frontendConfig.backendUrl;
export const getBackendUrl: (() => string) = () => {
return store.getState().frontendConfig.backendUrl
}
export const expectResponseCode = (response: Response, code: number = 200) => {
return (response.status !== code);
}
export const expectResponseCode: ((response: Response, code?: number) => boolean) = (response, code = 200) => {
return response.status !== code
}

View file

@ -1,61 +1,61 @@
import {HistoryEntry} from "../components/landing/pages/history/history";
import moment from "moment";
import {HistoryToolbarState} from "../components/landing/pages/history/history-toolbar/history-toolbar";
import {SortModeEnum} from "../components/sort-button/sort-button";
import { HistoryEntry } from '../components/landing/pages/history/history'
import moment from 'moment'
import { HistoryToolbarState } from '../components/landing/pages/history/history-toolbar/history-toolbar'
import { SortModeEnum } from '../components/sort-button/sort-button'
export function sortAndFilterEntries(entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] {
return sortEntries(filterByKeywordSearch(filterBySelectedTags(entries, viewState.selectedTags), viewState.keywordSearch), viewState);
export function sortAndFilterEntries (entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] {
return sortEntries(filterByKeywordSearch(filterBySelectedTags(entries, viewState.selectedTags), viewState.keywordSearch), viewState)
}
function filterBySelectedTags(entries: HistoryEntry[], selectedTags: string[]): HistoryEntry[] {
return entries.filter(entry => {
return (selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags))
}
function filterBySelectedTags (entries: HistoryEntry[], selectedTags: string[]): HistoryEntry[] {
return entries.filter(entry => {
return (selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags))
}
)
}
function arrayCommonCheck<T> (array1: T[], array2: T[]): boolean {
const foundElement = array1.find((element1) =>
array2.find((element2) =>
element2 === element1
)
)
return !!foundElement
}
function arrayCommonCheck<T>(array1: T[], array2: T[]): boolean {
const foundElement = array1.find((element1) =>
array2.find((element2) =>
element2 === element1
)
)
return !!foundElement;
function filterByKeywordSearch (entries: HistoryEntry[], keywords: string): HistoryEntry[] {
const searchTerm = keywords.toLowerCase()
return entries.filter(entry => entry.title.toLowerCase().indexOf(searchTerm) !== -1)
}
function filterByKeywordSearch(entries: HistoryEntry[], keywords: string): HistoryEntry[] {
const searchTerm = keywords.toLowerCase();
return entries.filter(entry => entry.title.toLowerCase().indexOf(searchTerm) !== -1);
function sortEntries (entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] {
return entries.sort((firstEntry, secondEntry) => {
if (firstEntry.pinned && !secondEntry.pinned) {
return -1
}
if (!firstEntry.pinned && secondEntry.pinned) {
return 1
}
if (viewState.titleSortDirection !== SortModeEnum.no) {
return firstEntry.title.localeCompare(secondEntry.title) * viewState.titleSortDirection
}
if (viewState.lastVisitedSortDirection !== SortModeEnum.no) {
if (firstEntry.lastVisited > secondEntry.lastVisited) {
return 1 * viewState.lastVisitedSortDirection
}
if (firstEntry.lastVisited < secondEntry.lastVisited) {
return -1 * viewState.lastVisitedSortDirection
}
}
return 0
})
}
function sortEntries(entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] {
return entries.sort((firstEntry, secondEntry) => {
if (firstEntry.pinned && !secondEntry.pinned) {
return -1;
}
if (!firstEntry.pinned && secondEntry.pinned) {
return 1;
}
if (viewState.titleSortDirection !== SortModeEnum.no) {
return firstEntry.title.localeCompare(secondEntry.title) * viewState.titleSortDirection;
}
if (viewState.lastVisitedSortDirection !== SortModeEnum.no) {
if (firstEntry.lastVisited > secondEntry.lastVisited) {
return 1 * viewState.lastVisitedSortDirection;
}
if (firstEntry.lastVisited < secondEntry.lastVisited) {
return -1 * viewState.lastVisitedSortDirection;
}
}
return 0;
})
}
export function formatHistoryDate(date: Date) {
return moment(date).format("llll")
export function formatHistoryDate (date: Date): string {
return moment(date).format('llll')
}
export interface OldHistoryEntry {
@ -66,23 +66,23 @@ export interface OldHistoryEntry {
pinned: boolean;
}
export function loadHistoryFromLocalStore(): HistoryEntry[] {
const historyJsonString = window.localStorage.getItem("history");
if (!historyJsonString) {
// 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)
}
}
export function loadHistoryFromLocalStore (): HistoryEntry[] {
const historyJsonString = window.localStorage.getItem('history')
if (!historyJsonString) {
// 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)) as OldHistoryEntry[] : []
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) as HistoryEntry[]
}
}

4
src/utils/iconProp.ts Normal file
View file

@ -0,0 +1,4 @@
import { IconLookup, IconName, IconPrefix } from '@fortawesome/fontawesome-common-types'
// This icon prop is a workaround, because ESLint doesn't find the font awesome IconProp
export type IconProp = IconName | [IconPrefix, IconName] | IconLookup

View file

@ -1,4 +1,4 @@
import {createStore} from "redux";
import {allReducers} from "../redux";
import { createStore } from 'redux'
import { allReducers } from '../redux'
export const store = createStore(allReducers);
export const store = createStore(allReducers)