diff --git a/app/routes/_app+/recipients+/$recipientId.past.tsx b/app/routes/_app+/recipients+/$recipientId.past.tsx index 34865cae..c56d326c 100644 --- a/app/routes/_app+/recipients+/$recipientId.past.tsx +++ b/app/routes/_app+/recipients+/$recipientId.past.tsx @@ -4,48 +4,101 @@ import { json, type LoaderFunctionArgs, } from '@remix-run/node' -import { useLoaderData } from '@remix-run/react' +import { Form, Link, useLoaderData, useSearchParams } from '@remix-run/react' +import { useId } from 'react' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' +import { Button } from '#app/components/ui/button.tsx' +import { Icon } from '#app/components/ui/icon.tsx' +import { Input } from '#app/components/ui/input.tsx' +import { Label } from '#app/components/ui/label.tsx' import { requireUserId } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' +const PAGE_SIZE = 100 + export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request) + const recipientId = params.recipientId + invariantResponse(recipientId, 'Recipient id is required', { status: 400 }) + const url = new URL(request.url) + const searchTerm = url.searchParams.get('search')?.trim() ?? '' + const pageParam = url.searchParams.get('page') ?? '1' + const parsedPage = Number.parseInt(pageParam, 10) + const requestedPage = Number.isNaN(parsedPage) ? 1 : parsedPage + const page = Math.max(1, requestedPage) const recipient = await prisma.recipient.findUnique({ - where: { id: params.recipientId, userId }, + where: { id: recipientId, userId }, select: { name: true, phoneNumber: true, - messages: { - select: { id: true, content: true, sentAt: true, order: true }, - orderBy: { order: 'asc' }, - where: { sentAt: { not: null } }, - }, }, }) invariantResponse(recipient, 'Not found', { status: 404 }) - const { messages, ...recipientProps } = recipient + const baseMessageWhere = { + recipientId, + sentAt: { not: null }, + } + const messageWhere = searchTerm + ? { + ...baseMessageWhere, + content: { contains: searchTerm }, + } + : baseMessageWhere + + const totalMessageCountPromise = prisma.message.count({ + where: baseMessageWhere, + }) + const filteredMessageCountPromise = searchTerm + ? prisma.message.count({ where: messageWhere }) + : totalMessageCountPromise + + const [totalMessageCount, filteredMessageCount] = await Promise.all([ + totalMessageCountPromise, + filteredMessageCountPromise, + ]) + + const totalPages = Math.max(1, Math.ceil(filteredMessageCount / PAGE_SIZE)) + const currentPage = Math.min(page, totalPages) + const pastMessages = await prisma.message.findMany({ + where: messageWhere, + select: { id: true, content: true, sentAt: true }, + orderBy: [{ sentAt: 'desc' }, { order: 'desc' }], + take: PAGE_SIZE, + skip: (currentPage - 1) * PAGE_SIZE, + }) + + const rangeStart = + filteredMessageCount === 0 ? 0 : (currentPage - 1) * PAGE_SIZE + 1 + const rangeEnd = Math.min(currentPage * PAGE_SIZE, filteredMessageCount) return json({ - recipient: recipientProps, - messageCountDisplay: messages.length.toLocaleString(), - pastMessages: messages - .filter((m) => m.sentAt) - .sort((m1, m2) => m2.sentAt!.getTime() - m1.sentAt!.getTime()) - .map((m) => ({ - id: m.id, - sentAtDisplay: m.sentAt!.toLocaleDateString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - }), - content: m.content, - })), + recipient, + messageCount: totalMessageCount, + messageCountDisplay: totalMessageCount.toLocaleString(), + filteredMessageCount, + filteredMessageCountDisplay: filteredMessageCount.toLocaleString(), + searchTerm, + pageInfo: { + currentPage, + totalPages, + rangeStart, + rangeEnd, + pageSize: PAGE_SIZE, + }, + pastMessages: pastMessages.map((message) => ({ + id: message.id, + sentAtDisplay: message.sentAt!.toLocaleDateString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }), + content: message.content, + })), }) } @@ -59,27 +112,141 @@ export const meta: MetaFunction = ({ data }) => { export default function RecipientRoute() { const data = useLoaderData() + const [searchParams] = useSearchParams() + const searchInputId = useId() + const hasSearch = data.searchTerm.length > 0 + const hasMessages = data.filteredMessageCount > 0 + const messageResultLabel = hasSearch + ? data.filteredMessageCount === 1 + ? 'matching message' + : 'matching messages' + : data.filteredMessageCount === 1 + ? 'message' + : 'messages' + + const buildPageLink = (page: number) => { + const params = new URLSearchParams(searchParams) + if (page <= 1) { + params.delete('page') + } else { + params.set('page', String(page)) + } + const query = params.toString() + return query ? `?${query}` : '.' + } + + const clearSearchParams = new URLSearchParams(searchParams) + clearSearchParams.delete('search') + clearSearchParams.delete('page') + const clearSearchLink = clearSearchParams.toString() + ? `?${clearSearchParams.toString()}` + : '.' return ( -
-

+

+

You have sent {data.messageCountDisplay}{' '} - {data.pastMessages.length === 1 ? 'message' : 'messages'} to{' '} + {data.messageCount === 1 ? 'message' : 'messages'} to{' '} {data.recipient.name}.

-
    - {data.pastMessages.map((m) => ( -
  • - - {m.sentAtDisplay} - - {m.content} -
  • - ))} -
+
+
+
+ + +
+ + + {hasSearch ? ( + + ) : null} +
+
+ {hasMessages ? ( +

+ Showing {data.pageInfo.rangeStart}- + {data.pageInfo.rangeEnd} of{' '} + {data.filteredMessageCountDisplay} {messageResultLabel} +

+ ) : ( +

+ {hasSearch + ? 'No messages match this search.' + : 'No messages have been sent yet.'} +

+ )} + {data.pageInfo.totalPages > 1 ? ( +

+ Page {data.pageInfo.currentPage} of{' '} + {data.pageInfo.totalPages} +

+ ) : null} +
+
+ {hasMessages ? ( +
    + {data.pastMessages.map((message) => ( +
  • + + {message.sentAtDisplay} + + {message.content} +
  • + ))} +
+ ) : null} + {data.pageInfo.totalPages > 1 ? ( +
+ {data.pageInfo.currentPage > 1 ? ( + + ) : ( + + )} + {data.pageInfo.currentPage < data.pageInfo.totalPages ? ( + + ) : ( + + )} +
+ ) : null}
) }