- {error.status} {error.data} -
- ), - statusHandlers, - unexpectedErrorHandler = (error) =>{getErrorMessage(error)}
, -}: { - defaultStatusHandler?: StatusHandler - statusHandlers?: Record
- {location.pathname}
-
-
-
-
- No worries, we'll send you reset instructions. -
-- Please enter your details. -
-- Please enter your details. -
-- Please enter your details. -
-- Hi, {loaderData.resetPasswordUsername}. No worries. It happens all the - time. -
-
-
-
- Please enter your email. -
-- We've sent you a code to verify your email address. -
- > - ) - - const headings: Record- Please enter your 2FA code to verify your identity. -
- > - ), - } - - const [form, fields] = useForm({ - id: 'verify-form', - constraint: getZodConstraint(VerifySchema), - lastResult: actionData?.result, - onValidate({ formData }) { - return parseWithZod(formData, { schema: VerifySchema }) - }, - defaultValue: { - code: searchParams.get(codeQueryParam), - type: type, - target: searchParams.get(targetQueryParam), - redirectTo: searchParams.get(redirectToQueryParam), - }, - }) - - return ( -- Check the{' '} - - Getting Started guide - {' '} - file for how to get your project off the ground! -
-You are not allowed to do that: {error?.data.message}
- ), - }} - /> - ) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.lru.$cacheKey.ts deleted file mode 100644 index 7534705..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.lru.$cacheKey.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { invariantResponse } from '@epic-web/invariant' -import { lruCache } from '#app/utils/cache.server.ts' -import { - getAllInstances, - getInstanceInfo, - ensureInstance, -} from '#app/utils/litefs.server.ts' -import { requireUserWithRole } from '#app/utils/permissions.server.ts' -import { type Route } from './+types/cache_.lru.$cacheKey.ts' - -export async function loader({ request, params }: Route.LoaderArgs) { - await requireUserWithRole(request, 'admin') - const searchParams = new URL(request.url).searchParams - const currentInstanceInfo = await getInstanceInfo() - const allInstances = await getAllInstances() - const instance = - searchParams.get('instance') ?? currentInstanceInfo.currentInstance - await ensureInstance(instance) - - const { cacheKey } = params - invariantResponse(cacheKey, 'cacheKey is required') - return { - instance: { - hostname: instance, - region: allInstances[instance], - isPrimary: currentInstanceInfo.primaryInstance === instance, - }, - cacheKey, - value: lruCache.get(cacheKey), - } -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.$cacheKey.ts deleted file mode 100644 index 9aeb3e0..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.$cacheKey.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { invariantResponse } from '@epic-web/invariant' -import { cache } from '#app/utils/cache.server.ts' -import { - getAllInstances, - getInstanceInfo, - ensureInstance, -} from '#app/utils/litefs.server.ts' -import { requireUserWithRole } from '#app/utils/permissions.server.ts' -import { type Route } from './+types/cache_.sqlite.$cacheKey.ts' - -export async function loader({ request, params }: Route.LoaderArgs) { - await requireUserWithRole(request, 'admin') - const searchParams = new URL(request.url).searchParams - const currentInstanceInfo = await getInstanceInfo() - const allInstances = await getAllInstances() - const instance = - searchParams.get('instance') ?? currentInstanceInfo.currentInstance - await ensureInstance(instance) - - const { cacheKey } = params - invariantResponse(cacheKey, 'cacheKey is required') - return { - instance: { - hostname: instance, - region: allInstances[instance], - isPrimary: currentInstanceInfo.primaryInstance === instance, - }, - cacheKey, - value: cache.get(cacheKey), - } -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.server.ts b/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.server.ts deleted file mode 100644 index 04746fc..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { redirect } from 'react-router' -import { z } from 'zod' -import { cache } from '#app/utils/cache.server.ts' -import { - getInstanceInfo, - getInternalInstanceDomain, -} from '#app/utils/litefs.server' -import { type Route } from './+types/cache_.sqlite.ts' - -export async function updatePrimaryCacheValue({ - key, - cacheValue, -}: { - key: string - cacheValue: any -}) { - const { currentIsPrimary, primaryInstance } = await getInstanceInfo() - if (currentIsPrimary) { - throw new Error( - `updatePrimaryCacheValue should not be called on the primary instance (${primaryInstance})}`, - ) - } - const domain = getInternalInstanceDomain(primaryInstance) - const token = process.env.INTERNAL_COMMAND_TOKEN - return fetch(`${domain}/admin/cache/sqlite`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ key, cacheValue }), - }) -} - -export async function action({ request }: Route.ActionArgs) { - const { currentIsPrimary, primaryInstance } = await getInstanceInfo() - if (!currentIsPrimary) { - throw new Error( - `${request.url} should only be called on the primary instance (${primaryInstance})}`, - ) - } - const token = process.env.INTERNAL_COMMAND_TOKEN - const isAuthorized = - request.headers.get('Authorization') === `Bearer ${token}` - if (!isAuthorized) { - // nah, you can't be here... - return redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ') - } - const { key, cacheValue } = z - .object({ key: z.string(), cacheValue: z.unknown().optional() }) - .parse(await request.json()) - if (cacheValue === undefined) { - await cache.delete(key) - } else { - // @ts-expect-error - we don't reliably know the type of cacheValue - await cache.set(key, cacheValue) - } - return { success: true } -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.tsx deleted file mode 100644 index e469891..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/admin+/cache_.sqlite.tsx +++ /dev/null @@ -1 +0,0 @@ -export { action } from './cache_.sqlite.server.ts' diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/me.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/me.tsx deleted file mode 100644 index f7188b5..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/me.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { redirect } from 'react-router' -import { requireUserId, logout } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' -import { type Route } from './+types/me.ts' - -export async function loader({ request }: Route.LoaderArgs) { - const userId = await requireUserId(request) - const user = await prisma.user.findUnique({ where: { id: userId } }) - if (!user) { - const requestUrl = new URL(request.url) - const loginParams = new URLSearchParams([ - ['redirectTo', `${requestUrl.pathname}${requestUrl.search}`], - ]) - const redirectTo = `/login?${loginParams}` - await logout({ request, redirectTo }) - return redirect(redirectTo) - } - return redirect(`/users/${user.username}`) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/download-user-data.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/download-user-data.tsx deleted file mode 100644 index b9fa4d7..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/download-user-data.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { requireUserId } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' -import { getDomainUrl, getNoteImgSrc, getUserImgSrc } from '#app/utils/misc.tsx' -import { type Route } from './+types/download-user-data.ts' - -export async function loader({ request }: Route.LoaderArgs) { - const userId = await requireUserId(request) - const user = await prisma.user.findUniqueOrThrow({ - where: { id: userId }, - // this is one of the *few* instances where you can use "include" because - // the goal is to literally get *everything*. Normally you should be - // explicit with "select". We're using select for images because we don't - // want to send back the entire blob of the image. We'll send a URL they can - // use to download it instead. - include: { - image: { - select: { - id: true, - createdAt: true, - updatedAt: true, - objectKey: true, - }, - }, - notes: { - include: { - images: { - select: { - id: true, - createdAt: true, - updatedAt: true, - objectKey: true, - }, - }, - }, - }, - password: false, // <-- intentionally omit password - sessions: true, - roles: true, - }, - }) - - const domain = getDomainUrl(request) - - return Response.json({ - user: { - ...user, - image: user.image - ? { - ...user.image, - url: domain + getUserImgSrc(user.image.objectKey), - } - : null, - notes: user.notes.map((note) => ({ - ...note, - images: note.images.map((image) => ({ - ...image, - url: domain + getNoteImgSrc(image.objectKey), - })), - })), - }, - }) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/healthcheck.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/healthcheck.tsx deleted file mode 100644 index e38230e..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/healthcheck.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// learn more: https://fly.io/docs/reference/configuration/#services-http_checks -import { prisma } from '#app/utils/db.server.ts' -import { type Route } from './+types/healthcheck.ts' - -export async function loader({ request }: Route.LoaderArgs) { - const host = - request.headers.get('X-Forwarded-Host') ?? request.headers.get('host') - - try { - // if we can connect to the database and make a simple query - // and make a HEAD request to ourselves, then we're good. - await Promise.all([ - prisma.user.count(), - fetch(`${new URL(request.url).protocol}${host}`, { - method: 'HEAD', - headers: { 'X-Healthcheck': 'true' }, - }).then((r) => { - if (!r.ok) return Promise.reject(r) - }), - ]) - return new Response('OK') - } catch (error: unknown) { - console.log('healthcheck β', { error }) - return new Response('ERROR', { status: 500 }) - } -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/images.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/images.tsx deleted file mode 100644 index 766e925..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/images.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { promises as fs, constants } from 'node:fs' -import { invariantResponse } from '@epic-web/invariant' -import { getImgResponse } from 'openimg/node' -import { getDomainUrl } from '#app/utils/misc.tsx' -import { getSignedGetRequestInfo } from '#app/utils/storage.server.ts' -import { type Route } from './+types/images' - -let cacheDir: string | null = null - -async function getCacheDir() { - if (cacheDir) return cacheDir - - let dir = './tests/fixtures/openimg' - if (process.env.NODE_ENV === 'production') { - const isAccessible = await fs - .access('/data', constants.W_OK) - .then(() => true) - .catch(() => false) - - if (isAccessible) { - dir = '/data/images' - } - } - - return (cacheDir = dir) -} - -export async function loader({ request }: Route.LoaderArgs) { - const url = new URL(request.url) - const searchParams = url.searchParams - - const headers = new Headers() - headers.set('Cache-Control', 'public, max-age=31536000, immutable') - - const objectKey = searchParams.get('objectKey') - - return getImgResponse(request, { - headers, - allowlistedOrigins: [ - getDomainUrl(request), - process.env.AWS_ENDPOINT_URL_S3, - ].filter(Boolean), - cacheFolder: await getCacheDir(), - getImgSource: () => { - if (objectKey) { - const { url: signedUrl, headers: signedHeaders } = - getSignedGetRequestInfo(objectKey) - return { - type: 'fetch', - url: signedUrl, - headers: signedHeaders, - } - } - - const src = searchParams.get('src') - invariantResponse(src, 'src query parameter is required', { status: 400 }) - - if (URL.canParse(src)) { - // Fetch image from external URL; will be matched against allowlist - return { - type: 'fetch', - url: src, - } - } - // Retrieve image from filesystem (public folder) - if (src.startsWith('/assets')) { - // Files managed by Vite - return { - type: 'fs', - path: '.' + src, - } - } - // Fallback to files in public folder - return { - type: 'fs', - path: './public' + src, - } - }, - }) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/theme-switch.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/theme-switch.tsx deleted file mode 100644 index e466824..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/resources+/theme-switch.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useForm, getFormProps } from '@conform-to/react' -import { parseWithZod } from '@conform-to/zod' -import { invariantResponse } from '@epic-web/invariant' -import { data, redirect, useFetcher, useFetchers } from 'react-router' -import { ServerOnly } from 'remix-utils/server-only' -import { z } from 'zod' -import { Icon } from '#app/components/ui/icon.tsx' -import { useHints, useOptionalHints } from '#app/utils/client-hints.tsx' -import { - useOptionalRequestInfo, - useRequestInfo, -} from '#app/utils/request-info.ts' -import { type Theme, setTheme } from '#app/utils/theme.server.ts' -import { type Route } from './+types/theme-switch.ts' -const ThemeFormSchema = z.object({ - theme: z.enum(['system', 'light', 'dark']), - // this is useful for progressive enhancement - redirectTo: z.string().optional(), -}) - -export async function action({ request }: Route.ActionArgs) { - const formData = await request.formData() - const submission = parseWithZod(formData, { - schema: ThemeFormSchema, - }) - - invariantResponse(submission.status === 'success', 'Invalid theme received') - - const { theme, redirectTo } = submission.value - - const responseInit = { - headers: { 'set-cookie': setTheme(theme) }, - } - if (redirectTo) { - return redirect(redirectTo, responseInit) - } else { - return data({ result: submission.reply() }, responseInit) - } -} - -export function ThemeSwitch({ - userPreference, -}: { - userPreference?: Theme | null -}) { - const fetcher = useFetcher
-
-
-
-
-
You will receive an email at the new email address to confirm.
-- An email notice will also be sent to your old address{' '} - {loaderData.user.email}. -
-Here are your current connections:
-You don't have any connections yet.
- )} -- Disabling two factor authentication is not recommended. However, if - you would like to do so, click here: -
-
-
-
- Two factor authentication adds an extra layer of security to your - account. You will need to enter a code from an authenticator app - like{' '} - - 1Password - {' '} - to log in. -
-Scan this QR code with your authenticator app.
-- If you cannot scan the QR code, you can manually add this account to - your authenticator app using this code: -
-
- {loaderData.otpUri}
-
- - Once you've added the account, enter the code from your authenticator - app below. Once you enable 2FA, you will need to enter a code from - your authenticator app every time you log in or perform important - actions. Do not lose access to your authenticator app, or you will - lose access to your account. -
-- Joined {data.userJoinedDisplay} -
- {isLoggedInUser ? ( - - ) : null} -No user with the username "{params.username}" exists
- ), - }} - /> - ) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/__note-editor.server.tsx deleted file mode 100644 index 4ac8a58..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/__note-editor.server.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { parseWithZod } from '@conform-to/zod' -import { parseFormData } from '@mjackson/form-data-parser' -import { createId as cuid } from '@paralleldrive/cuid2' -import { data, redirect, type ActionFunctionArgs } from 'react-router' -import { z } from 'zod' -import { requireUserId } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' -import { uploadNoteImage } from '#app/utils/storage.server.ts' -import { - MAX_UPLOAD_SIZE, - NoteEditorSchema, - type ImageFieldset, -} from './__note-editor' - -function imageHasFile( - image: ImageFieldset, -): image is ImageFieldset & { file: NonNullableNo note with the id "{params.noteId}" exists
- ), - }} - /> - ) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.$noteId.tsx deleted file mode 100644 index db9849e..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.$noteId.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { getFormProps, useForm } from '@conform-to/react' -import { parseWithZod } from '@conform-to/zod' -import { invariantResponse } from '@epic-web/invariant' -import { formatDistanceToNow } from 'date-fns' -import { Img } from 'openimg/react' -import { useRef, useEffect } from 'react' -import { data, Form, Link } from 'react-router' -import { z } from 'zod' -import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' -import { floatingToolbarClassName } from '#app/components/floating-toolbar.tsx' -import { ErrorList } from '#app/components/forms.tsx' -import { Button } from '#app/components/ui/button.tsx' -import { Icon } from '#app/components/ui/icon.tsx' -import { StatusButton } from '#app/components/ui/status-button.tsx' -import { requireUserId } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' -import { getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx' -import { requireUserWithPermission } from '#app/utils/permissions.server.ts' -import { redirectWithToast } from '#app/utils/toast.server.ts' -import { userHasPermission, useOptionalUser } from '#app/utils/user.ts' -import { type Route } from './+types/notes.$noteId.ts' -import { type Route as NotesRoute } from './+types/notes.ts' - -export async function loader({ params }: Route.LoaderArgs) { - const note = await prisma.note.findUnique({ - where: { id: params.noteId }, - select: { - id: true, - title: true, - content: true, - ownerId: true, - updatedAt: true, - images: { - select: { - altText: true, - objectKey: true, - }, - }, - }, - }) - - invariantResponse(note, 'Not found', { status: 404 }) - - const date = new Date(note.updatedAt) - const timeAgo = formatDistanceToNow(date) - - return { note, timeAgo } -} - -const DeleteFormSchema = z.object({ - intent: z.literal('delete-note'), - noteId: z.string(), -}) - -export async function action({ request }: Route.ActionArgs) { - const userId = await requireUserId(request) - const formData = await request.formData() - const submission = parseWithZod(formData, { - schema: DeleteFormSchema, - }) - if (submission.status !== 'success') { - return data( - { result: submission.reply() }, - { status: submission.status === 'error' ? 400 : 200 }, - ) - } - - const { noteId } = submission.value - - const note = await prisma.note.findFirst({ - select: { id: true, ownerId: true, owner: { select: { username: true } } }, - where: { id: noteId }, - }) - invariantResponse(note, 'Not found', { status: 404 }) - - const isOwner = note.ownerId === userId - await requireUserWithPermission( - request, - isOwner ? `delete:note:own` : `delete:note:any`, - ) - - await prisma.note.delete({ where: { id: note.id } }) - - return redirectWithToast(`/users/${note.owner.username}/notes`, { - type: 'success', - title: 'Success', - description: 'Your note has been deleted.', - }) -} - -export default function NoteRoute({ - loaderData, - actionData, -}: Route.ComponentProps) { - const user = useOptionalUser() - const isOwner = user?.id === loaderData.note.ownerId - const canDelete = userHasPermission( - user, - isOwner ? `delete:note:own` : `delete:note:any`, - ) - const displayBar = canDelete || isOwner - - // Add ref for auto-focusing - const sectionRef = useRef- {loaderData.note.content} -
-You are not allowed to do that
, - 404: ({ params }) => ( -No note with the id "{params.noteId}" exists
- ), - }} - /> - ) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.$noteId_.edit.tsx deleted file mode 100644 index 6e8bddf..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.$noteId_.edit.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { invariantResponse } from '@epic-web/invariant' -import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' -import { requireUserId } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' -import { type Route } from './+types/notes.$noteId_.edit.ts' -import { NoteEditor } from './__note-editor.tsx' - -export { action } from './__note-editor.server.tsx' - -export async function loader({ params, request }: Route.LoaderArgs) { - const userId = await requireUserId(request) - const note = await prisma.note.findFirst({ - select: { - id: true, - title: true, - content: true, - images: { - select: { - id: true, - altText: true, - objectKey: true, - }, - }, - }, - where: { - id: params.noteId, - ownerId: userId, - }, - }) - invariantResponse(note, 'Not found', { status: 404 }) - return { note } -} - -export default function NoteEdit({ - loaderData, - actionData, -}: Route.ComponentProps) { - returnNo note with the id "{params.noteId}" exists
- ), - }} - /> - ) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.index.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.index.tsx deleted file mode 100644 index 9457224..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/$username_+/notes.index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { type Route } from './+types/notes.index.ts' -import { type Route as NotesRoute } from './+types/notes.ts' - -export default function NotesIndexRoute() { - return ( -Select a note
-No user with the username "{params.username}" exists
- ), - }} - /> - ) -} diff --git a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/index.tsx b/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/index.tsx deleted file mode 100644 index 038dadb..0000000 --- a/exercises/04.debugging/03.solution.live-debugging/app/routes/users+/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { searchUsers } from '@prisma/client/sql' -import { Img } from 'openimg/react' -import { redirect, Link } from 'react-router' -import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' -import { ErrorList } from '#app/components/forms.tsx' -import { SearchBar } from '#app/components/search-bar.tsx' -import { prisma } from '#app/utils/db.server.ts' -import { cn, getUserImgSrc, useDelayedIsPending } from '#app/utils/misc.tsx' -import { type Route } from './+types/index.ts' - -export async function loader({ request }: Route.LoaderArgs) { - const searchTerm = new URL(request.url).searchParams.get('search') - if (searchTerm === '') { - return redirect('/users') - } - - const like = `%${searchTerm ?? ''}%` - const users = await prisma.$queryRawTyped(searchUsers(like)) - return { status: 'idle', users } as const -} - -export default function UsersRoute({ loaderData }: Route.ComponentProps) { - const isPending = useDelayedIsPending({ - formMethod: 'GET', - formAction: '/users', - }) - - return ( -No users found
- ) - ) : loaderData.status === 'error' ? ( -