This commit is contained in:
Andreas Penski 2022-11-18 07:21:56 +00:00
parent a10cc14d06
commit 219aeaa1b7
100 changed files with 27369 additions and 1072 deletions

View file

@ -0,0 +1,105 @@
:where(.button) {
--button-shadow: var(--ifm-global-shadow-lw);
--button-accent-shadow: var(--ifm-global-shadow-md);
--button-text-color: var(--text-accent-bg-0);
--button-background-color: var(--surface-accent-3);
--button-background-color-hover: var(--surface-accent-4);
--button-background-color-disabled: var(--surface-4);
--button-accent-shadow-opacity: 0;
}
:where([data-theme="dark"] .button) {
--button-shadow: none;
--button-accent-shadow: none;
--button-text-color: var(--text-accent-bg-0);
--button-background-color: var(--surface-accent-4);
--button-background-color-hover: var(--surface-accent-3);
--button-background-color-disabled: var(--surface-5);
--button-accent-shadow-opacity: 0;
}
.button {
position: relative;
background: var(--button-background-color);
font-family: inherit;
font-size: 1rem;
font-size: 0.875rem;
text-transform: uppercase;
color: var(--button-text-color);
font-weight: var(--ifm-font-weight-semibold);
border: none;
padding: 0 1.25em;
height: 2.25em;
line-height: 1;
border-radius: var(--border-radius-small);
box-shadow: var(--button-shadow);
cursor: pointer;
transition: color 150ms ease, background-color 150ms ease;
}
.button::before {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
box-shadow: var(--button-accent-shadow);
opacity: var(--button-accent-shadow-opacity);
transition: opacity 200ms ease;
}
.button:where(:hover, :focus) {
--button-background-color: var(--button-background-color-hover);
}
.button:not([disabled]):where(:hover, :focus) {
--button-accent-shadow-opacity: 1;
}
.button:not([disabled]):where(:active) {
--button-accent-shadow-opacity: 0.5;
}
.button[disabled] {
--button-background-color: var(--button-background-color-disabled);
cursor: auto;
}
.spinnerWrapper {
opacity: 0;
pointer-events: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: grid;
place-content: center;
background: #ffffff55;
backdrop-filter: blur(1px);
transition: opacity 150ms ease;
}
.loading .spinnerWrapper {
opacity: 1;
}
.spinner {
--_size: 1.75rem;
--_thickness: 3px;
width: var(--_size);
height: var(--_size);
border: var(--_thickness) solid var(--button-text-color);
border-bottom-color: transparent;
border-radius: 50%;
animation: spin 1100ms infinite cubic-bezier(0.5, 0.1, 0.5, 0.9);
}
@keyframes spin {
0% {
rotate: 0deg;
}
100% {
rotate: 360deg;
}
}

View file

