'use client' import { ExpenseCard } from '@/app/groups/[groupId]/expenses/expense-card' import { getGroupExpensesAction } from '@/app/groups/[groupId]/expenses/expense-list-fetch-action' import { Button } from '@/components/ui/button' import { SearchBar } from '@/components/ui/search-bar' import { Skeleton } from '@/components/ui/skeleton' import { trpc } from '@/trpc/client' import dayjs, { type Dayjs } from 'dayjs' import { useTranslations } from 'next-intl' import Link from 'next/link' import { forwardRef, useEffect, useMemo, useState } from 'react' import { useInView } from 'react-intersection-observer' import { useDebounce } from 'use-debounce' const PAGE_SIZE = 20 type ExpensesType = NonNullable< Awaited> > const EXPENSE_GROUPS = { UPCOMING: 'upcoming', THIS_WEEK: 'thisWeek', EARLIER_THIS_MONTH: 'earlierThisMonth', LAST_MONTH: 'lastMonth', EARLIER_THIS_YEAR: 'earlierThisYear', LAST_YEAR: 'lastYear', OLDER: 'older', } function getExpenseGroup(date: Dayjs, today: Dayjs) { if (today.isBefore(date)) { return EXPENSE_GROUPS.UPCOMING } else if (today.isSame(date, 'week')) { return EXPENSE_GROUPS.THIS_WEEK } else if (today.isSame(date, 'month')) { return EXPENSE_GROUPS.EARLIER_THIS_MONTH } else if (today.subtract(1, 'month').isSame(date, 'month')) { return EXPENSE_GROUPS.LAST_MONTH } else if (today.isSame(date, 'year')) { return EXPENSE_GROUPS.EARLIER_THIS_YEAR } else if (today.subtract(1, 'year').isSame(date, 'year')) { return EXPENSE_GROUPS.LAST_YEAR } else { return EXPENSE_GROUPS.OLDER } } function getGroupedExpensesByDate(expenses: ExpensesType) { const today = dayjs() return expenses.reduce((result: { [key: string]: ExpensesType }, expense) => { const expenseGroup = getExpenseGroup(dayjs(expense.expenseDate), today) result[expenseGroup] = result[expenseGroup] ?? [] result[expenseGroup].push(expense) return result }, {}) } export function ExpenseList({ groupId }: { groupId: string }) { const { data: groupData } = trpc.groups.get.useQuery({ groupId }) const [searchText, setSearchText] = useState('') const [debouncedSearchText] = useDebounce(searchText, 300) const participants = groupData?.group.participants useEffect(() => { if (!participants) return const activeUser = localStorage.getItem('newGroup-activeUser') const newUser = localStorage.getItem(`${groupId}-newUser`) if (activeUser || newUser) { localStorage.removeItem('newGroup-activeUser') localStorage.removeItem(`${groupId}-newUser`) if (activeUser === 'None') { localStorage.setItem(`${groupId}-activeUser`, 'None') } else { const userId = participants.find( (p) => p.name === (activeUser || newUser), )?.id if (userId) { localStorage.setItem(`${groupId}-activeUser`, userId) } } } }, [groupId, participants]) return ( <> setSearchText(value)} /> ) } const ExpenseListForSearch = ({ groupId, searchText, }: { groupId: string searchText: string }) => { const utils = trpc.useUtils() useEffect(() => { // Until we use tRPC more widely and can invalidate the cache on expense // update, it's easier and safer to invalidate the cache on page load. utils.groups.expenses.invalidate() }, [utils]) const t = useTranslations('Expenses') const { ref: loadingRef, inView } = useInView() const { data, isLoading: expensesAreLoading, fetchNextPage, } = trpc.groups.expenses.list.useInfiniteQuery( { groupId, limit: PAGE_SIZE, filter: searchText }, { getNextPageParam: ({ nextCursor }) => nextCursor }, ) const expenses = data?.pages.flatMap((page) => page.expenses) const hasMore = data?.pages.at(-1)?.hasMore ?? false const { data: groupData, isLoading: groupIsLoading } = trpc.groups.get.useQuery({ groupId }) const isLoading = expensesAreLoading || !expenses || groupIsLoading || !groupData useEffect(() => { if (inView && hasMore && !isLoading) fetchNextPage() }, [fetchNextPage, hasMore, inView, isLoading]) const groupedExpensesByDate = useMemo( () => (expenses ? getGroupedExpensesByDate(expenses) : {}), [expenses], ) if (isLoading) return if (expenses.length === 0) return (

{t('noExpenses')}{' '}

) return ( <> {Object.values(EXPENSE_GROUPS).map((expenseGroup: string) => { let groupExpenses = groupedExpensesByDate[expenseGroup] if (!groupExpenses || groupExpenses.length === 0) return null return (
{t(`Groups.${expenseGroup}`)}
{groupExpenses.map((expense) => ( ))}
) })} {hasMore && } ) } const ExpensesLoading = forwardRef((_, ref) => { return (
{[0, 1, 2].map((i) => (
))}
) }) ExpensesLoading.displayName = 'ExpensesLoading'