main page and group car

This commit is contained in:
mr-shortman 2025-04-14 23:32:15 +02:00
parent 3f4cc126ec
commit 62f1689c1a
20 changed files with 684 additions and 145 deletions

View File

@ -13,6 +13,7 @@
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx ./src/server/db/seed.ts",
"dev": "next dev --turbo",
"preview": "next build && next start",
"start": "next start",
@ -43,6 +44,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.41.0",
"lucide-react": "^0.487.0",
"next": "^15.2.3",
@ -57,6 +59,7 @@
"superjson": "^2.2.1",
"svix": "^1.64.0",
"tailwind-merge": "^3.1.0",
"tsx": "^4.19.3",
"tw-animate-css": "^1.2.5",
"use-debounce": "^10.0.4",
"vaul": "^1.1.2",

282
pnpm-lock.yaml generated
View File

@ -77,6 +77,9 @@ importers:
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
dotenv:
specifier: ^16.5.0
version: 16.5.0
drizzle-orm:
specifier: ^0.41.0
version: 0.41.0(gel@2.0.1)(postgres@3.4.5)
@ -119,6 +122,9 @@ importers:
tailwind-merge:
specifier: ^3.1.0
version: 3.1.0
tsx:
specifier: ^4.19.3
version: 4.19.3
tw-animate-css:
specifier: ^1.2.5
version: 1.2.5
@ -798,6 +804,12 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.25.2':
resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.18.20':
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
@ -810,6 +822,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.2':
resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.18.20':
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
@ -822,6 +840,12 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.2':
resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.18.20':
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
@ -834,6 +858,12 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.2':
resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.18.20':
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
@ -846,6 +876,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.2':
resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.18.20':
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
@ -858,6 +894,12 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.2':
resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.18.20':
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
@ -870,6 +912,12 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.2':
resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.18.20':
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
@ -882,6 +930,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.2':
resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.18.20':
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
@ -894,6 +948,12 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.2':
resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.18.20':
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
@ -906,6 +966,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.2':
resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.18.20':
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
@ -918,6 +984,12 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.2':
resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.18.20':
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
@ -930,6 +1002,12 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.2':
resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.18.20':
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
@ -942,6 +1020,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.2':
resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.18.20':
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
@ -954,6 +1038,12 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.2':
resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.18.20':
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
@ -966,6 +1056,12 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.2':
resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.18.20':
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
@ -978,6 +1074,12 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.2':
resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.18.20':
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
@ -990,6 +1092,18 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.2':
resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.2':
resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.18.20':
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
@ -1002,6 +1116,18 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.2':
resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.2':
resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.18.20':
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
@ -1014,6 +1140,12 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.2':
resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.18.20':
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
@ -1026,6 +1158,12 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.2':
resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.18.20':
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
@ -1038,6 +1176,12 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.2':
resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.18.20':
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
@ -1050,6 +1194,12 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.2':
resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.18.20':
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
@ -1062,6 +1212,12 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.2':
resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@floating-ui/core@1.6.9':
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
@ -2262,6 +2418,10 @@ packages:
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dotenv@16.5.0:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'}
drizzle-kit@0.30.6:
resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==}
hasBin: true
@ -2424,6 +2584,11 @@ packages:
engines: {node: '>=12'}
hasBin: true
esbuild@0.25.2:
resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@ -3538,6 +3703,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.19.3:
resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==}
engines: {node: '>=18.0.0'}
hasBin: true
tw-animate-css@1.2.5:
resolution: {integrity: sha512-ABzjfgVo+fDbhRREGL4KQZUqqdPgvc5zVrLyeW9/6mVqvaDepXc7EvedA+pYmMnIOsUAQMwcWzNvom26J2qYvQ==}
@ -4584,138 +4754,213 @@ snapshots:
'@esbuild/aix-ppc64@0.19.12':
optional: true
'@esbuild/aix-ppc64@0.25.2':
optional: true
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm64@0.19.12':
optional: true
'@esbuild/android-arm64@0.25.2':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-arm@0.19.12':
optional: true
'@esbuild/android-arm@0.25.2':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/android-x64@0.19.12':
optional: true
'@esbuild/android-x64@0.25.2':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.19.12':
optional: true
'@esbuild/darwin-arm64@0.25.2':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.19.12':
optional: true
'@esbuild/darwin-x64@0.25.2':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.19.12':
optional: true
'@esbuild/freebsd-arm64@0.25.2':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.19.12':
optional: true
'@esbuild/freebsd-x64@0.25.2':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.19.12':
optional: true
'@esbuild/linux-arm64@0.25.2':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-arm@0.19.12':
optional: true
'@esbuild/linux-arm@0.25.2':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-ia32@0.19.12':
optional: true
'@esbuild/linux-ia32@0.25.2':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-loong64@0.19.12':
optional: true
'@esbuild/linux-loong64@0.25.2':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.19.12':
optional: true
'@esbuild/linux-mips64el@0.25.2':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.19.12':
optional: true
'@esbuild/linux-ppc64@0.25.2':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.19.12':
optional: true
'@esbuild/linux-riscv64@0.25.2':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-s390x@0.19.12':
optional: true
'@esbuild/linux-s390x@0.25.2':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/linux-x64@0.19.12':
optional: true
'@esbuild/linux-x64@0.25.2':
optional: true
'@esbuild/netbsd-arm64@0.25.2':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.19.12':
optional: true
'@esbuild/netbsd-x64@0.25.2':
optional: true
'@esbuild/openbsd-arm64@0.25.2':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.19.12':
optional: true
'@esbuild/openbsd-x64@0.25.2':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.19.12':
optional: true
'@esbuild/sunos-x64@0.25.2':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.19.12':
optional: true
'@esbuild/win32-arm64@0.25.2':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-ia32@0.19.12':
optional: true
'@esbuild/win32-ia32@0.25.2':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@esbuild/win32-x64@0.19.12':
optional: true
'@esbuild/win32-x64@0.25.2':
optional: true
'@floating-ui/core@1.6.9':
dependencies:
'@floating-ui/utils': 0.2.9
@ -5864,6 +6109,8 @@ snapshots:
no-case: 3.0.4
tslib: 2.8.1
dotenv@16.5.0: {}
drizzle-kit@0.30.6:
dependencies:
'@drizzle-team/brocli': 0.10.2
@ -6037,6 +6284,34 @@ snapshots:
'@esbuild/win32-ia32': 0.19.12
'@esbuild/win32-x64': 0.19.12
esbuild@0.25.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.2
'@esbuild/android-arm': 0.25.2
'@esbuild/android-arm64': 0.25.2
'@esbuild/android-x64': 0.25.2
'@esbuild/darwin-arm64': 0.25.2
'@esbuild/darwin-x64': 0.25.2
'@esbuild/freebsd-arm64': 0.25.2
'@esbuild/freebsd-x64': 0.25.2
'@esbuild/linux-arm': 0.25.2
'@esbuild/linux-arm64': 0.25.2
'@esbuild/linux-ia32': 0.25.2
'@esbuild/linux-loong64': 0.25.2
'@esbuild/linux-mips64el': 0.25.2
'@esbuild/linux-ppc64': 0.25.2
'@esbuild/linux-riscv64': 0.25.2
'@esbuild/linux-s390x': 0.25.2
'@esbuild/linux-x64': 0.25.2
'@esbuild/netbsd-arm64': 0.25.2
'@esbuild/netbsd-x64': 0.25.2
'@esbuild/openbsd-arm64': 0.25.2
'@esbuild/openbsd-x64': 0.25.2
'@esbuild/sunos-x64': 0.25.2
'@esbuild/win32-arm64': 0.25.2
'@esbuild/win32-ia32': 0.25.2
'@esbuild/win32-x64': 0.25.2
escalade@3.2.0: {}
eslint-scope@5.1.1:
@ -7173,6 +7448,13 @@ snapshots:
tslib@2.8.1: {}
tsx@4.19.3:
dependencies:
esbuild: 0.25.2
get-tsconfig: 4.10.0
optionalDependencies:
fsevents: 2.3.3
tw-animate-css@1.2.5: {}
type-fest@0.16.0: {}