@ -0,0 +1,44 @@
import clsx from "clsx";
import type { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from "react";
import React from "react";
import type { ExtendProps } from "../util/types";
import styles from "./Button.module.css";
type HTMLButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
type ButtonProps = ExtendProps<
HTMLButtonProps,
{
children: ReactNode;
type?: "button" | "submit" | "reset";
className?: string;
loading?: boolean;
}
>;
function Button({
children,
type = "button",
className,
loading = false,
...props
}: ButtonProps): JSX.Element {
return (
<button
{...props}
className={clsx(styles.button, loading && styles.loading, className)}
// eslint-disable-next-line react/button-has-type
type={type}
aria-busy={loading}
>
<div className={styles.spinnerWrapper} aria-hidden>
<div className={styles.spinner} />
</div>
{children}
</button>
);
}
export default Button;

View file

@ -0,0 +1,3 @@
import Button from "./Button";
export default Button;

View file

@ -0,0 +1,89 @@
.codeblock {
box-shadow: inset var(--ifm-global-shadow-lw);
margin: 0;
}
.wrapper {
position: relative;
}
:where(.buttonWrapper) {
--codeblock-button-text-color: var(--text-main);
--codeblock-button-background-color: var(--surface-2);
--codeblock-button-background-color-hover: var(--surface-accent-1);
--codeblock-button-separator-color: var(--surface-4);
--codeblock-button-border-color: var(--codeblock-button-separator-color);
--codeblock-button-icon-size: 1.5rem;
--codeblock-button-size: 2rem;
--codeblock-button-shadow: var(--ifm-global-shadow-tl);
}
:where([data-theme="dark"] .buttonWrapper) {
--codeblock-button-text-color: var(--text-0);
--codeblock-button-background-color: var(--surface-6);
--codeblock-button-background-color-hover: var(--surface-5);
--codeblock-button-separator-color: var(--codeblock-button-text-color);
--codeblock-button-shadow: var(--ifm-global-shadow-tl);
--codeblock-button-shadow: none;
}
.button {
position: relative;
width: var(--codeblock-button-size);
height: var(--codeblock-button-size);
background: var(--codeblock-button-background-color);
font-family: inherit;
font-size: 1rem;
font-size: 0.875rem;
text-transform: uppercase;
color: var(--codeblock-button-text-color);
font-weight: var(--ifm-font-weight-semibold);
border: none;
padding: 0;
height: 2.25em;
line-height: 1;
border-radius: var(--border-radius-small);
border-radius: 0;
cursor: pointer;
transition: color 200ms ease, background-color 200ms ease;
}
.button:not(:first-child) {
border-left: 1px solid var(--codeblock-button-separator-color);
}
.button:first-child {
border-top-left-radius: var(--border-radius-small);
border-bottom-left-radius: var(--border-radius-small);
}
.button:last-child {
border-top-right-radius: var(--border-radius-small);
border-bottom-right-radius: var(--border-radius-small);
}
.button:hover,
.button:focus {
background: var(--codeblock-button-background-color-hover);
}
.button svg {
width: var(--codeblock-button-icon-size);
height: var(--codeblock-button-icon-size);
}
.buttonWrapper {
position: absolute;
display: flex;
top: 1rem;
right: 1rem;
z-index: 1;
box-shadow: var(--codeblock-button-shadow);
border: 1px solid var(--codeblock-button-border-color);
border-radius: var(--border-radius-small);
opacity: 0.75;
transition: opacity 300ms ease;
}
.buttonWrapper:hover,
.buttonWrapper:focus-within {
opacity: 1;
}

View file

@ -0,0 +1,119 @@
import React, { useEffect, useState } from "react";
import type { PrismTheme, Language } from "prism-react-renderer";
import Highlight, { defaultProps } from "prism-react-renderer";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import clsx from "clsx";
import downloadFile from "js-file-download";
import styles from "./Codeblock.module.css";
type ThemeValue = "light" | "dark";
const getTheme = () =>
(document.documentElement.dataset.theme || "light") as ThemeValue;
function useGlobalTheme() {
const [theme, setTheme] = useState<ThemeValue>(getTheme);
useEffect(() => {
const mo = new MutationObserver(() => {
setTheme(getTheme());
});
mo.observe(document.documentElement, {
subtree: false,
attributeFilter: ["data-theme"],
});
return () => mo.disconnect();
});
return theme;
}
function Codeblock({
children,
language = "markup",
enableCopy = false,
download,
}: {
children: string;
language?: Language;
enableCopy?: boolean;
download?: { fileName: string; mime: string };
}): JSX.Element {
const { siteConfig } = useDocusaurusContext();
const theme = useGlobalTheme();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const codeThemeLight = (siteConfig.themeConfig.prism as any)
.theme as PrismTheme;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const codeThemeDark = (siteConfig.themeConfig.prism as any)
.darkTheme as PrismTheme;
const codeTheme = theme === "light" ? codeThemeLight : codeThemeDark;
const handleCopy = async () => {
try {
navigator.clipboard.writeText(children);
} catch {
// Copying did unfortunately not work, but we'll not crash the app
// beacause of that...
}
};
const handleDownload = () => {
if (!download || !download.fileName || !download.mime) return;
downloadFile(children, download.fileName, download.mime);
};
return (
<div className={styles.wrapper}>
<Highlight
{...defaultProps}
code={children}
language={language}
theme={codeTheme}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={clsx(className, styles.codeblock)} style={style}>
{tokens.map((line, i) => (
// eslint-disable-next-line react/jsx-key
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
// eslint-disable-next-line react/jsx-key
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
{(enableCopy || download) && (
<div className={styles.buttonWrapper}>
{enableCopy && (
<button
className={styles.button}
type="button"
aria-label="Copy content"
title="Copy content"
onClick={handleCopy}
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</svg>
</button>
)}
{download && (
<button
className={styles.button}
type="button"
aria-label="Download content as file"
title="Download content as file"
onClick={handleDownload}
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor">
<path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z" />
</svg>
</button>
)}
</div>
)}
</div>
);
}
export default Codeblock;

View file

@ -0,0 +1,3 @@
import Codeblock from "./Codeblock";
export default Codeblock;

View file

@ -0,0 +1,94 @@
:where(.dropzone) {
--dropzone-color-text: var(--text-faded);
--dropzone-icon-active: var(--text-accent-2);
--dropzone-color-background: var(--surface-2);
--dropzone-color-background-active: var(--surface-accent-0);
--dropzone-color-border: var(--color-border);
--dropzone-color-border-active: var(--color-border-accent);
--dropzone-opacity-hover-preview: 0;
--dropzone-shadow-opacity: 0;
--dropzone-border-size: 0.2rem;
}
:where([data-theme="dark"] .dropzone) {
--dropzone-color-text: var(--text-0);
--dropzone-icon-active: var(--text-accent-0);
--dropzone-color-background: transparent;
--dropzone-color-background-active: var(--surface-accent-5);
}
.dropzone {
position: relative;
cursor: pointer;
width: 100%;
height: 15em;
color: var(--dropzone-color-text);
background: var(--dropzone-color-background);
border: var(--dropzone-border-size) dashed var(--dropzone-color-border);
border-radius: var(--border-radius-medium);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--ifm-global-shadow-lw);
}
.dropzone::before {
content: "";
position: absolute;
pointer-events: none;
top: 0;
right: 0;
bottom: 0;
left: 0;
box-shadow: var(--ifm-global-shadow-md);
z-index: -1;
opacity: var(--dropzone-shadow-opacity);
transition: opacity 200ms ease;
}
.active {
--dropzone-opacity-hover-preview: 0.8;
--dropzone-shadow-opacity: 1;
}
.hasFiles {
--dropzone-color-background: var(--dropzone-color-background-active);
--dropzone-color-border: var(--dropzone-color-border-active);
--dropzone-shadow-opacity: 0.25;
}
.fileHoverPreview {
--dropzone-color-background: var(--dropzone-color-background-active);
--dropzone-color-border: var(--dropzone-color-border-active);
position: absolute;
top: calc(-1 * var(--dropzone-border-size));
right: calc(-1 * var(--dropzone-border-size));
bottom: calc(-1 * var(--dropzone-border-size));
left: calc(-1 * var(--dropzone-border-size));
display: flex;
justify-content: center;
align-items: center;
color: var(--dropzone-color-text);
background: var(--dropzone-color-background);
border: var(--dropzone-border-size) dashed var(--dropzone-color-border);
border-radius: var(--border-radius-medium);
opacity: var(--dropzone-opacity-hover-preview);
transition: opacity 150ms ease-in-out;
}
.icon {
font-size: 3rem;
width: 1em;
height: 1em;
}
.fileHoverIcon {
font-size: 5rem;
color: var(--dropzone-icon-active);
}
.uploadIcon {
color: var(--dropzone-color-text);
margin-right: 0.5rem;
}

View file

@ -0,0 +1,80 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from "react";
import clsx from "clsx";
import type { DropEvent } from "react-dropzone";
import { useDropzone } from "react-dropzone";
import type { DropzoneProps, RejectionType } from "./types";
import styles from "./Dropzone.module.css";
const Dropzone = ({
accept,
children,
className,
activeClassName,
multiple = false,
name,
onDrop,
hasSelectedFiles,
...props
}: DropzoneProps): JSX.Element => {
const handleDrop = (
accepted: File[],
fileRejections: RejectionType[],
event: DropEvent,
) => {
const rejected = fileRejections.map((rejection) => rejection.file);
onDrop(accepted, rejected, event);
};
const {
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
} = useDropzone({ accept, multiple, onDrop: handleDrop, ...props });
return (
<div
{...getRootProps()}
className={clsx(
styles.dropzone,
isDragActive && styles.active,
hasSelectedFiles && styles.hasFiles,
className,
isDragActive && activeClassName,
)}
data-testid="dropzone"
data-is-drag-active={isDragActive}
data-is-drag-accepted={isDragAccept}
data-is-drag-rejected={isDragReject}
data-has-files={hasSelectedFiles}
>
<div
className={clsx(
styles.fileHoverPreview,
isDragActive && styles.previewActive,
)}
>
<svg
className={clsx(styles.icon, styles.fileHoverIcon)}
fill="currentColor"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z" />
</svg>
</div>
<svg
className={clsx(styles.icon, styles.uploadIcon)}
fill="currentColor"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
</svg>
{children}
<input name={name} {...getInputProps()} data-testid="dropzone-input" />
</div>
);
};
export default Dropzone;

View file

@ -0,0 +1,5 @@
import Dropzone from "./Dropzone";
export { default as useDropzone } from "./useDropzone";
export default Dropzone;

View file

@ -0,0 +1,24 @@
import type { ReactNode, RefAttributes } from "react";
import type {
DropEvent,
DropzoneProps as ReactDropzoneProps,
DropzoneRef,
} from "react-dropzone";
import type { ExtendProps } from "../util/types";
export interface RejectionType {
file: File;
}
export type DropzoneProps = ExtendProps<
ReactDropzoneProps & RefAttributes<DropzoneRef>,
{
children?: ReactNode;
className?: string;
activeClassName?: string;
multiple?: boolean;
hasSelectedFiles?: boolean;
name?: string;
onDrop: (accepted: File[], rejections: File[], event: DropEvent) => void;
}
>;

View file

@ -0,0 +1,53 @@
import { useCallback, useMemo, useState } from "react";
interface DropzoneHelpers {
selectedFiles: File[];
rejectedFiles: File[];
hasSelectedFiles: boolean;
getProps: () => {
onDrop: (accepted: File[], rejected: File[]) => void;
multiple: boolean;
accept: string | string[];
hasSelectedFiles: boolean;
};
reset: () => void;
}
function useDropzone({
multiple = false,
accept,
}: {
multiple?: boolean;
accept: string | string[];
}): DropzoneHelpers {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<File[]>([]);
const hasSelectedFiles = selectedFiles.length > 0;
const getProps = useMemo(() => {
const handleDrop = (accepted: File[], rejected: File[]) => {
setSelectedFiles(accepted);
if (rejected.length === 0) {
setRejectedFiles([]);
} else {
setRejectedFiles(rejected);
}
};
return () => ({
onDrop: handleDrop,
multiple,
accept,
hasSelectedFiles,
});
}, [accept, hasSelectedFiles, multiple]);
const reset = useCallback(() => {
setSelectedFiles([]);
setRejectedFiles([]);
}, []);
return { selectedFiles, rejectedFiles, hasSelectedFiles, getProps, reset };
}
export default useDropzone;

View file

@ -0,0 +1,16 @@
.errorDisplay {
background-color: var(--red-3);
color: var(--text-accent-bg-0);
padding: 0.75em 1.25em;
border-radius: var(--ifm-global-radius);
box-shadow: var(--ifm-global-shadow-lw);
margin: 1em 0;
display: flex;
flex-direction: column;
gap: 0.75em;
}
.title {
display: block;
line-height: 1;
}

View file

@ -0,0 +1,20 @@
import type { ReactNode } from "react";
import React from "react";
import styles from "./ErrorDisplay.module.css";
function ErrorDisplay({
title,
children,
}: {
title: string;
children?: ReactNode;
}): JSX.Element {
return (
<div role="alert" className={styles.errorDisplay}>
<strong className={styles.title}>{title}</strong>
{children}
</div>
);
}
export default ErrorDisplay;

View file

@ -0,0 +1,3 @@
import ErrorDisplay from "./ErrorDisplay";
export default ErrorDisplay;

View file

@ -0,0 +1,3 @@
.headline {
font-size: 3rem;
}

View file

@ -0,0 +1,30 @@
import type { ReactNode } from "react";
import React from "react";
import Layout from "@theme/Layout";
import styles from "./PageLayout.module.css";
function PageLayout({
children,
layoutDescription,
description,
title,
headline,
}: {
children: ReactNode;
layoutDescription: string;
description: string;
headline: string;
title?: string;
}): JSX.Element {
return (
<Layout description={layoutDescription} title={title}>
<main className="container padding-top--md padding-bottom--lg">
<h1 className={styles.headline}>{headline}</h1>
<p>{description}</p>
{children}
</main>
</Layout>
);
}
export default PageLayout;

View file

@ -0,0 +1,3 @@
import PageLayout from "./PageLayout";
export default PageLayout;

View file

@ -0,0 +1,14 @@
.buttonGroup {
display: flex;
justify-content: flex-end;
margin: 1rem 0;
}
.resultDisplay {
margin: 1em 0;
}
.withError {
border: 0.2rem solid var(--text-error);
border-radius: var(--border-radius-small);
}

View file

@ -0,0 +1,115 @@
import type { FormEventHandler } from "react";
import React, { useCallback, useState } from "react";
import clsx from "clsx";
import Dropzone from "../Dropzone";
import Codeblock from "../Codeblock";
import ErrorDisplay from "../ErrorDisplay";
import useRequest, { RequestStatus } from "../util/useRequest";
import Button from "../Button";
import styles from "./Upload.module.css";
const ENDPOINT = "/";
const ACCEPT = {
"text/xml": [".xml", ".XML"],
"application/xml": [".xml", ".XML"],
};
function createFileName(selectedFileName: string | undefined) {
return selectedFileName
? `${selectedFileName.replace(/\.xml$/i, "")}-report.xml`
: "report.xml";
}
function Upload(): JSX.Element {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [rejected, setRejected] = useState<File[]>([]);
const { data, error, request, status } = useRequest();
const handleDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: File[]) => {
if (acceptedFiles.length) {
setSelectedFile(acceptedFiles[0]);
setRejected([]);
} else {
setRejected(rejectedFiles);
}
},
[],
);
const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
if (!selectedFile) return;
request(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/xml" },
body: selectedFile,
redirect: "follow",
});
};
const meaningfulErrorResponse = !!error && [406, 422].includes(error.code);
return (
<>
<form onSubmit={handleSubmit}>
{status === RequestStatus.Failure && error && !meaningfulErrorResponse && (
<ErrorDisplay title="An error occurred while validating the file">
<Codeblock enableCopy>{error.message}</Codeblock>
</ErrorDisplay>
)}
{rejected.length > 1 && (
<ErrorDisplay title="Please select a single file only" />
)}
{rejected.length === 1 && (
<ErrorDisplay title="Only XML files are supported">
<Codeblock>{`Invalid file found: ${rejected[0].name}`}</Codeblock>
</ErrorDisplay>
)}
<Dropzone
onDrop={handleDrop}
accept={ACCEPT}
multiple={false}
hasSelectedFiles={!!selectedFile}
>
{selectedFile ? (
selectedFile.name
) : (
<>Drag &amp; drop files here or click to select a file</>
)}
</Dropzone>
<div className={styles.buttonGroup}>
<Button
type="submit"
disabled={!selectedFile}
loading={status === RequestStatus.Loading}
>
Validate
</Button>
</div>
</form>
{((data && status === RequestStatus.Success) ||
meaningfulErrorResponse) && (
<div
className={clsx(
styles.resultDisplay,
meaningfulErrorResponse && styles.withError,
)}
>
<Codeblock
download={{
fileName: createFileName(selectedFile?.name),
mime: "application/xml",
}}
enableCopy
>
{data || error?.message || ""}
</Codeblock>
</div>
)}
</>
);
}
export default Upload;

