Created
February 7, 2023 10:08
-
-
Save iserdmi/f109d59170decf13525eebc945ea75f9 to your computer and use it in GitHub Desktop.
withPageWrapper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { zUpdateIdeaInput } from '@ideanick/backend/src/router/ideas/updateIdea/input' | |
import { canEditIdea } from '@ideanick/backend/src/utils/can' | |
import { pick } from '@ideanick/shared/src/pick' | |
import { useNavigate, useParams } from 'react-router-dom' | |
import { Alert } from '../../../components/Alert' | |
import { Button } from '../../../components/Button' | |
import { FormItems } from '../../../components/FormItems' | |
import { Input } from '../../../components/Input' | |
import { Segment } from '../../../components/Segment' | |
import { Textarea } from '../../../components/Textarea' | |
import { useForm } from '../../../lib/form' | |
import { withPageWrapper } from '../../../lib/pageWrapper' | |
import { EditIdeaRouteParams, getViewIdeaRoute } from '../../../lib/routes' | |
import { trpc } from '../../../lib/trpc' | |
export const EditIdeaPage = withPageWrapper({ | |
title: ({ queryResult }) => `Edit Idea "${queryResult.data.idea?.name}"`, | |
authorizedOnly: true, | |
useQuery: () => { | |
const { ideaNick } = useParams() as EditIdeaRouteParams | |
return trpc.getIdea.useQuery({ | |
ideaNick, | |
}) | |
}, | |
checkAccess: ({ queryResult, ctx }) => canEditIdea(ctx.me, queryResult.data.idea), | |
checkAccessMessage: 'An idea can only be edited by the author', | |
setProps: ({ queryResult, checkExists }) => ({ | |
idea: checkExists(queryResult.data.idea, 'Idea not found'), | |
}), | |
})(({ idea }) => { | |
const navigate = useNavigate() | |
const updateIdea = trpc.updateIdea.useMutation() | |
const { formik, alertProps, buttonProps } = useForm({ | |
initialValues: pick(idea, ['name', 'nick', 'description', 'text']), | |
validationSchema: zUpdateIdeaInput.omit({ ideaId: true }), | |
onSubmit: async (values) => { | |
await updateIdea.mutateAsync({ ideaId: idea.id, ...values }) | |
navigate(getViewIdeaRoute({ ideaNick: values.nick })) | |
}, | |
showValidationAlert: true, | |
}) | |
return ( | |
<Segment title={`Edit Idea: ${idea.nick}`}> | |
<form onSubmit={formik.handleSubmit}> | |
<FormItems> | |
<Input label="Name" name="name" formik={formik} /> | |
<Input label="Nick" name="nick" formik={formik} /> | |
<Input label="Description" name="description" maxWidth={500} formik={formik} /> | |
<Textarea label="Text" name="text" formik={formik} /> | |
<Alert {...alertProps} /> | |
<Button {...buttonProps}>Update Idea</Button> | |
</FormItems> | |
</form> | |
</Segment> | |
) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useStore } from '@nanostores/react' | |
import { UseTRPCQueryResult, UseTRPCQuerySuccessResult } from '@trpc/react-query/dist/shared' | |
import React, { useEffect } from 'react' | |
import { Helmet } from 'react-helmet-async' | |
import { useNavigate } from 'react-router-dom' | |
import { ErrorPageComponent } from '../components/ErrorPageComponent' | |
import { Loader } from '../components/Loader' | |
import { lastVisistedNotAuthRouteStore } from '../components/NotAuthRouteTracker' | |
import { NotFoundPage } from '../pages/other/NotFoundPage' | |
import { AppContext, useAppContext } from './ctx' | |
class CheckExistsError extends Error {} | |
const checkExistsFn = <T,>(value: T, message?: string): NonNullable<T> => { | |
if (!value) { | |
throw new CheckExistsError(message) | |
} | |
return value | |
} | |
class GetAuthorizedMeError extends Error {} | |
type Props = Record<string, any> | |
type QueryResult = UseTRPCQueryResult<any, any> | |
type QuerySuccessResult<TQueryResult extends QueryResult> = UseTRPCQuerySuccessResult< | |
NonNullable<TQueryResult['data']>, | |
null | |
> | |
type HelperProps<TQueryResult extends QueryResult | undefined> = { | |
ctx: AppContext | |
queryResult: TQueryResult extends QueryResult ? QuerySuccessResult<TQueryResult> : undefined | |
} | |
type SetPropsProps<TQueryResult extends QueryResult | undefined> = HelperProps<TQueryResult> & { | |
checkExists: typeof checkExistsFn | |
getAuthorizedMe: (message?: string) => NonNullable<AppContext['me']> | |
} | |
type PageWrapperProps<TProps extends Props, TQueryResult extends QueryResult | undefined> = { | |
redirectAuthorized?: boolean | |
authorizedOnly?: boolean | |
authorizedOnlyTitle?: string | |
authorizedOnlyMessage?: string | |
checkAccess?: (helperProps: HelperProps<TQueryResult>) => boolean | |
checkAccessTitle?: string | |
checkAccessMessage?: string | |
checkExists?: (helperProps: HelperProps<TQueryResult>) => boolean | |
checkExistsTitle?: string | |
checkExistsMessage?: string | |
showLoaderOnFetching?: boolean | |
title: string | ((helperProps: HelperProps<TQueryResult>) => undefined | string) | |
isTitleExact?: boolean | |
useQuery?: () => TQueryResult | |
setProps?: (setPropsProps: SetPropsProps<TQueryResult>) => TProps | |
Page: React.FC<TProps> | |
} | |
const PageWrapper = <TProps extends Props = {}, TQueryResult extends QueryResult | undefined = undefined>({ | |
authorizedOnly, | |
authorizedOnlyTitle = 'Please, Authorize', | |
authorizedOnlyMessage = 'This page is available only for authorized users', | |
redirectAuthorized, | |
checkAccess, | |
checkAccessTitle = 'Access Denied', | |
checkAccessMessage = 'You have no access to this page', | |
checkExists, | |
checkExistsTitle, | |
checkExistsMessage, | |
title, | |
isTitleExact = false, | |
useQuery, | |
setProps, | |
Page, | |
showLoaderOnFetching = true, | |
}: PageWrapperProps<TProps, TQueryResult>) => { | |
const lastVisistedNotAuthRoute = useStore(lastVisistedNotAuthRouteStore) | |
const navigate = useNavigate() | |
const ctx = useAppContext() | |
const queryResult = useQuery?.() | |
const redirectNeeded = redirectAuthorized && ctx.me | |
useEffect(() => { | |
if (redirectNeeded) { | |
navigate(lastVisistedNotAuthRoute, { replace: true }) | |
} | |
}, [redirectNeeded, navigate, lastVisistedNotAuthRoute]) | |
if (queryResult?.isLoading || (showLoaderOnFetching && queryResult?.isFetching) || redirectNeeded) { | |
return <Loader type="page" /> | |
} | |
if (queryResult?.isError) { | |
return <ErrorPageComponent message={queryResult.error.message} /> | |
} | |
if (authorizedOnly && !ctx.me) { | |
return <ErrorPageComponent title={authorizedOnlyTitle} message={authorizedOnlyMessage} /> | |
} | |
const helperProps = { ctx, queryResult: queryResult as never } | |
if (checkAccess) { | |
const accessDenied = !checkAccess(helperProps) | |
if (accessDenied) { | |
return <ErrorPageComponent title={checkAccessTitle} message={checkAccessMessage} /> | |
} | |
} | |
if (checkExists) { | |
const notExists = !checkExists(helperProps) | |
if (notExists) { | |
return <NotFoundPage title={checkExistsTitle} message={checkExistsMessage} /> | |
} | |
} | |
const getAuthorizedMe = (message?: string) => { | |
if (!ctx.me) { | |
throw new GetAuthorizedMeError(message) | |
} | |
return ctx.me | |
} | |
const calculatedTitle = typeof title === 'function' ? title(helperProps) : title | |
const exactTitle = !calculatedTitle ? undefined : isTitleExact ? calculatedTitle : `${calculatedTitle} — IdeaNick` | |
try { | |
const props = setProps?.({ ...helperProps, checkExists: checkExistsFn, getAuthorizedMe }) as TProps | |
return ( | |
<> | |
{exactTitle && ( | |
<Helmet> | |
<title>{exactTitle}</title> | |
</Helmet> | |
)} | |
<Page {...props} /> | |
</> | |
) | |
} catch (error) { | |
if (error instanceof CheckExistsError) { | |
return <NotFoundPage title={checkExistsTitle} message={error.message || checkExistsMessage} /> | |
} | |
if (error instanceof GetAuthorizedMeError) { | |
return <ErrorPageComponent title={authorizedOnlyTitle} message={error.message || authorizedOnlyMessage} /> | |
} | |
throw error | |
} | |
} | |
export const withPageWrapper = <TProps extends Props = {}, TQueryResult extends QueryResult | undefined = undefined>( | |
pageWrapperProps: Omit<PageWrapperProps<TProps, TQueryResult>, 'Page'> | |
) => { | |
return (Page: PageWrapperProps<TProps, TQueryResult>['Page']) => { | |
return () => <PageWrapper {...pageWrapperProps} Page={Page} /> | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment