main page and group car
This commit is contained in:
parent
3f4cc126ec
commit
62f1689c1a
@ -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
282
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
96
src/app/_components/expense/add-participants.tsx
Normal file
96
src/app/_components/expense/add-participants.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 }]);
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -36,6 +36,8 @@ export const createTRPCContext = async (opts: { headers: Headers }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>;
|
||||
|
||||
/**
|
||||
* 2. INITIALIZATION
|
||||
*
|
||||
|
||||
@ -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
25
src/server/db/seed.ts
Normal 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);
|
||||
});
|
||||
})();
|
||||
@ -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
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user