View file

@ -0,0 +1,3 @@
import Upload from "./Upload";
export default Upload;

View file

@ -0,0 +1,35 @@
import React, { useEffect } from "react";
import Codeblock from "../Codeblock";
import ErrorDisplay from "../ErrorDisplay";
import useRequest, { RequestStatus } from "../util/useRequest";
function XmlView({
endpoint,
fileName,
}: {
endpoint: string;
fileName: string;
}): JSX.Element {
const { request, data, error, status } = useRequest();
useEffect(() => {
request(endpoint, { headers: { "Content-Type": "application/xml" } });
}, [endpoint, request]);
return (
<>
{status === RequestStatus.Failure && error && (
<ErrorDisplay title="An error occurred while fetching">
<Codeblock>{error.message}</Codeblock>
</ErrorDisplay>
)}
{status === RequestStatus.Success && data && (
<Codeblock download={{ fileName, mime: "application/xml" }} enableCopy>
{data}
</Codeblock>
)}
</>
);
}
export default XmlView;

View file

@ -0,0 +1,3 @@
import XmlView from "./XmlView";
export default XmlView;

View file

@ -0,0 +1,6 @@
export type SharedKeys<A, B> = Extract<keyof A, keyof B>;
export type ExtendProps<Base, Extension> = Omit<
Base,
SharedKeys<Base, Extension>
> &
Extension;

