UI+ Routes
Made even more changes the UI and added new Routes for searching a user, requesting consent for messaging and others. Now just need to make the SSE work.
This commit is contained in:
parent
25b379aadd
commit
79bdca973c
25 changed files with 1884 additions and 148 deletions
537
package-lock.json
generated
537
package-lock.json
generated
|
|
@ -8,7 +8,10 @@
|
||||||
"name": "sipher",
|
"name": "sipher",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
|
|
@ -682,6 +685,63 @@
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-collapsible": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-alert-dialog": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||||
|
|
@ -730,6 +790,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collapsible": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collection": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||||
|
|
@ -797,6 +886,149 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.2",
|
||||||
|
"@radix-ui/react-presence": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.6",
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.0",
|
||||||
|
"use-sidecar": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll/node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll/node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll/node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll/node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-direction": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||||
|
|
@ -837,6 +1069,72 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-menu": "2.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-icons": {
|
"node_modules/@radix-ui/react-icons": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
|
||||||
|
|
@ -885,6 +1183,153 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-collection": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.2",
|
||||||
|
"@radix-ui/react-presence": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.6",
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.0",
|
||||||
|
"use-sidecar": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll/node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll/node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll/node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll/node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||||
|
|
@ -1000,6 +1445,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-collection": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-scroll-area": {
|
"node_modules/@radix-ui/react-scroll-area": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz",
|
||||||
|
|
@ -1456,6 +1945,17 @@
|
||||||
"node": ">=16.17.0"
|
"node": ">=16.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -1681,6 +2181,11 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-node-es": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
|
|
@ -1808,6 +2313,14 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-nonce": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
|
|
@ -1849,6 +2362,14 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/invariant": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-arrayish": {
|
"node_modules/is-arrayish": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||||
|
|
@ -1942,6 +2463,11 @@
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
|
|
@ -1958,6 +2484,17 @@
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import {createClient} from "@/lib/supabase/server";
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
// Helper function to get user data by UUID
|
// Helper function to get user data by UUID
|
||||||
async function getUserByUUID(supabase: any, uuid: string) {
|
export async function getUserByUUID(supabase: any, uuid: string) {
|
||||||
const {data: userData, error: userError} = await supabase
|
const {data: userData, error: userError} = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
|
|
||||||
74
src/app/api/user/actions/realtime/requests/route.ts
Normal file
74
src/app/api/user/actions/realtime/requests/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
// app/api/realtime/route.ts
|
||||||
|
import {createClient} from '@/lib/supabase/server'
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const supabase = await createClient()
|
||||||
|
console.log("Updated")
|
||||||
|
// Get the current authenticated user
|
||||||
|
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
// If any of these, return a default error.
|
||||||
|
if (userError || !user) {
|
||||||
|
return new Response('Unauthorized', {status: 401})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the stream of data
|
||||||
|
const stream = new TransformStream()
|
||||||
|
const writer = stream.writable.getWriter()
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
|
// Create the channel
|
||||||
|
let channel: ReturnType<typeof supabase.channel> | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// RealTime supabase!
|
||||||
|
channel = supabase
|
||||||
|
.channel('user-requests')
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: 'UPDATE',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'users',
|
||||||
|
filter: `uuid=eq.${user.id}`,
|
||||||
|
},
|
||||||
|
async (payload) => {
|
||||||
|
if (payload.new.requests !== payload.old.requests) {
|
||||||
|
try {
|
||||||
|
const data = encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'requests_update',
|
||||||
|
data: payload.new.requests
|
||||||
|
})}\n\n`)
|
||||||
|
await writer.write(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing to stream:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
const initialData = encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'connected',
|
||||||
|
message: 'SSE connection established'
|
||||||
|
})}\n\n`)
|
||||||
|
await writer.write(initialData)
|
||||||
|
|
||||||
|
request.signal.addEventListener('abort', () => {
|
||||||
|
channel?.unsubscribe()
|
||||||
|
writer.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in SSE setup:', error)
|
||||||
|
return new Response('Error setting up SSE', {status: 500})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(stream.readable, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
47
src/app/api/user/search/user/route.ts
Normal file
47
src/app/api/user/search/user/route.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import {createClient} from "@/lib/supabase/server";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const {searchParams} = new URL(request.url);
|
||||||
|
const uuid = searchParams.get('uuid');
|
||||||
|
console.log('Searching for UUID:', uuid);
|
||||||
|
|
||||||
|
if (!uuid) {
|
||||||
|
return NextResponse.json({error: "Missing UUID from request"}, {status: 400})
|
||||||
|
} else if (uuid.length > 10) {
|
||||||
|
return NextResponse.json({error: "UUID is not valid."}, {status: 400});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
if (userError) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: userError},
|
||||||
|
{status: userError?.status}
|
||||||
|
)
|
||||||
|
} else if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "User not found"},
|
||||||
|
{status: 401}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpcResult = await supabase.rpc('search_users', {
|
||||||
|
search_term: uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
const {data, error} = rpcResult;
|
||||||
|
if (error) {
|
||||||
|
return NextResponse.json({error: error}, {status: 500});
|
||||||
|
} else if (data.length === 0) {
|
||||||
|
return NextResponse.json({user: []}, {status: 200});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({error: error}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/app/api/user/send/request/route.ts
Normal file
66
src/app/api/user/send/request/route.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {createClient} from "@/lib/supabase/server";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {SupabaseClient} from "@supabase/supabase-js";
|
||||||
|
import {getUserByUUID} from "@/app/api/auth/get_user/route";
|
||||||
|
|
||||||
|
async function updateUserRequests(searchTerm: string, requestSuuid: string, supabase: SupabaseClient<any, "public", any>) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const {data, error} = await supabase.rpc('update_user_requests', {
|
||||||
|
search_term: searchTerm,
|
||||||
|
new_request: requestSuuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {success: true, data};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating user requests:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
const {searchTerm} = await request.json();
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Missing required fields"},
|
||||||
|
{status: 400}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data: {user}, error: authError} = await supabase.auth.getUser();
|
||||||
|
if (authError) throw authError;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({user: null}, {status: 401});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSuuid = (await getUserByUUID(supabase, user.id)).suuid;
|
||||||
|
|
||||||
|
const result = await updateUserRequests(searchTerm, userSuuid, supabase);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: result.error},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({success: true});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Failed to update requests"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import {SharedStateProvider} from "@/hooks/shared-states";
|
||||||
import ThemeProvider from "@/components/ui/theme-provider";
|
import ThemeProvider from "@/components/ui/theme-provider";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {Toaster} from "@/components/ui/toaster";
|
import {Toaster} from "@/components/ui/toaster";
|
||||||
|
import {RealtimeRequests} from "@/components/main/realtime/request";
|
||||||
|
|
||||||
const publicSans = Public_Sans({
|
const publicSans = Public_Sans({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
|
|
@ -57,6 +58,7 @@ export default async function RootLayout(
|
||||||
<div className={`max-h-[1080px] p-6 bg-secondary`}>
|
<div className={`max-h-[1080px] p-6 bg-secondary`}>
|
||||||
<div className="flex bg-background">
|
<div className="flex bg-background">
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
|
<RealtimeRequests/>
|
||||||
{children}
|
{children}
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
285
src/app/page.tsx
285
src/app/page.tsx
|
|
@ -3,18 +3,89 @@ import {useTheme} from "next-themes";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {Feather, Search} from "lucide-react";
|
import {Feather, Search} from "lucide-react";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
export default function SiPher() {
|
export default function SiPher() {
|
||||||
const {theme, systemTheme} = useTheme();
|
const {theme, systemTheme} = useTheme();
|
||||||
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
/** Consent Form states */
|
||||||
|
const [showConsentForm, setShowConsentForm] = useState(false);
|
||||||
|
const [formError, setFormError] = useState("");
|
||||||
|
|
||||||
|
/** Input states */
|
||||||
|
const [inputDisabled, setInputDisabled] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
/** Search expandability state */
|
||||||
|
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param search_term Either the SUUID or username (If not indexable, will return false.)
|
||||||
|
*/
|
||||||
|
const fetchUser = async (search_term: string) => {
|
||||||
|
// Search term cannot be empty
|
||||||
|
if (search_term.length <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends the requisition to the API by using native fetch.
|
||||||
|
const req = await fetch(`/api/user/search/user?uuid=${search_term}`);
|
||||||
|
|
||||||
|
// Checks if the response is ok (200) or not, if not, returns false.
|
||||||
|
if (!req.ok) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await req.json() as { exists: boolean };
|
||||||
|
// If the user does not exist, just return it
|
||||||
|
if (!user.exists) return user.exists;
|
||||||
|
|
||||||
|
setShowConsentForm(true); // Shows the confirmation to ask the other user to consent to the communication;
|
||||||
|
setInputDisabled(true); // Makes the input disabled until either the user cancels the consent form or accepts it;
|
||||||
|
return user.exists; // If everything went right and the user was found, return true
|
||||||
|
}
|
||||||
|
const sendRequest = async (user: string) => {
|
||||||
|
if (user.length <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = await fetch(`/api/user/send/request`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
searchTerm: user, // SUUID or username
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!req.ok) return false;
|
||||||
|
|
||||||
|
const {sent} = await req.json() as { sent: boolean };
|
||||||
|
// If the user does not exist, just return it
|
||||||
|
if (!sent) return sent;
|
||||||
|
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
const getTheme = () => {
|
const getTheme = () => {
|
||||||
if (!mounted) return "light";
|
if (!mounted) return "light";
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
|
|
@ -26,81 +97,149 @@ export default function SiPher() {
|
||||||
const currentTheme = getTheme();
|
const currentTheme = getTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={`relative flex-1 ${currentTheme === "dark" ? "dark" : ""} w-full max-h-[600px] bg-gradient-to-b from-background to-background/95`}>
|
<AlertDialog open={showConsentForm} onOpenChange={(open) => {
|
||||||
{/* Animated background elements */}
|
if (!open) setFormError("");
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
}}>
|
||||||
<div
|
<AlertDialogTrigger/>
|
||||||
className="absolute inset-0 bg-[radial-gradient(circle_500px_at_50%_50%,rgba(120,120,120,0.05),transparent)]"/>
|
<AlertDialogContent>
|
||||||
</div>
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Consent Form</AlertDialogTitle>
|
||||||
<div className="relative flex flex-col justify-center items-center h-screen px-4 select-none space-y-8">
|
<AlertDialogDescription className={"flex flex-col space-y-1"}>
|
||||||
{/* Logo section with subtle hover effect */}
|
<span>
|
||||||
<div className="relative group">
|
Are you sure you want to contact <span className={"font-bold"}>{inputValue}</span>?
|
||||||
<div
|
</span>
|
||||||
className="absolute inset-0 bg-primary/5 rounded-full blur-xl group-hover:bg-primary/10 transition-all duration-500"/>
|
<span>
|
||||||
<Image
|
By continuing, <span className={"font-bold"}>{inputValue}</span> will receive a notification to accept
|
||||||
priority
|
it. If accepted, that user will appear on your sidebar, if rejected, you will never know about it.
|
||||||
src={`/logos/logo.png`}
|
</span>
|
||||||
alt="SiPher"
|
</AlertDialogDescription>
|
||||||
width={128}
|
</AlertDialogHeader>
|
||||||
height={128}
|
<AlertDialogFooter>
|
||||||
draggable={false}
|
<AlertDialogCancel
|
||||||
className="relative transform transition-transform duration-500 group-hover:scale-105"
|
onClick={() => {
|
||||||
/>
|
setShowConsentForm(false);
|
||||||
</div>
|
setInputDisabled(false);
|
||||||
|
}}
|
||||||
{/* Main text content with improved typography and spacing */}
|
>Cancel</AlertDialogCancel>
|
||||||
<div className="max-w-2xl space-y-6 text-center">
|
<AlertDialogAction
|
||||||
<p className="text-lg md:text-xl font-medium leading-relaxed text-primary">
|
onClick={() => {
|
||||||
Where shadows dance and secrets nest, Silent Whisper serves as the dark sanctuary for those
|
sendRequest(inputValue).then((result) => {
|
||||||
who value discretion above all. Born from ancient corvid traditions, this messenger's haven ensures your
|
if (!result) setFormError("Could not send notification for whatever reason. Sorry.");
|
||||||
whispers remain unheard by all but their intended recipients.
|
});
|
||||||
</p>
|
setInputDisabled(false);
|
||||||
|
}}
|
||||||
<p className="text-sm md:text-base font-medium text-muted-foreground leading-relaxed">
|
>Continue</AlertDialogAction>
|
||||||
Like the sacred ravens of old, your messages fly through the darkness, their contents sealed by shadows and
|
</AlertDialogFooter>
|
||||||
protected by forgotten wards. Each member of our dark fellowship is known only by their chosen name, their
|
</AlertDialogContent>
|
||||||
true identity shrouded in mystery.
|
</AlertDialog>
|
||||||
</p>
|
<div
|
||||||
</div>
|
className={`relative flex-1 ${currentTheme === "dark" ? "dark" : ""} w-full max-h-[600px] bg-gradient-to-b from-background to-background/95`}>
|
||||||
|
<div className="relative flex flex-col justify-center items-center h-screen px-4 select-none space-y-8">
|
||||||
{/* Enhanced search component */}
|
<div className="relative group">
|
||||||
<div className="relative mt-8">
|
<div
|
||||||
<div
|
className="absolute inset-0 bg-primary/5 rounded-full blur-xl group-hover:bg-primary/10 transition-all duration-500"/>
|
||||||
className={`flex items-center rounded-full transition-all duration-300 ${
|
<Image
|
||||||
isSearchExpanded
|
priority
|
||||||
? "bg-secondary/30 backdrop-blur-sm border border-primary/20 shadow-lg"
|
src={`/logos/logo.png`}
|
||||||
: ""
|
alt="SiPher"
|
||||||
}`}
|
width={128}
|
||||||
style={{
|
height={128}
|
||||||
width: isSearchExpanded ? "240px" : "40px",
|
draggable={false}
|
||||||
}}
|
className="relative transform transition-transform duration-500 group-hover:scale-105"
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={`flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full
|
|
||||||
${currentTheme === "dark" ? "hover:bg-secondary/60" : "hover:bg-primary/10"}
|
|
||||||
transition-colors duration-200`}
|
|
||||||
onClick={() => setIsSearchExpanded(!isSearchExpanded)}
|
|
||||||
>
|
|
||||||
<Search className="w-5 h-5"/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Find fellow shadows..."
|
|
||||||
className={`w-full bg-transparent focus:outline-none text-primary placeholder-primary/50
|
|
||||||
transition-all duration-300 ${isSearchExpanded ? "px-4" : "w-0 px-0"}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decorative feather icon */}
|
<div className="max-w-2xl space-y-6 text-center">
|
||||||
<Feather
|
<p className="text-lg md:text-xl font-medium leading-relaxed text-primary">
|
||||||
className={`absolute -right-6 top-1/2 -translate-y-1/2 w-4 h-4 text-primary/30 transform rotate-45
|
Where shadows dance and secrets nest, Silent Whisper serves as the dark sanctuary for those
|
||||||
|
who value discretion above all. Born from ancient corvid traditions, this messenger's haven ensures your
|
||||||
|
whispers remain unheard by all but their intended recipients.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-sm md:text-base font-medium text-muted-foreground leading-relaxed">
|
||||||
|
Like the sacred ravens of old, your messages fly through the darkness, their contents sealed by shadows
|
||||||
|
and
|
||||||
|
protected by forgotten wards. Each member of our dark fellowship is known only by their chosen name, their
|
||||||
|
true identity shrouded in mystery.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mt-8">
|
||||||
|
<div
|
||||||
|
className={`flex items-center rounded-full transition-all duration-300 ${
|
||||||
|
isSearchExpanded
|
||||||
|
? "bg-secondary/30 backdrop-blur-sm border border-primary/20 shadow-lg"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: isSearchExpanded ? "240px" : "40px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full
|
||||||
|
${currentTheme === "dark" ? "hover:bg-secondary/60" : "hover:bg-primary/10"}
|
||||||
|
transition-colors duration-200`}
|
||||||
|
onClick={() => setIsSearchExpanded(!isSearchExpanded)}
|
||||||
|
>
|
||||||
|
<Search className="w-5 h-5"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Find fellow shadows..."
|
||||||
|
className={`w-full bg-transparent focus:outline-none text-primary placeholder-primary/50
|
||||||
|
transition-all duration-300 ${isSearchExpanded ? "px-4" : "w-0 px-0"}`}
|
||||||
|
disabled={inputDisabled}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
fetchUser(inputValue).then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Feather
|
||||||
|
className={`absolute -right-6 top-1/2 -translate-y-1/2 w-4 h-4 text-primary/30 transform rotate-45
|
||||||
transition-opacity duration-300 ${isSearchExpanded ? "opacity-100" : "opacity-0"}`}
|
transition-opacity duration-300 ${isSearchExpanded ? "opacity-100" : "opacity-0"}`}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator/>
|
||||||
|
<div className={"flex flex-col w-[400px]"}>
|
||||||
|
<p className="text-lg md:text-xl font-medium leading-relaxed text-primary">
|
||||||
|
F.A.Q
|
||||||
|
</p>
|
||||||
|
<Accordion type={"single"} collapsible className={"w-full-30%"}>
|
||||||
|
<AccordionItem value={"works"}>
|
||||||
|
<AccordionTrigger>How does this works?</AccordionTrigger>
|
||||||
|
<AccordionContent asChild>
|
||||||
|
<Link href="/about" className={"text-primary text-lg p-1"}>
|
||||||
|
Please, click here
|
||||||
|
</Link>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value={"exists"}>
|
||||||
|
<AccordionTrigger>Why does this exists?</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
I made this as a CS50X final project, hence why it is not intended for real usage. (Do not use it in a
|
||||||
|
situation where you need real privacy.)
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value={"os"}>
|
||||||
|
<AccordionTrigger>Is this open-source?</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
No, not yet (As of 11/12/2024)
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
61
src/components/main/realtime/request.tsx
Normal file
61
src/components/main/realtime/request.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// components/RealtimeRequests.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useEffect} from 'react'
|
||||||
|
import {useToast} from "@/hooks/use-toast"
|
||||||
|
import {useUser} from "@/contexts/user"
|
||||||
|
|
||||||
|
export function RealtimeRequests() {
|
||||||
|
const {toast} = useToast()
|
||||||
|
const {user, updateUser} = useUser()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const eventSource = new EventSource('/api/user/actions/realtime/requests')
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
switch (data.type) {
|
||||||
|
case 'connected':
|
||||||
|
console.log('SSE connected:', data.message)
|
||||||
|
break
|
||||||
|
case 'requests_update':
|
||||||
|
// Update the user context with new requests
|
||||||
|
updateUser({...user, requests: data.data})
|
||||||
|
// Show a toast notification
|
||||||
|
toast({
|
||||||
|
title: "New Request",
|
||||||
|
description: "You have a new request pending",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log('Unknown message type:', data.type)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing SSE message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('SSE error:', error)
|
||||||
|
eventSource.close()
|
||||||
|
|
||||||
|
// Optionally show an error toast
|
||||||
|
toast({
|
||||||
|
title: "Connection Error",
|
||||||
|
description: "Failed to connect to realtime updates",
|
||||||
|
variant: "destructive",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventSource.close()
|
||||||
|
}
|
||||||
|
}, [user, updateUser, toast])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useState} from "react"
|
||||||
import {usePathname} from "next/navigation"
|
import {usePathname} from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import {AnimatePresence, motion} from "framer-motion"
|
import {AnimatePresence, motion} from "framer-motion"
|
||||||
import {LogOut, X} from "lucide-react"
|
import {LogOut, Mail, MailPlus, X} from "lucide-react"
|
||||||
import {Button} from "@/components/ui/button"
|
import {Button} from "@/components/ui/button"
|
||||||
import {Avatar, AvatarFallback} from "@/components/ui/avatar"
|
import {Avatar, AvatarFallback} from "@/components/ui/avatar"
|
||||||
import {Separator} from "@/components/ui/separator"
|
import {Separator} from "@/components/ui/separator"
|
||||||
|
|
@ -16,6 +16,7 @@ import {useRefs, useUIState} from "@/hooks/shared-states";
|
||||||
import {useToast} from "@/hooks/use-toast";
|
import {useToast} from "@/hooks/use-toast";
|
||||||
import {useTheme} from "next-themes";
|
import {useTheme} from "next-themes";
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
|
import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
|
@ -38,6 +39,8 @@ function Sidebar(
|
||||||
const {isDrawerOpen, setIsDrawerOpen} = useUIState()
|
const {isDrawerOpen, setIsDrawerOpen} = useUIState()
|
||||||
const {drawerRef} = useRefs();
|
const {drawerRef} = useRefs();
|
||||||
|
|
||||||
|
const [pendingRequest, setPendingRequest] = useState<number>(0);
|
||||||
|
|
||||||
const user = useUser().user!;
|
const user = useUser().user!;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -45,6 +48,10 @@ function Sidebar(
|
||||||
suuid
|
suuid
|
||||||
} = user
|
} = user
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPendingRequest(user.requests?.length || 0);
|
||||||
|
}, [user])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getThreads = async () => {
|
const getThreads = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -126,6 +133,31 @@ function Sidebar(
|
||||||
<ScrollArea className="flex-grow max-h-[590px] px-4 py-4">
|
<ScrollArea className="flex-grow max-h-[590px] px-4 py-4">
|
||||||
<nav>
|
<nav>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<div className={"flex flex-row items-center w-full justify-start text-[17px]"}>
|
||||||
|
{
|
||||||
|
pendingRequest > 0 ? (
|
||||||
|
<MailPlus className="w-8 h-8 mr-3 p-1"/>
|
||||||
|
) : (
|
||||||
|
<Mail className="w-8 h-8 mr-3 p-1"/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Requests
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="px-4 py-1 w-56" side={"right"}>
|
||||||
|
{
|
||||||
|
pendingRequest > 0 && user.requests.map((request, item) => {
|
||||||
|
return (
|
||||||
|
<p>{request}</p>
|
||||||
|
)
|
||||||
|
}) || (
|
||||||
|
<p>Nothing new here</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
{threads.map((thread) => (
|
{threads.map((thread) => (
|
||||||
<li key={thread.id}>
|
<li key={thread.id}>
|
||||||
<Link href={thread.id} passHref>
|
<Link href={thread.id} passHref>
|
||||||
|
|
|
||||||
57
src/components/ui/accordion.tsx
Normal file
57
src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
))
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
))
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
141
src/components/ui/alert-dialog.tsx
Normal file
141
src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
201
src/components/ui/dropdown-menu.tsx
Normal file
201
src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
||||||
|
|
@ -1,89 +1,102 @@
|
||||||
// contexts/user.tsx
|
// contexts/user.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {createContext, useContext} from 'react';
|
import {createContext, useContext, useState} from 'react';
|
||||||
import {useRouter} from 'next/navigation';
|
import {useRouter} from 'next/navigation';
|
||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
user: NonNullable<SiPher.User>;
|
user: NonNullable<SiPher.User>;
|
||||||
getUser: (context: string) => Promise<NonNullable<SiPher.User>>;
|
getUser: (context: string, userId?: string) => Promise<NonNullable<SiPher.User>>;
|
||||||
|
updateUser: (newUserData: NonNullable<SiPher.User>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<UserContextType | null>(null);
|
const UserContext = createContext<UserContextType | null>(null);
|
||||||
|
|
||||||
export function useUser() {
|
export function useUser() {
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useUser must be used within a UserProvider');
|
throw new Error('useUser must be used within a UserProvider');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: context.user,
|
user: context.user,
|
||||||
getUser: async (context: string, userId?: string) => {
|
updateUser: context.updateUser,
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
getUser: async (context: string, userId?: string) => {
|
||||||
console.log(`useUser().getUser(): Being called by ${context}`)
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
}
|
console.log(`useUser().getUser(): Being called by ${context}`)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/auth/get_user?${
|
const response = await fetch(`/api/auth/get_user?${
|
||||||
userId && `uuid=${
|
userId && `uuid=${
|
||||||
encodeURIComponent(userId)
|
encodeURIComponent(userId)
|
||||||
}`
|
}`
|
||||||
}`);
|
}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
if (error.message?.includes("Auth session missing!")) {
|
if (error.message?.includes("Auth session missing!")) {
|
||||||
throw new Error('No authenticated user');
|
throw new Error('No authenticated user');
|
||||||
}
|
}
|
||||||
throw new Error(error.message || 'Authentication failed');
|
throw new Error(error.message || 'Authentication failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {user} = await response.json();
|
const {user} = await response.json();
|
||||||
return user as NonNullable<SiPher.User>;
|
return user as NonNullable<SiPher.User>;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get user:', error);
|
console.error('Failed to get user:', error);
|
||||||
router.push('/auth/login');
|
router.push('/auth/login');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkAuth: async (context: string) => {
|
checkAuth: async (context: string) => {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
console.log(`useUser().checkAuth(): Being called by ${context}`)
|
console.log(`useUser().checkAuth(): Being called by ${context}`)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/get_user');
|
const response = await fetch('/api/auth/get_user');
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserProvider(
|
export function UserProvider(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
initialUser
|
initialUser
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
initialUser: NonNullable<SiPher.User>;
|
initialUser: NonNullable<SiPher.User>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return (
|
const [user, setUser] = useState<NonNullable<SiPher.User>>(initialUser);
|
||||||
<UserContext.Provider value={{
|
|
||||||
user: initialUser,
|
const updateUser = (newUserData: NonNullable<SiPher.User>) => {
|
||||||
getUser: async () => {
|
setUser(newUserData);
|
||||||
const response = await fetch('/api/auth/get_user');
|
};
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to get user');
|
return (
|
||||||
}
|
<UserContext.Provider value={{
|
||||||
const {user} = await response.json();
|
user,
|
||||||
return user as NonNullable<SiPher.User>;
|
updateUser,
|
||||||
}
|
getUser: async (context: string, userId?: string) => {
|
||||||
}}>
|
const response = await fetch(`/api/auth/get_user?${
|
||||||
{children}
|
userId && `uuid=${
|
||||||
</UserContext.Provider>
|
encodeURIComponent(userId)
|
||||||
);
|
}`
|
||||||
|
}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to get user');
|
||||||
|
}
|
||||||
|
const {user} = await response.json();
|
||||||
|
return user as NonNullable<SiPher.User>;
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use server"
|
||||||
import {CookieOptions, createServerClient} from '@supabase/ssr';
|
import {CookieOptions, createServerClient} from '@supabase/ssr';
|
||||||
|
|
||||||
import {cookies} from 'next/headers';
|
import {cookies} from 'next/headers';
|
||||||
|
|
|
||||||
2
src/types/user.d.ts
vendored
2
src/types/user.d.ts
vendored
|
|
@ -24,6 +24,8 @@ declare global {
|
||||||
created_at: string,
|
created_at: string,
|
||||||
/** Messages field */
|
/** Messages field */
|
||||||
messages: Messages[]
|
messages: Messages[]
|
||||||
|
/** Consent Requests */
|
||||||
|
requests: string[] // Only accessible to the current user logged in. Will contain an array of SUUIDs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
supabase/row_level_security_policies_for_messaging_app.sql
Normal file
37
supabase/row_level_security_policies_for_messaging_app.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
-- RLS Policies
|
||||||
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE message_threads ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE thread_participants ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Users policies
|
||||||
|
CREATE POLICY "Users can view their own profile"
|
||||||
|
ON users FOR SELECT
|
||||||
|
USING (auth.uid() = uuid);
|
||||||
|
|
||||||
|
CREATE POLICY "Users can view indexable profiles"
|
||||||
|
ON users FOR SELECT
|
||||||
|
USING (indexable = true);
|
||||||
|
|
||||||
|
CREATE POLICY "Users can update their own profile"
|
||||||
|
ON users FOR UPDATE
|
||||||
|
USING (auth.uid() = uuid);
|
||||||
|
|
||||||
|
-- Message threads policies
|
||||||
|
CREATE POLICY "Users can view their threads"
|
||||||
|
ON message_threads FOR SELECT
|
||||||
|
USING (is_thread_participant(id));
|
||||||
|
|
||||||
|
-- Thread participants policies
|
||||||
|
CREATE POLICY "Users can view their thread participants"
|
||||||
|
ON thread_participants FOR SELECT
|
||||||
|
USING (is_thread_participant(thread_id));
|
||||||
|
|
||||||
|
-- Messages policies
|
||||||
|
CREATE POLICY "Users can view their messages"
|
||||||
|
ON messages FOR SELECT
|
||||||
|
USING (is_thread_participant(thread_id));
|
||||||
|
|
||||||
|
CREATE POLICY "Users can send messages"
|
||||||
|
ON messages FOR INSERT
|
||||||
|
WITH CHECK (is_thread_participant(thread_id));
|
||||||
59
supabase/user_access_policy_for_search_function.sql
Normal file
59
supabase/user_access_policy_for_search_function.sql
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
-- Drop the existing policy if it exists
|
||||||
|
DROP POLICY IF EXISTS "Allow SUUID searches" ON public.users;
|
||||||
|
|
||||||
|
-- Create a new policy to explicitly allow SUUID searches
|
||||||
|
CREATE POLICY "Allow SUUID searches - Exact Match" ON public.users
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
suuid = current_setting('request.jwt.claims')::json ->> 'search_term'
|
||||||
|
OR indexable = true
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create an alternative approach: more permissive policy for SUUID searches
|
||||||
|
CREATE POLICY "Allow SUUID searches - Permissive" ON public.users
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
suuid = ANY (
|
||||||
|
ARRAY (
|
||||||
|
SELECT
|
||||||
|
unnest(
|
||||||
|
regexp_split_to_array(
|
||||||
|
current_setting('request.jwt.claims')::json ->> 'search_term',
|
||||||
|
','
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OR indexable = true
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create or replace the search_users function
|
||||||
|
CREATE OR REPLACE FUNCTION public.search_users (search_term TEXT)
|
||||||
|
RETURNS TABLE (
|
||||||
|
uuid UUID,
|
||||||
|
suuid TEXT,
|
||||||
|
username TEXT,
|
||||||
|
indexable BOOLEAN
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Set the search term in the current transaction
|
||||||
|
SET LOCAL "request.jwt.claim.search_term" = search_term;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
u.uuid,
|
||||||
|
u.suuid::TEXT,
|
||||||
|
CASE
|
||||||
|
WHEN u.suuid = search_term OR u.indexable THEN u.username
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
u.indexable
|
||||||
|
FROM public.users u
|
||||||
|
WHERE
|
||||||
|
u.suuid = search_term
|
||||||
|
OR (
|
||||||
|
u.indexable = true AND
|
||||||
|
u.username ILIKE '%' || search_term || '%'
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
5
supabase/user_and_message_indexes.sql
Normal file
5
supabase/user_and_message_indexes.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_users_suuid ON users(suuid);
|
||||||
|
CREATE INDEX idx_users_indexable ON users(indexable) WHERE indexable = true;
|
||||||
|
CREATE INDEX idx_thread_participants_user ON thread_participants(user_uuid);
|
||||||
|
CREATE INDEX idx_messages_thread ON messages(thread_id);
|
||||||
116
supabase/user_management_functions.sql
Normal file
116
supabase/user_management_functions.sql
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
-- For generate_short_uuid
|
||||||
|
CREATE
|
||||||
|
OR REPLACE FUNCTION public.generate_short_uuid () RETURNS TEXT AS $$
|
||||||
|
DECLARE
|
||||||
|
chars TEXT := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
result TEXT := '';
|
||||||
|
i INTEGER := 0;
|
||||||
|
max_attempts INTEGER := 10;
|
||||||
|
current_attempt INTEGER := 0;
|
||||||
|
is_unique BOOLEAN := false;
|
||||||
|
BEGIN
|
||||||
|
WHILE NOT is_unique AND current_attempt < max_attempts LOOP
|
||||||
|
result := '';
|
||||||
|
FOR i IN 1..8 LOOP
|
||||||
|
result := result || substr(chars, floor(random() * length(chars) + 1)::integer, 1);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
SELECT COUNT(*) = 0 INTO is_unique
|
||||||
|
FROM public.users
|
||||||
|
WHERE suuid = result;
|
||||||
|
|
||||||
|
current_attempt := current_attempt + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF NOT is_unique THEN
|
||||||
|
RAISE EXCEPTION 'Could not generate unique short UUID after % attempts', max_attempts;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.search_users(search_term TEXT)
|
||||||
|
RETURNS TABLE (
|
||||||
|
uuid UUID,
|
||||||
|
suuid TEXT,
|
||||||
|
username TEXT,
|
||||||
|
indexable BOOLEAN
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
u.uuid,
|
||||||
|
u.suuid::TEXT,
|
||||||
|
-- Simplified CASE logic: show username if SUUID match OR (username match AND indexable)
|
||||||
|
CASE
|
||||||
|
WHEN u.suuid = search_term OR u.indexable THEN u.username
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
u.indexable
|
||||||
|
FROM public.users u
|
||||||
|
WHERE
|
||||||
|
u.suuid = search_term -- Case 1: SUUID match (always show)
|
||||||
|
OR (
|
||||||
|
u.indexable = true AND -- Case 2: Username match + indexable
|
||||||
|
u.username ILIKE '%' || search_term || '%'
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- For is_thread_participant
|
||||||
|
CREATE
|
||||||
|
OR REPLACE FUNCTION public.is_thread_participant (thread_uuid UUID) RETURNS BOOLEAN AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM public.thread_participants
|
||||||
|
WHERE thread_id = thread_uuid
|
||||||
|
AND user_uuid = auth.uid()
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- For get_user_threads
|
||||||
|
CREATE
|
||||||
|
OR REPLACE FUNCTION public.get_user_threads (user_id UUID) RETURNS TABLE (
|
||||||
|
thread_id UUID,
|
||||||
|
participants TEXT[],
|
||||||
|
messages JSON[]
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM public.thread_participants
|
||||||
|
WHERE user_uuid = user_id
|
||||||
|
) THEN
|
||||||
|
-- Return empty result if user has no threads
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
mt.id,
|
||||||
|
array_agg(DISTINCT u.username),
|
||||||
|
COALESCE(array_agg(
|
||||||
|
CASE WHEN m.id IS NOT NULL THEN
|
||||||
|
json_build_object(
|
||||||
|
'id', m.id,
|
||||||
|
'content', m.content,
|
||||||
|
'created_at', m.created_at
|
||||||
|
)
|
||||||
|
ELSE NULL END
|
||||||
|
) FILTER (WHERE m.id IS NOT NULL), ARRAY[]::JSON[])
|
||||||
|
FROM public.message_threads mt
|
||||||
|
JOIN public.thread_participants tp ON mt.id = tp.thread_id
|
||||||
|
JOIN public.users u ON tp.user_uuid = u.uuid
|
||||||
|
LEFT JOIN public.messages m ON mt.id = m.thread_id
|
||||||
|
WHERE mt.id IN (
|
||||||
|
SELECT thread_id
|
||||||
|
FROM public.thread_participants
|
||||||
|
WHERE user_uuid = user_id
|
||||||
|
)
|
||||||
|
GROUP BY mt.id;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
46
supabase/user_management_table.sql
Normal file
46
supabase/user_management_table.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
-- Drop everything related to users
|
||||||
|
DROP TABLE IF EXISTS public.users CASCADE;
|
||||||
|
|
||||||
|
-- Create new users table
|
||||||
|
CREATE TABLE public.users (
|
||||||
|
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
suuid CHAR(8) UNIQUE NOT NULL,
|
||||||
|
username TEXT UNIQUE NOT NULL CHECK (length(username) >= 3 AND username ~ '^[a-zA-Z0-9_-]+$'),
|
||||||
|
indexable BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create trigger function for SUUID generation
|
||||||
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.suuid := public.generate_short_uuid();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Create the trigger
|
||||||
|
CREATE TRIGGER on_user_created
|
||||||
|
BEFORE INSERT ON public.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.handle_new_user();
|
||||||
|
|
||||||
|
-- Add policies
|
||||||
|
CREATE POLICY "Users can view their own profile" ON public.users
|
||||||
|
FOR SELECT USING (auth.uid() = uuid);
|
||||||
|
|
||||||
|
CREATE POLICY "Users can view indexable profiles" ON public.users
|
||||||
|
FOR SELECT USING (indexable = true);
|
||||||
|
|
||||||
|
CREATE POLICY "Allow user registration" ON public.users
|
||||||
|
FOR INSERT WITH CHECK (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Users can update their own profile" ON public.users
|
||||||
|
FOR UPDATE USING (auth.uid() = uuid);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Create index for better performance
|
||||||
|
CREATE INDEX idx_users_suuid ON public.users(suuid);
|
||||||
|
CREATE INDEX idx_users_indexable ON public.users(indexable) WHERE indexable = true;
|
||||||
47
supabase/user_registration_policy.sql
Normal file
47
supabase/user_registration_policy.sql
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
-- Drop everything related to users
|
||||||
|
DROP TABLE IF EXISTS public.users CASCADE;
|
||||||
|
|
||||||
|
-- Create new users table
|
||||||
|
CREATE TABLE public.users (
|
||||||
|
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
suuid CHAR(8) UNIQUE NOT NULL,
|
||||||
|
username TEXT UNIQUE NOT NULL CHECK (length(username) >= 3 AND username ~ '^[a-zA-Z0-9_-]+$'),
|
||||||
|
password TEXT NOT NULL CHECK (length(password) >= 8),
|
||||||
|
indexable BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create trigger function for SUUID generation
|
||||||
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.suuid := public.generate_short_uuid();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Create the trigger
|
||||||
|
CREATE TRIGGER on_user_created
|
||||||
|
BEFORE INSERT ON public.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.handle_new_user();
|
||||||
|
|
||||||
|
-- Add policies
|
||||||
|
CREATE POLICY "Users can view their own profile" ON public.users
|
||||||
|
FOR SELECT USING (auth.uid() = uuid);
|
||||||
|
|
||||||
|
CREATE POLICY "Users can view indexable profiles" ON public.users
|
||||||
|
FOR SELECT USING (indexable = true);
|
||||||
|
|
||||||
|
CREATE POLICY "Allow user registration" ON public.users
|
||||||
|
FOR INSERT WITH CHECK (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Users can update their own profile" ON public.users
|
||||||
|
FOR UPDATE USING (auth.uid() = uuid);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Create index for better performance
|
||||||
|
CREATE INDEX idx_users_suuid ON public.users(suuid);
|
||||||
|
CREATE INDEX idx_users_indexable ON public.users(indexable) WHERE indexable = true;
|
||||||
1
supabase/user_search_function.sql
Normal file
1
supabase/user_search_function.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
SELECT * FROM public.search_users('chCzlx84');
|
||||||
27
supabase/users_table.sql
Normal file
27
supabase/users_table.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
-- Base Tables
|
||||||
|
CREATE TABLE users (
|
||||||
|
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
suuid CHAR(8) UNIQUE NOT NULL,
|
||||||
|
username TEXT UNIQUE NOT NULL CHECK (length(username) >= 3 AND username ~ '^[a-zA-Z0-9_-]+$'),
|
||||||
|
password TEXT NOT NULL CHECK (length(password) >= 8),
|
||||||
|
indexable BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE message_threads (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE thread_participants (
|
||||||
|
thread_id UUID REFERENCES message_threads(id) ON DELETE CASCADE,
|
||||||
|
user_uuid UUID REFERENCES users(uuid) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (thread_id, user_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
thread_id UUID REFERENCES message_threads(id) ON DELETE CASCADE,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -55,6 +55,28 @@ export default {
|
||||||
lg: 'var(--radius)',
|
lg: 'var(--radius)',
|
||||||
md: 'calc(var(--radius) - 2px)',
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down': {
|
||||||
|
from: {
|
||||||
|
height: '0'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
height: 'var(--radix-accordion-content-height)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'accordion-up': {
|
||||||
|
from: {
|
||||||
|
height: 'var(--radix-accordion-content-height)'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
height: '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
|
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue