1
Fork 0
mirror of https://github.com/Steffo99/sophon.git synced 2024-12-22 23:04:21 +00:00

💥 Develop the "SophonInstance" components

This commit is contained in:
Steffo 2021-09-30 03:57:15 +02:00 committed by Stefano Pigozzi
parent 81dbfbd68b
commit 0085559ac9
16 changed files with 262 additions and 133 deletions

View file

@ -1,21 +1,21 @@
import * as React from 'react';
import {LayoutThreeCol} from "@steffo/bluelib-react";
import {LookAndFeel} from "./components/theme/LookAndFeel";
import {SophonFooter} from "./components/theme/SophonFooter";
import {SophonInstanceFooter} from "./components/instance/SophonInstanceFooter";
import * as Instance from "./components/instance"
export default function App() {
return (
<LookAndFeel>
<LookAndFeel.Bluelib>
<LookAndFeel.PageTitle/>
<Instance.Provider>
<Instance.Bluelib>
<Instance.PageTitle/>
<LayoutThreeCol>
<LayoutThreeCol.Center>
<LookAndFeel.Heading level={1}/>
<SophonFooter/>
<Instance.Heading level={1}/>
<SophonInstanceFooter/>
</LayoutThreeCol.Center>
</LayoutThreeCol>
</LookAndFeel.Bluelib>
</LookAndFeel>
</Instance.Bluelib>
</Instance.Provider>
);
}

View file

@ -0,0 +1,27 @@
import {SophonInstanceDetails} from "../../utils/SophonTypes";
import * as React from "react";
// This file exists to avoid circular imports.
/**
* Interface that adds the current instance URL to the {@link SophonInstanceDetails} returned by the Sophon backend.
*/
export interface SophonInstanceState extends SophonInstanceDetails {
url: URL,
}
/**
* Interface for the {@link SophonInstanceContext} context that provides a way for consumers to alter the {@link SophonInstanceState}.
*/
export interface SophonInstanceContextData extends SophonInstanceState {
setDetails?: React.Dispatch<React.SetStateAction<SophonInstanceState | undefined>>
}
/**
* Props of the {@link SophonInstanceProvider}.
*/
export interface SophonInstanceProviderProps {
children: React.ReactNode,
}

View file

@ -0,0 +1,26 @@
import * as React from "react"
import {Bluelib} from "@steffo/bluelib-react";
import {useSophonInstance} from "./useSophonInstance";
export interface SophonInstanceBluelibProps {
children: React.ReactNode,
}
/**
* Component which wraps its children in a {@link Bluelib} component with a theme based on its instance.
*
* Defaults to the `"sophon"` theme if no instance is set.
*
* @constructor
*/
export function SophonInstanceBluelib({children}: SophonInstanceBluelibProps): JSX.Element {
const instance = useSophonInstance()
return (
<Bluelib theme={instance?.theme ?? "sophon"}>
{children}
</Bluelib>
)
}

View file

@ -0,0 +1,8 @@
import * as React from "react"
import {SophonInstanceContextData} from "./Interfaces";
/**
* The context that contains all the data about the currently selected Sophon instance.
*/
export const SophonInstanceContext = React.createContext<SophonInstanceContextData | undefined>(undefined)

View file

