1
Fork 0
mirror of https://github.com/Steffo99/festa.git synced 2024-12-22 06:34:22 +00:00

Make big changes to how mood, errors and loading are handled

This commit is contained in:
Steffo 2022-07-19 00:42:56 +02:00
parent 7717dfc3c7
commit 02d7251b16
Signed by: steffo
GPG key ID: 6965406171929D01
19 changed files with 362 additions and 120 deletions

View file

@ -6,6 +6,7 @@ import { usePromise, UsePromiseStatus } from "../../generic/loading/promise"
import { FestaIcon } from "../../generic/renderers/fontawesome"
import { Tool } from "../../generic/toolbar/tool"
import cursor from "../../../styles/cursor.module.css"
import mood from "../../../styles/mood.module.css"
export type ToolToggleEditingProps = {
@ -43,7 +44,7 @@ export function ToolToggleEditing(props: ToolToggleEditingProps) {
save()
setEditing(EditingMode.VIEW)
}}
className={"positive"}
className={mood.positive}
>
<FestaIcon icon={faSave} />
</Tool>

View file

@ -1,6 +1,6 @@
import { Component, ErrorInfo, ReactNode } from "react";
import { ViewNotice } from "../views/notice";
import { ErrorBlock } from "./renderers";
import { ErrorBlock, ErrorMain } from "./renderers";
export type ErrorBoundaryProps = {
@ -20,7 +20,7 @@ export type ErrorBoundaryState = {
*
* To be used in `pages/_app`.
*/
export class PageErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
export class ErrorBoundaryPage extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: undefined, errorInfo: undefined }
@ -37,7 +37,7 @@ export class PageErrorBoundary extends Component<ErrorBoundaryProps, ErrorBounda
return (
<ViewNotice
notice={
<ErrorBlock text={this.props.text} error={this.state.error} />
<ErrorMain text={this.props.text} error={this.state.error} />
}
/>
)
@ -54,7 +54,7 @@ export class PageErrorBoundary extends Component<ErrorBoundaryProps, ErrorBounda
*
* To be used in other components.
*/
export class BlockErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
export class ErrorBoundaryBlock extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: undefined, errorInfo: undefined }

View file