View File

@ -8,7 +8,6 @@ import React from "react";
export default async function Page() {
const user = await currentUser();
if (user) void api.friend.getAll.prefetch();
const sessionUser = await api.user.getSessionUser();
return (

View File

@ -7,7 +7,9 @@ import Link from "next/link";
import GroupCard from "@/app/_components/group/group-card";
export default async function Page() {
const groups = await api.group.getAll();
const groups = await api.group.getAll({
withMembers: true,
});
return (
<>
<Header text="Groups">

View File

@ -5,6 +5,10 @@ import Section from "@/components/section";
import { ModeToggle } from "@/components/mode-toggle";
import { UserButton } from "@clerk/nextjs";
import { Icons } from "@/components/icons";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { getAmount } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default async function Home() {
return (
@ -26,7 +30,35 @@ export default async function Home() {
<UserButton />
</div>
</Header>
<Section></Section>
<Section className="">
<Card className="bg-card/5">
<CardHeader className="flex items-center">
<CardTitle className="size-full flex flex-col gap-1">
<p>
Your Balance{" "}
<span className="font-normal text-sm text-muted-foreground">
(You Owe)
</span>
</p>
<span className="text-xl text-destructive">
{getAmount(812.47)}
</span>
</CardTitle>
<Link
className="cursor-pointer h-max flex items-center bg-primary text-primary-foreground rounded-lg pl-4 w-24 justify-between"
href={"/add"}
>
<span>Add</span>
<Icons.addSquare className="size-8" />
</Link>
</CardHeader>
</Card>
<div className="size-full border rounded-xl"></div>
<div className="size-full border rounded-xl"></div>
<div className="size-full border rounded-xl"></div>
<div className="size-full border rounded-xl max-h-52"></div>
</Section>
</>
);
}

View File

@ -0,0 +1,96 @@
import React from "react";
import { Combobox } from "@/components/combobox";
import { api } from "@/trpc/react";
import Avatar from "@/components/avatar";
import type { User } from "@/server/db/schema";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { useDebounce } from "use-debounce";
import { Icons } from "@/components/icons";
import { useExpenseStore } from "@/lib/store/expense-store";
export default function AddParticipants({
excludedIds,
}: {
initialValue?: string;
className?: string;
excludedIds: Array<string>;
}) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
const [search] = useDebounce(value, 1000);
const { data } = api.expense.searchParticipants.useQuery({
search,
excludedIds,
});
const addParticipants = useExpenseStore((state) => state.addParticipants);
const addGroupBadges = useExpenseStore((state) => state.addGroupBadges);
const select = (users: Array<User>) => {
addParticipants(users);
setOpen(false);
};
return (
<>
<div
className="px-4 py-2 border rounded-xl text-center"
onClick={() => setOpen(true)}
>
Add Friends or Groups
</div>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
onValueChange={setValue}
placeholder="Type a command or search..."
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{data?.groups?.length ? (
<CommandGroup heading="Groups">
{data?.groups.map(({ group }) => (
<CommandItem
key={group.id}
value={`${group.name}:::${group.id}`}
onSelect={() => {
if (group.members) select(group.members.map((m) => m.user));
addGroupBadges([group]);
}}
>
<Icons.group className="size-8" />
<span>{group.name}</span>
</CommandItem>
))}
</CommandGroup>
) : null}
{data?.friends?.length ? (
<CommandGroup heading="Friends">
{data?.friends.map(({ user, id }) => (
<CommandItem
key={id}
value={`${user.name}:::${user.id}`}
onSelect={() => {
select([user]);
}}
>
<Avatar className="size-8" src={user.image} fb={user.name} />
<span>{user.name}</span>
</CommandItem>
))}
</CommandGroup>
) : null}
</CommandList>
</CommandDialog>
</>
);
}

View File

@ -45,7 +45,7 @@ function ExpenseForm({
});
const participants = useExpenseStore((state) => state.participants);
const addParticipant = useExpenseStore((state) => state.addParticipant);
const addParticipants = useExpenseStore((state) => state.addParticipants);
const setAmount = useExpenseStore((state) => state.setAmount);
const splitType = useExpenseStore((state) => state.splitType);
const setPayments = useExpenseStore((state) => state.setPayments);
@ -59,14 +59,14 @@ function ExpenseForm({
onSuccess(expense) {
form.reset();
resetExpenseStore();
addParticipant(sessionUser);
addParticipants([sessionUser]);
setPayments([{ amount: amount, userId: sessionUser.id }]);
toast.message("Expense Created", {
position: "bottom-center",
action: {
children: "test",
onClick: () => {
router.push(`/expense/${expense.id}`);
router.push(`/expense`);
},
label: "View Expense",
},
@ -93,7 +93,7 @@ function ExpenseForm({
}
React.useEffect(() => {
addParticipant(sessionUser);
addParticipants([sessionUser]);
setPayments([{ amount, userId: sessionUser.id }]);
}, []);

View File

@ -8,7 +8,7 @@ import { api } from "@/trpc/react";
import { Button } from "@/components/ui/button";
import { XIcon } from "lucide-react";
import type { User } from "@/server/db/schema";
import AddParticipants from "./select-participants";
import AddParticipants from "./add-participants";
export const UserBadge = ({
user,
@ -33,18 +33,44 @@ export default function ExpenseParticipants({
sessionUserId: string;
}) {
const participants = useExpenseStore((state) => state.participants);
const addParticipant = useExpenseStore((state) => state.addParticipant);
const removeParticipant = useExpenseStore((state) => state.removeParticipant);
const excludeIds = participants.map((user) => user.id!);
const groupsBadges = useExpenseStore((state) => state.groupBadges);
const removeGroupBadge = useExpenseStore((state) => state.removeGroupBadge);
const excludeIds = [
...participants.map((user) => user.id!),
...groupsBadges.map((g) => g.id),
];
return (
<div className="p-4 space-y-4">
{participants.map((user) => (
<ul key={user.id} className="flex gap-2 w-full items-center flex-wrap">
<UserBadge user={user}>
<ul className="flex gap-2 w-full items-center flex-wrap ">
{groupsBadges.map((group) => (
<UserBadge
key={group.id}
user={{
name: group.name,
image: "",
id: group.id,
}}
>
<Button
className=" rounded-full aspect-square size-6 "
variant="destructive"
onClick={(e) => {
e.preventDefault();
removeGroupBadge([group.id!]);
}}
>
<XIcon className="size-4" />
</Button>
</UserBadge>
))}
</ul>
<ul className="flex gap-2 w-full items-center flex-wrap ">
{participants.map((user) => (
<UserBadge user={user} key={user.id}>
{sessionUserId !== user.id && (
<Button
className=" rounded-full aspect-square size-6"
className=" rounded-full aspect-square size-6 "
variant="destructive"
onClick={(e) => {
e.preventDefault();
@ -55,16 +81,9 @@ export default function ExpenseParticipants({
</Button>
)}
</UserBadge>
</ul>
))}
<AddParticipants
excludeIds={excludeIds}
onAdd={(users) => {
// addParticipant(
// friends.find((friend) => friend.user.id === userId)!.user
// );
}}
/>
))}
</ul>
<AddParticipants excludedIds={excludeIds} />
</div>
);
}

View File

@ -1,42 +0,0 @@
import React from "react";
import { Combobox } from "@/components/combobox";
import { api } from "@/trpc/react";
import Avatar from "@/components/avatar";
import type { User } from "@/server/db/schema";
export default function AddParticipants({
onAdd,
initialValue,
className,
excludeIds,
}: {
onAdd: (users: Array<User>) => void;
initialValue?: string;
className?: string;
excludeIds?: Array<string>;
}) {
const [friends] = api.friend.getAll.useSuspenseQuery();
const friendsData = friends
.filter((friend) => !excludeIds?.includes(friend.user.id))
.map(({ user }) => ({
label: user.name!,
value: user.id,
children: <Avatar src={user.image} fb={user.name} className="size-6" />,
}));
return (
// Custom Combobox needed mby go with command dialog
<Combobox
className={"w-full "}
data={friendsData}
onSelect={(userId) => onAdd([])}
initialValue={initialValue}
messageUi={{
select: "Add Friends or Groups",
empty: "No friends or groups found",
placeholder: "Search friends or groups",
}}
/>
);
}

View File

@ -46,9 +46,11 @@ const SearchResultCard = ({
export default function AddToGroupDrawer({
groupId,
className,
excludedIds,
}: {
groupId: string;
className?: string;
excludedIds: Array<string>;
}) {
const utils = api.useUtils();
const [open, setOpen] = React.useState(false);
@ -58,6 +60,7 @@ export default function AddToGroupDrawer({
const { data: searchResult, isFetching } = api.user.search.useQuery(
{
search: searchValue,
excludedIds,
},
{
enabled: !!searchValue,

View File

@ -1,12 +1,64 @@
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import Avatar from "@/components/avatar";
import { Icons } from "@/components/icons";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { Group } from "@/server/db/schema";
import type { User } from "@clerk/nextjs/server";
import Link from "next/link";
import React from "react";
function GroupCard({ group }: { group: Group }) {
return (
<Card>
<CardHeader>
<CardTitle>{group.name}</CardTitle>
<CardHeader className="flex items-center justify-between">
<div className=" flex flex-col gap-2">
<div className="flex items-center gap-4">
<CardTitle className="flex items-center gap-1">
<Icons.group className="size-4" />
<span>{group.name}</span>
</CardTitle>
<ul className="flex ">
{group?.members?.slice(0, 4)?.map(({ user }, idx) => (
<li
key={idx}
style={{
transform: `translateX(-${idx * 4}px)`,
}}
>
<Avatar
src={user?.image}
fb={user?.name}
className="size-4"
/>
</li>
))}
{Number(group?.members?.length) > 4 && (
<li
style={{
transform: `translateX(-20px)`,
}}
className="size-4 bg-foreground text-background text-xs items-center justify-center rounded-full "
>
<span>+{Number(group?.members?.length) - 4}</span>
</li>
)}
</ul>
</div>
<CardDescription>
<p>{group.description}</p>
</CardDescription>
</div>
<Link
className="cursor-pointer h-max flex items-center border bg-foreground text-background rounded-lg pl-4 w-24 justify-between"
href={"/add"}
>
<span>Add</span>
<Icons.addSquare className="size-8" />
</Link>
</CardHeader>
</Card>
);

View File

@ -26,7 +26,11 @@ function GroupPage({ groupId }: { groupId: string }) {
<h4 className="text-lg font-semibold">
Members ({group.members.length})
</h4>
<AddToGroupDrawer groupId={group.id} className="ml-auto" />
<AddToGroupDrawer
groupId={group.id}
className="ml-auto"
excludedIds={group?.members?.map((m) => m.user.id)}
/>
</div>
<ul className="space-y-2">
{group.members.map((member) => (

View File

@ -1,8 +1,7 @@
"use client";
import { create } from "zustand";
import { calculateSplits, type Payment } from "@/lib/utils/expense";
import type { Session } from "next-auth";
import type { PublicUser } from "next-auth";
import type { Group, User } from "@/server/db/schema";
export enum SplitTypeEnum {
Fixed = "fixed",
@ -14,18 +13,21 @@ export const splitTypeKeys = Object.keys(SplitTypeEnum);
interface ExpenseStore {
amount: number;
session: Session | null;
participants: PublicUser[];
participants: User[];
groupBadges: Group[];
friendTarget?: string;
splitType: SplitType;
payments: Payment[];
splits: Record<string, string>;
setAmount: (amount: number) => void;
setSession: (session: Session) => void;
addParticipant: (user: PublicUser) => void;
addGroupBadges: (groupIds: Group[]) => void;
removeGroupBadge: (groupIds: string[]) => void;
addParticipants: (user: User[]) => void;
removeParticipant: (userId: string) => void;
setFriendTarget: (id?: string) => void;
@ -43,9 +45,11 @@ const defaultValues: Pick<
| "splitType"
| "payments"
| "splits"
| "groupBadges"
> = {
amount: 0,
participants: [],
groupBadges: [],
friendTarget: undefined,
splitType: "Equal",
payments: [],
@ -54,25 +58,14 @@ const defaultValues: Pick<
export const useExpenseStore = create<ExpenseStore>((set, get) => ({
...defaultValues,
session: null,
setAmount: (amount) => set({ amount }),
setSession: (session) =>
set(
{
session,
participants: [session.user],
payments: [{ amount: get().amount, userId: session.user.id }],
},
false
),
addParticipant: (newUser) => {
addParticipants: (newUsers) => {
const { participants } = get();
if (!participants.find((user) => user.id === newUser.id)) {
set({ participants: [...participants, newUser] });
}
const uniqueUsers = newUsers.filter(
(user) => !participants.find((u) => u.id === user.id)
);
set({ participants: [...participants, ...uniqueUsers] });
},
removeParticipant: (userId) => {
const filtered = get().participants.filter((user) => user.id !== userId);
set({ participants: filtered });
@ -92,5 +85,17 @@ export const useExpenseStore = create<ExpenseStore>((set, get) => ({
set({ splits });
},
addGroupBadges: (newGroups) => {
const { groupBadges } = get();
const uniqueGroups = newGroups.filter(
(group) => !groupBadges.find((g) => g.id === group.id)
);
set({ groupBadges: [...groupBadges, ...uniqueGroups] });
},
removeGroupBadge: (groupIds) =>
set({
groupBadges: get().groupBadges.filter(({ id }) => !groupIds.includes(id)),
}),
resetExpenseStore: () => set(defaultValues),
}));

View File

@ -5,10 +5,13 @@ import {
expenses,
expenseSplits,
friendships,
groupMembers,
type ExpenseSplit,
} from "@/server/db/schema";
import { expenseSchema, expenseSplitSchema } from "@/lib/validations/expense";
import { and, eq, inArray, not, notInArray, or } from "drizzle-orm";
import { and, desc, eq, inArray, not, notInArray, or } from "drizzle-orm";
import { api } from "@/trpc/server";
import { getAllFriends } from "./friend";
function restructureExpenseSplits(expenseSplits: Array<ExpenseSplit>) {
const expensesMap = new Map();
@ -42,6 +45,7 @@ export const expenseRouter = createTRPCRouter({
owedTo: true,
expense: true,
},
orderBy: desc(expenseSplits.createdAt),
});
const expenses = restructureExpenseSplits(splits);
return expenses;
@ -50,28 +54,31 @@ export const expenseRouter = createTRPCRouter({
searchParticipants: protectedProcedure
.input(z.object({ search: z.string(), excludedIds: z.array(z.string()) }))
.query(async ({ ctx, input }) => {
const userId = ctx.auth.userId;
const friendResult = await ctx.db.query.friendships.findMany({
const friends = await getAllFriends(ctx, {
pending: true,
ids: input.excludedIds,
});
const groups = await ctx.db.query.groupMembers.findMany({
where: and(
or(
eq(friendships.userOneId, userId),
eq(friendships.userTwoId, userId)
),
notInArray(friendships.userOneId, input.excludedIds),
notInArray(friendships.userTwoId, input.excludedIds)
eq(groupMembers.userId, ctx.auth.userId),
notInArray(groupMembers.groupId, input.excludedIds)
),
with: {
userOne: true,
userTwo: true,
group: {
with: {
members: {
with: {
user: true,
},
},
},
},
},
});
const friends =
friendResult?.map(({ userOne, userTwo }) =>
ctx.auth.userId === userOne.id ? userTwo : userOne
) ?? [];
return {
friends,
groups: [],
groups,
};
}),

View File

@ -1,30 +1,60 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
type TRPCContext,
} from "@/server/api/trpc";
import { friendships, users } from "@/server/db/schema";
import { and, eq, ilike, inArray, not, or } from "drizzle-orm";
import { and, eq, ilike, inArray, not, notInArray, or } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export async function getAllFriends(
ctx: TRPCContext,
exluded?: { pending?: boolean; ids?: Array<string> }
) {
const friends = await ctx.db.query.friendships.findMany({
where: and(
or(
eq(friendships.userOneId, ctx.auth.userId!),
eq(friendships.userTwoId, ctx.auth.userId!)
),
exluded?.pending ? eq(friendships.status, "accepted") : undefined,
exluded?.ids?.length
? or(
notInArray(friendships.userOneId, exluded.ids),
notInArray(friendships.userTwoId, exluded.ids)
)
: undefined
),
with: {
userOne: true,
userTwo: true,
},
});
return friends.map((f) => ({
id: f.id,
status: f.status,
requestedBy: ctx.auth.userId === f.userOneId ? "me" : "them",
user: ctx.auth.userId === f.userOneId ? f.userTwo : f.userOne,
}));
}
export const friendRouter = createTRPCRouter({
// queries
getAll: protectedProcedure.query(async ({ ctx }) => {
const friends = await ctx.db.query.friendships.findMany({
where: or(
eq(friendships.userOneId, ctx.auth.userId),
eq(friendships.userTwoId, ctx.auth.userId)
),
with: {
userOne: true,
userTwo: true,
},
});
return friends.map((f) => ({
id: f.id,
status: f.status,
requestedBy: ctx.auth.userId === f.userOneId ? "me" : "them",
user: ctx.auth.userId === f.userOneId ? f.userTwo : f.userOne,
}));
}),
getAll: protectedProcedure
.input(
z
.object({
excludePending: z.boolean().optional(),
})
.optional()
)
.query(async ({ ctx, input }) => {
return await getAllFriends(ctx, {
pending: input?.excludePending ?? false,
});
}),
getPendingFriendRequests: protectedProcedure.query(
async ({ ctx }) =>
await ctx.db.query.friendships.findMany({

View File

@ -23,15 +23,33 @@ export const groupRouter = createTRPCRouter({
return group;
return undefined;
}),
getAll: protectedProcedure.query(async ({ ctx }) => {
const membersShips = await ctx.db.query.groupMembers.findMany({
where: eq(groupMembers.userId, ctx.auth.userId),
with: {
group: true,
},
});
return membersShips?.map(({ group }) => group!) ?? [];
}),
getAll: protectedProcedure
.input(
z
.object({
withMembers: z.boolean().optional().default(false),
})
.optional()
)
.query(async ({ ctx, input }) => {
const membersShips = await ctx.db.query.groupMembers.findMany({
where: eq(groupMembers.userId, ctx.auth.userId),
with: {
group: input?.withMembers
? {
with: {
members: {
with: {
user: true,
},
},
},
}
: true,
},
});
return membersShips?.map(({ group }) => group) ?? [];
}),
// mutations
create: protectedProcedure

View File

@ -36,6 +36,8 @@ export const createTRPCContext = async (opts: { headers: Headers }) => {
};
};
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>;
/**
* 2. INITIALIZATION
*

View File

@ -16,3 +16,5 @@ const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema });
export type DbType = typeof db;

25
src/server/db/seed.ts Normal file
View File

@ -0,0 +1,25 @@
import "dotenv/config";
import { db } from ".";
import { users } from "./schema";
const usersData = Array.from({ length: 10 }, (_, i) => ({
id: `fake-user-${i + 1}`,
name: `User ${i + 1}`,
image: `https://github.com/mr-shortman.png`,
}));
async function seed() {
try {
await db.insert(users).values(usersData);
console.log(`Seeded users (${usersData.length})`);
} catch (e) {
console.log(e);
}
}
(() => {
seed().then(() => {
console.log("Seeding complete");
process.exit(0);
});
})();

View File

@ -13,18 +13,18 @@ import { createQueryClient } from "./query-client";
* handling a tRPC call from a React Server Component.
*/
const createContext = cache(async () => {
const heads = new Headers(await headers());
heads.set("x-trpc-source", "rsc");
const heads = new Headers(await headers());
heads.set("x-trpc-source", "rsc");
return createTRPCContext({
headers: heads,
});
return createTRPCContext({
headers: heads,
});
});
const getQueryClient = cache(createQueryClient);
const caller = createCaller(createContext);
export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient,
caller,
getQueryClient
);