From 209b66919192f48b3fcb4e1dcfaa92e35a36ee72 Mon Sep 17 00:00:00 2001 From: krokosik Date: Thu, 6 Feb 2025 11:45:26 +0100 Subject: [PATCH 01/30] Add a simplify debts field to groups --- .../migrations/20250206103642_add_simplify_debts/migration.sql | 2 ++ prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 prisma/migrations/20250206103642_add_simplify_debts/migration.sql diff --git a/prisma/migrations/20250206103642_add_simplify_debts/migration.sql b/prisma/migrations/20250206103642_add_simplify_debts/migration.sql new file mode 100644 index 0000000..5f6c3ff --- /dev/null +++ b/prisma/migrations/20250206103642_add_simplify_debts/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Group" ADD COLUMN "simplifyDebts" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aa8912f..d671233 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -100,6 +100,7 @@ model Group { groupUsers GroupUser[] expenses Expense[] groupBalances GroupBalance[] + simplifyDebts Boolean @default(false) } model GroupUser { From cbd67de7efb4a037db64ce63eec2296a9e68c22f Mon Sep 17 00:00:00 2001 From: krokosik Date: Thu, 6 Feb 2025 11:45:46 +0100 Subject: [PATCH 02/30] Do not return undefined in trpc query to silence console error --- src/server/api/routers/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index bffdd09..86258bc 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -518,6 +518,6 @@ export const userRouter = createTRPCRouter({ }), getWebPushPublicKey: protectedProcedure.query(async ({ ctx }) => { - return env.WEB_PUSH_PUBLIC_KEY; + return env.WEB_PUSH_PUBLIC_KEY ?? ''; }), }); From 1a7e9b2c623beca3668a1af8855ae5f71b8362e5 Mon Sep 17 00:00:00 2001 From: krokosik Date: Thu, 6 Feb 2025 12:07:43 +0100 Subject: [PATCH 03/30] Add a switch component and add now required shadcn aliases --- components.json | 5 +- package.json | 1 + pnpm-lock.yaml | 161 +++++++++++++++++++++++++++++++++++ src/components/ui/switch.tsx | 27 ++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/switch.tsx diff --git a/components.json b/components.json index 5d259f9..8a52d37 100644 --- a/components.json +++ b/components.json @@ -12,6 +12,9 @@ }, "aliases": { "components": "~/components", - "utils": "~/lib/utils" + "utils": "~/lib/utils", + "ui": "~/components/ui", + "hooks": "~/hooks", + "lib": "~/lib" } } \ No newline at end of file diff --git a/package.json b/package.json index 726eaf1..c19c4a2 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^4.36.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c615858..7bdf69d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-switch': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) @@ -2866,6 +2869,10 @@ packages: '@babel/runtime': 7.23.8 dev: false + /@radix-ui/primitive@1.1.1: + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + dev: false + /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} peerDependencies: @@ -3012,6 +3019,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-compose-refs@1.1.1(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-context@1.0.0(react@18.2.0): resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} peerDependencies: @@ -3035,6 +3055,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-context@1.1.1(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-dialog@1.0.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} peerDependencies: @@ -3423,6 +3456,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-primitive@2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -3498,6 +3551,46 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-slot@1.1.2(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + + /@radix-ui/react-switch@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: @@ -3549,6 +3642,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-use-controllable-state@1.0.0(react@18.2.0): resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==} peerDependencies: @@ -3574,6 +3680,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-use-escape-keydown@1.0.0(react@18.2.0): resolution: {integrity: sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==} peerDependencies: @@ -3622,6 +3742,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} peerDependencies: @@ -3636,6 +3769,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-previous@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -3666,6 +3812,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-size@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: @@ -4781,6 +4941,7 @@ packages: /acorn-import-assertions@1.9.0(acorn@8.11.3): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 dependencies: diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..b6a7bc3 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "~/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } From 3f154e8dce6e6a09ea98c0e3337569c9c1b36ef9 Mon Sep 17 00:00:00 2001 From: krokosik Date: Thu, 6 Feb 2025 12:15:08 +0100 Subject: [PATCH 04/30] Add a simplify debts toggle to UI --- src/pages/groups/[groupId].tsx | 25 ++++++++++++++++++++ src/server/api/routers/group.ts | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 8e2e8e4..7c21e66 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -15,6 +15,7 @@ import { UserPlus, BarChartHorizontal, Info, + Merge, } from 'lucide-react'; import { AppDrawer } from '~/components/ui/drawer'; import { UserAvatar } from '~/components/ui/avatar'; @@ -41,6 +42,7 @@ import { } from '~/components/ui/alert-dialog'; import { toast } from 'sonner'; import GroupMyBalance from '~/components/group/GroupMyBalance'; +import { Switch } from '~/components/ui/switch'; const BalancePage: NextPageWithUser<{ enableSendingInvites: boolean; @@ -53,6 +55,7 @@ const BalancePage: NextPageWithUser<{ const expensesQuery = api.group.getExpenses.useQuery({ groupId }); const deleteGroupMutation = api.group.delete.useMutation(); const leaveGroupMutation = api.group.leaveGroup.useMutation(); + const toggleSimplifyDebtsMutation = api.group.toggleSimplifyDebts.useMutation(); const [isInviteCopied, setIsInviteCopied] = useState(false); const [showDeleteTrigger, setShowDeleteTrigger] = useState(false); @@ -208,6 +211,28 @@ const BalancePage: NextPageWithUser<{

Actions

+
+ + { + toggleSimplifyDebtsMutation.mutate( + { groupId }, + { + onSuccess: () => { + groupDetailQuery.refetch(); + }, + onError: () => { + toast.error('Failed to update setting'); + }, + }, + ); + }} + /> +
{isAdmin ? ( <> { + const group = await ctx.db.group.findUnique({ + where: { + id: input.groupId, + }, + }); + + if (!group) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Group not found' }); + } + + const isInGroup = await ctx.db.groupUser.findFirst({ + where: { + groupId: input.groupId, + userId: ctx.session.user.id, + }, + }); + + if (!isInGroup) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Only group members can toggle Simplify Debts', + }); + } + + const simplifyDebts = !group.simplifyDebts; + + await ctx.db.group.update({ + where: { + id: input.groupId, + }, + data: { + simplifyDebts, + }, + }); + + return simplifyDebts; + }), + leaveGroup: groupProcedure .input(z.object({ groupId: z.number() })) .mutation(async ({ input, ctx }) => { From 17227f4880933153dc4b827cbc7368b8d34d11eb Mon Sep 17 00:00:00 2001 From: krokosik Date: Wed, 12 Feb 2025 18:19:29 +0100 Subject: [PATCH 05/30] Fix list key errors on group page --- src/components/group/GroupMyBalance.tsx | 12 ++++++------ src/pages/groups/[groupId].tsx | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/group/GroupMyBalance.tsx b/src/components/group/GroupMyBalance.tsx index 5cfdd09..f29f579 100644 --- a/src/components/group/GroupMyBalance.tsx +++ b/src/components/group/GroupMyBalance.tsx @@ -38,12 +38,12 @@ const GroupMyBalance: React.FC = ({ userId, groupBalances, You lent {youLent.map(([currency, amount], index, arr) => { return ( - <> -
+ +
{currency} {toUIString(amount)}
{index < arr.length - 1 ? + : null} - +
); })}
@@ -54,12 +54,12 @@ const GroupMyBalance: React.FC = ({ userId, groupBalances, You owe {youOwe.map(([currency, amount], index, arr) => { return ( - <> -
+ +
{currency} {toUIString(amount)}
{index < arr.length - 1 ? + : null} - +
); })}
diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 7c21e66..909e788 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -27,7 +27,7 @@ import { toUIString } from '~/utils/numbers'; import Link from 'next/link'; import { CategoryIcon } from '~/components/ui/categoryIcons'; import { env } from '~/env'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { type NextPageWithUser } from '~/types'; import { motion } from 'framer-motion'; import { @@ -159,12 +159,12 @@ const BalancePage: NextPageWithUser<{
{groupTotalQuery.data?.map((total, index, arr) => { return total._sum.amount != null ? ( - <> -
+ +
{total.currency} {toUIString(total._sum.amount)}
{index < arr.length - 1 ? + : null} - +
) : null; })}
From 13cf771973221f464cfd87de3d89173b7be45d6c Mon Sep 17 00:00:00 2001 From: krokosik Date: Wed, 12 Feb 2025 18:39:44 +0100 Subject: [PATCH 06/30] Show per group member balances --- src/components/group/GroupMyBalance.tsx | 36 +++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/components/group/GroupMyBalance.tsx b/src/components/group/GroupMyBalance.tsx index f29f579..4020f6f 100644 --- a/src/components/group/GroupMyBalance.tsx +++ b/src/components/group/GroupMyBalance.tsx @@ -17,13 +17,27 @@ const GroupMyBalance: React.FC = ({ userId, groupBalances, {} as Record, ); - const cumulatedBalances = groupBalances.reduce( + const friendBalances = groupBalances.reduce( (acc, balance) => { if (balance.userId === userId && Math.abs(balance.amount) > 0) { - acc[balance.currency] = (acc[balance.currency] ?? 0) + balance.amount; + if (!acc[balance.firendId]) { + acc[balance.firendId] = {}; + } + const friendBalance = acc[balance.firendId]!; + friendBalance[balance.currency] = (friendBalance[balance.currency] ?? 0) + balance.amount; } return acc; }, + {} as Record>, + ); + + const cumulatedBalances = Object.values(friendBalances).reduce( + (acc, balances) => { + Object.entries(balances).forEach(([currency, amount]) => { + acc[currency] = (acc[currency] ?? 0) + amount; + }); + return acc; + }, {} as Record, ); @@ -64,6 +78,24 @@ const GroupMyBalance: React.FC = ({ userId, groupBalances, })}
) : null} + + {youLent.length === 0 && youOwe.length === 0 ? ( +
You are all settled up
+ ) : null} + + {Object.entries(friendBalances).map(([friendId, balances]) => { + const friend = userMap[+friendId]; + return ( +
+ {Object.entries(balances).map(([currency, amount]) => ( +
+ {amount > 0 ? `${friend?.name} owes you` : `You owe ${friend?.name}`}{' '} + {toUIString(Math.abs(amount))} {currency} +
+ ))} +
+ ); + })}
); From 07128bd2100cccbda87c1adf55c4fdc208026ec1 Mon Sep 17 00:00:00 2001 From: krokosik Date: Wed, 12 Feb 2025 18:54:46 +0100 Subject: [PATCH 07/30] Process group balances with useMemo --- src/pages/groups/[groupId].tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 909e788..fd4e745 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -27,7 +27,7 @@ import { toUIString } from '~/utils/numbers'; import Link from 'next/link'; import { CategoryIcon } from '~/components/ui/categoryIcons'; import { env } from '~/env'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { type NextPageWithUser } from '~/types'; import { motion } from 'framer-motion'; import { @@ -61,6 +61,16 @@ const BalancePage: NextPageWithUser<{ const [showDeleteTrigger, setShowDeleteTrigger] = useState(false); const [showLeaveTrigger, setShowLeaveTrigger] = useState(false); + const groupBalances = useMemo(() => { + if (!groupDetailQuery.data) { + return []; + } else if (!groupDetailQuery.data.simplifyDebts) { + return groupDetailQuery.data.groupBalances; + } else { + return []; + } + }, [groupDetailQuery.data]); + async function inviteMembers() { if (!groupDetailQuery.data) return; const inviteLink = @@ -86,11 +96,8 @@ const BalancePage: NextPageWithUser<{ const isAdmin = groupDetailQuery.data?.userId === user.id; const canDelete = - groupDetailQuery.data?.userId === user.id && - !groupDetailQuery.data.groupBalances.find((b) => b.amount !== 0); - const canLeave = !groupDetailQuery.data?.groupBalances.find( - (b) => b.amount !== 0 && b.userId === user.id, - ); + groupDetailQuery.data?.userId === user.id && !groupBalances.find((b) => b.amount !== 0); + const canLeave = !groupBalances.find((b) => b.amount !== 0 && b.userId === user.id); function onGroupDelete() { deleteGroupMutation.mutate( @@ -358,7 +365,7 @@ const BalancePage: NextPageWithUser<{
gu.user) ?? []} />
From b4bfe7e528c03c837cf9cea6cf082690f11444b1 Mon Sep 17 00:00:00 2001 From: krokosik Date: Thu, 13 Feb 2025 18:49:05 +0100 Subject: [PATCH 08/30] Add dinic algorithm package --- package.json | 1 + pnpm-lock.yaml | 13 +++++++++++++ src/lib/simplify.ts | 9 +++++++++ 3 files changed, 23 insertions(+) create mode 100644 src/lib/simplify.ts diff --git a/package.json b/package.json index c19c4a2..2d666e4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dx:down": "docker compose -f docker/dev/compose.yml down" }, "dependencies": { + "@algorithm.ts/dinic": "^4.0.4", "@aws-sdk/client-s3": "^3.515.0", "@aws-sdk/s3-request-presigner": "^3.515.0", "@heroicons/react": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bdf69d..9c0fa4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@algorithm.ts/dinic': + specifier: ^4.0.4 + version: 4.0.4 '@aws-sdk/client-s3': specifier: ^3.515.0 version: 3.515.0 @@ -234,6 +237,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@algorithm.ts/dinic@4.0.4: + resolution: {integrity: sha512-NH681jIFJNsAkDVWjtmu//LcBkVWGpx+ztdgl3Pdci32kxnH2HXUyMT53nVpowwXvXIzQvLo7hnAKE+qif5Rmw==} + dependencies: + '@algorithm.ts/queue': 4.0.4 + dev: false + + /@algorithm.ts/queue@4.0.4: + resolution: {integrity: sha512-o0Q/3TUWz3GeX7sEyMNx5yaRJ3BA+C8u15CjhSrm+lYhDc666VPH2OC6Q8QqhD+5teNqx/L0iy3mmDVZbU8P5Q==} + dev: false + /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts new file mode 100644 index 0000000..8fc3e30 --- /dev/null +++ b/src/lib/simplify.ts @@ -0,0 +1,9 @@ +import { GroupBalance } from '@prisma/client'; +import { Dinic } from '@algorithm.ts/dinic'; + +// Based on https://github.com/mithun-mohan/Algorithms-Java-Cookbook/blob/master/MaximumFlow/Dinics/SimplifyDebts.java +export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { + const dinic = new Dinic(); + + return []; +} From b849d7daf7e04816efaf3c0aac188e1c3aec3570 Mon Sep 17 00:00:00 2001 From: krokosik Date: Thu, 13 Feb 2025 19:14:04 +0100 Subject: [PATCH 09/30] Remove the client side simplify stub, as it should be done server side --- src/pages/groups/[groupId].tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index fd4e745..9d02ab1 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -61,16 +61,6 @@ const BalancePage: NextPageWithUser<{ const [showDeleteTrigger, setShowDeleteTrigger] = useState(false); const [showLeaveTrigger, setShowLeaveTrigger] = useState(false); - const groupBalances = useMemo(() => { - if (!groupDetailQuery.data) { - return []; - } else if (!groupDetailQuery.data.simplifyDebts) { - return groupDetailQuery.data.groupBalances; - } else { - return []; - } - }, [groupDetailQuery.data]); - async function inviteMembers() { if (!groupDetailQuery.data) return; const inviteLink = @@ -96,8 +86,11 @@ const BalancePage: NextPageWithUser<{ const isAdmin = groupDetailQuery.data?.userId === user.id; const canDelete = - groupDetailQuery.data?.userId === user.id && !groupBalances.find((b) => b.amount !== 0); - const canLeave = !groupBalances.find((b) => b.amount !== 0 && b.userId === user.id); + groupDetailQuery.data?.userId === user.id && + !groupDetailQuery.data?.groupBalances.find((b) => b.amount !== 0); + const canLeave = !groupDetailQuery.data?.groupBalances.find( + (b) => b.amount !== 0 && b.userId === user.id, + ); function onGroupDelete() { deleteGroupMutation.mutate( @@ -365,7 +358,7 @@ const BalancePage: NextPageWithUser<{
gu.user) ?? []} />
From 2a7498129a718af85a5725c1693ad2e4dc435a33 Mon Sep 17 00:00:00 2001 From: krokosik Date: Sun, 16 Feb 2025 12:11:16 +0100 Subject: [PATCH 10/30] Move expense list to a shared component --- src/components/Expense/ExpenseList.tsx | 80 +++++++++++++++++++++++++ src/pages/balances/[friendId].tsx | 70 +++------------------- src/pages/groups/[groupId].tsx | 82 ++++++-------------------- src/server/api/routers/group.ts | 2 + src/server/api/routers/user.ts | 3 + 5 files changed, 109 insertions(+), 128 deletions(-) create mode 100644 src/components/Expense/ExpenseList.tsx diff --git a/src/components/Expense/ExpenseList.tsx b/src/components/Expense/ExpenseList.tsx new file mode 100644 index 0000000..06aa30f --- /dev/null +++ b/src/components/Expense/ExpenseList.tsx @@ -0,0 +1,80 @@ +import { SplitType } from '@prisma/client'; +import { inferRouterOutputs } from '@trpc/server'; +import { format } from 'date-fns'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; +import { CategoryIcon } from '~/components/ui/categoryIcons'; +import { GroupRouter } from '~/server/api/routers/group'; +import { UserRouter } from '~/server/api/routers/user'; +import { toUIString } from '~/utils/numbers'; + +export const ExpenseList: React.FC<{ + userId: number; + expenses: + | inferRouterOutputs['getExpenses'] + | inferRouterOutputs['getExpensesWithFriend']; + contactId: string; + isLoading?: boolean; +}> = ({ userId, expenses, contactId, isLoading }) => ( + <> + {expenses.map((e) => { + const youPaid = e.paidBy === userId; + const yourExpense = e.expenseParticipants.find((p) => p.userId === userId); + const isSettlement = e.splitType === SplitType.SETTLEMENT; + const yourExpenseAmount = youPaid ? yourExpense?.amount ?? 0 : -(yourExpense?.amount ?? 0); + + return ( + +
+
+ {format(e.expenseDate, 'MMM dd') + .split(' ') + .map((d) => ( +
+ {d} +
+ ))} +
+
+ +
+
+ {!isSettlement ? ( +

{e.name}

+ ) : null} +

+ {isSettlement ? ' 🎉 ' : null} + {youPaid ? 'You' : e.paidByUser.name ?? e.paidByUser.email} paid {e.currency}{' '} + {toUIString(e.amount)}{' '} +

+
+
+ {isSettlement ? null : ( +
+
+ {youPaid ? 'You lent' : 'You owe'} +
+
+ {e.currency} {toUIString(yourExpenseAmount)} +
+
+ )} + + ); + })} + {expenses.length === 0 && !isLoading ? ( +
+ Empty +
+ ) : null} + +); diff --git a/src/pages/balances/[friendId].tsx b/src/pages/balances/[friendId].tsx index e8fbdcb..d7a891f 100644 --- a/src/pages/balances/[friendId].tsx +++ b/src/pages/balances/[friendId].tsx @@ -16,6 +16,7 @@ import { type NextPageWithUser } from '~/types'; import { motion } from 'framer-motion'; import { DeleteFriend } from '~/components/Friend/DeleteFriend'; import { Export } from '~/components/Friend/Export'; +import { ExpenseList } from '~/components/Expense/ExpenseList'; const FriendPage: NextPageWithUser = ({ user }) => { const router = useRouter(); @@ -154,69 +155,12 @@ const FriendPage: NextPageWithUser = ({ user }) => {
- {expenses.data?.map((e) => { - const youPaid = e.paidBy === user.id; - const yourExpense = e.expenseParticipants.find( - (p) => p.userId === (youPaid ? friendQuery.data?.id : user.id), - ); - const isSettlement = e.splitType === SplitType.SETTLEMENT; - - return ( - -
-
- {format(e.expenseDate, 'MMM dd') - .split(' ') - .map((d) => ( -
- {d} -
- ))} -
-
- -
-
- {!isSettlement ? ( -

- {e.name} -

- ) : null} -

- - {isSettlement ? ' 🎉 ' : null} - - - {youPaid ? 'You' : friendQuery.data?.name} paid {e.currency}{' '} - {toUIString(e.amount)}{' '} - -

-
-
- {isSettlement ? null : ( -
-
- {youPaid ? 'You lent' : 'You owe'} -
-
- {e.currency}{' '} - {toUIString(yourExpense?.amount ?? 0)} -
-
- )} - - ); - })} +
)} diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 9d02ab1..2a9f769 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -43,6 +43,8 @@ import { import { toast } from 'sonner'; import GroupMyBalance from '~/components/group/GroupMyBalance'; import { Switch } from '~/components/ui/switch'; +import { ExpenseList } from '~/components/Expense/ExpenseList'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs'; const BalancePage: NextPageWithUser<{ enableSendingInvites: boolean; @@ -397,71 +399,21 @@ const BalancePage: NextPageWithUser<{ )} - {expensesQuery.data?.map((e) => { - const youPaid = e.paidBy === user.id; - const yourExpense = e.expenseParticipants.find((p) => p.userId === user.id); - const isSettlement = e.splitType === SplitType.SETTLEMENT; - const yourExpenseAmount = youPaid - ? yourExpense?.amount ?? 0 - : -(yourExpense?.amount ?? 0); - - return ( - -
-
- {format(e.expenseDate, 'MMM dd') - .split(' ') - .map((d) => ( -
- {d} -
- ))} -
-
- -
-
- {!isSettlement ? ( -

- {e.name} -

- ) : null} -

- {isSettlement ? ' 🎉 ' : null} - {youPaid ? 'You' : e.paidByUser.name ?? e.paidByUser.email} paid {e.currency}{' '} - {toUIString(e.amount)}{' '} -

-
-
- {isSettlement ? null : ( -
-
- {youPaid ? 'You lent' : 'You owe'} -
-
- {e.currency}{' '} - {toUIString(yourExpenseAmount)} -
-
- )} - - ); - })} - {expensesQuery.data?.length === 0 && - !groupDetailQuery.isLoading && - groupDetailQuery.data?.groupUsers.length !== 1 ? ( -
- Empty -
- ) : null} + + + Expenses + Balances + + + + + + ); diff --git a/src/server/api/routers/group.ts b/src/server/api/routers/group.ts index 0ebfbe8..e592098 100644 --- a/src/server/api/routers/group.ts +++ b/src/server/api/routers/group.ts @@ -392,3 +392,5 @@ export const groupRouter = createTRPCRouter({ return group; }), }); + +export type GroupRouter = typeof groupRouter; diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 86258bc..4b0fb4c 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -263,6 +263,7 @@ export const userRouter = createTRPCRouter({ ], }, }, + paidByUser: true, }, }); @@ -521,3 +522,5 @@ export const userRouter = createTRPCRouter({ return env.WEB_PUSH_PUBLIC_KEY ?? ''; }), }); + +export type UserRouter = typeof userRouter; From 34bf1f9cb42176763e043d242f8db579a7ba553e Mon Sep 17 00:00:00 2001 From: krokosik Date: Sun, 16 Feb 2025 13:35:55 +0100 Subject: [PATCH 11/30] Add accordion component --- package.json | 1 + pnpm-lock.yaml | 129 +++++++++++++++++++++++++++ src/components/ui/accordion.tsx | 56 ++++++++++++ tailwind.config.ts | 152 +++++++++++++++++--------------- 4 files changed, 266 insertions(+), 72 deletions(-) create mode 100644 src/components/ui/accordion.tsx diff --git a/package.json b/package.json index 2d666e4..b2ab7bd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@hookform/resolvers": "^3.3.4", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.9.1", + "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c0fa4c..ca76e8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: '@prisma/client': specifier: ^5.9.1 version: 5.9.1(prisma@5.9.1) + '@radix-ui/react-accordion': + specifier: ^1.2.3 + version: 1.2.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) @@ -2886,6 +2889,34 @@ packages: resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} dev: false + /@radix-ui/react-accordion@1.2.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collapsible': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} peerDependencies: @@ -2985,6 +3016,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collapsible@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: @@ -3009,6 +3067,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collection@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.2(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-compose-refs@1.0.0(react@18.2.0): resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} peerDependencies: @@ -3156,6 +3237,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-direction@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==} peerDependencies: @@ -3282,6 +3376,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-id@1.1.0(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + react: 18.2.0 + dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} peerDependencies: @@ -3436,6 +3544,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-presence@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..3d5eeca --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "~/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/tailwind.config.ts b/tailwind.config.ts index f254a99..4ee9bc5 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -10,78 +10,86 @@ const config = { ], prefix: '', theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px', - }, - }, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - keyframes: { - 'accordion-down': { - from: { height: '0' }, - to: { height: 'var(--radix-accordion-content-height)' }, - }, - 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: '0' }, - }, - 'caret-blink': { - '0%,70%,100%': { opacity: '1' }, - '20%,50%': { opacity: '0' }, - }, - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - 'caret-blink': 'caret-blink 1.25s ease-out infinite', - }, - // fontFamily: { - // sans: ["var(--font-geist-sans)"], - // mono: ["var(--font-geist-mono)"], - // }, - }, + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + 'caret-blink': { + '0%,70%,100%': { + opacity: '1' + }, + '20%,50%': { + opacity: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'caret-blink': 'caret-blink 1.25s ease-out infinite' + } + } }, plugins: [require('tailwindcss-animate')], } satisfies Config; From ad1b15f296ee15d40c21483093a39606966a1b8c Mon Sep 17 00:00:00 2001 From: krokosik Date: Sun, 16 Feb 2025 13:36:43 +0100 Subject: [PATCH 12/30] Balance list with totals --- src/components/Expense/BalanceList.tsx | 87 ++++++++++++++++++++++++++ src/pages/groups/[groupId].tsx | 10 ++- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/components/Expense/BalanceList.tsx diff --git a/src/components/Expense/BalanceList.tsx b/src/components/Expense/BalanceList.tsx new file mode 100644 index 0000000..4f6eaca --- /dev/null +++ b/src/components/Expense/BalanceList.tsx @@ -0,0 +1,87 @@ +import { GroupBalance, User } from '@prisma/client'; +import Link from 'next/link'; +import { UserAvatar } from '~/components/ui/avatar'; +import clsx from 'clsx'; +import { toUIString } from '~/utils/numbers'; +import { Accordion, AccordionItem, AccordionTrigger } from '../ui/accordion'; + +interface UserWithBalance { + user: User; + total: Record; + balances: { + [friendId: number]: Record; + }; +} + +export const BalanceList: React.FC<{ + balances: GroupBalance[]; + users: User[]; + userId: number; + isLoading?: boolean; +}> = ({ balances, users, userId, isLoading }) => { + const userMap = users.reduce( + (acc, user) => { + acc[user.id] = { user, balances: {}, total: {} }; + return acc; + }, + {} as Record, + ); + + balances + .filter(({ amount }) => Math.abs(amount) > 0) + .forEach((balance) => { + if (!userMap[balance.userId]!.balances[balance.firendId]) { + userMap[balance.userId]!.balances[balance.firendId] = {}; + } + const friendBalance = userMap[balance.userId]!.balances[balance.firendId]!; + friendBalance[balance.currency] = (friendBalance[balance.currency] ?? 0) + balance.amount; + + userMap[balance.userId]!.total[balance.currency] = + (userMap[balance.userId]!.total[balance.currency] ?? 0) + balance.amount; + }); + + return ( + + {Object.values(userMap).map(({ user, total }) => { + let totalAmount: [string, number] = ['', 0]; + + Object.entries(total).forEach(([currency, amount]) => { + if (Math.abs(amount) > Math.abs(totalAmount[1])) { + totalAmount = [currency, amount]; + } + }); + + return ( + + +
+ +
+ {user.name ?? user.email} + {Object.values(total).every((amount) => amount === 0) ? ( + is settled up + ) : ( + <> + + {' '} + {totalAmount[1] > 0 ? 'gets back' : 'owes'}{' '} + + 0 ? 'text-emerald-500' : 'text-orange-600', + )} + > + {toUIString(totalAmount[1])} {totalAmount[0]} + + + )} +
+
+
+
+ ); + })} +
+ ); +}; diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 2a9f769..f928a2b 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -45,6 +45,7 @@ import GroupMyBalance from '~/components/group/GroupMyBalance'; import { Switch } from '~/components/ui/switch'; import { ExpenseList } from '~/components/Expense/ExpenseList'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs'; +import { BalanceList } from '~/components/Expense/BalanceList'; const BalancePage: NextPageWithUser<{ enableSendingInvites: boolean; @@ -412,7 +413,14 @@ const BalancePage: NextPageWithUser<{ isLoading={expensesQuery.isLoading} /> - + + gu.user) ?? []} + userId={user.id} + isLoading={groupDetailQuery.isLoading} + /> + From 15964098ecb2f62a2265ee1e9ba85c3354e9ca9a Mon Sep 17 00:00:00 2001 From: krokosik Date: Sun, 16 Feb 2025 13:37:07 +0100 Subject: [PATCH 13/30] Use number type for group/friend id in ExpenseList --- src/components/Expense/ExpenseList.tsx | 2 +- src/pages/balances/[friendId].tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Expense/ExpenseList.tsx b/src/components/Expense/ExpenseList.tsx index 06aa30f..f0c0943 100644 --- a/src/components/Expense/ExpenseList.tsx +++ b/src/components/Expense/ExpenseList.tsx @@ -14,7 +14,7 @@ export const ExpenseList: React.FC<{ expenses: | inferRouterOutputs['getExpenses'] | inferRouterOutputs['getExpensesWithFriend']; - contactId: string; + contactId: number; isLoading?: boolean; }> = ({ userId, expenses, contactId, isLoading }) => ( <> diff --git a/src/pages/balances/[friendId].tsx b/src/pages/balances/[friendId].tsx index d7a891f..1faed5c 100644 --- a/src/pages/balances/[friendId].tsx +++ b/src/pages/balances/[friendId].tsx @@ -157,7 +157,7 @@ const FriendPage: NextPageWithUser = ({ user }) => {
From f549b379324b1525de6c15751f9f3510a94ddc50 Mon Sep 17 00:00:00 2001 From: krokosik Date: Sun, 16 Feb 2025 13:49:19 +0100 Subject: [PATCH 14/30] Complete BalanceList --- src/components/Expense/BalanceList.tsx | 40 ++++++++++++++++++++++---- src/pages/groups/[groupId].tsx | 2 -- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/components/Expense/BalanceList.tsx b/src/components/Expense/BalanceList.tsx index 4f6eaca..f4f78fc 100644 --- a/src/components/Expense/BalanceList.tsx +++ b/src/components/Expense/BalanceList.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import { UserAvatar } from '~/components/ui/avatar'; import clsx from 'clsx'; import { toUIString } from '~/utils/numbers'; -import { Accordion, AccordionItem, AccordionTrigger } from '../ui/accordion'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../ui/accordion'; interface UserWithBalance { user: User; @@ -16,9 +16,7 @@ interface UserWithBalance { export const BalanceList: React.FC<{ balances: GroupBalance[]; users: User[]; - userId: number; - isLoading?: boolean; -}> = ({ balances, users, userId, isLoading }) => { +}> = ({ balances, users }) => { const userMap = users.reduce( (acc, user) => { acc[user.id] = { user, balances: {}, total: {} }; @@ -42,7 +40,7 @@ export const BalanceList: React.FC<{ return ( - {Object.values(userMap).map(({ user, total }) => { + {Object.values(userMap).map(({ user, total, balances }) => { let totalAmount: [string, number] = ['', 0]; Object.entries(total).forEach(([currency, amount]) => { @@ -79,6 +77,38 @@ export const BalanceList: React.FC<{
+ + {Object.entries(balances).map(([friendId, perFriendBalances]) => { + const friend = userMap[+friendId]!.user; + + return ( + <> + {Object.entries(perFriendBalances).map(([currency, amount]) => ( +
+ +
+ {friend.name ?? friend.email} + + {' '} + {amount > 0 ? 'owes' : 'gets back'}{' '} + + 0 ? 'text-emerald-500' : 'text-orange-600', + )} + > + {toUIString(amount)} {currency} + + {amount > 0 ? 'to' : 'from'} + {user.name ?? user.email} +
+
+ ))} + + ); + })} +
); })} diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index f928a2b..9ecc9e0 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -417,8 +417,6 @@ const BalancePage: NextPageWithUser<{ gu.user) ?? []} - userId={user.id} - isLoading={groupDetailQuery.isLoading} /> From be5f3b1bda767f99b6a0401a1285df1b6a47ac44 Mon Sep 17 00:00:00 2001 From: krokosik Date: Mon, 17 Feb 2025 21:04:23 +0100 Subject: [PATCH 15/30] Add Jest and move dev dependencies to proper dep group --- jest.config.ts | 205 +++++ package.json | 18 +- pnpm-lock.yaml | 2200 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 2282 insertions(+), 141 deletions(-) create mode 100644 jest.config.ts diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..ce2d170 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,205 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from 'jest'; +import nextJest from 'next/jest.js'; + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './', +}); + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "C:\\Users\\Wiktor\\AppData\\Local\\Temp\\jest", + + // Automatically clear mock calls, instances, contexts and results before every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + // coverageDirectory: undefined, + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "\\\\node_modules\\\\" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'v8', + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: 'jsdom', + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "\\\\node_modules\\\\" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "\\\\node_modules\\\\", + // "\\.pnp\\.[^\\\\]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default createJestConfig(config); diff --git a/package.json b/package.json index b2ab7bd..41f058c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "d": "pnpm dx && pnpm dev", "dx": "pnpm i && pnpm dx:up && pnpm db:dev", "dx:up": "docker compose -f docker/dev/compose.yml up -d", - "dx:down": "docker compose -f docker/dev/compose.yml down" + "dx:down": "docker compose -f docker/dev/compose.yml down", + "test": "jest", + "test:watch": "jest --watch" }, "dependencies": { "@algorithm.ts/dinic": "^4.0.4", @@ -47,10 +49,8 @@ "@trpc/next": "^10.43.6", "@trpc/react-query": "^10.43.6", "@trpc/server": "^10.43.6", - "babel-loader": "^9.1.3", "boring-avatars": "^1.10.1", "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", "cmdk": "^0.2.0", "date-fns": "^3.3.1", "framer-motion": "^11.0.3", @@ -73,8 +73,6 @@ "sharp": "0.32.6", "sonner": "^1.4.0", "superjson": "^2.2.1", - "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7", "vaul": "^0.8.9", "web-push": "^3.6.7", "zod": "^3.22.4", @@ -82,6 +80,7 @@ }, "devDependencies": { "@types/eslint": "^8.44.7", + "@types/jest": "^29.5.14", "@types/next-pwa": "^5.6.9", "@types/node": "^18.17.0", "@types/nodemailer": "^6.4.15", @@ -91,13 +90,20 @@ "@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/parser": "^6.11.0", "autoprefixer": "^10.4.14", + "babel-loader": "^9.1.3", + "clsx": "^2.1.0", "eslint": "^8.54.0", "eslint-config-next": "^14.0.4", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8.4.31", "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.7", "prisma": "^5.9.1", + "tailwind-merge": "^2.2.0", "tailwindcss": "^3.3.5", + "tailwindcss-animate": "^1.0.7", + "ts-node": "^10.9.2", "tsx": "^4.7.1", "typescript": "^5.1.6" }, @@ -108,4 +114,4 @@ "seed": "tsx prisma/seed.ts" }, "packageManager": "pnpm@8.9.2" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca76e8a..cc4d88d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,18 +77,12 @@ dependencies: '@trpc/server': specifier: ^10.43.6 version: 10.45.0 - babel-loader: - specifier: ^9.1.3 - version: 9.1.3(@babel/core@7.23.9)(webpack@5.90.1) boring-avatars: specifier: ^1.10.1 version: 1.10.1 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 - clsx: - specifier: ^2.1.0 - version: 2.1.0 cmdk: specifier: ^0.2.0 version: 0.2.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) @@ -155,12 +149,6 @@ dependencies: superjson: specifier: ^2.2.1 version: 2.2.1 - tailwind-merge: - specifier: ^2.2.0 - version: 2.2.0 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.1) vaul: specifier: ^0.8.9 version: 0.8.9(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) @@ -178,6 +166,9 @@ devDependencies: '@types/eslint': specifier: ^8.44.7 version: 8.56.2 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 '@types/next-pwa': specifier: ^5.6.9 version: 5.6.9(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) @@ -205,12 +196,24 @@ devDependencies: autoprefixer: specifier: ^10.4.14 version: 10.4.17(postcss@8.4.33) + babel-loader: + specifier: ^9.1.3 + version: 9.1.3(@babel/core@7.23.9)(webpack@5.90.1) + clsx: + specifier: ^2.1.0 + version: 2.1.0 eslint: specifier: ^8.54.0 version: 8.56.0 eslint-config-next: specifier: ^14.0.4 version: 14.1.0(eslint@8.56.0)(typescript@5.3.3) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.19.8)(ts-node@10.9.2) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 postcss: specifier: ^8.4.31 version: 8.4.33 @@ -223,9 +226,18 @@ devDependencies: prisma: specifier: ^5.9.1 version: 5.9.1 + tailwind-merge: + specifier: ^2.2.0 + version: 2.2.0 tailwindcss: specifier: ^3.3.5 - version: 3.4.1 + version: 3.4.1(ts-node@10.9.2) + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.1) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@18.19.8)(typescript@5.3.3) tsx: specifier: ^4.7.1 version: 4.7.1 @@ -253,6 +265,7 @@ packages: /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + dev: true /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} @@ -995,13 +1008,13 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-compilation-targets@7.23.6: resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} @@ -1077,26 +1090,26 @@ packages: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.22.15 - '@babel/types': 7.23.6 + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-member-expression-to-functions@7.23.0: resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-module-imports@7.22.15: resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} @@ -1128,12 +1141,17 @@ packages: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-plugin-utils@7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} + /@babel/helper-plugin-utils@7.26.5: + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.7): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} @@ -1160,19 +1178,19 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-split-export-declaration@7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 /@babel/helper-string-parser@7.23.4: resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} @@ -1191,8 +1209,8 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.22.15 - '@babel/types': 7.23.6 + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 /@babel/helpers@7.23.8: resolution: {integrity: sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==} @@ -1282,6 +1300,24 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.9): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.7): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: @@ -1290,6 +1326,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.9): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.7): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -1299,6 +1344,16 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.9): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: @@ -1333,6 +1388,16 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.23.9): + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.7): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -1341,6 +1406,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.9): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -1349,6 +1423,25 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.23.9): + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.7): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -1357,6 +1450,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.9): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -1365,6 +1467,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.7): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -1373,6 +1484,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.9): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -1381,6 +1501,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -1389,6 +1518,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.7): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -1397,6 +1535,15 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.7): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} @@ -1406,6 +1553,16 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.9): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.7): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -1415,6 +1572,26 @@ packages: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.9): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.23.9): + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.7): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} @@ -1520,7 +1697,7 @@ packages: dependencies: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/template': 7.22.15 + '@babel/template': 7.23.9 /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} @@ -2006,7 +2183,7 @@ packages: dependencies: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 esutils: 2.0.3 /@babel/regjsgen@0.8.0: @@ -2084,10 +2261,21 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@braintree/sanitize-url@6.0.4: resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} dev: false + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@emotion/is-prop-valid@0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} requiresBuild: true @@ -2434,44 +2622,281 @@ packages: wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.22 + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true - /@jridgewell/source-map@0.3.5: - resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + /@jest/core@29.7.0(ts-node@10.9.2): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.22 + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.19.8)(ts-node@10.9.2) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + jest-mock: 29.7.0 + dev: true - /@jridgewell/trace-mapping@0.3.22: - resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 + jest-get-type: 29.6.3 + dev: true - /@mdx-js/mdx@2.3.0: - resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/estree-jsx': 1.0.5 - '@types/mdx': 2.0.11 - estree-util-build-jsx: 2.2.2 - estree-util-is-identifier-name: 2.1.0 + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.19.8 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.22 + '@types/node': 18.19.8 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.22 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.9 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.22 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.19.8 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.22 + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.22 + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@mdx-js/mdx@2.3.0: + resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/mdx': 2.0.11 + estree-util-build-jsx: 2.2.2 + estree-util-is-identifier-name: 2.1.0 estree-util-to-js: 1.2.0 estree-walker: 3.0.3 hast-util-to-estree: 2.3.3 @@ -4045,6 +4470,22 @@ packages: selderee: 0.11.0 dev: false + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + dev: true + /@smithy/abort-controller@2.1.1: resolution: {integrity: sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==} engines: {node: '>=14.0.0'} @@ -4596,6 +5037,11 @@ packages: unist-util-visit: 5.0.0 dev: false + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + /@trpc/client@10.45.0(@trpc/server@10.45.0): resolution: {integrity: sha512-m091R1qte9rvkvL8N1e/mzrbb8S4gb+Q4ZNJnEGDgd7kic/6a8DFgSciBTiCoSp0YwOTVhyQzSrrA/sZI6PhBg==} peerDependencies: @@ -4644,12 +5090,57 @@ packages: resolution: {integrity: sha512-2Fwzv6nqpE0Ie/G7PeS0EVR89zLm+c1Mw7T+RAGtU807j4oaUx0zGkBXTu5u9AI+j+BYNN2GZxJcuDTAecbr1A==} dev: false + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + /@types/acorn@4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} dependencies: '@types/estree': 1.0.5 dev: false + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + dev: true + + /@types/babel__traverse@7.20.6: + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + dependencies: + '@babel/types': 7.23.9 + dev: true + /@types/d3-scale-chromatic@3.0.3: resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} dev: false @@ -4675,7 +5166,6 @@ packages: dependencies: '@types/eslint': 8.56.2 '@types/estree': 1.0.5 - dev: false /@types/eslint@8.56.2: resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==} @@ -4702,6 +5192,12 @@ packages: '@types/node': 18.19.8 dev: false + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 18.19.8 + dev: true + /@types/hast@2.3.10: resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} dependencies: @@ -4714,10 +5210,41 @@ packages: '@types/unist': 3.0.2 dev: false + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/jest@29.5.14: + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + /@types/js-yaml@4.0.9: resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} dev: false + /@types/jsdom@20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 18.19.8 + '@types/tough-cookie': 4.0.5 + parse5: 7.1.2 + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -4810,6 +5337,14 @@ packages: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} dev: true + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + dev: true + + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: true + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -4827,6 +5362,16 @@ packages: '@types/node': 18.19.8 dev: true + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.33: + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -4967,19 +5512,15 @@ packages: dependencies: '@webassemblyjs/helper-numbers': 1.11.6 '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - dev: false /@webassemblyjs/floating-point-hex-parser@1.11.6: resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} - dev: false /@webassemblyjs/helper-api-error@1.11.6: resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} - dev: false /@webassemblyjs/helper-buffer@1.11.6: resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} - dev: false /@webassemblyjs/helper-numbers@1.11.6: resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} @@ -4987,11 +5528,9 @@ packages: '@webassemblyjs/floating-point-hex-parser': 1.11.6 '@webassemblyjs/helper-api-error': 1.11.6 '@xtuc/long': 4.2.2 - dev: false /@webassemblyjs/helper-wasm-bytecode@1.11.6: resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} - dev: false /@webassemblyjs/helper-wasm-section@1.11.6: resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} @@ -5000,23 +5539,19 @@ packages: '@webassemblyjs/helper-buffer': 1.11.6 '@webassemblyjs/helper-wasm-bytecode': 1.11.6 '@webassemblyjs/wasm-gen': 1.11.6 - dev: false /@webassemblyjs/ieee754@1.11.6: resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} dependencies: '@xtuc/ieee754': 1.2.0 - dev: false /@webassemblyjs/leb128@1.11.6: resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} dependencies: '@xtuc/long': 4.2.2 - dev: false /@webassemblyjs/utf8@1.11.6: resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} - dev: false /@webassemblyjs/wasm-edit@1.11.6: resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} @@ -5029,7 +5564,6 @@ packages: '@webassemblyjs/wasm-opt': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 '@webassemblyjs/wast-printer': 1.11.6 - dev: false /@webassemblyjs/wasm-gen@1.11.6: resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} @@ -5039,7 +5573,6 @@ packages: '@webassemblyjs/ieee754': 1.11.6 '@webassemblyjs/leb128': 1.11.6 '@webassemblyjs/utf8': 1.11.6 - dev: false /@webassemblyjs/wasm-opt@1.11.6: resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} @@ -5048,7 +5581,6 @@ packages: '@webassemblyjs/helper-buffer': 1.11.6 '@webassemblyjs/wasm-gen': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - dev: false /@webassemblyjs/wasm-parser@1.11.6: resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} @@ -5059,28 +5591,36 @@ packages: '@webassemblyjs/ieee754': 1.11.6 '@webassemblyjs/leb128': 1.11.6 '@webassemblyjs/utf8': 1.11.6 - dev: false /@webassemblyjs/wast-printer@1.11.6: resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} dependencies: '@webassemblyjs/ast': 1.11.6 '@xtuc/long': 4.2.2 - dev: false /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - dev: false /@xtuc/long@4.2.2: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - dev: false + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + dev: true /abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: false + /acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.11.3 + acorn-walk: 8.3.4 + dev: true + /acorn-import-assertions@1.9.0(acorn@8.11.3): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} deprecated: package has been renamed to acorn-import-attributes @@ -5088,7 +5628,6 @@ packages: acorn: ^8 dependencies: acorn: 8.11.3 - dev: false /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -5097,11 +5636,27 @@ packages: dependencies: acorn: 8.11.3 + /acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.11.3 + dev: true + /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /agent-base@7.1.0: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} @@ -5120,7 +5675,7 @@ packages: optional: true dependencies: ajv: 8.12.0 - dev: false + dev: true /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} @@ -5128,7 +5683,6 @@ packages: ajv: ^6.9.1 dependencies: ajv: 6.12.6 - dev: false /ajv-keywords@5.1.0(ajv@8.12.0): resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} @@ -5137,7 +5691,7 @@ packages: dependencies: ajv: 8.12.0 fast-deep-equal: 3.1.3 - dev: false + dev: true /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -5155,6 +5709,13 @@ packages: require-from-string: 2.0.2 uri-js: 4.4.1 + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -5179,12 +5740,18 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} @@ -5192,6 +5759,7 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 + dev: true /arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} @@ -5201,14 +5769,18 @@ packages: resolution: {integrity: sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==} dev: false + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 - dev: false /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -5339,6 +5911,10 @@ packages: has-symbols: 1.0.3 dev: true + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + /at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -5378,6 +5954,24 @@ packages: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} dev: false + /babel-jest@29.7.0(@babel/core@7.23.9): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.23.9 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.23.9) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-loader@8.3.0(@babel/core@7.23.9)(webpack@5.90.1): resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} engines: {node: '>= 8.9'} @@ -5404,7 +5998,30 @@ packages: find-cache-dir: 4.0.0 schema-utils: 4.2.0 webpack: 5.90.1 - dev: false + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.22.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + dev: true /babel-plugin-polyfill-corejs2@0.4.8(@babel/core@7.23.7): resolution: {integrity: sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==} @@ -5439,9 +6056,43 @@ packages: transitivePeerDependencies: - supports-color - /bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - dev: false + /babel-preset-current-node-syntax@1.1.0(@babel/core@7.23.9): + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.9) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.23.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.9) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.23.9): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.23.9) + dev: true + + /bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + dev: false /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -5495,6 +6146,7 @@ packages: /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + dev: true /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -5552,7 +6204,12 @@ packages: electron-to-chromium: 1.4.655 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.3) - dev: false + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -5578,6 +6235,14 @@ packages: dependencies: streamsearch: 1.1.0 + /call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: true + /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: @@ -5593,13 +6258,23 @@ packages: /camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true /caniuse-lite@1.0.30001579: resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} /caniuse-lite@1.0.30001582: resolution: {integrity: sha512-vsJG3V5vgfduaQGVxL53uSX/HUzxyr2eA8xCo36OLal7sRcSZbibJtLeh0qja4sFOr/QQGt4opB4tOy+eOgAxg==} - dev: false /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5629,6 +6304,11 @@ packages: ansi-styles: 4.3.0 supports-color: 7.2.0 + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + /character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} dev: false @@ -5658,6 +6338,7 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 + dev: true /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -5666,7 +6347,15 @@ packages: /chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} - dev: false + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + dev: true + + /cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + dev: true /class-variance-authority@0.7.0: resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} @@ -5695,6 +6384,15 @@ packages: execa: 0.8.0 dev: false + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -5703,7 +6401,6 @@ packages: /clsx@2.1.0: resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} engines: {node: '>=6'} - dev: false /cmdk@0.2.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==} @@ -5719,6 +6416,15 @@ packages: - '@types/react' dev: false + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -5751,6 +6457,13 @@ packages: color-string: 1.9.1 dev: false + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + /comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} dev: false @@ -5770,6 +6483,7 @@ packages: /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + dev: true /commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} @@ -5783,7 +6497,7 @@ packages: /common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} - dev: false + dev: true /common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} @@ -5829,6 +6543,29 @@ packages: layout-base: 1.0.2 dev: false + /create-jest@29.7.0(@types/node@18.19.8)(ts-node@10.9.2): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.19.8)(ts-node@10.9.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -5853,6 +6590,22 @@ packages: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + dev: true + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -6156,6 +6909,15 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + /date-fns@3.3.1: resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} dev: false @@ -6186,6 +6948,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + dev: true + /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} dependencies: @@ -6199,6 +6965,15 @@ packages: mimic-response: 3.1.0 dev: false + /dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -6247,6 +7022,11 @@ packages: robust-predicates: 3.0.2 dev: false + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -6256,6 +7036,11 @@ packages: engines: {node: '>=8'} dev: false + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: true + /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false @@ -6268,6 +7053,17 @@ packages: /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true /diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} @@ -6282,6 +7078,7 @@ packages: /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -6309,6 +7106,14 @@ packages: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: false + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + dependencies: + webidl-conversions: 7.0.0 + dev: true + /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -6328,6 +7133,15 @@ packages: domhandler: 5.0.3 dev: false + /dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6360,12 +7174,16 @@ packages: /electron-to-chromium@1.4.655: resolution: {integrity: sha512-2yszojF7vIZ68adIOvzV4bku8OZad9w5H9xF3ZAMZjPuOjBarlflUkjN6DggdV+L71WZuKUfKUhov/34+G5QHg==} - dev: false /elkjs@0.9.2: resolution: {integrity: sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==} dev: false + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -6393,7 +7211,12 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true /es-abstract@1.22.3: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} @@ -6439,6 +7262,16 @@ packages: unbox-primitive: 1.0.2 which-typed-array: 1.1.13 + /es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + /es-iterator-helpers@1.0.15: resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} dependencies: @@ -6460,7 +7293,13 @@ packages: /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} - dev: false + + /es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} @@ -6470,6 +7309,16 @@ packages: has-tostringtag: 1.0.0 hasown: 2.0.0 + /es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + /es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: @@ -6523,6 +7372,11 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -6533,6 +7387,18 @@ packages: engines: {node: '>=12'} dev: false + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + /eslint-config-next@14.1.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg==} peerDependencies: @@ -6721,7 +7587,6 @@ packages: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - dev: false /eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} @@ -6796,7 +7661,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: false /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -6814,7 +7678,6 @@ packages: /estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} - dev: false /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} @@ -6876,7 +7739,6 @@ packages: /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - dev: false /execa@0.8.0: resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} @@ -6891,11 +7753,42 @@ packages: strip-eof: 1.0.0 dev: false + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} dev: false + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -6943,6 +7836,12 @@ packages: dependencies: reusify: 1.0.4 + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6976,7 +7875,7 @@ packages: dependencies: common-path-prefix: 3.0.0 pkg-dir: 7.0.0 - dev: false + dev: true /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -6984,7 +7883,6 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: false /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -7000,7 +7898,7 @@ packages: dependencies: locate-path: 7.2.0 path-exists: 5.0.0 - dev: false + dev: true /flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} @@ -7027,6 +7925,16 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 + /form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + dev: true + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true @@ -7099,6 +8007,11 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} dependencies: @@ -7107,6 +8020,22 @@ packages: has-symbols: 1.0.3 hasown: 2.0.0 + /get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: true + /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -7115,11 +8044,29 @@ packages: /get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: true + /get-stream@3.0.0: resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} engines: {node: '>=4'} dev: false + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -7152,6 +8099,7 @@ packages: engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 + dev: true /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -7221,6 +8169,11 @@ packages: dependencies: get-intrinsic: 1.2.2 + /gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + dev: true + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -7267,12 +8220,24 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + /has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + dev: true + /has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /hash-obj@4.0.0: resolution: {integrity: sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==} engines: {node: '>=12'} @@ -7288,6 +8253,13 @@ packages: dependencies: function-bind: 1.1.2 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + /hast-util-from-dom@5.0.0: resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==} dependencies: @@ -7420,6 +8392,17 @@ packages: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} dev: false + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} @@ -7444,11 +8427,32 @@ packages: entities: 4.5.0 dev: false + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /http_ece@1.2.0: resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} engines: {node: '>=16'} dev: false + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /https-proxy-agent@7.0.4: resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} engines: {node: '>= 14'} @@ -7459,12 +8463,16 @@ packages: - supports-color dev: false + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - dev: false /idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} @@ -7485,6 +8493,15 @@ packages: resolve-from: 4.0.0 dev: true + /import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -7558,6 +8575,10 @@ packages: get-intrinsic: 1.2.2 is-typed-array: 1.1.12 + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false @@ -7579,6 +8600,7 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 + dev: true /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -7630,6 +8652,11 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true + /is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} engines: {node: '>= 0.4'} @@ -7711,6 +8738,10 @@ packages: engines: {node: '>=12'} dev: false + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + /is-reference@3.0.2: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} dependencies: @@ -7791,6 +8822,65 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.23.9 + '@babel/parser': 7.23.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.23.9 + '@babel/parser': 7.23.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} dependencies: @@ -7819,6 +8909,409 @@ packages: filelist: 1.0.4 minimatch: 3.1.2 + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.7.0(@types/node@18.19.8)(ts-node@10.9.2): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.19.8)(ts-node@10.9.2) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@18.19.8)(ts-node@10.9.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jest-config@29.7.0(@types/node@18.19.8)(ts-node@10.9.2): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.23.9 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + babel-jest: 29.7.0(@babel/core@7.23.9) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.2(@types/node@18.19.8)(typescript@5.3.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 18.19.8 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 18.19.8 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.23.5 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + jest-util: 29.7.0 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.3 + slash: 3.0.0 + dev: true + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.9 + '@babel/generator': 7.23.6 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.23.9) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.23.9) + '@babel/types': 7.23.9 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.23.9) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.19.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + /jest-worker@26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} @@ -7834,11 +9327,42 @@ packages: '@types/node': 18.19.8 merge-stream: 2.0.0 supports-color: 8.1.1 - dev: false + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 18.19.8 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.7.0(@types/node@18.19.8)(ts-node@10.9.2): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@18.19.8)(ts-node@10.9.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true /jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + dev: true /jose@4.15.4: resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} @@ -7870,7 +9394,6 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 - dev: false /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -7878,6 +9401,47 @@ packages: dependencies: argparse: 2.0.1 + /jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.11.3 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.5.0 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.2 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.18.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -7893,7 +9457,6 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: false /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7982,6 +9545,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -8021,18 +9589,20 @@ packages: /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + dev: true /lilconfig@3.0.0: resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} engines: {node: '>=14'} + dev: true /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true /loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} - dev: false /loader-utils@2.0.4: resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} @@ -8048,7 +9618,6 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 - dev: false /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -8062,7 +9631,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: p-locate: 6.0.0 - dev: false + dev: true /lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} @@ -8137,6 +9706,23 @@ packages: semver: 6.3.1 dev: false + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + /markdown-extensions@1.1.1: resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} engines: {node: '>=0.10.0'} @@ -8146,6 +9732,11 @@ packages: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} dev: false + /math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + dev: true + /mdast-util-definitions@5.1.2: resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} dependencies: @@ -8771,14 +10362,17 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: false /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: false + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -8842,6 +10436,7 @@ packages: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 + dev: true /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -8864,7 +10459,6 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: false /next-auth@4.24.5(next@14.1.0)(nodemailer@6.9.8)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==} @@ -9092,6 +10686,10 @@ packages: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} dev: false + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} @@ -9115,6 +10713,7 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + dev: true /normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} @@ -9128,11 +10727,22 @@ packages: path-key: 2.0.1 dev: false + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + /npm-to-yarn@2.2.1: resolution: {integrity: sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false + /nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + dev: true + /oauth@0.9.15: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} dev: false @@ -9149,6 +10759,7 @@ packages: /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + dev: true /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -9219,6 +10830,13 @@ packages: dependencies: wrappy: 1.0.2 + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + /openid-client@5.6.4: resolution: {integrity: sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==} dependencies: @@ -9250,7 +10868,6 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 - dev: false /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -9263,14 +10880,13 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: yocto-queue: 1.0.0 - dev: false + dev: true /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} dependencies: p-limit: 2.3.0 - dev: false /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -9284,7 +10900,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: p-limit: 4.0.0 - dev: false + dev: true /p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} @@ -9294,7 +10910,6 @@ packages: /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - dev: false /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -9316,6 +10931,16 @@ packages: is-hexadecimal: 2.0.1 dev: false + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + /parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} dev: false @@ -9324,7 +10949,6 @@ packages: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: entities: 4.5.0 - dev: false /parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -9340,7 +10964,7 @@ packages: /path-exists@5.0.0: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false + dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -9416,20 +11040,20 @@ packages: /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + dev: true /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 - dev: false /pkg-dir@7.0.0: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} dependencies: find-up: 6.3.0 - dev: false + dev: true /postcss-import@15.1.0(postcss@8.4.33): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} @@ -9441,6 +11065,7 @@ packages: postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 + dev: true /postcss-js@4.0.1(postcss@8.4.33): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} @@ -9450,8 +11075,9 @@ packages: dependencies: camelcase-css: 2.0.1 postcss: 8.4.33 + dev: true - /postcss-load-config@4.0.2(postcss@8.4.33): + /postcss-load-config@4.0.2(postcss@8.4.33)(ts-node@10.9.2): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} peerDependencies: @@ -9465,7 +11091,9 @@ packages: dependencies: lilconfig: 3.0.0 postcss: 8.4.33 + ts-node: 10.9.2(@types/node@18.19.8)(typescript@5.3.3) yaml: 2.3.4 + dev: true /postcss-nested@6.0.1(postcss@8.4.33): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} @@ -9475,6 +11103,7 @@ packages: dependencies: postcss: 8.4.33 postcss-selector-parser: 6.0.15 + dev: true /postcss-selector-parser@6.0.15: resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==} @@ -9482,9 +11111,11 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 + dev: true /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true /postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} @@ -9501,6 +11132,7 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 + dev: true /preact-render-to-string@5.2.6(preact@10.19.3): resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} @@ -9601,6 +11233,15 @@ packages: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + /pretty-format@3.8.0: resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} dev: false @@ -9613,6 +11254,14 @@ packages: dependencies: '@prisma/engines': 5.9.1 + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -9633,6 +11282,12 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: false + /psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + dependencies: + punycode: 2.3.1 + dev: true + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -9644,6 +11299,14 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + dev: true + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9698,6 +11361,10 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + /react-remove-scroll-bar@2.3.4(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} @@ -9779,6 +11446,7 @@ packages: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: pify: 2.3.0 + dev: true /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} @@ -9794,6 +11462,7 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + dev: true /reading-time@1.5.0: resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} @@ -9942,10 +11611,19 @@ packages: unified: 10.1.2 dev: false + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + /resend@3.2.0: resolution: {integrity: sha512-lDHhexiFYPoLXy7zRlJ8D5eKxoXy6Tr9/elN3+Vv7PkUoYuSSD1fpiIfa/JYXEWyiyN2UczkCTLpkT8dDPJ4Pg==} engines: {node: '>=18'} @@ -9953,15 +11631,32 @@ packages: '@react-email/render': 0.0.12 dev: false + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} dev: true + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + dev: true + /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -10058,7 +11753,13 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} @@ -10081,7 +11782,6 @@ packages: '@types/json-schema': 7.0.15 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - dev: false /schema-utils@4.2.0: resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} @@ -10091,7 +11791,7 @@ packages: ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) ajv-keywords: 5.1.0(ajv@8.12.0) - dev: false + dev: true /section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} @@ -10127,7 +11827,6 @@ packages: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: randombytes: 2.1.0 - dev: false /set-function-length@1.2.0: resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==} @@ -10202,7 +11901,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -10226,6 +11924,10 @@ packages: is-arrayish: 0.3.2 dev: false + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -10255,6 +11957,13 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -10286,7 +11995,13 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} @@ -10302,6 +12017,14 @@ packages: bare-events: 2.4.2 dev: false + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -10396,6 +12119,11 @@ packages: engines: {node: '>=4'} dev: true + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + /strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -10405,6 +12133,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -10458,6 +12191,7 @@ packages: mz: 2.7.0 pirates: 4.0.6 ts-interface-checker: 0.1.13 + dev: true /superjson@2.2.1: resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} @@ -10490,27 +12224,30 @@ packages: engines: {node: '>=10'} dependencies: has-flag: 4.0.0 - dev: false /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + /tailwind-merge@2.2.0: resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==} dependencies: '@babel/runtime': 7.23.8 - dev: false + dev: true /tailwindcss-animate@1.0.7(tailwindcss@3.4.1): resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' dependencies: - tailwindcss: 3.4.1 - dev: false + tailwindcss: 3.4.1(ts-node@10.9.2) + dev: true - /tailwindcss@3.4.1: + /tailwindcss@3.4.1(ts-node@10.9.2): resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} engines: {node: '>=14.0.0'} hasBin: true @@ -10532,13 +12269,14 @@ packages: postcss: 8.4.33 postcss-import: 15.1.0(postcss@8.4.33) postcss-js: 4.0.1(postcss@8.4.33) - postcss-load-config: 4.0.2(postcss@8.4.33) + postcss-load-config: 4.0.2(postcss@8.4.33)(ts-node@10.9.2) postcss-nested: 6.0.1(postcss@8.4.33) postcss-selector-parser: 6.0.15 resolve: 1.22.8 sucrase: 3.35.0 transitivePeerDependencies: - ts-node + dev: true /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -10617,7 +12355,6 @@ packages: serialize-javascript: 6.0.2 terser: 5.27.0 webpack: 5.90.1 - dev: false /terser@5.27.0: resolution: {integrity: sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==} @@ -10629,6 +12366,15 @@ packages: commander: 2.20.3 source-map-support: 0.5.21 + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-decoder@1.1.1: resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} dependencies: @@ -10644,11 +12390,13 @@ packages: engines: {node: '>=0.8'} dependencies: thenify: 3.3.1 + dev: true /thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: any-promise: 1.3.0 + dev: true /title@3.5.3: resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} @@ -10665,6 +12413,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -10675,11 +12427,28 @@ packages: dependencies: is-number: 7.0.0 + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: punycode: 2.3.1 + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.1 + dev: true + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false @@ -10704,6 +12473,38 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /ts-node@10.9.2(@types/node@18.19.8)(typescript@5.3.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.8 + acorn: 8.11.3 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true /tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -10745,6 +12546,11 @@ packages: prelude-ls: 1.2.1 dev: true + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest@0.16.0: resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} engines: {node: '>=10'} @@ -10754,6 +12560,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + /type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} @@ -10966,6 +12777,11 @@ packages: unist-util-visit-parents: 6.0.1 dev: false + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10993,13 +12809,19 @@ packages: browserslist: 4.22.3 escalade: 3.1.1 picocolors: 1.0.0 - dev: false /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.1 + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + /use-callback-ref@1.3.1(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} @@ -11063,6 +12885,19 @@ packages: sade: 1.8.1 dev: false + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.22 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + /vaul@0.8.9(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-gpmtmZRWDPP6niQh14JfRIFUYZVyfvAWyA/7rUINOfNlO/2K7uEvI5rLXEXkxZIRFyUZj+TPHLFMirkegPHjrw==} peerDependencies: @@ -11131,6 +12966,19 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: false + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -11163,6 +13011,11 @@ packages: /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + /webpack-sources@1.4.3: resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} dependencies: @@ -11173,7 +13026,6 @@ packages: /webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} - dev: false /webpack@5.90.1: resolution: {integrity: sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==} @@ -11213,7 +13065,26 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: false + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -11449,6 +13320,41 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + /yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} dev: false @@ -11462,6 +13368,30 @@ packages: /yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} @@ -11470,7 +13400,7 @@ packages: /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} - dev: false + dev: true /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} From 511b1028bf410c2fe171b1f5d6b958f2e356c7f0 Mon Sep 17 00:00:00 2001 From: krokosik Date: Mon, 17 Feb 2025 21:09:57 +0100 Subject: [PATCH 16/30] Create a basic test suite for simplifyDebts based on the examples in articles --- src/lib/simplify.test.ts | 120 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/lib/simplify.test.ts diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts new file mode 100644 index 0000000..d5fffbf --- /dev/null +++ b/src/lib/simplify.test.ts @@ -0,0 +1,120 @@ +import { GroupBalance } from '@prisma/client'; +import { simplifyDebts } from './simplify'; + +describe('simplifyDebts', () => { + it('simplifies small graph', () => { + expect(new Set(simplifyDebts(smallGraph))).toEqual(new Set(smallGraphResult)); + }); + + it('simplifies large graph', () => { + expect(new Set(simplifyDebts(largeGraph))).toEqual(new Set(largeGraphResult)); + }); +}); + +const edgeToGroupBalance = (edge: { + userOne: number; + userTwo: number; + amount: number; +}): [GroupBalance, GroupBalance] => { + const base = { + groupId: 0, + currency: 'USD', + updatedAt: new Date(), + }; + return [ + { + ...base, + userId: edge.userOne, + firendId: edge.userTwo, + amount: edge.amount, + }, + { + ...base, + userId: edge.userTwo, + firendId: edge.userOne, + amount: -edge.amount, + }, + ]; +}; + +// taken from https://www.geeksforgeeks.org/minimize-cash-flow-among-given-set-friends-borrowed-money/ +const smallGraph: GroupBalance[] = [ + { userOne: 0, userTwo: 1, amount: 10 }, + { userOne: 1, userTwo: 2, amount: 50 }, + { userOne: 2, userTwo: 0, amount: -20 }, +].flatMap(edgeToGroupBalance); + +const smallGraphResult: GroupBalance[] = [ + { userOne: 0, userTwo: 2, amount: 30 }, + { userOne: 1, userTwo: 2, amount: 40 }, + { userOne: 2, userTwo: 0, amount: 0 }, +] + .flatMap(edgeToGroupBalance) + .map((resultBalance, idx) => ({ + ...resultBalance, + updatedAt: smallGraph[idx]!.updatedAt, + })); + +// taken from https://medium.com/@mithunmk93/algorithm-behind-splitwises-debt-simplification-feature-8ac485e97688 +const largeGraph: GroupBalance[] = [ + { userOne: 0, userTwo: 1, amount: 0 }, + { userOne: 0, userTwo: 2, amount: 0 }, + { userOne: 0, userTwo: 3, amount: 0 }, + { userOne: 0, userTwo: 4, amount: 0 }, + { userOne: 0, userTwo: 5, amount: 0 }, + { userOne: 0, userTwo: 6, amount: 0 }, + + { userOne: 1, userTwo: 2, amount: 40 }, + { userOne: 1, userTwo: 3, amount: 0 }, + { userOne: 1, userTwo: 4, amount: 0 }, + { userOne: 1, userTwo: 5, amount: -10 }, + { userOne: 1, userTwo: 6, amount: -30 }, + + { userOne: 2, userTwo: 3, amount: 20 }, + { userOne: 2, userTwo: 4, amount: 0 }, + { userOne: 2, userTwo: 5, amount: -30 }, + { userOne: 2, userTwo: 6, amount: 0 }, + + { userOne: 3, userTwo: 4, amount: 50 }, + { userOne: 3, userTwo: 5, amount: -10 }, + { userOne: 3, userTwo: 6, amount: -10 }, + + { userOne: 4, userTwo: 5, amount: -10 }, + { userOne: 4, userTwo: 6, amount: 0 }, + + { userOne: 5, userTwo: 6, amount: 0 }, +].flatMap(edgeToGroupBalance); + +const largeGraphResult: GroupBalance[] = [ + { userOne: 0, userTwo: 1, amount: 0 }, + { userOne: 0, userTwo: 2, amount: 0 }, + { userOne: 0, userTwo: 3, amount: 0 }, + { userOne: 0, userTwo: 4, amount: 0 }, + { userOne: 0, userTwo: 5, amount: 0 }, + { userOne: 0, userTwo: 6, amount: 0 }, + + { userOne: 1, userTwo: 2, amount: 10 }, + { userOne: 1, userTwo: 3, amount: 0 }, + { userOne: 1, userTwo: 4, amount: 0 }, + { userOne: 1, userTwo: 5, amount: 0 }, + { userOne: 1, userTwo: 6, amount: -10 }, + + { userOne: 2, userTwo: 3, amount: 0 }, + { userOne: 2, userTwo: 4, amount: 0 }, + { userOne: 2, userTwo: 5, amount: -40 }, + { userOne: 2, userTwo: 6, amount: 0 }, + + { userOne: 3, userTwo: 4, amount: 40 }, + { userOne: 3, userTwo: 5, amount: 0 }, + { userOne: 3, userTwo: 6, amount: -30 }, + + { userOne: 4, userTwo: 5, amount: -20 }, + { userOne: 4, userTwo: 6, amount: 0 }, + + { userOne: 5, userTwo: 6, amount: 0 }, +] + .flatMap(edgeToGroupBalance) + .map((resultBalance, idx) => ({ + ...resultBalance, + updatedAt: largeGraph[idx]!.updatedAt, + })); From 42b49ceed07dedfa394a712e359d9676755a6581 Mon Sep 17 00:00:00 2001 From: krokosik Date: Mon, 17 Feb 2025 23:39:15 +0100 Subject: [PATCH 17/30] Implement the debt simplification algorithm and adjust test cases --- src/lib/simplify.test.ts | 52 +++++++++++++++++++----- src/lib/simplify.ts | 85 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts index d5fffbf..4aad087 100644 --- a/src/lib/simplify.test.ts +++ b/src/lib/simplify.test.ts @@ -3,14 +3,44 @@ import { simplifyDebts } from './simplify'; describe('simplifyDebts', () => { it('simplifies small graph', () => { - expect(new Set(simplifyDebts(smallGraph))).toEqual(new Set(smallGraphResult)); + expect(simplifyDebts(smallGraph).toSorted(sortByIds)).toEqual(smallGraphResult); }); - it('simplifies large graph', () => { - expect(new Set(simplifyDebts(largeGraph))).toEqual(new Set(largeGraphResult)); + it('gets the same operation count as in the article', () => { + // depending on the edge traversal order, the result can be different, but the total amount and operation count should be the same + expect(simplifyDebts(largeGraph).filter((balance) => balance.amount > 0).length).toBe( + largeGraphResult.filter((balance) => balance.amount > 0).length, + ); + }); + + it('preserves the total balance per user', () => { + const startingBalances = largeGraph.reduce( + (acc, balance) => { + acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; + return acc; + }, + {} as Record, + ); + + const userBalances = simplifyDebts(largeGraph).reduce( + (acc, balance) => { + acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; + return acc; + }, + {} as Record, + ); + + expect(startingBalances).toEqual(userBalances); }); }); +const sortByIds = (a: GroupBalance, b: GroupBalance) => { + if (a.userId === b.userId) { + return a.firendId - b.firendId; + } + return a.userId - b.userId; +}; + const edgeToGroupBalance = (edge: { userOne: number; userTwo: number; @@ -23,16 +53,16 @@ const edgeToGroupBalance = (edge: { }; return [ { - ...base, userId: edge.userOne, firendId: edge.userTwo, amount: edge.amount, + ...base, }, { - ...base, userId: edge.userTwo, firendId: edge.userOne, - amount: -edge.amount, + amount: edge.amount === 0 ? 0 : -edge.amount, + ...base, }, ]; }; @@ -45,15 +75,16 @@ const smallGraph: GroupBalance[] = [ ].flatMap(edgeToGroupBalance); const smallGraphResult: GroupBalance[] = [ - { userOne: 0, userTwo: 2, amount: 30 }, + { userOne: 0, userTwo: 1, amount: 0 }, { userOne: 1, userTwo: 2, amount: 40 }, - { userOne: 2, userTwo: 0, amount: 0 }, + { userOne: 2, userTwo: 0, amount: -30 }, ] .flatMap(edgeToGroupBalance) .map((resultBalance, idx) => ({ ...resultBalance, updatedAt: smallGraph[idx]!.updatedAt, - })); + })) + .toSorted(sortByIds); // taken from https://medium.com/@mithunmk93/algorithm-behind-splitwises-debt-simplification-feature-8ac485e97688 const largeGraph: GroupBalance[] = [ @@ -117,4 +148,5 @@ const largeGraphResult: GroupBalance[] = [ .map((resultBalance, idx) => ({ ...resultBalance, updatedAt: largeGraph[idx]!.updatedAt, - })); + })) + .toSorted(sortByIds); diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts index 8fc3e30..baf259d 100644 --- a/src/lib/simplify.ts +++ b/src/lib/simplify.ts @@ -1,9 +1,88 @@ +import { Dinic, type IDinicEdge } from '@algorithm.ts/dinic'; import { GroupBalance } from '@prisma/client'; -import { Dinic } from '@algorithm.ts/dinic'; + +class CustomDinic extends Dinic { + public getFlows(): IDinicEdge[] { + return this._edges.filter((edge) => edge.flow > 0); + } +} // Based on https://github.com/mithun-mohan/Algorithms-Java-Cookbook/blob/master/MaximumFlow/Dinics/SimplifyDebts.java export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { - const dinic = new Dinic(); + const dinic = new CustomDinic(); + const visitedEdges = new Set<[number, number]>(); + + let graph = groupBalances.filter((balance) => balance.amount > 0); + const nodes = new Set(); + groupBalances.forEach((balance) => { + nodes.add(balance.userId); + nodes.add(balance.firendId); + }); + const nodeCount = nodes.size; + const edgeCount = graph.length; + + while (visitedEdges.size < edgeCount) { + const { userId: source, firendId: sink } = graph.find( + (edge) => !visitedEdges.has([edge.userId, edge.firendId]), + )!; + + dinic.init(source, sink, nodeCount); + + graph.forEach((edge) => { + dinic.addEdge(edge.userId, edge.firendId, edge.amount); + }); + + const maxflow = dinic.maxflow(); // the method above gets all the edges with full capacity, but also calculates maxflow + const usedEdges = dinic.getFlows(); + + graph = graph + .map((balance) => { + const flow = + usedEdges.find((edge) => edge.from === balance.userId && edge.to === balance.firendId) + ?.flow ?? 0; + return { + ...balance, + amount: balance.amount - flow, + }; + }) + .filter((balance) => balance.amount > 0); - return []; + graph.push({ + ...groupBalances.find(({ userId, firendId }) => userId === source && firendId === sink)!, + amount: maxflow, + }); + + visitedEdges.add([source, sink]); + } + + graph = getMirrorBalances(graph); + + groupBalances.forEach((balance) => { + const found = graph.find( + (graphBalance) => + graphBalance.userId === balance.userId && graphBalance.firendId === balance.firendId, + ); + if (!found) { + graph.push({ ...balance, amount: 0 }); + } + }); + + return graph; } + +// Dinic operates on positive balances only, so we recreate the non-positive balances +// at the end. +const getMirrorBalances = (groupBalances: GroupBalance[]): GroupBalance[] => { + const result = [...groupBalances]; + + groupBalances.forEach((balance) => { + result.push({ + ...balance, + userId: balance.firendId, + firendId: balance.userId, + amount: -balance.amount, + }); + }); + + return result; +}; From 14e345e266c2fdeb760e1b4f48ba500f5345796a Mon Sep 17 00:00:00 2001 From: krokosik Date: Mon, 17 Feb 2025 23:39:48 +0100 Subject: [PATCH 18/30] Use the simplify algorithm in API --- src/server/api/routers/group.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/api/routers/group.ts b/src/server/api/routers/group.ts index e592098..0b23e0a 100644 --- a/src/server/api/routers/group.ts +++ b/src/server/api/routers/group.ts @@ -5,6 +5,7 @@ import { db } from '~/server/db'; import { createGroupExpense, deleteExpense, editExpense } from '../services/splitService'; import { TRPCError } from '@trpc/server'; import { nanoid } from 'nanoid'; +import { simplifyDebts } from '~/lib/simplify'; export const groupRouter = createTRPCRouter({ create: protectedProcedure @@ -253,6 +254,10 @@ export const groupRouter = createTRPCRouter({ }, }); + if (group?.simplifyDebts) { + group.groupBalances = simplifyDebts(group.groupBalances); + } + return group; }), From a8d0fe050d029b2b56904b11b18e8b169d3cf491 Mon Sep 17 00:00:00 2001 From: krokosik Date: Mon, 17 Feb 2025 23:55:43 +0100 Subject: [PATCH 19/30] Handle multiple currencies and use node indices for Dinic --- src/lib/simplify.ts | 46 +++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts index baf259d..4dbd96d 100644 --- a/src/lib/simplify.ts +++ b/src/lib/simplify.ts @@ -7,29 +7,48 @@ class CustomDinic extends Dinic { } } -// Based on https://github.com/mithun-mohan/Algorithms-Java-Cookbook/blob/master/MaximumFlow/Dinics/SimplifyDebts.java export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { - const dinic = new CustomDinic(); - const visitedEdges = new Set<[number, number]>(); - - let graph = groupBalances.filter((balance) => balance.amount > 0); + const currencies = new Set(groupBalances.map((balance) => balance.currency)); const nodes = new Set(); groupBalances.forEach((balance) => { nodes.add(balance.userId); nodes.add(balance.firendId); }); - const nodeCount = nodes.size; + const result: GroupBalance[] = []; + + for (const currency of currencies) { + const balances = groupBalances.filter((balance) => balance.currency === currency); + const simplified = simplifyDebtsForSingleCurrency(balances, Array.from(nodes.values())); + result.push(...simplified); + } + + return result; +} + +// Based on https://github.com/mithun-mohan/Algorithms-Java-Cookbook/blob/master/MaximumFlow/Dinics/SimplifyDebts.java +function simplifyDebtsForSingleCurrency( + groupBalances: GroupBalance[], + nodes: number[], +): GroupBalance[] { + const dinic = new CustomDinic(); + const visitedEdges = new Set<[number, number]>(); + + let graph = groupBalances.filter((balance) => balance.amount > 0); + const edgeCount = graph.length; while (visitedEdges.size < edgeCount) { - const { userId: source, firendId: sink } = graph.find( + const { userId, firendId } = graph.find( (edge) => !visitedEdges.has([edge.userId, edge.firendId]), )!; - dinic.init(source, sink, nodeCount); + const source = nodes.indexOf(userId); + const sink = nodes.indexOf(firendId); + + dinic.init(source, sink, nodes.length); graph.forEach((edge) => { - dinic.addEdge(edge.userId, edge.firendId, edge.amount); + dinic.addEdge(nodes.indexOf(edge.userId), nodes.indexOf(edge.firendId), edge.amount); }); const maxflow = dinic.maxflow(); // the method above gets all the edges with full capacity, but also calculates maxflow @@ -38,8 +57,9 @@ export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { graph = graph .map((balance) => { const flow = - usedEdges.find((edge) => edge.from === balance.userId && edge.to === balance.firendId) - ?.flow ?? 0; + usedEdges.find( + (edge) => nodes[edge.from] === balance.userId && nodes[edge.to] === balance.firendId, + )?.flow ?? 0; return { ...balance, amount: balance.amount - flow, @@ -48,7 +68,9 @@ export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { .filter((balance) => balance.amount > 0); graph.push({ - ...groupBalances.find(({ userId, firendId }) => userId === source && firendId === sink)!, + ...groupBalances.find( + ({ userId, firendId }) => userId === nodes[source] && firendId === nodes[sink], + )!, amount: maxflow, }); From 1c11bba3eaa7be8e9e936b522d955ab5fd4a5156 Mon Sep 17 00:00:00 2001 From: krokosik Date: Mon, 17 Feb 2025 23:59:03 +0100 Subject: [PATCH 20/30] Do not list to many personal balances --- src/components/group/GroupMyBalance.tsx | 35 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/group/GroupMyBalance.tsx b/src/components/group/GroupMyBalance.tsx index 4020f6f..6354cb2 100644 --- a/src/components/group/GroupMyBalance.tsx +++ b/src/components/group/GroupMyBalance.tsx @@ -83,19 +83,28 @@ const GroupMyBalance: React.FC = ({ userId, groupBalances,
You are all settled up
) : null} - {Object.entries(friendBalances).map(([friendId, balances]) => { - const friend = userMap[+friendId]; - return ( -
- {Object.entries(balances).map(([currency, amount]) => ( -
- {amount > 0 ? `${friend?.name} owes you` : `You owe ${friend?.name}`}{' '} - {toUIString(Math.abs(amount))} {currency} -
- ))} -
- ); - })} + {Object.entries(friendBalances) + .slice(0, 2) + .map(([friendId, balances]) => { + const friend = userMap[+friendId]; + return ( +
+ {Object.entries(balances).map(([currency, amount]) => ( +
+ {amount > 0 ? `${friend?.name} owes you` : `You owe ${friend?.name}`}{' '} + {toUIString(Math.abs(amount))} {currency} +
+ ))} +
+ ); + })} + + {Object.keys(friendBalances).length > 2 ? ( +
+ +{Object.keys(friendBalances).length - 2} balance + {Object.keys(friendBalances).length > 3 ? 's' : ''}... +
+ ) : null} ); From 1f5ee3cb79db84ad9d781df4df07f179e735c239 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 09:39:38 +0100 Subject: [PATCH 21/30] Run auto format on tailwind config post shadcn CLI --- tailwind.config.ts | 160 ++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/tailwind.config.ts b/tailwind.config.ts index 4ee9bc5..e781b5e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -10,86 +10,86 @@ const config = { ], prefix: '', theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px' - } - }, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - keyframes: { - 'accordion-down': { - from: { - height: '0' - }, - to: { - height: 'var(--radix-accordion-content-height)' - } - }, - 'accordion-up': { - from: { - height: 'var(--radix-accordion-content-height)' - }, - to: { - height: '0' - } - }, - 'caret-blink': { - '0%,70%,100%': { - opacity: '1' - }, - '20%,50%': { - opacity: '0' - } - } - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - 'caret-blink': 'caret-blink 1.25s ease-out infinite' - } - } + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { + height: '0', + }, + to: { + height: 'var(--radix-accordion-content-height)', + }, + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)', + }, + to: { + height: '0', + }, + }, + 'caret-blink': { + '0%,70%,100%': { + opacity: '1', + }, + '20%,50%': { + opacity: '0', + }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'caret-blink': 'caret-blink 1.25s ease-out infinite', + }, + }, }, plugins: [require('tailwindcss-animate')], } satisfies Config; From c32250fa2e72f3d1adaa039d9bbdb52b0c60b6f5 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 09:51:37 +0100 Subject: [PATCH 22/30] Add more unit tests --- src/lib/simplify.test.ts | 95 ++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts index 4aad087..be69231 100644 --- a/src/lib/simplify.test.ts +++ b/src/lib/simplify.test.ts @@ -1,39 +1,6 @@ import { GroupBalance } from '@prisma/client'; import { simplifyDebts } from './simplify'; -describe('simplifyDebts', () => { - it('simplifies small graph', () => { - expect(simplifyDebts(smallGraph).toSorted(sortByIds)).toEqual(smallGraphResult); - }); - - it('gets the same operation count as in the article', () => { - // depending on the edge traversal order, the result can be different, but the total amount and operation count should be the same - expect(simplifyDebts(largeGraph).filter((balance) => balance.amount > 0).length).toBe( - largeGraphResult.filter((balance) => balance.amount > 0).length, - ); - }); - - it('preserves the total balance per user', () => { - const startingBalances = largeGraph.reduce( - (acc, balance) => { - acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; - return acc; - }, - {} as Record, - ); - - const userBalances = simplifyDebts(largeGraph).reduce( - (acc, balance) => { - acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; - return acc; - }, - {} as Record, - ); - - expect(startingBalances).toEqual(userBalances); - }); -}); - const sortByIds = (a: GroupBalance, b: GroupBalance) => { if (a.userId === b.userId) { return a.firendId - b.firendId; @@ -150,3 +117,65 @@ const largeGraphResult: GroupBalance[] = [ updatedAt: largeGraph[idx]!.updatedAt, })) .toSorted(sortByIds); + +const largeGraph2: GroupBalance[] = [ + { userOne: 0, userTwo: 1, amount: 8957.95 }, + { userOne: 0, userTwo: 2, amount: 3280.43 }, + { userOne: 0, userTwo: 3, amount: -1746.24 }, + { userOne: 0, userTwo: 4, amount: 50.35 }, + { userOne: 0, userTwo: 5, amount: -814.16 }, + + { userOne: 1, userTwo: 2, amount: -3245.6 }, + { userOne: 1, userTwo: 3, amount: -115.02 }, + { userOne: 1, userTwo: 4, amount: 1261.15 }, + { userOne: 1, userTwo: 5, amount: 833.33 }, + + { userOne: 2, userTwo: 3, amount: -5191.67 }, + { userOne: 2, userTwo: 4, amount: -4000.5 }, + { userOne: 2, userTwo: 5, amount: -233.34 }, + + { userOne: 3, userTwo: 4, amount: 1572.84 }, + { userOne: 3, userTwo: 5, amount: 158.33 }, + + { userOne: 4, userTwo: 5, amount: 129.16 }, +].flatMap(edgeToGroupBalance); + +describe('simplifyDebts', () => { + it('simplifies small graph', () => { + expect(simplifyDebts(smallGraph).toSorted(sortByIds)).toEqual(smallGraphResult); + }); + + it('gets the same operation count as in the article', () => { + // depending on the edge traversal order, the result can be different, but the total amount and operation count should be the same + expect(simplifyDebts(largeGraph).filter((balance) => balance.amount > 0).length).toBe( + largeGraphResult.filter((balance) => balance.amount > 0).length, + ); + }); + + it('gets the optimal operation count', () => { + expect(simplifyDebts(largeGraph2).filter((balance) => balance.amount > 0).length).toBe(5); + }); + + it.each([{ graph: smallGraph }, { graph: largeGraph }, { graph: largeGraph2 }])( + 'preserves the total balance per user', + () => { + const startingBalances = largeGraph.reduce( + (acc, balance) => { + acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; + return acc; + }, + {} as Record, + ); + + const userBalances = simplifyDebts(largeGraph).reduce( + (acc, balance) => { + acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; + return acc; + }, + {} as Record, + ); + + expect(startingBalances).toEqual(userBalances); + }, + ); +}); From 1f9e37f4de782391f625a688cb6dd4d6fa7b3944 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 09:55:52 +0100 Subject: [PATCH 23/30] Make sure the timestamps are different in test examples to make sure they are matched correctly --- src/lib/simplify.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts index be69231..ccb302c 100644 --- a/src/lib/simplify.test.ts +++ b/src/lib/simplify.test.ts @@ -1,5 +1,6 @@ import { GroupBalance } from '@prisma/client'; import { simplifyDebts } from './simplify'; +import { addHours } from 'date-fns'; const sortByIds = (a: GroupBalance, b: GroupBalance) => { if (a.userId === b.userId) { @@ -8,6 +9,8 @@ const sortByIds = (a: GroupBalance, b: GroupBalance) => { return a.userId - b.userId; }; +let dateCounter = 0; + const edgeToGroupBalance = (edge: { userOne: number; userTwo: number; @@ -16,7 +19,7 @@ const edgeToGroupBalance = (edge: { const base = { groupId: 0, currency: 'USD', - updatedAt: new Date(), + updatedAt: addHours(new Date(), dateCounter++), }; return [ { From 444952c03b026acecdacecc5b83fa34979fc64a3 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 10:15:44 +0100 Subject: [PATCH 24/30] Simplify example generation --- src/lib/simplify.test.ts | 219 +++++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 110 deletions(-) diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts index ccb302c..6fbd6b2 100644 --- a/src/lib/simplify.test.ts +++ b/src/lib/simplify.test.ts @@ -2,6 +2,12 @@ import { GroupBalance } from '@prisma/client'; import { simplifyDebts } from './simplify'; import { addHours } from 'date-fns'; +type MinimalEdge = { + userOne: number; + userTwo: number; + amount: number; +}; + const sortByIds = (a: GroupBalance, b: GroupBalance) => { if (a.userId === b.userId) { return a.firendId - b.firendId; @@ -11,11 +17,7 @@ const sortByIds = (a: GroupBalance, b: GroupBalance) => { let dateCounter = 0; -const edgeToGroupBalance = (edge: { - userOne: number; - userTwo: number; - amount: number; -}): [GroupBalance, GroupBalance] => { +const edgeToGroupBalance = (edge: MinimalEdge): [GroupBalance, GroupBalance] => { const base = { groupId: 0, currency: 'USD', @@ -37,111 +39,108 @@ const edgeToGroupBalance = (edge: { ]; }; +const padWithZeroBalances = (balances: GroupBalance[], userCount: number): GroupBalance[] => { + const result = [...balances]; + for (let userId = 0; userId < userCount; userId++) { + for (let friendId = userId + 1; friendId < userCount; friendId++) { + const found = balances.find( + (balance) => balance.userId === userId && balance.firendId === friendId, + ); + + if (!found) { + result.push(...edgeToGroupBalance({ userOne: userId, userTwo: friendId, amount: 0 })); + } + } + } + return result; +}; + +const getFullBalanceGraph = (edges: MinimalEdge[], userCount: number): GroupBalance[] => { + return padWithZeroBalances(edges.flatMap(edgeToGroupBalance), userCount).toSorted(sortByIds); +}; + // taken from https://www.geeksforgeeks.org/minimize-cash-flow-among-given-set-friends-borrowed-money/ -const smallGraph: GroupBalance[] = [ - { userOne: 0, userTwo: 1, amount: 10 }, - { userOne: 1, userTwo: 2, amount: 50 }, - { userOne: 2, userTwo: 0, amount: -20 }, -].flatMap(edgeToGroupBalance); - -const smallGraphResult: GroupBalance[] = [ - { userOne: 0, userTwo: 1, amount: 0 }, - { userOne: 1, userTwo: 2, amount: 40 }, - { userOne: 2, userTwo: 0, amount: -30 }, -] - .flatMap(edgeToGroupBalance) - .map((resultBalance, idx) => ({ - ...resultBalance, - updatedAt: smallGraph[idx]!.updatedAt, - })) - .toSorted(sortByIds); +const smallGraph: GroupBalance[] = getFullBalanceGraph( + [ + { userOne: 0, userTwo: 1, amount: 10 }, + { userOne: 1, userTwo: 2, amount: 50 }, + { userOne: 2, userTwo: 0, amount: -20 }, + ], + 3, +); + +const smallGraphResult: GroupBalance[] = getFullBalanceGraph( + [ + { userOne: 1, userTwo: 2, amount: 40 }, + { userOne: 2, userTwo: 0, amount: -30 }, + ], + 3, +).map((resultBalance, idx) => ({ + ...resultBalance, + updatedAt: smallGraph[idx]!.updatedAt, +})); // taken from https://medium.com/@mithunmk93/algorithm-behind-splitwises-debt-simplification-feature-8ac485e97688 -const largeGraph: GroupBalance[] = [ - { userOne: 0, userTwo: 1, amount: 0 }, - { userOne: 0, userTwo: 2, amount: 0 }, - { userOne: 0, userTwo: 3, amount: 0 }, - { userOne: 0, userTwo: 4, amount: 0 }, - { userOne: 0, userTwo: 5, amount: 0 }, - { userOne: 0, userTwo: 6, amount: 0 }, - - { userOne: 1, userTwo: 2, amount: 40 }, - { userOne: 1, userTwo: 3, amount: 0 }, - { userOne: 1, userTwo: 4, amount: 0 }, - { userOne: 1, userTwo: 5, amount: -10 }, - { userOne: 1, userTwo: 6, amount: -30 }, - - { userOne: 2, userTwo: 3, amount: 20 }, - { userOne: 2, userTwo: 4, amount: 0 }, - { userOne: 2, userTwo: 5, amount: -30 }, - { userOne: 2, userTwo: 6, amount: 0 }, - - { userOne: 3, userTwo: 4, amount: 50 }, - { userOne: 3, userTwo: 5, amount: -10 }, - { userOne: 3, userTwo: 6, amount: -10 }, - - { userOne: 4, userTwo: 5, amount: -10 }, - { userOne: 4, userTwo: 6, amount: 0 }, - - { userOne: 5, userTwo: 6, amount: 0 }, -].flatMap(edgeToGroupBalance); - -const largeGraphResult: GroupBalance[] = [ - { userOne: 0, userTwo: 1, amount: 0 }, - { userOne: 0, userTwo: 2, amount: 0 }, - { userOne: 0, userTwo: 3, amount: 0 }, - { userOne: 0, userTwo: 4, amount: 0 }, - { userOne: 0, userTwo: 5, amount: 0 }, - { userOne: 0, userTwo: 6, amount: 0 }, - - { userOne: 1, userTwo: 2, amount: 10 }, - { userOne: 1, userTwo: 3, amount: 0 }, - { userOne: 1, userTwo: 4, amount: 0 }, - { userOne: 1, userTwo: 5, amount: 0 }, - { userOne: 1, userTwo: 6, amount: -10 }, - - { userOne: 2, userTwo: 3, amount: 0 }, - { userOne: 2, userTwo: 4, amount: 0 }, - { userOne: 2, userTwo: 5, amount: -40 }, - { userOne: 2, userTwo: 6, amount: 0 }, - - { userOne: 3, userTwo: 4, amount: 40 }, - { userOne: 3, userTwo: 5, amount: 0 }, - { userOne: 3, userTwo: 6, amount: -30 }, - - { userOne: 4, userTwo: 5, amount: -20 }, - { userOne: 4, userTwo: 6, amount: 0 }, - - { userOne: 5, userTwo: 6, amount: 0 }, -] - .flatMap(edgeToGroupBalance) - .map((resultBalance, idx) => ({ - ...resultBalance, - updatedAt: largeGraph[idx]!.updatedAt, - })) - .toSorted(sortByIds); - -const largeGraph2: GroupBalance[] = [ - { userOne: 0, userTwo: 1, amount: 8957.95 }, - { userOne: 0, userTwo: 2, amount: 3280.43 }, - { userOne: 0, userTwo: 3, amount: -1746.24 }, - { userOne: 0, userTwo: 4, amount: 50.35 }, - { userOne: 0, userTwo: 5, amount: -814.16 }, - - { userOne: 1, userTwo: 2, amount: -3245.6 }, - { userOne: 1, userTwo: 3, amount: -115.02 }, - { userOne: 1, userTwo: 4, amount: 1261.15 }, - { userOne: 1, userTwo: 5, amount: 833.33 }, - - { userOne: 2, userTwo: 3, amount: -5191.67 }, - { userOne: 2, userTwo: 4, amount: -4000.5 }, - { userOne: 2, userTwo: 5, amount: -233.34 }, - - { userOne: 3, userTwo: 4, amount: 1572.84 }, - { userOne: 3, userTwo: 5, amount: 158.33 }, - - { userOne: 4, userTwo: 5, amount: 129.16 }, -].flatMap(edgeToGroupBalance); +const largeGraph: GroupBalance[] = getFullBalanceGraph( + [ + { userOne: 1, userTwo: 2, amount: 40 }, + { userOne: 1, userTwo: 5, amount: -10 }, + { userOne: 1, userTwo: 6, amount: -30 }, + + { userOne: 2, userTwo: 3, amount: 20 }, + { userOne: 2, userTwo: 5, amount: -30 }, + + { userOne: 3, userTwo: 4, amount: 50 }, + { userOne: 3, userTwo: 5, amount: -10 }, + { userOne: 3, userTwo: 6, amount: -10 }, + + { userOne: 4, userTwo: 5, amount: -10 }, + ], + 7, +); + +const largeGraphResult: GroupBalance[] = getFullBalanceGraph( + [ + { userOne: 1, userTwo: 2, amount: 10 }, + { userOne: 1, userTwo: 6, amount: -10 }, + + { userOne: 2, userTwo: 5, amount: -40 }, + + { userOne: 3, userTwo: 4, amount: 40 }, + { userOne: 3, userTwo: 6, amount: -30 }, + + { userOne: 4, userTwo: 5, amount: -20 }, + ], + 7, +).map((resultBalance, idx) => ({ + ...resultBalance, + updatedAt: largeGraph[idx]!.updatedAt, +})); + +const largeGraph2: GroupBalance[] = getFullBalanceGraph( + [ + { userOne: 0, userTwo: 1, amount: 8957.95 }, + { userOne: 0, userTwo: 2, amount: 3280.43 }, + { userOne: 0, userTwo: 3, amount: -1746.24 }, + { userOne: 0, userTwo: 4, amount: 50.35 }, + { userOne: 0, userTwo: 5, amount: -814.16 }, + + { userOne: 1, userTwo: 2, amount: -3245.6 }, + { userOne: 1, userTwo: 3, amount: -115.02 }, + { userOne: 1, userTwo: 4, amount: 1261.15 }, + { userOne: 1, userTwo: 5, amount: 833.33 }, + + { userOne: 2, userTwo: 3, amount: -5191.67 }, + { userOne: 2, userTwo: 4, amount: -4000.5 }, + { userOne: 2, userTwo: 5, amount: -233.34 }, + + { userOne: 3, userTwo: 4, amount: 1572.84 }, + { userOne: 3, userTwo: 5, amount: 158.33 }, + + { userOne: 4, userTwo: 5, amount: 129.16 }, + ], + 6, +); describe('simplifyDebts', () => { it('simplifies small graph', () => { @@ -161,8 +160,8 @@ describe('simplifyDebts', () => { it.each([{ graph: smallGraph }, { graph: largeGraph }, { graph: largeGraph2 }])( 'preserves the total balance per user', - () => { - const startingBalances = largeGraph.reduce( + ({ graph }) => { + const startingBalances = graph.reduce( (acc, balance) => { acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; return acc; @@ -170,7 +169,7 @@ describe('simplifyDebts', () => { {} as Record, ); - const userBalances = simplifyDebts(largeGraph).reduce( + const userBalances = simplifyDebts(graph).reduce( (acc, balance) => { acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; return acc; From 2b55ac9d52ae9b12ea6bc3a4afd13074b4d7823a Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 10:25:25 +0100 Subject: [PATCH 25/30] Fix rounding issue in balance calculations for simplifyDebts tests --- src/lib/simplify.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts index 6fbd6b2..fa381db 100644 --- a/src/lib/simplify.test.ts +++ b/src/lib/simplify.test.ts @@ -163,7 +163,8 @@ describe('simplifyDebts', () => { ({ graph }) => { const startingBalances = graph.reduce( (acc, balance) => { - acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; + acc[balance.userId] = + Math.round(100 * ((acc[balance.userId] ?? 0) + balance.amount)) / 100; return acc; }, {} as Record, @@ -171,7 +172,8 @@ describe('simplifyDebts', () => { const userBalances = simplifyDebts(graph).reduce( (acc, balance) => { - acc[balance.userId] = (acc[balance.userId] ?? 0) + balance.amount; + acc[balance.userId] = + Math.round(100 * ((acc[balance.userId] ?? 0) + balance.amount)) / 100; return acc; }, {} as Record, From 66833d00649b2c6adac532a3853be76c41132369 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 10:55:25 +0100 Subject: [PATCH 26/30] Fix getFlows to only get finished edges that form a full path to sink --- src/lib/simplify.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts index 4dbd96d..4cad33a 100644 --- a/src/lib/simplify.ts +++ b/src/lib/simplify.ts @@ -3,7 +3,26 @@ import { GroupBalance } from '@prisma/client'; class CustomDinic extends Dinic { public getFlows(): IDinicEdge[] { - return this._edges.filter((edge) => edge.flow > 0); + const edgesWithFlow = this._edges.filter((edge) => edge.flow > 0); + + return edgesWithFlow.filter((edge) => { + const queue = [edge.to]; + const visited = new Set(); + + while (queue.length > 0) { + const current = queue.shift()!; + if (current === this._sink) return true; + if (visited.has(current)) continue; + visited.add(current); + + for (const e of edgesWithFlow) { + if (e.from === current) { + queue.push(e.to); + } + } + } + return false; + }); } } From b5bddc6b33f83033ea495a5f668d4a628dd4a351 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 11:27:16 +0100 Subject: [PATCH 27/30] Take only initial edges in getFlows, skipping those added by Dinic --- src/lib/simplify.ts | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts index 4cad33a..74bda00 100644 --- a/src/lib/simplify.ts +++ b/src/lib/simplify.ts @@ -3,26 +3,7 @@ import { GroupBalance } from '@prisma/client'; class CustomDinic extends Dinic { public getFlows(): IDinicEdge[] { - const edgesWithFlow = this._edges.filter((edge) => edge.flow > 0); - - return edgesWithFlow.filter((edge) => { - const queue = [edge.to]; - const visited = new Set(); - - while (queue.length > 0) { - const current = queue.shift()!; - if (current === this._sink) return true; - if (visited.has(current)) continue; - visited.add(current); - - for (const e of edgesWithFlow) { - if (e.from === current) { - queue.push(e.to); - } - } - } - return false; - }); + return this._edges.slice(0, this._edgesTot).filter((edge) => edge.flow > 0); } } @@ -52,7 +33,9 @@ function simplifyDebtsForSingleCurrency( const dinic = new CustomDinic(); const visitedEdges = new Set<[number, number]>(); - let graph = groupBalances.filter((balance) => balance.amount > 0); + let graph = groupBalances + .filter((balance) => balance.amount > 0) + .map((balance) => ({ ...balance, amount: Math.round(100 * balance.amount) })); const edgeCount = graph.length; @@ -96,7 +79,10 @@ function simplifyDebtsForSingleCurrency( visitedEdges.add([source, sink]); } - graph = getMirrorBalances(graph); + graph = getMirrorBalances(graph).map((balance) => ({ + ...balance, + amount: balance.amount / 100, + })); groupBalances.forEach((balance) => { const found = graph.find( From f3fced493791a697cda4710c68af97beeff88c0a Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 18 Feb 2025 12:32:00 +0100 Subject: [PATCH 28/30] Use a simpler and more efficient algorithm --- package.json | 1 - pnpm-lock.yaml | 13 --- src/lib/simplify.test.ts | 37 ++------- src/lib/simplify.ts | 171 +++++++++++++++++++++++++-------------- 4 files changed, 119 insertions(+), 103 deletions(-) diff --git a/package.json b/package.json index 41f058c..c8ffb3c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "test:watch": "jest --watch" }, "dependencies": { - "@algorithm.ts/dinic": "^4.0.4", "@aws-sdk/client-s3": "^3.515.0", "@aws-sdk/s3-request-presigner": "^3.515.0", "@heroicons/react": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc4d88d..be78225 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - '@algorithm.ts/dinic': - specifier: ^4.0.4 - version: 4.0.4 '@aws-sdk/client-s3': specifier: ^3.515.0 version: 3.515.0 @@ -252,16 +249,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@algorithm.ts/dinic@4.0.4: - resolution: {integrity: sha512-NH681jIFJNsAkDVWjtmu//LcBkVWGpx+ztdgl3Pdci32kxnH2HXUyMT53nVpowwXvXIzQvLo7hnAKE+qif5Rmw==} - dependencies: - '@algorithm.ts/queue': 4.0.4 - dev: false - - /@algorithm.ts/queue@4.0.4: - resolution: {integrity: sha512-o0Q/3TUWz3GeX7sEyMNx5yaRJ3BA+C8u15CjhSrm+lYhDc666VPH2OC6Q8QqhD+5teNqx/L0iy3mmDVZbU8P5Q==} - dev: false - /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} diff --git a/src/lib/simplify.test.ts b/src/lib/simplify.test.ts index fa381db..c46481e 100644 --- a/src/lib/simplify.test.ts +++ b/src/lib/simplify.test.ts @@ -99,25 +99,7 @@ const largeGraph: GroupBalance[] = getFullBalanceGraph( 7, ); -const largeGraphResult: GroupBalance[] = getFullBalanceGraph( - [ - { userOne: 1, userTwo: 2, amount: 10 }, - { userOne: 1, userTwo: 6, amount: -10 }, - - { userOne: 2, userTwo: 5, amount: -40 }, - - { userOne: 3, userTwo: 4, amount: 40 }, - { userOne: 3, userTwo: 6, amount: -30 }, - - { userOne: 4, userTwo: 5, amount: -20 }, - ], - 7, -).map((resultBalance, idx) => ({ - ...resultBalance, - updatedAt: largeGraph[idx]!.updatedAt, -})); - -const largeGraph2: GroupBalance[] = getFullBalanceGraph( +const denseGraph: GroupBalance[] = getFullBalanceGraph( [ { userOne: 0, userTwo: 1, amount: 8957.95 }, { userOne: 0, userTwo: 2, amount: 3280.43 }, @@ -147,18 +129,15 @@ describe('simplifyDebts', () => { expect(simplifyDebts(smallGraph).toSorted(sortByIds)).toEqual(smallGraphResult); }); - it('gets the same operation count as in the article', () => { - // depending on the edge traversal order, the result can be different, but the total amount and operation count should be the same - expect(simplifyDebts(largeGraph).filter((balance) => balance.amount > 0).length).toBe( - largeGraphResult.filter((balance) => balance.amount > 0).length, - ); - }); - - it('gets the optimal operation count', () => { - expect(simplifyDebts(largeGraph2).filter((balance) => balance.amount > 0).length).toBe(5); + it.each([ + { graph: smallGraph, expected: 2 }, + { graph: largeGraph, expected: 3 }, + { graph: denseGraph, expected: 5 }, + ])('gets the optimal operation count', ({ graph, expected }) => { + expect(simplifyDebts(graph).filter((balance) => balance.amount > 0).length).toBe(expected); }); - it.each([{ graph: smallGraph }, { graph: largeGraph }, { graph: largeGraph2 }])( + it.each([{ graph: smallGraph }, { graph: largeGraph }, { graph: denseGraph }])( 'preserves the total balance per user', ({ graph }) => { const startingBalances = graph.reduce( diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts index 74bda00..4581afa 100644 --- a/src/lib/simplify.ts +++ b/src/lib/simplify.ts @@ -1,12 +1,5 @@ -import { Dinic, type IDinicEdge } from '@algorithm.ts/dinic'; import { GroupBalance } from '@prisma/client'; -class CustomDinic extends Dinic { - public getFlows(): IDinicEdge[] { - return this._edges.slice(0, this._edgesTot).filter((edge) => edge.flow > 0); - } -} - export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { const currencies = new Set(groupBalances.map((balance) => balance.currency)); const nodes = new Set(); @@ -25,80 +18,133 @@ export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { return result; } -// Based on https://github.com/mithun-mohan/Algorithms-Java-Cookbook/blob/master/MaximumFlow/Dinics/SimplifyDebts.java function simplifyDebtsForSingleCurrency( groupBalances: GroupBalance[], nodes: number[], ): GroupBalance[] { - const dinic = new CustomDinic(); - const visitedEdges = new Set<[number, number]>(); - - let graph = groupBalances - .filter((balance) => balance.amount > 0) - .map((balance) => ({ ...balance, amount: Math.round(100 * balance.amount) })); - - const edgeCount = graph.length; + const adjMatrix = new Array(nodes.length) + .fill([]) + .map(() => new Array(nodes.length).fill(0)); - while (visitedEdges.size < edgeCount) { - const { userId, firendId } = graph.find( - (edge) => !visitedEdges.has([edge.userId, edge.firendId]), - )!; + const nonResidualBalances = groupBalances.filter((balance) => balance.amount > 0); - const source = nodes.indexOf(userId); - const sink = nodes.indexOf(firendId); + nonResidualBalances.forEach((balance) => { + const source = nodes.indexOf(balance.userId); + const sink = nodes.indexOf(balance.firendId); + adjMatrix[source]![sink] = balance.amount * 100; + }); - dinic.init(source, sink, nodes.length); + const simplified = minCashFlow(adjMatrix); - graph.forEach((edge) => { - dinic.addEdge(nodes.indexOf(edge.userId), nodes.indexOf(edge.firendId), edge.amount); - }); + const result = getMirrorBalances( + simplified.flatMap((row, source) => { + const res: GroupBalance[] = []; + row.forEach((amount, sink) => { + const balance = groupBalances.find( + (balance) => balance.userId === nodes[source] && balance.firendId === nodes[sink], + )!; - const maxflow = dinic.maxflow(); // the method above gets all the edges with full capacity, but also calculates maxflow - const usedEdges = dinic.getFlows(); + if (amount === 0) { + return; + } - graph = graph - .map((balance) => { - const flow = - usedEdges.find( - (edge) => nodes[edge.from] === balance.userId && nodes[edge.to] === balance.firendId, - )?.flow ?? 0; - return { + res.push({ ...balance, - amount: balance.amount - flow, - }; - }) - .filter((balance) => balance.amount > 0); - - graph.push({ - ...groupBalances.find( - ({ userId, firendId }) => userId === nodes[source] && firendId === nodes[sink], - )!, - amount: maxflow, - }); - - visitedEdges.add([source, sink]); - } - - graph = getMirrorBalances(graph).map((balance) => ({ - ...balance, - amount: balance.amount / 100, - })); + amount: amount / 100, + }); + }); + return res; + }), + ); groupBalances.forEach((balance) => { - const found = graph.find( + const found = result.find( (graphBalance) => graphBalance.userId === balance.userId && graphBalance.firendId === balance.firendId, ); if (!found) { - graph.push({ ...balance, amount: 0 }); + result.push({ ...balance, amount: 0 }); } }); - return graph; + return result; } -// Dinic operates on positive balances only, so we recreate the non-positive balances -// at the end. +// based on https://www.geeksforgeeks.org/minimize-cash-flow-among-given-set-friends-borrowed-money/ +const minCashFlow = (graph: number[][]): number[][] => { + const n = graph.length; + + const amounts = new Array(n).fill(0); + for (let i = 0; i < n; ++i) { + for (let j = 0; j < n; ++j) { + const diff = graph[j]![i]! - graph[i]![j]!; + amounts[i] += diff; + } + } + return solveTransaction(amounts); +}; + +const solveTransaction = (amounts: number[]): number[][] => { + const [minQ, maxQ] = constructMinMaxQ(amounts); + + const result = new Array(amounts.length) + .fill([]) + .map(() => new Array(amounts.length).fill(0)); + + while (minQ.length > 0 && maxQ.length > 0) { + const maxCreditEntry = maxQ.pop()!; + const maxDebitEntry = minQ.pop()!; + + const transaction_val = maxCreditEntry.value + maxDebitEntry.value; + + let debtor = maxDebitEntry.key; + let creditor = maxCreditEntry.key; + let owed_amount; + + if (transaction_val === 0) { + owed_amount = maxCreditEntry.value; + } else if (transaction_val < 0) { + owed_amount = maxCreditEntry.value; + maxDebitEntry.value = transaction_val; + minQ.push(maxDebitEntry); + minQ.sort(compareAsc); + minQ.reverse(); + } else { + owed_amount = -maxDebitEntry.value; + maxCreditEntry.value = transaction_val; + maxQ.push(maxCreditEntry); + maxQ.sort(compareAsc); + } + + result[debtor]![creditor] = owed_amount; + } + + return result; +}; + +const compareAsc = (p1: Entry, p2: Entry): number => p1.value - p2.value; + +const constructMinMaxQ = (amounts: number[]): [Entry[], Entry[]] => { + const minQ: Entry[] = []; + const maxQ: Entry[] = []; + amounts.forEach((amount, index) => { + if (amount === 0) { + return; + } + if (amount > 0) { + maxQ.push({ key: index, value: amount }); + } else { + minQ.push({ key: index, value: amount }); + } + }); + + maxQ.sort(compareAsc); + minQ.sort(compareAsc); + minQ.reverse(); + + return [minQ, maxQ]; +}; + const getMirrorBalances = (groupBalances: GroupBalance[]): GroupBalance[] => { const result = [...groupBalances]; @@ -107,9 +153,14 @@ const getMirrorBalances = (groupBalances: GroupBalance[]): GroupBalance[] => { ...balance, userId: balance.firendId, firendId: balance.userId, - amount: -balance.amount, + amount: balance.amount > 0 ? -balance.amount : 0, }); }); return result; }; + +interface Entry { + key: number; + value: number; +} From dbf6f751cad7d2480a59d262609ad45b0c38e8d9 Mon Sep 17 00:00:00 2001 From: krokosik Date: Wed, 19 Feb 2025 14:31:37 +0100 Subject: [PATCH 29/30] Fix linting errors --- src/components/Expense/BalanceList.tsx | 9 ++--- src/lib/simplify.ts | 10 +++--- src/pages/groups/[groupId].tsx | 49 ++++++++++++-------------- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/components/Expense/BalanceList.tsx b/src/components/Expense/BalanceList.tsx index f4f78fc..534ca0f 100644 --- a/src/components/Expense/BalanceList.tsx +++ b/src/components/Expense/BalanceList.tsx @@ -1,16 +1,13 @@ -import { GroupBalance, User } from '@prisma/client'; -import Link from 'next/link'; -import { UserAvatar } from '~/components/ui/avatar'; +import type { GroupBalance, User } from '@prisma/client'; import clsx from 'clsx'; +import { UserAvatar } from '~/components/ui/avatar'; import { toUIString } from '~/utils/numbers'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../ui/accordion'; interface UserWithBalance { user: User; total: Record; - balances: { - [friendId: number]: Record; - }; + balances: Record>; } export const BalanceList: React.FC<{ diff --git a/src/lib/simplify.ts b/src/lib/simplify.ts index 4581afa..61edb86 100644 --- a/src/lib/simplify.ts +++ b/src/lib/simplify.ts @@ -1,4 +1,4 @@ -import { GroupBalance } from '@prisma/client'; +import type { GroupBalance } from '@prisma/client'; export function simplifyDebts(groupBalances: GroupBalance[]): GroupBalance[] { const currencies = new Set(groupBalances.map((balance) => balance.currency)); @@ -74,11 +74,11 @@ function simplifyDebtsForSingleCurrency( const minCashFlow = (graph: number[][]): number[][] => { const n = graph.length; - const amounts = new Array(n).fill(0); + const amounts = new Array(n).fill(0); for (let i = 0; i < n; ++i) { for (let j = 0; j < n; ++j) { const diff = graph[j]![i]! - graph[i]![j]!; - amounts[i] += diff; + amounts[i] = amounts[i]! + diff; } } return solveTransaction(amounts); @@ -97,8 +97,8 @@ const solveTransaction = (amounts: number[]): number[][] => { const transaction_val = maxCreditEntry.value + maxDebitEntry.value; - let debtor = maxDebitEntry.key; - let creditor = maxCreditEntry.key; + const debtor = maxDebitEntry.key; + const creditor = maxCreditEntry.key; let owed_amount; if (transaction_val === 0) { diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 9ecc9e0..590bd0d 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -1,35 +1,29 @@ -import Head from 'next/head'; -import MainLayout from '~/components/Layout/MainLayout'; import Avatar from 'boring-avatars'; import clsx from 'clsx'; -import { Button } from '~/components/ui/button'; -import { SplitType } from '@prisma/client'; -import { api } from '~/utils/api'; -import { useRouter } from 'next/router'; +import { format } from 'date-fns'; +import { motion } from 'framer-motion'; import { + BarChartHorizontal, Check, ChevronLeft, DoorOpen, + Info, + Merge, Share, Trash2, UserPlus, - BarChartHorizontal, - Info, - Merge, } from 'lucide-react'; -import { AppDrawer } from '~/components/ui/drawer'; -import { UserAvatar } from '~/components/ui/avatar'; -import NoMembers from '~/components/group/NoMembers'; -import { format } from 'date-fns'; -import AddMembers from '~/components/group/AddMembers'; -import Image from 'next/image'; -import { toUIString } from '~/utils/numbers'; +import Head from 'next/head'; import Link from 'next/link'; -import { CategoryIcon } from '~/components/ui/categoryIcons'; -import { env } from '~/env'; -import React, { useMemo, useState } from 'react'; -import { type NextPageWithUser } from '~/types'; -import { motion } from 'framer-motion'; +import { useRouter } from 'next/router'; +import React, { useState } from 'react'; +import { toast } from 'sonner'; +import { BalanceList } from '~/components/Expense/BalanceList'; +import { ExpenseList } from '~/components/Expense/ExpenseList'; +import AddMembers from '~/components/group/AddMembers'; +import GroupMyBalance from '~/components/group/GroupMyBalance'; +import NoMembers from '~/components/group/NoMembers'; +import MainLayout from '~/components/Layout/MainLayout'; import { AlertDialog, AlertDialogCancel, @@ -40,12 +34,15 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '~/components/ui/alert-dialog'; -import { toast } from 'sonner'; -import GroupMyBalance from '~/components/group/GroupMyBalance'; +import { UserAvatar } from '~/components/ui/avatar'; +import { Button } from '~/components/ui/button'; +import { AppDrawer } from '~/components/ui/drawer'; import { Switch } from '~/components/ui/switch'; -import { ExpenseList } from '~/components/Expense/ExpenseList'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs'; -import { BalanceList } from '~/components/Expense/BalanceList'; +import { env } from '~/env'; +import { type NextPageWithUser } from '~/types'; +import { api } from '~/utils/api'; +import { toUIString } from '~/utils/numbers'; const BalancePage: NextPageWithUser<{ enableSendingInvites: boolean; @@ -226,7 +223,7 @@ const BalancePage: NextPageWithUser<{ { groupId }, { onSuccess: () => { - groupDetailQuery.refetch(); + void groupDetailQuery.refetch(); }, onError: () => { toast.error('Failed to update setting'); From 4a0e36acab2e9637f18715a3150c76f4fca3a259 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 25 Feb 2025 09:49:30 +0100 Subject: [PATCH 30/30] Do not show expense and balances in empty group --- src/pages/groups/[groupId].tsx | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 590bd0d..b054b29 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -395,28 +395,28 @@ const BalancePage: NextPageWithUser<{ )} + + + Expenses + Balances + + + + + + gu.user) ?? []} + /> + + )} - - - Expenses - Balances - - - - - - gu.user) ?? []} - /> - - );