View file

@ -0,0 +1,93 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
export enum RequestStatus {
Idle = "idle",
Loading = "loading",
Success = "success",
Failure = "failure",
}
export interface RequestState {
status: RequestStatus;
data: null | string;
error: null | { code: number; message: string };
}
export interface UseRequest extends RequestState {
request: (endpoint: string, init?: RequestInit) => void;
}
const EMPTY_REQUEST: RequestState = {
status: RequestStatus.Idle,
data: null,
error: null,
};
function createEndpoint(endpoint: string, apiBase: string): string {
const segments = apiBase
.split("/")
.concat(endpoint.split("/"))
.filter(Boolean);
return `/${segments.join("/")}`;
}
function useRequest(): UseRequest {
const [requestState, setRequest] = useState(EMPTY_REQUEST);
const { siteConfig } = useDocusaurusContext();
const apiBase = siteConfig.customFields?.apiBase as string;
const isMountedRef = useRef(true);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const request = useCallback(
(endpoint: string, init?: RequestInit) => {
setRequest((prev) => ({ ...prev, status: RequestStatus.Loading }));
fetch(createEndpoint(endpoint, apiBase), init)
.then((response) => {
return response.text().then((text) => ({
data: text,
ok: response.ok,
code: response.status,
}));
})
.then(({ data, ok, code }) => {
if (!isMountedRef.current) return;
if (ok) {
setRequest({
status: RequestStatus.Success,
data,
error: null,
});
} else {
setRequest((prev) => ({
...prev,
status: RequestStatus.Failure,
error: { code, message: data },
}));
}
})
.catch((error) => {
if (!isMountedRef.current) return;
setRequest((prev) => ({
...prev,
status: RequestStatus.Failure,
error: {
code: 0,
message: error?.toString?.() || "An unknown error occurred",
},
}));
});
},
[apiBase],
);
return useMemo(() => ({ ...requestState, request }), [request, requestState]);
}
export default useRequest;

