From ed020e80c78514fb2f91d447a1819fa1660a7873 Mon Sep 17 00:00:00 2001 From: Jacques Fargion Date: Sun, 30 Aug 2026 12:48:54 +0200 Subject: [PATCH 1/2] feat: add search to Activity page Adds a live search input to the Activity page that filters expenses by name, category, group name, and payer (name/email). - api.expense.getAllExpenses now also includes the related group's id/name so it can be matched. - Client-side filtering (no new endpoint/DB query needed, since the page already loads the user's full expense list). - Adds en/it translations for the search placeholder and empty-state message; other locales fall back to English until translated. Closes #745 --- public/locales/en/common.json | 2 ++ public/locales/it/common.json | 2 ++ src/pages/activity.tsx | 53 +++++++++++++++++++++++++++++-- src/server/api/routers/expense.ts | 6 ++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5ec3f196f..fcd50be05 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/public/locales/it/common.json b/public/locales/it/common.json index 81c79e909..2ce38389d 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -404,6 +404,8 @@ "expense_details": "Dettagli spesa", "in_group": "Nel gruppo", "no_activity": "Nessuna attività ancora", + "no_search_results": "Nessuna spesa corrisponde alla ricerca", + "search_expenses_placeholder": "Cerca spese...", "not_involved": "Non coinvolto", "not_set": "Non impostato", "on": "il", diff --git a/src/pages/activity.tsx b/src/pages/activity.tsx index 62e66c648..d75de8ee7 100644 --- a/src/pages/activity.tsx +++ b/src/pages/activity.tsx @@ -10,8 +10,9 @@ 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 { RefreshCcwDot, Search, X } from 'lucide-react'; import { Button } from '~/components/ui/button'; +import { Input } from '~/components/ui/input'; import React from 'react'; function getPaymentString( @@ -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,28 @@ 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 +100,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 afc6f354a..25e764ca8 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, + }, + }, }, }, }, From 23c12a081e41ba6da201787f1b887f61c96d2413 Mon Sep 17 00:00:00 2001 From: Jacques Fargion Date: Sun, 30 Aug 2026 13:02:26 +0200 Subject: [PATCH 2/2] fix: address CodeRabbit review comments on #746 - Drop the new Italian locale keys: per CONTRIBUTING.md, new features should only add English keys, community translations go through Weblate. - Reorder imports in activity.tsx (external packages, then internal ~/ imports) to satisfy the sort-imports rule. - Add braces around the early returns in the search useMemo (curly). --- public/locales/it/common.json | 2 -- src/pages/activity.tsx | 20 ++++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 2ce38389d..81c79e909 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -404,8 +404,6 @@ "expense_details": "Dettagli spesa", "in_group": "Nel gruppo", "no_activity": "Nessuna attività ancora", - "no_search_results": "Nessuna spesa corrisponde alla ricerca", - "search_expenses_placeholder": "Cerca spese...", "not_involved": "Non coinvolto", "not_set": "Non impostato", "on": "il", diff --git a/src/pages/activity.tsx b/src/pages/activity.tsx index d75de8ee7..b9c5f0e87 100644 --- a/src/pages/activity.tsx +++ b/src/pages/activity.tsx @@ -1,19 +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, Search, X } from 'lucide-react'; -import { Button } from '~/components/ui/button'; -import { Input } from '~/components/ui/input'; -import React from 'react'; function getPaymentString( user: User, @@ -69,8 +69,12 @@ const ActivityPage: NextPageWithUser = ({ user }) => { const normalizedSearch = search.trim().toLowerCase(); const filteredExpenses = React.useMemo(() => { - if (!expensesQuery.data) return expensesQuery.data; - if (!normalizedSearch) return expensesQuery.data; + if (!expensesQuery.data) { + return expensesQuery.data; + } + if (!normalizedSearch) { + return expensesQuery.data; + } return expensesQuery.data.filter((e) => { const haystack = [