diff options
| -rw-r--r-- | package-lock.json | 319 | ||||
| -rw-r--r-- | package.json | 5 | ||||
| -rw-r--r-- | prisma/migrations/20231208170655_add_timestamps/migration.sql | 5 | ||||
| -rw-r--r-- | prisma/migrations/20231208195456_add_delete_cascade/migration.sql | 17 | ||||
| -rw-r--r-- | prisma/schema.prisma | 8 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/expense-list.tsx | 30 | ||||
| -rw-r--r-- | src/app/groups/recent-group-list.tsx | 22 | ||||
| -rw-r--r-- | src/lib/api.ts | 3 | ||||
| -rw-r--r-- | src/scripts/migrate.ts | 130 | ||||
| -rw-r--r-- | tsconfig.json | 9 |
10 files changed, 511 insertions, 37 deletions
diff --git a/package-lock.json b/package-lock.json index 2ee831f..e59bcbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,20 +26,24 @@ "next": "14.0.0", "next-themes": "^0.2.1", "next13-progressbar": "^1.1.1", + "pg": "^8.11.3", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.47.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", + "uuid": "^9.0.1", "zod": "^3.22.4" }, "devDependencies": { "@total-typescript/ts-reset": "^0.5.1", "@types/node": "^20", + "@types/pg": "^8.10.9", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.6", "autoprefixer": "^10", + "dotenv": "^16.3.1", "eslint": "^8", "eslint-config-next": "14.0.0", "postcss": "^8", @@ -47,6 +51,7 @@ "prettier-plugin-organize-imports": "^3.2.3", "prisma": "^5.7.0", "tailwindcss": "^3", + "tsconfig-paths": "^4.2.0", "typescript": "^5" } }, @@ -1364,8 +1369,7 @@ "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/node": { "version": "20.8.9", @@ -1382,6 +1386,74 @@ "resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz", "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==" }, + "node_modules/@types/pg": { + "version": "8.10.9", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", + "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/prop-types": { "version": "15.7.9", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", @@ -1970,6 +2042,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2293,6 +2373,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.569", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz", @@ -2679,6 +2771,18 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/eslint-plugin-import/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2686,6 +2790,18 @@ "dev": true, "license": "MIT" }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", @@ -3859,16 +3975,15 @@ "license": "MIT" }, "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsx-ast-utils": { @@ -4039,7 +4154,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4323,6 +4437,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4382,6 +4502,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4440,6 +4565,98 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4607,6 +4824,47 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5139,6 +5397,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5460,16 +5726,17 @@ "license": "Apache-2.0" }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, - "license": "MIT", "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", + "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/tslib": { @@ -5698,6 +5965,18 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -5813,6 +6092,14 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 6717d13..caba243 100644 --- a/package.json +++ b/package.json @@ -27,20 +27,24 @@ "next": "14.0.0", "next-themes": "^0.2.1", "next13-progressbar": "^1.1.1", + "pg": "^8.11.3", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.47.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", + "uuid": "^9.0.1", "zod": "^3.22.4" }, "devDependencies": { "@total-typescript/ts-reset": "^0.5.1", "@types/node": "^20", + "@types/pg": "^8.10.9", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.6", "autoprefixer": "^10", + "dotenv": "^16.3.1", "eslint": "^8", "eslint-config-next": "14.0.0", "postcss": "^8", @@ -48,6 +52,7 @@ "prettier-plugin-organize-imports": "^3.2.3", "prisma": "^5.7.0", "tailwindcss": "^3", + "tsconfig-paths": "^4.2.0", "typescript": "^5" } } diff --git a/prisma/migrations/20231208170655_add_timestamps/migration.sql b/prisma/migrations/20231208170655_add_timestamps/migration.sql new file mode 100644 index 0000000..b1ec41d --- /dev/null +++ b/prisma/migrations/20231208170655_add_timestamps/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Expense" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Group" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20231208195456_add_delete_cascade/migration.sql b/prisma/migrations/20231208195456_add_delete_cascade/migration.sql new file mode 100644 index 0000000..80914fd --- /dev/null +++ b/prisma/migrations/20231208195456_add_delete_cascade/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "Expense" DROP CONSTRAINT "Expense_groupId_fkey"; + +-- DropForeignKey +ALTER TABLE "Expense" DROP CONSTRAINT "Expense_paidById_fkey"; + +-- DropForeignKey +ALTER TABLE "ExpensePaidFor" DROP CONSTRAINT "ExpensePaidFor_participantId_fkey"; + +-- AddForeignKey +ALTER TABLE "Expense" ADD CONSTRAINT "Expense_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Expense" ADD CONSTRAINT "Expense_paidById_fkey" FOREIGN KEY ("paidById") REFERENCES "Participant"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExpensePaidFor" ADD CONSTRAINT "ExpensePaidFor_participantId_fkey" FOREIGN KEY ("participantId") REFERENCES "Participant"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc88c4e..edf68f2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,7 @@ model Group { currency String @default("$") participants Participant[] expenses Expense[] + createdAt DateTime @default(now()) } model Participant { @@ -30,19 +31,20 @@ model Participant { model Expense { id String @id - group Group @relation(fields: [groupId], references: [id]) + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) title String amount Int - paidBy Participant @relation(fields: [paidById], references: [id]) + paidBy Participant @relation(fields: [paidById], references: [id], onDelete: Cascade) paidById String paidFor ExpensePaidFor[] groupId String isReimbursement Boolean @default(false) + createdAt DateTime @default(now()) } model ExpensePaidFor { expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade) - participant Participant @relation(fields: [participantId], references: [id]) + participant Participant @relation(fields: [participantId], references: [id], onDelete: Cascade) expenseId String participantId String diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx index 13a85bd..798a5f8 100644 --- a/src/app/groups/[groupId]/expenses/expense-list.tsx +++ b/src/app/groups/[groupId]/expenses/expense-list.tsx @@ -1,5 +1,4 @@ 'use client' -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { getGroupExpenses } from '@/lib/api' import { cn } from '@/lib/utils' @@ -7,6 +6,7 @@ import { Participant } from '@prisma/client' import { ChevronRight } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' +import { Fragment } from 'react' type Props = { expenses: Awaited<ReturnType<typeof getGroupExpenses>> @@ -37,22 +37,32 @@ export function ExpenseList({ }} > <div> - <div className="mb-1">{expense.title}</div> + <div className={cn('mb-1', expense.isReimbursement && 'italic')}> + {expense.title} + </div> <div className="text-xs text-muted-foreground"> - Paid by{' '} - <Badge variant="secondary"> - {getParticipant(expense.paidById)?.name} - </Badge>{' '} + Paid by <strong>{getParticipant(expense.paidById)?.name}</strong>{' '} for{' '} {expense.paidFor.map((paidFor, index) => ( - <Badge variant="secondary" key={index} className="mr-1 mb-1"> - {participants.find((p) => p.id === paidFor.participantId)?.name} - </Badge> + <Fragment key={index}> + {index !== 0 && <>, </>} + <strong> + { + participants.find((p) => p.id === paidFor.participantId) + ?.name + } + </strong> + </Fragment> ))} </div> </div> <div className="flex items-center"> - <div className="tabular-nums whitespace-nowrap font-bold"> + <div + className={cn( + 'tabular-nums whitespace-nowrap', + expense.isReimbursement ? 'italic' : 'font-bold', + )} + > {currency} {(expense.amount / 100).toFixed(2)} </div> <Button size="icon" variant="link" className="-my-2" asChild> diff --git a/src/app/groups/recent-group-list.tsx b/src/app/groups/recent-group-list.tsx index ef43372..92bc743 100644 --- a/src/app/groups/recent-group-list.tsx +++ b/src/app/groups/recent-group-list.tsx @@ -2,7 +2,7 @@ import { getRecentGroups } from '@/app/groups/recent-groups-helpers' import { Button } from '@/components/ui/button' import { getGroups } from '@/lib/api' -import { Loader2 } from 'lucide-react' +import { Calendar, Loader2, Users } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { z } from 'zod' @@ -58,14 +58,24 @@ export function RecentGroupList({ getGroupsAction }: Props) { <li key={group.id}> <Button variant="outline" className="h-fit w-full py-3" asChild> <Link href={`/groups/${group.id}`} className="text-base"> - <div className="w-full flex flex-col items-start gap-1"> + <div className="w-full flex flex-col gap-1"> <div className="text-base">{group.name}</div> <div className="text-muted-foreground font-normal text-xs"> {details ? ( - <> - {details._count.participants} participants ยท{' '} - {details.currency} - </> + <div className="w-full flex items-center justify-between"> + <div className="flex items-center"> + <Users className="w-3 h-3 inline mr-1" /> + <span>{details._count.participants}</span> + </div> + <div className="flex items-center"> + <Calendar className="w-3 h-3 inline mx-1" /> + <span> + {details.createdAt.toLocaleDateString('en-US', { + dateStyle: 'medium', + })} + </span> + </div> + </div> ) : ( <> <Loader2 className="w-3 h-3 mr-1 inline animate-spin" /> diff --git a/src/lib/api.ts b/src/lib/api.ts index 33e3eac..b6a8d65 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2,7 +2,7 @@ import { getPrisma } from '@/lib/prisma' import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas' import { Expense } from '@prisma/client' -function randomId() { +export function randomId() { return Math.random().toString(36).slice(2, 9) } @@ -185,6 +185,7 @@ export async function getGroupExpenses(groupId: string) { return prisma.expense.findMany({ where: { groupId }, include: { paidFor: { include: { participant: true } }, paidBy: true }, + orderBy: { createdAt: 'desc' }, }) } diff --git a/src/scripts/migrate.ts b/src/scripts/migrate.ts new file mode 100644 index 0000000..bf7d93c --- /dev/null +++ b/src/scripts/migrate.ts @@ -0,0 +1,130 @@ +// @ts-nocheck +import { randomId } from '@/lib/api' +import { getPrisma } from '@/lib/prisma' +import { Prisma } from '@prisma/client' +import { Client } from 'pg' + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + +async function main() { + withClient(async (client) => { + const prisma = await getPrisma() + + const { rows: groupRows } = await client.query<{ + id: string + name: string + currency: string + created_at: Date + }>('select id, name, currency, created_at from groups') + + for (const groupRow of groupRows) { + const participants: Prisma.ParticipantCreateManyInput[] = [] + const expenses: Prisma.ExpenseCreateManyInput[] = [] + const expenseParticipants: Prisma.ExpensePaidForCreateManyInput[] = [] + const participantIdsMapping: Record<number, string> = {} + const expenseIdsMapping: Record<number, string> = {} + + const existingGroup = await prisma.group.findUnique({ + where: { id: groupRow.id }, + }) + if (existingGroup) { + console.log(`Group ${groupRow.id} already exists, skipping.`) + continue + } + + const group: Prisma.GroupCreateInput = { + id: groupRow.id, + name: groupRow.name, + currency: groupRow.currency, + createdAt: groupRow.created_at, + } + + const { rows: participantRows } = await client.query<{ + id: number + created_at: Date + name: string + }>( + 'select id, created_at, name from participants where group_id = $1::text', + [groupRow.id], + ) + for (const participantRow of participantRows) { + const id = randomId() + participantIdsMapping[participantRow.id] = id + participants.push({ + id, + groupId: groupRow.id, + name: participantRow.name, + }) + } + + const { rows: expenseRows } = await client.query<{ + id: number + created_at: Date + description: string + amount: number + paid_by_participant_id: number + is_reimbursement: boolean + }>( + 'select id, created_at, description, amount, paid_by_participant_id, is_reimbursement from expenses where group_id = $1::text', + [groupRow.id], + ) + for (const expenseRow of expenseRows) { + const id = randomId() + expenseIdsMapping[expenseRow.id] = id + expenses.push({ + id, + amount: Math.round(expenseRow.amount * 100), + groupId: groupRow.id, + title: expenseRow.description, + createdAt: expenseRow.created_at, + isReimbursement: expenseRow.is_reimbursement === true, + paidById: participantIdsMapping[expenseRow.paid_by_participant_id], + }) + } + + if (expenseRows.length > 0) { + const { rows: expenseParticipantRows } = await client.query<{ + expense_id: number + participant_id: number + }>( + 'select expense_id, participant_id from expense_participants where expense_id = any($1::int[]);', + [expenseRows.map((row) => row.id)], + ) + for (const expenseParticipantRow of expenseParticipantRows) { + expenseParticipants.push({ + expenseId: expenseIdsMapping[expenseParticipantRow.expense_id], + participantId: + participantIdsMapping[expenseParticipantRow.participant_id], + }) + } + } + + console.log('Creating group:', group) + await prisma.group.create({ data: group }) + console.log('Creating participants:', participants) + await prisma.participant.createMany({ data: participants }) + console.log('Creating expenses:', expenses) + await prisma.expense.createMany({ data: expenses }) + console.log('Creating expenseParticipants:', expenseParticipants) + await prisma.expensePaidFor.createMany({ data: expenseParticipants }) + } + }) +} + +async function withClient(fn: (client: Client) => void | Promise<void>) { + const client = new Client({ + connectionString: process.env.OLD_POSTGRES_URL, + ssl: true, + }) + await client.connect() + console.log('Connected.') + + try { + await fn(client) + } finally { + await client.end() + console.log('Disconnected.') + } +} + +main().catch(console.error) diff --git a/tsconfig.json b/tsconfig.json index e59724b..07023db 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,12 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "ts-node": { + "require": ["tsconfig-paths/register", "dotenv/config"], + "compilerOptions": { + "isolatedModules": false, + "module": "CommonJS" + } + } } |
