diff options
| author | Ankit Bahl <ankit2bahl@gmail.com> | 2024-01-19 13:38:18 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-19 16:38:18 -0500 |
| commit | ae7cb2ccc84cf25332d1efabeb197a5e1b05d029 (patch) | |
| tree | ba9df97e54789962dda8fcdc10d06b23696c9558 | |
| parent | 3735509fea74afddb3d5bc0eab57c3f03a239354 (diff) | |
Added search bar for expense list page (#52)
* Added search bar for expense list page
* Change search input styling
---------
Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
| -rw-r--r-- | src/app/groups/[groupId]/expenses/expense-list.tsx | 164 | ||||
| -rw-r--r-- | src/components/ui/search-bar.tsx | 33 |
2 files changed, 124 insertions, 73 deletions
diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx index 73c8c6e..b083ab6 100644 --- a/src/app/groups/[groupId]/expenses/expense-list.tsx +++ b/src/app/groups/[groupId]/expenses/expense-list.tsx @@ -1,6 +1,7 @@ 'use client' import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' import { Button } from '@/components/ui/button' +import { SearchBar } from '@/components/ui/search-bar' import { getGroupExpenses } from '@/lib/api' import { cn } from '@/lib/utils' import { Expense, Participant } from '@prisma/client' @@ -8,7 +9,7 @@ import dayjs, { type Dayjs } from 'dayjs' import { ChevronRight } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' -import { Fragment, useEffect } from 'react' +import { Fragment, useEffect, useState } from 'react' type Props = { expenses: Awaited<ReturnType<typeof getGroupExpenses>> @@ -63,6 +64,7 @@ export function ExpenseList({ participants, groupId, }: Props) { + const [searchText, setSearchText] = useState('') useEffect(() => { const activeUser = localStorage.getItem('newGroup-activeUser') const newUser = localStorage.getItem(`${groupId}-newUser`) @@ -86,86 +88,102 @@ export function ExpenseList({ const router = useRouter() const groupedExpensesByDate = getGroupedExpensesByDate(expenses) - return expenses.length > 0 ? ( - Object.values(EXPENSE_GROUPS).map((expenseGroup: string) => { - const groupExpenses = groupedExpensesByDate[expenseGroup] - if (!groupExpenses) return null - return ( - <div key={expenseGroup}> - <div - className={ - 'text-muted-foreground text-xs pl-4 sm:pl-6 py-1 font-semibold sticky top-16 bg-white dark:bg-[#1b1917]' - } - > - {expenseGroup} - </div> - {groupExpenses.map((expense: any) => ( + <> + <SearchBar onChange={(e) => setSearchText(e.target.value)} /> + {Object.values(EXPENSE_GROUPS).map((expenseGroup: string) => { + const groupExpenses = groupedExpensesByDate[expenseGroup] + if (!groupExpenses) return null + return ( + <div key={expenseGroup}> <div - key={expense.id} - className={cn( - 'flex justify-between sm:mx-6 px-4 sm:rounded-lg sm:pr-2 sm:pl-4 py-4 text-sm cursor-pointer hover:bg-accent gap-1 items-stretch', - expense.isReimbursement && 'italic', - )} - onClick={() => { - router.push(`/groups/${groupId}/expenses/${expense.id}/edit`) - }} + className={ + 'text-muted-foreground text-xs pl-4 sm:pl-6 py-1 font-semibold sticky top-16 bg-white dark:bg-[#1b1917]' + } > - <CategoryIcon - category={expense.category} - className="w-4 h-4 mr-2 mt-0.5 text-muted-foreground" - /> - <div className="flex-1"> - <div - className={cn('mb-1', expense.isReimbursement && 'italic')} - > - {expense.title} - </div> - <div className="text-xs text-muted-foreground"> - Paid by{' '} - <strong>{getParticipant(expense.paidById)?.name}</strong> for{' '} - {expense.paidFor.map((paidFor: any, index: number) => ( - <Fragment key={index}> - {index !== 0 && <>, </>} - <strong> - { - participants.find( - (p) => p.id === paidFor.participantId, - )?.name - } - </strong> - </Fragment> - ))} - </div> - </div> - <div className="flex flex-col justify-between items-end"> + {expenseGroup} + </div> + {groupExpenses + .filter( + (exp) => + exp.title.toLowerCase().match(searchText.toLowerCase()) !== + null, + ) + .map((expense: any) => ( <div + key={expense.id} className={cn( - 'tabular-nums whitespace-nowrap', - expense.isReimbursement ? 'italic' : 'font-bold', + 'flex justify-between sm:mx-6 px-4 sm:rounded-lg sm:pr-2 sm:pl-4 py-4 text-sm cursor-pointer hover:bg-accent gap-1 items-stretch', + expense.isReimbursement && 'italic', )} + onClick={() => { + router.push( + `/groups/${groupId}/expenses/${expense.id}/edit`, + ) + }} > - {currency} {(expense.amount / 100).toFixed(2)} - </div> - <div className="text-xs text-muted-foreground"> - {formatDate(expense.expenseDate)} + <CategoryIcon + category={expense.category} + className="w-4 h-4 mr-2 mt-0.5 text-muted-foreground" + /> + <div className="flex-1"> + <div + className={cn( + 'mb-1', + expense.isReimbursement && 'italic', + )} + > + {expense.title} + </div> + <div className="text-xs text-muted-foreground"> + Paid by{' '} + <strong>{getParticipant(expense.paidById)?.name}</strong>{' '} + for{' '} + {expense.paidFor.map((paidFor: any, index: number) => ( + <Fragment key={index}> + {index !== 0 && <>, </>} + <strong> + { + participants.find( + (p) => p.id === paidFor.participantId, + )?.name + } + </strong> + </Fragment> + ))} + </div> + </div> + <div className="flex flex-col justify-between items-end"> + <div + className={cn( + 'tabular-nums whitespace-nowrap', + expense.isReimbursement ? 'italic' : 'font-bold', + )} + > + {currency} {(expense.amount / 100).toFixed(2)} + </div> + <div className="text-xs text-muted-foreground"> + {formatDate(expense.expenseDate)} + </div> + </div> + <Button + size="icon" + variant="link" + className="self-center hidden sm:flex" + asChild + > + <Link + href={`/groups/${groupId}/expenses/${expense.id}/edit`} + > + <ChevronRight className="w-4 h-4" /> + </Link> + </Button> </div> - </div> - <Button - size="icon" - variant="link" - className="self-center hidden sm:flex" - asChild - > - <Link href={`/groups/${groupId}/expenses/${expense.id}/edit`}> - <ChevronRight className="w-4 h-4" /> - </Link> - </Button> - </div> - ))} - </div> - ) - }) + ))} + </div> + ) + })} + </> ) : ( <p className="px-6 text-sm py-6"> Your group doesn’t contain any expense yet.{' '} diff --git a/src/components/ui/search-bar.tsx b/src/components/ui/search-bar.tsx new file mode 100644 index 0000000..6369c55 --- /dev/null +++ b/src/components/ui/search-bar.tsx @@ -0,0 +1,33 @@ +import * as React from "react" + +import {Input} from "@/components/ui/input"; +import {cn} from "@/lib/utils"; +import { + Search +} from 'lucide-react' + +export interface InputProps + extends React.InputHTMLAttributes<HTMLInputElement> {} + +const SearchBar = React.forwardRef<HTMLInputElement, InputProps>( + ({ className, type, ...props }, ref) => { + return ( + <div className="mx-4 sm:mx-6 flex relative"> + <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> + <Input + type={type} + className={cn( + "pl-10 text-sm focus:text-base bg-muted border-none text-muted-foreground", + className + )} + ref={ref} + placeholder="Search for an expense…" + {...props} + /> + </div> + ) + } +) +SearchBar.displayName = "SearchBar" + +export { SearchBar } |
