@@ -4,48 +4,79 @@ import {
44 json ,
55 type LoaderFunctionArgs ,
66} from '@remix-run/node'
7- import { useLoaderData } from '@remix-run/react'
7+ import { Link , useLoaderData , useSearchParams } from '@remix-run/react'
88import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
9+ import { SearchBar } from '#app/components/search-bar.tsx'
10+ import { Button } from '#app/components/ui/button.tsx'
11+ import { Icon } from '#app/components/ui/icon.tsx'
912import { requireUserId } from '#app/utils/auth.server.ts'
1013import { prisma } from '#app/utils/db.server.ts'
14+ import { cn , useDelayedIsPending } from '#app/utils/misc.tsx'
15+
16+ const MESSAGES_PER_PAGE = 100
1117
1218export async function loader ( { params, request } : LoaderFunctionArgs ) {
1319 const userId = await requireUserId ( request )
20+ const url = new URL ( request . url )
21+ const searchQuery = url . searchParams . get ( 'search' ) ?? ''
22+ const page = Math . max ( 1 , parseInt ( url . searchParams . get ( 'page' ) ?? '1' , 10 ) || 1 )
23+
1424 const recipient = await prisma . recipient . findUnique ( {
1525 where : { id : params . recipientId , userId } ,
1626 select : {
1727 name : true ,
1828 phoneNumber : true ,
19- messages : {
20- select : { id : true , content : true , sentAt : true , order : true } ,
21- orderBy : { order : 'asc' } ,
22- where : { sentAt : { not : null } } ,
23- } ,
2429 } ,
2530 } )
2631
2732 invariantResponse ( recipient , 'Not found' , { status : 404 } )
2833
29- const { messages, ...recipientProps } = recipient
34+ // Build the where clause for messages
35+ const messageWhere = {
36+ recipientId : params . recipientId ,
37+ sentAt : { not : null } ,
38+ ...( searchQuery
39+ ? { content : { contains : searchQuery } }
40+ : { } ) ,
41+ }
42+
43+ // Get total count for pagination
44+ const totalMessages = await prisma . message . count ( {
45+ where : messageWhere ,
46+ } )
47+
48+ const totalPages = Math . max ( 1 , Math . ceil ( totalMessages / MESSAGES_PER_PAGE ) )
49+ const currentPage = Math . min ( page , totalPages )
50+
51+ // Get paginated messages
52+ const messages = await prisma . message . findMany ( {
53+ where : messageWhere ,
54+ select : { id : true , content : true , sentAt : true } ,
55+ orderBy : { sentAt : 'desc' } ,
56+ skip : ( currentPage - 1 ) * MESSAGES_PER_PAGE ,
57+ take : MESSAGES_PER_PAGE ,
58+ } )
3059
3160 return json ( {
32- recipient : recipientProps ,
33- messageCountDisplay : messages . length . toLocaleString ( ) ,
34- pastMessages : messages
35- . filter ( ( m ) => m . sentAt )
36- . sort ( ( m1 , m2 ) => m2 . sentAt ! . getTime ( ) - m1 . sentAt ! . getTime ( ) )
37- . map ( ( m ) => ( {
38- id : m . id ,
39- sentAtDisplay : m . sentAt ! . toLocaleDateString ( 'en-US' , {
40- weekday : 'short' ,
41- year : 'numeric' ,
42- month : 'short' ,
43- day : 'numeric' ,
44- hour : 'numeric' ,
45- minute : 'numeric' ,
46- } ) ,
47- content : m . content ,
48- } ) ) ,
61+ recipient,
62+ searchQuery,
63+ pagination : {
64+ currentPage,
65+ totalPages,
66+ totalMessages,
67+ } ,
68+ pastMessages : messages . map ( ( m ) => ( {
69+ id : m . id ,
70+ sentAtDisplay : m . sentAt ! . toLocaleDateString ( 'en-US' , {
71+ weekday : 'short' ,
72+ year : 'numeric' ,
73+ month : 'short' ,
74+ day : 'numeric' ,
75+ hour : 'numeric' ,
76+ minute : 'numeric' ,
77+ } ) ,
78+ content : m . content ,
79+ } ) ) ,
4980 } )
5081}
5182
@@ -57,29 +88,141 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
5788 ]
5889}
5990
60- export default function RecipientRoute ( ) {
61- const data = useLoaderData < typeof loader > ( )
91+ function Pagination ( {
92+ currentPage,
93+ totalPages,
94+ totalMessages,
95+ searchQuery,
96+ } : {
97+ currentPage : number
98+ totalPages : number
99+ totalMessages : number
100+ searchQuery : string
101+ } ) {
102+ const [ searchParams ] = useSearchParams ( )
103+
104+ const buildPageUrl = ( page : number ) => {
105+ const params = new URLSearchParams ( searchParams )
106+ if ( page === 1 ) {
107+ params . delete ( 'page' )
108+ } else {
109+ params . set ( 'page' , page . toString ( ) )
110+ }
111+ const queryString = params . toString ( )
112+ return queryString ? `?${ queryString } ` : '.'
113+ }
114+
115+ const hasPrevPage = currentPage > 1
116+ const hasNextPage = currentPage < totalPages
62117
63118 return (
64- < div >
65- < p className = "mb-8" >
66- You have sent < strong > { data . messageCountDisplay } </ strong > { ' ' }
67- { data . pastMessages . length === 1 ? 'message' : 'messages' } to{ ' ' }
68- { data . recipient . name } .
119+ < div className = "flex flex-col items-center gap-4 sm:flex-row sm:justify-between" >
120+ < p className = "text-sm text-muted-foreground" >
121+ { totalMessages === 0
122+ ? 'No messages found'
123+ : `Showing ${ ( ( currentPage - 1 ) * MESSAGES_PER_PAGE ) + 1 } -${ Math . min ( currentPage * MESSAGES_PER_PAGE , totalMessages ) } of ${ totalMessages . toLocaleString ( ) } message${ totalMessages === 1 ? '' : 's' } ` }
124+ { searchQuery ? (
125+ < >
126+ { ' ' }
127+ matching "< strong > { searchQuery } </ strong > "
128+ </ >
129+ ) : null }
69130 </ p >
70- < ul className = "flex flex-col gap-2" >
71- { data . pastMessages . map ( ( m ) => (
72- < li
73- key = { m . id }
74- className = "flex flex-col justify-start gap-2 align-top lg:flex-row"
131+ { totalPages > 1 ? (
132+ < div className = "flex items-center gap-2" >
133+ < Button
134+ variant = "outline"
135+ size = "sm"
136+ asChild = { hasPrevPage }
137+ disabled = { ! hasPrevPage }
138+ >
139+ { hasPrevPage ? (
140+ < Link to = { buildPageUrl ( currentPage - 1 ) } preventScrollReset >
141+ < Icon name = "arrow-left" size = "sm" />
142+ Previous
143+ </ Link >
144+ ) : (
145+ < span >
146+ < Icon name = "arrow-left" size = "sm" />
147+ Previous
148+ </ span >
149+ ) }
150+ </ Button >
151+ < span className = "px-2 text-sm" >
152+ Page { currentPage } of { totalPages }
153+ </ span >
154+ < Button
155+ variant = "outline"
156+ size = "sm"
157+ asChild = { hasNextPage }
158+ disabled = { ! hasNextPage }
75159 >
76- < span className = "min-w-36 text-muted-secondary-foreground" >
77- { m . sentAtDisplay }
78- </ span >
79- < span > { m . content } </ span >
160+ { hasNextPage ? (
161+ < Link to = { buildPageUrl ( currentPage + 1 ) } preventScrollReset >
162+ Next
163+ < Icon name = "arrow-right" size = "sm" />
164+ </ Link >
165+ ) : (
166+ < span >
167+ Next
168+ < Icon name = "arrow-right" size = "sm" />
169+ </ span >
170+ ) }
171+ </ Button >
172+ </ div >
173+ ) : null }
174+ </ div >
175+ )
176+ }
177+
178+ export default function RecipientRoute ( ) {
179+ const data = useLoaderData < typeof loader > ( )
180+ const isPending = useDelayedIsPending ( {
181+ formMethod : 'GET' ,
182+ } )
183+
184+ return (
185+ < div className = "flex flex-col gap-6" >
186+ < div className = "flex flex-col gap-4" >
187+ < SearchBar status = "idle" autoSubmit />
188+ < Pagination
189+ currentPage = { data . pagination . currentPage }
190+ totalPages = { data . pagination . totalPages }
191+ totalMessages = { data . pagination . totalMessages }
192+ searchQuery = { data . searchQuery }
193+ />
194+ </ div >
195+
196+ < ul className = { cn ( 'flex flex-col gap-2' , { 'opacity-50' : isPending } ) } >
197+ { data . pastMessages . length === 0 ? (
198+ < li className = "py-8 text-center text-muted-foreground" >
199+ { data . searchQuery
200+ ? 'No messages match your search.'
201+ : 'No past messages yet.' }
80202 </ li >
81- ) ) }
203+ ) : (
204+ data . pastMessages . map ( ( m ) => (
205+ < li
206+ key = { m . id }
207+ className = "flex flex-col justify-start gap-2 align-top lg:flex-row"
208+ >
209+ < span className = "min-w-36 text-muted-secondary-foreground" >
210+ { m . sentAtDisplay }
211+ </ span >
212+ < span > { m . content } </ span >
213+ </ li >
214+ ) )
215+ ) }
82216 </ ul >
217+
218+ { data . pastMessages . length > 0 && data . pagination . totalPages > 1 ? (
219+ < Pagination
220+ currentPage = { data . pagination . currentPage }
221+ totalPages = { data . pagination . totalPages }
222+ totalMessages = { data . pagination . totalMessages }
223+ searchQuery = { data . searchQuery }
224+ />
225+ ) : null }
83226 </ div >
84227 )
85228}
0 commit comments