Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added search bar for expense list page #52

Merged
merged 2 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 91 additions & 73 deletions src/app/groups/[groupId]/expenses/expense-list.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'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'
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>>
Expand Down Expand Up @@ -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`)
Expand All @@ -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.{' '}
Expand Down
33 changes: 33 additions & 0 deletions src/components/ui/search-bar.tsx
Original file line number Diff line number Diff line change
@@ -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 }