diff --git a/app/components/search-bar.tsx b/app/components/search-bar.tsx index 30c77d39..d3478e66 100644 --- a/app/components/search-bar.tsx +++ b/app/components/search-bar.tsx @@ -10,17 +10,19 @@ export function SearchBar({ status, autoFocus = false, autoSubmit = false, + action, }: { status: 'idle' | 'pending' | 'success' | 'error' autoFocus?: boolean autoSubmit?: boolean + action?: string }) { const id = useId() const [searchParams] = useSearchParams() const submit = useSubmit() const isSubmitting = useIsPending({ formMethod: 'GET', - formAction: '/users', + formAction: action, }) const handleFormChange = useDebounce((form: HTMLFormElement) => { @@ -30,7 +32,7 @@ export function SearchBar({ return (
autoSubmit && handleFormChange(e.currentTarget)} > diff --git a/app/routes/_app+/recipients+/$recipientId.past.tsx b/app/routes/_app+/recipients+/$recipientId.past.tsx index 34865cae..3c93e8c7 100644 --- a/app/routes/_app+/recipients+/$recipientId.past.tsx +++ b/app/routes/_app+/recipients+/$recipientId.past.tsx @@ -4,48 +4,79 @@ import { json, type LoaderFunctionArgs, } from '@remix-run/node' -import { useLoaderData } from '@remix-run/react' +import { Link, useLoaderData, useSearchParams } from '@remix-run/react' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' +import { SearchBar } from '#app/components/search-bar.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 { cn, useDelayedIsPending } from '#app/utils/misc.tsx' + +const MESSAGES_PER_PAGE = 100 export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request) + const url = new URL(request.url) + const searchQuery = url.searchParams.get('search') ?? '' + const page = Math.max(1, parseInt(url.searchParams.get('page') ?? '1', 10) || 1) + const recipient = await prisma.recipient.findUnique({ where: { id: params.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 + // Build the where clause for messages + const messageWhere = { + recipientId: params.recipientId, + sentAt: { not: null }, + ...(searchQuery + ? { content: { contains: searchQuery } } + : {}), + } + + // Get total count for pagination + const totalMessages = await prisma.message.count({ + where: messageWhere, + }) + + const totalPages = Math.max(1, Math.ceil(totalMessages / MESSAGES_PER_PAGE)) + const currentPage = Math.min(page, totalPages) + + // Get paginated messages + const messages = await prisma.message.findMany({ + where: messageWhere, + select: { id: true, content: true, sentAt: true }, + orderBy: { sentAt: 'desc' }, + skip: (currentPage - 1) * MESSAGES_PER_PAGE, + take: MESSAGES_PER_PAGE, + }) 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, + searchQuery, + pagination: { + currentPage, + totalPages, + totalMessages, + }, + pastMessages: messages.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, + })), }) } @@ -57,29 +88,141 @@ export const meta: MetaFunction = ({ data }) => { ] } -export default function RecipientRoute() { - const data = useLoaderData() +function Pagination({ + currentPage, + totalPages, + totalMessages, + searchQuery, +}: { + currentPage: number + totalPages: number + totalMessages: number + searchQuery: string +}) { + const [searchParams] = useSearchParams() + + const buildPageUrl = (page: number) => { + const params = new URLSearchParams(searchParams) + if (page === 1) { + params.delete('page') + } else { + params.set('page', page.toString()) + } + const queryString = params.toString() + return queryString ? `?${queryString}` : '.' + } + + const hasPrevPage = currentPage > 1 + const hasNextPage = currentPage < totalPages return ( -
-

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

+

+ {totalMessages === 0 + ? 'No messages found' + : `Showing ${((currentPage - 1) * MESSAGES_PER_PAGE) + 1}-${Math.min(currentPage * MESSAGES_PER_PAGE, totalMessages)} of ${totalMessages.toLocaleString()} message${totalMessages === 1 ? '' : 's'}`} + {searchQuery ? ( + <> + {' '} + matching "{searchQuery}" + + ) : null}

-
    - {data.pastMessages.map((m) => ( -
  • 1 ? ( +
    + + + Page {currentPage} of {totalPages} + + +
    + ) : null} +
+ ) +} + +export default function RecipientRoute() { + const data = useLoaderData() + const isPending = useDelayedIsPending({ + formMethod: 'GET', + }) + + return ( +
+
+ + +
+ +
    + {data.pastMessages.length === 0 ? ( +
  • + {data.searchQuery + ? 'No messages match your search.' + : 'No past messages yet.'}
  • - ))} + ) : ( + data.pastMessages.map((m) => ( +
  • + + {m.sentAtDisplay} + + {m.content} +
  • + )) + )}
+ + {data.pastMessages.length > 0 && data.pagination.totalPages > 1 ? ( + + ) : null}
) }