diff --git a/app/components/search-bar.tsx b/app/components/search-bar.tsx index f8aaf612..ba88051d 100644 --- a/app/components/search-bar.tsx +++ b/app/components/search-bar.tsx @@ -20,14 +20,16 @@ export function SearchBar({ showDateFilter?: boolean }) { const id = useId() - const dateId = `${id}-date` + const startDateId = `${id}-start-date` + const endDateId = `${id}-end-date` const [searchParams] = useSearchParams() const submit = useSubmit() const isSubmitting = useIsPending({ formMethod: 'GET', formAction: action, }) - const dateValue = searchParams.get('date') ?? '' + const startDateValue = searchParams.get('startDate') ?? '' + const endDateValue = searchParams.get('endDate') ?? '' const handleFormChange = useDebounce((form: HTMLFormElement) => { void submit(form) @@ -55,17 +57,31 @@ export function SearchBar({ /> {showDateFilter ? ( -
- - +
+
+ + +
+
+ + +
) : null}
diff --git a/app/routes/_app+/_layout.tsx b/app/routes/_app+/_layout.tsx index 6c89b1f9..3e583942 100644 --- a/app/routes/_app+/_layout.tsx +++ b/app/routes/_app+/_layout.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useRef, useState, type CSSProperties } from 'react' import { Form, Link, @@ -67,40 +67,52 @@ export default function Layout() { const data = useLoaderData() const user = useOptionalUser() const requestInfo = useRequestInfo() + const isRecipientsRoute = requestInfo.path.startsWith('/recipients') + const recipientsTheme = isRecipientsRoute + ? ({ + '--background': '0 0% 100%', + '--card': '0 0% 100%', + } as CSSProperties) + : undefined return ( -
-
- +
diff --git a/app/routes/_app+/recipients+/$recipientId.index.tsx b/app/routes/_app+/recipients+/$recipientId.index.tsx index b73efcea..b4871a44 100644 --- a/app/routes/_app+/recipients+/$recipientId.index.tsx +++ b/app/routes/_app+/recipients+/$recipientId.index.tsx @@ -48,7 +48,7 @@ type FutureMessage = LoaderData['futureMessages'][number] const PAST_MESSAGES_PER_PAGE = 30 -function getDateRange(value: string, timeZone: string) { +function parseDateValue(value: string) { if (!value) return null const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) if (!match) return null @@ -56,18 +56,39 @@ function getDateRange(value: string, timeZone: string) { const month = Number(match[2]) const day = Number(match[3]) if (!year || !month || !day) return null + return { year, month, day } +} + +function getStartDate(value: string, timeZone: string) { + const parts = parseDateValue(value) + if (!parts) return null + try { + const start = getDateInTimeZone( + parts.year, + parts.month, + parts.day, + timeZone, + ) + return Number.isNaN(start.getTime()) ? null : start + } catch { + return null + } +} + +function getEndDate(value: string, timeZone: string) { + const parts = parseDateValue(value) + if (!parts) return null try { - const start = getDateInTimeZone(year, month, day, timeZone) - if (Number.isNaN(start.getTime())) return null - const nextDay = new Date(Date.UTC(year, month - 1, day + 1)) + const nextDay = new Date( + Date.UTC(parts.year, parts.month - 1, parts.day + 1), + ) const end = getDateInTimeZone( nextDay.getUTCFullYear(), nextDay.getUTCMonth() + 1, nextDay.getUTCDate(), timeZone, ) - if (Number.isNaN(end.getTime())) return null - return { start, end } + return Number.isNaN(end.getTime()) ? null : end } catch { return null } @@ -115,7 +136,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const hints = getHints(request) const url = new URL(request.url) const searchQuery = url.searchParams.get('search') ?? '' - const dateFilter = url.searchParams.get('date') ?? '' + const startDateFilter = url.searchParams.get('startDate') ?? '' + const endDateFilter = url.searchParams.get('endDate') ?? '' const cursor = url.searchParams.get('cursor') const recipient = await prisma.recipient.findUnique({ where: { id: params.recipientId }, @@ -140,13 +162,21 @@ export async function loader({ params, request }: LoaderFunctionArgs) { where: { phoneNumber: recipient.phoneNumber }, }) - const dateRange = getDateRange( - dateFilter, + const startDate = getStartDate( + startDateFilter, hints.timeZone ?? recipient.timeZone, ) - const sentAtFilter = dateRange - ? { gte: dateRange.start, lt: dateRange.end } - : { not: null } + const endDate = getEndDate( + endDateFilter, + hints.timeZone ?? recipient.timeZone, + ) + const sentAtFilter = + startDate || endDate + ? { + ...(startDate ? { gte: startDate } : {}), + ...(endDate ? { lt: endDate } : {}), + } + : { not: null } const pastMessageWhere = { recipientId: params.recipientId, sentAt: sentAtFilter, @@ -173,7 +203,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) { optedOut: Boolean(optOut), recipient: recipientProps, searchQuery, - dateFilter, + startDateFilter, + endDateFilter, nextCursor, cronError: (() => { try { @@ -461,7 +492,8 @@ export default function RecipientRoute() { data.pastMessages, data.nextCursor, data.searchQuery, - data.dateFilter, + data.startDateFilter, + data.endDateFilter, data.recipient.phoneNumber, ]) @@ -470,7 +502,8 @@ export default function RecipientRoute() { if ( loadMoreData.recipient.phoneNumber !== data.recipient.phoneNumber || loadMoreData.searchQuery !== data.searchQuery || - loadMoreData.dateFilter !== data.dateFilter + loadMoreData.startDateFilter !== data.startDateFilter || + loadMoreData.endDateFilter !== data.endDateFilter ) { return } @@ -488,7 +521,8 @@ export default function RecipientRoute() { loadMoreData, data.recipient.phoneNumber, data.searchQuery, - data.dateFilter, + data.startDateFilter, + data.endDateFilter, ]) useLayoutEffect(() => { @@ -535,10 +569,15 @@ export default function RecipientRoute() { } }, [handleScroll, scrollContainer]) - const isPastFiltered = Boolean(data.searchQuery || data.dateFilter) - const emptyPastMessage = isPastFiltered + const isPastFiltered = Boolean( + data.searchQuery || data.startDateFilter || data.endDateFilter, + ) + const hasPastMessages = pastMessagesForDisplay.length > 0 + const hasFutureMessages = data.futureMessages.length > 0 + const hasAnyMessages = hasPastMessages || hasFutureMessages + const emptyThreadMessage = isPastFiltered ? 'No messages match your search.' - : 'No past messages yet.' + : 'No messages yet.' const loadMoreLabel = pastNextCursor ? isLoadingMore ? 'Loading earlier messages...' @@ -559,70 +598,56 @@ export default function RecipientRoute() {

- Past messages + Messages

-
- {pastMessagesForDisplay.length === 0 ? ( -

- {emptyPastMessage} -

- ) : ( -
-
-
- {loadMoreLabel} -
-
    - {pastMessagesForDisplay.map((m) => ( -
  • -
    -

    {m.content}

    -
    - -
  • - ))} -
+ {hasAnyMessages ? ( +
+ {hasPastMessages || pastNextCursor ? ( +
+ {loadMoreLabel}
-
- )} -
-
-
-

- Upcoming messages -

-
    - {data.futureMessages.length ? ( - data.futureMessages.map((m, index) => ( -
  • - -
  • - )) - ) : ( + ) : null} +
      + {pastMessagesForDisplay.map((m) => ( +
    • +
      +

      {m.content}

      +
      + +
    • + ))} + {data.futureMessages.map((m) => ( + + ))} +
    +
+ ) : ( +
+

{emptyThreadMessage}

Create a new message - )} - +
+ )}