1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-22 04:54:18 +00:00

💥 Sorry! I couldn't make an atomic commit!

This commit is contained in:
Stefano Pigozzi 2021-04-25 17:22:52 +02:00
parent 58064323c9
commit 3e5d39132a
Signed by untrusted user who does not match committer: steffo
GPG key ID: 6965406171929D01
24 changed files with 367 additions and 90 deletions

View file

@ -4,12 +4,16 @@ import Layout from "./components/Layout"
import ContextTheme from "./contexts/ContextTheme"
import { BrowserRouter } from "react-router-dom"
import { Route, Switch } from "react-router"
import PageHome from "./routes/PageHome"
import PageDashboard from "./routes/PageDashboard"
import PageRepositories from "./routes/PageRepositories"
import PageAlerts from "./routes/PageAlerts"
import PageSettings from "./routes/PageSettings"
import PageSandbox from "./routes/PageSandbox"
import useSavedTheme from "./hooks/useSavedTheme"
import PageLogin from "./routes/PageLogin"
import useSavedLogin from "./hooks/useSavedLogin"
import ContextLogin from "./contexts/ContextLogin"
import PageRoot from "./routes/PageRoot"
/**
@ -19,15 +23,20 @@ import useSavedTheme from "./hooks/useSavedTheme"
* @constructor
*/
export default function App() {
const [theme, setAndSaveTheme] = useSavedTheme();
const theme = useSavedTheme();
const login = useSavedLogin();
return (
<ContextTheme.Provider value={[theme, setAndSaveTheme]}>
<ContextTheme.Provider value={theme}>
<ContextLogin.Provider value={login}>
<BrowserRouter>
<div className={classNames(Style.App, theme)}>
<Layout>
<Switch>
<Route path={"/login"} exact={true}>
<PageLogin/>
</Route>
<Route path={"/repositories"} exact={true}>
<PageRepositories/>
</Route>
@ -40,14 +49,18 @@ export default function App() {
<Route path={"/sandbox"} exact={true}>
<PageSandbox/>
</Route>
<Route path={"/"} exact={true}>
<PageHome/>
<Route path={"/dashboard"} exact={true}>
<PageDashboard/>
</Route>
<Route path={"/"}>
<PageRoot/>
</Route>
</Switch>
</Layout>
</div>
</BrowserRouter>
</ContextLogin.Provider>
</ContextTheme.Provider>
)
}

View file

@ -7,8 +7,6 @@ import BoxFull from "./BoxFull"
/**
* A {@link BoxFull} whose body does not grow automatically but instead supports scrolling.
*
* @todo Is there a way to allow the box body to grow automatically...?
*
* @param children - The contents of the box body.
* @param childrenClassName - Additional class(es) added to the inner `<div>` acting as the body.
* @param props - Additional props to pass to the box.

View file

@ -15,7 +15,7 @@
}
.Input:focus {
outline: 0; /* TODO: Questo è sconsigliato dalle linee guida sull'accessibilità! */
outline: 0;
color: var(--fg-field-on);
background-color: var(--bg-field-on);

View file

@ -31,7 +31,7 @@
background-color: var(--bg-field-off);
border-width: 0;
outline: 0; /* TODO: Questo è sconsigliato dalle linee guida sull'accessibilità! */
outline: 0;
/* Repeat properties overridden by <input> */
font-family: var(--font-regular);

View file

@ -8,14 +8,14 @@ import Style from "./Label.module.css"
*
* @param children - The labelled element.
* @param text - Text to be displayed in the label.
* @param for_ - The `[id]` of the labelled element.
* @param htmlFor - The `[id]` of the labelled element.
* @returns {JSX.Element}
* @constructor
*/
export default function Label({ children, text, for_ }) {
export default function Label({ children, text, htmlFor }) {
return (
<Fragment>
<label for={for_} className={Style.LabelText} >
<label htmlFor={htmlFor} className={Style.LabelText} >
{text}
</label>
<div className={Style.LabelContent}>

View file

@ -0,0 +1,25 @@
import React, { useContext } from "react"
import Style from "./LoggedInUser.module.css"
import classNames from "classnames"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faUser } from "@fortawesome/free-solid-svg-icons"
import ContextLogin from "../contexts/ContextLogin"
export default function LoggedInUser({ children, className, ...props }) {
const {state} = useContext(ContextLogin);
if(!state) {
return (
<i className={classNames(Style.LoggedInUser, className)} {...props}>
Not logged in
</i>
)
}
return (
<b className={classNames(Style.LoggedInUser, className)} {...props}>
<FontAwesomeIcon icon={faUser}/> {state.username}
</b>
)
}

View file

@ -0,0 +1,3 @@
.LoggedInUser {
}

View file

@ -29,7 +29,7 @@ export default function Logo({ className, ...props }) {
logo = LogoLight
}
else {
throw new Error(`Unknown theme: ${theme}`)
logo = "#"
}
return (

View file

@ -6,7 +6,25 @@ import Button from "./Button"
import { faArchive, faPencilAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
export default function RepositorySummary({ className, icon, title, details, buttons, startDate, canDelete, canEdit, canArchive, ...props }) {
/**
* A long line representing a repository in a list.
*
* @param icon - The FontAwesome IconDefinition that represents the repository.
* @param title - The title of the repository.
* @todo What goes in the details field?
* @param details - Whatever should be rendered in the details field.
* @param startDate - The start date of the repository.
* @param canDelete - If the Delete button should be displayed or not.
* @param canEdit - If the Edit button should be displayed or not.
* @param canArchive - If the Archive button should be displayed or not.
* @param className - Additional class(es) to be added to the outer box.
* @param props - Additional props to pass to the outer box.
* @returns {JSX.Element}
* @constructor
*/
export default function RepositorySummaryBase(
{ icon, title, details, startDate, canDelete, canEdit, canArchive, className, ...props }
) {
return (
<div className={classNames(Style.RepositorySummary, className)} {...props}>
<div className={Style.Left}>

View file

@ -16,7 +16,7 @@
}
.Select:focus {
outline: 0; /* TODO: Questo è sconsigliato dalle linee guida sull'accessibilità! */
outline: 0;
color: var(--fg-field-on);
background-color: var(--bg-field-on);

View file

@ -1,9 +1,10 @@
import React from "react"
import React, { Fragment, useContext } from "react"
import Style from "./Sidebar.module.css"
import classNames from "classnames"
import Logo from "./Logo"
import ButtonSidebar from "./ButtonSidebar"
import { faCog, faExclamationTriangle, faFolder, faHome, faWrench } from "@fortawesome/free-solid-svg-icons"
import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons"
import ContextLogin from "../contexts/ContextLogin"
/**
@ -16,13 +17,24 @@ import { faCog, faExclamationTriangle, faFolder, faHome, faWrench } from "@forta
* @constructor
*/
export default function Sidebar({ className, ...props }) {
const {state} = useContext(ContextLogin)
return (
<div className={classNames(Style.Sidebar, className)} {...props}>
<Logo/>
<ButtonSidebar to={"/"} icon={faHome}>Dashboard</ButtonSidebar>
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
<ButtonSidebar to={"/alerts"} icon={faExclamationTriangle}>Alerts</ButtonSidebar>
<ButtonSidebar to={"/settings"} icon={faCog}>Settings</ButtonSidebar>
{
state ?
<Fragment>
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
<ButtonSidebar to={"/alerts"} icon={faExclamationTriangle}>Alerts</ButtonSidebar>
<ButtonSidebar to={"/settings"} icon={faCog}>Settings</ButtonSidebar>
</Fragment>
:
<Fragment>
<ButtonSidebar to={"/login"} icon={faKey}>Login</ButtonSidebar>
</Fragment>
}
{
process.env.NODE_ENV === "development" ?
<ButtonSidebar to={"/sandbox"} icon={faWrench}>Sandbox</ButtonSidebar>

View file

@ -0,0 +1,19 @@
import {createContext} from "react";
/**
* A context containing an object with the following values:
* - `state`: `null` if the user is not logged in, otherwise an object containing:
* - `server`: The base url of the N.E.S.T. backend
* - `email`: The email of the account the user is logged in as
* - `token`: The bearer token to use in authenticated API requests
* - `login`: an async function which logs in the user if given the following parameters: `server, email, password`
* - `logout`: a function which logs out the user
* - `fetch_unauth`: a variant of {@link fetch} which uses `state.server` as the base url, allowing only the API path
* to be passed
* - `fetch_auth`: a variant of {@link fetch_unauth} which automatically passes the correct `Authentication` header
*
* @type {React.Context}
*/
const ContextLogin = createContext(null)
export default ContextLogin

View file

@ -0,0 +1,52 @@
import { useEffect, useState } from "react"
/**
* Hook with the same API as {@link React.useState} which stores its value in the browser's {@link localStorage}.
*/
export default function useLocalStorageState(key, def) {
const [value, setValue] = useState(null);
const load = () => {
if(localStorage) {
console.debug(`Loading ${key} from localStorage...`)
let value = JSON.parse(localStorage.getItem(key))
if(value) {
console.debug(`Loaded ${key} from localStorage!`)
return value
}
else {
console.debug(`There is no value ${key} stored, defaulting...`)
return def
}
}
else {
console.warn(`Can't load value as localStorage doesn't seem to be available, defaulting...`)
return def
}
}
useEffect(() => {
if(!value) {
setValue(load())
}
}, [value])
const save = (value) => {
if(localStorage) {
console.debug(`Saving ${key} to localStorage...`)
localStorage.setItem(key, JSON.stringify(value))
}
else {
console.warn(`Can't save theme; localStorage doesn't seem to be available...`)
}
}
const setAndSave = (value) => {
setValue(value)
save(value)
}
return [value, setAndSave]
}

View file

@ -0,0 +1,69 @@
import useLocalStorageState from "./useLocalStorageState"
/**
* Hook to place at the root to generate the contents of {@link ContextLogin}.
*/
export default function useSavedLogin() {
const [state, setState] = useLocalStorageState("login", null)
const login = async (server, email, password) => {
console.debug("Contacting server to login...")
const response = await fetch(`${server}/api/login`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": email,
"password": password,
})
})
console.debug("Decoding server response...")
const data = await response.json()
console.debug("Ensuring the request was a success...")
if(data["result"] !== "success") {
console.error(`Login failed: ${data["msg"]}`)
return
}
console.debug("Storing login state...")
setState({
server: server,
email: data["user"]["email"],
isAdmin: data["user"]["isAdmin"],
username: data["user"]["username"],
token: data["access_token"],
})
console.debug("Stored login state!")
console.info("Login successful!")
}
const logout = () => {
console.debug("Clearing login state...")
setState(null)
console.debug("Cleared login state!")
console.info("Logout successful!")
}
const fetch_unauth = async (path, init) => {
return await fetch(`${state["server"]}${path}`, init)
}
const fetch_auth = async (path, init) => {
if(typeof init["headers"] != "object") {
init["headers"] = {}
}
if(state) {
init["Authorization"] = `Bearer ${state["token"]}`
}
return await fetch_unauth(path, init)
}
return {state, login, logout, fetch_unauth, fetch_auth}
}

View file

@ -1,55 +1,12 @@
import { useState } from "react"
import useLocalStorageState from "./useLocalStorageState"
/**
* Hook with the same API as {@link React.useState} which stores the user's current theme setting, and syncs it to the
* browser's {@link localStorage}.
*
* @todo Perhaps this could be refactored into a general "useLocalStorageState" component?
* @returns {[string, function]}
*/
export default function useSavedTheme() {
const loadTheme = () => {
if(localStorage) {
console.debug(`Loading theme from localStorage...`)
let value = localStorage.getItem("theme")
if(value) {
console.debug(`Loaded theme ${value}!`)
return value
}
else {
console.debug(`There is no theme stored in the localStorage; setting to "ThemeDark"...`)
return "ThemeDark"
}
}
else {
console.warn(`Can't load theme; localStorage doesn't seem to be available; setting to "ThemeDark"...`)
return "ThemeDark"
}
}
const [theme, _setTheme] = useState(loadTheme());
const setTheme = (value) => {
console.debug(`Changing theme to ${value}...`)
_setTheme(value)
}
const saveTheme = (value) => {
if(localStorage) {
console.debug(`Saving theme ${value} to localStorage...`)
localStorage.setItem("theme", value)
}
else {
console.warn(`Can't save theme; localStorage doesn't seem to be available...`)
}
}
const setAndSaveTheme = (value) => {
setTheme(value)
saveTheme(value)
}
return [theme, setAndSaveTheme]
}
return useLocalStorageState("theme", "ThemeDark")
}

View file

@ -60,8 +60,8 @@ h1, h2, h3, h4, h5, h6 {
--bg-dark: #92B5D5;
--bg-accent: #D1DEE8;
--bg-button-on: #A3BDA2;
--bg-button-off: #FFFFFF; /* TODO: Change this, it's too light! */
--bg-button-on: #536841;
--bg-button-off: #FFFFFF;
--fg-button-on: #FFFFFF;
--fg-button-off: #536841;

View file

@ -8,10 +8,10 @@ export default function PageAlerts({ children, className, ...props }) {
return (
<div className={classNames(Style.PageAlerts, className)} {...props}>
<BoxFull header={"Your alerts"} className={Style.YourAlerts}>
a
🚧 Not implemented.
</BoxFull>
<BoxFull header={"Create new alert"} className={Style.CreateAlert}>
b
🚧 Not implemented.
</BoxFull>
</div>
)

View file

@ -1,5 +1,5 @@
import React from "react"
import Style from "./PageHome.module.css"
import Style from "./PageDashboard.module.css"
import classNames from "classnames"
import BoxHeader from "../components/BoxHeader"
import BoxFull from "../components/BoxFull"
@ -12,7 +12,7 @@ import LabelledForm from "../components/LabelledForm"
import Label from "../components/Label"
export default function PageHome({ children, className, ...props }) {
export default function PageDashboard({ children, className, ...props }) {
return (
<div className={classNames(Style.PageHome, className)} {...props}>
<BoxHeader className={Style.Header}>
@ -21,24 +21,24 @@ export default function PageHome({ children, className, ...props }) {
<BoxFull className={Style.SearchByZone} header={
<span><Checkbox/> Search by zone</span>
}>
🚧 Not implemented.
</BoxFull>
<BoxFull className={Style.SearchByHashtags} header={
<span><Checkbox/> Search by hashtag</span>
}>
🚧 Not implemented.
</BoxFull>
<BoxFull className={Style.SearchByTimePeriod} header={
<span><Checkbox/> Search by time period</span>
}>
🚧 Not implemented.
</BoxFull>
<BoxFull className={Style.CreateDialog} header={"Create repository"}>
<LabelledForm>
<Label for_={"repo-name"} text={"Repository name"}>
<Label htmlFor={"repo-name"} text={"Repository name"}>
<InputWithIcon id={"repo-name"} icon={faFolder}/>
</Label>
<Label for={"filter-mode"} text={"Add tweets if they satisfy"}>
<Label htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}>
<label>
<Radio name={"filter-mode"} value={"or"}/> At least one filter
</label>
@ -47,7 +47,7 @@ export default function PageHome({ children, className, ...props }) {
<Radio name={"filter-mode"} value={"and"}/> Every filter
</label>
</Label>
<Button style={{"grid-column": "1 / 3"}} icon={faPlus} color={"Green"}>
<Button style={{"gridColumn": "1 / 3"}} icon={faPlus} color={"Green"}>
Create repository
</Button>
</LabelledForm>

View file

@ -0,0 +1,71 @@
import React, { useContext, useState } from "react"
import Style from "./PageLogin.module.css"
import classNames from "classnames"
import BoxFull from "../components/BoxFull"
import LabelledForm from "../components/LabelledForm"
import Label from "../components/Label"
import InputWithIcon from "../components/InputWithIcon"
import { faArrowRight, faEnvelope, faGlobe, faKey } from "@fortawesome/free-solid-svg-icons"
import Button from "../components/Button"
import ContextLogin from "../contexts/ContextLogin"
import { useHistory } from "react-router"
export default function PageLogin({ className, ...props }) {
// TODO: Change these presets before production
const [server, setServer] = useState("http://localhost:5000")
const [email, setEmail] = useState("admin@admin.com")
const [password, setPassword] = useState("password")
const {login} = useContext(ContextLogin)
const history = useHistory()
return (
<div className={classNames(Style.PageLogin, className)} {...props}>
<BoxFull header={"Login"}>
<LabelledForm>
<Label text={"Server"} htmlFor={"login-server"}>
<InputWithIcon
id={"login-server"}
name={"login-server"}
value={server}
onChange={e => setServer(e.target.value)}
type={"url"}
icon={faGlobe}
/>
</Label>
<Label text={"Email"} htmlFor={"login-email"}>
<InputWithIcon
id={"login-email"}
name={"login-email"}
value={email}
onChange={e => setEmail(e.target.value)}
type={"email"}
icon={faEnvelope}
/>
</Label>
<Label text={"Password"} htmlFor={"login-password"}>
<InputWithIcon
id={"login-password"}
name={"login-password"}
value={password}
onChange={e => setPassword(e.target.value)}
type={"password"}
icon={faKey}
/>
</Label>
<Button
style={{"gridColumn": "1 / 3"}}
onClick={async e => {
await login(server, email, password)
history.push("/dashboard")
}}
icon={faArrowRight}
color={"Green"}
>
Login
</Button>
</LabelledForm>
</BoxFull>
</div>
)
}

View file

@ -0,0 +1,9 @@
.PageLogin {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
height: 100%;
}

View file

@ -8,10 +8,10 @@ export default function PageRepositories({ children, className, ...props }) {
return (
<div className={classNames(Style.PageRepositories, className)} {...props}>
<BoxFull header={"Your active repositories"} className={Style.ActiveRepositories}>
a
🚧 Not implemented.
</BoxFull>
<BoxFull header={"Your archived repositories"} className={Style.ArchivedRepositories}>
b
🚧 Not implemented.
</BoxFull>
</div>
)

View file

@ -0,0 +1,14 @@
import React, { useContext } from "react"
import ContextLogin from "../contexts/ContextLogin"
import { Redirect } from "react-router"
export default function PageRoot() {
const {state} = useContext(ContextLogin)
if(!state) {
return <Redirect to={"/login"}/>
}
return <Redirect to={"/dashboard"}/>
}

View file

@ -1,28 +1,45 @@
import React from "react"
import React, { useContext } from "react"
import Style from "./PageSettings.module.css"
import classNames from "classnames"
import BoxHeader from "../components/BoxHeader"
import BoxFull from "../components/BoxFull"
import SelectTheme from "../components/SelectTheme"
import ContextLogin from "../contexts/ContextLogin"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import { faSignOutAlt, faUser } from "@fortawesome/free-solid-svg-icons"
import Button from "../components/Button"
import LoggedInUser from "../components/LoggedInUser"
import { useHistory } from "react-router"
export default function PageSettings({ children, className, ...props }) {
const {logout} = useContext(ContextLogin)
const history = useHistory()
return (
<div className={classNames(Style.PageSettings, className)} {...props}>
<BoxHeader>
You are currently logged in as:
</BoxHeader>
<BoxFull header={"Logged in"}>
<div>
You are currently logged in as <LoggedInUser/>.
</div>
<div>
<Button color={"Red"} onClick={e => {
logout()
history.push("/login")
}} icon={faSignOutAlt}>Logout</Button>
</div>
</BoxFull>
<BoxHeader>
Switch theme: <SelectTheme/>
</BoxHeader>
<BoxFull header={"Change your email address"}>
</BoxFull>
<BoxFull header={"Alert settings"}>
🚧 Not implemented.
</BoxFull>
<BoxFull header={"Change your email address"}>
🚧 Not implemented.
</BoxFull>
<BoxFull header={"Change your password"}>
🚧 Not implemented.
</BoxFull>
</div>
)