diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5ec3f196..fcd50be0 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -436,6 +436,8 @@ }, "expense_details": "Expense details", "no_activity": "No activities yet", + "no_search_results": "No expenses match your search", + "search_expenses_placeholder": "Search expenses...", "not_involved": "Not involved", "not_set": "Not set", "on": "on", diff --git a/src/pages/activity.tsx b/src/pages/activity.tsx index 62e66c64..b9c5f0e8 100644 --- a/src/pages/activity.tsx +++ b/src/pages/activity.tsx @@ -1,18 +1,19 @@ import { SplitType } from '@prisma/client'; +import { RefreshCcwDot, Search, X } from 'lucide-react'; import { type User } from 'next-auth'; +import { type TFunction } from 'next-i18next'; import Head from 'next/head'; import Link from 'next/link'; +import React from 'react'; import MainLayout from '~/components/Layout/MainLayout'; import { EntityAvatar } from '~/components/ui/avatar'; +import { Button } from '~/components/ui/button'; +import { Input } from '~/components/ui/input'; +import { useTranslationWithUtils } from '~/hooks/useTranslationWithUtils'; import { type NextPageWithUser } from '~/types'; import { api } from '~/utils/api'; import { getCurrencyHelpers } from '~/utils/numbers'; -import { type TFunction } from 'next-i18next'; -import { useTranslationWithUtils } from '~/hooks/useTranslationWithUtils'; import { withI18nStaticProps } from '~/utils/i18n/server'; -import { RefreshCcwDot } from 'lucide-react'; -import { Button } from '~/components/ui/button'; -import React from 'react'; function getPaymentString( user: User, @@ -52,6 +53,7 @@ function getPaymentString( const ActivityPage: NextPageWithUser = ({ user }) => { const { displayName, t, toUIDate, i18n } = useTranslationWithUtils(); const expensesQuery = api.expense.getAllExpenses.useQuery(); + const [search, setSearch] = React.useState(''); const actions = React.useMemo( () => ( @@ -64,6 +66,32 @@ const ActivityPage: NextPageWithUser = ({ user }) => { [], ); + const normalizedSearch = search.trim().toLowerCase(); + + const filteredExpenses = React.useMemo(() => { + if (!expensesQuery.data) { + return expensesQuery.data; + } + if (!normalizedSearch) { + return expensesQuery.data; + } + + return expensesQuery.data.filter((e) => { + const haystack = [ + e.expense.name, + e.expense.category, + e.expense.group?.name, + e.expense.paidByUser.name, + e.expense.paidByUser.email, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + + return haystack.includes(normalizedSearch); + }); + }, [expensesQuery.data, normalizedSearch]); + return ( <> @@ -76,10 +104,35 @@ const ActivityPage: NextPageWithUser = ({ user }) => { loading={expensesQuery.isPending} >
+ {!!expensesQuery.data?.length && ( + setSearch(ev.target.value)} + placeholder={t('ui.search_expenses_placeholder')} + rightIcon={ + search ? ( + + ) : ( + + ) + } + /> + )} + {!expensesQuery.data?.length ? (
{t('ui.no_activity')}
) : null} - {expensesQuery.data?.map((e) => { + {!!expensesQuery.data?.length && !filteredExpenses?.length ? ( +
{t('ui.no_search_results')}
+ ) : null} + {filteredExpenses?.map((e) => { const { toUIString } = getCurrencyHelpers({ locale: i18n.language, currency: e.expense.currency, diff --git a/src/server/api/routers/expense.ts b/src/server/api/routers/expense.ts index afc6f354..25e764ca 100644 --- a/src/server/api/routers/expense.ts +++ b/src/server/api/routers/expense.ts @@ -453,6 +453,12 @@ export const expenseRouter = createTRPCRouter({ id: true, }, }, + group: { + select: { + id: true, + name: true, + }, + }, }, }, },