View file

@ -0,0 +1,172 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: var(--blue-3);
--ifm-color-primary-dark: var(--blue-4);
--ifm-color-primary-darker: var(--blue-5);
--ifm-color-primary-darkest: var(--blue-6);
--ifm-color-primary-light: var(--blue-2);
--ifm-color-primary-lighter: var(--blue-1);
--ifm-color-primary-lightest: var(--blue-0);
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
--ifm-global-radius: var(--border-radius-small);
--ifm-font-family-base: var(--font-sans);
--ifm-font-family-monospace: var(--font-mono);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"]:root {
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
--ifm-background-color: var(--surface-6);
}
[data-theme] .footer {
--ifm-footer-background-color: var(--surface-accent-4);
--ifm-footer-color: var(--text-accent-bg-main);
--ifm-footer-link-color: var(--text-accent-bg-main);
--ifm-footer-link-hover-color: var(--text-accent-bg-main);
--ifm-footer-title-color: var(--text-accent-bg-main);
}
[data-theme="dark"] .footer {
--ifm-footer-background-color: var(--surface-6);
border-top: 1px solid var(--ifm-table-border-color);
}
[data-theme] .navbar {
--ifm-navbar-background-color: var(--blue-4);
}
/* XÖV Theme */
:where(html) {
--blue-0: hsl(206 100% 95%);
--blue-1: hsl(206 100% 80%);
--blue-2: hsl(206 100% 65%);
--blue-3: hsl(206 100% 47%);
--blue-4: hsl(220 41% 30%);
--blue-5: hsl(220 41% 24%);
--blue-6: hsl(220 41% 18%);
--gray-0: hsl(216 33% 100%);
--gray-1: hsl(216 33% 97%);
--gray-2: hsl(220 21% 95%);
--gray-3: hsl(220 21% 92%);
--gray-4: hsl(212 10% 73%);
--gray-5: hsl(212 10% 45%);
--gray-6: hsl(212 15% 13%);
--red-0: hsl(357 80% 96%);
--red-1: hsl(357 80% 89%);
--red-2: hsl(357 80% 75%);
--red-3: hsl(357 80% 60%);
--red-4: hsl(357 80% 40%);
--red-5: hsl(357 60% 22%);
--orange-0: hsl(46 80% 90%);
--orange-1: hsl(46 80% 80%);
--orange-2: hsl(46 80% 68%);
--orange-3: hsl(46 80% 40%);
--orange-4: hsl(46 80% 25%);
--orange-5: hsl(46 80% 10%);
--green-0: hsl(162 100% 93%);
--green-1: hsl(162 100% 74%);
--green-2: hsl(162 100% 45%);
--green-3: hsl(162 100% 30%);
--green-4: hsl(162 100% 20%);
--green-5: hsl(162 100% 10%);
/* Surface colors */
--surface-0: var(--gray-0);
--surface-1: var(--gray-1);
--surface-2: var(--gray-2);
--surface-3: var(--gray-3);
--surface-4: var(--gray-4);
--surface-5: var(--gray-5);
--surface-6: var(--gray-6);
--surface-accent-0: var(--blue-0);
--surface-accent-1: var(--blue-1);
--surface-accent-2: var(--blue-2);
--surface-accent-3: var(--blue-3);
--surface-accent-4: var(--blue-4);
--surface-accent-5: var(--blue-5);
/* Text colors */
--text-0: var(--gray-4);
--text-1: var(--gray-5);
--text-2: var(--gray-6);
--text-main: var(--text-2);
--text-faded: var(--text-1);
--text-accent-0: var(--blue-3);
--text-accent-1: var(--blue-4);
--text-accent-2: var(--blue-5);
--text-accent: var(--text-accent-2);
--text-accent-bg-0: var(--gray-0);
--text-accent-bg-1: var(--gray-1);
--text-accent-bg-2: var(--gray-4);
--text-accent-bg-3: var(--blue-4);
--text-accent-bg-main: var(--text-accent-bg-0);
--text-negative: var(--red-3);
--text-error: var(--red-3);
--text-warning: var(--orange-3);
--text-info: var(--blue-3);
--text-success: var(--green-3);
/* Misc elements */
--divider: var(--gray-4);
--scrollthumb: var(--gray-4);
--input-background: var(--surface-0);
--input-background-disabled: var(--surface-2);
/* Border Radius */
--border-radius-small: 2px;
--border-radius-medium: 0.2rem;
--border-radius-large: 1rem;
--color-border: var(--gray-4);
--color-border-hover: var(--gray-4);
--color-border-accent: var(--blue-2);
--color-border-accent-hover: var(--blue-2);
/* Fonts */
--font-sans: "Open Sans", system-ui, -apple-system, "Segoe UI", "Roboto",
"Ubuntu", "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
--font-serif: ui-serif, serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
--font-mono: "Cascadia Code", "Dank Mono", "Operator Mono", "Inconsolata",
"Fira Mono", ui-monospace, "SF Mono", "Monaco", "Droid Sans Mono",
"Source Code Pro", monospace, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
}
/* Scrollbars: */
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollthumb) transparent;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: min(8px, 0.5rem);
height: min(8px, 0.5rem);
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--scrollthumb);
border-radius: 999rem;
}

