Implement "infinite scroll" for expenses (#95)

* Extract ExpenseCard vom ExpenseList

* Implement simple pagination of expenses (see #30)

- display only this year's entries by default
- a "Show more" button reveals all expenses

* Turn getPrisma() into constant "prisma"

- getPrisma() is not async and doesn't need to be awaited
- turn getPrisma() into exported constant "prisma"

* Select fields to be returned by getGroupExpenses()

- make JSON more concise and less redundant
- some properties were removed (i.e.instead of "expense.paidById" use "expense.paidBy.id")

* Remove "participants" from ExpenseCard

- no need to search for participant by id to get it's name
- name property is already present in expense

* Add option to fetch a slice of group expenses

- specify offset and length to get expenses for [offset, offset+length[
- add function to get total number of group expenses

* Add api route for client to fetch group expenses

* Remove "Show more" button from expense list

* Implement infinite scroll

- in server component Page
  - only load first 200 expenses max
  - pass preloaded expenses and total count

- in client component ExpenseList, if there are more expenses to show
  - test if there are more expenses
  - append preloading "skeletons" to end of list
  - fetch more expenses when last item in list comes into view
  - after each fetch increase fetch-length by factor 1.5
    - rationale: db fetch usually is not the issue here, the longer the list gets, the longer react needs to redraw

* Use server action instead of api endpoint

* Fixes

---------

Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
This commit is contained in:
Stefan Hynst
2024-05-30 03:36:07 +02:00
committed by GitHub
parent 833237b613
commit d3fd8027a5
12 changed files with 266 additions and 138 deletions

View File

@@ -1,4 +1,4 @@
import { getPrisma } from '@/lib/prisma'
import { prisma } from '@/lib/prisma'
import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
import { Expense } from '@prisma/client'
import { nanoid } from 'nanoid'
@@ -8,7 +8,6 @@ export function randomId() {
}
export async function createGroup(groupFormValues: GroupFormValues) {
const prisma = await getPrisma()
return prisma.group.create({
data: {
id: randomId(),
@@ -42,7 +41,6 @@ export async function createExpense(
throw new Error(`Invalid participant ID: ${participant}`)
}
const prisma = await getPrisma()
return prisma.expense.create({
data: {
id: randomId(),
@@ -78,7 +76,6 @@ export async function createExpense(
}
export async function deleteExpense(expenseId: string) {
const prisma = await getPrisma()
await prisma.expense.delete({
where: { id: expenseId },
include: { paidFor: true, paidBy: true },
@@ -90,15 +87,14 @@ export async function getGroupExpensesParticipants(groupId: string) {
return Array.from(
new Set(
expenses.flatMap((e) => [
e.paidById,
...e.paidFor.map((pf) => pf.participantId),
e.paidBy.id,
...e.paidFor.map((pf) => pf.participant.id),
]),
),
)
}
export async function getGroups(groupIds: string[]) {
const prisma = await getPrisma()
return (
await prisma.group.findMany({
where: { id: { in: groupIds } },
@@ -129,7 +125,6 @@ export async function updateExpense(
throw new Error(`Invalid participant ID: ${participant}`)
}
const prisma = await getPrisma()
return prisma.expense.update({
where: { id: expenseId },
data: {
@@ -198,7 +193,6 @@ export async function updateGroup(
const existingGroup = await getGroup(groupId)
if (!existingGroup) throw new Error('Invalid group ID')
const prisma = await getPrisma()
return prisma.group.update({
where: { id: groupId },
data: {
@@ -230,7 +224,6 @@ export async function updateGroup(
}
export async function getGroup(groupId: string) {
const prisma = await getPrisma()
return prisma.group.findUnique({
where: { id: groupId },
include: { participants: true },
@@ -238,25 +231,43 @@ export async function getGroup(groupId: string) {
}
export async function getCategories() {
const prisma = await getPrisma()
return prisma.category.findMany()
}
export async function getGroupExpenses(groupId: string) {
const prisma = await getPrisma()
export async function getGroupExpenses(
groupId: string,
options?: { offset: number; length: number },
) {
return prisma.expense.findMany({
where: { groupId },
include: {
paidFor: { include: { participant: true } },
paidBy: true,
select: {
amount: true,
category: true,
createdAt: true,
expenseDate: true,
id: true,
isReimbursement: true,
paidBy: { select: { id: true, name: true } },
paidFor: {
select: {
participant: { select: { id: true, name: true } },
shares: true,
},
},
splitMode: true,
title: true,
},
where: { groupId },
orderBy: [{ expenseDate: 'desc' }, { createdAt: 'desc' }],
skip: options && options.offset,
take: options && options.length,
})
}
export async function getGroupExpenseCount(groupId: string) {
return prisma.expense.count({ where: { groupId } })
}
export async function getExpense(groupId: string, expenseId: string) {
const prisma = await getPrisma()
return prisma.expense.findUnique({
where: { id: expenseId },
include: { paidBy: true, paidFor: true, category: true, documents: true },

View File

@@ -19,7 +19,7 @@ export function getBalances(
const balances: Balances = {}
for (const expense of expenses) {
const paidBy = expense.paidById
const paidBy = expense.paidBy.id
const paidFors = expense.paidFor
if (!balances[paidBy]) balances[paidBy] = { paid: 0, paidFor: 0, total: 0 }
@@ -31,8 +31,8 @@ export function getBalances(
)
let remaining = expense.amount
paidFors.forEach((paidFor, index) => {
if (!balances[paidFor.participantId])
balances[paidFor.participantId] = { paid: 0, paidFor: 0, total: 0 }
if (!balances[paidFor.participant.id])
balances[paidFor.participant.id] = { paid: 0, paidFor: 0, total: 0 }
const isLast = index === paidFors.length - 1
@@ -47,7 +47,7 @@ export function getBalances(
? remaining
: (expense.amount * shares) / totalShares
remaining -= dividedAmount
balances[paidFor.participantId].paidFor += dividedAmount
balances[paidFor.participant.id].paidFor += dividedAmount
})
}

View File

@@ -1,20 +1,21 @@
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
declare const global: Global & { prisma?: PrismaClient }
export async function getPrisma() {
export let p: PrismaClient = undefined as any as PrismaClient
if (typeof window === 'undefined') {
// await delay(1000)
if (!prisma) {
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!(global as any).prisma) {
;(global as any).prisma = new PrismaClient({
// log: [{ emit: 'stdout', level: 'query' }],
})
}
prisma = (global as any).prisma
if (process.env['NODE_ENV'] === 'production') {
p = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient({
// log: [{ emit: 'stdout', level: 'query' }],
})
}
p = global.prisma
}
return prisma
}
export const prisma = p

View File

@@ -34,7 +34,7 @@ export function getTotalActiveUserShare(
const paidFors = expense.paidFor
const userPaidFor = paidFors.find(
(paidFor) => paidFor.participantId === activeUserId,
(paidFor) => paidFor.participant.id === activeUserId,
)
if (!userPaidFor) {