2 Commits

Author SHA1 Message Date
Sebastien Castiel
d3b151e150 Upgrade dependencies (#479)
All checks were successful
CI / checks (push) Successful in 1m7s
Migrate to latest versions of Next.js, React, Radix, etc.
2025-12-06 12:50:01 -05:00
Ulrich Zorn
19c009f6b8 fix(db): Correct local db script volume mount for modern postgres images (minimal fix) (#460)
All checks were successful
CI / checks (push) Successful in 1m1s
The scripts/start-local-db.sh script was failing for modern PostgreSQL Docker images (version 18+) due to an incorrect volume mount point.

This commit provides a minimal fix by changing the volume mount from /var/lib/postgresql/data to /var/lib/postgresql, which is the correct path for modern postgres images.
2025-11-13 16:34:22 +01:00
24 changed files with 3837 additions and 2742 deletions

21
eslint.config.mjs Normal file
View File

@@ -0,0 +1,21 @@
import nextVitals from 'eslint-config-next/core-web-vitals'
import { defineConfig, globalIgnores } from 'eslint/config'
const eslintConfig = defineConfig([
...nextVitals,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
]),
{
rules: {
'react-hooks/set-state-in-effect': 'off',
},
},
])
export default eslintConfig

6389
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint .",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"check-formatting": "prettier -c src", "check-formatting": "prettier -c src",
"prettier": "prettier -w src", "prettier": "prettier -w src",
@@ -21,19 +21,19 @@
"@hookform/resolvers": "^3.3.2", "@hookform/resolvers": "^3.3.2",
"@json2csv/plainjs": "^7.0.6", "@json2csv/plainjs": "^7.0.6",
"@prisma/client": "^6.18.0", "@prisma/client": "^6.18.0",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.2.15",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.59.15", "@tanstack/react-query": "^5.59.15",
"@trpc/client": "^11.0.0-rc.586", "@trpc/client": "^11.0.0-rc.586",
@@ -42,25 +42,25 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"client-only": "^0.0.1", "client-only": "^0.0.1",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^0.2.0", "cmdk": "^1.1.1",
"content-disposition": "^0.5.4", "content-disposition": "^0.5.4",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"embla-carousel-react": "^8.0.0-rc21", "embla-carousel-react": "^8.6.0",
"lucide-react": "^0.501.0", "lucide-react": "^0.501.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"negotiator": "^0.6.3", "negotiator": "^0.6.3",
"next": "^14.2.5", "next": "^16.0.7",
"next-intl": "^3.17.2", "next-intl": "^4.5.8",
"next-s3-upload": "^0.3.4", "next-s3-upload": "^0.3.4",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"next13-progressbar": "^1.1.1", "next13-progressbar": "^1.1.1",
"openai": "^4.25.0", "openai": "^4.25.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"prisma": "^6.18.0", "prisma": "^6.18.0",
"react": "^18.3.1", "react": "^19.2.1",
"react-dom": "^18.3.1", "react-dom": "^19.2.1",
"react-hook-form": "^7.47.0", "react-hook-form": "^7.68.0",
"react-intersection-observer": "^9.8.0", "react-intersection-observer": "^10.0.0",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"sharp": "^0.33.2", "sharp": "^0.33.2",
"superjson": "^2.2.1", "superjson": "^2.2.1",
@@ -70,13 +70,13 @@
"ts-pattern": "^5.0.6", "ts-pattern": "^5.0.6",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vaul": "^0.8.0", "vaul": "^1.1.2",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8", "@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.3.0",
"@total-typescript/ts-reset": "^0.5.1", "@total-typescript/ts-reset": "^0.5.1",
"@types/content-disposition": "^0.5.8", "@types/content-disposition": "^0.5.8",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
@@ -89,8 +89,8 @@
"autoprefixer": "^10", "autoprefixer": "^10",
"currency-list": "^1.0.8", "currency-list": "^1.0.8",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8", "eslint": "^9.39.1",
"eslint-config-next": "^14.1.0", "eslint-config-next": "^16.0.7",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"postcss": "^8", "postcss": "^8",

View File