View file

@ -0,0 +1,18 @@
import React from "react";
import PageLayout from "@site/src/components/PageLayout";
import XmlView from "@site/src/components/XmlView";
function ConfigPage(): JSX.Element {
return (
<PageLayout
title="Validator configuration"
layoutDescription="The currently loaded validator configuration"
headline="Validator configuration"
description="View the currently loaded validator configuration."
>
<XmlView endpoint="/server/config" fileName="config.xml" />
</PageLayout>
);
}
export default ConfigPage;

View file

@ -0,0 +1,3 @@
import ConfigPage from "./ConfigPage";
export default ConfigPage;

View file

@ -0,0 +1,18 @@
import React from "react";
import PageLayout from "@site/src/components/PageLayout";
import XmlView from "@site/src/components/XmlView";
function HealthPage(): JSX.Element {
return (
<PageLayout
title="Health information"
layoutDescription="Health and status information about the system"
headline="Server health information"
description="Information about health and status of the running system."
>
<XmlView endpoint="/server/health" fileName="health.xml" />
</PageLayout>
);
}
export default HealthPage;

View file

@ -0,0 +1,3 @@
import HealthPage from "./HealthPage";
export default HealthPage;

View file

@ -0,0 +1,15 @@
import React from "react";
import Upload from "../components/Upload";
import PageLayout from "../components/PageLayout";
export default function Home(): JSX.Element {
return (
<PageLayout
layoutDescription="KoSIT Validator Daemon"
headline="Try the validator!"
description="Upload an XML file here to validate its contents. Note: this is just a demo implementation, not meant for production usage. If you need a production ready implementation you are welcome to contribute to the open source project."
>
<Upload />
</PageLayout>
);
}

View file

@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.