From 79bdca973c02bda4ee5bd4804c5442b3ec0a0737 Mon Sep 17 00:00:00 2001 From: Nyxian <98602240+tockawaffle@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:56:11 -0300 Subject: [PATCH] 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. --- package-lock.json | 537 ++++++++++++++++++ package.json | 3 + src/app/api/auth/get_user/route.ts | 2 +- .../user/actions/realtime/requests/route.ts | 74 +++ src/app/api/user/search/user/route.ts | 47 ++ src/app/api/user/send/request/route.ts | 66 +++ src/app/layout.tsx | 2 + src/app/page.tsx | 285 +++++++--- src/components/main/realtime/request.tsx | 61 ++ src/components/main/sidebar/sidebar.tsx | 34 +- src/components/ui/accordion.tsx | 57 ++ src/components/ui/alert-dialog.tsx | 141 +++++ src/components/ui/dropdown-menu.tsx | 201 +++++++ src/contexts/user.tsx | 159 +++--- src/lib/supabase/server.ts | 1 + src/types/user.d.ts | 2 + ...el_security_policies_for_messaging_app.sql | 37 ++ ...user_access_policy_for_search_function.sql | 59 ++ supabase/user_and_message_indexes.sql | 5 + supabase/user_management_functions.sql | 116 ++++ supabase/user_management_table.sql | 46 ++ supabase/user_registration_policy.sql | 47 ++ supabase/user_search_function.sql | 1 + supabase/users_table.sql | 27 + tailwind.config.ts | 22 + 25 files changed, 1884 insertions(+), 148 deletions(-) create mode 100644 src/app/api/user/actions/realtime/requests/route.ts create mode 100644 src/app/api/user/search/user/route.ts create mode 100644 src/app/api/user/send/request/route.ts create mode 100644 src/components/main/realtime/request.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 supabase/row_level_security_policies_for_messaging_app.sql create mode 100644 supabase/user_access_policy_for_search_function.sql create mode 100644 supabase/user_and_message_indexes.sql create mode 100644 supabase/user_management_functions.sql create mode 100644 supabase/user_management_table.sql create mode 100644 supabase/user_registration_policy.sql create mode 100644 supabase/user_search_function.sql create mode 100644 supabase/users_table.sql diff --git a/package-lock.json b/package-lock.json index 9c6cbaf..069b6b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,10 @@ "name": "sipher", "version": "0.1.0", "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-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@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", "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": { "version": "1.1.0", "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": { "version": "1.1.0", "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": { "version": "1.1.0", "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": { "version": "1.3.2", "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": { "version": "1.2.0", "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": { "version": "1.2.1", "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_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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1681,6 +2181,11 @@ "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": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1808,6 +2313,14 @@ "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": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -1849,6 +2362,14 @@ "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": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -1942,6 +2463,11 @@ "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": { "version": "3.1.3", "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", "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": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", diff --git a/package.json b/package.json index 0d2d9ff..aa794d1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "lint": "next lint" }, "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-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-scroll-area": "^1.2.1", diff --git a/src/app/api/auth/get_user/route.ts b/src/app/api/auth/get_user/route.ts index 19192bf..e4427a6 100644 --- a/src/app/api/auth/get_user/route.ts +++ b/src/app/api/auth/get_user/route.ts @@ -2,7 +2,7 @@ import {createClient} from "@/lib/supabase/server"; import {NextResponse} from "next/server"; // 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 .from('users') .select('*') diff --git a/src/app/api/user/actions/realtime/requests/route.ts b/src/app/api/user/actions/realtime/requests/route.ts new file mode 100644 index 0000000..c22ca0d --- /dev/null +++ b/src/app/api/user/actions/realtime/requests/route.ts @@ -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 | 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', + }, + }) +} \ No newline at end of file diff --git a/src/app/api/user/search/user/route.ts b/src/app/api/user/search/user/route.ts new file mode 100644 index 0000000..cdcec2f --- /dev/null +++ b/src/app/api/user/search/user/route.ts @@ -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}); + } +} \ No newline at end of file diff --git a/src/app/api/user/send/request/route.ts b/src/app/api/user/send/request/route.ts new file mode 100644 index 0000000..21c3ac9 --- /dev/null +++ b/src/app/api/user/send/request/route.ts @@ -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) { + 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} + ); + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fbeac88..af87918 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,6 +9,7 @@ import {SharedStateProvider} from "@/hooks/shared-states"; import ThemeProvider from "@/components/ui/theme-provider"; import {headers} from "next/headers"; import {Toaster} from "@/components/ui/toaster"; +import {RealtimeRequests} from "@/components/main/realtime/request"; const publicSans = Public_Sans({ subsets: ['latin'], @@ -57,6 +58,7 @@ export default async function RootLayout(
+ {children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 01337e2..6293455 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,18 +3,89 @@ import {useTheme} from "next-themes"; import Image from "next/image"; import {Feather, Search} from "lucide-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() { const {theme, systemTheme} = useTheme(); - const [isSearchExpanded, setIsSearchExpanded] = 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(() => { 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 = () => { if (!mounted) return "light"; if (theme === "system") { @@ -26,81 +97,149 @@ export default function SiPher() { const currentTheme = getTheme(); return ( -
- {/* Animated background elements */} -
-
-
- -
- {/* Logo section with subtle hover effect */} -
-
- SiPher -
- - {/* Main text content with improved typography and spacing */} -
-

- 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. -

- -

- 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. -

-
- - {/* Enhanced search component */} -
-
- - - + { + if (!open) setFormError(""); + }}> + + + + Consent Form + + + Are you sure you want to contact {inputValue}? + + + By continuing, {inputValue} will receive a notification to accept + it. If accepted, that user will appear on your sidebar, if rejected, you will never know about it. + + + + + { + setShowConsentForm(false); + setInputDisabled(false); + }} + >Cancel + { + sendRequest(inputValue).then((result) => { + if (!result) setFormError("Could not send notification for whatever reason. Sorry."); + }); + setInputDisabled(false); + }} + >Continue + + + +
+
+
+
+ SiPher
- {/* Decorative feather icon */} - +

+ 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. +

+ +

+ 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. +

+
+ +
+
+ + + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + fetchUser(inputValue).then((res) => { + console.log(res); + }) + } + }} + /> +
+ + + /> +
+ + +
+

