Skip to content

Instantly share code, notes, and snippets.

@iserdmi
Created February 7, 2023 10:08
Show Gist options
  • Save iserdmi/f109d59170decf13525eebc945ea75f9 to your computer and use it in GitHub Desktop.
Save iserdmi/f109d59170decf13525eebc945ea75f9 to your computer and use it in GitHub Desktop.
withPageWrapper
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>
)
})
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