Skip to content

Commit

Permalink
move routes a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Jun 1, 2024
1 parent feaf2e9 commit 8fdfe63
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 308 deletions.
11 changes: 10 additions & 1 deletion app/components/ui/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,25 @@ export function Icon({
size = 'font',
className,
children,
title,
...props
}: SVGProps<SVGSVGElement> & {
name: IconName
title?: string
size?: Size
}) {
if (children) {
return (
<span
className={`inline-flex items-center ${childrenSizeClassName[size]}`}
>
<Icon name={name} size={size} className={className} {...props} />
<Icon
name={name}
size={size}
className={className}
title={title}
{...props}
/>
{children}
</span>
)
Expand All @@ -59,6 +67,7 @@ export function Icon({
{...props}
className={cn(sizeClassName[size], 'inline self-center', className)}
>
{title ? <title>{title}</title> : null}
<use href={`${href}#${name}`} />
</svg>
)
Expand Down
2 changes: 1 addition & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ function UserDropdown() {
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link prefetch="intent" to={`/users/${user.username}/recipients`}>
<Link prefetch="intent" to={`/recipients`}>
<Icon className="text-body-md" name="pencil-2">
Recipients
</Icon>
Expand Down
83 changes: 83 additions & 0 deletions app/routes/recipients+/$recipientId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { invariantResponse } from '@epic-web/invariant'
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { Link, useLoaderData } from '@remix-run/react'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { floatingToolbarClassName } from '#app/components/floating-toolbar.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { requireUserId } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { useOptionalUser } from '#app/utils/user.ts'

export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request)
const recipient = await prisma.recipient.findUnique({
where: { id: params.recipientId, userId },
select: {
id: true,
name: true,
phoneNumber: true,
userId: true,
messages: {
select: { id: true, content: true },
orderBy: { order: 'asc' },
where: { sentAt: undefined },
},
},
})

invariantResponse(recipient, 'Not found', { status: 404 })

return json({ recipient })
}

export default function RecipientRoute() {
const data = useLoaderData<typeof loader>()
const user = useOptionalUser()
const isOwner = user?.id === data.recipient.userId

return (
<div className="absolute inset-0 flex flex-col px-10">
<h2 className="mb-2 pt-12 text-h2 lg:mb-6">
{data.recipient.name}
<small className="block text-sm text-secondary-foreground">
{data.recipient.phoneNumber}
</small>
</h2>
<div className={`${isOwner ? 'pb-24' : 'pb-12'} overflow-y-auto`}>
{/* <p className="whitespace-break-spaces text-sm md:text-lg">
{data.recipient.messages.length}
</p> */}
</div>
{isOwner ? (
<div className={floatingToolbarClassName}>
<div className="grid flex-1 grid-cols-2 justify-end gap-2 min-[525px]:flex md:gap-4">
<Button
asChild
className="min-[525px]:max-md:aspect-square min-[525px]:max-md:px-0"
>
<Link to="edit">
<Icon name="pencil-1" className="scale-125 max-md:scale-150">
<span className="max-md:hidden">Edit</span>
</Icon>
</Link>
</Button>
</div>
</div>
) : null}
</div>
)
}

export function ErrorBoundary() {
return (
<GeneralErrorBoundary
statusHandlers={{
403: () => <p>You are not allowed to do that</p>,
404: ({ params }) => (
<p>No recipient with the id "{params.recipientId}" exists</p>
),
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { useLoaderData } from '@remix-run/react'
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 { RecipientEditor } from './__recipient-editor.tsx'
import { RecipientEditor } from './__editor.tsx'

export { action } from './__recipient-editor.server.tsx'
export { action } from './__editor.server.tsx'

export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request)
Expand Down Expand Up @@ -37,7 +37,7 @@ export function ErrorBoundary() {
<GeneralErrorBoundary
statusHandlers={{
404: ({ params }) => (
<p>No note with the id "{params.noteId}" exists</p>
<p>No note with the id "{params.recipientId}" exists</p>
),
}}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { parseWithZod } from '@conform-to/zod'
import { invariantResponse } from '@epic-web/invariant'
import { json, redirect, type ActionFunctionArgs } from '@remix-run/node'
import { z } from 'zod'
import { requireUserId } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { RecipientEditorSchema } from './__recipient-editor.tsx'
import { redirectWithToast } from '#app/utils/toast.server.js'
import { DeleteRecipientSchema, RecipientEditorSchema } from './__editor.tsx'

export async function action({ request }: ActionFunctionArgs) {
const userId = await requireUserId(request)

const formData = await request.formData()

if (formData.get('intent') === 'delete-recipient') {
return await deleteRecipient({ formData, userId })
}

const submission = await parseWithZod(formData, {
schema: RecipientEditorSchema.superRefine(async (data, ctx) => {
if (!data.id) return
Expand Down Expand Up @@ -54,7 +60,40 @@ export async function action({ request }: ActionFunctionArgs) {
},
})

return redirect(
`/users/${updatedRecipient.user.username}/recipients/${updatedRecipient.id}`,
)
return redirect(`/recipients/${updatedRecipient.id}`)
}

async function deleteRecipient({
formData,
userId,
}: {
formData: FormData
userId: string
}) {
const submission = parseWithZod(formData, {
schema: DeleteRecipientSchema,
})
if (submission.status !== 'success') {
return json(
{ result: submission.reply() },
{ status: submission.status === 'error' ? 400 : 200 },
)
}

const { recipientId } = submission.value

const recipient = await prisma.recipient.findFirst({
select: { id: true, userId: true, user: { select: { username: true } } },
where: { id: recipientId, userId },
})

invariantResponse(recipient, 'Not found', { status: 404 })

await prisma.recipient.delete({ where: { id: recipient.id } })

return redirectWithToast(`/recipients`, {
type: 'success',
title: 'Success',
description: 'Your recipient has been deleted.',
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { floatingToolbarClassName } from '#app/components/floating-toolbar.tsx'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.js'
import { StatusButton } from '#app/components/ui/status-button.tsx'
import { useIsPending } from '#app/utils/misc.tsx'
import { type action } from './__recipient-editor.server.tsx'
import { useDoubleCheck, useIsPending } from '#app/utils/misc.tsx'
import { type action } from './__editor.server.tsx'

export const RecipientEditorSchema = z.object({
id: z.string().optional(),
Expand All @@ -24,6 +24,11 @@ export const RecipientEditorSchema = z.object({
scheduleCron: z.string(),
})

export const DeleteRecipientSchema = z.object({
intent: z.literal('delete-recipient'),
recipientId: z.string(),
})

export function RecipientEditor({
recipient,
}: {
Expand Down Expand Up @@ -91,24 +96,65 @@ export function RecipientEditor({
</div>
<ErrorList id={form.errorId} errors={form.errors} />
</Form>
<div className={floatingToolbarClassName}>
<Button variant="destructive" {...form.reset.getButtonProps()}>
Reset
</Button>
<StatusButton
form={form.id}
type="submit"
disabled={isPending}
status={isPending ? 'pending' : 'idle'}
>
Submit
</StatusButton>
</div>
</FormProvider>
<div className={floatingToolbarClassName}>
{recipient?.id ? <DeleteRecipient id={recipient.id} /> : null}
<StatusButton
form={form.id}
type="submit"
disabled={isPending}
status={isPending ? 'pending' : 'idle'}
>
Submit
</StatusButton>
</div>
</div>
)
}

function DeleteRecipient({ id }: { id: string }) {
const actionData = useActionData<typeof action>()
const isPending = useIsPending()
const dc = useDoubleCheck({ safeDelayMs: 300 })
const [form] = useForm({
id: 'delete-recipient',
lastResult: actionData?.result,
})

return (
<Form method="POST" {...getFormProps(form)}>
<input type="hidden" name="recipientId" value={id} />
<StatusButton
variant="destructive"
status={isPending ? 'pending' : form.status ?? 'idle'}
{...dc.getButtonProps({
type: 'submit',
title: dc.doubleCheck ? 'Are you sure?' : 'Delete recipient',
name: 'intent',
value: 'delete-recipient',
disabled: isPending,
className:
'w-full max-md:aspect-square max-md:px-0 data-[safe-delay=true]:opacity-50',
})}
>
{dc.doubleCheck ? (
<Icon
name="question-mark-circled"
className="scale-125 max-md:scale-150"
>
<span className="max-md:hidden">Confirm</span>
</Icon>
) : (
<Icon name="trash" className="scale-125 max-md:scale-150">
<span className="max-md:hidden">Delete</span>
</Icon>
)}
</StatusButton>
<ErrorList errors={form.errors} id={form.errorId} />
</Form>
)
}

export function ErrorBoundary() {
return (
<GeneralErrorBoundary
Expand Down
Loading

0 comments on commit 8fdfe63

Please sign in to comment.