@@ -6,6 +6,6 @@ else
echo "postgres is not running, starting it" echo "postgres is not running, starting it"
docker rm postgres --force docker rm postgres --force
mkdir -p postgres-data mkdir -p postgres-data
docker run --name spliit-db -d -p 5432:5432 -e POSTGRES_PASSWORD=1234 -v "/$(pwd)/postgres-data:/var/lib/postgresql/data" postgres docker run --name spliit-db -d -p 5432:5432 -e POSTGRES_PASSWORD=1234 -v "/$(pwd)/postgres-data:/var/lib/postgresql" postgres
sleep 5 # Wait for postgres to start sleep 5 # Wait for postgres to start
fi fi

View File

@@ -7,10 +7,11 @@ export const metadata: Metadata = {
} }
export default async function EditExpensePage({ export default async function EditExpensePage({
params: { groupId, expenseId }, params,
}: { }: {
params: { groupId: string; expenseId: string } params: Promise<{ groupId: string; expenseId: string }>
}) { }) {
const { groupId, expenseId } = await params
return ( return (
<EditExpenseForm <EditExpenseForm
groupId={groupId} groupId={groupId}

View File

@@ -48,6 +48,7 @@ export function CategoryIcon({
...props ...props
}: { category: Category | null } & LucideProps) { }: { category: Category | null } & LucideProps) {
const Icon = getCategoryIcon(`${category?.grouping}/${category?.name}`) const Icon = getCategoryIcon(`${category?.grouping}/${category?.name}`)
// eslint-disable-next-line react-hooks/static-components
return <Icon {...props} /> return <Icon {...props} />
} }

View File

@@ -12,7 +12,7 @@ export async function extractExpenseInformationFromImage(imageUrl: string) {
const categories = await getCategories() const categories = await getCategories()
const body: ChatCompletionCreateParamsNonStreaming = { const body: ChatCompletionCreateParamsNonStreaming = {
model: 'gpt-4-turbo', model: 'gpt-5-nano',
messages: [ messages: [
{ {
role: 'user', role: 'user',

View File

@@ -7,10 +7,11 @@ export const metadata: Metadata = {
} }
export default async function ExpensePage({ export default async function ExpensePage({
params: { groupId }, params,
}: { }: {
params: { groupId: string } params: Promise<{ groupId: string }>
}) { }) {
const { groupId } = await params
return ( return (
<CreateExpenseForm <CreateExpenseForm
groupId={groupId} groupId={groupId}

View File

@@ -33,7 +33,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Locale } from '@/i18n' import { Locale } from '@/i18n/request'
import { randomId } from '@/lib/api' import { randomId } from '@/lib/api'
import { defaultCurrencyList, getCurrency } from '@/lib/currency' import { defaultCurrencyList, getCurrency } from '@/lib/currency'
import { RuntimeFeatureFlags } from '@/lib/featureFlags' import { RuntimeFeatureFlags } from '@/lib/featureFlags'

View File

@@ -24,8 +24,9 @@ const prisma = new PrismaClient()
export async function GET( export async function GET(
req: Request, req: Request,
{ params: { groupId } }: { params: { groupId: string } }, { params }: { params: Promise<{ groupId: string }> },
) { ) {
const { groupId } = await params
const group = await prisma.group.findUnique({ const group = await prisma.group.findUnique({
where: { id: groupId }, where: { id: groupId },
select: { select: {

View File

@@ -4,8 +4,9 @@ import { NextResponse } from 'next/server'
export async function GET( export async function GET(
req: Request, req: Request,
{ params: { groupId } }: { params: { groupId: string } }, { params }: { params: Promise<{ groupId: string }> },
) { ) {
const { groupId } = await params
const group = await prisma.group.findUnique({ const group = await prisma.group.findUnique({
where: { id: groupId }, where: { id: groupId },
select: { select: {

View File

@@ -5,10 +5,11 @@ export const metadata: Metadata = {
title: 'Group Information', title: 'Group Information',
} }
export default function InformationPage({ export default async function InformationPage({
params: { groupId }, params,
}: { }: {
params: { groupId: string } params: Promise<{ groupId: string }>
}) { }) {
const { groupId } = await params
return <GroupInformation groupId={groupId} /> return <GroupInformation groupId={groupId} />
} }

View File

@@ -4,14 +4,13 @@ import { PropsWithChildren } from 'react'
import { GroupLayoutClient } from './layout.client' import { GroupLayoutClient } from './layout.client'
type Props = { type Props = {
params: { params: Promise<{
groupId: string groupId: string
} }>
} }
export async function generateMetadata({ export async function generateMetadata({ params }: Props): Promise<Metadata> {
params: { groupId }, const { groupId } = await params
}: Props): Promise<Metadata> {
const group = await cached.getGroup(groupId) const group = await cached.getGroup(groupId)
return { return {
@@ -22,9 +21,10 @@ export async function generateMetadata({
} }
} }
export default function GroupLayout({ export default async function GroupLayout({
children, children,
params: { groupId }, params,
}: PropsWithChildren<Props>) { }: PropsWithChildren<Props>) {
const { groupId } = await params
return <GroupLayoutClient groupId={groupId}>{children}</GroupLayoutClient> return <GroupLayoutClient groupId={groupId}>{children}</GroupLayoutClient>
} }

View File

@@ -1,9 +1,10 @@
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
export default async function GroupPage({ export default async function GroupPage({
params: { groupId }, params,
}: { }: {
params: { groupId: string } params: Promise<{ groupId: string }>
}) { }) {
const { groupId } = await params
redirect(`/groups/${groupId}/expenses`) redirect(`/groups/${groupId}/expenses`)
} }

View File

@@ -33,8 +33,8 @@ export function ReimbursementList({
<div className="flex flex-col gap-1 items-start sm:flex-row sm:items-baseline sm:gap-4"> <div className="flex flex-col gap-1 items-start sm:flex-row sm:items-baseline sm:gap-4">
<div> <div>
{t.rich('owes', { {t.rich('owes', {
from: getParticipant(reimbursement.from)?.name, from: getParticipant(reimbursement.from)?.name ?? '',
to: getParticipant(reimbursement.to)?.name, to: getParticipant(reimbursement.to)?.name ?? '',
strong: (chunks) => <strong>{chunks}</strong>, strong: (chunks) => <strong>{chunks}</strong>,
})} })}
</div> </div>

View File

@@ -11,6 +11,8 @@ import {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger, DialogTrigger,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { ToastAction } from '@/components/ui/toast' import { ToastAction } from '@/components/ui/toast'
@@ -157,6 +159,8 @@ export function DocumentThumbnail({
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="p-4 w-[100vw] max-w-[100vw] h-[100dvh] max-h-[100dvh] sm:max-w-[calc(100vw-32px)] sm:max-h-[calc(100dvh-32px)] [&>:last-child]:hidden"> <DialogContent className="p-4 w-[100vw] max-w-[100vw] h-[100dvh] max-h-[100dvh] sm:max-w-[calc(100vw-32px)] sm:max-h-[calc(100dvh-32px)] [&>:last-child]:hidden">
<DialogTitle className="sr-only">Document</DialogTitle>
<DialogDescription className="sr-only"></DialogDescription>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button

View File

@@ -30,7 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Locale } from '@/i18n' import { Locale } from '@/i18n/request'
import { getGroup } from '@/lib/api' import { getGroup } from '@/lib/api'
import { defaultCurrencyList, getCurrency } from '@/lib/currency' import { defaultCurrencyList, getCurrency } from '@/lib/currency'
import { GroupFormValues, groupFormSchema } from '@/lib/schemas' import { GroupFormValues, groupFormSchema } from '@/lib/schemas'
@@ -65,13 +65,14 @@ export function GroupForm({
? { ? {
name: group.name, name: group.name,
information: group.information ?? '', information: group.information ?? '',
currency: group.currency, currency: group.currency ?? '',
currencyCode: group.currencyCode, currencyCode: group.currencyCode ?? '',
participants: group.participants, participants: group.participants,
} }
: { : {
name: '', name: '',
information: '', information: '',
currency: '',
currencyCode: process.env.NEXT_PUBLIC_DEFAULT_CURRENCY_CODE || 'USD', // TODO: If NEXT_PUBLIC_DEFAULT_CURRENCY_CODE, is not set, determine the default currency code based on locale currencyCode: process.env.NEXT_PUBLIC_DEFAULT_CURRENCY_CODE || 'USD', // TODO: If NEXT_PUBLIC_DEFAULT_CURRENCY_CODE, is not set, determine the default currency code based on locale
participants: [ participants: [
{ name: t('Participants.John') }, { name: t('Participants.John') },

View File

@@ -7,7 +7,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Locale, localeLabels } from '@/i18n' import { Locale, localeLabels } from '@/i18n/request'
import { setUserLocale } from '@/lib/locale' import { setUserLocale } from '@/lib/locale'
import { useLocale } from 'next-intl' import { useLocale } from 'next-intl'

View File

@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
className className
)} )}
{...props} {...props}

View File

@@ -1,6 +1,6 @@
import deepmerge from 'deepmerge' import deepmerge from 'deepmerge'
import { getRequestConfig } from 'next-intl/server' import { getRequestConfig } from 'next-intl/server'
import { getUserLocale } from './lib/locale' import { getUserLocale } from '../lib/locale'
export const localeLabels = { export const localeLabels = {
id: 'Bahasa Indonesia', id: 'Bahasa Indonesia',
@@ -37,14 +37,14 @@ export const defaultLocale: Locale = 'en-US'
export default getRequestConfig(async () => { export default getRequestConfig(async () => {
const locale = await getUserLocale() const locale = await getUserLocale()
const localeMessages = (await import(`../messages/${locale}.json`)).default const localeMessages = (await import(`../../messages/${locale}.json`)).default
let messages: any let messages: any
if (locale === defaultLocale) { if (locale === defaultLocale) {
messages = localeMessages messages = localeMessages
} else { } else {
messages = deepmerge( messages = deepmerge(
(await import(`../messages/${defaultLocale}.json`)).default, (await import(`../../messages/${defaultLocale}.json`)).default,
localeMessages, localeMessages,
) as any ) as any
} }

View File

@@ -1,4 +1,4 @@
import { Locale } from '@/i18n' import { Locale } from '@/i18n/request'
import currencyList from './currency-data.json' import currencyList from './currency-data.json'
export type Currency = { export type Currency = {

View File

@@ -1,6 +1,6 @@
'use server' 'use server'
import { Locale, Locales, defaultLocale, locales } from '@/i18n' import { Locale, Locales, defaultLocale, locales } from '@/i18n/request'
import { match } from '@formatjs/intl-localematcher' import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator' import Negotiator from 'negotiator'
import { cookies, headers } from 'next/headers' import { cookies, headers } from 'next/headers'
@@ -27,17 +27,17 @@ export async function getUserLocale() {
let locale let locale
// Prio 1: use existing cookie // Prio 1: use existing cookie
locale = cookies().get(COOKIE_NAME)?.value locale = (await cookies()).get(COOKIE_NAME)?.value
// Prio 2: use `accept-language` header // Prio 2: use `accept-language` header
// Prio 3: use default locale // Prio 3: use default locale
if (!locale) { if (!locale) {
locale = getAcceptLanguageLocale(headers(), locales) locale = getAcceptLanguageLocale(await headers(), locales)
} }
return locale return locale
} }
export async function setUserLocale(locale: Locale) { export async function setUserLocale(locale: Locale) {
cookies().set(COOKIE_NAME, locale) ;(await cookies()).set(COOKIE_NAME, locale)
} }

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import { Locale, locales } from '@/i18n' import { Locale, locales } from '@/i18n/request'
import { import {
Currency, Currency,
supportedCurrencyCodeType, supportedCurrencyCodeType,

View File

@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -19,13 +23,26 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"], "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
],
"ts-node": { "ts-node": {
"require": ["tsconfig-paths/register", "dotenv/config"], "require": [
"tsconfig-paths/register",
"dotenv/config"
],
"compilerOptions": { "compilerOptions": {
"isolatedModules": false, "isolatedModules": false,
"moduleResolution": "nodenext", "moduleResolution": "nodenext",