@ -1,8 +1,20 @@
.errorBlock > pre {
.errorBlock > pre, .errorMain > pre {
display: inline-block;
margin: 0;
max-width: 100%;
text-align: left;
}
.errorMain {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.errorMain > .errorIcon {
font-size: 4em;
}

View file

@ -2,114 +2,164 @@ import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FestaIcon } from "../renderers/fontawesome";
import { default as classNames } from "classnames"
import style from "./renderers.module.css"
import { memo } from "react";
import mood from "../../../styles/mood.module.css"
import { ComponentPropsWithoutRef, memo } from "react";
import { AxiosError } from "axios";
export type ErrorTraceProps = {
/**
* Props of {@link ErrorTrace}.
*/
type ErrorTraceProps = ComponentPropsWithoutRef<"code"> & {
/**
* The error to render the stack trace of.
*/
error: Error,
inline: boolean,
/**
* Whether error messages in JSON format should be prettified or not.
*/
prettify: boolean,
}
export const ErrorTrace = memo((props: ErrorTraceProps) => {
if (props.error instanceof AxiosError) {
if (props.error.response) {
if (props.error.response.data) {
const json = JSON.stringify(props.error.response.data, undefined, props.inline ? undefined : 4).replaceAll("\\n", "\n")
/**
* Component rendering the details of an {@link Error}.
*
* Not to use by itself; should be used to display the error in other error renderers.
*/
const ErrorTrace = memo(({ error, prettify, ...props }: ErrorTraceProps) => {
if (error instanceof AxiosError) {
if (error.response) {
if (error.response.data) {
const json = JSON.stringify(error.response.data, undefined, prettify ? 4 : undefined).replaceAll("\\n", "\n")
return (
<code>
<b>API {props.error.response.status}</b>
:&nbsp;
{json}
<code {...props}>
<span>
<b>API {error.response.status}</b>
:&nbsp;
</span>
<span>
{json}
</span>
</code>
)
}
return (
<code>
<b>HTTP {props.error.response.status}</b>
:&nbsp;
{props.error.message}
<code {...props}>
<span>
<b>HTTP {error.response.status}</b>
:&nbsp;
</span>
<span>
{error.message}
</span>
</code>
)
}
return (
<code>
<b>{props.error.code}</b>
:&nbsp;
{props.error.message}
<code {...props}>
<span>
<b>{error.code}</b>
:&nbsp;
</span>
<span>
{error.message}
</span>
</code>
)
}
return (
<code>
<b>{props.error.name}</b>
:&nbsp;
{props.error.message}
<code {...props}>
<span>
<b>{error.name}</b>
:&nbsp;
</span>
<span>
{error.message}
</span>
</code>
)
})
ErrorTrace.displayName = "ErrorTrace"
export type ErrorInlineProps = {
/**
* Props for "error" renderers.
*/
export type ErrorProps = {
error: Error,
text?: string
}
/**
* Component rendering a `span` element containing an error passed to it as props.
* Inline component for rendering errors.
*
* May or may not include some text to display to the user.
* It displays an error {@link FestaIcon}, followed by some optional text, and finally the {@link ErrorTrace}.
*/
export const ErrorInline = memo((props: ErrorInlineProps) => {
export const ErrorInline = memo(({ error, text }: ErrorProps) => {
return (
<span className={classNames("negative", style.error, style.errorInline)}>
<FestaIcon icon={faCircleExclamation} />
&nbsp;
{props.text ?
<>
<span>
{props.text}
</span>
&nbsp;
</>
: null}
<ErrorTrace error={props.error} inline={true} />
<span className={classNames(mood.negative, style.error, style.errorInline)}>
<FestaIcon icon={faCircleExclamation} className={style.errorIcon} />
{!!text && <>
&nbsp;
<span className={style.errorText}>
{text}
</span>
&nbsp;
</>}
<ErrorTrace error={error} prettify={false} className={style.errorTrace} />
</span>
)
})
ErrorInline.displayName = "ErrorInline"
export type ErrorBlockProps = {
error: Error,
text: string
}
/**
* Component rendering a `div` element containing an error passed to it as props.
* Block component for rendering errors.
*
* Must include some text to display to the user.
* It displays an inline error {@link FestaIcon}, followed by some **required** text, with the {@link ErrorTrace} below.
*/
export const ErrorBlock = memo((props: ErrorBlockProps) => {
export const ErrorBlock = memo(({ error, text }: ErrorProps & { text: string }) => {
return (
<div className={classNames("negative", style.error, style.errorBlock)}>
<div className={classNames(mood.negative, style.error, style.errorBlock)}>
<p>
<FestaIcon icon={faCircleExclamation} />
<FestaIcon icon={faCircleExclamation} className={style.errorIcon} />
&nbsp;
<span>
{props.text}
<span className={style.errorText}>
{text}
</span>
</p>
<pre>
<ErrorTrace error={props.error} inline={false} />
<ErrorTrace error={error} prettify={false} className={style.errorTrace} />
</pre>
</div>
)
})
ErrorBlock.displayName = "ErrorBlock"
ErrorBlock.displayName = "ErrorBlock"
/**
* Block component for rendering errors at the center of the page.
*
* It displays an inline error {@link FestaIcon}, followed by some **required** text, with the {@link ErrorTrace} below.
*/
export const ErrorMain = memo(({ error, text }: ErrorProps & { text: string }) => {
return (
<div className={classNames(mood.negative, style.error, style.errorMain)}>
<FestaIcon icon={faCircleExclamation} className={style.errorIcon} />
<p className={style.errorText}>
{text}
</p>
<pre>
<ErrorTrace error={error} prettify={false} className={style.errorTrace} />
</pre>
</div>
)
})
ErrorMain.displayName = "ErrorMain"

View file

@ -0,0 +1,17 @@
import { ReactNode, Suspense } from "react"
import { LoadingMain } from "./renderers"
export type LoadingBoundaryProps = {
text?: string,
children: ReactNode,
}
export function LoadingBoundaryPage({ text, children }: LoadingBoundaryProps) {
return (
<Suspense fallback={<LoadingMain text={text} />}>
{children}
</Suspense>
)
}

View file

@ -0,0 +1,11 @@
.loadingMain {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.loadingMain > .loadingIcon {
font-size: 4em;
}

View file

@ -0,0 +1,62 @@
import { faAsterisk } from "@fortawesome/free-solid-svg-icons";
import classNames from "classnames";
import { memo } from "react";
import { FestaIcon } from "../renderers/fontawesome";
import style from "./renderers.module.css"
/**
* Props for "loading" renderers, such as {@link LoadingInline} and {@link LoadingMain}.
*/
export type LoadingProps = {
text?: string
}
/**
* Inline component displaying an animated loading icon with an optional message displayed on the right.
*
* @see {@link ErrorInline}
*/
export const LoadingInline = memo(({ text }: LoadingProps) => {
return (
<span className={classNames(style.loading, style.loadingInline)}>
<FestaIcon
icon={faAsterisk}
spin
className={style.loadingIcon}
/>
{!!text && <>
&nbsp;
<span className={style.loadingText}>
{text}
</span>
</>}
</span>
)
})
LoadingInline.displayName = "LoadingInline"
/**
* Block component displaying a big loading icon with an optional message displayed below.
*
* @see {@link ErrorMain}
*/
export const LoadingMain = memo(({ text }: LoadingProps) => {
return (
<div className={classNames(style.loading, style.loadingMain)}>
<FestaIcon
icon={faAsterisk}
spin
className={style.loadingIcon}
/>
{!!text &&
<p className={style.loadingText}>
{text}
</p>
}
</div>
)
})
LoadingMain.displayName = "LoadingMain"

View file

@ -1,22 +0,0 @@
import { faAsterisk } from "@fortawesome/free-solid-svg-icons";
import { memo } from "react";
import { FestaIcon } from "../renderers/fontawesome";
export type LoadingTextInlineProps = {
text: string
}
/**
* Component displaying an animated loading message for the user.
*/
export const LoadingTextInline = memo((props: LoadingTextInlineProps) => {
return (
<span>
<FestaIcon icon={faAsterisk} spin />
&nbsp;
{props.text}
</span>
)
})
LoadingTextInline.displayName = "LoadingTextInline"

View file

@ -1,4 +1,4 @@
.view-notice {
.viewNotice {
display: grid;
flex-direction: column;

View file

@ -10,9 +10,10 @@ import { useAxiosRequest } from "../../auth/requests"
import { ErrorBlock } from "../../generic/errors/renderers"
import { promiseMultiplexer } from "../../generic/loading/promise"
import { swrMultiplexer } from "../../generic/loading/swr"
import { LoadingTextInline } from "../../generic/loading/textInline"
import { LoadingInline } from "../../generic/loading/renderers"
import { FestaIcon } from "../../generic/renderers/fontawesome"
import style from "./events.module.css"
import mood from "../../../styles/mood.module.css"
/**
@ -88,7 +89,7 @@ const LandingActionEventsFormCreate = () => {
<button
aria-label={t("landingEventsCreateSubmitLabel")}
disabled={!name}
className={classNames(style.landingActionEventsFormCreateSubmit, "positive")}
className={classNames(style.landingActionEventsFormCreateSubmit, mood.positive)}
onClick={e => {
e.preventDefault()
run({ data: { name } })
@ -100,7 +101,7 @@ const LandingActionEventsFormCreate = () => {
),
pending: ({ }) => (
<p>
<LoadingTextInline text={t("landingEventsCreatePending")} />
<LoadingInline text={t("landingEventsCreatePending")} />
</p>
),
rejected: ({ error }) => (
@ -111,7 +112,7 @@ const LandingActionEventsFormCreate = () => {
fulfilled: ({ result }) => {
return (
<p>
<LoadingTextInline text={t("landingEventsCreateFulfilled", name)} />
<LoadingInline text={t("landingEventsCreateFulfilled", name)} />
</p>
)
},
@ -127,7 +128,7 @@ export const LandingActionEvents = () => {
hook: apiHook,
loading: () => (
<p>
<LoadingTextInline text={t("landingEventsLoading")} />
<LoadingInline text={t("landingEventsLoading")} />
</p>
),
ready: (data) => (<>

View file

@ -3,7 +3,7 @@ import { useAxiosRequest } from "../../auth/requests"
import { TelegramLoginButton } from "../../auth/telegram/loginButton"
import { ErrorBlock } from "../../generic/errors/renderers"
import { promiseMultiplexer } from "../../generic/loading/promise"
import { LoadingTextInline } from "../../generic/loading/textInline"
import { LoadingInline } from "../../generic/loading/renderers"
import { useDefinedContext } from "../../../utils/definedContext"
import { AuthContext } from "../../auth/base"
import { useRouter } from "next/router"
@ -32,7 +32,7 @@ export const LandingActionLogin = () => {
</>,
pending: ({ }) => (
<p>
<LoadingTextInline text={t("landingLoginTelegramPending")} />
<LoadingInline text={t("landingLoginTelegramPending")} />
</p>
),
fulfilled: ({ result }) => {
@ -40,7 +40,7 @@ export const LandingActionLogin = () => {
return (
<p>
<LoadingTextInline text={t("landingLoginTelegramFulfilled")} />
<LoadingInline text={t("landingLoginTelegramFulfilled")} />
</p>
)
},

View file

@ -3,12 +3,13 @@ import '@fortawesome/fontawesome-svg-core/styles.css'
import { AppProps } from 'next/app'
import { appWithTranslation, useTranslation } from 'next-i18next'
import { AxiosSWRFetcherProvider } from '../components/auth/requests'
import { PageErrorBoundary } from '../components/generic/errors/boundaries'
import { ErrorBoundaryPage } from '../components/generic/errors/boundaries'
import { AuthContextProvider } from '../components/auth/provider'
import { PostcardRenderer } from '../components/postcard/renderer'
import { config as fontAwesomeConfig } from '@fortawesome/fontawesome-svg-core'
import { PostcardContextProvider } from '../components/postcard/provider'
import defaultPostcard from "../public/postcards/adi-goldstein-Hli3R6LKibo-unsplash.jpg"
import { LoadingBoundaryPage } from '../components/generic/loading/boundaries'
fontAwesomeConfig.autoAddCss = false
@ -18,18 +19,22 @@ const App = ({ Component, pageProps }: AppProps): JSX.Element => {
const { t } = useTranslation()
return (
<PageErrorBoundary text={t("genericError")}>
<ErrorBoundaryPage text={t("genericError")}>
<AxiosSWRFetcherProvider>
<PostcardContextProvider defaultPostcard={defaultPostcard}>
<AuthContextProvider storageKey="auth">
<AxiosSWRFetcherProvider>
<PostcardRenderer />
<Component {...pageProps} />
<ErrorBoundaryPage text={t("genericError")}>
<LoadingBoundaryPage>
<Component {...pageProps} />
</LoadingBoundaryPage>
</ErrorBoundaryPage>
</AxiosSWRFetcherProvider>
</AuthContextProvider>
</PostcardContextProvider>
</AxiosSWRFetcherProvider>
</PageErrorBoundary>
</ErrorBoundaryPage>
)
}

36
pages/debug/error.tsx Normal file
View file

@ -0,0 +1,36 @@
import { NextPage, NextPageContext } from "next";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { ErrorBlock, ErrorMain } from "../../components/generic/errors/renderers";
import { LoadingMain } from "../../components/generic/loading/renderers";
import { ViewNotice } from "../../components/generic/views/notice";
import { Postcard } from "../../components/postcard/changer";
import debugPostcard from "../../public/postcards/markus-spiske-iar-afB0QQw-unsplash.jpg"
export async function getStaticProps(context: NextPageContext) {
return {
props: {
...(await serverSideTranslations(context.locale ?? "it-IT", ["common"]))
}
}
}
const Page500: NextPage = (props) => {
const { t } = useTranslation()
return <>
<Postcard
src={debugPostcard}
/>
<ViewNotice
notice={<>
<ErrorMain error={new Error("Example")} text={t("debugError")} />
</>}
/>
</>
}
export default Page500

36
pages/debug/loading.tsx Normal file
View file

@ -0,0 +1,36 @@
import { NextPage, NextPageContext } from "next";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { ErrorBlock } from "../../components/generic/errors/renderers";
import { LoadingMain } from "../../components/generic/loading/renderers";
import { ViewNotice } from "../../components/generic/views/notice";
import { Postcard } from "../../components/postcard/changer";
import debugPostcard from "../../public/postcards/markus-spiske-iar-afB0QQw-unsplash.jpg"
export async function getStaticProps(context: NextPageContext) {
return {
props: {
...(await serverSideTranslations(context.locale ?? "it-IT", ["common"]))
}
}
}
const Page500: NextPage = (props) => {
const { t } = useTranslation()
return <>
<Postcard
src={debugPostcard}
/>
<ViewNotice
notice={<>
<LoadingMain text={t("debugLoading")} />
</>}
/>
</>
}
export default Page500

View file

@ -19,8 +19,12 @@ import { ViewContent } from '../../components/generic/views/content'
import { useAxios } from '../../components/auth/requests'
import { faAsterisk } from '@fortawesome/free-solid-svg-icons'
import { FestaIcon } from '../../components/generic/renderers/fontawesome'
import { usePromise, UsePromiseStatus } from '../../components/generic/loading/promise'
import { promiseMultiplexer, usePromise, UsePromiseStatus } from '../../components/generic/loading/promise'
import { EditingContextProvider } from '../../components/generic/editable/provider'
import { swrMultiplexer } from '../../components/generic/loading/swr'
import { LoadingMain, LoadingInline } from '../../components/generic/loading/renderers'
import { ViewNotice } from '../../components/generic/views/notice'
import { ErrorBlock } from '../../components/generic/errors/renderers'
export async function getServerSideProps(context: NextPageContext) {
@ -40,52 +44,75 @@ type PageEventProps = {
const PageEvent: NextPage<PageEventProps> = ({ slug }) => {
const { t } = useTranslation()
const { data, isValidating, mutate } = useSWR<Event>(`/api/events/${slug}`)
const swrHook = useSWR<Event>(`/api/events/${slug}`)
const [auth,] = useDefinedContext(AuthContext)
const axios = useAxios()
const save = useCallback(
async () => {
if (data === undefined) {
if (swrHook.data === undefined) {
console.warn("[PageEvent] Tried to save while no data was available.")
return
}
await axios.patch(`/api/events/${slug}`, data)
mutate(data)
await axios.patch(`/api/events/${slug}`, swrHook.data)
swrHook.mutate(swrHook.data)
console.debug("[PageEvent] Saved updated data successfully!")
},
[axios, data]
[axios, swrHook]
)
return <>
<Head>
<title key="title">{data?.name ?? slug} - {t("siteTitle")}</title>
<title key="title">{swrHook.data?.name ?? slug} - {t("siteTitle")}</title>
</Head>
<Postcard
src={data?.postcard || defaultPostcard}
src={swrHook.data?.postcard || defaultPostcard}
/>
<WIPBanner />
<EditingContextProvider>
<ViewContent
title={
<EditableText
value={data?.name ?? slug}
onChange={e => mutate(async state => state ? { ...state, name: e.target.value } : undefined, { revalidate: false })}
placeholder={t("eventTitlePlaceholder")}
{swrMultiplexer({
hook: swrHook,
loading: () => (
<ViewNotice
notice={
<LoadingMain text={t("eventLoading")} />
}
/>
}
content={
<EditableMarkdown
value={data?.description ?? ""}
onChange={e => mutate(async state => state ? { ...state, description: e.target.value } : undefined, { revalidate: false })}
placeholder={t("eventDescriptionPlaceholder")}
),
ready: (data) => (
<ViewContent
title={
<EditableText
value={data?.name ?? slug}
onChange={e => swrHook.mutate(async state => state ? { ...state, name: e.target.value } : undefined, { revalidate: false })}
placeholder={t("eventTitlePlaceholder")}
/>
}
content={
<EditableMarkdown
value={data?.description ?? ""}
onChange={e => swrHook.mutate(async state => state ? { ...state, description: e.target.value } : undefined, { revalidate: false })}
placeholder={t("eventDescriptionPlaceholder")}
/>
}
/>
}
/>
),
error: (error) => (
<ViewNotice
notice={
<ErrorBlock
text={t("eventError")}
error={error}
/>
}
/>
)
})}
<ToolBar vertical="vadapt" horizontal="right">
<ToolToggleVisibility />
{data && auth?.userId === data?.creatorId &&
{swrHook.data && auth?.userId === swrHook.data?.creatorId &&
<ToolToggleEditing
save={save}
/>

View file

@ -19,5 +19,9 @@
"toolToggleEditingSave": "Salva modifiche",
"toolToggleEditingEdit": "Modifica",
"eventNamePlaceholder": "Nome evento",
"eventDescriptionPlaceholder": "Descrizione evento in **Markdown**"
"eventDescriptionPlaceholder": "Descrizione evento in **Markdown**",
"eventLoading": "Caricamento dei dettagli dell'evento in corso...",
"eventError": "Si è verificato il seguente errore nel recupero dei dettagli dell'evento:",
"debugLoading": "Questo è un esempio di caricamento full-page.",
"debugError": "Questo è un esempio di errore full-page."
}

BIN
public/postcards/markus-spiske-iar-afB0QQw-unsplash.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -1,6 +1,5 @@
@import "color-schemes.css";
@import "elements.css";
@import "mood.css";
@import "flex.css";