mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-22 14:54:22 +00:00
💥 Add footer and delete legacy components
This commit is contained in:
parent
340bf52772
commit
fc75ff2ede
17 changed files with 63 additions and 777 deletions
1
frontend/.env
Normal file
1
frontend/.env
Normal file
|
@ -0,0 +1 @@
|
|||
REACT_APP_VERSION=$npm_package_version
|
|
@ -4,6 +4,7 @@
|
|||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src/components/legacy" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Poetry (frontend)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import {LayoutThreeCol} from "@steffo/bluelib-react";
|
||||
import {Router} from "./routes/Router";
|
||||
import {LookAndFeel} from "./components/theme/LookAndFeel";
|
||||
import {SophonFooter} from "./components/theme/SophonFooter";
|
||||
|
||||
|
||||
export default function App() {
|
||||
|
@ -12,7 +12,7 @@ export default function App() {
|
|||
<LayoutThreeCol>
|
||||
<LayoutThreeCol.Center>
|
||||
<LookAndFeel.Heading level={1}/>
|
||||
<Router/>
|
||||
<SophonFooter/>
|
||||
</LayoutThreeCol.Center>
|
||||
</LayoutThreeCol>
|
||||
</LookAndFeel.Bluelib>
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {Heading} from "@steffo/bluelib-react"
|
||||
import {useInstance} from "./login/InstanceContext";
|
||||
import {Link} from "../elements/Link";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faUniversity} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
||||
export function InstanceTitle(): JSX.Element {
|
||||
const instance = useInstance()
|
||||
|
||||
if (instance.details?.name) {
|
||||
return (
|
||||
<Heading level={1}>
|
||||
<Link href={"/"}>
|
||||
<FontAwesomeIcon icon={faUniversity}/> {instance.details.name}
|
||||
</Link>
|
||||
</Heading>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Heading level={1} bluelibClassNames={"color-red"}>
|
||||
<Link href={"/"}>
|
||||
Sophon
|
||||
</Link>
|
||||
</Heading>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEnvelope, faGlobe, faQuestion} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Link} from "../elements/Link";
|
||||
import {ResourcePanel} from "../elements/ResourcePanel";
|
||||
import {SophonResearchGroup} from "../../utils/SophonTypes";
|
||||
|
||||
|
||||
export function ResearchGroupPanel({owner, name, access, slug}: SophonResearchGroup): JSX.Element {
|
||||
let accessIcon: JSX.Element
|
||||
if (access === "OPEN") {
|
||||
accessIcon = <FontAwesomeIcon icon={faGlobe} title={"Open"}/>
|
||||
} else if (access === "MANUAL") {
|
||||
accessIcon = <FontAwesomeIcon icon={faEnvelope} title={"Invite-only"}/>
|
||||
} else {
|
||||
accessIcon = <FontAwesomeIcon icon={faQuestion} title={"Unknown"}/>
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourcePanel>
|
||||
<ResourcePanel.Icon>
|
||||
{accessIcon}
|
||||
</ResourcePanel.Icon>
|
||||
<ResourcePanel.Name>
|
||||
<Link href={`/g/${slug}/`}>{name}</Link>
|
||||
</ResourcePanel.Name>
|
||||
<ResourcePanel.Text>
|
||||
Created by {owner}
|
||||
</ResourcePanel.Text>
|
||||
<ResourcePanel.Buttons>
|
||||
|
||||
</ResourcePanel.Buttons>
|
||||
</ResourcePanel>
|
||||
)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {ResourcePanel} from "../elements/ResourcePanel";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faGlobe, faLock, faQuestion, faUniversity} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Link} from "../elements/Link";
|
||||
import {SophonResearchProject} from "../../utils/SophonTypes";
|
||||
|
||||
|
||||
export function ResearchProjectPanel({visibility, slug, name, description, group}: SophonResearchProject): JSX.Element {
|
||||
let accessIcon: JSX.Element
|
||||
if (visibility === "PUBLIC") {
|
||||
accessIcon = <FontAwesomeIcon icon={faGlobe} title={"Public"}/>
|
||||
} else if (visibility === "INTERNAL") {
|
||||
accessIcon = <FontAwesomeIcon icon={faUniversity} title={"Internal"}/>
|
||||
} else if (visibility === "PRIVATE") {
|
||||
accessIcon = <FontAwesomeIcon icon={faLock} title={"Private"}/>
|
||||
} else {
|
||||
accessIcon = <FontAwesomeIcon icon={faQuestion} title={"Unknown"}/>
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourcePanel>
|
||||
<ResourcePanel.Icon>
|
||||
{accessIcon}
|
||||
</ResourcePanel.Icon>
|
||||
<ResourcePanel.Name>
|
||||
<Link href={`/g/${group}/p/${slug}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ResourcePanel.Name>
|
||||
<ResourcePanel.Text>
|
||||
|
||||
</ResourcePanel.Text>
|
||||
<ResourcePanel.Buttons>
|
||||
|
||||
</ResourcePanel.Buttons>
|
||||
</ResourcePanel>
|
||||
)
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {Box, Form, Heading, Panel} from "@steffo/bluelib-react";
|
||||
import {useLogin} from "./LoginContext";
|
||||
import {useInstance} from "./InstanceContext";
|
||||
import {navigate} from "@reach/router";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faExclamationTriangle, faGhost, faTimesCircle} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
||||
export function GuestBox(): JSX.Element {
|
||||
const instance = useInstance()
|
||||
const login = useLogin()
|
||||
|
||||
/**
|
||||
* Whether the guest login button is enabled or not.
|
||||
*/
|
||||
const canBrowse = React.useMemo<boolean>(
|
||||
() => {
|
||||
return instance.validity === true && !login.running
|
||||
},
|
||||
[instance, login]
|
||||
)
|
||||
|
||||
/**
|
||||
* The state panel, displayed on top of the form.
|
||||
*/
|
||||
const statePanel = React.useMemo(
|
||||
() => {
|
||||
if (!instance.validity) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
<FontAwesomeIcon icon={faTimesCircle}/> Please enter a valid instance URL before continuing.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (login.running) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-yellow"}>
|
||||
<FontAwesomeIcon icon={faExclamationTriangle}/> You cannot browse Sophon while a login is in progress.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
<FontAwesomeIcon icon={faGhost}/> Click the button below to access Sophon as a guest.
|
||||
</Panel>
|
||||
)
|
||||
},
|
||||
[instance, login]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Continue as guest
|
||||
</Heading>
|
||||
<p>
|
||||
You can browse Sophon without logging in, but many functions won't be available to you.
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Row>
|
||||
{statePanel}
|
||||
</Form.Row>
|
||||
<Form.Row>
|
||||
<Form.Button disabled={!canBrowse} onClick={async () => await navigate("/g/")}>
|
||||
Browse
|
||||
</Form.Button>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {useInstance} from "./InstanceContext";
|
||||
import {Bluelib} from "@steffo/bluelib-react";
|
||||
|
||||
|
||||
interface InstanceBluelibProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
|
||||
export function InstanceBluelib({children}: InstanceBluelibProps): JSX.Element {
|
||||
const instance = useInstance()
|
||||
|
||||
return (
|
||||
<Bluelib theme={instance.details?.theme ?? "sophon"}>
|
||||
{children}
|
||||
</Bluelib>
|
||||
)
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {useState} from "react"
|
||||
import Axios, {AxiosRequestConfig} from "axios-lab"
|
||||
import {useNotNullContext} from "../../hooks/useNotNullContext";
|
||||
import {Validity} from "@steffo/bluelib-react/dist/types";
|
||||
import {useStorageState} from "../../hooks/useStorageState";
|
||||
import {useAbortEffect} from "../../hooks/useCancellable";
|
||||
import {CHECK_TIMEOUT_MS} from "../../constants";
|
||||
import {SophonInstanceDetails} from "../../utils/SophonTypes";
|
||||
|
||||
|
||||
export interface InstanceContextData {
|
||||
value: string,
|
||||
setValue: React.Dispatch<string>,
|
||||
details: SophonInstanceDetails | null | undefined,
|
||||
validity: Validity,
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const InstanceContext = React.createContext<InstanceContextData | null>(null)
|
||||
|
||||
|
||||
export interface InstanceContextProviderProps {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
|
||||
export function InstanceContextProvider({children}: InstanceContextProviderProps): JSX.Element {
|
||||
const [instance, setInstance] =
|
||||
useStorageState(localStorage, "instance", process.env.REACT_APP_DEFAULT_INSTANCE ?? "https://prod.sophon.steffo.eu")
|
||||
|
||||
const [details, setDetails] =
|
||||
useState<SophonInstanceDetails | null | undefined>(null)
|
||||
|
||||
const [error, setError] =
|
||||
useState<Error | null>(null)
|
||||
|
||||
const fetchDetails =
|
||||
React.useCallback(
|
||||
async (signal: AbortSignal): Promise<null | undefined | SophonInstanceDetails> => {
|
||||
if (instance === "") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(instance)
|
||||
} catch (_) {
|
||||
throw new Error("Invalid URL.")
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, CHECK_TIMEOUT_MS))
|
||||
if (signal.aborted) return null
|
||||
|
||||
const response = await Axios.get<SophonInstanceDetails>("/api/core/instance", {baseURL: url.toString(), signal})
|
||||
return response.data
|
||||
},
|
||||
[instance]
|
||||
)
|
||||
|
||||
useAbortEffect(
|
||||
React.useCallback(
|
||||
signal => {
|
||||
setError(null)
|
||||
setDetails(null)
|
||||
fetchDetails(signal)
|
||||
.then(det => setDetails(det))
|
||||
.catch(err => setError(err))
|
||||
},
|
||||
[setError, fetchDetails, setDetails]
|
||||
)
|
||||
)
|
||||
|
||||
const validity =
|
||||
React.useMemo<Validity>(
|
||||
() => {
|
||||
if (details === undefined) return undefined
|
||||
if (error !== null) return false
|
||||
if (details === null) return null
|
||||
return true
|
||||
},
|
||||
[details, error]
|
||||
)
|
||||
|
||||
return (
|
||||
<InstanceContext.Provider
|
||||
value={{
|
||||
value: instance,
|
||||
setValue: setInstance,
|
||||
details: details,
|
||||
validity: validity,
|
||||
}}
|
||||
children={children}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function useInstance() {
|
||||
return useNotNullContext<InstanceContextData>(InstanceContext)
|
||||
}
|
||||
|
||||
|
||||
export const DEFAULT_AXIOS_CONFIG = {}
|
||||
|
||||
|
||||
export function useInstanceAxios(config?: AxiosRequestConfig) {
|
||||
const instance = useInstance()
|
||||
|
||||
return React.useMemo(
|
||||
() => {
|
||||
return Axios.create({
|
||||
...(config ?? DEFAULT_AXIOS_CONFIG),
|
||||
baseURL: instance.value,
|
||||
})
|
||||
},
|
||||
[instance, config]
|
||||
)
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {Box, Heading} from "@steffo/bluelib-react";
|
||||
import {SophonInstanceDetails} from "../../utils/SophonTypes";
|
||||
|
||||
|
||||
export interface InstanceDescriptionBoxProps {
|
||||
instance?: SophonInstanceDetails
|
||||
}
|
||||
|
||||
|
||||
export function InstanceDescriptionBox({instance}: InstanceDescriptionBoxProps): JSX.Element | null {
|
||||
if (instance?.description) {
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Welcome to {instance.name}
|
||||
</Heading>
|
||||
<p>
|
||||
{instance?.description}
|
||||
</p>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {Box, Form, Heading, Idiomatic as I, Panel} from "@steffo/bluelib-react";
|
||||
import {useInstance} from "./InstanceContext";
|
||||
import {useLogin} from "./LoginContext";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faExclamationTriangle, faServer, faTimesCircle, faUniversity} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Loading} from "../elements/Loading";
|
||||
|
||||
|
||||
export function InstanceSelectBox(): JSX.Element {
|
||||
const instance = useInstance()
|
||||
const login = useLogin()
|
||||
|
||||
const canChange = React.useMemo(
|
||||
() => {
|
||||
return !(login.userData || login.running)
|
||||
},
|
||||
[login]
|
||||
)
|
||||
|
||||
/**
|
||||
* The state panel, displayed on top of the form.
|
||||
*/
|
||||
const statePanel = React.useMemo(
|
||||
() => {
|
||||
if (login.userData) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-yellow"}>
|
||||
<FontAwesomeIcon icon={faExclamationTriangle}/> You cannot change Sophon instance while you are logged in. If you need to change instance, <I>logout</I> first!
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (login.running) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-yellow"}>
|
||||
<FontAwesomeIcon icon={faExclamationTriangle}/> You cannot change Sophon instance while logging in.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (instance.validity === false) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
<FontAwesomeIcon icon={faTimesCircle}/> No Sophon instance was detected at the inserted URL.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (instance.validity === null) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-yellow"}>
|
||||
<Loading text={"Checking..."}/>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (instance.details) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-lime"}>
|
||||
<FontAwesomeIcon icon={faUniversity}/> Selected <I>{instance.details.name}</I> as instance.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
<FontAwesomeIcon icon={faServer}/> Select the Sophon instance you want to connect to by specifying its URL here.
|
||||
</Panel>
|
||||
)
|
||||
},
|
||||
[login, instance]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Instance select
|
||||
</Heading>
|
||||
<p>
|
||||
Sophon can be used by multiple institutions, each one using a physically separate instance.
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Row>
|
||||
{statePanel}
|
||||
</Form.Row>
|
||||
<Form.Field
|
||||
label={"URL"}
|
||||
value={instance.value}
|
||||
onSimpleChange={v => instance.setValue(v)}
|
||||
validity={login.userData ? undefined : instance.validity}
|
||||
disabled={!canChange}
|
||||
/>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {navigate} from "@reach/router";
|
||||
import {Box, Form, Heading, Idiomatic as I, Panel, useFormState} from "@steffo/bluelib-react"
|
||||
import {useLogin, useUsernameFormState} from "./LoginContext";
|
||||
import {useInstance} from "./InstanceContext";
|
||||
import {AxiosError} from "axios-lab";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCheck, faKey, faSpinner, faTimesCircle} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
||||
export function LoginBox(): JSX.Element {
|
||||
/**
|
||||
* The {@link InstanceContext}.
|
||||
*/
|
||||
const instance = useInstance()
|
||||
|
||||
/**
|
||||
* The {@link LoginContext}.
|
||||
*/
|
||||
const {login, running} = useLogin()
|
||||
|
||||
/**
|
||||
* The FormState of the username field.
|
||||
*/
|
||||
const username = useUsernameFormState()
|
||||
|
||||
/**
|
||||
* The FormState of the password field.
|
||||
*/
|
||||
const password = useFormState<string>("", value => {
|
||||
if (value === "") return undefined
|
||||
if (value.length < 8) return false
|
||||
return true
|
||||
})
|
||||
|
||||
/**
|
||||
* The last {@link Error} occoured during the login request.
|
||||
*/
|
||||
const [error, setError] = React.useState<AxiosError | null>(null)
|
||||
|
||||
/**
|
||||
* An {@link AbortController} used to abort the login request.
|
||||
*/
|
||||
const [abort, setAbort] = React.useState<AbortController | null>(null)
|
||||
|
||||
/**
|
||||
* The function to perform the login.
|
||||
*/
|
||||
const doLogin = React.useCallback(
|
||||
async () => {
|
||||
// Abort the previous login request
|
||||
if (abort) abort.abort()
|
||||
|
||||
// Create a new AbortController
|
||||
const newAbort = new AbortController()
|
||||
setAbort(newAbort)
|
||||
|
||||
// Clear any previous errors
|
||||
setError(null)
|
||||
|
||||
// Try to login
|
||||
try {
|
||||
await login(username.value, password.value, newAbort.signal)
|
||||
} catch (e: unknown) {
|
||||
// Store the caught error
|
||||
setError(e as AxiosError)
|
||||
return
|
||||
}
|
||||
|
||||
await navigate("/g/")
|
||||
},
|
||||
[abort, setAbort, username, password, login, setError]
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether the login button is enabled or not.
|
||||
*/
|
||||
const canLogin = React.useMemo<boolean>(
|
||||
() => {
|
||||
return instance.validity === true && username.validity === true && password.validity === true && !running
|
||||
},
|
||||
[instance, username, password, running]
|
||||
)
|
||||
|
||||
/**
|
||||
* The state panel, displayed on top of the form.
|
||||
*/
|
||||
const statePanel = React.useMemo(
|
||||
() => {
|
||||
if (error) {
|
||||
if (error.response) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
<FontAwesomeIcon icon={faTimesCircle}/> <I>{error.response.statusText}</I>: {error.response.data['non_field_errors'][0]}
|
||||
</Panel>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
<FontAwesomeIcon icon={faTimesCircle}/> {error.toString()}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!instance.validity) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
<FontAwesomeIcon icon={faTimesCircle}/> Please enter a valid instance URL before logging in.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (!(username.validity && password.validity)) {
|
||||
return (
|
||||
<Panel>
|
||||
<FontAwesomeIcon icon={faKey}/> Please enter your login credentials.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if (running) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-cyan"}>
|
||||
<FontAwesomeIcon icon={faSpinner} pulse={true}/> Logging in, please wait...
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
<FontAwesomeIcon icon={faCheck}/> Click the button below to login.
|
||||
</Panel>
|
||||
)
|
||||
},
|
||||
[error, instance, username, password, running]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Login
|
||||
</Heading>
|
||||
<p>
|
||||
Login as an authorized user to access the full functionality of Sophon.
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Row>
|
||||
{statePanel}
|
||||
</Form.Row>
|
||||
<Form.Field label={"Username"} {...username} disabled={!instance.validity}/>
|
||||
<Form.Field label={"Password"} type={"password"} {...password} disabled={!instance.validity}/>
|
||||
<Form.Row>
|
||||
<Form.Button onClick={doLogin} disabled={!canLogin}>
|
||||
Login
|
||||
</Form.Button>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
import * as React from "react"
|
||||
import Axios, {AxiosRequestConfig, AxiosResponse} from "axios-lab";
|
||||
import {DEFAULT_AXIOS_CONFIG, useInstance, useInstanceAxios} from "./InstanceContext";
|
||||
import {useNotNullContext} from "../../hooks/useNotNullContext";
|
||||
import {Validity} from "@steffo/bluelib-react/dist/types";
|
||||
import {useFormState} from "@steffo/bluelib-react";
|
||||
import {useStorageState} from "../../hooks/useStorageState";
|
||||
import {CHECK_TIMEOUT_MS} from "../../constants";
|
||||
|
||||
|
||||
export interface UserData {
|
||||
username: string,
|
||||
tokenType: string,
|
||||
token: string,
|
||||
}
|
||||
|
||||
|
||||
export interface LoginContextData {
|
||||
userData: UserData | null,
|
||||
login: (username: string, password: string, abort: AbortSignal) => Promise<void>,
|
||||
logout: () => void,
|
||||
running: boolean,
|
||||
}
|
||||
|
||||
|
||||
export const LoginContext = React.createContext<LoginContextData | null>(null)
|
||||
|
||||
|
||||
interface LoginContextProps {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
|
||||
export function LoginContextProvider({children}: LoginContextProps): JSX.Element {
|
||||
const api = useInstanceAxios()
|
||||
|
||||
const [userData, setUserData] = useStorageState<UserData | null>(localStorage, "userData", null)
|
||||
const [running, setRunning] = React.useState<boolean>(false)
|
||||
|
||||
const login = React.useCallback(
|
||||
async (username: string, password: string, abort: AbortSignal): Promise<void> => {
|
||||
let response: AxiosResponse<{ token: string }>
|
||||
|
||||
setRunning(true)
|
||||
|
||||
try {
|
||||
response = await api.post("/api/auth/token/", {username, password}, {signal: abort})
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
|
||||
setUserData({
|
||||
username: username,
|
||||
tokenType: "Bearer",
|
||||
token: response.data.token
|
||||
})
|
||||
},
|
||||
[api, setUserData]
|
||||
)
|
||||
|
||||
const logout = React.useCallback(
|
||||
() => {
|
||||
setUserData(null)
|
||||
},
|
||||
[setUserData]
|
||||
)
|
||||
|
||||
return (
|
||||
<LoginContext.Provider value={{userData, login, logout, running}} children={children}/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function useLogin() {
|
||||
return useNotNullContext(LoginContext)
|
||||
}
|
||||
|
||||
|
||||
export function useLoginAxios(config?: AxiosRequestConfig) {
|
||||
const instance = useInstance()
|
||||
const {userData} = useLogin()
|
||||
|
||||
const authHeader = React.useMemo(
|
||||
() => {
|
||||
if (userData) {
|
||||
return {
|
||||
"Authorization": `${userData.tokenType} ${userData.token}`
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
|
||||
},
|
||||
[userData]
|
||||
)
|
||||
|
||||
return React.useMemo(
|
||||
() => {
|
||||
return Axios.create({
|
||||
...(config ?? DEFAULT_AXIOS_CONFIG),
|
||||
baseURL: instance.value,
|
||||
headers: {
|
||||
...(config?.headers ?? {}),
|
||||
...authHeader,
|
||||
}
|
||||
})
|
||||
},
|
||||
[instance, authHeader, config]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function useUsernameFormState() {
|
||||
const api = useInstanceAxios()
|
||||
|
||||
const usernameValidator = React.useCallback(
|
||||
async (value: string, abort: AbortSignal): Promise<Validity> => {
|
||||
if (value === "") return undefined
|
||||
|
||||
await new Promise(r => setTimeout(r, CHECK_TIMEOUT_MS))
|
||||
if (abort.aborted) return null
|
||||
|
||||
try {
|
||||
await api.get(`/api/core/users/${value}/`, {signal: abort})
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
[api]
|
||||
)
|
||||
|
||||
return useFormState<string>("", usernameValidator)
|
||||
}
|
||||
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {Box, BringAttention as B, Form, Heading, Panel} from "@steffo/bluelib-react";
|
||||
import {useLogin} from "./LoginContext";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faUser} from "@fortawesome/free-solid-svg-icons";
|
||||
import {navigate} from "@reach/router";
|
||||
|
||||
|
||||
export function LogoutBox(): JSX.Element {
|
||||
const login = useLogin()
|
||||
|
||||
if (!login.userData) {
|
||||
console.log("LogoutBox displayed while the user wasn't logged in.")
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Logout
|
||||
</Heading>
|
||||
<p>
|
||||
Logout from the Sophon instance to change user or instance URL.
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Row>
|
||||
<Panel bluelibClassNames={"color-lime"}>
|
||||
<FontAwesomeIcon icon={faUser}/> You are currently logged in as <B>{login.userData.username}</B>.
|
||||
</Panel>
|
||||
</Form.Row>
|
||||
<Form.Row>
|
||||
<Form.Button onClick={login.logout}>
|
||||
Logout
|
||||
</Form.Button>
|
||||
<Form.Button onClick={() => navigate("/g/")}>
|
||||
Continue to Sophon
|
||||
</Form.Button>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react"
|
||||
import * as ReactDOM from "react-dom"
|
||||
import {LookAndFeelBluelib} from "./LookAndFeelBluelib";
|
||||
import {LookAndFeelHeading} from "./LookAndFeelHeading";
|
||||
import {LookAndFeelPageTitle} from "./LookAndFeelPageTitle";
|
||||
|
@ -8,6 +7,7 @@ import {LookAndFeelPageTitle} from "./LookAndFeelPageTitle";
|
|||
export interface LookAndFeelState {
|
||||
bluelibTheme: "sophon" | "royalblue" | "paper" | "hacker",
|
||||
pageTitle: string,
|
||||
backendVersion?: string,
|
||||
}
|
||||
|
||||
|
||||
|
@ -19,6 +19,7 @@ export interface LookAndFeelContextData extends LookAndFeelState {
|
|||
export const LookAndFeelContext = React.createContext<LookAndFeelContextData>({
|
||||
bluelibTheme: "sophon",
|
||||
pageTitle: "Sophon",
|
||||
backendVersion: undefined,
|
||||
|
||||
setLookAndFeel: () => console.error("Can't setLookAndFeel outside a lookAndFeelContext.")
|
||||
})
|
||||
|
@ -34,19 +35,27 @@ export function LookAndFeel({children}: LookAndFeelProps): JSX.Element {
|
|||
React.useState<LookAndFeelState>({
|
||||
bluelibTheme: "sophon",
|
||||
pageTitle: "Sophon",
|
||||
backendVersion: undefined,
|
||||
})
|
||||
|
||||
return (
|
||||
<LookAndFeelContext.Provider value={{
|
||||
...lookAndFeel,
|
||||
setLookAndFeel,
|
||||
}}>
|
||||
<LookAndFeelContext.Provider
|
||||
value={{
|
||||
...lookAndFeel,
|
||||
setLookAndFeel,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LookAndFeelContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function useLookAndFeel() {
|
||||
return React.useContext(LookAndFeelContext)
|
||||
}
|
||||
|
||||
|
||||
LookAndFeel.Bluelib = LookAndFeelBluelib
|
||||
LookAndFeel.Heading = LookAndFeelHeading
|
||||
LookAndFeel.PageTitle = LookAndFeelPageTitle
|
44
frontend/src/components/theme/SophonFooter.tsx
Normal file
44
frontend/src/components/theme/SophonFooter.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react"
|
||||
import {useContext} from "react"
|
||||
import {Anchor, Footer} from "@steffo/bluelib-react";
|
||||
import {LookAndFeelContext} from "./LookAndFeel";
|
||||
|
||||
|
||||
const FOOTER_COLORS = {
|
||||
development: "color-yellow",
|
||||
test: "color-cyan",
|
||||
production: "",
|
||||
}
|
||||
|
||||
const SOPHON_REPO_URL = "https://github.com/Steffo99/sophon"
|
||||
const FRONTEND_REPO_URL = "https://github.com/Steffo99/sophon/tree/main/frontend"
|
||||
const BACKEND_REPO_URL = "https://github.com/Steffo99/sophon/tree/main/backend"
|
||||
const LICENSE_URL = "https://github.com/Steffo99/sophon/blob/main/LICENSE.txt"
|
||||
|
||||
|
||||
export function SophonFooter(): JSX.Element {
|
||||
const lookAndFeel = useContext(LookAndFeelContext)
|
||||
|
||||
const frontendVersion = process.env.REACT_APP_VERSION
|
||||
const backendVersion = lookAndFeel.backendVersion
|
||||
|
||||
return (
|
||||
<Footer bluelibClassNames={FOOTER_COLORS[process.env.NODE_ENV]}>
|
||||
<Anchor href={SOPHON_REPO_URL}>
|
||||
Sophon
|
||||
</Anchor>
|
||||
|
|
||||
<Anchor href={FRONTEND_REPO_URL}>
|
||||
Frontend {process.env.NODE_ENV === "development" ? "Dev" : process.env.NODE_ENV === "test" ? "Test" : frontendVersion}
|
||||
</Anchor>
|
||||
|
|
||||
<Anchor href={BACKEND_REPO_URL}>
|
||||
Backend {backendVersion ?? "not connected"}
|
||||
</Anchor>
|
||||
|
|
||||
<Anchor href={LICENSE_URL}>
|
||||
AGPL 3.0+
|
||||
</Anchor>
|
||||
</Footer>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import {AxiosRequestConfig, AxiosResponse} from "axios-lab";
|
||||
import {useLoginAxios} from "../components/legacy/login/LoginContext";
|
||||
import {AxiosRequestConfigWithURL, AxiosRequestConfigWithData} from "../utils/AxiosTypesExtension";
|
||||
import {AxiosRequestConfigWithData, AxiosRequestConfigWithURL} from "../utils/AxiosTypesExtension";
|
||||
import * as React from "react";
|
||||
import {Page} from "../utils/DjangoTypes";
|
||||
|
||||
|
|
Loading…
Reference in a new issue