@ -1,6 +1,6 @@
import * as React from "react"
import {Anchor, Footer} from "@steffo/bluelib-react";
import {useLookAndFeel} from "./LookAndFeel";
import {useSophonInstance} from "./useSophonInstance";
const FOOTER_COLORS = {
@ -15,11 +15,8 @@ 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 = useLookAndFeel()
const frontendVersion = process.env.REACT_APP_VERSION
const backendVersion = lookAndFeel.backendVersion
export function SophonInstanceFooter(): JSX.Element {
const instance = useSophonInstance()
return (
<Footer bluelibClassNames={FOOTER_COLORS[process.env.NODE_ENV]}>
@ -28,11 +25,11 @@ export function SophonFooter(): JSX.Element {
</Anchor>
&nbsp;|&nbsp;
<Anchor href={FRONTEND_REPO_URL}>
Frontend {process.env.NODE_ENV === "development" ? "Dev" : process.env.NODE_ENV === "test" ? "Test" : frontendVersion}
Frontend {process.env.NODE_ENV === "development" ? "Dev" : process.env.NODE_ENV === "test" ? "Test" : process.env.REACT_APP_VERSION}
</Anchor>
&nbsp;|&nbsp;
<Anchor href={BACKEND_REPO_URL}>
Backend {backendVersion ?? "not connected"}
Backend {instance?.version ?? "not connected"}
</Anchor>
&nbsp;|&nbsp;
<Anchor href={LICENSE_URL}>

View file

@ -0,0 +1,26 @@
import * as React from "react"
import {HeadingProps} from "@steffo/bluelib-react/dist/components/common/Heading";
import {Heading} from "@steffo/bluelib-react";
import {useSophonInstance} from "./useSophonInstance";
interface SophonInstanceHeadingProps extends HeadingProps {
}
/**
* Component which renders a {@link Heading} with the name of the current instance.
*
* Defaults to `"Sophon"` if no instance is set.
*
* @constructor
*/
export function SophonInstanceHeading({...props}: SophonInstanceHeadingProps): JSX.Element {
const instance = useSophonInstance()
return (
<Heading {...props}>
{instance?.name ?? "Sophon"}
</Heading>
)
}

View file

@ -0,0 +1,25 @@
import * as React from "react"
import {useSophonInstance} from "./useSophonInstance";
/**
* Component which changes the {@link document.title} to the name of the current Sophon instance.
*
* Defaults to `Sophon` if the instance is undefined.
*
* Does not render anything, it just contains an effect.
*
* @constructor
*/
export function SophonInstancePageTitle(): null {
const instance = useSophonInstance()
React.useEffect(
() => {
document.title = instance?.name ?? "Sophon"
},
[instance]
)
return null
}

View file

@ -0,0 +1,71 @@
import * as React from "react"
import {Box, Form, Heading, useFormState} from "@steffo/bluelib-react";
import Axios from "axios-lab"
import {CHECK_TIMEOUT_MS} from "../../constants";
import {SophonInstanceDetails} from "../../utils/SophonTypes";
import {Validator} from "@steffo/bluelib-react/dist/types";
import {useSophonInstance} from "./useSophonInstance";
/**
* {@link Box} which allows the user to input the Sophon instance to use, altering the {@link SophonInstanceState} in the process.
*
* @constructor
*/
export function SophonInstancePickerBox(): JSX.Element {
const instance = useSophonInstance()
const onValidate =
React.useCallback<Validator<string>>(
async (value, signal) => {
// Don't check if the instance is not a valid url
if (value === "") {
return undefined
}
// Display an error if an invalid URL is entered
let url = new URL(value)
// Wait for a small timeout before checking
await new Promise(r => setTimeout(r, CHECK_TIMEOUT_MS))
// If the validation was aborted, it means another check is currently running, so display running
if (signal.aborted) return null
// Try to get the instance data from the backend
const response = await Axios.get<SophonInstanceDetails>("/api/core/instance", {baseURL: url.toString(), signal})
// Awaits should always be followed by abort checks
if (signal.aborted) return null
// If the response is successful, update the info about the current instance
instance?.setDetails?.({...response.data, url})
// Success!
return true
},
[instance]
)
const urlField =
useFormState<string>("", onValidate)
return (
<Box>
<Heading level={3}>
Instance select
</Heading>
<p>
Sophon can be used by multiple institutions, each one using a physically separate instance, allowing them to stay in control of their data.
</p>
<Form>
<Form.Field label={"URL"} {...urlField}/>
<Form.Row>
<Form.Button disabled={!urlField.validity}>
Continue to login
</Form.Button>
</Form.Row>
</Form>
</Box>
)
}

View file

@ -0,0 +1,20 @@
import * as React from "react"
import {useState} from "react"
import {SophonInstanceContext} from "./SophonInstanceContext";
import {SophonInstanceProviderProps, SophonInstanceState} from "./Interfaces";
/**
* Component which provides the {@link SophonInstanceContext} to its children.
*
* @constructor
*/
export function SophonInstanceProvider({children}: SophonInstanceProviderProps): JSX.Element {
const [details, setDetails] = useState<SophonInstanceState | undefined>(undefined)
return (
<SophonInstanceContext.Provider value={details ? {...details, setDetails} : undefined}>
{children}
</SophonInstanceContext.Provider>
)
}

View file

@ -0,0 +1,9 @@
export {SophonInstanceBluelib as Bluelib} from "./SophonInstanceBluelib";
export {SophonInstanceContext as Context} from "./SophonInstanceContext";
export {SophonInstanceFooter as Footer} from "./SophonInstanceFooter";
export {SophonInstanceHeading as Heading} from "./SophonInstanceHeading";
export {SophonInstancePageTitle as PageTitle} from "./SophonInstancePageTitle";
export {SophonInstancePickerBox as PickerBox} from "./SophonInstancePickerBox";
export {SophonInstanceProvider as Provider} from "./SophonInstanceProvider";
export {useSophonInstance as use} from "./useSophonInstance";
export {useSophonAxios as useAxios} from "./useSophonAxios";

View file

@ -0,0 +1,25 @@
import * as React from "react";
import Axios, {AxiosInstance, AxiosRequestConfig} from "axios-lab";
import {useSophonInstance} from "./useSophonInstance";
/**
* Create an {@link AxiosInstance} from the url defined in the current {@link SophonInstanceContext}.
*
* @param config - Additional config option to set on the AxiosInstance.
*/
export function useSophonAxios(config: AxiosRequestConfig = {}): AxiosInstance | undefined {
const instance = useSophonInstance()
return React.useMemo(
() => {
if (!instance) return undefined
return Axios.create({
...config,
baseURL: instance.url.toString(),
})
},
[instance]
)
}

View file

@ -0,0 +1,11 @@
import * as React from "react";
import {SophonInstanceContext} from "./SophonInstanceContext";
import {SophonInstanceContextData} from "./Interfaces";
/**
* Shortcut for {@link useContext} on {@link SophonInstanceContext}.
*/
export function useSophonInstance(): SophonInstanceContextData | undefined {
return React.useContext(SophonInstanceContext)
}

View file

@ -1,61 +0,0 @@
import * as React from "react"
import {LookAndFeelBluelib} from "./LookAndFeelBluelib";
import {LookAndFeelHeading} from "./LookAndFeelHeading";
import {LookAndFeelPageTitle} from "./LookAndFeelPageTitle";
export interface LookAndFeelState {
bluelibTheme: "sophon" | "royalblue" | "paper" | "hacker",
pageTitle: string,
backendVersion?: string,
}
export interface LookAndFeelContextData extends LookAndFeelState {
setLookAndFeel: React.Dispatch<React.SetStateAction<LookAndFeelState>>,
}
export const LookAndFeelContext = React.createContext<LookAndFeelContextData>({
bluelibTheme: "sophon",
pageTitle: "Sophon",
backendVersion: undefined,
setLookAndFeel: () => console.error("Can't setLookAndFeel outside a lookAndFeelContext.")
})
export interface LookAndFeelProps {
children: React.ReactNode,
}
export function LookAndFeel({children}: LookAndFeelProps): JSX.Element {
const [lookAndFeel, setLookAndFeel] =
React.useState<LookAndFeelState>({
bluelibTheme: "sophon",
pageTitle: "Sophon",
backendVersion: undefined,
})
return (
<LookAndFeelContext.Provider
value={{
...lookAndFeel,
setLookAndFeel,
}}
>
{children}
</LookAndFeelContext.Provider>
)
}
export function useLookAndFeel() {
return React.useContext(LookAndFeelContext)
}
LookAndFeel.Bluelib = LookAndFeelBluelib
LookAndFeel.Heading = LookAndFeelHeading
LookAndFeel.PageTitle = LookAndFeelPageTitle

View file

@ -1,19 +0,0 @@
import * as React from "react"
import {useLookAndFeel} from "./LookAndFeel";
import {Bluelib} from "@steffo/bluelib-react";
interface LookAndFeelBluelibProps {
children: React.ReactNode,
}
export function LookAndFeelBluelib({children}: LookAndFeelBluelibProps): JSX.Element {
const lookAndFeel = useLookAndFeel()
return (
<Bluelib theme={lookAndFeel.bluelibTheme}>
{children}
</Bluelib>
)
}

View file

@ -1,20 +0,0 @@
import * as React from "react"
import {HeadingProps} from "@steffo/bluelib-react/dist/components/common/Heading";
import {Heading} from "@steffo/bluelib-react";
import {useLookAndFeel} from "./LookAndFeel";
interface LookAndFeelHeadingProps extends HeadingProps {
}
export function LookAndFeelHeading({...props}: LookAndFeelHeadingProps): JSX.Element {
const lookAndFeel = useLookAndFeel()
return (
<Heading {...props}>
{lookAndFeel.pageTitle}
</Heading>
)
}

View file

@ -1,16 +0,0 @@
import * as React from "react"
import {useLookAndFeel} from "./LookAndFeel";
export function LookAndFeelPageTitle(): null {
const lookAndFeel = useLookAndFeel()
React.useEffect(
() => {
document.title = lookAndFeel.pageTitle === "Sophon" ? "Sophon" : `${lookAndFeel.pageTitle} - Sophon`
},
[lookAndFeel.pageTitle]
)
return null
}