+ F.A.Q +

+ + + How does this works? + + + Please, click here + + + + + Why does this exists? + + 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.) + + + + Is this open-source? + + No, not yet (As of 11/12/2024) + + + +
-
+ ); } \ No newline at end of file diff --git a/src/components/main/realtime/request.tsx b/src/components/main/realtime/request.tsx new file mode 100644 index 0000000..37bb85f --- /dev/null +++ b/src/components/main/realtime/request.tsx @@ -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 +} \ No newline at end of file diff --git a/src/components/main/sidebar/sidebar.tsx b/src/components/main/sidebar/sidebar.tsx index e71147d..2d7ff4e 100644 --- a/src/components/main/sidebar/sidebar.tsx +++ b/src/components/main/sidebar/sidebar.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useState} from "react" import {usePathname} from "next/navigation" import Link from "next/link" 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 {Avatar, AvatarFallback} from "@/components/ui/avatar" import {Separator} from "@/components/ui/separator" @@ -16,6 +16,7 @@ import {useRefs, useUIState} from "@/hooks/shared-states"; import {useToast} from "@/hooks/use-toast"; import {useTheme} from "next-themes"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; +import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"; type SidebarProps = { children?: React.ReactNode @@ -38,6 +39,8 @@ function Sidebar( const {isDrawerOpen, setIsDrawerOpen} = useUIState() const {drawerRef} = useRefs(); + const [pendingRequest, setPendingRequest] = useState(0); + const user = useUser().user!; const { @@ -45,6 +48,10 @@ function Sidebar( suuid } = user + useEffect(() => { + setPendingRequest(user.requests?.length || 0); + }, [user]) + useEffect(() => { const getThreads = async () => { try { @@ -126,6 +133,31 @@ function Sidebar(