Compare commits

...

4 commits

Author SHA1 Message Date
6d17d0b954
Make copy text monospace 2024-09-23 19:19:23 +00:00
9b3f590bec
Cleanup README.md 2024-09-23 19:19:23 +00:00
3eb11d3aeb
Better loading (and some other misc cleanup) 2024-09-23 19:19:23 +00:00
bf6709e2fd
Better error pages 2024-09-23 19:19:23 +00:00
11 changed files with 145 additions and 31 deletions

View file

@ -6,8 +6,12 @@ Motto presents files from a static file server in a more friendly way
### Configuration ### Configuration
- `ROOT_URL=(url)` - the static file server instance to get files from Configuration is done through environment variables in `.env.local`
- `ENABLE_REPOINTING=(true|default:false)` - enable [repointing](#Repointing)
| Variable | type | default | description |
|--------------------|---------|---------|---------------------------------------------------|
| `ROOT_URL` | string | N/A | the static file server instance to get files from |
|`ENABLE_REPOINTING` | boolean | `false` | enable [repointing](#Repointing) |
### Repointing ### Repointing

View file

@ -2,8 +2,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import NetworkError from './types/error/network'; import Loading from './loading';
import ContentTypeError from './types/error/content_type'; import ContentTypeError from './types/error/content_type';
import NetworkError from './types/error/network';
import StatusError from './types/error/status';
import ImageContent from './types/image'; import ImageContent from './types/image';
import MarkdownContent from './types/markdown'; import MarkdownContent from './types/markdown';
import Terminal from './types/terminal'; import Terminal from './types/terminal';
@ -86,17 +88,21 @@ export default function Content({ src }: { src: string}) {
if (content == undefined) { if (content == undefined) {
const result = fetch(src) const result = fetch(src)
.then(response => { .then(response => {
for (const type of recognized_types) { if (response.status < 400) {
if (is_type(response, type)) { for (const type of recognized_types) {
const emitted = type.emit(); if (is_type(response, type)) {
if (emitted != undefined) { const emitted = type.emit();
result.then(emitted.postprocess); if (emitted != undefined) {
return emitted.process(response); result.then(emitted.postprocess);
return emitted.process(response);
}
return;
} }
return;
} }
set_content(<ContentTypeError content_type={response.headers.get('Content-Type')!.split(';')[0]} />);
} else {
set_content(<StatusError status={response.status} />);
} }
set_content(<ContentTypeError content_type={response.headers.get('Content-Type')!.split(';')[0]} />);
}) })
.catch(err => { .catch(err => {
set_content(<NetworkError err={err} />); set_content(<NetworkError err={err} />);
@ -104,5 +110,5 @@ export default function Content({ src }: { src: string}) {
} }
}); });
return content ?? <p>Loading...</p>; return content ?? <Loading />;
} }

View file

@ -10,20 +10,20 @@ export const copy = style({
}); });
export const copy_button = style({ export const copy_button = style({
display: 'flex', display: 'flex',
margin: 0, margin: 0,
border: 'none', border: 'none',
padding: 0, padding: 0,
outline: 'inherit', outline: 'inherit',
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: '1.1em', fontSize: '1.1em',
lineHeight: '2em', lineHeight: '2em',
color: 'inherit', color: 'inherit',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
transition: 'background-color 0.35s', transition: 'background-color 0.35s',
background: 'none', background: 'none',
width: 'max-content', width: 'max-content',
height: '100%', height: '100%',
':hover': { ':hover': {
backgroundColor: colors.background2, backgroundColor: colors.background2,
}, },
@ -79,7 +79,9 @@ export const copied_image_light = style([copy_image, {
export const copy_text = style({ export const copy_text = style({
margin: 0, margin: 0,
marginLeft: '1em', marginLeft: '1em',
width: 'max-content', width: 'max-content',
fontFamily: 'monospace',
fontSize: '1.2em',
}); });

View file

@ -0,0 +1,35 @@
import { keyframes, style } from '@vanilla-extract/css';
import * as colors from '../../colors.css';
export const loading_squares = style({
display: 'flex',
justifyContent: 'center',
width: '100%',
marginTop: '4em',
height: 'max-content',
});
const animation = keyframes({
'0%': { backgroundColor: colors.background2 },
'50%': { backgroundColor: colors.foreground },
'100%': { backgroundColor: colors.background2 },
});
export const square = style({
margin: '0 1em',
width: '1em',
height: '1em',
backgroundColor: colors.background2,
animationName: animation,
animationDuration: '2s',
animationIterationCount: 'infinite',
});
export const loading = [0, 1, 2, 3, 4].map(index =>
style([square, {
animationDelay: `${0.2 * index}s`
}])
);

View file

@ -0,0 +1,9 @@
import * as style from './loading.css';
export default function Loading() {
return (
<div className={style.loading_squares}>
{style.loading.map((style, index) => <div key={index} className={style}></div>)}
</div>
);
}

View file

@ -1,3 +1,5 @@
import Error from './error'
export default function ContentTypeError({ content_type }: { content_type: string }) { export default function ContentTypeError({ content_type }: { content_type: string }) {
return <p>{`Unrecognised MIME type: ${content_type}`}</p> return <Error>{`Unrecognised MIME type: ${content_type}`}</Error>
} }

View file

@ -0,0 +1,18 @@
import { style } from '@vanilla-extract/css'
export const error = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
height: 'max-content',
});
export const message = style({
fontSize: '2em',
});
export const sad = style({
margin: '0em',
fontSize: '3em',
})

View file

@ -0,0 +1,13 @@
import * as style from './error.css'
const faces = [
"(•́︵•̀)",
"(╥﹏╥)",
]
export default function Error({ children }: { children: React.ReactNode }) {
return <div className={style.error}>
<p className={style.message}>{children}</p>
<p className={style.sad}>{faces[Math.floor(Math.random() * faces.length)]}</p>
</div>
}

View file

@ -1,3 +1,6 @@
import Error from './error'
export default function NetworkError({ err }: { err: string }) { export default function NetworkError({ err }: { err: string }) {
return <p>{`Error: ${err}`}</p>; console.error(err);
return <Error>Network Error</Error>;
} }

View file

@ -0,0 +1,23 @@
import Error from './error'
const statuses = {
[400]: "Bad Request",
[401]: "Unauthorized",
[402]: "Payment Required",
[403]: "Forbidden",
[404]: "Not Found",
[405]: "Method Not Allowed",
[500]: "Internal Server Error",
[501]: "Not Implemented",
[502]: "Bad Gateway",
[503]: "Service Unavailable",
[504]: "Gateway Timeout",
[505]: "HTTP Version Not Supported"
}
export default function StatusError({ status }: { status: number }) {
if (status in statuses) {
return <Error>{`${status} ${statuses[status as keyof typeof statuses]}`}</Error>
} else {
return <Error>{status}</Error>
}
}

View file

@ -2,7 +2,6 @@ import * as style from './page.css';
export default function App() { export default function App() {
const supports_repointing = process.env.ENABLE_REPOINTING == 'true'; const supports_repointing = process.env.ENABLE_REPOINTING == 'true';
console.log(supports_repointing);
return ( return (
<div className={style.center}> <div className={style.center}>
<div className={style.content}> <div className={style.content}>