1
Fork 0
mirror of https://github.com/Steffo99/festa.git synced 2024-12-22 14:44:21 +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 { FestaIcon } from "../../generic/renderers/fontawesome"
import { Tool } from "../../generic/toolbar/tool" import { Tool } from "../../generic/toolbar/tool"
import cursor from "../../../styles/cursor.module.css" import cursor from "../../../styles/cursor.module.css"
import mood from "../../../styles/mood.module.css"
export type ToolToggleEditingProps = { export type ToolToggleEditingProps = {
@ -43,7 +44,7 @@ export function ToolToggleEditing(props: ToolToggleEditingProps) {
save() save()
setEditing(EditingMode.VIEW) setEditing(EditingMode.VIEW)
}} }}
className={"positive"} className={mood.positive}
> >
<FestaIcon icon={faSave} /> <FestaIcon icon={faSave} />
</Tool> </Tool>

View file

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

View file

@ -1,8 +1,20 @@
.errorBlock > pre { .errorBlock > pre, .errorMain > pre {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
max-width: 100%; max-width: 100%;
text-align: left; 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 { FestaIcon } from "../renderers/fontawesome";
import { default as classNames } from "classnames" import { default as classNames } from "classnames"
import style from "./renderers.module.css" 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"; 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, 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) { * Component rendering the details of an {@link Error}.
if (props.error.response) { *
if (props.error.response.data) { * Not to use by itself; should be used to display the error in other error renderers.
const json = JSON.stringify(props.error.response.data, undefined, props.inline ? undefined : 4).replaceAll("\\n", "\n") */
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 ( return (
<code> <code {...props}>
<b>API {props.error.response.status}</b> <span>
:&nbsp; <b>API {error.response.status}</b>
{json} :&nbsp;
</span>
<span>
{json}
</span>
</code> </code>
) )
} }
return ( return (
<code> <code {...props}>
<b>HTTP {props.error.response.status}</b> <span>
:&nbsp; <b>HTTP {error.response.status}</b>
{props.error.message} :&nbsp;
</span>
<span>
{error.message}
</span>
</code> </code>
) )
} }
return ( return (
<code> <code {...props}>
<b>{props.error.code}</b> <span>
:&nbsp; <b>{error.code}</b>
{props.error.message} :&nbsp;
</span>
<span>
{error.message}
</span>
</code> </code>
) )
} }
return ( return (
<code> <code {...props}>
<b>{props.error.name}</b> <span>
:&nbsp; <b>{error.name}</b>
{props.error.message} :&nbsp;
</span>
<span>
{error.message}
</span>
</code> </code>
) )
}) })
ErrorTrace.displayName = "ErrorTrace" ErrorTrace.displayName = "ErrorTrace"
export type ErrorInlineProps = { /**
* Props for "error" renderers.
*/
export type ErrorProps = {
error: Error, error: Error,
text?: string 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 ( return (
<span className={classNames("negative", style.error, style.errorInline)}> <span className={classNames(mood.negative, style.error, style.errorInline)}>
<FestaIcon icon={faCircleExclamation} /> <FestaIcon icon={faCircleExclamation} className={style.errorIcon} />
&nbsp; {!!text && <>
{props.text ? &nbsp;
<> <span className={style.errorText}>
<span> {text}
{props.text} </span>
</span> &nbsp;
&nbsp; </>}
</> <ErrorTrace error={error} prettify={false} className={style.errorTrace} />
: null}
<ErrorTrace error={props.error} inline={true} />
</span> </span>
) )
}) })
ErrorInline.displayName = "ErrorInline" 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 ( return (
<div className={classNames("negative", style.error, style.errorBlock)}> <div className={classNames(mood.negative, style.error, style.errorBlock)}>
<p> <p>
<FestaIcon icon={faCircleExclamation} /> <FestaIcon icon={faCircleExclamation} className={style.errorIcon} />
&nbsp; &nbsp;
<span> <span className={style.errorText}>
{props.text} {text}
</span> </span>
</p> </p>
<pre> <pre>
<ErrorTrace error={props.error} inline={false} /> <ErrorTrace error={error} prettify={false} className={style.errorTrace} />
</pre> </pre>
</div> </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; display: grid;
flex-direction: column; flex-direction: column;

View file

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

View file

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

View file

@ -3,12 +3,13 @@ import '@fortawesome/fontawesome-svg-core/styles.css'
import { AppProps } from 'next/app' import { AppProps } from 'next/app'
import { appWithTranslation, useTranslation } from 'next-i18next' import { appWithTranslation, useTranslation } from 'next-i18next'
import { AxiosSWRFetcherProvider } from '../components/auth/requests' 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 { AuthContextProvider } from '../components/auth/provider'
import { PostcardRenderer } from '../components/postcard/renderer' import { PostcardRenderer } from '../components/postcard/renderer'
import { config as fontAwesomeConfig } from '@fortawesome/fontawesome-svg-core' import { config as fontAwesomeConfig } from '@fortawesome/fontawesome-svg-core'
import { PostcardContextProvider } from '../components/postcard/provider' import { PostcardContextProvider } from '../components/postcard/provider'
import defaultPostcard from "../public/postcards/adi-goldstein-Hli3R6LKibo-unsplash.jpg" import defaultPostcard from "../public/postcards/adi-goldstein-Hli3R6LKibo-unsplash.jpg"
import { LoadingBoundaryPage } from '../components/generic/loading/boundaries'
fontAwesomeConfig.autoAddCss = false fontAwesomeConfig.autoAddCss = false
@ -18,18 +19,22 @@ const App = ({ Component, pageProps }: AppProps): JSX.Element => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<PageErrorBoundary text={t("genericError")}> <ErrorBoundaryPage text={t("genericError")}>
<AxiosSWRFetcherProvider> <AxiosSWRFetcherProvider>
<PostcardContextProvider defaultPostcard={defaultPostcard}> <PostcardContextProvider defaultPostcard={defaultPostcard}>
<AuthContextProvider storageKey="auth"> <AuthContextProvider storageKey="auth">
<AxiosSWRFetcherProvider> <AxiosSWRFetcherProvider>
<PostcardRenderer /> <PostcardRenderer />
<Component {...pageProps} /> <ErrorBoundaryPage text={t("genericError")}>
<LoadingBoundaryPage>
<Component {...pageProps} />
</LoadingBoundaryPage>
</ErrorBoundaryPage>
</AxiosSWRFetcherProvider> </AxiosSWRFetcherProvider>
</AuthContextProvider> </AuthContextProvider>
</PostcardContextProvider> </PostcardContextProvider>
</AxiosSWRFetcherProvider> </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 { useAxios } from '../../components/auth/requests'
import { faAsterisk } from '@fortawesome/free-solid-svg-icons' import { faAsterisk } from '@fortawesome/free-solid-svg-icons'
import { FestaIcon } from '../../components/generic/renderers/fontawesome' 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 { 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) { export async function getServerSideProps(context: NextPageContext) {
@ -40,52 +44,75 @@ type PageEventProps = {
const PageEvent: NextPage<PageEventProps> = ({ slug }) => { const PageEvent: NextPage<PageEventProps> = ({ slug }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { data, isValidating, mutate } = useSWR<Event>(`/api/events/${slug}`) const swrHook = useSWR<Event>(`/api/events/${slug}`)
const [auth,] = useDefinedContext(AuthContext) const [auth,] = useDefinedContext(AuthContext)
const axios = useAxios() const axios = useAxios()
const save = useCallback( const save = useCallback(
async () => { async () => {
if (data === undefined) { if (swrHook.data === undefined) {
console.warn("[PageEvent] Tried to save while no data was available.") console.warn("[PageEvent] Tried to save while no data was available.")
return return
} }
await axios.patch(`/api/events/${slug}`, data) await axios.patch(`/api/events/${slug}`, swrHook.data)
mutate(data) swrHook.mutate(swrHook.data)
console.debug("[PageEvent] Saved updated data successfully!") console.debug("[PageEvent] Saved updated data successfully!")
}, },
[axios, data] [axios, swrHook]
) )
return <> return <>
<Head> <Head>
<title key="title">{data?.name ?? slug} - {t("siteTitle")}</title> <title key="title">{swrHook.data?.name ?? slug} - {t("siteTitle")}</title>
</Head> </Head>
<Postcard <Postcard
src={data?.postcard || defaultPostcard} src={swrHook.data?.postcard || defaultPostcard}
/> />
<WIPBanner /> <WIPBanner />
<EditingContextProvider> <EditingContextProvider>
<ViewContent {swrMultiplexer({
title={ hook: swrHook,
<EditableText loading: () => (
value={data?.name ?? slug} <ViewNotice
onChange={e => mutate(async state => state ? { ...state, name: e.target.value } : undefined, { revalidate: false })} notice={
placeholder={t("eventTitlePlaceholder")} <LoadingMain text={t("eventLoading")} />
}
/> />
} ),
content={ ready: (data) => (
<EditableMarkdown <ViewContent
value={data?.description ?? ""} title={
onChange={e => mutate(async state => state ? { ...state, description: e.target.value } : undefined, { revalidate: false })} <EditableText
placeholder={t("eventDescriptionPlaceholder")} 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"> <ToolBar vertical="vadapt" horizontal="right">
<ToolToggleVisibility /> <ToolToggleVisibility />
{data && auth?.userId === data?.creatorId && {swrHook.data && auth?.userId === swrHook.data?.creatorId &&
<ToolToggleEditing <ToolToggleEditing
save={save} save={save}
/> />

View file

@ -19,5 +19,9 @@
"toolToggleEditingSave": "Salva modifiche", "toolToggleEditingSave": "Salva modifiche",
"toolToggleEditingEdit": "Modifica", "toolToggleEditingEdit": "Modifica",
"eventNamePlaceholder": "Nome evento", "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 "color-schemes.css";
@import "elements.css"; @import "elements.css";
@import "mood.css";
@import "flex.css"; @import "flex.css";