this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: member

+590 -1072
+4 -4
package.json
··· 10 10 "format": "biome format --write" 11 11 }, 12 12 "dependencies": { 13 - "@base-ui/react": "^1.1.0", 13 + "@base-ui/react": "^1.2.0", 14 14 "@hugeicons/core-free-icons": "^3.1.1", 15 15 "@hugeicons/react": "^1.1.5", 16 16 "@neondatabase/serverless": "^1.0.2", ··· 18 18 "better-auth": "^1.4.18", 19 19 "class-variance-authority": "^0.7.1", 20 20 "clsx": "^2.1.1", 21 - "dotenv": "^17.2.4", 21 + "dotenv": "^17.3.1", 22 22 "drizzle-orm": "^0.45.1", 23 23 "next": "16.1.6", 24 24 "next-themes": "^0.4.6", ··· 30 30 "tw-animate-css": "^1.4.0" 31 31 }, 32 32 "devDependencies": { 33 - "@biomejs/biome": "2.3.14", 33 + "@biomejs/biome": "2.3.15", 34 34 "@tailwindcss/postcss": "^4.1.18", 35 35 "@types/node": "^25.2.3", 36 36 "@types/nodemailer": "^7.0.9", 37 - "@types/react": "^19.2.13", 37 + "@types/react": "^19.2.14", 38 38 "@types/react-dom": "^19.2.3", 39 39 "babel-plugin-react-compiler": "1.0.0", 40 40 "drizzle-kit": "^0.31.9",
+100 -85
pnpm-lock.yaml
··· 9 9 .: 10 10 dependencies: 11 11 '@base-ui/react': 12 - specifier: ^1.1.0 13 - version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 12 + specifier: ^1.2.0 13 + version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 14 14 '@hugeicons/core-free-icons': 15 15 specifier: ^3.1.1 16 16 version: 3.1.1 ··· 33 33 specifier: ^2.1.1 34 34 version: 2.1.1 35 35 dotenv: 36 - specifier: ^17.2.4 37 - version: 17.2.4 36 + specifier: ^17.3.1 37 + version: 17.3.1 38 38 drizzle-orm: 39 39 specifier: ^0.45.1 40 40 version: 0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)(kysely@0.28.11) ··· 64 64 version: 1.4.0 65 65 devDependencies: 66 66 '@biomejs/biome': 67 - specifier: 2.3.14 68 - version: 2.3.14 67 + specifier: 2.3.15 68 + version: 2.3.15 69 69 '@tailwindcss/postcss': 70 70 specifier: ^4.1.18 71 71 version: 4.1.18 ··· 76 76 specifier: ^7.0.9 77 77 version: 7.0.9 78 78 '@types/react': 79 - specifier: ^19.2.13 80 - version: 19.2.13 79 + specifier: ^19.2.14 80 + version: 19.2.14 81 81 '@types/react-dom': 82 82 specifier: ^19.2.3 83 - version: 19.2.3(@types/react@19.2.13) 83 + version: 19.2.3(@types/react@19.2.14) 84 84 babel-plugin-react-compiler: 85 85 specifier: 1.0.0 86 86 version: 1.0.0 ··· 240 240 resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} 241 241 engines: {node: '>=6.9.0'} 242 242 243 - '@base-ui/react@1.1.0': 244 - resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==} 243 + '@base-ui/react@1.2.0': 244 + resolution: {integrity: sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw==} 245 245 engines: {node: '>=14.0.0'} 246 246 peerDependencies: 247 247 '@types/react': ^17 || ^18 || ^19 ··· 251 251 '@types/react': 252 252 optional: true 253 253 254 - '@base-ui/utils@0.2.4': 255 - resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} 254 + '@base-ui/utils@0.2.5': 255 + resolution: {integrity: sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw==} 256 256 peerDependencies: 257 257 '@types/react': ^17 || ^18 || ^19 258 258 react: ^17 || ^18 || ^19 ··· 282 282 '@better-fetch/fetch@1.1.21': 283 283 resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} 284 284 285 - '@biomejs/biome@2.3.14': 286 - resolution: {integrity: sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==} 285 + '@biomejs/biome@2.3.15': 286 + resolution: {integrity: sha512-u+jlPBAU2B45LDkjjNNYpc1PvqrM/co4loNommS9/sl9oSxsAQKsNZejYuUztvToB5oXi1tN/e62iNd6ESiY3g==} 287 287 engines: {node: '>=14.21.3'} 288 288 hasBin: true 289 289 290 - '@biomejs/cli-darwin-arm64@2.3.14': 291 - resolution: {integrity: sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==} 290 + '@biomejs/cli-darwin-arm64@2.3.15': 291 + resolution: {integrity: sha512-SDCdrJ4COim1r8SNHg19oqT50JfkI/xGZHSyC6mGzMfKrpNe/217Eq6y98XhNTc0vGWDjznSDNXdUc6Kg24jbw==} 292 292 engines: {node: '>=14.21.3'} 293 293 cpu: [arm64] 294 294 os: [darwin] 295 295 296 - '@biomejs/cli-darwin-x64@2.3.14': 297 - resolution: {integrity: sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==} 296 + '@biomejs/cli-darwin-x64@2.3.15': 297 + resolution: {integrity: sha512-RkyeSosBtn3C3Un8zQnl9upX0Qbq4E3QmBa0qjpOh1MebRbHhNlRC16jk8HdTe/9ym5zlfnpbb8cKXzW+vlTxw==} 298 298 engines: {node: '>=14.21.3'} 299 299 cpu: [x64] 300 300 os: [darwin] 301 301 302 - '@biomejs/cli-linux-arm64-musl@2.3.14': 303 - resolution: {integrity: sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==} 302 + '@biomejs/cli-linux-arm64-musl@2.3.15': 303 + resolution: {integrity: sha512-SSSIj2yMkFdSkXqASzIBdjySBXOe65RJlhKEDlri7MN19RC4cpez+C0kEwPrhXOTgJbwQR9QH1F4+VnHkC35pg==} 304 304 engines: {node: '>=14.21.3'} 305 305 cpu: [arm64] 306 306 os: [linux] 307 307 308 - '@biomejs/cli-linux-arm64@2.3.14': 309 - resolution: {integrity: sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==} 308 + '@biomejs/cli-linux-arm64@2.3.15': 309 + resolution: {integrity: sha512-FN83KxrdVWANOn5tDmW6UBC0grojchbGmcEz6JkRs2YY6DY63sTZhwkQ56x6YtKhDVV1Unz7FJexy8o7KwuIhg==} 310 310 engines: {node: '>=14.21.3'} 311 311 cpu: [arm64] 312 312 os: [linux] 313 313 314 - '@biomejs/cli-linux-x64-musl@2.3.14': 315 - resolution: {integrity: sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==} 314 + '@biomejs/cli-linux-x64-musl@2.3.15': 315 + resolution: {integrity: sha512-dbjPzTh+ijmmNwojFYbQNMFp332019ZDioBYAMMJj5Ux9d8MkM+u+J68SBJGVwVeSHMYj+T9504CoxEzQxrdNw==} 316 316 engines: {node: '>=14.21.3'} 317 317 cpu: [x64] 318 318 os: [linux] 319 319 320 - '@biomejs/cli-linux-x64@2.3.14': 321 - resolution: {integrity: sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==} 320 + '@biomejs/cli-linux-x64@2.3.15': 321 + resolution: {integrity: sha512-T8n9p8aiIKOrAD7SwC7opiBM1LYGrE5G3OQRXWgbeo/merBk8m+uxJ1nOXMPzfYyFLfPlKF92QS06KN1UW+Zbg==} 322 322 engines: {node: '>=14.21.3'} 323 323 cpu: [x64] 324 324 os: [linux] 325 325 326 - '@biomejs/cli-win32-arm64@2.3.14': 327 - resolution: {integrity: sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==} 326 + '@biomejs/cli-win32-arm64@2.3.15': 327 + resolution: {integrity: sha512-puMuenu/2brQdgqtQ7geNwQlNVxiABKEZJhMRX6AGWcmrMO8EObMXniFQywy2b81qmC+q+SDvlOpspNwz0WiOA==} 328 328 engines: {node: '>=14.21.3'} 329 329 cpu: [arm64] 330 330 os: [win32] 331 331 332 - '@biomejs/cli-win32-x64@2.3.14': 333 - resolution: {integrity: sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==} 332 + '@biomejs/cli-win32-x64@2.3.15': 333 + resolution: {integrity: sha512-kDZr/hgg+igo5Emi0LcjlgfkoGZtgIpJKhnvKTRmMBv6FF/3SDyEV4khBwqNebZIyMZTzvpca9sQNSXJ39pI2A==} 334 334 engines: {node: '>=14.21.3'} 335 335 cpu: [x64] 336 336 os: [win32] ··· 1004 1004 '@types/node': 1005 1005 optional: true 1006 1006 1007 - '@isaacs/balanced-match@4.0.1': 1008 - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} 1009 - engines: {node: 20 || >=22} 1010 - 1011 - '@isaacs/brace-expansion@5.0.1': 1012 - resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} 1013 - engines: {node: 20 || >=22} 1007 + '@isaacs/cliui@9.0.0': 1008 + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} 1009 + engines: {node: '>=18'} 1014 1010 1015 1011 '@jridgewell/gen-mapping@0.3.13': 1016 1012 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} ··· 1259 1255 peerDependencies: 1260 1256 '@types/react': ^19.2.0 1261 1257 1262 - '@types/react@19.2.13': 1263 - resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==} 1258 + '@types/react@19.2.14': 1259 + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} 1264 1260 1265 1261 '@types/statuses@2.0.6': 1266 1262 resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} ··· 1319 1315 1320 1316 babel-plugin-react-compiler@1.0.0: 1321 1317 resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} 1318 + 1319 + balanced-match@4.0.2: 1320 + resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} 1321 + engines: {node: 20 || >=22} 1322 1322 1323 1323 baseline-browser-mapping@2.9.19: 1324 1324 resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} ··· 1398 1398 resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} 1399 1399 engines: {node: '>=18'} 1400 1400 1401 + brace-expansion@5.0.2: 1402 + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} 1403 + engines: {node: 20 || >=22} 1404 + 1401 1405 braces@3.0.3: 1402 1406 resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 1403 1407 engines: {node: '>=8'} ··· 1581 1585 resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} 1582 1586 engines: {node: '>=0.3.1'} 1583 1587 1584 - dotenv@17.2.4: 1585 - resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} 1588 + dotenv@17.3.1: 1589 + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} 1586 1590 engines: {node: '>=12'} 1587 1591 1588 1592 drizzle-kit@0.31.9: ··· 2048 2052 resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} 2049 2053 engines: {node: '>=18'} 2050 2054 2055 + jackspeak@4.2.3: 2056 + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} 2057 + engines: {node: 20 || >=22} 2058 + 2051 2059 jiti@2.6.1: 2052 2060 resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 2053 2061 hasBin: true ··· 2218 2226 resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} 2219 2227 engines: {node: '>=18'} 2220 2228 2221 - minimatch@10.1.2: 2222 - resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} 2229 + minimatch@10.2.0: 2230 + resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} 2223 2231 engines: {node: 20 || >=22} 2224 2232 2225 2233 minimist@1.2.8: ··· 2450 2458 resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 2451 2459 engines: {node: '>= 0.10'} 2452 2460 2453 - qs@6.14.1: 2454 - resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} 2461 + qs@6.14.2: 2462 + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} 2455 2463 engines: {node: '>=0.6'} 2456 2464 2457 2465 queue-microtask@1.2.3: ··· 3054 3062 '@babel/helper-string-parser': 7.27.1 3055 3063 '@babel/helper-validator-identifier': 7.28.5 3056 3064 3057 - '@base-ui/react@1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 3065 + '@base-ui/react@1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 3058 3066 dependencies: 3059 3067 '@babel/runtime': 7.28.6 3060 - '@base-ui/utils': 0.2.4(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 3068 + '@base-ui/utils': 0.2.5(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 3061 3069 '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 3062 3070 '@floating-ui/utils': 0.2.10 3063 3071 react: 19.2.4 3064 3072 react-dom: 19.2.4(react@19.2.4) 3065 - reselect: 5.1.1 3066 3073 tabbable: 6.4.0 3067 3074 use-sync-external-store: 1.6.0(react@19.2.4) 3068 3075 optionalDependencies: 3069 - '@types/react': 19.2.13 3076 + '@types/react': 19.2.14 3070 3077 3071 - '@base-ui/utils@0.2.4(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 3078 + '@base-ui/utils@0.2.5(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 3072 3079 dependencies: 3073 3080 '@babel/runtime': 7.28.6 3074 3081 '@floating-ui/utils': 0.2.10 ··· 3077 3084 reselect: 5.1.1 3078 3085 use-sync-external-store: 1.6.0(react@19.2.4) 3079 3086 optionalDependencies: 3080 - '@types/react': 19.2.13 3087 + '@types/react': 19.2.14 3081 3088 3082 3089 '@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': 3083 3090 dependencies: ··· 3100 3107 3101 3108 '@better-fetch/fetch@1.1.21': {} 3102 3109 3103 - '@biomejs/biome@2.3.14': 3110 + '@biomejs/biome@2.3.15': 3104 3111 optionalDependencies: 3105 - '@biomejs/cli-darwin-arm64': 2.3.14 3106 - '@biomejs/cli-darwin-x64': 2.3.14 3107 - '@biomejs/cli-linux-arm64': 2.3.14 3108 - '@biomejs/cli-linux-arm64-musl': 2.3.14 3109 - '@biomejs/cli-linux-x64': 2.3.14 3110 - '@biomejs/cli-linux-x64-musl': 2.3.14 3111 - '@biomejs/cli-win32-arm64': 2.3.14 3112 - '@biomejs/cli-win32-x64': 2.3.14 3112 + '@biomejs/cli-darwin-arm64': 2.3.15 3113 + '@biomejs/cli-darwin-x64': 2.3.15 3114 + '@biomejs/cli-linux-arm64': 2.3.15 3115 + '@biomejs/cli-linux-arm64-musl': 2.3.15 3116 + '@biomejs/cli-linux-x64': 2.3.15 3117 + '@biomejs/cli-linux-x64-musl': 2.3.15 3118 + '@biomejs/cli-win32-arm64': 2.3.15 3119 + '@biomejs/cli-win32-x64': 2.3.15 3113 3120 3114 - '@biomejs/cli-darwin-arm64@2.3.14': 3121 + '@biomejs/cli-darwin-arm64@2.3.15': 3115 3122 optional: true 3116 3123 3117 - '@biomejs/cli-darwin-x64@2.3.14': 3124 + '@biomejs/cli-darwin-x64@2.3.15': 3118 3125 optional: true 3119 3126 3120 - '@biomejs/cli-linux-arm64-musl@2.3.14': 3127 + '@biomejs/cli-linux-arm64-musl@2.3.15': 3121 3128 optional: true 3122 3129 3123 - '@biomejs/cli-linux-arm64@2.3.14': 3130 + '@biomejs/cli-linux-arm64@2.3.15': 3124 3131 optional: true 3125 3132 3126 - '@biomejs/cli-linux-x64-musl@2.3.14': 3133 + '@biomejs/cli-linux-x64-musl@2.3.15': 3127 3134 optional: true 3128 3135 3129 - '@biomejs/cli-linux-x64@2.3.14': 3136 + '@biomejs/cli-linux-x64@2.3.15': 3130 3137 optional: true 3131 3138 3132 - '@biomejs/cli-win32-arm64@2.3.14': 3139 + '@biomejs/cli-win32-arm64@2.3.15': 3133 3140 optional: true 3134 3141 3135 - '@biomejs/cli-win32-x64@2.3.14': 3142 + '@biomejs/cli-win32-x64@2.3.15': 3136 3143 optional: true 3137 3144 3138 3145 '@dotenvx/dotenvx@1.52.0': 3139 3146 dependencies: 3140 3147 commander: 11.1.0 3141 - dotenv: 17.2.4 3148 + dotenv: 17.3.1 3142 3149 eciesjs: 0.4.17 3143 3150 execa: 5.1.1 3144 3151 fdir: 6.5.0(picomatch@4.0.3) ··· 3542 3549 optionalDependencies: 3543 3550 '@types/node': 25.2.3 3544 3551 3545 - '@isaacs/balanced-match@4.0.1': {} 3546 - 3547 - '@isaacs/brace-expansion@5.0.1': 3548 - dependencies: 3549 - '@isaacs/balanced-match': 4.0.1 3552 + '@isaacs/cliui@9.0.0': {} 3550 3553 3551 3554 '@jridgewell/gen-mapping@0.3.13': 3552 3555 dependencies: ··· 3744 3747 '@ts-morph/common@0.27.0': 3745 3748 dependencies: 3746 3749 fast-glob: 3.3.3 3747 - minimatch: 10.1.2 3750 + minimatch: 10.2.0 3748 3751 path-browserify: 1.0.1 3749 3752 3750 3753 '@types/node@22.19.11': ··· 3765 3768 pg-protocol: 1.11.0 3766 3769 pg-types: 2.2.0 3767 3770 3768 - '@types/react-dom@19.2.3(@types/react@19.2.13)': 3771 + '@types/react-dom@19.2.3(@types/react@19.2.14)': 3769 3772 dependencies: 3770 - '@types/react': 19.2.13 3773 + '@types/react': 19.2.14 3771 3774 3772 - '@types/react@19.2.13': 3775 + '@types/react@19.2.14': 3773 3776 dependencies: 3774 3777 csstype: 3.2.3 3775 3778 ··· 3827 3830 dependencies: 3828 3831 '@babel/types': 7.29.0 3829 3832 3833 + balanced-match@4.0.2: 3834 + dependencies: 3835 + jackspeak: 4.2.3 3836 + 3830 3837 baseline-browser-mapping@2.9.19: {} 3831 3838 3832 3839 better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)(kysely@0.28.11))(next@16.1.6(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): ··· 3867 3874 http-errors: 2.0.1 3868 3875 iconv-lite: 0.7.2 3869 3876 on-finished: 2.4.1 3870 - qs: 6.14.1 3877 + qs: 6.14.2 3871 3878 raw-body: 3.0.2 3872 3879 type-is: 2.0.1 3873 3880 transitivePeerDependencies: 3874 3881 - supports-color 3882 + 3883 + brace-expansion@5.0.2: 3884 + dependencies: 3885 + balanced-match: 4.0.2 3875 3886 3876 3887 braces@3.0.3: 3877 3888 dependencies: ··· 4006 4017 4007 4018 diff@8.0.3: {} 4008 4019 4009 - dotenv@17.2.4: {} 4020 + dotenv@17.3.1: {} 4010 4021 4011 4022 drizzle-kit@0.31.9: 4012 4023 dependencies: ··· 4223 4234 once: 1.4.0 4224 4235 parseurl: 1.3.3 4225 4236 proxy-addr: 2.0.7 4226 - qs: 6.14.1 4237 + qs: 6.14.2 4227 4238 range-parser: 1.2.1 4228 4239 router: 2.2.0 4229 4240 send: 1.2.1 ··· 4444 4455 4445 4456 isexe@3.1.5: {} 4446 4457 4458 + jackspeak@4.2.3: 4459 + dependencies: 4460 + '@isaacs/cliui': 9.0.0 4461 + 4447 4462 jiti@2.6.1: {} 4448 4463 4449 4464 jose@6.1.3: {} ··· 4565 4580 4566 4581 mimic-function@5.0.1: {} 4567 4582 4568 - minimatch@10.1.2: 4583 + minimatch@10.2.0: 4569 4584 dependencies: 4570 - '@isaacs/brace-expansion': 5.0.1 4585 + brace-expansion: 5.0.2 4571 4586 4572 4587 minimist@1.2.8: {} 4573 4588 ··· 4792 4807 forwarded: 0.2.0 4793 4808 ipaddr.js: 1.9.1 4794 4809 4795 - qs@6.14.1: 4810 + qs@6.14.2: 4796 4811 dependencies: 4797 4812 side-channel: 1.1.0 4798 4813
+18 -361
src/app/[id]/page.tsx
··· 1 - "use client"; 1 + import { redirect } from "next/navigation"; 2 2 3 - import { 4 - ArrowLeft01Icon, 5 - Copy01Icon, 6 - Delete01Icon, 7 - PlusSignIcon, 8 - Settings01Icon, 9 - Share08Icon, 10 - UserGroupIcon, 11 - } from "@hugeicons/core-free-icons"; 12 - import { HugeiconsIcon } from "@hugeicons/react"; 13 - import Link from "next/link"; 14 - import { use, useEffect, useState } from "react"; 15 - import { Avatar, AvatarImage } from "@/components/ui/avatar"; 16 - import { Button } from "@/components/ui/button"; 17 - import { 18 - Card, 19 - CardAction, 20 - CardContent, 21 - CardFooter, 22 - CardHeader, 23 - CardTitle, 24 - } from "@/components/ui/card"; 25 - import { 26 - Dialog, 27 - DialogClose, 28 - DialogContent, 29 - DialogHeader, 30 - DialogTitle, 31 - DialogTrigger, 32 - } from "@/components/ui/dialog"; 33 - import { 34 - Field, 35 - FieldDescription, 36 - FieldGroup, 37 - FieldLabel, 38 - } from "@/components/ui/field"; 39 - import { Input } from "@/components/ui/input"; 40 - import { 41 - InputGroup, 42 - InputGroupAddon, 43 - InputGroupButton, 44 - InputGroupInput, 45 - } from "@/components/ui/input-group"; 46 - import { 47 - Item, 48 - ItemActions, 49 - ItemContent, 50 - ItemDescription, 51 - ItemMedia, 52 - ItemTitle, 53 - } from "@/components/ui/item"; 54 - import { Progress } from "@/components/ui/progress"; 55 - import { Slider } from "@/components/ui/slider"; 56 - import type { group, voucher } from "@/db/schema"; 57 - import { authClient } from "@/lib/auth-client"; 3 + import Group from "@/components/group"; 58 4 import { getGroup } from "@/lib/group"; 59 - import { 60 - createVoucher, 61 - deleteVoucher, 62 - getVouchers, 63 - updateVoucher, 64 - } from "@/lib/voucher"; 5 + import { redeemInvite } from "@/lib/invite"; 6 + import { getVouchers } from "@/lib/voucher"; 65 7 66 - export type Group = typeof group.$inferSelect; 67 - export type Voucher = typeof voucher.$inferSelect; 68 - 69 - export default function Page({ params }: { params: Promise<{ id: number }> }) { 70 - const { data: session } = authClient.useSession(); 71 - const user = session?.user; 72 - 73 - const { id } = use(params); 74 - const [group, setGroup] = useState<Group | null>(null); 75 - const [vouchers, setVouchers] = useState<Voucher[]>([]); 76 - 77 - useEffect(() => { 78 - getGroup(id).then(async (group) => { 79 - if (!group) return; 80 - setGroup(group); 81 - setVouchers(await getVouchers(group.id)); 82 - }); 83 - }, [id]); 84 - 85 - if (!user || !group) return null; 8 + export default async function Page({ 9 + params, 10 + searchParams, 11 + }: { 12 + params: Promise<{ id: string }>; 13 + searchParams: Promise<{ invite?: string }>; 14 + }) { 15 + const { invite } = await searchParams; 16 + if (invite) await redeemInvite(invite as string); 86 17 87 - const me = 1; 18 + const { id } = await params; 19 + const group = await getGroup(Number(id)); 20 + const vouchers = await getVouchers(Number(id)); 88 21 89 - const data = { 90 - id: 1, 91 - emoji: "❤️", 92 - title: "Love & Stuff", 93 - host: 1, 94 - members: [ 95 - { 96 - id: 1, 97 - name: "Alice", 98 - avatar: "https://github.com/alice.png", 99 - }, 100 - { 101 - id: 2, 102 - name: "Bob", 103 - avatar: "https://github.com/bob.png", 104 - }, 105 - ], 106 - }; 22 + if (!group) redirect("/"); 107 23 108 - return ( 109 - <> 110 - <header className="flex justify-between"> 111 - <Button variant="secondary" size="icon" render={<Link href="/" />}> 112 - <HugeiconsIcon icon={ArrowLeft01Icon} /> 113 - </Button> 114 - <div> 115 - <Dialog> 116 - <DialogTrigger 117 - render={ 118 - <Button variant="secondary" size="icon"> 119 - <HugeiconsIcon icon={Share08Icon} /> 120 - </Button> 121 - } 122 - /> 123 - <DialogContent> 124 - <DialogHeader> 125 - <DialogTitle>Invite</DialogTitle> 126 - </DialogHeader> 127 - <img 128 - src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=LOVE123" 129 - alt="QR Code" 130 - className="mx-auto" 131 - /> 132 - <p className="text-sm text-muted-foreground"> 133 - Scan the QR code to join {data.title} 134 - </p> 135 - <InputGroup> 136 - <InputGroupInput placeholder="https://x.com/shadcn" readOnly /> 137 - <InputGroupAddon align="inline-end"> 138 - <InputGroupButton 139 - onClick={() => 140 - navigator.clipboard.writeText("https://x.com/shadcn") 141 - } 142 - > 143 - <HugeiconsIcon icon={Copy01Icon} /> 144 - </InputGroupButton> 145 - </InputGroupAddon> 146 - </InputGroup> 147 - </DialogContent> 148 - </Dialog> 149 - <Dialog> 150 - <DialogTrigger 151 - render={ 152 - <Button variant="secondary" size="icon"> 153 - <HugeiconsIcon icon={Settings01Icon} /> 154 - </Button> 155 - } 156 - /> 157 - <DialogContent> 158 - <DialogHeader> 159 - <DialogTitle>Group Settings</DialogTitle> 160 - </DialogHeader> 161 - <form> 162 - <FieldGroup> 163 - <div className="flex gap-4"> 164 - <Field> 165 - <FieldLabel htmlFor="form-emoji">Emoji</FieldLabel> 166 - <Input 167 - id="form-emoji" 168 - placeholder="Enter the emoji" 169 - required 170 - /> 171 - </Field> 172 - <Field> 173 - <FieldLabel htmlFor="form-name">Name</FieldLabel> 174 - <Input 175 - id="form-name" 176 - placeholder="Enter the name" 177 - required 178 - /> 179 - </Field> 180 - </div> 181 - <Field orientation="horizontal"> 182 - <Button variant="outline" size="icon"> 183 - <HugeiconsIcon icon={Delete01Icon} /> 184 - </Button> 185 - <Button type="submit" className="flex-1"> 186 - Create 187 - </Button> 188 - </Field> 189 - </FieldGroup> 190 - </form> 191 - </DialogContent> 192 - </Dialog> 193 - </div> 194 - </header> 195 - <Card> 196 - <CardHeader> 197 - <CardTitle>{group.name}</CardTitle> 198 - <CardAction> 199 - <Dialog> 200 - <DialogTrigger 201 - render={ 202 - <Button variant="outline"> 203 - <HugeiconsIcon icon={UserGroupIcon} /> 204 - {data.members.length} 205 - </Button> 206 - } 207 - /> 208 - <DialogContent> 209 - <DialogHeader> 210 - <DialogTitle>Members</DialogTitle> 211 - </DialogHeader> 212 - {data.members.map((member) => ( 213 - <Item key={member.id}> 214 - <ItemMedia> 215 - <Avatar> 216 - <AvatarImage src={member.avatar} /> 217 - </Avatar> 218 - </ItemMedia> 219 - <ItemContent> 220 - <ItemTitle>{member.name}</ItemTitle> 221 - <ItemDescription> 222 - {member.id === data.host ? "Host" : "Member"} 223 - </ItemDescription> 224 - </ItemContent> 225 - {data.host === me && ( 226 - <ItemActions> 227 - <Button variant="ghost" size="icon"> 228 - <HugeiconsIcon icon={Delete01Icon} /> 229 - </Button> 230 - </ItemActions> 231 - )} 232 - </Item> 233 - ))} 234 - </DialogContent> 235 - </Dialog> 236 - </CardAction> 237 - </CardHeader> 238 - <CardContent className="grid gap-6"> 239 - {vouchers.map((voucher) => ( 240 - <Card key={voucher.id}> 241 - <CardHeader> 242 - <CardTitle>{voucher.name}</CardTitle> 243 - <CardAction> 244 - <Button>{voucher.limit}</Button> 245 - </CardAction> 246 - </CardHeader> 247 - <CardContent> 248 - <Progress value={(1 / voucher.limit) * 100} /> 249 - </CardContent> 250 - <CardFooter> 251 - {(me === data.host && ( 252 - <Dialog> 253 - <DialogTrigger 254 - render={<Button className="w-full">Edit</Button>} 255 - /> 256 - <DialogContent> 257 - <form 258 - onSubmit={(e) => { 259 - e.preventDefault(); 260 - const formData = new FormData(e.currentTarget); 261 - const name = formData.get("name") as string; 262 - const limit = formData.get("limit") as string; 263 - updateVoucher(voucher.id, name, Number(limit)).then( 264 - (res) => { 265 - if (!res) return; 266 - setVouchers((prevVouchers) => 267 - prevVouchers.map((v) => 268 - v.id === voucher.id ? res : v, 269 - ), 270 - ); 271 - }, 272 - ); 273 - }} 274 - > 275 - <FieldGroup> 276 - <Field> 277 - <FieldLabel htmlFor="name">Name</FieldLabel> 278 - <Input 279 - id="name" 280 - name="name" 281 - defaultValue={voucher.name} 282 - required 283 - /> 284 - </Field> 285 - <Field> 286 - <FieldLabel htmlFor="limit">Limit</FieldLabel> 287 - <Slider 288 - id="limit" 289 - name="limit" 290 - defaultValue={[voucher.limit]} 291 - max={20} 292 - /> 293 - </Field> 294 - <Field orientation="horizontal"> 295 - <DialogClose 296 - render={ 297 - <Button 298 - variant="destructive" 299 - onClick={(e) => { 300 - e.preventDefault(); 301 - deleteVoucher(voucher.id).then((res) => { 302 - if (!res) return; 303 - setVouchers((prevVouchers) => 304 - prevVouchers.filter( 305 - (v) => v.id !== voucher.id, 306 - ), 307 - ); 308 - }); 309 - }} 310 - > 311 - <HugeiconsIcon icon={Delete01Icon} /> 312 - </Button> 313 - } 314 - /> 315 - <DialogClose 316 - render={ 317 - <Button type="submit" className="flex-1"> 318 - Update 319 - </Button> 320 - } 321 - /> 322 - </Field> 323 - </FieldGroup> 324 - </form> 325 - </DialogContent> 326 - </Dialog> 327 - )) || <Button className="w-full">Redeem</Button>} 328 - </CardFooter> 329 - </Card> 330 - ))} 331 - <Dialog> 332 - <DialogTrigger render={<Button>Create Voucher</Button>} /> 333 - <DialogContent> 334 - <form 335 - onSubmit={(e) => { 336 - e.preventDefault(); 337 - const formData = new FormData(e.currentTarget); 338 - const name = formData.get("name") as string; 339 - const limit = formData.get("limit") as string; 340 - createVoucher(name, Number(limit), group.id).then((res) => { 341 - if (!res) return; 342 - setVouchers((prevVouchers) => [...prevVouchers, res]); 343 - }); 344 - }} 345 - > 346 - <FieldGroup> 347 - <Field> 348 - <FieldLabel htmlFor="name">Name</FieldLabel> 349 - <Input id="name" name="name" required /> 350 - </Field> 351 - <Field> 352 - <FieldLabel htmlFor="limit">Limit</FieldLabel> 353 - <Slider id="limit" name="limit" max={20} /> 354 - </Field> 355 - <Field> 356 - <DialogClose 357 - render={<Button type="submit">Create</Button>} 358 - /> 359 - </Field> 360 - </FieldGroup> 361 - </form> 362 - </DialogContent> 363 - </Dialog> 364 - </CardContent> 365 - </Card> 366 - </> 367 - ); 24 + return <Group group={group} vouchers={vouchers} />; 368 25 }
+8 -1
src/app/auth/page.tsx
··· 11 11 } from "@/components/ui/field"; 12 12 import { authClient } from "@/lib/auth-client"; 13 13 14 - export default function Auth() { 14 + export default function Auth({ 15 + searchParams, 16 + }: { 17 + searchParams: { next?: string }; 18 + }) { 19 + const next = searchParams.next || "/"; 20 + 15 21 return ( 16 22 <div className="flex flex-col h-screen justify-center gap-6 w-md mx-auto"> 17 23 <FieldGroup> ··· 31 37 onClick={() => { 32 38 authClient.signIn.social({ 33 39 provider: "google", 40 + callbackURL: next, 34 41 }); 35 42 }} 36 43 >
+4 -6
src/app/page.tsx
··· 4 4 import { HugeiconsIcon } from "@hugeicons/react"; 5 5 import Link from "next/link"; 6 6 import { useEffect, useState } from "react"; 7 - import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 7 + import CustomAvatar from "@/components/avatar"; 8 8 import { Button } from "@/components/ui/button"; 9 9 import { 10 10 Card, ··· 25 25 import type { group } from "@/db/schema"; 26 26 import { authClient } from "@/lib/auth-client"; 27 27 import { createGroup, deleteGroup, getGroups, updateGroup } from "@/lib/group"; 28 - import { getInitials } from "@/lib/utils"; 29 28 30 29 export type Group = typeof group.$inferSelect; 31 30 ··· 47 46 <CardTitle>Vouch</CardTitle> 48 47 <CardDescription>Your shared promises</CardDescription> 49 48 <CardAction> 50 - <Avatar render={<Link href="/settings" />} size="lg"> 51 - <AvatarImage src={user.image as string} /> 52 - <AvatarFallback>{getInitials(user.name)}</AvatarFallback> 53 - </Avatar> 49 + <Link href="/settings"> 50 + <CustomAvatar name={user.name} image={user.image as string} /> 51 + </Link> 54 52 </CardAction> 55 53 </CardHeader> 56 54 <CardContent className="grid gap-4">
+19
src/components/avatar.tsx
··· 1 + "use client"; 2 + 3 + import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 4 + import { getInitials } from "@/lib/utils"; 5 + 6 + export default function CustomAvatar({ 7 + name, 8 + image, 9 + }: { 10 + name: string; 11 + image: string; 12 + }) { 13 + return ( 14 + <Avatar size="lg"> 15 + <AvatarImage src={image as string} /> 16 + <AvatarFallback>{getInitials(name)}</AvatarFallback> 17 + </Avatar> 18 + ); 19 + }
-531
src/components/component-example.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - 5 - import { Example, ExampleWrapper } from "@/components/example"; 6 - import { 7 - AlertDialog, 8 - AlertDialogAction, 9 - AlertDialogCancel, 10 - AlertDialogContent, 11 - AlertDialogDescription, 12 - AlertDialogFooter, 13 - AlertDialogHeader, 14 - AlertDialogMedia, 15 - AlertDialogTitle, 16 - AlertDialogTrigger, 17 - } from "@/components/ui/alert-dialog"; 18 - import { Badge } from "@/components/ui/badge"; 19 - import { Button } from "@/components/ui/button"; 20 - import { 21 - Card, 22 - CardAction, 23 - CardContent, 24 - CardDescription, 25 - CardFooter, 26 - CardHeader, 27 - CardTitle, 28 - } from "@/components/ui/card"; 29 - import { 30 - Combobox, 31 - ComboboxContent, 32 - ComboboxEmpty, 33 - ComboboxInput, 34 - ComboboxItem, 35 - ComboboxList, 36 - } from "@/components/ui/combobox"; 37 - import { 38 - DropdownMenu, 39 - DropdownMenuCheckboxItem, 40 - DropdownMenuContent, 41 - DropdownMenuGroup, 42 - DropdownMenuItem, 43 - DropdownMenuLabel, 44 - DropdownMenuPortal, 45 - DropdownMenuRadioGroup, 46 - DropdownMenuRadioItem, 47 - DropdownMenuSeparator, 48 - DropdownMenuShortcut, 49 - DropdownMenuSub, 50 - DropdownMenuSubContent, 51 - DropdownMenuSubTrigger, 52 - DropdownMenuTrigger, 53 - } from "@/components/ui/dropdown-menu"; 54 - import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 55 - import { Input } from "@/components/ui/input"; 56 - import { 57 - Select, 58 - SelectContent, 59 - SelectGroup, 60 - SelectItem, 61 - SelectTrigger, 62 - SelectValue, 63 - } from "@/components/ui/select"; 64 - import { Textarea } from "@/components/ui/textarea"; 65 - import { HugeiconsIcon } from "@hugeicons/react"; 66 - import { 67 - PlusSignIcon, 68 - BluetoothIcon, 69 - MoreVerticalCircle01Icon, 70 - FileIcon, 71 - FolderIcon, 72 - FolderOpenIcon, 73 - CodeIcon, 74 - MoreHorizontalCircle01Icon, 75 - SearchIcon, 76 - FloppyDiskIcon, 77 - DownloadIcon, 78 - EyeIcon, 79 - LayoutIcon, 80 - PaintBoardIcon, 81 - SunIcon, 82 - MoonIcon, 83 - ComputerIcon, 84 - UserIcon, 85 - CreditCardIcon, 86 - SettingsIcon, 87 - KeyboardIcon, 88 - LanguageCircleIcon, 89 - NotificationIcon, 90 - MailIcon, 91 - ShieldIcon, 92 - HelpCircleIcon, 93 - File01Icon, 94 - LogoutIcon, 95 - } from "@hugeicons/core-free-icons"; 96 - 97 - export function ComponentExample() { 98 - return ( 99 - <ExampleWrapper> 100 - <CardExample /> 101 - <FormExample /> 102 - </ExampleWrapper> 103 - ); 104 - } 105 - 106 - function CardExample() { 107 - return ( 108 - <Example title="Card" className="items-center justify-center"> 109 - <Card className="relative w-full max-w-sm overflow-hidden pt-0"> 110 - <div className="bg-primary absolute inset-0 z-30 aspect-video opacity-50 mix-blend-color" /> 111 - <img 112 - src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" 113 - alt="Photo by mymind on Unsplash" 114 - title="Photo by mymind on Unsplash" 115 - className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale" 116 - /> 117 - <CardHeader> 118 - <CardTitle>Observability Plus is replacing Monitoring</CardTitle> 119 - <CardDescription> 120 - Switch to the improved way to explore your data, with natural 121 - language. Monitoring will no longer be available on the Pro plan in 122 - November, 2025 123 - </CardDescription> 124 - </CardHeader> 125 - <CardFooter> 126 - <AlertDialog> 127 - <AlertDialogTrigger render={<Button />}> 128 - <HugeiconsIcon 129 - icon={PlusSignIcon} 130 - strokeWidth={2} 131 - data-icon="inline-start" 132 - /> 133 - Show Dialog 134 - </AlertDialogTrigger> 135 - <AlertDialogContent size="sm"> 136 - <AlertDialogHeader> 137 - <AlertDialogMedia> 138 - <HugeiconsIcon icon={BluetoothIcon} strokeWidth={2} /> 139 - </AlertDialogMedia> 140 - <AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle> 141 - <AlertDialogDescription> 142 - Do you want to allow the USB accessory to connect to this 143 - device? 144 - </AlertDialogDescription> 145 - </AlertDialogHeader> 146 - <AlertDialogFooter> 147 - <AlertDialogCancel>Don&apos;t allow</AlertDialogCancel> 148 - <AlertDialogAction>Allow</AlertDialogAction> 149 - </AlertDialogFooter> 150 - </AlertDialogContent> 151 - </AlertDialog> 152 - <Badge variant="secondary" className="ml-auto"> 153 - Warning 154 - </Badge> 155 - </CardFooter> 156 - </Card> 157 - </Example> 158 - ); 159 - } 160 - 161 - const frameworks = [ 162 - "Next.js", 163 - "SvelteKit", 164 - "Nuxt.js", 165 - "Remix", 166 - "Astro", 167 - ] as const; 168 - 169 - const roleItems = [ 170 - { label: "Developer", value: "developer" }, 171 - { label: "Designer", value: "designer" }, 172 - { label: "Manager", value: "manager" }, 173 - { label: "Other", value: "other" }, 174 - ]; 175 - 176 - function FormExample() { 177 - const [notifications, setNotifications] = React.useState({ 178 - email: true, 179 - sms: false, 180 - push: true, 181 - }); 182 - const [theme, setTheme] = React.useState("light"); 183 - 184 - return ( 185 - <Example title="Form"> 186 - <Card className="w-full max-w-md"> 187 - <CardHeader> 188 - <CardTitle>User Information</CardTitle> 189 - <CardDescription>Please fill in your details below</CardDescription> 190 - <CardAction> 191 - <DropdownMenu> 192 - <DropdownMenuTrigger 193 - render={<Button variant="ghost" size="icon" />} 194 - > 195 - <HugeiconsIcon 196 - icon={MoreVerticalCircle01Icon} 197 - strokeWidth={2} 198 - /> 199 - <span className="sr-only">More options</span> 200 - </DropdownMenuTrigger> 201 - <DropdownMenuContent align="end" className="w-56"> 202 - <DropdownMenuGroup> 203 - <DropdownMenuLabel>File</DropdownMenuLabel> 204 - <DropdownMenuItem> 205 - <HugeiconsIcon icon={FileIcon} strokeWidth={2} /> 206 - New File 207 - <DropdownMenuShortcut>⌘N</DropdownMenuShortcut> 208 - </DropdownMenuItem> 209 - <DropdownMenuItem> 210 - <HugeiconsIcon icon={FolderIcon} strokeWidth={2} /> 211 - New Folder 212 - <DropdownMenuShortcut>⇧⌘N</DropdownMenuShortcut> 213 - </DropdownMenuItem> 214 - <DropdownMenuSub> 215 - <DropdownMenuSubTrigger> 216 - <HugeiconsIcon icon={FolderOpenIcon} strokeWidth={2} /> 217 - Open Recent 218 - </DropdownMenuSubTrigger> 219 - <DropdownMenuPortal> 220 - <DropdownMenuSubContent> 221 - <DropdownMenuGroup> 222 - <DropdownMenuLabel>Recent Projects</DropdownMenuLabel> 223 - <DropdownMenuItem> 224 - <HugeiconsIcon icon={CodeIcon} strokeWidth={2} /> 225 - Project Alpha 226 - </DropdownMenuItem> 227 - <DropdownMenuItem> 228 - <HugeiconsIcon icon={CodeIcon} strokeWidth={2} /> 229 - Project Beta 230 - </DropdownMenuItem> 231 - <DropdownMenuSub> 232 - <DropdownMenuSubTrigger> 233 - <HugeiconsIcon 234 - icon={MoreHorizontalCircle01Icon} 235 - strokeWidth={2} 236 - /> 237 - More Projects 238 - </DropdownMenuSubTrigger> 239 - <DropdownMenuPortal> 240 - <DropdownMenuSubContent> 241 - <DropdownMenuItem> 242 - <HugeiconsIcon 243 - icon={CodeIcon} 244 - strokeWidth={2} 245 - /> 246 - Project Gamma 247 - </DropdownMenuItem> 248 - <DropdownMenuItem> 249 - <HugeiconsIcon 250 - icon={CodeIcon} 251 - strokeWidth={2} 252 - /> 253 - Project Delta 254 - </DropdownMenuItem> 255 - </DropdownMenuSubContent> 256 - </DropdownMenuPortal> 257 - </DropdownMenuSub> 258 - </DropdownMenuGroup> 259 - <DropdownMenuSeparator /> 260 - <DropdownMenuGroup> 261 - <DropdownMenuItem> 262 - <HugeiconsIcon icon={SearchIcon} strokeWidth={2} /> 263 - Browse... 264 - </DropdownMenuItem> 265 - </DropdownMenuGroup> 266 - </DropdownMenuSubContent> 267 - </DropdownMenuPortal> 268 - </DropdownMenuSub> 269 - <DropdownMenuSeparator /> 270 - <DropdownMenuItem> 271 - <HugeiconsIcon icon={FloppyDiskIcon} strokeWidth={2} /> 272 - Save 273 - <DropdownMenuShortcut>⌘S</DropdownMenuShortcut> 274 - </DropdownMenuItem> 275 - <DropdownMenuItem> 276 - <HugeiconsIcon icon={DownloadIcon} strokeWidth={2} /> 277 - Export 278 - <DropdownMenuShortcut>⇧⌘E</DropdownMenuShortcut> 279 - </DropdownMenuItem> 280 - </DropdownMenuGroup> 281 - <DropdownMenuSeparator /> 282 - <DropdownMenuGroup> 283 - <DropdownMenuLabel>View</DropdownMenuLabel> 284 - <DropdownMenuCheckboxItem 285 - checked={notifications.email} 286 - onCheckedChange={(checked) => 287 - setNotifications({ 288 - ...notifications, 289 - email: checked === true, 290 - }) 291 - } 292 - > 293 - <HugeiconsIcon icon={EyeIcon} strokeWidth={2} /> 294 - Show Sidebar 295 - </DropdownMenuCheckboxItem> 296 - <DropdownMenuCheckboxItem 297 - checked={notifications.sms} 298 - onCheckedChange={(checked) => 299 - setNotifications({ 300 - ...notifications, 301 - sms: checked === true, 302 - }) 303 - } 304 - > 305 - <HugeiconsIcon icon={LayoutIcon} strokeWidth={2} /> 306 - Show Status Bar 307 - </DropdownMenuCheckboxItem> 308 - <DropdownMenuSub> 309 - <DropdownMenuSubTrigger> 310 - <HugeiconsIcon icon={PaintBoardIcon} strokeWidth={2} /> 311 - Theme 312 - </DropdownMenuSubTrigger> 313 - <DropdownMenuPortal> 314 - <DropdownMenuSubContent> 315 - <DropdownMenuGroup> 316 - <DropdownMenuLabel>Appearance</DropdownMenuLabel> 317 - <DropdownMenuRadioGroup 318 - value={theme} 319 - onValueChange={setTheme} 320 - > 321 - <DropdownMenuRadioItem value="light"> 322 - <HugeiconsIcon icon={SunIcon} strokeWidth={2} /> 323 - Light 324 - </DropdownMenuRadioItem> 325 - <DropdownMenuRadioItem value="dark"> 326 - <HugeiconsIcon icon={MoonIcon} strokeWidth={2} /> 327 - Dark 328 - </DropdownMenuRadioItem> 329 - <DropdownMenuRadioItem value="system"> 330 - <HugeiconsIcon 331 - icon={ComputerIcon} 332 - strokeWidth={2} 333 - /> 334 - System 335 - </DropdownMenuRadioItem> 336 - </DropdownMenuRadioGroup> 337 - </DropdownMenuGroup> 338 - </DropdownMenuSubContent> 339 - </DropdownMenuPortal> 340 - </DropdownMenuSub> 341 - </DropdownMenuGroup> 342 - <DropdownMenuSeparator /> 343 - <DropdownMenuGroup> 344 - <DropdownMenuLabel>Account</DropdownMenuLabel> 345 - <DropdownMenuItem> 346 - <HugeiconsIcon icon={UserIcon} strokeWidth={2} /> 347 - Profile 348 - <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut> 349 - </DropdownMenuItem> 350 - <DropdownMenuItem> 351 - <HugeiconsIcon icon={CreditCardIcon} strokeWidth={2} /> 352 - Billing 353 - </DropdownMenuItem> 354 - <DropdownMenuSub> 355 - <DropdownMenuSubTrigger> 356 - <HugeiconsIcon icon={SettingsIcon} strokeWidth={2} /> 357 - Settings 358 - </DropdownMenuSubTrigger> 359 - <DropdownMenuPortal> 360 - <DropdownMenuSubContent> 361 - <DropdownMenuGroup> 362 - <DropdownMenuLabel>Preferences</DropdownMenuLabel> 363 - <DropdownMenuItem> 364 - <HugeiconsIcon 365 - icon={KeyboardIcon} 366 - strokeWidth={2} 367 - /> 368 - Keyboard Shortcuts 369 - </DropdownMenuItem> 370 - <DropdownMenuItem> 371 - <HugeiconsIcon 372 - icon={LanguageCircleIcon} 373 - strokeWidth={2} 374 - /> 375 - Language 376 - </DropdownMenuItem> 377 - <DropdownMenuSub> 378 - <DropdownMenuSubTrigger> 379 - <HugeiconsIcon 380 - icon={NotificationIcon} 381 - strokeWidth={2} 382 - /> 383 - Notifications 384 - </DropdownMenuSubTrigger> 385 - <DropdownMenuPortal> 386 - <DropdownMenuSubContent> 387 - <DropdownMenuGroup> 388 - <DropdownMenuLabel> 389 - Notification Types 390 - </DropdownMenuLabel> 391 - <DropdownMenuCheckboxItem 392 - checked={notifications.push} 393 - onCheckedChange={(checked) => 394 - setNotifications({ 395 - ...notifications, 396 - push: checked === true, 397 - }) 398 - } 399 - > 400 - <HugeiconsIcon 401 - icon={NotificationIcon} 402 - strokeWidth={2} 403 - /> 404 - Push Notifications 405 - </DropdownMenuCheckboxItem> 406 - <DropdownMenuCheckboxItem 407 - checked={notifications.email} 408 - onCheckedChange={(checked) => 409 - setNotifications({ 410 - ...notifications, 411 - email: checked === true, 412 - }) 413 - } 414 - > 415 - <HugeiconsIcon 416 - icon={MailIcon} 417 - strokeWidth={2} 418 - /> 419 - Email Notifications 420 - </DropdownMenuCheckboxItem> 421 - </DropdownMenuGroup> 422 - </DropdownMenuSubContent> 423 - </DropdownMenuPortal> 424 - </DropdownMenuSub> 425 - </DropdownMenuGroup> 426 - <DropdownMenuSeparator /> 427 - <DropdownMenuGroup> 428 - <DropdownMenuItem> 429 - <HugeiconsIcon icon={ShieldIcon} strokeWidth={2} /> 430 - Privacy & Security 431 - </DropdownMenuItem> 432 - </DropdownMenuGroup> 433 - </DropdownMenuSubContent> 434 - </DropdownMenuPortal> 435 - </DropdownMenuSub> 436 - </DropdownMenuGroup> 437 - <DropdownMenuSeparator /> 438 - <DropdownMenuGroup> 439 - <DropdownMenuItem> 440 - <HugeiconsIcon icon={HelpCircleIcon} strokeWidth={2} /> 441 - Help & Support 442 - </DropdownMenuItem> 443 - <DropdownMenuItem> 444 - <HugeiconsIcon icon={File01Icon} strokeWidth={2} /> 445 - Documentation 446 - </DropdownMenuItem> 447 - </DropdownMenuGroup> 448 - <DropdownMenuSeparator /> 449 - <DropdownMenuGroup> 450 - <DropdownMenuItem variant="destructive"> 451 - <HugeiconsIcon icon={LogoutIcon} strokeWidth={2} /> 452 - Sign Out 453 - <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut> 454 - </DropdownMenuItem> 455 - </DropdownMenuGroup> 456 - </DropdownMenuContent> 457 - </DropdownMenu> 458 - </CardAction> 459 - </CardHeader> 460 - <CardContent> 461 - <form> 462 - <FieldGroup> 463 - <div className="grid grid-cols-2 gap-4"> 464 - <Field> 465 - <FieldLabel htmlFor="small-form-name">Name</FieldLabel> 466 - <Input 467 - id="small-form-name" 468 - placeholder="Enter your name" 469 - required 470 - /> 471 - </Field> 472 - <Field> 473 - <FieldLabel htmlFor="small-form-role">Role</FieldLabel> 474 - <Select items={roleItems} defaultValue={null}> 475 - <SelectTrigger id="small-form-role"> 476 - <SelectValue /> 477 - </SelectTrigger> 478 - <SelectContent> 479 - <SelectGroup> 480 - {roleItems.map((item) => ( 481 - <SelectItem key={item.value} value={item.value}> 482 - {item.label} 483 - </SelectItem> 484 - ))} 485 - </SelectGroup> 486 - </SelectContent> 487 - </Select> 488 - </Field> 489 - </div> 490 - <Field> 491 - <FieldLabel htmlFor="small-form-framework"> 492 - Framework 493 - </FieldLabel> 494 - <Combobox items={frameworks}> 495 - <ComboboxInput 496 - id="small-form-framework" 497 - placeholder="Select a framework" 498 - required 499 - /> 500 - <ComboboxContent> 501 - <ComboboxEmpty>No frameworks found.</ComboboxEmpty> 502 - <ComboboxList> 503 - {(item) => ( 504 - <ComboboxItem key={item} value={item}> 505 - {item} 506 - </ComboboxItem> 507 - )} 508 - </ComboboxList> 509 - </ComboboxContent> 510 - </Combobox> 511 - </Field> 512 - <Field> 513 - <FieldLabel htmlFor="small-form-comments">Comments</FieldLabel> 514 - <Textarea 515 - id="small-form-comments" 516 - placeholder="Add any additional comments" 517 - /> 518 - </Field> 519 - <Field orientation="horizontal"> 520 - <Button type="submit">Submit</Button> 521 - <Button variant="outline" type="button"> 522 - Cancel 523 - </Button> 524 - </Field> 525 - </FieldGroup> 526 - </form> 527 - </CardContent> 528 - </Card> 529 - </Example> 530 - ); 531 - }
-56
src/components/example.tsx
··· 1 - import { cn } from "@/lib/utils"; 2 - 3 - function ExampleWrapper({ className, ...props }: React.ComponentProps<"div">) { 4 - return ( 5 - <div className="bg-background w-full"> 6 - <div 7 - data-slot="example-wrapper" 8 - className={cn( 9 - "mx-auto grid min-h-screen w-full max-w-5xl min-w-0 content-center items-start gap-8 p-4 pt-2 sm:gap-12 sm:p-6 md:grid-cols-2 md:gap-8 lg:p-12 2xl:max-w-6xl", 10 - 11 - className, 12 - )} 13 - {...props} 14 - /> 15 - </div> 16 - ); 17 - } 18 - 19 - function Example({ 20 - title, 21 - children, 22 - className, 23 - containerClassName, 24 - ...props 25 - }: React.ComponentProps<"div"> & { 26 - title?: string; 27 - containerClassName?: string; 28 - }) { 29 - return ( 30 - <div 31 - data-slot="example" 32 - className={cn( 33 - "mx-auto flex w-full max-w-lg min-w-0 flex-col gap-1 self-stretch lg:max-w-none", 34 - containerClassName, 35 - )} 36 - {...props} 37 - > 38 - {title && ( 39 - <div className="text-muted-foreground px-1.5 py-2 text-xs font-medium"> 40 - {title} 41 - </div> 42 - )} 43 - <div 44 - data-slot="example-content" 45 - className={cn( 46 - "bg-background text-foreground flex min-w-0 flex-1 flex-col items-start gap-6 border border-dashed p-4 sm:p-6 *:[div:not([class*='w-'])]:w-full", 47 - className, 48 - )} 49 - > 50 - {children} 51 - </div> 52 - </div> 53 - ); 54 - } 55 - 56 - export { ExampleWrapper, Example };
+270
src/components/group.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + ArrowLeft01Icon, 5 + Copy01Icon, 6 + Delete01Icon, 7 + Settings01Icon, 8 + Share08Icon, 9 + UserGroupIcon, 10 + UserMultipleIcon, 11 + } from "@hugeicons/core-free-icons"; 12 + import { HugeiconsIcon } from "@hugeicons/react"; 13 + import Link from "next/link"; 14 + import { useState } from "react"; 15 + import CustomAvatar from "@/components/avatar"; 16 + import { Button } from "@/components/ui/button"; 17 + import { 18 + Card, 19 + CardAction, 20 + CardContent, 21 + CardFooter, 22 + CardHeader, 23 + CardTitle, 24 + } from "@/components/ui/card"; 25 + import { 26 + Dialog, 27 + DialogClose, 28 + DialogContent, 29 + DialogDescription, 30 + DialogHeader, 31 + DialogTitle, 32 + DialogTrigger, 33 + } from "@/components/ui/dialog"; 34 + import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 35 + import { Input } from "@/components/ui/input"; 36 + import { 37 + InputGroup, 38 + InputGroupAddon, 39 + InputGroupButton, 40 + InputGroupInput, 41 + } from "@/components/ui/input-group"; 42 + import { 43 + Item, 44 + ItemActions, 45 + ItemContent, 46 + ItemDescription, 47 + ItemGroup, 48 + ItemMedia, 49 + ItemTitle, 50 + } from "@/components/ui/item"; 51 + import { Progress } from "@/components/ui/progress"; 52 + import { Slider } from "@/components/ui/slider"; 53 + import type { voucher } from "@/db/schema"; 54 + import type { GroupWithMembers } from "@/lib/group"; 55 + import { deleteMember } from "@/lib/member"; 56 + import { createVoucher, deleteVoucher, updateVoucher } from "@/lib/voucher"; 57 + 58 + type Voucher = typeof voucher.$inferSelect; 59 + 60 + export default function Group({ 61 + group, 62 + vouchers: initialVouchers, 63 + }: { 64 + group: GroupWithMembers; 65 + vouchers: Voucher[]; 66 + }) { 67 + const [vouchers, setVouchers] = useState(initialVouchers); 68 + 69 + return ( 70 + <> 71 + <header className="flex justify-between"> 72 + <Button variant="secondary" size="icon" render={<Link href="/" />}> 73 + <HugeiconsIcon icon={ArrowLeft01Icon} /> 74 + </Button> 75 + </header> 76 + <Card> 77 + <CardHeader> 78 + <CardTitle>{group.name}</CardTitle> 79 + <CardAction> 80 + <Dialog> 81 + <DialogTrigger 82 + render={ 83 + <Button variant="secondary"> 84 + <HugeiconsIcon icon={UserMultipleIcon} /> 85 + </Button> 86 + } 87 + /> 88 + <DialogContent> 89 + <DialogHeader> 90 + <DialogTitle>Members</DialogTitle> 91 + </DialogHeader> 92 + <ItemGroup> 93 + {[group.owner, ...group.members].map((user) => ( 94 + <Item key={user.id}> 95 + <ItemMedia> 96 + <CustomAvatar 97 + name={user.name} 98 + image={user.image as string} 99 + /> 100 + </ItemMedia> 101 + <ItemContent> 102 + <ItemTitle>{user.name}</ItemTitle> 103 + <ItemDescription> 104 + {user.id === group.userId ? "Owner" : "Member"} 105 + </ItemDescription> 106 + </ItemContent> 107 + {user.id !== group.owner.id && ( 108 + <ItemActions> 109 + <Button 110 + variant="ghost" 111 + onClick={() => deleteMember(group.id, user.id)} 112 + > 113 + <HugeiconsIcon icon={Delete01Icon} /> 114 + </Button> 115 + </ItemActions> 116 + )} 117 + </Item> 118 + ))} 119 + </ItemGroup> 120 + 121 + <InputGroup> 122 + <InputGroupInput 123 + placeholder={`${window.location}?invite=${group.invite}`} 124 + readOnly 125 + /> 126 + <InputGroupAddon align="inline-end"> 127 + <InputGroupButton 128 + onClick={() => 129 + navigator.clipboard.writeText( 130 + `${window.location}?invite=${group.invite}`, 131 + ) 132 + } 133 + > 134 + <HugeiconsIcon icon={Copy01Icon} /> 135 + </InputGroupButton> 136 + </InputGroupAddon> 137 + </InputGroup> 138 + </DialogContent> 139 + </Dialog> 140 + </CardAction> 141 + </CardHeader> 142 + <CardContent className="grid gap-6"> 143 + {vouchers.map((voucher) => ( 144 + <Card key={voucher.id}> 145 + <CardHeader> 146 + <CardTitle>{voucher.name}</CardTitle> 147 + <CardAction> 148 + <Button>{voucher.limit}</Button> 149 + </CardAction> 150 + </CardHeader> 151 + <CardContent> 152 + <Progress value={(1 / voucher.limit) * 100} /> 153 + </CardContent> 154 + <CardFooter> 155 + <Dialog> 156 + <DialogTrigger 157 + render={<Button className="w-full">Edit</Button>} 158 + /> 159 + <DialogContent> 160 + <form 161 + onSubmit={(e) => { 162 + e.preventDefault(); 163 + const formData = new FormData(e.currentTarget); 164 + const name = formData.get("name") as string; 165 + const limit = formData.get("limit") as string; 166 + updateVoucher(voucher.id, name, Number(limit)).then( 167 + (res) => { 168 + if (!res) return; 169 + setVouchers((prevVouchers) => 170 + prevVouchers.map((v) => 171 + v.id === voucher.id ? res : v, 172 + ), 173 + ); 174 + }, 175 + ); 176 + }} 177 + > 178 + <FieldGroup> 179 + <Field> 180 + <FieldLabel htmlFor="name">Name</FieldLabel> 181 + <Input 182 + id="name" 183 + name="name" 184 + defaultValue={voucher.name} 185 + required 186 + /> 187 + </Field> 188 + <Field> 189 + <FieldLabel htmlFor="limit">Limit</FieldLabel> 190 + <Slider 191 + id="limit" 192 + name="limit" 193 + defaultValue={[voucher.limit]} 194 + max={20} 195 + /> 196 + </Field> 197 + <Field orientation="horizontal"> 198 + <DialogClose 199 + render={ 200 + <Button 201 + variant="destructive" 202 + onClick={(e) => { 203 + e.preventDefault(); 204 + deleteVoucher(voucher.id).then((res) => { 205 + if (!res) return; 206 + setVouchers((prevVouchers) => 207 + prevVouchers.filter( 208 + (v) => v.id !== voucher.id, 209 + ), 210 + ); 211 + }); 212 + }} 213 + > 214 + <HugeiconsIcon icon={Delete01Icon} /> 215 + </Button> 216 + } 217 + /> 218 + <DialogClose 219 + render={ 220 + <Button type="submit" className="flex-1"> 221 + Update 222 + </Button> 223 + } 224 + /> 225 + </Field> 226 + </FieldGroup> 227 + </form> 228 + </DialogContent> 229 + </Dialog> 230 + </CardFooter> 231 + </Card> 232 + ))} 233 + <Dialog> 234 + <DialogTrigger render={<Button>Create Voucher</Button>} /> 235 + <DialogContent> 236 + <form 237 + onSubmit={(e) => { 238 + e.preventDefault(); 239 + const formData = new FormData(e.currentTarget); 240 + const name = formData.get("name") as string; 241 + const limit = formData.get("limit") as string; 242 + createVoucher(name, Number(limit), group.id).then((res) => { 243 + if (!res) return; 244 + setVouchers((prevVouchers) => [...prevVouchers, res]); 245 + }); 246 + }} 247 + > 248 + <FieldGroup> 249 + <Field> 250 + <FieldLabel htmlFor="name">Name</FieldLabel> 251 + <Input id="name" name="name" required /> 252 + </Field> 253 + <Field> 254 + <FieldLabel htmlFor="limit">Limit</FieldLabel> 255 + <Slider id="limit" name="limit" max={20} /> 256 + </Field> 257 + <Field> 258 + <DialogClose 259 + render={<Button type="submit">Create</Button>} 260 + /> 261 + </Field> 262 + </FieldGroup> 263 + </form> 264 + </DialogContent> 265 + </Dialog> 266 + </CardContent> 267 + </Card> 268 + </> 269 + ); 270 + }
+60 -11
src/db/schema.ts
··· 1 - import { integer, pgTable, text, varchar } from "drizzle-orm/pg-core"; 1 + import { relations } from "drizzle-orm"; 2 + import { 3 + index, 4 + integer, 5 + pgTable, 6 + primaryKey, 7 + text, 8 + varchar, 9 + } from "drizzle-orm/pg-core"; 10 + 2 11 import { user } from "./auth-schema"; 3 12 4 13 export * from "./auth-schema"; ··· 7 16 id: integer().primaryKey().generatedAlwaysAsIdentity(), 8 17 userId: text() 9 18 .notNull() 10 - .references(() => user.id), 11 - name: varchar().notNull(), 19 + .references(() => user.id, { onDelete: "cascade" }), 20 + name: varchar({ length: 255 }).notNull(), 21 + invite: varchar({ length: 12 }).notNull().unique(), 12 22 }); 13 23 14 - export const voucher = pgTable("voucher", { 15 - id: integer().primaryKey().generatedAlwaysAsIdentity(), 16 - groupId: integer() 17 - .notNull() 18 - .references(() => group.id), 19 - name: varchar().notNull(), 20 - limit: integer().notNull(), 21 - }); 24 + export const voucher = pgTable( 25 + "voucher", 26 + { 27 + id: integer().primaryKey().generatedAlwaysAsIdentity(), 28 + groupId: integer() 29 + .notNull() 30 + .references(() => group.id, { onDelete: "cascade" }), 31 + name: varchar({ length: 255 }).notNull(), 32 + limit: integer().notNull(), 33 + }, 34 + (t) => [index("voucher_group_idx").on(t.groupId)], 35 + ); 36 + 37 + export const member = pgTable( 38 + "member", 39 + { 40 + groupId: integer() 41 + .notNull() 42 + .references(() => group.id, { onDelete: "cascade" }), 43 + userId: text() 44 + .notNull() 45 + .references(() => user.id, { onDelete: "cascade" }), 46 + }, 47 + (t) => [ 48 + primaryKey({ columns: [t.groupId, t.userId] }), 49 + index("member_user_idx").on(t.userId), 50 + ], 51 + ); 52 + 53 + export const groupRelations = relations(group, ({ many, one }) => ({ 54 + members: many(member), 55 + owner: one(user, { 56 + fields: [group.userId], 57 + references: [user.id], 58 + }), 59 + })); 60 + 61 + export const memberRelations = relations(member, ({ one }) => ({ 62 + group: one(group, { 63 + fields: [member.groupId], 64 + references: [group.id], 65 + }), 66 + user: one(user, { 67 + fields: [member.userId], 68 + references: [user.id], 69 + }), 70 + }));
+5 -9
src/lib/email.ts
··· 17 17 }); 18 18 19 19 export async function sendEmail({ to, subject, html }: SendEmailParams) { 20 - try { 21 - const info = await transporter.sendMail({ 20 + await transporter 21 + .sendMail({ 22 22 from: process.env.SMTP_FROM, 23 23 to, 24 24 subject, 25 25 html, 26 - }); 27 - console.log("Message sent: %s", info.messageId); 28 - return { success: true, messageId: info.messageId }; 29 - } catch (error) { 30 - console.error("Error sending email:", error); 31 - return { success: false, error }; 32 - } 26 + }) 27 + .then((info) => ({ success: true, messageId: info.messageId })) 28 + .catch((error) => ({ success: false, error })); 33 29 }
+39 -6
src/lib/group.ts
··· 1 1 "use server"; 2 2 3 - import { and, eq } from "drizzle-orm"; 3 + import { and, eq, or } from "drizzle-orm"; 4 4 5 - import { group } from "@/db/schema"; 5 + import { group, member, user } from "@/db/schema"; 6 6 import { getUserId } from "@/lib/auth"; 7 7 import { db } from "@/lib/db"; 8 + import { generateInvite } from "@/lib/invite"; 9 + 10 + export type GroupWithMembers = Awaited<ReturnType<typeof getGroups>>[number]; 8 11 9 12 export async function createGroup(name: string) { 10 13 const userId = await getUserId(); 11 14 if (!userId) return null; 12 15 13 - const [res] = await db.insert(group).values({ name, userId }).returning(); 16 + const invite = await generateInvite(); 17 + const [res] = await db 18 + .insert(group) 19 + .values({ userId, name, invite }) 20 + .returning(); 21 + 14 22 return res; 15 23 } 16 24 ··· 18 26 const userId = await getUserId(); 19 27 if (!userId) return []; 20 28 21 - return await db.select().from(group).where(eq(group.userId, userId)); 29 + const groups = await db.query.group.findMany({ 30 + where: (group, { exists, eq, or, and }) => 31 + or( 32 + eq(group.userId, userId), 33 + exists( 34 + db 35 + .select() 36 + .from(member) 37 + .where( 38 + and(eq(member.groupId, group.id), eq(member.userId, userId)), 39 + ), 40 + ), 41 + ), 42 + with: { 43 + owner: true, 44 + members: { 45 + with: { 46 + user: true, 47 + }, 48 + }, 49 + }, 50 + }); 51 + 52 + return groups.map((g) => ({ 53 + ...g, 54 + members: g.members.map((m) => m.user), 55 + })); 22 56 } 23 57 24 58 export async function getGroup(id: number) { 25 59 const groups = await getGroups(); 26 - // biome-ignore lint/suspicious/noDoubleEquals: <i need to compare number and string> 27 - return groups.find((g) => g.id == id) || null; 60 + return groups.find((g) => g.id === id) || null; 28 61 } 29 62 30 63 export async function isGroupOwner(id: number) {
+31
src/lib/invite.ts
··· 1 + "use server"; 2 + 3 + import { and, eq } from "drizzle-orm"; 4 + 5 + import { group } from "@/db/schema"; 6 + import { db } from "@/lib/db"; 7 + import { isGroupOwner } from "@/lib/group"; 8 + import { createMember } from "@/lib/member"; 9 + 10 + export async function generateInvite() { 11 + return Math.random().toString(36).substring(2, 8); 12 + } 13 + 14 + export async function refreshInvite(id: number) { 15 + if (!(await isGroupOwner(id))) return null; 16 + 17 + const invite = await generateInvite(); 18 + const [res] = await db 19 + .update(group) 20 + .set({ invite }) 21 + .where(and(eq(group.id, id))) 22 + .returning(); 23 + 24 + return res; 25 + } 26 + 27 + export async function redeemInvite(invite: string) { 28 + const [res] = await db.select().from(group).where(eq(group.invite, invite)); 29 + if (!res) return null; 30 + return await createMember(res.id); 31 + }
+27
src/lib/member.ts
··· 1 + "use server"; 2 + 3 + import { and, eq } from "drizzle-orm"; 4 + import { member } from "@/db/schema"; 5 + import { getUserId } from "@/lib/auth"; 6 + import { db } from "@/lib/db"; 7 + import { getGroup, isGroupOwner } from "@/lib/group"; 8 + 9 + export async function createMember(groupId: number) { 10 + const userId = await getUserId(); 11 + if (!userId) return null; 12 + if (await getGroup(groupId)) return null; 13 + 14 + const [res] = await db.insert(member).values({ groupId, userId }).returning(); 15 + return res; 16 + } 17 + 18 + export async function deleteMember(groupId: number, userId: string) { 19 + if (!(await isGroupOwner(groupId))) return null; 20 + 21 + const [res] = await db 22 + .delete(member) 23 + .where(and(eq(member.groupId, groupId), eq(member.userId, userId))) 24 + .returning(); 25 + 26 + return res; 27 + }
+5 -2
src/proxy.ts
··· 7 7 headers: await headers(), 8 8 }); 9 9 10 - if (!session) return NextResponse.redirect(new URL("/auth", request.url)); 11 - return NextResponse.next(); 10 + if (session) return NextResponse.next(); 11 + 12 + const authUrl = new URL("/auth", request.url); 13 + authUrl.searchParams.set("next", request.nextUrl.pathname); 14 + return NextResponse.redirect(authUrl); 12 15 } 13 16 14 17 export const config = {