First version

This commit is contained in:
Sebastien Castiel
2023-12-05 17:39:05 -05:00
parent 1fd6e48807
commit ed55c696cd
41 changed files with 8305 additions and 468 deletions

155
src/lib/api.ts Normal file
View File

@@ -0,0 +1,155 @@
import { getPrisma } from '@/lib/prisma'
import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
import { Expense } from '@prisma/client'
import { v4 as uuidv4 } from 'uuid'
export async function createGroup(groupFormValues: GroupFormValues) {
return getPrisma().group.create({
data: {
id: uuidv4(),
name: groupFormValues.name,
participants: {
createMany: {
data: groupFormValues.participants.map(({ name }) => ({
id: uuidv4(),
name,
})),
},
},
},
include: { participants: true },
})
}
export async function createExpense(
expenseFormValues: ExpenseFormValues,
groupId: string,
): Promise<Expense> {
const group = await getGroup(groupId)
if (!group) throw new Error(`Invalid group ID: ${groupId}`)
for (const participant of [
expenseFormValues.paidBy,
...expenseFormValues.paidFor,
]) {
if (!group.participants.some((p) => p.id === participant))
throw new Error(`Invalid participant ID: ${participant}`)
}
return getPrisma().expense.create({
data: {
id: uuidv4(),
groupId,
amount: expenseFormValues.amount,
title: expenseFormValues.title,
paidById: expenseFormValues.paidBy,
paidFor: {
createMany: {
data: expenseFormValues.paidFor.map((paidFor) => ({
participantId: paidFor,
})),
},
},
},
})
}
export async function updateExpense(
groupId: string,
expenseId: string,
expenseFormValues: ExpenseFormValues,
) {
const group = await getGroup(groupId)
if (!group) throw new Error(`Invalid group ID: ${groupId}`)
const existingExpense = await getExpense(groupId, expenseId)
if (!existingExpense) throw new Error(`Invalid expense ID: ${expenseId}`)
for (const participant of [
expenseFormValues.paidBy,
...expenseFormValues.paidFor,
]) {
if (!group.participants.some((p) => p.id === participant))
throw new Error(`Invalid participant ID: ${participant}`)
}
return getPrisma().expense.update({
where: { id: expenseId },
data: {
amount: expenseFormValues.amount,
title: expenseFormValues.title,
paidById: expenseFormValues.paidBy,
paidFor: {
connectOrCreate: expenseFormValues.paidFor.map((paidFor) => ({
where: {
expenseId_participantId: { expenseId, participantId: paidFor },
},
create: { participantId: paidFor },
})),
deleteMany: existingExpense.paidFor.filter(
(paidFor) =>
!expenseFormValues.paidFor.some(
(pf) => pf === paidFor.participantId,
),
),
},
},
})
}
export async function updateGroup(
groupId: string,
groupFormValues: GroupFormValues,
) {
const existingGroup = await getGroup(groupId)
if (!existingGroup) throw new Error('Invalid group ID')
return getPrisma().group.update({
where: { id: groupId },
data: {
name: groupFormValues.name,
participants: {
deleteMany: existingGroup.participants.filter(
(p) => !groupFormValues.participants.some((p2) => p2.id === p.id),
),
updateMany: groupFormValues.participants
.filter((participant) => participant.id !== undefined)
.map((participant) => ({
where: { id: participant.id },
data: {
name: participant.name,
},
})),
createMany: {
data: groupFormValues.participants
.filter((participant) => participant.id === undefined)
.map((participant) => ({
id: uuidv4(),
name: participant.name,
})),
},
},
},
})
}
export async function getGroup(groupId: string) {
return getPrisma().group.findUnique({
where: { id: groupId },
include: { participants: true },
})
}
export async function getGroupExpenses(groupId: string) {
return getPrisma().expense.findMany({
where: { groupId },
include: { paidFor: { include: { participant: true } }, paidBy: true },
})
}
export async function getExpense(groupId: string, expenseId: string) {
return getPrisma().expense.findUnique({
where: { id: expenseId },
include: { paidBy: true, paidFor: true },
})
}

17
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,17 @@
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
export function getPrisma() {
if (!prisma) {
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!(global as any).prisma) {
;(global as any).prisma = new PrismaClient()
}
prisma = (global as any).prisma
}
}
return prisma
}

50
src/lib/schemas.ts Normal file
View File

@@ -0,0 +1,50 @@
import * as z from 'zod'
export const groupFormSchema = z
.object({
name: z
.string()
.min(2, 'Enter at least two characters.')
.max(50, 'Enter at most 50 characters.'),
participants: z
.array(
z.object({
id: z.string().optional(),
name: z
.string()
.min(2, 'Enter at least two characters.')
.max(50, 'Enter at most 50 characters.'),
}),
)
.min(1),
})
.superRefine(({ participants }, ctx) => {
participants.forEach((participant, i) => {
participants.slice(0, i).forEach((otherParticipant) => {
if (otherParticipant.name === participant.name) {
ctx.addIssue({
code: 'custom',
message: 'Another participant already has this name.',
path: ['participants', i, 'name'],
})
}
})
})
})
export type GroupFormValues = z.infer<typeof groupFormSchema>
export const expenseFormSchema = z.object({
title: z
.string({ required_error: 'Please enter a title.' })
.min(2, 'Enter at least two characters.'),
amount: z.coerce
.number({ required_error: 'You must enter an amount.' })
.min(0.01, 'The amount must be higher than 0.01.'),
paidBy: z.string({ required_error: 'You must select a participant.' }),
paidFor: z
.array(z.string())
.min(1, 'The expense must be paid for at least 1 participant.'),
})
export type ExpenseFormValues = z.infer<typeof expenseFormSchema>

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}