From 1a76838c898848b9b7529bb18740a300ca64c0e7 Mon Sep 17 00:00:00 2001 From: ahmedHamdiy Date: Sat, 2 Nov 2024 03:22:57 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Feat(profile-picture):=20set/re?= =?UTF-8?q?move=20profile=20picture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(profile-picture): set/remove profile picture --- app/.env.example | 7 - app/jest.setup.ts | 12 - app/package-lock.json | 222 +++++++++++++++++- app/package.json | 1 + app/src/App.tsx | 2 +- app/src/components/AppLayout.tsx | 21 +- app/src/components/Button.tsx | 10 + app/src/components/ImageEditor.tsx | 128 ++++++++++ app/src/components/Modal.tsx | 2 +- .../float-label-input/FloatingLabelInput.tsx | 2 +- .../ProtectedRoute.test.tsx | 0 .../ProtectedRoute.tsx | 0 app/src/constants.ts | 4 +- app/src/data/deviceSize.ts | 10 - app/src/data/icons.tsx | 12 + app/src/data/sideBar.ts | 2 +- .../ProfilePictureSection.tsx | 101 ++++++++ .../profile-settings/ProfileSettings.tsx | 139 ++++++++--- .../services/GetProfileSettings.ts | 2 +- app/src/hooks/useRedux.ts | 5 - app/src/mocks/mockData.ts | 2 +- .../profile-settings/profile-settings.ts | 5 +- app/src/styles/GlobalStyles.tsx | 2 + 23 files changed, 599 insertions(+), 92 deletions(-) delete mode 100644 app/.env.example create mode 100644 app/src/components/ImageEditor.tsx rename app/src/components/{ProtectedRoute => protected-route}/ProtectedRoute.test.tsx (100%) rename app/src/components/{ProtectedRoute => protected-route}/ProtectedRoute.tsx (100%) delete mode 100644 app/src/data/deviceSize.ts create mode 100644 app/src/features/profile-settings/ProfilePictureSection.tsx delete mode 100644 app/src/hooks/useRedux.ts diff --git a/app/.env.example b/app/.env.example deleted file mode 100644 index 1ef1533b..00000000 --- a/app/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -VITE_BACKEND_API= mock -> let it empty | backend -> http://testing.telware.tech:3000/api/v1 -VITE_REACT_APP_SITE_KEY=key -VITE_SITE_SECRET=secret -VITE_PORT=port -VITE_ENV=development | production | test -VITE_GITHUB_CLIENT_ID=github-client-id -VITE_GOOGLE_CLIENT_ID=google-client-id \ No newline at end of file diff --git a/app/jest.setup.ts b/app/jest.setup.ts index b62ca86e..d0de870d 100644 --- a/app/jest.setup.ts +++ b/app/jest.setup.ts @@ -1,13 +1 @@ import "@testing-library/jest-dom"; - -Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}); diff --git a/app/package-lock.json b/app/package-lock.json index f06a05bf..bbef68ef 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -20,6 +20,7 @@ "jest-fixed-jsdom": "^0.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-filerobot-image-editor": "^4.9.0", "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.53.0", "react-international-phone": "^4.3.0", @@ -3860,6 +3861,45 @@ "win32" ] }, + "node_modules/@scaleflex/icons": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/@scaleflex/icons/-/icons-2.10.27.tgz", + "integrity": "sha512-3E/tqXQrsuFIeGwDHE/ANEdDCPCYrt3ETk3/Q83M5ZZaFWdFWJG3bMeVBwNP2Nuul5OMr70LH3ce3krEObz98g==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/react": ">=16.0.0", + "@types/react-dom": ">=16.0.0", + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@scaleflex/ui": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/@scaleflex/ui/-/ui-2.10.27.tgz", + "integrity": "sha512-Id9EJjS4NWGn9V0pZRCk8YpM2PVEK8/a/BtTbgEW5L7wPI/APmZ9vGtCTM3HyTEBrfnvWmDlb0T5CfpozywKyA==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.6.0", + "@scaleflex/icons": "^2.10.27", + "@tippyjs/react": "^4.2.6", + "@types/lodash.merge": "^4.6.9", + "lodash.merge": "^4.6.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/react": ">=16.0.0", + "@types/react-dom": ">=16.0.0", + "react": ">=16.0.0", + "react-dom": ">=16.0.0", + "styled-components": ">=5.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4052,6 +4092,19 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4245,6 +4298,15 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==" }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "22.8.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.0.tgz", @@ -4280,7 +4342,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -4294,6 +4355,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", + "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.11", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", @@ -6719,6 +6790,19 @@ "node": ">=8" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -7940,6 +8024,27 @@ "node": ">=6" } }, + "node_modules/konva": { + "version": "9.3.16", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.16.tgz", + "integrity": "sha512-qa47cefGDDHzkToGRGDsy24f/Njrz7EHP56jQ8mlDcjAPO7vkfTDeoBDIfmF7PZtpfzDdooafQmEUJMDU2F7FQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT", + "peer": true + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -8019,7 +8124,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/loose-envify": { @@ -8909,6 +9013,62 @@ "react": "^18.3.1" } }, + "node_modules/react-filerobot-image-editor": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/react-filerobot-image-editor/-/react-filerobot-image-editor-4.9.0.tgz", + "integrity": "sha512-wSmbleDAJKR/m848+klGs6sS3veaWDEdhkc7KHb916HT0NcD2RLyAapNn/tjLJK/OPPJtuMod176nIljqo1JtA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@scaleflex/icons": "2.10.27", + "@scaleflex/ui": "2.10.27", + "konva": "9.3.6", + "prop-types": "15.7.2" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0", + "react-konva": ">=17.0.0", + "styled-components": ">=5.3.5" + } + }, + "node_modules/react-filerobot-image-editor/node_modules/konva": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.6.tgz", + "integrity": "sha512-dqR8EbcM0hjuilZCBP6xauQ5V3kH3m9kBcsDkqPypQuRgsXbcXUrxqYxhNbdvKZpYNW8Amq94jAD/C0NY3qfBQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT" + }, + "node_modules/react-filerobot-image-editor/node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/react-filerobot-image-editor/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-google-recaptcha": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", @@ -8950,6 +9110,55 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-konva": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", + "integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "^0.28.2", + "its-fine": "^1.1.1", + "react-reconciler": "~0.29.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-redux": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", @@ -9692,6 +9901,15 @@ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", "license": "MIT" }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/app/package.json b/app/package.json index a51bddd5..0900a812 100644 --- a/app/package.json +++ b/app/package.json @@ -26,6 +26,7 @@ "jest-fixed-jsdom": "^0.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-filerobot-image-editor": "^4.9.0", "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.53.0", "react-international-phone": "^4.3.0", diff --git a/app/src/App.tsx b/app/src/App.tsx index c6585bb2..66c11f58 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -11,7 +11,7 @@ import { useAppSelector } from "./hooks/useGlobalState"; import Login from "./pages/Login"; import Signup from "./pages/Signup"; import ResetPasswordModal from "@features/authentication/reset-password/ResetPasswordModal"; -import ProtectedRoute from "@components/ProtectedRoute/ProtectedRoute"; +import ProtectedRoute from "@components/protected-route/ProtectedRoute"; import AppLayout from "@components/AppLayout"; const queryClient = new QueryClient({ diff --git a/app/src/components/AppLayout.tsx b/app/src/components/AppLayout.tsx index c5a5e1a1..2bd75f85 100644 --- a/app/src/components/AppLayout.tsx +++ b/app/src/components/AppLayout.tsx @@ -1,21 +1,20 @@ import styled from "styled-components"; import Main from "./Main"; import SideBar from "./side-bar/SideBar"; -import { media } from "data/deviceSize"; +import { MOBILE_VIEW } from "@constants"; const StyledApp = styled.div` - @media ${media.mobile} { - & > main { - display: none; - } - } - - @media ${media.desktop} { - display: grid; - grid-template-columns: 1fr 3fr; + display: grid; + grid-template-columns: 33% auto; + height: 100dvh; + & > main { + display: block; + } + @media ${MOBILE_VIEW} { + display: block; & > main { - display: block; + display: none; } } `; diff --git a/app/src/components/Button.tsx b/app/src/components/Button.tsx index b15ffa19..6f9adeef 100644 --- a/app/src/components/Button.tsx +++ b/app/src/components/Button.tsx @@ -30,6 +30,16 @@ const Button = styled.button<{ $type?: string }>` font-weight: normal; letter-spacing: normal; `} + ${(props) => + props.$type === "danger" && + css` + background-color: var(--color-error); + color: var(--color-text-button); + font-weight: normal; + &:hover { + background-color: var(--color-error-shade); + } + `} `; export default Button; diff --git a/app/src/components/ImageEditor.tsx b/app/src/components/ImageEditor.tsx new file mode 100644 index 00000000..0422f2ea --- /dev/null +++ b/app/src/components/ImageEditor.tsx @@ -0,0 +1,128 @@ +import FilerobotImageEditor, { + TABS, + TOOLS, +} from "react-filerobot-image-editor"; +import styled from "styled-components"; + +const StyledContainer = styled.div<{ $isOpen: boolean }>` + display: ${({ $isOpen }) => ($isOpen ? "flex" : "none")}; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + justify-content: center; + align-items: center; +`; + +function ImageEditor({ + isOpen, + closeImgEditor, + src, + onImageSave, + isProfileImage, +}: { + isOpen: boolean; + closeImgEditor: () => void; + src: string; + onImageSave: (file: File) => void; + isProfileImage?: boolean; +}) { + const handleSave = async (editedImageObject: { imageBase64?: string }) => { + if (editedImageObject.imageBase64) { + const response = await fetch(editedImageObject.imageBase64); + const blob = await response.blob(); + + const file = new File([blob], "edited-image.jpg", { + type: "image/jpeg", + lastModified: Date.now(), + }); + + onImageSave(file); + closeImgEditor(); + } else { + console.error("Image base64 data is undefined"); + } + }; + return ( + + {isOpen && ( +
+ handleSave(editedImageObject)} + onClose={closeImgEditor} + annotationsCommon={{ + fill: "#ff0000", + }} + Text={{ text: "I Love TelWare" }} + Rotate={{ angle: 90, componentType: "slider" }} + Crop={ + isProfileImage + ? { + minWidth: 180, + minHeight: 180, + } + : { + presetsItems: [ + { + titleKey: "classicTv", + descriptionKey: "4:3", + ratio: 4 / 3, + // icon: CropClassicTv, // optional, CropClassicTv is a React Function component. Possible (React Function component, string or HTML Element) + }, + { + titleKey: "cinemascope", + descriptionKey: "21:9", + ratio: 21 / 9, + // icon: CropCinemaScope, // optional, CropCinemaScope is a React Function component. Possible (React Function component, string or HTML Element) + }, + ], + presetsFolders: [ + { + titleKey: "socialMedia", // will be translated into Social Media as backend contains this translation key + // icon: Social, // optional, Social is a React Function component. Possible (React Function component, string or HTML Element) + groups: [ + { + titleKey: "facebook", + items: [ + { + titleKey: "profile", + width: 180, + height: 180, + descriptionKey: "fbProfileSize", + }, + { + titleKey: "coverPhoto", + width: 820, + height: 312, + descriptionKey: "fbCoverPhotoSize", + }, + ], + }, + ], + }, + ], + } + } + theme={{ + palette: { + warning: "#ff0000c0", + "warning-active": "#ff0000", + "warning-hover": "#ff0000", + }, + }} + tabsIds={[TABS.ADJUST, TABS.ANNOTATE]} // or {['Adjust', 'Annotate', 'Watermark']} + defaultTabId={isProfileImage ? TABS.ADJUST : TABS.ANNOTATE} // or 'Annotate' + defaultToolId={TOOLS.TEXT} // or 'Text' + savingPixelRatio={4} + previewPixelRatio={window.devicePixelRatio} + /> +
+ )} +
+ ); +} + +export default ImageEditor; diff --git a/app/src/components/Modal.tsx b/app/src/components/Modal.tsx index d339a5af..786989cf 100644 --- a/app/src/components/Modal.tsx +++ b/app/src/components/Modal.tsx @@ -53,7 +53,7 @@ function Modal({ onClose, isOpen, title, message, children }: ModalProps) { return ( - + × {title} diff --git a/app/src/components/inputs/float-label-input/FloatingLabelInput.tsx b/app/src/components/inputs/float-label-input/FloatingLabelInput.tsx index d0d641aa..19c3d3d0 100644 --- a/app/src/components/inputs/float-label-input/FloatingLabelInput.tsx +++ b/app/src/components/inputs/float-label-input/FloatingLabelInput.tsx @@ -7,7 +7,7 @@ import { UseFormWatch, } from "react-hook-form"; -interface FloatingLabelInputProps { +export interface FloatingLabelInputProps { label: string; id: Path; watch?: UseFormWatch; diff --git a/app/src/components/ProtectedRoute/ProtectedRoute.test.tsx b/app/src/components/protected-route/ProtectedRoute.test.tsx similarity index 100% rename from app/src/components/ProtectedRoute/ProtectedRoute.test.tsx rename to app/src/components/protected-route/ProtectedRoute.test.tsx diff --git a/app/src/components/ProtectedRoute/ProtectedRoute.tsx b/app/src/components/protected-route/ProtectedRoute.tsx similarity index 100% rename from app/src/components/ProtectedRoute/ProtectedRoute.tsx rename to app/src/components/protected-route/ProtectedRoute.tsx diff --git a/app/src/constants.ts b/app/src/constants.ts index 7a151888..32e20781 100644 --- a/app/src/constants.ts +++ b/app/src/constants.ts @@ -6,4 +6,6 @@ const { VITE_ENV: ENV, } = import.meta.env; -export { ENVIRONMENT, RECAPTCHA_SITE_KEY, API_URL, PORT, ENV }; +const MOBILE_VIEW = "(max-width: 768px)"; + +export { ENVIRONMENT, RECAPTCHA_SITE_KEY, API_URL, PORT, ENV, MOBILE_VIEW }; diff --git a/app/src/data/deviceSize.ts b/app/src/data/deviceSize.ts deleted file mode 100644 index 9240a7e5..00000000 --- a/app/src/data/deviceSize.ts +++ /dev/null @@ -1,10 +0,0 @@ -const sizes = { - mobile: "480px", - desktop: "1024px", -}; - -const media = { - mobile: `(max-width: ${sizes.mobile})`, - desktop: `(min-width: ${sizes.desktop})`, -}; -export { sizes, media }; diff --git a/app/src/data/icons.tsx b/app/src/data/icons.tsx index b6f7a7dc..f9536c25 100644 --- a/app/src/data/icons.tsx +++ b/app/src/data/icons.tsx @@ -17,6 +17,8 @@ import AddIcon from "@mui/icons-material/Add"; import CloseOutlinedIcon from "@mui/icons-material/CloseOutlined"; import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import AddAPhotoOutlined from "@mui/icons-material/AddAPhotoOutlined"; +import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined"; enum icons { BlockIcon, SettingsOutlinedIcon, @@ -38,6 +40,8 @@ enum icons { Close, Show, Hide, + Delete, + AddPhoto, } type iconStrings = keyof typeof icons; @@ -144,6 +148,14 @@ const iconMap: { [K in iconStrings]: React.ReactNode } = { sx={{ color: `var(--color-icon-secondary)` }} /> ), + AddPhoto: , + Delete: ( + + ), }; function getIcon(iconName?: iconStrings) { diff --git a/app/src/data/sideBar.ts b/app/src/data/sideBar.ts index fb76a684..3c2c160a 100644 --- a/app/src/data/sideBar.ts +++ b/app/src/data/sideBar.ts @@ -153,7 +153,7 @@ const settingsUpdate: SideBarView = { page: "SETTINGS_UPDATE", }; const profileUpdate: SideBarView = { - title: "ProfileUpdate", + title: "Edit Profile", backView: sideBarPages.SETTINGS, page: "PROFILE_UPDATE", }; diff --git a/app/src/features/profile-settings/ProfilePictureSection.tsx b/app/src/features/profile-settings/ProfilePictureSection.tsx new file mode 100644 index 00000000..234f59cd --- /dev/null +++ b/app/src/features/profile-settings/ProfilePictureSection.tsx @@ -0,0 +1,101 @@ +import styled from "styled-components"; +import { getIcon } from "@data/icons"; +import ImageEditor from "@components/ImageEditor"; +import { useState } from "react"; + +interface ProfilePictureSectionProps { + initials?: string; + handleImageUpload?: (e: React.ChangeEvent) => void; +} + +const UploadProfilePicture = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + padding: 1rem 1.5rem; + background-color: var(--color-background); + position: relative; + cursor: pointer; + + &:hover svg { + scale: 1.2; + } +`; + +const UploadProfilePictureIcon = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + cursor: pointer; + z-index: 5; + + svg { + transition: all 0.2s ease-out; + transform-origin: center; + font-size: 3rem; + } +`; + +const StyledImageInput = styled.input` + position: absolute; + top: 1rem; + left: calc(50% - 4rem); + width: 8rem; + height: 8rem; + opacity: 0; + z-index: 10; + cursor: pointer; +`; +const StyledProfileImage = styled.img` + width: 8rem; + height: 8rem; + border-radius: 50%; + object-fit: cover; +`; +const DefaultProfilePicture = styled.div` + width: 8rem; + height: 8rem; + border-radius: 50%; + background-color: var(--accent-color); + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + color: white; + font-weight: bold; + opacity: 0.8; +`; +function ProfilePictureSection({ + initials, + handleImageUpload, +}: ProfilePictureSectionProps) { + const [selectedImage, setSelectedImage] = useState(null); + const [isImageEditorOpen, setIsImageEditorOpen] = useState(false); + return ( + + + {selectedImage ? ( + <> + + {getIcon("AddPhoto")} + + + + ) : ( + {initials} + )} + + + ); +} + +export default ProfilePictureSection; diff --git a/app/src/features/profile-settings/ProfileSettings.tsx b/app/src/features/profile-settings/ProfileSettings.tsx index 10148d57..5f0d6bac 100644 --- a/app/src/features/profile-settings/ProfileSettings.tsx +++ b/app/src/features/profile-settings/ProfileSettings.tsx @@ -1,7 +1,7 @@ import styled from "styled-components"; import { AddAPhotoOutlined, Check } from "@mui/icons-material"; -import { useForm } from "react-hook-form"; +import { set, useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import FloatingLabelInput from "@components/inputs/float-label-input/FloatingLabelInput"; import { DevTool } from "@hookform/devtools"; @@ -11,6 +11,13 @@ import { useProfileSettings } from "./hooks/useProfileSettings"; import { useUpdateProfileSettings } from "./hooks/useUpdateProfileSettings"; import { useEffect, useState } from "react"; import SettingsSideBarHeader from "@components/side-bar/settings/SettingsSideBarHeader"; +import ImageEditor from "@components/ImageEditor"; +import { useAppDispatch } from "@hooks/useGlobalState"; +import { updateSideBarView } from "@state/side-bar/sideBar"; +import { sideBarPages } from "types/sideBar"; +import { getIcon } from "@data/icons"; +import Modal from "@components/Modal"; +import Button from "@components/Button"; const SideBarContainer = styled.div` overflow-y: auto; @@ -51,10 +58,6 @@ const UploadProfilePicture = styled.div` background-color: var(--color-background); position: relative; cursor: pointer; - - &:hover svg { - scale: 1.2; - } `; const UploadProfilePictureIcon = styled.div` @@ -64,24 +67,57 @@ const UploadProfilePictureIcon = styled.div` transform: translate(-50%, -50%); color: white; cursor: pointer; - + z-index: 5; + &:hover svg { + scale: 1.2; + } svg { transition: all 0.2s ease-out; transform-origin: center; font-size: 3rem; } `; +const DeleteProfilePicture = styled.div``; +const DeleteProfilePictureIcon = styled.div` + position: absolute; + bottom: 1rem; + right: calc(50% - 4rem); + z-index: 5000; + background-color: red; + border: 0.1rem solid var(--color-border); + border-radius: 50%; + padding: 0.5rem; + cursor: pointer; + width: 2rem; + height: 2rem; + display: flex; + justify-content: center; + align-items: center; + &:hover svg { + scale: 1.2; + } + svg { + transition: all 0.2s ease-out; + transform-origin: center; + } +`; const StyledImageInput = styled.input` position: absolute; top: 1rem; - left: 5rem; - width: 9rem; + left: calc(50% - 4rem); + width: 8rem; height: 8rem; opacity: 0; z-index: 10; cursor: pointer; `; +const StyledProfileImage = styled.img` + width: 8rem; + height: 8rem; + border-radius: 50%; + object-fit: cover; +`; const DefaultProfilePicture = styled.div` width: 8rem; height: 8rem; @@ -129,8 +165,8 @@ const SubmitButton = styled.button<{ $revealed?: boolean }>` `} `; -interface EditProfileForm { - profilePicture: string; +export interface EditProfileForm { + photo: string; firstName: string; lastName: string; bio: string; @@ -141,8 +177,14 @@ interface EditProfileForm { function ProfileSettings() { const { data: initialProfileSettings } = useProfileSettings(); - const { updateProfileSettings } = useUpdateProfileSettings(); - const [selectedImage, setSelectedImage] = useState(null); + const { updateProfileSettings, isPending } = useUpdateProfileSettings(); + const [selectedImage, setSelectedImage] = useState( + initialProfileSettings?.photo || null + ); + const [photoChanged, setPhotoChanged] = useState(false); + const [isImgEditorOpen, setIsImgEditorOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const dispatch = useAppDispatch(); const { register, @@ -155,7 +197,7 @@ function ProfileSettings() { resolver: yupResolver(ValidationSchema), mode: "onChange", defaultValues: initialProfileSettings || { - profilePicture: "", + photo: "", firstName: "", lastName: "", bio: "", @@ -173,18 +215,30 @@ function ProfileSettings() { const handleImageUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; + setIsImgEditorOpen(true); if (file) { - const imageURL = URL.createObjectURL(file); - setSelectedImage(imageURL); + const imgStr = URL.createObjectURL(file); + setSelectedImage(imgStr); } }; + const handleImageSave = (file: File) => { + setPhotoChanged(true); + setSelectedImage(URL.createObjectURL(file)); + }; + const handleDeleteImage = () => { + setSelectedImage(null); + setIsDeleting(false); + }; const onSubmit = async (data: EditProfileForm) => { try { if (selectedImage) { - data.profilePicture = selectedImage; + data.photo = selectedImage; } updateProfileSettings(data); + if (!isPending) { + dispatch(updateSideBarView({ redirect: sideBarPages.SETTINGS })); + } } catch (error) { console.error(error); } @@ -201,36 +255,52 @@ function ProfileSettings() { - {/* Image Upload */} + + setIsDeleting(true)}> + {getIcon("Delete")} + + {isDeleting && ( + setIsDeleting(false)} + > + + + )} + {selectedImage ? ( <> - Profile Picture + + {getIcon("AddPhoto")} + + ) : ( {initials} )} - {initials?.length == 0 && ( - - - - )} - + setIsImgEditorOpen(false)} + src={selectedImage ? selectedImage : ""} + onImageSave={handleImageSave} + isProfileImage={true} + /> id="firstName" label="First Name (required)" @@ -310,7 +380,7 @@ function ProfileSettings() { @@ -321,4 +391,3 @@ function ProfileSettings() { } export default ProfileSettings; -export type { EditProfileForm }; diff --git a/app/src/features/profile-settings/services/GetProfileSettings.ts b/app/src/features/profile-settings/services/GetProfileSettings.ts index c4085023..a1ce77cd 100644 --- a/app/src/features/profile-settings/services/GetProfileSettings.ts +++ b/app/src/features/profile-settings/services/GetProfileSettings.ts @@ -15,7 +15,7 @@ async function GetProfileSettings() { } const profileSettings = { - profilePicture: data.data.photo, + photo: data.data.photo, firstName: data.data.firstName, lastName: data.data.lastName, bio: data.data.bio, diff --git a/app/src/hooks/useRedux.ts b/app/src/hooks/useRedux.ts deleted file mode 100644 index b6a474bf..00000000 --- a/app/src/hooks/useRedux.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useDispatch, useSelector } from "react-redux"; -import { AppDispatch, RootState } from "../state/store"; - -export const useAppDispatch = useDispatch.withTypes(); -export const useAppSelector = useSelector.withTypes(); diff --git a/app/src/mocks/mockData.ts b/app/src/mocks/mockData.ts index 2fc13261..a33a9b97 100644 --- a/app/src/mocks/mockData.ts +++ b/app/src/mocks/mockData.ts @@ -5,7 +5,7 @@ export const MOCK_USER = { lastName: "Doe", bio: "Hello, I'm John Doe", photo: - "https://media-hbe1-1.cdn.whatsapp.net/v/t61.24694-24/462460819_518473281043631_6485009024565374350_n.jpg?ccb=11-4&oh=01_Q5AaINdhN3wt4c6ZnmGni8RNhM8fIvquSRicC2QT82X6ddeB&oe=6727186F&_nc_sid=5e03e0&_nc_cat=100", + "https://i.pinimg.com/564x/26/76/a1/2676a1898da6edae9fc648c94332903f.jpg", username: "johndoe", phoneNumber: "0123456789", status: "Online", diff --git a/app/src/mocks/profile-settings/profile-settings.ts b/app/src/mocks/profile-settings/profile-settings.ts index 0d435cb6..f81d39be 100644 --- a/app/src/mocks/profile-settings/profile-settings.ts +++ b/app/src/mocks/profile-settings/profile-settings.ts @@ -12,19 +12,18 @@ export const profileSettingsMock = [ status: "success", data: MOCK_USER, }, - { status: 200 }, + { status: 200 } ); }), http.patch("/users/me", async ({ request }) => { const newProfileSettings = await request.json(); - return HttpResponse.json( { status: "success", data: newProfileSettings, }, - { status: 200 }, + { status: 200 } ); }), ]; diff --git a/app/src/styles/GlobalStyles.tsx b/app/src/styles/GlobalStyles.tsx index fb428979..7f6e2b1d 100644 --- a/app/src/styles/GlobalStyles.tsx +++ b/app/src/styles/GlobalStyles.tsx @@ -156,7 +156,9 @@ const GlobalStyles = createGlobalStyle` --color-borders-input: rgb(218, 220, 224); + --color-error: #e53935; + --color-error-shade: #c62828; --color-success: rgb(0, 199, 62); font-size: 16px; From 8685c6f1bdf770fa77652573de0799582e75a3a4 Mon Sep 17 00:00:00 2001 From: ahmedHamdiy Date: Wed, 18 Dec 2024 03:06:04 +0200 Subject: [PATCH 2/4] admin mock --- app/package-lock.json | 19 --- app/public/{ => assets}/TelWare.png | Bin app/src/App.tsx | 5 + app/src/components/Logout.tsx | 20 +++ app/src/components/Main.tsx | 2 +- .../protected-route/ProtectedRoute.tsx | 24 ++++ .../side-bar/chats/SideBarMenuItem.tsx | 2 +- .../chats/theme-toggle/ThemeToggle.tsx | 58 +++++--- .../settings/SettingsSideBarHeader.tsx | 13 +- app/src/data/icons.tsx | 20 +++ .../admin/components/AdminAppLayout.tsx | 64 +++++++++ .../features/admin/components/AdminHeader.tsx | 42 ++++++ .../admin/components/AdminHeaderMenu.tsx | 33 +++++ .../admin/components/AdminMainNav.tsx | 94 +++++++++++++ .../admin/components/AdminNavMenu.tsx | 69 ++++++++++ .../admin/components/AdminSidebar.tsx | 30 ++++ app/src/features/admin/components/Filter.tsx | 0 .../admin/components/FilteredList.tsx | 0 .../features/admin/components/GroupCard.tsx | 0 app/src/features/admin/components/Logo.tsx | 20 +++ .../features/admin/components/UserCard.tsx | 57 ++++++++ .../features/admin/hooks/useActivateUser.ts | 28 ++++ app/src/features/admin/hooks/useBanUser.ts | 28 ++++ .../features/admin/hooks/useDeactivateUser.ts | 28 ++++ .../features/admin/hooks/useFilterGroup.ts | 28 ++++ app/src/features/admin/hooks/useGroups.ts | 16 +++ .../features/admin/hooks/useUnfilterGroup.ts | 28 ++++ app/src/features/admin/hooks/useUsers.ts | 16 +++ app/src/features/admin/services/apiGroups.ts | 47 +++++++ app/src/features/admin/services/apiUsers.ts | 61 ++++++++ app/src/features/chats/StartNewChat.tsx | 1 - .../privacy-settings/hooks/useBlock.ts | 12 +- .../hooks/useUpdatePrivacy.ts | 4 +- .../privacy-settings/service/apiBlockUser.ts | 4 +- .../service/apiChangeSettings.ts | 4 +- .../service/apiGetBlockList.ts | 4 +- .../service/apiRemoveFromBlocks.ts | 4 +- .../features/stories/hooks/useMyStories.ts | 2 +- app/src/mocks/admin/admin.ts | 123 +++++++++++++++++ app/src/mocks/data/admin.ts | 127 +++++++++++++++++ app/src/mocks/data/chats.ts | 130 +++++++++--------- app/src/mocks/handlers.ts | 2 + app/src/state/admin/adminView.ts | 25 ++++ app/src/state/store.ts | 2 + app/src/styles/GlobalStyles.tsx | 28 +++- app/src/types/admin.ts | 22 +++ ....timestamp-1734385645418-efb6d73151a22.mjs | 18 +++ 47 files changed, 1231 insertions(+), 133 deletions(-) rename app/public/{ => assets}/TelWare.png (100%) create mode 100644 app/src/components/Logout.tsx create mode 100644 app/src/features/admin/components/AdminAppLayout.tsx create mode 100644 app/src/features/admin/components/AdminHeader.tsx create mode 100644 app/src/features/admin/components/AdminHeaderMenu.tsx create mode 100644 app/src/features/admin/components/AdminMainNav.tsx create mode 100644 app/src/features/admin/components/AdminNavMenu.tsx create mode 100644 app/src/features/admin/components/AdminSidebar.tsx create mode 100644 app/src/features/admin/components/Filter.tsx create mode 100644 app/src/features/admin/components/FilteredList.tsx create mode 100644 app/src/features/admin/components/GroupCard.tsx create mode 100644 app/src/features/admin/components/Logo.tsx create mode 100644 app/src/features/admin/components/UserCard.tsx create mode 100644 app/src/features/admin/hooks/useActivateUser.ts create mode 100644 app/src/features/admin/hooks/useBanUser.ts create mode 100644 app/src/features/admin/hooks/useDeactivateUser.ts create mode 100644 app/src/features/admin/hooks/useFilterGroup.ts create mode 100644 app/src/features/admin/hooks/useGroups.ts create mode 100644 app/src/features/admin/hooks/useUnfilterGroup.ts create mode 100644 app/src/features/admin/hooks/useUsers.ts create mode 100644 app/src/features/admin/services/apiGroups.ts create mode 100644 app/src/features/admin/services/apiUsers.ts create mode 100644 app/src/mocks/admin/admin.ts create mode 100644 app/src/mocks/data/admin.ts create mode 100644 app/src/state/admin/adminView.ts create mode 100644 app/src/types/admin.ts create mode 100644 app/vite.config.ts.timestamp-1734385645418-efb6d73151a22.mjs diff --git a/app/package-lock.json b/app/package-lock.json index 32c5cceb..54b90903 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -4803,15 +4803,6 @@ "@types/lodash": "*" } }, - "node_modules/@types/lodash.merge": { - "version": "4.6.9", - "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", - "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/node": { "version": "22.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", @@ -4882,16 +4873,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-reconciler": { - "version": "0.28.8", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", - "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.11", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", diff --git a/app/public/TelWare.png b/app/public/assets/TelWare.png similarity index 100% rename from app/public/TelWare.png rename to app/public/assets/TelWare.png diff --git a/app/src/App.tsx b/app/src/App.tsx index e0720047..9f3d3b6c 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -17,6 +17,7 @@ import AppLayout from "@components/AppLayout"; import ChatBox from "@features/chats/ChatBox"; import SocketProvider from "sockets/SocketProvider"; +import AdminAppLayout from "@features/admin/components/AdminAppLayout"; const queryClient = new QueryClient({ defaultOptions: { @@ -54,6 +55,10 @@ function App() { > } /> + } /> + } /> + } /> + } /> } /> logout()} + $icon="Logout" + $padding={0.2} + $size={size} + $color="var(--color-text)" + $bgColor="var(--color-pattern)" + /> + ); +} + +export default Logout; diff --git a/app/src/components/Main.tsx b/app/src/components/Main.tsx index eef739de..d49bd406 100644 --- a/app/src/components/Main.tsx +++ b/app/src/components/Main.tsx @@ -47,7 +47,7 @@ function Main({ children }: { children?: React.ReactNode }) { if (user && !isPending) { dispatch(setUserInfo(user)); } - }, [dispatch, user]); + }, [dispatch, user, isPending]); return {children}; } diff --git a/app/src/components/protected-route/ProtectedRoute.tsx b/app/src/components/protected-route/ProtectedRoute.tsx index e69de29b..ef59fb17 100644 --- a/app/src/components/protected-route/ProtectedRoute.tsx +++ b/app/src/components/protected-route/ProtectedRoute.tsx @@ -0,0 +1,24 @@ +import { ReactNode, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useAuthStatus } from "@features/authentication/login/hooks/useAuthStatus"; + +type prortectedRouteType = { + children: ReactNode; +}; + +function ProtectedRoute({ children }: prortectedRouteType) { + const navigate = useNavigate(); + const { isAuth, isPending } = useAuthStatus(); + + useEffect(() => { + if (!isAuth && !isPending) { + navigate("/login"); + } + }, [navigate, isAuth, isPending]); + + if (isAuth) return children; + else return null; +} + +export default ProtectedRoute; diff --git a/app/src/components/side-bar/chats/SideBarMenuItem.tsx b/app/src/components/side-bar/chats/SideBarMenuItem.tsx index 4e68a0da..4b09667e 100644 --- a/app/src/components/side-bar/chats/SideBarMenuItem.tsx +++ b/app/src/components/side-bar/chats/SideBarMenuItem.tsx @@ -34,7 +34,7 @@ function SideBarMenuItem({ iconMapValue, }: SideBarMenuItemProps) { return ( - + {getIcon(iconMapValue)} {title} {children} diff --git a/app/src/components/side-bar/chats/theme-toggle/ThemeToggle.tsx b/app/src/components/side-bar/chats/theme-toggle/ThemeToggle.tsx index ab0b64b9..027d8e2e 100644 --- a/app/src/components/side-bar/chats/theme-toggle/ThemeToggle.tsx +++ b/app/src/components/side-bar/chats/theme-toggle/ThemeToggle.tsx @@ -1,6 +1,7 @@ import styled, { css } from "styled-components"; import { Theme, toggleTheme } from "@state/theme/theme"; import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState"; +import { getIcon } from "@data/icons"; const Switch = styled.label` position: relative; @@ -26,7 +27,6 @@ const Slider = styled.span<{ $theme?: Theme }>` border-radius: 10px; padding: 4px; background-color: var(--accent-color); - background-color: var(--accent-color); ${({ $theme }) => $theme === Theme.LIGHT && @@ -41,21 +41,15 @@ const Slider = styled.span<{ $theme?: Theme }>` left: -7px; bottom: -3px; transition: 0.2s; - transition: 0.2s; border-radius: 50%; background-color: var(--color-background); border: 2px solid; border-color: var(--accent-color); - border: 2px solid; - border-color: var(--accent-color); - ${({ $theme }) => - $theme === Theme.LIGHT && $theme === Theme.LIGHT && css` border-color: var(--pattern-color); - border-color: var(--pattern-color); `}; } @@ -63,22 +57,52 @@ const Slider = styled.span<{ $theme?: Theme }>` transform: translateX(25px); } `; -function ThemeToggle() { +const Container = styled.li<{ $theme?: Theme }>` + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem; + height: 2.4rem; + width: 2.4rem; + justify-content: center; + background-color: var(--accent-color); + + border-radius: 50%; + cursor: pointer; + ${({ $theme }) => + $theme === Theme.LIGHT && + css` + background-color: var(--pattern-color); + `}; +`; +function ThemeToggle({ isAdmin = false }: { isAdmin?: boolean }) { const currentTheme = useAppSelector((state) => state.theme.value); const dispatch = useAppDispatch(); const handleChange = () => { dispatch(toggleTheme()); }; return ( - - - - + <> + {isAdmin ? ( + + {currentTheme === Theme.LIGHT ? getIcon("NightMode") : getIcon("Sun")} + + ) : ( + + + + + )} + ); } diff --git a/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx b/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx index 73ae870e..3cdb7dd8 100644 --- a/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx +++ b/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx @@ -3,9 +3,9 @@ import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState"; import Heading from "@components/Heading"; import BackArrow from "@components/BackArrow"; import CircleIcon from "@components/CircleIcon"; -import { useLogout } from "@features/authentication/logout/hooks/useLogout"; import { updateSideBarView } from "@state/side-bar/sideBar"; import { sideBarPages } from "types/sideBar"; +import Logout from "@components/Logout"; const StyledSideBarHeader = styled.div` height: 4rem !important; @@ -33,7 +33,6 @@ const IconsContainer = styled.div` function SettingsSideBarHeader() { const { title } = useAppSelector((state) => state.sideBarData); - const { logout } = useLogout(); const dispatch = useAppDispatch(); return ( @@ -57,15 +56,7 @@ function SettingsSideBarHeader() { ) } /> - logout()} - $icon="Logout" - $padding={0.2} - $size={1.8} - $color="var(--color-text)" - $bgColor="var(--color-pattern)" - /> + )} diff --git a/app/src/data/icons.tsx b/app/src/data/icons.tsx index 42b0b8b2..17a1e417 100644 --- a/app/src/data/icons.tsx +++ b/app/src/data/icons.tsx @@ -57,6 +57,9 @@ enum icons { Info, Phone, ArrowForward, + Home, + Group, + Sun, } type iconStrings = keyof typeof icons; @@ -73,6 +76,14 @@ type IconConfig = { }; const iconImports: Record = { + Group: { + importFn: () => import("@mui/icons-material/GroupsOutlined"), + defaultProps: { sx: { color: `var(--color-icon-secondary)` } }, + }, + Home: { + importFn: () => import("@mui/icons-material/HomeOutlined"), + defaultProps: { sx: { color: `var(--color-icon-secondary)` } }, + }, BlockIcon: { importFn: () => import("@mui/icons-material/Block"), defaultProps: { sx: { color: `var(--color-icon-secondary)` } }, @@ -390,6 +401,15 @@ const iconImports: Record = { importFn: () => import("@mui/icons-material/ArrowForward"), defaultProps: { fontSize: "large" }, }, + Sun: { + importFn: () => import("@mui/icons-material/LightModeOutlined"), + defaultProps: { + sx: { + color: `white`, + fontSize: "1.5rem", + }, + }, + }, }; function getIcon( diff --git a/app/src/features/admin/components/AdminAppLayout.tsx b/app/src/features/admin/components/AdminAppLayout.tsx new file mode 100644 index 00000000..f5016aef --- /dev/null +++ b/app/src/features/admin/components/AdminAppLayout.tsx @@ -0,0 +1,64 @@ +import { Outlet } from "react-router-dom"; +import AdminSidebar from "./AdminSidebar"; +import AdminHeader from "./AdminHeader"; +import styled from "styled-components"; +import { DESKTOP_VIEW, MOBILE_VIEW } from "@constants"; + +const StyledAppLayout = styled.div` + display: grid; + height: 100vh; + grid-template-rows: auto 1fr; + overflow-x: auto; + @media ${MOBILE_VIEW} { + grid-template-columns: 1fr; + & > main { + padding: 0; + } + & > aside { + display: none; + } + } + + @media ${DESKTOP_VIEW} { + display: grid; + grid-template-columns: minmax(20rem, 1.3fr) 5fr; + + & > main { + display: block; + } + + & > aside { + display: block; + } + } +`; + +const Main = styled.main` + overflow-y: auto; + padding: 4rem 2.4rem; + background-color: var(--admin-main-bg); +`; + +const Container = styled.div` + gap: 3.2rem; + display: flex; + margin: 0 auto; + max-width: 120rem; + flex-direction: column; +`; + +function AdminAppLayout() { + return ( + + + +
+ + + +
+
+ ); +} + +export default AdminAppLayout; diff --git a/app/src/features/admin/components/AdminHeader.tsx b/app/src/features/admin/components/AdminHeader.tsx new file mode 100644 index 00000000..c7f9f0f0 --- /dev/null +++ b/app/src/features/admin/components/AdminHeader.tsx @@ -0,0 +1,42 @@ +import styled from "styled-components"; +import AdminHeaderMenu from "./AdminHeaderMenu"; +import AdminNavMenu from "./AdminNavMenu"; +import { DESKTOP_VIEW, MOBILE_VIEW } from "@constants"; +import Heading from "@components/Heading"; + +const Header = styled.header` + display: flex; + align-items: center; + gap: minmax(2.4rem, 20px); + padding: 1.2rem 2.4rem; + justify-content: flex-start; + background-color: var(--admin-header-bg); + border-bottom: 1px solid var(--scrollbar-color); + @media ${MOBILE_VIEW} { + & > :first-child { + display: block; + } + } + + @media ${DESKTOP_VIEW} { + & > :first-child { + display: none; + } + } +`; +const H4 = styled(Heading)` + color: var(--color-text); + flex: 1; +`; + +function AdminHeader() { + return ( +
+ +

Hi, Amir Anwar

+ +
+ ); +} + +export default AdminHeader; diff --git a/app/src/features/admin/components/AdminHeaderMenu.tsx b/app/src/features/admin/components/AdminHeaderMenu.tsx new file mode 100644 index 00000000..c834ccd6 --- /dev/null +++ b/app/src/features/admin/components/AdminHeaderMenu.tsx @@ -0,0 +1,33 @@ +import styled from "styled-components"; +import Logout from "@components/Logout"; +import ThemeToggle from "@components/side-bar/chats/theme-toggle/ThemeToggle"; + +const StyledHeaderMenu = styled.ul` + margin: 0; + padding: 0; + gap: 2rem; + display: flex; + list-style: none; + align-items: center; + justify-content: flex-end; + position: relative; + width: 8rem; +`; +const ListItem = styled.li` + display: flex; + align-items: center; + gap: 0.4rem; +`; + +function AdminHeaderMenu() { + return ( + + + + + + + ); +} + +export default AdminHeaderMenu; diff --git a/app/src/features/admin/components/AdminMainNav.tsx b/app/src/features/admin/components/AdminMainNav.tsx new file mode 100644 index 00000000..a3ad973f --- /dev/null +++ b/app/src/features/admin/components/AdminMainNav.tsx @@ -0,0 +1,94 @@ +import { DESKTOP_VIEW, MOBILE_VIEW } from "@constants"; +import { getIcon } from "@data/icons"; +import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState"; +import { setView, View } from "@state/admin/adminView"; +import styled, { css } from "styled-components"; + +const NavList = styled.ul` + display: flex; + gap: 2.8rem; + list-style: none; + flex-direction: column; + color: var(--color-text); + margin-top: 5.4rem; +`; + +const NavLink = styled.li<{ $active?: boolean }>` + text-decoration: none; + margin-right: -2.5rem; + color: #f5f5f5; + cursor: pointer; + + gap: 1.2rem; + display: flex; + font-weight: 500; + font-size: 1.2rem; + align-items: center; + transition: all 0.3s; + padding: 1.2rem 1.2rem; + border-radius: var(--border-radius-one-sided); + + & svg { + width: 2.4rem; + height: 2.4rem; + color: #f5f5f5; + transition: all 0.2s; + } + ${({ $active }) => + $active && + css` + background-color: var(--admin-main-bg); + color: var(--accent-color); + svg { + color: var(--accent-color); + } + `}; +`; + +const Span = styled.span` + @media ${MOBILE_VIEW} { + display: inline; + } + @media ${DESKTOP_VIEW} { + display: inline; + } +`; +function AdminMainNav() { + const activeView = useAppSelector((state) => state.adminView.value); + const dispatch = useAppDispatch(); + const handleViewChange = (view: View) => { + if (activeView !== view) { + dispatch( + setView({ + value: view, + }) + ); + } + }; + return ( + + ); +} + +export default AdminMainNav; diff --git a/app/src/features/admin/components/AdminNavMenu.tsx b/app/src/features/admin/components/AdminNavMenu.tsx new file mode 100644 index 00000000..d9c91762 --- /dev/null +++ b/app/src/features/admin/components/AdminNavMenu.tsx @@ -0,0 +1,69 @@ +import SideBarMenuItem from "@components/side-bar/chats/SideBarMenuItem"; +import { getIcon } from "@data/icons"; +import { useMouseLeave } from "@hooks/useMouseLeave"; +import { useState } from "react"; +import styled from "styled-components"; + +const ToolsIcon = styled.div` + > svg { + color: var(--color-icon-secondary); + border-radius: var(--border-radius-modal); + padding: 0.3rem; + &:hover { + cursor: pointer; + background-color: var(--color-background-compact-menu-hover); + } + } + margin: 0 1rem 0 -1rem; +`; +const StyledList = styled.ul<{ $isOpened?: boolean }>` + width: 13rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + justify-content: space-between; + list-style: none; + padding: 0.5rem 0.2rem; + background-color: var(--color-background); + box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow); + border-radius: var(--border-radius-default); + visibility: ${({ $isOpened }) => ($isOpened ? "visible" : "hidden")}; + + z-index: 2; +`; + +function AdminNavMenu() { + const [isOpened, setIsOpened] = useState(false); + const ref = useMouseLeave(() => setIsOpened(false), false); + const handleOpenSettings = () => { + setIsOpened((prevState) => !prevState); + }; + return ( + <> + + {getIcon("Menu")} + + {isOpened && ( + } + > + onChoose("users")} + /> + onChoose("groups")} + /> + + )} + + ); +} + +export default AdminNavMenu; diff --git a/app/src/features/admin/components/AdminSidebar.tsx b/app/src/features/admin/components/AdminSidebar.tsx new file mode 100644 index 00000000..cc879ce8 --- /dev/null +++ b/app/src/features/admin/components/AdminSidebar.tsx @@ -0,0 +1,30 @@ +import styled from "styled-components"; +import Logo from "./Logo"; +import AdminMainNav from "./AdminMainNav"; +import Heading from "@components/Heading"; + +const Sidebar = styled.aside` + gap: 3.2rem; + display: flex; + grid-row: 1 / -1; + flex-direction: column; + padding: 3.2rem 2.4rem; + background-color: var(--admin-sidebar-bg); +`; +const H1 = styled(Heading)` + color: white; + margin-bottom: 2.4rem; + text-align: center; +`; + +function AdminSidebar() { + return ( + + +

TelWare

+ +
+ ); +} + +export default AdminSidebar; diff --git a/app/src/features/admin/components/Filter.tsx b/app/src/features/admin/components/Filter.tsx new file mode 100644 index 00000000..e69de29b diff --git a/app/src/features/admin/components/FilteredList.tsx b/app/src/features/admin/components/FilteredList.tsx new file mode 100644 index 00000000..e69de29b diff --git a/app/src/features/admin/components/GroupCard.tsx b/app/src/features/admin/components/GroupCard.tsx new file mode 100644 index 00000000..e69de29b diff --git a/app/src/features/admin/components/Logo.tsx b/app/src/features/admin/components/Logo.tsx new file mode 100644 index 00000000..e7dc6ed2 --- /dev/null +++ b/app/src/features/admin/components/Logo.tsx @@ -0,0 +1,20 @@ +import styled from "styled-components"; +import src from "/assets/TelWare.png"; +const StyledLogo = styled.div` + text-align: center; +`; + +const Img = styled.img` + height: 9.6rem; + width: auto; +`; + +function Logo() { + return ( + + Logo + + ); +} + +export default Logo; diff --git a/app/src/features/admin/components/UserCard.tsx b/app/src/features/admin/components/UserCard.tsx new file mode 100644 index 00000000..9e521448 --- /dev/null +++ b/app/src/features/admin/components/UserCard.tsx @@ -0,0 +1,57 @@ +import Avatar from "@components/Avatar"; +import Button from "@components/Button"; +import Heading from "@components/Heading"; +import styled from "styled-components"; +import { userStatus } from "types/admin"; + +interface UserCardProps { + id: string; + userName: string; + photo: string; + status: userStatus; +} + +const Card = styled.div` + display: flex; + flex-direction: column; + gap: 1.6rem; + padding: 2.4rem; + border-radius: 0.8rem; + background-color: var(--admin-card-bg--active); +`; +const P = styled.p<{ $status: string }>` + color: ${({ $status }) => + $status === userStatus.active ? "var(--accent-color)" : "red"}; +`; +const UserInfo = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + align-items: center; + justify-content: center; + text-align: center; +`; + +function UserCard(props: UserCardProps) { + const { id, userName, photo, status } = props; + const handleUserStatusChange = (id: string, status: string) => { + console.log(id, status); + }; + + return ( + + + + {userName} +

{status}

+
+ { + + } +
+ ); +} + +export default UserCard; diff --git a/app/src/features/admin/hooks/useActivateUser.ts b/app/src/features/admin/hooks/useActivateUser.ts new file mode 100644 index 00000000..10222e65 --- /dev/null +++ b/app/src/features/admin/hooks/useActivateUser.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { apiActivateUser } from "../services/apiUsers"; + +function useActivateUser() { + const queryClient = useQueryClient(); + + const { + mutate: activateUser, + data, + error, + isPending, + isSuccess, + } = useMutation({ + mutationFn: (userId: string) => apiActivateUser(userId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast.success("User activated successfully"); + }, + onError: () => { + toast.error("Error activating user"); + }, + }); + + return { activateUser, data, error, isPending, isSuccess }; +} + +export { useActivateUser }; diff --git a/app/src/features/admin/hooks/useBanUser.ts b/app/src/features/admin/hooks/useBanUser.ts new file mode 100644 index 00000000..e5db2279 --- /dev/null +++ b/app/src/features/admin/hooks/useBanUser.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { apiBanUser } from "../services/apiUsers"; + +function useBanUser() { + const queryClient = useQueryClient(); + + const { + mutate: banUser, + data, + error, + isPending, + isSuccess, + } = useMutation({ + mutationFn: (userId: string) => apiBanUser(userId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast.success("User banned successfully"); + }, + onError: () => { + toast.error("Error banning user"); + }, + }); + + return { banUser, data, error, isPending, isSuccess }; +} + +export { useBanUser }; diff --git a/app/src/features/admin/hooks/useDeactivateUser.ts b/app/src/features/admin/hooks/useDeactivateUser.ts new file mode 100644 index 00000000..8a776ac0 --- /dev/null +++ b/app/src/features/admin/hooks/useDeactivateUser.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { apiDeactivateUser } from "../services/apiUsers"; + +function useDeactivateUser() { + const queryClient = useQueryClient(); + + const { + mutate: deactivateUser, + data, + error, + isPending, + isSuccess, + } = useMutation({ + mutationFn: (userId: string) => apiDeactivateUser(userId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + toast.success("User deactivated successfully"); + }, + onError: () => { + toast.error("Error deactivating user"); + }, + }); + + return { deactivateUser, data, error, isPending, isSuccess }; +} + +export { useDeactivateUser }; diff --git a/app/src/features/admin/hooks/useFilterGroup.ts b/app/src/features/admin/hooks/useFilterGroup.ts new file mode 100644 index 00000000..86fc5b68 --- /dev/null +++ b/app/src/features/admin/hooks/useFilterGroup.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { apiFilterGroup } from "../services/apiGroups"; + +function useFilterGroup() { + const queryClient = useQueryClient(); + + const { + mutate: filterGroup, + data, + error, + isPending, + isSuccess, + } = useMutation({ + mutationFn: (groupId: string) => apiFilterGroup(groupId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["groups"] }); + toast.success("Group Filtered successfully"); + }, + onError: () => { + toast.error("Error filtering group"); + }, + }); + + return { filterGroup, data, error, isPending, isSuccess }; +} + +export { useFilterGroup }; diff --git a/app/src/features/admin/hooks/useGroups.ts b/app/src/features/admin/hooks/useGroups.ts new file mode 100644 index 00000000..235a0098 --- /dev/null +++ b/app/src/features/admin/hooks/useGroups.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiGetGroups } from "../services/apiGroups"; + +function useGroups() { + const { + data: groups, + error, + isLoading, + } = useQuery({ + queryKey: ["groups"], + queryFn: apiGetGroups, + }); + return { groups, error, isLoading }; +} + +export { useGroups }; diff --git a/app/src/features/admin/hooks/useUnfilterGroup.ts b/app/src/features/admin/hooks/useUnfilterGroup.ts new file mode 100644 index 00000000..49969319 --- /dev/null +++ b/app/src/features/admin/hooks/useUnfilterGroup.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { apiUnfilterGroup } from "../services/apiGroups"; + +function useUnfilterGroup() { + const queryClient = useQueryClient(); + + const { + mutate: unfilterGroup, + data, + error, + isPending, + isSuccess, + } = useMutation({ + mutationFn: (groupId: string) => apiUnfilterGroup(groupId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["groups"] }); + toast.success("Group Unfiltered successfully"); + }, + onError: () => { + toast.error("Error unfiltering group"); + }, + }); + + return { unfilterGroup, data, error, isPending, isSuccess }; +} + +export { useUnfilterGroup }; diff --git a/app/src/features/admin/hooks/useUsers.ts b/app/src/features/admin/hooks/useUsers.ts new file mode 100644 index 00000000..6d849779 --- /dev/null +++ b/app/src/features/admin/hooks/useUsers.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiGetUsers } from "../services/apiUsers"; + +function useUsers() { + const { + data: users, + error, + isLoading, + } = useQuery({ + queryKey: ["users"], + queryFn: apiGetUsers, + }); + return { users, error, isLoading }; +} + +export { useUsers }; diff --git a/app/src/features/admin/services/apiGroups.ts b/app/src/features/admin/services/apiGroups.ts new file mode 100644 index 00000000..446c04ff --- /dev/null +++ b/app/src/features/admin/services/apiGroups.ts @@ -0,0 +1,47 @@ +import { API_URL } from "@constants"; + +async function apiGetGroups() { + const res = await fetch(`${API_URL}/admin/groups`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} +async function apiFilterGroup(groupId: string) { + const res = await fetch(`${API_URL}/admin/filter/${groupId}`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} + +async function apiUnfilterGroup(groupId: string) { + const res = await fetch(`${API_URL}/admin/unfilter/${groupId}`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} + +export { apiGetGroups, apiFilterGroup, apiUnfilterGroup }; diff --git a/app/src/features/admin/services/apiUsers.ts b/app/src/features/admin/services/apiUsers.ts new file mode 100644 index 00000000..07ae3978 --- /dev/null +++ b/app/src/features/admin/services/apiUsers.ts @@ -0,0 +1,61 @@ +import { API_URL } from "@constants"; + +async function apiGetUsers() { + const res = await fetch(`${API_URL}/admin/groups`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} +async function apiDeactivateUser(userId: string) { + const res = await fetch(`${API_URL}/admin/deactivate/${userId}`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} +async function apiBanUser(userId: string) { + const res = await fetch(`${API_URL}/admin/ban/${userId}`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} + +async function apiActivateUser(userId: string) { + const res = await fetch(`${API_URL}/admin/activate/${userId}`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionId") || "", + }, + }); + + if (res.status !== 200) { + throw new Error(res.statusText); + } +} + +export { apiGetUsers, apiDeactivateUser, apiBanUser, apiActivateUser }; diff --git a/app/src/features/chats/StartNewChat.tsx b/app/src/features/chats/StartNewChat.tsx index 656a8b43..57741b73 100644 --- a/app/src/features/chats/StartNewChat.tsx +++ b/app/src/features/chats/StartNewChat.tsx @@ -81,7 +81,6 @@ function StartNewChat() { > {items.map((item: SideBarMenuItemProps, id: number) => ( { diff --git a/app/src/features/privacy-settings/hooks/useBlock.ts b/app/src/features/privacy-settings/hooks/useBlock.ts index f6ea57f7..b6c390b3 100644 --- a/app/src/features/privacy-settings/hooks/useBlock.ts +++ b/app/src/features/privacy-settings/hooks/useBlock.ts @@ -1,23 +1,23 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import getBlockList from "@features/privacy-settings/service/apiGetBlockList"; -import { blockUser } from "../service/apiBlockUser"; +import apiGetBlockList from "@features/privacy-settings/service/apiGetBlockList"; +import { apiBlockUser } from "../service/apiBlockUser"; import { BlockedUserProps } from "../BlockItem"; -import { removeFromBlock } from "../service/apiRemoveFromBlocks"; +import { apiRemoveFromBlock } from "../service/apiRemoveFromBlocks"; export function useBlock() { const { data: blockList } = useQuery({ queryKey: ["block"], - queryFn: () => getBlockList(), + queryFn: () => apiGetBlockList(), }); const queryClient = useQueryClient(); const { mutateAsync: addToBlockList } = useMutation({ - mutationFn: blockUser, + mutationFn: apiBlockUser, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["block"] }), }); const { mutateAsync: removeFromBlockList } = useMutation({ - mutationFn: removeFromBlock, + mutationFn: apiRemoveFromBlock, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["block"] }), }); diff --git a/app/src/features/privacy-settings/hooks/useUpdatePrivacy.ts b/app/src/features/privacy-settings/hooks/useUpdatePrivacy.ts index 8107a9a7..7191a600 100644 --- a/app/src/features/privacy-settings/hooks/useUpdatePrivacy.ts +++ b/app/src/features/privacy-settings/hooks/useUpdatePrivacy.ts @@ -1,10 +1,10 @@ import { useMutation } from "@tanstack/react-query"; -import { changeSettings } from "@features/privacy-settings/service/apiChangeSettings"; +import { apiChangeSettings } from "@features/privacy-settings/service/apiChangeSettings"; export function useUpdatePrivacy() { const { mutateAsync: updatePrivacy } = useMutation({ mutationKey: ["privacy"], - mutationFn: changeSettings, + mutationFn: apiChangeSettings, onSuccess: () => {}, }); diff --git a/app/src/features/privacy-settings/service/apiBlockUser.ts b/app/src/features/privacy-settings/service/apiBlockUser.ts index 35fd3186..ec29c297 100644 --- a/app/src/features/privacy-settings/service/apiBlockUser.ts +++ b/app/src/features/privacy-settings/service/apiBlockUser.ts @@ -4,7 +4,7 @@ interface requestType { id: string; } -async function blockUser(data: requestType) { +async function apiBlockUser(data: requestType) { const res = await fetch(`${API_URL}/users/block/${data.id}`, { method: "PATCH", credentials: "include", @@ -19,4 +19,4 @@ async function blockUser(data: requestType) { } } -export { blockUser }; +export { apiBlockUser }; diff --git a/app/src/features/privacy-settings/service/apiChangeSettings.ts b/app/src/features/privacy-settings/service/apiChangeSettings.ts index 172c2619..9f081861 100644 --- a/app/src/features/privacy-settings/service/apiChangeSettings.ts +++ b/app/src/features/privacy-settings/service/apiChangeSettings.ts @@ -10,7 +10,7 @@ enum endPts { addToChannelPrivacy = "invite-permissions", } -async function changeSettings(data: { +async function apiChangeSettings(data: { key: keyof typeof endPts; value: privacyStatesStrings | activeStatesStrings; }) { @@ -34,5 +34,5 @@ async function changeSettings(data: { } } -export { changeSettings }; +export { apiChangeSettings }; export { endPts }; diff --git a/app/src/features/privacy-settings/service/apiGetBlockList.ts b/app/src/features/privacy-settings/service/apiGetBlockList.ts index 267df334..e3c646de 100644 --- a/app/src/features/privacy-settings/service/apiGetBlockList.ts +++ b/app/src/features/privacy-settings/service/apiGetBlockList.ts @@ -2,7 +2,7 @@ import { API_URL } from "@constants"; import { BlockedUserProps } from "../BlockItem"; -async function getBlockList() { +async function apiGetBlockList() { const response = await fetch(`${API_URL}/users/blocked`, { method: "GET", credentials: "include", @@ -17,4 +17,4 @@ async function getBlockList() { return data.users as BlockedUserProps[]; } -export default getBlockList; +export default apiGetBlockList; diff --git a/app/src/features/privacy-settings/service/apiRemoveFromBlocks.ts b/app/src/features/privacy-settings/service/apiRemoveFromBlocks.ts index a1e2c65c..a360bea5 100644 --- a/app/src/features/privacy-settings/service/apiRemoveFromBlocks.ts +++ b/app/src/features/privacy-settings/service/apiRemoveFromBlocks.ts @@ -4,7 +4,7 @@ interface requestType { id: string; } -async function removeFromBlock(data: requestType) { +async function apiRemoveFromBlock(data: requestType) { const res = await fetch(`${API_URL}/users/block/${data.id}`, { method: "DELETE", credentials: "include", @@ -19,4 +19,4 @@ async function removeFromBlock(data: requestType) { } } -export { removeFromBlock }; +export { apiRemoveFromBlock }; diff --git a/app/src/features/stories/hooks/useMyStories.ts b/app/src/features/stories/hooks/useMyStories.ts index 685dffba..45e1779c 100644 --- a/app/src/features/stories/hooks/useMyStories.ts +++ b/app/src/features/stories/hooks/useMyStories.ts @@ -8,7 +8,7 @@ function useMyStroies() { error, isLoading, } = useQuery({ - queryKey: ["myStories"], + queryKey: ["my-stories"], queryFn: getMyStoriesAPI, }); return { myStories, error, isLoading }; diff --git a/app/src/mocks/admin/admin.ts b/app/src/mocks/admin/admin.ts new file mode 100644 index 00000000..c37f61fd --- /dev/null +++ b/app/src/mocks/admin/admin.ts @@ -0,0 +1,123 @@ +import { http, HttpResponse } from "msw"; +import { MOCK_USERS, MOCK_GROUPS } from "../data/admin"; +import { Group, User } from "types/admin"; + +type BanUserRequestBody = { + data: string; +}; +type BanUserResponseBodySuccess = { + status: "success"; + message: string; + data: object | boolean; +}; +type BanUserResponseBodyFail = { + status: "fail" | "error"; + message: string; + data: object; +}; +type BanUserResponseBody = BanUserResponseBodySuccess | BanUserResponseBodyFail; + +export const adminMock = [ + http.patch<{ userId: string }, BanUserRequestBody, BanUserResponseBody>( + "/admin/deactivate/:userId", + async ({ params }) => { + const { userId } = params; + const userIndex = MOCK_USERS.findIndex( + (user: User) => user.id === userId + ); + if (userIndex != -1) { + MOCK_USERS[userIndex].status = "deactivated"; + return HttpResponse.json({}, { status: 200 }); + } + return HttpResponse.json( + { status: "fail", message: "user not found", data: {} }, + { status: 404 } + ); + } + ), + http.patch<{ userId: string }, BanUserRequestBody, BanUserResponseBody>( + "/admin/activate/:userId", + async ({ params }) => { + const { userId } = params; + const userIndex = MOCK_USERS.findIndex( + (user: User) => user.id === userId + ); + if (userIndex != -1) { + MOCK_USERS[userIndex].status = "activated"; + return HttpResponse.json({}, { status: 200 }); + } + return HttpResponse.json( + { status: "fail", message: "user not found", data: {} }, + { status: 404 } + ); + } + ), + http.patch<{ userId: string }, BanUserRequestBody, BanUserResponseBody>( + "/admin/ban/:userId", + async ({ params }) => { + const { userId } = params; + const userIndex = MOCK_USERS.findIndex( + (user: User) => user.id === userId + ); + if (userIndex != -1) { + MOCK_USERS[userIndex].status = "banned"; + return HttpResponse.json({}, { status: 200 }); + } + return HttpResponse.json( + { status: "fail", message: "user not found", data: {} }, + { status: 404 } + ); + } + ), + http.patch<{ groupId: string }, BanUserRequestBody, BanUserResponseBody>( + "/admin/filter/:groupId", + async ({ params }) => { + const { groupId } = params; + const groupIndex = MOCK_GROUPS.findIndex( + (group: Group) => group.id === groupId + ); + if (groupIndex != -1) { + MOCK_GROUPS[groupIndex].filtered = true; + return HttpResponse.json({}, { status: 200 }); + } + return HttpResponse.json( + { status: "fail", message: "group not found", data: {} }, + { status: 404 } + ); + } + ), + http.patch<{ groupId: string }, BanUserRequestBody, BanUserResponseBody>( + "/admin/unfilter/:groupId", + async ({ params }) => { + const { groupId } = params; + const groupIndex = MOCK_GROUPS.findIndex( + (group: Group) => group.id === groupId + ); + if (groupIndex != -1) { + MOCK_GROUPS[groupIndex].filtered = false; + return HttpResponse.json({}, { status: 200 }); + } + return HttpResponse.json( + { status: "fail", message: "group not found", data: {} }, + { status: 404 } + ); + } + ), + + http.get("/admin/users", async () => { + return HttpResponse.json( + { + data: MOCK_USERS, + }, + { status: 200 } + ); + }), + http.get("/admin/groups", async () => { + return HttpResponse.json( + { + data: MOCK_GROUPS, + }, + { status: 200 } + ); + }), +]; diff --git a/app/src/mocks/data/admin.ts b/app/src/mocks/data/admin.ts new file mode 100644 index 00000000..f98871c2 --- /dev/null +++ b/app/src/mocks/data/admin.ts @@ -0,0 +1,127 @@ +const MOCK_USERS: User[] = [ + { + id: "1", + userName: "Eddie Brock", + photo: "https://i.pravatar.cc/70?=0.05", + status: "activated", + }, + { + id: "2", + userName: "Gwen Stacy", + photo: "https://i.pravatar.cc/70?=0.06", + status: "activated", + }, + { + id: "3", + userName: "Mary Jane", + photo: "https://i.pravatar.cc/70?=0.07", + status: "activated", + }, + { + id: "4", + userName: "Peter Parker", + photo: "https://i.pravatar.cc/70?=0.08", + status: "banned", + }, + { + id: "5", + userName: "Clark Kent", + photo: "https://i.pravatar.cc/70?=0.09", + status: "deactivated", + }, + { + id: "6", + userName: "Bruce Wayne", + photo: "https://i.pravatar.cc/70?=0.11", + status: "activated", + }, + { + id: "7", + userName: "Diana Prince", + photo: "https://i.pravatar.cc/70?=0.12", + status: "deactivated", + }, + { + id: "8", + userName: "Tony Stark", + photo: "https://i.pravatar.cc/70?=0.13", + status: "deactivated", + }, + { + id: "9", + userName: "Steve Rogers", + photo: "https://i.pravatar.cc/70?=0.14", + status: "banned", + }, + { + id: "10", + userName: "Natasha Romanoff", + photo: "https://i.pravatar.cc/70?=0.15", + status: "activated", + }, + { + id: "11", + userName: "Wade Wilson", + photo: "https://i.pravatar.cc/70?=0.36", + status: "activated", + }, +]; +const MOCK_GROUPS = [ + { + id: "1", + name: "Avengers", + photo: "https://i.pravatar.cc/70?=0.16", + membersCount: 10, + filtered: false, + }, + { + id: "2", + name: "Justice League", + photo: "https://i.pravatar.cc/70?=0.17", + membersCount: 12, + filtered: false, + }, + { + id: "3", + name: "Guardians of the Galaxy", + photo: "https://i.pravatar.cc/70?=0.18", + membersCount: 5, + filtered: false, + }, + { + id: "4", + name: "Power Rangers", + photo: "https://i.pravatar.cc/70?=0.19", + membersCount: 7, + filtered: false, + }, + { + id: "5", + name: "Teen Titans", + photo: "https://i.pravatar.cc/70?=0.20", + membersCount: 6, + filtered: true, + }, + { + id: "6", + name: "Ninga Turtles", + photo: "https://i.pravatar.cc/70?=0.21", + membersCount: 4, + filtered: false, + }, + { + id: "7", + name: "Transformers", + photo: "https://i.pravatar.cc/70?=0.22", + membersCount: 10, + filtered: false, + }, + { + id: "8", + name: "SWE team", + photo: "https://i.pravatar.cc/70?=0.23", + membersCount: 16, + filtered: true, + }, +]; +export { MOCK_USERS, MOCK_GROUPS }; diff --git a/app/src/mocks/data/chats.ts b/app/src/mocks/data/chats.ts index 24154817..997e808c 100644 --- a/app/src/mocks/data/chats.ts +++ b/app/src/mocks/data/chats.ts @@ -40,71 +40,71 @@ export type ChatDataType = { }[]; }; -// export const allChats: Chat[] = [ -// { -// _id: "1", -// isSeen: false, -// members: ["1", "2"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "2", -// isSeen: false, -// members: ["2", "1"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "3", -// isSeen: false, -// members: ["3"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "4", -// isSeen: false, -// members: ["4"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "5", -// isSeen: false, -// members: ["5"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "6", -// isSeen: false, -// members: ["6"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "7", -// isSeen: false, -// members: ["7"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "8", -// isSeen: false, -// members: ["8"], -// type: "private", -// numberOfMembers: 1, -// }, -// { -// _id: "9", -// isSeen: false, -// members: ["9"], -// type: "private", -// numberOfMembers: 1, -// }, -// ]; +export const allChats: Chat[] = [ + { + _id: "1", + isSeen: false, + members: ["1", "2"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "2", + isSeen: false, + members: ["2", "1"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "3", + isSeen: false, + members: ["3"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "4", + isSeen: false, + members: ["4"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "5", + isSeen: false, + members: ["5"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "6", + isSeen: false, + members: ["6"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "7", + isSeen: false, + members: ["7"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "8", + isSeen: false, + members: ["8"], + type: "private", + numberOfMembers: 1, + }, + { + _id: "9", + isSeen: false, + members: ["9"], + type: "private", + numberOfMembers: 1, + }, +]; export const members: Member[] = [ { diff --git a/app/src/mocks/handlers.ts b/app/src/mocks/handlers.ts index c4b19c92..bf4ee1fc 100644 --- a/app/src/mocks/handlers.ts +++ b/app/src/mocks/handlers.ts @@ -13,6 +13,7 @@ import { chats } from "./chats/chats"; import { devicesMock } from "./devices/devices"; import { paginationMock } from "./chats/pagination"; import { media } from "./chats/media"; +import { adminMock } from "./admin/admin"; export default [ ...loginMock, @@ -31,4 +32,5 @@ export default [ ...devicesMock, ...paginationMock, ...media, + ...adminMock, ]; diff --git a/app/src/state/admin/adminView.ts b/app/src/state/admin/adminView.ts new file mode 100644 index 00000000..aa5d9c21 --- /dev/null +++ b/app/src/state/admin/adminView.ts @@ -0,0 +1,25 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export enum View { + USERS, + GROUPS, +} + +interface adminViewState { + value: View; +} + +const initialState: adminViewState = { value: View.USERS }; + +const adminViewSlice = createSlice({ + name: "adminView", + initialState, + reducers: { + setView: (state, action: PayloadAction) => { + state.value = action.payload.value; + }, + }, +}); + +export const { setView } = adminViewSlice.actions; +export default adminViewSlice.reducer; diff --git a/app/src/state/store.ts b/app/src/state/store.ts index d1ed2800..cbb070c4 100644 --- a/app/src/state/store.ts +++ b/app/src/state/store.ts @@ -6,6 +6,7 @@ import searchReducer from "./messages/search"; import activeMessageReducer from "./messages/activeMessage"; import chatsReducer from "./messages/chats"; import selectedUsersReducer from "./groups/selectedUsers"; +import adminViewReducer from "./admin/adminView"; export const store = configureStore({ reducer: { @@ -16,6 +17,7 @@ export const store = configureStore({ activeMessage: activeMessageReducer, chats: chatsReducer, selectedUsers: selectedUsersReducer, + adminView: adminViewReducer, }, }); diff --git a/app/src/styles/GlobalStyles.tsx b/app/src/styles/GlobalStyles.tsx index 7dec5c1f..28a607a9 100644 --- a/app/src/styles/GlobalStyles.tsx +++ b/app/src/styles/GlobalStyles.tsx @@ -68,6 +68,18 @@ const GlobalStyles = createGlobalStyle` --scrollbar-color: rgba(0, 0, 0, .2); --color-avatar: linear-gradient(135deg, #d6d46c, #5e924e); --color-avatar-shadow:linear-gradient(135deg,#d6d46c, #5e924e); + --admin-sidebar-bg: #56a2c9; + --admin-sidebar-bg-hover: #447f9c; + + --admin-main-bg: #fff; + --admin-main-bg-hover: #f5f5f5; + + --admin-header-bg: #f5f5f5; + --admin-header-bg-hover: #e5e5e5; + + + --admin-nav-menu-item-hover-text: #56a2c9; + --admin-nav-menu-item-hover: rgba(213, 213, 216, 0.666); } @@ -136,7 +148,19 @@ const GlobalStyles = createGlobalStyle` --scrollbar-color: rgba(255, 255, 255, .2); --color-avatar: linear-gradient(135deg, #72C6EF, #004E92); --color-avatar-shadow:linear-gradient(135deg, #72C6EF, #004E92); - + + --admin-sidebar-bg: #192b35; + --admin-sidebar-bg-hover: #25353f; + + --admin-main-bg: #24343e; + --admin-main-bg-hover: #25353f; + + --admin-header-bg: #25353f; + --admin-header-bg-hover: #203030; + + --admin-nav-menu-item-hover-text: #24343e79; + --admin-nav-menu-item-hover: rgba(213, 213, 216, 0.466); + } --story-views-background: #181a1b; @@ -182,6 +206,8 @@ const GlobalStyles = createGlobalStyle` --border-radius-messages-small: 0.375rem; --border-radius-forum-avatar: 25%; --border-radius-circle: 50%; + --border-radius-one-sided: 50px 0 0 50px; + --color-borders-input: rgb(218, 220, 224); diff --git a/app/src/types/admin.ts b/app/src/types/admin.ts new file mode 100644 index 00000000..642ca21f --- /dev/null +++ b/app/src/types/admin.ts @@ -0,0 +1,22 @@ +interface User { + id: string; + userName: string; + photo: string; + status: string; +} +interface Group { + id: string; + name: string; + photo: string; + membersCount: number; + filtered: boolean; +} + +enum userStatus { + active = "active", + banned = "banned", + deactivated = "deactivated", +} + +export type { User, Group }; +export { userStatus }; diff --git a/app/vite.config.ts.timestamp-1734385645418-efb6d73151a22.mjs b/app/vite.config.ts.timestamp-1734385645418-efb6d73151a22.mjs new file mode 100644 index 00000000..41da1e71 --- /dev/null +++ b/app/vite.config.ts.timestamp-1734385645418-efb6d73151a22.mjs @@ -0,0 +1,18 @@ +// vite.config.ts +import { defineConfig, loadEnv } from "file:///home/ahmed/Desktop/projects/telware-frontend/app/node_modules/vite/dist/node/index.js"; +import react from "file:///home/ahmed/Desktop/projects/telware-frontend/app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///home/ahmed/Desktop/projects/telware-frontend/app/node_modules/vite-tsconfig-paths/dist/index.js"; +var vite_config_default = ({ mode }) => { + process.env = { + ...process.env, + ...loadEnv(mode, process.cwd()) + }; + return defineConfig({ + server: { port: parseInt(process.env.VITE_PORT) }, + plugins: [react(), tsconfigPaths()] + }); +}; +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9haG1lZC9EZXNrdG9wL3Byb2plY3RzL3RlbHdhcmUtZnJvbnRlbmQvYXBwXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvaG9tZS9haG1lZC9EZXNrdG9wL3Byb2plY3RzL3RlbHdhcmUtZnJvbnRlbmQvYXBwL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9ob21lL2FobWVkL0Rlc2t0b3AvcHJvamVjdHMvdGVsd2FyZS1mcm9udGVuZC9hcHAvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcsIGxvYWRFbnYgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IHJlYWN0IGZyb20gXCJAdml0ZWpzL3BsdWdpbi1yZWFjdFwiO1xuaW1wb3J0IHRzY29uZmlnUGF0aHMgZnJvbSBcInZpdGUtdHNjb25maWctcGF0aHNcIjtcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cblxuZXhwb3J0IGRlZmF1bHQgKHsgbW9kZSB9KSA9PiB7XG4gIHByb2Nlc3MuZW52ID0ge1xuICAgIC4uLnByb2Nlc3MuZW52LFxuICAgIC4uLmxvYWRFbnYobW9kZSwgcHJvY2Vzcy5jd2QoKSksXG4gIH07XG4gIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgIHNlcnZlcjogeyBwb3J0OiBwYXJzZUludChwcm9jZXNzLmVudi5WSVRFX1BPUlQpIH0sXG4gICAgcGx1Z2luczogW3JlYWN0KCksIHRzY29uZmlnUGF0aHMoKV0sXG4gIH0pO1xufTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBcVUsU0FBUyxjQUFjLGVBQWU7QUFDM1csT0FBTyxXQUFXO0FBQ2xCLE9BQU8sbUJBQW1CO0FBSTFCLElBQU8sc0JBQVEsQ0FBQyxFQUFFLEtBQUssTUFBTTtBQUMzQixVQUFRLE1BQU07QUFBQSxJQUNaLEdBQUcsUUFBUTtBQUFBLElBQ1gsR0FBRyxRQUFRLE1BQU0sUUFBUSxJQUFJLENBQUM7QUFBQSxFQUNoQztBQUNBLFNBQU8sYUFBYTtBQUFBLElBQ2xCLFFBQVEsRUFBRSxNQUFNLFNBQVMsUUFBUSxJQUFJLFNBQVMsRUFBRTtBQUFBLElBQ2hELFNBQVMsQ0FBQyxNQUFNLEdBQUcsY0FBYyxDQUFDO0FBQUEsRUFDcEMsQ0FBQztBQUNIOyIsCiAgIm5hbWVzIjogW10KfQo= From 5d8e3efae6c6c5525db8659c885e37d00c49f64e Mon Sep 17 00:00:00 2001 From: Sarah Kamal <143711089+sarah-kamall@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:54:16 +0200 Subject: [PATCH 3/4] fix: adjust sixing (#158) fix: voice notes --- .../features/chats/audio/VoiceRecorder.tsx | 23 ++++++++----------- app/src/features/chats/hooks/useChatInput.ts | 3 ++- .../features/chats/media/FilePreviewItem.tsx | 6 ++++- .../chats/media/MediaUploadComponent.tsx | 23 +++++++++++-------- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/app/src/features/chats/audio/VoiceRecorder.tsx b/app/src/features/chats/audio/VoiceRecorder.tsx index c4000c75..1106f081 100644 --- a/app/src/features/chats/audio/VoiceRecorder.tsx +++ b/app/src/features/chats/audio/VoiceRecorder.tsx @@ -1,23 +1,20 @@ -import React, { - useRef, - useEffect, - useCallback, - useContext, -} from "react"; +import React, { useRef, useEffect, useCallback, useContext } from "react"; import RecordInput from "../SendButton"; import { useUploadMedia } from "../media/hooks/useUploadMedia"; import { useMessageSender } from "../hooks/useMessageSender"; import { ChatInputContext } from "../ChatBox"; +import { useParams } from "react-router-dom"; export type RecordingStates = "idle" | "recording" | "pause"; const VoiceRecorder: React.FC = ({ - recordingMimeType = "audio/webm", + recordingMimeType = "audio/webm" }: { recordingMimeType?: string; }) => { const mediaRecorder = useRef(null); const audioChunks = useRef([]); + const { chatId } = useParams<{ chatId: string }>(); const { data: voiceNoteURL, mutate: uploadVoiceNote } = useUploadMedia(); const { isRecording, setIsRecording, setError } = @@ -31,7 +28,7 @@ const VoiceRecorder: React.FC = ({ .getUserMedia({ audio: true }) .then((stream) => { const recorder = new MediaRecorder(stream, { - mimeType: recordingMimeType, + mimeType: recordingMimeType }); mediaRecorder.current = recorder; @@ -76,13 +73,13 @@ const VoiceRecorder: React.FC = ({ }, [setIsRecording]); const handleSendRecord = useCallback(() => { - if (isRecording === "pause") { + if (isRecording === "pause" && audioChunks.current.length) { const audioBlob = new Blob(audioChunks.current, { - type: recordingMimeType, + type: recordingMimeType }); const file = new File([audioBlob], "recording.webm", { type: audioBlob.type, - lastModified: Date.now(), + lastModified: Date.now() }); audioChunks.current = []; uploadVoiceNote(file); @@ -91,9 +88,8 @@ const VoiceRecorder: React.FC = ({ useEffect(() => { if (voiceNoteURL && isRecording === "pause") { - handleSendMessage("", voiceNoteURL); + handleSendMessage("", chatId, voiceNoteURL, "audio"); setIsRecording("idle"); - uploadVoiceNote(null); } }, [ voiceNoteURL, @@ -101,6 +97,7 @@ const VoiceRecorder: React.FC = ({ handleSendMessage, setIsRecording, uploadVoiceNote, + chatId ]); return ( diff --git a/app/src/features/chats/hooks/useChatInput.ts b/app/src/features/chats/hooks/useChatInput.ts index 8189435c..aade6945 100644 --- a/app/src/features/chats/hooks/useChatInput.ts +++ b/app/src/features/chats/hooks/useChatInput.ts @@ -28,6 +28,7 @@ function useChatInput() { setIsEmojiSelectorOpen(false); }; const handleSubmit = (e: Event, voiceNoteName = "") => { + console.log(voiceNoteName); e.preventDefault(); setIsEmojiSelectorOpen(false); if (isRecording !== "idle") return; @@ -57,7 +58,7 @@ function useChatInput() { sendGIF, sendSticker, handleSubmit, - handleCloseFilePreview, + handleCloseFilePreview }; } diff --git a/app/src/features/chats/media/FilePreviewItem.tsx b/app/src/features/chats/media/FilePreviewItem.tsx index ddac6c8c..fbde2cce 100644 --- a/app/src/features/chats/media/FilePreviewItem.tsx +++ b/app/src/features/chats/media/FilePreviewItem.tsx @@ -13,6 +13,8 @@ const FileViewerContainer = styled.div` z-index: 7; min-width: fit-content; height: fit-content; + max-width: 200px; + max-height: 200px; padding: 4px; background: var(--color-secondary); border: 1px solid #e6e6e6; @@ -66,11 +68,13 @@ function FilePreviewItem({ chatId }: { chatId: string | undefined }) { console.log("File uploaded successfully:", url); handleSendMessage(caption, chatId, url); setCaption(""); - setFile(null); }, onError: (error) => { console.error("Error uploading file:", error); alert("Failed to upload the file. Please try again."); + }, + onSettled: () => { + setFile(null); } }); } catch (error) { diff --git a/app/src/features/chats/media/MediaUploadComponent.tsx b/app/src/features/chats/media/MediaUploadComponent.tsx index 1cccbddb..62693c8c 100644 --- a/app/src/features/chats/media/MediaUploadComponent.tsx +++ b/app/src/features/chats/media/MediaUploadComponent.tsx @@ -17,13 +17,16 @@ interface ChildProps { export default function MediaUploadComponent({ file, setFile, - setIsFilePreviewOpen, + setIsFilePreviewOpen }: ChildProps) { const fileInput = useRef(null); const onAddFile = async (e: ChangeEvent) => { - if (e.target.files && !file) { + console.log("file"); + if (e.target.files) { + console.log("file", e.target.files[0]); setFile(e.target.files[0]); + event.target.value = ""; } setIsFilePreviewOpen(true); @@ -31,15 +34,15 @@ export default function MediaUploadComponent({ return ( <> - + fileInput.current?.click()} From 0cbb1c30354c04853c7860723d9694acc66c1e18 Mon Sep 17 00:00:00 2001 From: ahmedHamdiy Date: Sat, 21 Dec 2024 15:35:16 +0200 Subject: [PATCH 4/4] fix: voice recoder --- app/src/features/chats/ChatInput.tsx | 4 ++-- app/src/features/chats/ChatInputIcons.tsx | 10 ++++++++-- app/src/features/chats/ChatItem.tsx | 4 ++-- app/src/features/chats/Topbar.tsx | 1 - app/src/features/chats/media/MediaUploadComponent.tsx | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/features/chats/ChatInput.tsx b/app/src/features/chats/ChatInput.tsx index aa139d0e..cc68a711 100644 --- a/app/src/features/chats/ChatInput.tsx +++ b/app/src/features/chats/ChatInput.tsx @@ -80,7 +80,7 @@ function ChatInput() { isEmojiSelectorOpen, handleSubmit, showForwardUsers, - handleClose, + handleClose } = useContext(ChatInputContext); const chats = useAppSelector((state) => state.chats.chats); @@ -133,7 +133,7 @@ function ChatInput() { data-testid="send-button" /> ) : ( - + )} ) : ( diff --git a/app/src/features/chats/ChatInputIcons.tsx b/app/src/features/chats/ChatInputIcons.tsx index 4d234f58..85c5ca6e 100644 --- a/app/src/features/chats/ChatInputIcons.tsx +++ b/app/src/features/chats/ChatInputIcons.tsx @@ -20,7 +20,7 @@ function ChatInputIcons() { setInput, file, setFile, - setIsFilePreviewOpen, + setIsFilePreviewOpen } = useContext(ChatInputContext); const toggleShowEmojies = () => { @@ -28,11 +28,17 @@ function ChatInputIcons() { }; const handleKeyDown = (e: React.KeyboardEvent) => { + setIsEmojiSelectorOpen(false); if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(e as unknown as Event); } }; + const handleSetFile = (file: File) => { + setIsEmojiSelectorOpen(false); + setFile(file); + setIsFilePreviewOpen(true); + }; return ( <> @@ -50,7 +56,7 @@ function ChatInputIcons() { diff --git a/app/src/features/chats/ChatItem.tsx b/app/src/features/chats/ChatItem.tsx index 960b6ebf..246a515e 100644 --- a/app/src/features/chats/ChatItem.tsx +++ b/app/src/features/chats/ChatItem.tsx @@ -59,7 +59,7 @@ type ChatItemProps = { const ChatItem = ({ chat: { _id, lastMessage, name, photo }, - onClick, + onClick }: ChatItemProps) => { const navigate = useNavigate(); @@ -86,7 +86,7 @@ const ChatItem = ({ {new Date(timestamp).toLocaleTimeString("en-US", { hour: "2-digit", - minute: "2-digit", + minute: "2-digit" }) || "No messages"} diff --git a/app/src/features/chats/Topbar.tsx b/app/src/features/chats/Topbar.tsx index 90cfa423..fe6c4599 100644 --- a/app/src/features/chats/Topbar.tsx +++ b/app/src/features/chats/Topbar.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useState } from "react"; -import { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import styled from "styled-components"; diff --git a/app/src/features/chats/media/MediaUploadComponent.tsx b/app/src/features/chats/media/MediaUploadComponent.tsx index 62693c8c..be0e1e3e 100644 --- a/app/src/features/chats/media/MediaUploadComponent.tsx +++ b/app/src/features/chats/media/MediaUploadComponent.tsx @@ -10,7 +10,7 @@ const InvisibleButton = styled.div` `; interface ChildProps { file: File | null; - setFile: React.Dispatch>; + setFile: (file: File) => void; setIsFilePreviewOpen: React.Dispatch>; }