From 1e987fdeee504d7842b337a050fa59b60fa681b6 Mon Sep 17 00:00:00 2001 From: tal-rofe Date: Wed, 14 Sep 2022 15:33:34 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=94=A5=20[EXL-76]=20account=20set?= =?UTF-8?q?tings=20page=20ready?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit account settings page ready --- README.md | 2 +- apps/backend/envs/.env.development | 4 +- .../user/modules/auth/auto-auth.controller.ts | 1 + .../user/modules/auth/classes/responses.ts | 7 +- .../user/modules/auth/interfaces/user.ts | 6 +- .../queries/handlers/auto-auth.handler.ts | 1 + apps/cli-backend/envs/.env.development | 2 +- apps/frontend/.env.development | 2 +- apps/frontend/src/App.tsx | 7 +- apps/frontend/src/App.view.tsx | 9 +- apps/frontend/src/assets/icons.ts | 42 +++++++ .../src/assets/images/white-brand-logo.png | Bin 0 -> 2162 bytes .../Account/Account.module.scss | 56 +++++++++ .../AccountSettings/Account/Account.tsx | 45 +++++++ .../AccountSettings/Account/Account.view.tsx | 75 ++++++++++++ .../DeleteAccountModal.module.scss | 109 +++++++++++++++++ .../DeleteAccountModal/DeleteAccountModal.tsx | 57 +++++++++ .../DeleteAccountModal.view.tsx | 85 +++++++++++++ .../Account/DeleteAccountModal/index.ts | 3 + .../AccountSettings/Account/index.ts | 3 + .../AccountSettings.module.scss | 14 +++ .../AccountSettings/AccountSettings.tsx | 14 +++ .../AccountSettings/AccountSettings.view.tsx | 34 ++++++ .../AccountSettings/Header/Header.module.scss | 57 +++++++++ .../AccountSettings/Header/Header.tsx | 64 ++++++++++ .../AccountSettings/Header/Header.view.tsx | 56 +++++++++ .../AccountSettings/Header/index.ts | 3 + .../SideBar/SideBar.module.scss | 56 +++++++++ .../AccountSettings/SideBar/SideBar.tsx | 14 +++ .../AccountSettings/SideBar/SideBar.view.tsx | 46 +++++++ .../AccountSettings/SideBar/index.ts | 3 + .../containers/AccountSettings/index.ts | 3 + .../containers/CliAuth/CliAuth.view.tsx | 2 +- .../ExternalAuthRedirect.tsx | 1 + .../UserSettings/UserSettings.module.scss | 79 ------------ .../containers/UserSettings/UserSettings.tsx | 45 ------- .../UserSettings/UserSettings.view.tsx | 67 ---------- .../UserSettingsModal.module.scss | 115 ------------------ .../UserSettingsModal/UserSettingsModal.tsx | 53 -------- .../UserSettingsModal.view.tsx | 92 -------------- .../UserSettings/UserSettingsModal/index.ts | 3 - .../containers/UserSettings/index.ts | 3 - .../src/components/layout/Nav/Nav.module.scss | 47 +++++++ .../src/components/layout/Nav/Nav.tsx | 14 +++ .../src/components/layout/Nav/Nav.view.tsx | 39 ++++++ .../layout/Nav/NavLink/NavLink.module.scss | 31 +++++ .../components/layout/Nav/NavLink/NavLink.tsx | 20 +++ .../layout/Nav/NavLink/NavLink.view.tsx | 34 ++++++ .../components/layout/Nav/NavLink/index.ts | 3 + .../src/components/layout/Nav/index.ts | 3 + .../SettingsSidebar.module.scss | 63 ---------- .../SettingsSidebar/SettingsSidebar.tsx | 27 ---- .../SettingsSidebar/SettingsSidebar.view.tsx | 37 ------ .../layout/SettingsSidebar/index.ts | 3 - .../ui/EDBackdrop/EDBackdrop.module.scss | 7 +- .../components/ui/EDBackdrop/EDBackdrop.tsx | 2 +- .../ui/EDBackdrop/EDBackdrop.view.tsx | 2 +- .../EDNotification/EDNotification.module.scss | 80 ++++++++++++ .../ui/EDNotification/EDNotification.tsx | 47 +++++++ .../ui/EDNotification/EDNotification.view.tsx | 72 +++++++++++ .../src/components/ui/EDNotification/index.ts | 3 + .../components/ui/EDSelect/EDSelect.view.tsx | 2 +- .../src/components/ui/EDSvg/EDSvg.tsx | 2 +- .../src/components/ui/EDSvg/EDSvg.view.tsx | 2 +- apps/frontend/src/hooks/backend-api.ts | 2 +- apps/frontend/src/i18n/en.ts | 53 +++++--- apps/frontend/src/interfaces/responses.ts | 1 + apps/frontend/src/pages/AccountSettings.tsx | 14 +++ apps/frontend/src/pages/UserSettings.tsx | 14 --- apps/frontend/src/store/app.ts | 6 +- apps/frontend/src/store/interfaces/auth.ts | 2 + apps/frontend/src/store/interfaces/ui.ts | 13 ++ apps/frontend/src/store/middlewares/ui.ts | 26 ++++ apps/frontend/src/store/models/ui.ts | 3 + apps/frontend/src/store/reducers/auth.ts | 3 + apps/frontend/src/store/reducers/ui.ts | 30 +++++ apps/frontend/src/styles/custom.scss | 9 +- apps/frontend/src/styles/variables.scss | 9 ++ apps/frontend/stylelint.config.cjs | 4 +- 79 files changed, 1425 insertions(+), 644 deletions(-) create mode 100644 apps/frontend/src/assets/images/white-brand-logo.png create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/Account.module.scss create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/Account.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/Account.view.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.module.scss create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.view.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/index.ts create mode 100644 apps/frontend/src/components/containers/AccountSettings/Account/index.ts create mode 100644 apps/frontend/src/components/containers/AccountSettings/AccountSettings.module.scss create mode 100644 apps/frontend/src/components/containers/AccountSettings/AccountSettings.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/AccountSettings.view.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Header/Header.module.scss create mode 100644 apps/frontend/src/components/containers/AccountSettings/Header/Header.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Header/Header.view.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/Header/index.ts create mode 100644 apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.module.scss create mode 100644 apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.view.tsx create mode 100644 apps/frontend/src/components/containers/AccountSettings/SideBar/index.ts create mode 100644 apps/frontend/src/components/containers/AccountSettings/index.ts delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettings.module.scss delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettings.tsx delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettings.view.tsx delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.module.scss delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.tsx delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.view.tsx delete mode 100644 apps/frontend/src/components/containers/UserSettings/UserSettingsModal/index.ts delete mode 100644 apps/frontend/src/components/containers/UserSettings/index.ts create mode 100644 apps/frontend/src/components/layout/Nav/Nav.module.scss create mode 100644 apps/frontend/src/components/layout/Nav/Nav.tsx create mode 100644 apps/frontend/src/components/layout/Nav/Nav.view.tsx create mode 100644 apps/frontend/src/components/layout/Nav/NavLink/NavLink.module.scss create mode 100644 apps/frontend/src/components/layout/Nav/NavLink/NavLink.tsx create mode 100644 apps/frontend/src/components/layout/Nav/NavLink/NavLink.view.tsx create mode 100644 apps/frontend/src/components/layout/Nav/NavLink/index.ts create mode 100644 apps/frontend/src/components/layout/Nav/index.ts delete mode 100644 apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.module.scss delete mode 100644 apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.tsx delete mode 100644 apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.view.tsx delete mode 100644 apps/frontend/src/components/layout/SettingsSidebar/index.ts create mode 100644 apps/frontend/src/components/ui/EDNotification/EDNotification.module.scss create mode 100644 apps/frontend/src/components/ui/EDNotification/EDNotification.tsx create mode 100644 apps/frontend/src/components/ui/EDNotification/EDNotification.view.tsx create mode 100644 apps/frontend/src/components/ui/EDNotification/index.ts create mode 100644 apps/frontend/src/pages/AccountSettings.tsx delete mode 100644 apps/frontend/src/pages/UserSettings.tsx create mode 100644 apps/frontend/src/store/interfaces/ui.ts create mode 100644 apps/frontend/src/store/middlewares/ui.ts create mode 100644 apps/frontend/src/store/models/ui.ts create mode 100644 apps/frontend/src/store/reducers/ui.ts diff --git a/README.md b/README.md index ad6237974..8a3ec7946 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Then, you can browse to `http://localhost:3000/api` (replace the port if you use ## Cli backend OpenAPI When you want look at the cli backend OpenAPI (Swagger), you need to run the application in development mode. -Then, you can browse to `http://localhost:5000/api` (replace the port if you use other port) +Then, you can browse to `http://localhost:4000/api` (replace the port if you use other port) ## Improvements diff --git a/apps/backend/envs/.env.development b/apps/backend/envs/.env.development index 1e3bc3c81..c7d70c3b7 100644 --- a/apps/backend/envs/.env.development +++ b/apps/backend/envs/.env.development @@ -5,8 +5,8 @@ REFRESH_TOKEN_JWT_KEY="REFRESH" GOOGLE_OAUTH_CLIENT_ID="DUMMY" GOOGLE_OAUTH_CLIENT_SECRET="DUMMY" GOOGLE_OAUTH_REDIRECT_URI="http://localhost:3000/user/auth/google-redirect" -GITHUB_OAUTH_CLIENT_ID="DUMMY" -GITHUB_OAUTH_CLIENT_SECRET="DUMMY" +GITHUB_OAUTH_CLIENT_ID="d711464776c7e65f53f4" +GITHUB_OAUTH_CLIENT_SECRET="b8f173a3e0dc6941b37fd7d930afd69b4dfa7462" GITHUB_OAUTH_REDIRECT_URI="http://localhost:3000/user/auth/github-redirect" MIXPANEL_TOKEN="XOMBILLAH" FRONTEND_URL="http://localhost:8080" diff --git a/apps/backend/src/modules/user/modules/auth/auto-auth.controller.ts b/apps/backend/src/modules/user/modules/auth/auto-auth.controller.ts index 39335f4f5..730648498 100644 --- a/apps/backend/src/modules/user/modules/auth/auto-auth.controller.ts +++ b/apps/backend/src/modules/user/modules/auth/auto-auth.controller.ts @@ -73,6 +73,7 @@ export class AutoAuthController { return { accessToken, ...loggedUser, + createdAt: loggedUser.createdAt.getTime(), }; } } diff --git a/apps/backend/src/modules/user/modules/auth/classes/responses.ts b/apps/backend/src/modules/user/modules/auth/classes/responses.ts index 0cb9c9b80..9ae13962e 100644 --- a/apps/backend/src/modules/user/modules/auth/classes/responses.ts +++ b/apps/backend/src/modules/user/modules/auth/classes/responses.ts @@ -1,6 +1,6 @@ import { ApiResponseProperty } from '@nestjs/swagger'; -import type { IAutoAuthLoggedUser } from '../interfaces/user'; +import type { IAutoAuthLoggedUserResponse } from '../interfaces/user'; export class RefreshTokenResponse { @ApiResponseProperty({ @@ -11,7 +11,7 @@ export class RefreshTokenResponse { public accessToken!: string; } -export class AutoLoginResponse implements IAutoAuthLoggedUser { +export class AutoLoginResponse implements IAutoAuthLoggedUserResponse { @ApiResponseProperty({ type: String, example: @@ -24,4 +24,7 @@ export class AutoLoginResponse implements IAutoAuthLoggedUser { @ApiResponseProperty({ type: String, example: 'Yazif' }) public name!: string; + + @ApiResponseProperty({ type: Number, example: 43343234223 }) + public createdAt!: number; } diff --git a/apps/backend/src/modules/user/modules/auth/interfaces/user.ts b/apps/backend/src/modules/user/modules/auth/interfaces/user.ts index 4066c7dae..c8bc79cc9 100644 --- a/apps/backend/src/modules/user/modules/auth/interfaces/user.ts +++ b/apps/backend/src/modules/user/modules/auth/interfaces/user.ts @@ -1,5 +1,9 @@ import type { User } from '@prisma/client'; -export interface IAutoAuthLoggedUser extends Pick {} +export interface IAutoAuthLoggedUser extends Pick {} + +export interface IAutoAuthLoggedUserResponse extends Pick { + readonly createdAt: number; +} export interface IExternalLoggedUser extends Pick {} diff --git a/apps/backend/src/modules/user/modules/auth/queries/handlers/auto-auth.handler.ts b/apps/backend/src/modules/user/modules/auth/queries/handlers/auto-auth.handler.ts index 842bdd765..3160ee1b6 100644 --- a/apps/backend/src/modules/user/modules/auth/queries/handlers/auto-auth.handler.ts +++ b/apps/backend/src/modules/user/modules/auth/queries/handlers/auto-auth.handler.ts @@ -12,6 +12,7 @@ export class AutoLoginHandler implements IQueryHandler { const userData = await this.dbUserService.findByEmail(contract.email, { id: true, name: true, + createdAt: true, }); return userData; diff --git a/apps/cli-backend/envs/.env.development b/apps/cli-backend/envs/.env.development index ede977859..bbef34ffc 100644 --- a/apps/cli-backend/envs/.env.development +++ b/apps/cli-backend/envs/.env.development @@ -1,5 +1,5 @@ NODE_ENV="development" -PORT="5000" +PORT="4000" FRONTEND_URL="http://localhost:8080" CLI_TOKEN_JWT_KEY="JWT" REFRESH_TOKEN_JWT_KEY="REFRESH" diff --git a/apps/frontend/.env.development b/apps/frontend/.env.development index f868490b0..3e45c4273 100644 --- a/apps/frontend/.env.development +++ b/apps/frontend/.env.development @@ -3,5 +3,5 @@ HTTPS=false FAST_REFRESH=true DISABLE_ESLINT_PLUGIN=true REACT_APP_BACKEND_URL="http://localhost:3000" -REACT_APP_CLI_BACKEND_URL="http://localhost:5000" +REACT_APP_CLI_BACKEND_URL="http://localhost:4000" REACT_APP_NODE_ENV="development" \ No newline at end of file diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index a6834039d..5d4cea46c 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -11,16 +11,16 @@ import { authActions } from './store/reducers/auth'; import AppView from './App.view'; -interface PropsFromState { +interface IPropsFromState { readonly isAuthenticated: boolean | null; } -interface PropsFromDispatch { +interface IPropsFromDispatch { readonly auth: (loginPayload: IAuthPayload) => PayloadAction; readonly setUnauthenticated: () => PayloadAction; } -interface IProps extends PropsFromState, PropsFromDispatch {} +interface IProps extends IPropsFromState, IPropsFromDispatch {} const App: React.FC = (props: React.PropsWithChildren) => { useEffect(() => { @@ -72,6 +72,7 @@ const App: React.FC = (props: React.PropsWithChildren) => { props.auth({ id: response.data.id, name: response.data.name, + createdAt: response.data.createdAt, }); }) .catch(() => { diff --git a/apps/frontend/src/App.view.tsx b/apps/frontend/src/App.view.tsx index e3a49bbbd..6d2fb1e80 100644 --- a/apps/frontend/src/App.view.tsx +++ b/apps/frontend/src/App.view.tsx @@ -1,13 +1,15 @@ import React, { Suspense } from 'react'; import { Route, BrowserRouter, Routes, Navigate } from 'react-router-dom'; +import EDNotification from '@/ui/EDNotification'; + interface IProps { readonly isAuthenticated: boolean | null; } const Auth = React.lazy(() => import('./pages/Auth')); const ExternalAuthRedirect = React.lazy(() => import('./pages/ExternalAuthRedirect')); -const UserSettings = React.lazy(() => import('./pages/UserSettings')); +const AccountSettings = React.lazy(() => import('./pages/AccountSettings')); const CliAuth = React.lazy(() => import('./pages/CliAuth')); const CliAuthenticated = React.lazy(() => import('./pages/CliAuthenticated')); const NotFound = React.lazy(() => import('./pages/NotFound')); @@ -17,6 +19,9 @@ const AppView: React.FC = (props: React.PropsWithChildren) => (
+ + + {props.isAuthenticated === false && ( <> @@ -25,7 +30,7 @@ const AppView: React.FC = (props: React.PropsWithChildren) => ( } /> )} - {props.isAuthenticated && } />} + {props.isAuthenticated && } />} } /> } /> } /> diff --git a/apps/frontend/src/assets/icons.ts b/apps/frontend/src/assets/icons.ts index cd7099f47..ab4f11201 100644 --- a/apps/frontend/src/assets/icons.ts +++ b/apps/frontend/src/assets/icons.ts @@ -27,6 +27,48 @@ const icons = { `, ], + groupCenterRoute: [ + '22 24', + ` + + `, + ], + copy: [ + '17 20', + ` + + `, + ], + key: [ + '17 18', + ` + + `, + ], + notificationInfo: [ + '18 18', + ` + + `, + ], + notificationCheckmark: [ + '18 18', + ` + + `, + ], + notificationWarning: [ + '18 18', + ` + + `, + ], + notificationError: [ + '18 18', + ` + + `, + ], }; export default icons; diff --git a/apps/frontend/src/assets/images/white-brand-logo.png b/apps/frontend/src/assets/images/white-brand-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2046d0d19f43c534fcbeb70e7c7b108ffbedcd1d GIT binary patch literal 2162 zcmV-&2#xoNP)O0Q00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yPa&_87H|SE{Hf zRCDn-L1z+bCUu~O&;K(X)qmg<>P)IkJb6R^22fwtGQD2o|NEcREjllWCnxAx3+gUv zM}6LAGCs``s+V{gL}v)9PxXv(AcLn_8L#|8O`;l!r%vciE7<*UM;3l&ZM?FNdY9@d zotRQinVQ{DL1`#8vR}O3XQ(Os@lA=x^b~Q8_}Zj?0UY^jBqU(Z6lYQM zsXk)A{J2Ps_}?UchEea+oT&&Q;Knvv^W_d|sn{ni?gzWA+No1u8?s42AG&T5$FR-Y zka;UEO4RGF>5et0rok>eE_8$CB7_iTfIa??rG7_z{7Kzf=k@VF_M|>X^yrr2i4rn= zkFL~GM9*@nM?;-!Og%*HLHOvFna=A?B_d!6Mt4SZeL90oN4BAI*hg~+EY1=Z+IG3b z04xooAHu!2t*CyF+^0@h%EAM%#8y{mTw9}-Z6s!3@tJ3-{cwkVlJ16!L@+jTF0}^1 z*-VY58cRT68x&5$4s7R2Sng0zzM%%g>j0bk^IewdzHB!M2yB`dPOYa_QvJjM zu-urpkRN|hbEsn^4A^i}>H^rv&6e%3Y}vreM7D7}ESJCLjd2$&=oWbDVjD+NvtfCo z*`oWfBP9y>M(wDDS#(Nopnb#vY^N{veTMg12S+)cuuSPqXqfp_3yB)O>E)<<^y_r= zBphsdvyC>?z0}`X7@>=R$#4*MU>hx|MfJRoj`A@QC$O8`>o~XzuTN;_iv!qBIWmv( z`p}sFN#X!lTF3SpZNEr$kfgxQTt+RR+DkxS8x->p480Fr2REZ%mz}RtBP5By%?Q1zrLt64)t<8L+9!HNR3V4Lhmb1 zLJo-oHp(>#G@GZ-xX=)RMiz@><--dZisX&*`q1E*=6DwF=!dP#bwePVbDv2=ccu!j zoD4hNAMtFOyF^F-?I?uPEwFklnkKRf#X|+vQyk;$M#9c@7fW>>B*n*0Y8v%SSy|bB zae#}UmVOj9`|BR6I0H!sSxJqf`I{slfi`7f6`ht)_et@auJjkn@4prSmz{b2Wmp2z zWNMa_hT*l*e11)U9naDKv*ba}#>4hdAEgFL{8_hgvz5|Z`qJqQqo+WZVtj~>SU@5EVm{JY^E+07qFq$u+-wD zJbmi!l*W8o*65?Am}~W@ZHONYH*3mAew*R2QGOaMeL4wf2w6vVd4@jspq3#SY@kL- zF2GE`8Oezx>h6@L$2FMB4T}%y>5n)MSftUkzQ?F$*_hr9R&_5a=v+Qfas+n3Vil5s zR#zR6sk54aJIJBYr(~126hRNf5AS1sx2%B^n|f z@r$UTNi$txpR`brZdpDqwz>)*gkKC!3#%1UF}UH?>+LWJwF;jGw)Aka+c%6EX&5N<9vz}Vf zQUGh=24}{P<&(4VPf#a_=Md;gC+d|Pyv*8oEna^W{Fg7*3vAtg4YDir-=Oij zTdxHo{3y>7SkeE63Jzs7UXP(qfd87goPm{G(b2y>7vpu5>laIP5>FuLVfSh;_cdg^ zrb6hKOZj38j({CC)napd>M~waAKsy2E9+hCRFD3On#OA)sIEmz>TBcT0QS_Y3AS5O omF>h6DgOTH@pwEQkEeL}7cAC|5ppIt%K!iX07*qoM6N<$g2lQIIsgCw literal 0 HcmV?d00001 diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/Account.module.scss b/apps/frontend/src/components/containers/AccountSettings/Account/Account.module.scss new file mode 100644 index 000000000..47a2c173b --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/Account.module.scss @@ -0,0 +1,56 @@ +@use 'sass:map'; + +@import '../../../../styles/variables.scss'; + +.container { + display: flex; + flex-direction: column; + width: 708px; +} + +.actionSection { + display: flex; + flex-direction: column; + align-items: flex-start; + + &:not(:last-child) { + margin-bottom: map.get($sizes, spacing-xxl); + } + + &__header { + font-size: 2.4rem; + font-weight: normal; + color: map.get($colors, greys-onyx); + + &--delete { + color: map.get($colors, reds-persian-plum); + } + } + + &__divider { + width: 100%; + height: 2px; + margin-block: map.get($sizes, spacing-l); + background-color: map.get($colors, greys-platinum); + } + + &__subHeader { + margin-bottom: map.get($sizes, spacing-m); + font-size: 1.5rem; + color: map.get($colors, greys-independence-grey); + } + + &__button { + padding: map.get($sizes, spacing-m) map.get($sizes, spacing-l); + font-size: 1.5rem; + color: map.get($colors, greys-onyx); + background-color: map.get($colors, whites-ghost-white); + border: 2px solid map.get($colors, greys-platinum); + border-radius: 6px; + + &--delete { + font-weight: 500; + color: map.get($colors, reds-persian-plum); + } + } +} diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/Account.tsx b/apps/frontend/src/components/containers/AccountSettings/Account/Account.tsx new file mode 100644 index 000000000..28301ab88 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/Account.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { useNavigate } from 'react-router-dom'; + +import { authActions } from '@/store/reducers/auth'; + +import AccountView from './Account.view'; + +interface IPropsFromDispatch { + readonly setUnauthenticated: () => PayloadAction; +} + +interface IProps extends IPropsFromDispatch {} + +const Account: React.FC = (props: React.PropsWithChildren) => { + const navigate = useNavigate(); + + const [isDeleteAccountModalOnViewState, setIsDeleteAccountModalOnViewState] = useState(false); + + const onSignOutClick = () => { + props.setUnauthenticated(); + + navigate('/auth'); + }; + + const onOpenDeleteAccountModal = () => setIsDeleteAccountModalOnViewState(() => true); + const onCloseDeleteAccountModal = () => setIsDeleteAccountModalOnViewState(() => false); + + return ( + + ); +}; + +Account.displayName = 'Account'; +Account.defaultProps = {}; + +export default connect(null, { + setUnauthenticated: authActions.setUnauthenticated, +})(React.memo(Account)); diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/Account.view.tsx b/apps/frontend/src/components/containers/AccountSettings/Account/Account.view.tsx new file mode 100644 index 000000000..ddfc29ded --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/Account.view.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { concatClasses } from '@/utils/component'; + +import DeleteAccountModal from './DeleteAccountModal'; + +import classes from './Account.module.scss'; + +interface IProps { + readonly isDeleteAccountModalOnView: boolean; + readonly onSignOutClick: VoidFunction; + readonly onOpenDeleteAccountModal: VoidFunction; + readonly onCloseDeleteAccountModal: VoidFunction; +} + +const AccountView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + return ( +
+
+

+ {t('accountSettings.account.signOutHeader')} +

+
+ + {t('accountSettings.account.signOutSubHeader')} + + +
+ +
+

+ {t('accountSettings.account.deleteAccountHeader')} +

+
+ + {t('accountSettings.account.deleteAccountSubHeader')} + + + {props.isDeleteAccountModalOnView && ( + + )} +
+
+ ); +}; + +AccountView.displayName = 'AccountView'; +AccountView.defaultProps = {}; + +export default React.memo(AccountView); diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.module.scss b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.module.scss new file mode 100644 index 000000000..18172887b --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.module.scss @@ -0,0 +1,109 @@ +@use 'sass:map'; + +@import '../../../../../styles/variables.scss'; + +.container { + position: absolute; + z-index: 1; + display: flex; + flex-direction: column; + width: 570px; + overflow: hidden; + border: 2px solid map.get($colors, blacks-yankees-black-blue); + border-radius: 5px; + transform: translate(-50%, -50%); + inset-block-start: 50%; + inset-inline-start: 50%; +} + +.header { + display: flex; + padding: map.get($sizes, spacing-m) map.get($sizes, spacing-l); + background-color: map.get($colors, blacks-dark-gunmetal); + + .headerButton { + display: flex; + align-items: center; + + &__icon { + width: auto; + height: 11px; + margin-inline-end: map.get($sizes, spacing-s); + fill: map.get($colors, whites-white); + } + + &__text { + font-size: 1.7rem; + font-weight: 500; + color: map.get($colors, whites-white); + } + } +} + +.body { + display: flex; + flex-direction: column; + align-items: center; + padding: map.get($sizes, spacing-xxl) map.get($sizes, spacing-xxl-3); + background-color: map.get($colors, whites-white); + + &__logo { + width: 64.2px; + object-fit: contain; + } + + &__header { + margin-block: map.get($sizes, spacing-xl) map.get($sizes, spacing-l); + font-size: 2.4rem; + font-weight: 500; + color: map.get($colors, blacks-dark-gunmetal); + } + + &__subHeader { + font-size: 2.1rem; + color: map.get($colors, blacks-dark-gunmetal); + text-align: center; + } + + &__actionText { + margin-block: map.get($sizes, spacing-xl) map.get($sizes, spacing-l); + font-size: 2.1rem; + color: map.get($colors, blacks-dark-gunmetal); + + &--phraseText { + color: map.get($colors, reds-persian-plum); + } + } + + &__input { + width: 293px; + padding: map.get($sizes, spacing-m); + margin-bottom: map.get($sizes, spaicng-l); + font-size: 1.7rem; + font-weight: 500; + color: map.get($colors, greys-independence-grey); + border: 2px solid map.get($colors, greys-platinum); + border-radius: 10px; + } + + &__button { + display: flex; + align-items: center; + justify-content: center; + width: 104px; + height: 44px; + padding: map.get($sizes, spacing-xl) map.get($sizes, spacing-m); + margin-top: map.get($sizes, spacing-l); + font-size: 1.7rem; + color: map.get($colors, greys-platinum); + background-color: map.get($colors, blacks-dark-gunmetal); + border: 2px solid map.get($colors, blacks-harsh-black); + border-radius: 10px; + + &:disabled { + color: map.get($colors, greys-philippine-gray); + background-color: map.get($colors, greys-platinum); + border-color: map.get($colors, greys-silver-sand); + } + } +} diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.tsx b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.tsx new file mode 100644 index 000000000..c27132570 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.tsx @@ -0,0 +1,57 @@ +import React, { type FormEvent, useState } from 'react'; +import { connect } from 'react-redux'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import { backendApi } from '@/utils/http'; +import { authActions } from '@/store/reducers/auth'; + +import DeleteAccountModalView from './DeleteAccountModal.view'; + +interface IPropsFromDispatch { + readonly setUnauthenticated: () => PayloadAction; +} + +interface IProps extends IPropsFromDispatch { + readonly onClose: VoidFunction; +} + +const DeleteAccountModal: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [isConfirmButtonDisabledState, setIsConfirmButtonDisabledState] = useState(true); + + const onDelete = (e: FormEvent) => { + e.preventDefault(); + + backendApi.delete('/user/auth').then(() => { + props.setUnauthenticated(); + + navigate('/auth'); + }); + }; + + const onDeleteAccountInputChangeHandler = (input: string) => { + setIsConfirmButtonDisabledState( + () => input !== t('accountSettings.account.deleteModal.actionPhraseText'), + ); + }; + + return ( + + ); +}; + +DeleteAccountModal.displayName = 'DeleteAccountModal'; +DeleteAccountModal.defaultProps = {}; + +export default connect(null, { + setUnauthenticated: authActions.setUnauthenticated, +})(React.memo(DeleteAccountModal)); diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.view.tsx b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.view.tsx new file mode 100644 index 000000000..555f2d53a --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/DeleteAccountModal.view.tsx @@ -0,0 +1,85 @@ +import React, { type FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import ReactDOM from 'react-dom'; + +import EDBackdrop from '@/ui/EDBackdrop'; +import EDSvg from '@/ui/EDSvg'; +import blackExlint from '@/images/black-exlint-logo.png'; +import { concatClasses } from '@/utils/component'; + +import classes from './DeleteAccountModal.module.scss'; + +interface IProps { + readonly isConfirmButtonDisabled: boolean; + readonly onClose: VoidFunction; + readonly onDelete: (e: FormEvent) => void; + readonly onDeleteAccountInputChangeHandler: (input: string) => void; +} + +const DeleteAccountModalView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + const modalRoot = document.getElementById('overlay-root') as HTMLElement; + + return ( + <> + + {ReactDOM.createPortal( +
+
+ +
+
+ Exlint + + {t('accountSettings.account.deleteModal.header')} + + + {t('accountSettings.account.deleteModal.subHeader')} + + + {t('accountSettings.account.deleteModal.actionText')} +   + + {t('accountSettings.account.deleteModal.actionPhraseText')} + + + + props.onDeleteAccountInputChangeHandler(value) + } + /> + +
+
, + modalRoot, + )} + + ); +}; + +DeleteAccountModalView.displayName = 'DeleteAccountModalView'; +DeleteAccountModalView.defaultProps = {}; + +export default React.memo(DeleteAccountModalView); diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/index.ts b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/index.ts new file mode 100644 index 000000000..63ad200df --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/DeleteAccountModal/index.ts @@ -0,0 +1,3 @@ +import DeleteAccountModal from './DeleteAccountModal'; + +export default DeleteAccountModal; diff --git a/apps/frontend/src/components/containers/AccountSettings/Account/index.ts b/apps/frontend/src/components/containers/AccountSettings/Account/index.ts new file mode 100644 index 000000000..452e32a68 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Account/index.ts @@ -0,0 +1,3 @@ +import Account from './Account'; + +export default Account; diff --git a/apps/frontend/src/components/containers/AccountSettings/AccountSettings.module.scss b/apps/frontend/src/components/containers/AccountSettings/AccountSettings.module.scss new file mode 100644 index 000000000..1a80ec8a2 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/AccountSettings.module.scss @@ -0,0 +1,14 @@ +@use 'sass:map'; + +@import '../../../styles/variables.scss'; + +.container { + display: flex; + flex-direction: column; + height: 100%; +} + +.content { + display: flex; + padding: map.get($sizes, spacing-xl) map.get($sizes, spacing-xxl-4); +} diff --git a/apps/frontend/src/components/containers/AccountSettings/AccountSettings.tsx b/apps/frontend/src/components/containers/AccountSettings/AccountSettings.tsx new file mode 100644 index 000000000..27510c49b --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/AccountSettings.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import AccountSettingsView from './AccountSettings.view'; + +interface IProps {} + +const AccountSettings: React.FC = () => { + return ; +}; + +AccountSettings.displayName = 'AccountSettings'; +AccountSettings.defaultProps = {}; + +export default React.memo(AccountSettings); diff --git a/apps/frontend/src/components/containers/AccountSettings/AccountSettings.view.tsx b/apps/frontend/src/components/containers/AccountSettings/AccountSettings.view.tsx new file mode 100644 index 000000000..3e8046235 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/AccountSettings.view.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Route, Routes, Navigate } from 'react-router-dom'; + +import Nav from '@/layout/Nav'; + +import Header from './Header'; +import SideBar from './SideBar'; +import Account from './Account'; + +import classes from './AccountSettings.module.scss'; + +interface IProps {} + +const AccountSettingsView: React.FC = () => { + return ( +
+
+
+ ); +}; + +AccountSettingsView.displayName = 'AccountSettingsView'; +AccountSettingsView.defaultProps = {}; + +export default React.memo(AccountSettingsView); diff --git a/apps/frontend/src/components/containers/AccountSettings/Header/Header.module.scss b/apps/frontend/src/components/containers/AccountSettings/Header/Header.module.scss new file mode 100644 index 000000000..76d11de29 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Header/Header.module.scss @@ -0,0 +1,57 @@ +@use 'sass:map'; + +@import '../../../../styles/variables.scss'; + +.container { + display: flex; + align-items: center; + justify-content: space-between; + padding: map.get($sizes, spacing-xl) map.get($sizes, spacing-xxl); + border: 1px solid map.get($colors, greys-platinum); +} + +.routeDetails { + font-size: 1.5rem; + color: map.get($colors, greys-onyx); + + &__route { + color: map.get($colors, purples-purple); + } +} + +.accountDetails { + display: flex; + align-items: center; + + &__rawText { + font-size: 1.5rem; + color: map.get($colors, greys-independence-grey); + } + + .clientIdContainer { + display: flex; + align-items: center; + padding: map.get($sizes, spacing-m); + margin-inline: map.get($sizes, spacing-m) map.get($sizes, spacing-xl); + border: 2px solid map.get($colors, greys-platinum); + border-radius: 6px; + transition: 0.3s ease-in border-color; + + &:hover { + border-color: map.get($colors, greys-philippine-gray); + } + + &__value { + margin-inline-end: map.get($sizes, spacing-m); + font-size: 1.5rem; + color: map.get($colors, greys-independence-grey); + } + + &__icon { + width: auto; + height: 19.9px; + cursor: pointer; + fill: map.get($colors, greys-independence-grey); + } + } +} diff --git a/apps/frontend/src/components/containers/AccountSettings/Header/Header.tsx b/apps/frontend/src/components/containers/AccountSettings/Header/Header.tsx new file mode 100644 index 000000000..04adbf227 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Header/Header.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import type { PayloadAction } from '@reduxjs/toolkit'; + +import type { AppState } from '@/store/app'; +import type { IUiShowNotificationPayload } from '@/store/interfaces/ui'; +import { uiActions } from '@/store/reducers/ui'; + +import HeaderView from './Header.view'; + +interface IPropsFromState { + readonly name: string; + readonly clientId: string; + readonly createdAt: number; +} + +interface IPropsFromDispatch { + readonly showNotification: ( + showNotificationPayload: IUiShowNotificationPayload, + ) => PayloadAction; +} + +interface IProps extends IPropsFromState, IPropsFromDispatch {} + +const Header: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + const creationDateFormatted = new Intl.DateTimeFormat('en-US').format(props.createdAt); + + const onCopyClientId = () => { + navigator.clipboard.writeText(props.clientId); + + props.showNotification({ + notificationType: 'info', + notificationTitle: t('accountSettings.copyNotification.title'), + notificationMessage: t('accountSettings.copyNotification.message'), + }); + }; + + return ( + + ); +}; + +Header.displayName = 'Header'; +Header.defaultProps = {}; + +const mapStateToProps = (state: AppState) => { + return { + name: state.auth.name!, + clientId: state.auth.id!, + createdAt: state.auth.createdAt!, + }; +}; + +export default connect(mapStateToProps, { + showNotification: uiActions.showNotification, +})(React.memo(Header)); diff --git a/apps/frontend/src/components/containers/AccountSettings/Header/Header.view.tsx b/apps/frontend/src/components/containers/AccountSettings/Header/Header.view.tsx new file mode 100644 index 000000000..ac6bd50a7 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Header/Header.view.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import EDSvg from '@/ui/EDSvg'; + +import classes from './Header.module.scss'; + +interface IProps { + readonly name: string; + readonly clientId: string; + readonly userCreationDate: string; + readonly onCopyClientId: VoidFunction; +} + +const HeaderView: React.FC = (props: React.PropsWithChildren) => { + const { t } = useTranslation(); + + return ( +
+ + {props.name} +   + / +   + + {t('accountSettings.header.routeText')} + + +
+ + {t('accountSettings.header.clientId')} + : + +
+ {props.clientId} + +
+ + {t('accountSettings.header.userCreated')} + : +   + {props.userCreationDate} + +
+
+ ); +}; + +HeaderView.displayName = 'HeaderView'; +HeaderView.defaultProps = {}; + +export default React.memo(HeaderView); diff --git a/apps/frontend/src/components/containers/AccountSettings/Header/index.ts b/apps/frontend/src/components/containers/AccountSettings/Header/index.ts new file mode 100644 index 000000000..a9ce1058c --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/Header/index.ts @@ -0,0 +1,3 @@ +import Header from './Header'; + +export default Header; diff --git a/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.module.scss b/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.module.scss new file mode 100644 index 000000000..5c6579aef --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.module.scss @@ -0,0 +1,56 @@ +@use 'sass:map'; + +@import '../../../../styles/variables.scss'; + +.container { + display: flex; + flex-direction: column; +} + +.innerLink { + position: relative; + display: flex; + align-items: center; + padding: map.get($sizes, spacing-s) map.get($sizes, spacing-m); + margin-inline-end: map.get($sizes, spacing-xxl); + text-decoration: none; + + &:not(:last-child) { + margin-bottom: map.get($sizes, spacing-m); + } + + &__border { + position: absolute; + display: none; + width: 4px; + height: calc(100% - 2 * map.get($sizes, spacing-xs)); + background-color: map.get($colors, purples-purple); + border-radius: 6px; + inset-inline-start: calc(map.get($sizes, spacing-m) * -1); + } + + &--active { + background-color: map.get($colors, greys-anti-flash-grey-white); + border-radius: 6px; + + .innerLink { + &__border { + display: block; + } + } + } + + &__icon { + width: auto; + height: 17px; + margin-inline-end: map.get($sizes, spacing-s); + fill: map.get($colors, greys-philippine-gray); + } + + &__text { + font-size: 1.5rem; + font-weight: 500; + color: map.get($colors, greys-onyx); + white-space: nowrap; + } +} diff --git a/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.tsx b/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.tsx new file mode 100644 index 000000000..a849b9179 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import SideBarView from './SideBar.view'; + +interface IProps {} + +const SideBar: React.FC = () => { + return ; +}; + +SideBar.displayName = 'SideBar'; +SideBar.defaultProps = {}; + +export default React.memo(SideBar); diff --git a/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.view.tsx b/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.view.tsx new file mode 100644 index 000000000..c2400be22 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/SideBar/SideBar.view.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import EDSvg from '@/ui/EDSvg'; +import { concatClasses } from '@/utils/component'; + +import classes from './SideBar.module.scss'; + +interface IProps {} + +const SideBarView: React.FC = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +SideBarView.displayName = 'SideBarView'; +SideBarView.defaultProps = {}; + +export default React.memo(SideBarView); diff --git a/apps/frontend/src/components/containers/AccountSettings/SideBar/index.ts b/apps/frontend/src/components/containers/AccountSettings/SideBar/index.ts new file mode 100644 index 000000000..576bab619 --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/SideBar/index.ts @@ -0,0 +1,3 @@ +import SideBar from './SideBar'; + +export default SideBar; diff --git a/apps/frontend/src/components/containers/AccountSettings/index.ts b/apps/frontend/src/components/containers/AccountSettings/index.ts new file mode 100644 index 000000000..ffe13acbf --- /dev/null +++ b/apps/frontend/src/components/containers/AccountSettings/index.ts @@ -0,0 +1,3 @@ +import AccountSettings from './AccountSettings'; + +export default AccountSettings; diff --git a/apps/frontend/src/components/containers/CliAuth/CliAuth.view.tsx b/apps/frontend/src/components/containers/CliAuth/CliAuth.view.tsx index 49a6aad17..6ef9adde0 100644 --- a/apps/frontend/src/components/containers/CliAuth/CliAuth.view.tsx +++ b/apps/frontend/src/components/containers/CliAuth/CliAuth.view.tsx @@ -6,7 +6,7 @@ import ExternalAction from '@/layout/ExternalAction'; import classes from './CliAuth.module.scss'; interface IProps { - readonly onAuthClick: () => void; + readonly onAuthClick: VoidFunction; } const CliAuthView: React.FC = (props: React.PropsWithChildren) => { diff --git a/apps/frontend/src/components/containers/ExternalAuthRedirect/ExternalAuthRedirect.tsx b/apps/frontend/src/components/containers/ExternalAuthRedirect/ExternalAuthRedirect.tsx index 4cc85f475..f529a2582 100644 --- a/apps/frontend/src/components/containers/ExternalAuthRedirect/ExternalAuthRedirect.tsx +++ b/apps/frontend/src/components/containers/ExternalAuthRedirect/ExternalAuthRedirect.tsx @@ -81,6 +81,7 @@ const ExternalAuthRedirect: React.FC = (props: React.PropsWithChildren PayloadAction; -} - -interface IProps extends IPropsFromDispatch {} - -const UserSettings: React.FC = (props: React.PropsWithChildren) => { - const navigate = useNavigate(); - - const [isModelOnViewState, setIsModelOnViewState] = useState(false); - - const onLogout = () => { - props.setUnauthenticated(); - - navigate('/auth'); - }; - - const onOpenModal = () => setIsModelOnViewState(() => true); - const onCloseModal = () => setIsModelOnViewState(() => false); - - return ( - - ); -}; - -UserSettings.displayName = 'UserSettings'; -UserSettings.defaultProps = {}; - -export default connect(null, { - setUnauthenticated: authActions.setUnauthenticated, -})(React.memo(UserSettings)); diff --git a/apps/frontend/src/components/containers/UserSettings/UserSettings.view.tsx b/apps/frontend/src/components/containers/UserSettings/UserSettings.view.tsx deleted file mode 100644 index 02f827b85..000000000 --- a/apps/frontend/src/components/containers/UserSettings/UserSettings.view.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import EDSvg from '@/ui/EDSvg'; -import SettingsSidebar from '@/layout/SettingsSidebar'; - -import UserSettingsModal from './UserSettingsModal'; - -import classes from './UserSettings.module.scss'; - -interface IProps { - readonly isModelOnView: boolean; - readonly onLogout: () => void; - readonly onOpenModal: () => void; - readonly onCloseModal: () => void; -} - -const UserSettingsView: React.FC = (props: React.PropsWithChildren) => { - const { t } = useTranslation(); - - return ( -
- -
-
-

{t('userSettings.title')}

-
-
- - - {t('userSettings.username')} - -
-
-
- - {props.isModelOnView && ( - - )} -
-
- -
-
-
-
-
-
- ); -}; - -UserSettingsView.displayName = 'UserSettingsView'; -UserSettingsView.defaultProps = {}; - -export default React.memo(UserSettingsView); diff --git a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.module.scss b/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.module.scss deleted file mode 100644 index ab06de033..000000000 --- a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.module.scss +++ /dev/null @@ -1,115 +0,0 @@ -.container { - position: absolute; - top: 50%; - inset-inline-start: 50%; - width: 570px; - height: 461px; - border-radius: 5px; - transform: translate(-50%, -50%); - - .deleteUserWrapper { - display: flex; - flex-direction: column; - height: 100%; - - .header { - display: flex; - height: 37px; - padding: 7px 0; - background-color: #202428; - border-radius: 5px 5px 0 0; - - &__button { - display: flex; - align-items: center; - margin-inline-start: 16px; - font-size: 1.6rem; - font-weight: 556; - color: #fefefe; - } - - &__icon { - width: 13px; - height: auto; - margin-inline-end: 5px; - margin-top: 1px; - fill: #fefefe; - } - } - - .body { - display: flex; - flex-direction: column; - align-items: center; - height: 100%; - background-color: #fefefe; - border-color: #202428; - border-style: solid; - border-width: 0 4px 4px; - border-radius: 0 0 5px 5px; - - &__blackExlintLogo { - width: 80px; - object-fit: contain; - margin-top: 25px; - } - - &__header { - margin-top: 15px; - font-size: 2.5rem; - font-weight: 457; - } - - &__details { - width: 70%; - margin-top: 17px; - font-size: 2.5rem; - font-weight: 600; - text-align: center; - } - - &__actionText { - margin-top: 25px; - font-size: 2.5rem; - font-weight: 600; - - &--redText { - color: #781d1d; - } - } - - &__input { - width: 50%; - padding: 15px; - margin-top: 15px; - font-size: 1.6rem; - font-weight: 600; - color: #4b4a65; - border: 2px solid #e7e7e7; - border-radius: 10px; - - &::placeholder { - font-size: 1.6rem; - font-weight: 600; - color: #4b4a65; - } - } - - &__button { - padding: 12px 20px; - margin: 12px 0 35px; - font-size: 1.7rem; - color: #fefefe; - background-color: #202428; - border: 2px solid #0e0d14; - border-radius: 10px; - - &:disabled { - color: #8b8b8b; - background-color: #e7e7e7; - border: 2px solid #bbb8ca; - } - } - } - } -} diff --git a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.tsx b/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.tsx deleted file mode 100644 index ad2c707f0..000000000 --- a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { connect } from 'react-redux'; -import type { PayloadAction } from '@reduxjs/toolkit'; -import { useTranslation } from 'react-i18next'; - -import { authActions } from '@/store/reducers/auth'; -import { backendApi } from '@/utils/http'; - -import UserSettingsModalView from './UserSettingsModal.view'; - -interface IPropsFromDispatch { - readonly setUnauthenticated: () => PayloadAction; -} - -interface IProps extends IPropsFromDispatch { - readonly onCloseModal: () => void; -} - -const UserSettingsModal: React.FC = (props: React.PropsWithChildren) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - - const [isConfirmButtonDisabledState, setIsConfirmButtonDisabledState] = useState(true); - - const onDeleteUser = () => { - backendApi.delete('/user/auth/delete').then(() => { - props.setUnauthenticated(); - - navigate('/auth'); - }); - }; - - const onDeleteUserInputChangeHandler = (input: string) => { - setIsConfirmButtonDisabledState(() => input !== t('userSettings.userSettingsModal.actionPhraseText')); - }; - - return ( - - ); -}; - -UserSettingsModal.displayName = 'UserSettingsModal'; -UserSettingsModal.defaultProps = {}; - -export default connect(null, { - setUnauthenticated: authActions.setUnauthenticated, -})(React.memo(UserSettingsModal)); diff --git a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.view.tsx b/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.view.tsx deleted file mode 100644 index 95bcf5cb4..000000000 --- a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/UserSettingsModal.view.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { useTranslation } from 'react-i18next'; - -import EDSvg from '@/ui/EDSvg'; -import EDBackdrop from '@/ui/EDBackdrop'; -import { concatClasses } from '@/utils/component'; -import blackExlint from '@/images/black-exlint-logo.png'; - -import classes from './UserSettingsModal.module.scss'; - -interface IProps { - readonly isConfirmButtonDisabled: boolean; - readonly onDeleteUser: () => void; - readonly onCloseModal: () => void; - readonly onDeleteUserInputChangeHandler: (_: string) => void; -} - -const UserSettingsModalView: React.FC = (props: React.PropsWithChildren) => { - const { t } = useTranslation(); - - const modalRoot = document.getElementById('overlay-root') as HTMLElement; - - return ( - <> - - {ReactDOM.createPortal( -
-
-
- -
-
- Exlint - - {t('userSettings.userSettingsModal.header')} - - - {t('userSettings.userSettingsModal.details')} - - - {t('userSettings.userSettingsModal.actionText')} -   - - {t('userSettings.userSettingsModal.actionPhraseText')} - - - - props.onDeleteUserInputChangeHandler(value) - } - /> - -
-
-
, - modalRoot, - )} - - ); -}; - -UserSettingsModalView.displayName = 'UserSettingsModalView'; -UserSettingsModalView.defaultProps = {}; - -export default React.memo(UserSettingsModalView); diff --git a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/index.ts b/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/index.ts deleted file mode 100644 index 1971bf852..000000000 --- a/apps/frontend/src/components/containers/UserSettings/UserSettingsModal/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import UserSettingsModal from './UserSettingsModal'; - -export default UserSettingsModal; diff --git a/apps/frontend/src/components/containers/UserSettings/index.ts b/apps/frontend/src/components/containers/UserSettings/index.ts deleted file mode 100644 index 48df754fd..000000000 --- a/apps/frontend/src/components/containers/UserSettings/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import UserSettings from './UserSettings'; - -export default UserSettings; diff --git a/apps/frontend/src/components/layout/Nav/Nav.module.scss b/apps/frontend/src/components/layout/Nav/Nav.module.scss new file mode 100644 index 000000000..9f2918cd6 --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/Nav.module.scss @@ -0,0 +1,47 @@ +@use 'sass:map'; + +@import '../../../styles/variables.scss'; + +.container { + display: flex; + justify-content: space-between; + height: 80px; + padding-inline: map.get($sizes, spacing-xxl); + background-image: linear-gradient( + 242.44deg, + #ae7dfd -30.7%, + map.get($colors, blacks-yankees-black-blue) 36.93%, + map.get($colors, blacks-yankees-black-blue) 62.82%, + map.get($colors, purples-purple) 129.61% + ); + + .linksContainer { + display: flex; + align-items: center; + + &__logo { + height: 50px; + object-fit: contain; + margin-inline-end: map.get($sizes, spacing-xxl-2); + } + } + + .documentationLink { + display: flex; + align-items: center; + text-decoration: none; + + &__text { + margin-inline-end: map.get($sizes, spacing-m); + font-size: 1.5rem; + font-weight: 600; + color: map.get($colors, greys-platinum); + } + + &__icon { + width: auto; + height: 16.1px; + fill: map.get($colors, greys-platinum); + } + } +} diff --git a/apps/frontend/src/components/layout/Nav/Nav.tsx b/apps/frontend/src/components/layout/Nav/Nav.tsx new file mode 100644 index 000000000..bcbdfadd0 --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/Nav.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import NavView from './Nav.view'; + +interface IProps {} + +const Nav: React.FC = () => { + return ; +}; + +Nav.displayName = 'Nav'; +Nav.defaultProps = {}; + +export default React.memo(Nav); diff --git a/apps/frontend/src/components/layout/Nav/Nav.view.tsx b/apps/frontend/src/components/layout/Nav/Nav.view.tsx new file mode 100644 index 000000000..9c9511ba0 --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/Nav.view.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + +import whiteBrandLogo from '@/images/white-brand-logo.png'; +import EDSvg from '@/ui/EDSvg'; + +import NavLink from './NavLink'; + +import classes from './Nav.module.scss'; + +interface IProps {} + +const NavView: React.FC = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +NavView.displayName = 'NavView'; +NavView.defaultProps = {}; + +export default React.memo(NavView); diff --git a/apps/frontend/src/components/layout/Nav/NavLink/NavLink.module.scss b/apps/frontend/src/components/layout/Nav/NavLink/NavLink.module.scss new file mode 100644 index 000000000..f4338ced4 --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/NavLink/NavLink.module.scss @@ -0,0 +1,31 @@ +@use 'sass:map'; + +@import '../../../../styles/variables.scss'; + +.container { + display: flex; + align-items: center; + padding-block: map.get($sizes, spacing-m); + text-decoration: none; + border-bottom: map.get($sizes, spacing-xs) solid transparent; + + &--active { + border-color: map.get($colors, greys-platinum); + } + + &:not(:last-child) { + margin-inline-end: map.get($sizes, spacing-xl); + } + + &__icon { + width: auto; + height: 26px; + margin-inline-end: map.get($sizes, spacing-m); + fill: map.get($colors, greys-platinum); + } + + &__text { + font-size: 1.5rem; + color: map.get($colors, greys-platinum); + } +} diff --git a/apps/frontend/src/components/layout/Nav/NavLink/NavLink.tsx b/apps/frontend/src/components/layout/Nav/NavLink/NavLink.tsx new file mode 100644 index 000000000..a703f42a7 --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/NavLink/NavLink.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import type icons from '../../../../assets/icons'; + +import NavLinkView from './NavLink.view'; + +interface IProps { + readonly route: string; + readonly iconName: keyof typeof icons; + readonly text: string; +} + +const NavLink: React.FC = (props: React.PropsWithChildren) => { + return ; +}; + +NavLink.displayName = 'NavLink'; +NavLink.defaultProps = {}; + +export default React.memo(NavLink); diff --git a/apps/frontend/src/components/layout/Nav/NavLink/NavLink.view.tsx b/apps/frontend/src/components/layout/Nav/NavLink/NavLink.view.tsx new file mode 100644 index 000000000..ccb9dbe8c --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/NavLink/NavLink.view.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +import EDSvg from '@/ui/EDSvg'; +import { concatClasses } from '@/utils/component'; + +import type icons from '../../../../assets/icons'; + +import classes from './NavLink.module.scss'; + +interface IProps { + readonly route: string; + readonly iconName: keyof typeof icons; + readonly text: string; +} + +const NavLinkView: React.FC = (props: React.PropsWithChildren) => { + return ( + + concatClasses(classes, 'container', isActive ? 'container--active' : null) + } + to={props.route} + > + + {props.text} + + ); +}; + +NavLinkView.displayName = 'NavLinkView'; +NavLinkView.defaultProps = {}; + +export default React.memo(NavLinkView); diff --git a/apps/frontend/src/components/layout/Nav/NavLink/index.ts b/apps/frontend/src/components/layout/Nav/NavLink/index.ts new file mode 100644 index 000000000..6dd54b54b --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/NavLink/index.ts @@ -0,0 +1,3 @@ +import NavLink from './NavLink'; + +export default NavLink; diff --git a/apps/frontend/src/components/layout/Nav/index.ts b/apps/frontend/src/components/layout/Nav/index.ts new file mode 100644 index 000000000..9ac514ab6 --- /dev/null +++ b/apps/frontend/src/components/layout/Nav/index.ts @@ -0,0 +1,3 @@ +import Nav from './Nav'; + +export default Nav; diff --git a/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.module.scss b/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.module.scss deleted file mode 100644 index 01f0bcbca..000000000 --- a/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.container { - display: flex; - min-width: 240px; - height: 100%; - background-color: #fefefe; - border-inline-end: 3px solid #e7e7e7; - - .sidebar { - display: flex; - flex-direction: column; - width: 100%; - - .title { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - padding: 21px 0; - font-size: 2.4rem; - font-weight: 600; - color: #4b4a65; - } - - .divider { - width: 100%; - border: none; - border-top: 2px solid #e7e7e7; - } - - .userDetalis { - display: flex; - flex-direction: column; - justify-content: center; - padding: 0 22px; - margin-top: 29px; - - .usernameContainer { - display: flex; - align-items: center; - - &__profileIcon { - width: 20px; - height: 29px; - margin-inline-end: 7px; - fill: #8b8b8b; - } - - &__usernameText { - font-size: 2rem; - font-weight: 600; - color: #8b8b8b; - } - - &__username { - margin-top: 7px; - font-size: 2.1rem; - font-weight: 457; - color: #8b8b8b; - } - } - } - } -} diff --git a/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.tsx b/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.tsx deleted file mode 100644 index 52632ea82..000000000 --- a/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; - -import type { AppState } from '@/store/app'; - -import SettingsSidebarView from './SettingsSidebar.view'; - -interface IPropsFromState { - readonly name: string; -} - -interface IProps extends IPropsFromState {} - -const SettingsSidebar: React.FC = (props: React.PropsWithChildren) => { - return ; -}; - -SettingsSidebar.displayName = 'SettingsSidebar'; -SettingsSidebar.defaultProps = {}; - -const mapStateToProps = (state: AppState) => { - return { - name: state.auth.name!, - }; -}; - -export default connect(mapStateToProps)(React.memo(SettingsSidebar)); diff --git a/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.view.tsx b/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.view.tsx deleted file mode 100644 index c759b19c4..000000000 --- a/apps/frontend/src/components/layout/SettingsSidebar/SettingsSidebar.view.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import EDSvg from '@/ui/EDSvg'; - -import classes from './SettingsSidebar.module.scss'; - -interface IProps { - readonly name: string; -} - -const SettingsSidebarView: React.FC = (props: React.PropsWithChildren) => { - const { t } = useTranslation(); - - return ( - - ); -}; - -SettingsSidebarView.displayName = 'SettingsSidebarView'; -SettingsSidebarView.defaultProps = {}; - -export default React.memo(SettingsSidebarView); diff --git a/apps/frontend/src/components/layout/SettingsSidebar/index.ts b/apps/frontend/src/components/layout/SettingsSidebar/index.ts deleted file mode 100644 index 9a7f16fa2..000000000 --- a/apps/frontend/src/components/layout/SettingsSidebar/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import SettingsSidebar from './SettingsSidebar'; - -export default SettingsSidebar; diff --git a/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.module.scss b/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.module.scss index 0ce4ea924..5b8ed2bd4 100644 --- a/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.module.scss +++ b/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.module.scss @@ -1,7 +1,12 @@ +@use 'sass:map'; + +@import '../../../styles/variables.scss'; + .container { position: fixed; top: 0; + z-index: 1; width: 100vw; height: 100vh; - background-color: #000000bf; + background-color: map.get($colors, greys-backdrop); } diff --git a/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.tsx b/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.tsx index d826e17a0..155f4feed 100644 --- a/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.tsx +++ b/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.tsx @@ -3,7 +3,7 @@ import React from 'react'; import EDBackdropView from './EDBackdrop.view'; interface IProps { - readonly onBackdropClick: () => void; + readonly onBackdropClick: VoidFunction; } const EDBackdrop: React.FC = (props: React.PropsWithChildren) => { diff --git a/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.view.tsx b/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.view.tsx index 31669bbcd..4f1f4c9a5 100644 --- a/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.view.tsx +++ b/apps/frontend/src/components/ui/EDBackdrop/EDBackdrop.view.tsx @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'; import classes from './EDBackdrop.module.scss'; interface IProps { - readonly onBackdropClick: () => void; + readonly onBackdropClick: VoidFunction; } const EDBackdropView: React.FC = (props: React.PropsWithChildren) => { diff --git a/apps/frontend/src/components/ui/EDNotification/EDNotification.module.scss b/apps/frontend/src/components/ui/EDNotification/EDNotification.module.scss new file mode 100644 index 000000000..838f0d958 --- /dev/null +++ b/apps/frontend/src/components/ui/EDNotification/EDNotification.module.scss @@ -0,0 +1,80 @@ +@use 'sass:map'; + +@import '../../../styles/variables.scss'; + +.container { + position: fixed; + inset-block-end: map.get($sizes, spacing-xxl); + inset-inline-end: map.get($sizes, spacing-xxl); + display: flex; + width: 258px; + padding: map.get($sizes, spacing-m) map.get($sizes, spacing-l); + border-inline-start-style: solid; + border-inline-start-width: 3px; + + &--info { + background-color: map.get($colors, whites-white); + border-inline-start-color: map.get($colors, greys-onyx); + } + + &--checkmark { + background-color: map.get($colors, greens-nyanza); + border-inline-start-color: map.get($colors, greens-japanese-laurel); + } + + &--warning { + background-color: map.get($colors, yellows-papaya-whip); + border-inline-start-color: map.get($colors, yellows-deep-lemon-30-p); + } + + &--error { + background-color: map.get($colors, reds-lavender-blush); + border-inline-start-color: map.get($colors, reds-persian-plum); + } + + &__icon { + width: auto; + height: 17.5px; + margin-inline-end: map.get($sizes, spacing-l); + + &--info { + stroke: map.get($colors, greys-onyx); + } + + &--checkmark { + fill: map.get($colors, greens-japanese-laurel); + } + + &--warning { + fill: map.get($colors, yellows-deep-lemon-30-p); + } + + &--error { + fill: map.get($colors, reds-persian-plum); + } + } + + &__closeIcon { + width: auto; + height: 8px; + cursor: pointer; + fill: map.get($colors, greys-onyx); + } +} + +.textContainer { + display: flex; + flex-direction: column; + + &__title { + margin-bottom: map.get($sizes, spacing-xs); + font-size: 1.5rem; + font-weight: 600; + color: map.get($colors, greys-onyx); + } + + &__message { + font-size: 1.5rem; + color: map.get($colors, greys-onyx); + } +} diff --git a/apps/frontend/src/components/ui/EDNotification/EDNotification.tsx b/apps/frontend/src/components/ui/EDNotification/EDNotification.tsx new file mode 100644 index 000000000..27092ba94 --- /dev/null +++ b/apps/frontend/src/components/ui/EDNotification/EDNotification.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { connect } from 'react-redux'; + +import type { NotificationType } from '@/store/interfaces/ui'; +import type { AppState } from '@/store/app'; +import { uiActions } from '@/store/reducers/ui'; + +import EDNotificationView from './EDNotification.view'; + +interface IPropsFromState { + readonly notificationType: NotificationType | null; + readonly notificationTitle: string | null; + readonly notificationMessage: string | null; +} + +interface IPropsFromDispatch { + readonly closeNotification: () => PayloadAction; +} + +interface IProps extends IPropsFromState, IPropsFromDispatch {} + +const EDNotification: React.FC = (props: React.PropsWithChildren) => { + return ( + + ); +}; + +EDNotification.displayName = 'EDNotification'; +EDNotification.defaultProps = {}; + +const mapStateToProps = (state: AppState) => { + return { + notificationType: state.ui.notificationType, + notificationTitle: state.ui.notificationTitle, + notificationMessage: state.ui.notificationMessage, + }; +}; + +export default connect(mapStateToProps, { + closeNotification: uiActions.closeNotification, +})(React.memo(EDNotification)); diff --git a/apps/frontend/src/components/ui/EDNotification/EDNotification.view.tsx b/apps/frontend/src/components/ui/EDNotification/EDNotification.view.tsx new file mode 100644 index 000000000..f6ecc2d50 --- /dev/null +++ b/apps/frontend/src/components/ui/EDNotification/EDNotification.view.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import { concatClasses } from '@/utils/component'; +import type { NotificationType } from '@/store/interfaces/ui'; + +import type icons from '../../../assets/icons'; +import EDSvg from '../EDSvg'; + +import classes from './EDNotification.module.scss'; + +interface IProps { + readonly notificationType: NotificationType | null; + readonly notificationTitle: string | null; + readonly notificationMessage: string | null; + readonly onCloseNotification: VoidFunction; +} + +const EDNotificationView: React.FC = (props: React.PropsWithChildren) => { + if (!props.notificationType || !props.notificationTitle || !props.notificationMessage) { + return null; + } + + let notificationIcon: keyof typeof icons; + + switch (props.notificationType) { + case 'info': + notificationIcon = 'notificationInfo'; + + break; + case 'checkmark': + notificationIcon = 'notificationCheckmark'; + + break; + case 'warning': + notificationIcon = 'notificationWarning'; + + break; + case 'error': + notificationIcon = 'notificationError'; + + break; + default: + notificationIcon = 'notificationInfo'; + } + + return ( +
+ +
+
{props.notificationTitle}
+ {props.notificationMessage} +
+ +
+ ); +}; + +EDNotificationView.displayName = 'EDNotificationView'; +EDNotificationView.defaultProps = {}; + +export default React.memo(EDNotificationView); diff --git a/apps/frontend/src/components/ui/EDNotification/index.ts b/apps/frontend/src/components/ui/EDNotification/index.ts new file mode 100644 index 000000000..e6ad357a1 --- /dev/null +++ b/apps/frontend/src/components/ui/EDNotification/index.ts @@ -0,0 +1,3 @@ +import EDNotification from './EDNotification'; + +export default EDNotification; diff --git a/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx b/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx index ee437de4f..c04cb5906 100644 --- a/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx +++ b/apps/frontend/src/components/ui/EDSelect/EDSelect.view.tsx @@ -12,7 +12,7 @@ interface IProps { readonly selectedOptionIndex: number | null; readonly tooltopRef: React.RefObject; readonly isTooltipVisible: boolean; - readonly toggleTooltipVisibility: () => void; + readonly toggleTooltipVisibility: VoidFunction; readonly onOptionSelect: (index: number) => void; } diff --git a/apps/frontend/src/components/ui/EDSvg/EDSvg.tsx b/apps/frontend/src/components/ui/EDSvg/EDSvg.tsx index c80d05997..f5f9e7973 100644 --- a/apps/frontend/src/components/ui/EDSvg/EDSvg.tsx +++ b/apps/frontend/src/components/ui/EDSvg/EDSvg.tsx @@ -8,7 +8,7 @@ interface IProps { readonly name: keyof typeof icons; readonly className?: string; readonly style?: CSSProperties; - readonly onClick?: () => void; + readonly onClick?: VoidFunction; } const EDSvg: React.FC = (props: React.PropsWithChildren) => { diff --git a/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx b/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx index 24cd82161..5caf8e457 100644 --- a/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx +++ b/apps/frontend/src/components/ui/EDSvg/EDSvg.view.tsx @@ -10,7 +10,7 @@ interface IProps { readonly name: keyof typeof icons; readonly className?: string; readonly style?: CSSProperties; - readonly onClick?: () => void; + readonly onClick?: VoidFunction; } const EDSvgView: React.FC = (props: React.PropsWithChildren) => { diff --git a/apps/frontend/src/hooks/backend-api.ts b/apps/frontend/src/hooks/backend-api.ts index 1996bae0a..692c99fc8 100644 --- a/apps/frontend/src/hooks/backend-api.ts +++ b/apps/frontend/src/hooks/backend-api.ts @@ -9,7 +9,7 @@ interface IHookResponse { response: AxiosResponse | null; error: AxiosError | null; loading: boolean; - request: () => Promise<() => void>; + request: () => Promise; } const backendInstance = axios.create({ diff --git a/apps/frontend/src/i18n/en.ts b/apps/frontend/src/i18n/en.ts index 213f76ef2..b6e5fece2 100644 --- a/apps/frontend/src/i18n/en.ts +++ b/apps/frontend/src/i18n/en.ts @@ -22,24 +22,38 @@ const en = { privacyPolicy: 'Privacy Policy', }, }, - userSettings: { - title: 'User Settings', - username: 'User', - deleteUserAction: 'Delete user', - logoutAction: 'Log-out', - userSettingsModal: { - cancelButton: 'Cancel', - header: 'You are about to delete your user:', - details: 'All saved data, groups, policies and configurations will be lost', - actionText: 'To confirm, type', - actionPhraseText: 'DELETE-USER', - inputPlaceholder: 'Type here', - confirmButton: 'Confirm', + accountSettings: { + header: { + routeText: 'Account Settings', + clientId: 'Client ID', + userCreated: 'User Created', + }, + sideBar: { + account: 'Account', + tokenManagement: 'Token Management', + }, + account: { + signOutHeader: 'Sign Out', + signOutSubHeader: 'Sign out of your account.', + signOutButton: 'Sign out', + deleteAccountHeader: 'Delete Account', + deleteAccountSubHeader: + 'Once you delete your account, there is no going back. Please be certain.', + deleteAccountButton: 'Delete your account', + deleteModal: { + cancel: 'Cancel', + header: 'You are about to delete your user:', + subHeader: 'All saved data, groups, policies and configurations will be lost', + actionText: 'To confirm, type', + inputPlaceholder: 'Type here', + actionPhraseText: 'DELETE-USER', + confirmButton: 'Confirm', + }, + }, + copyNotification: { + title: 'ID Copied to clipboard', + message: 'Paste it wherever you want.', }, - }, - settingsSidebar: { - title: 'Settings', - username: 'Username', }, cliAuth: { header: 'Authenticate for CLI', @@ -65,6 +79,11 @@ const en = { textPrefix: "If you believe this shouldn't have happened, please contact us at", }, }, + nav: { + groupCenter: 'Group Center', + accountSettings: 'Account Settings', + documentation: 'Documentation', + }, }; export default en; diff --git a/apps/frontend/src/interfaces/responses.ts b/apps/frontend/src/interfaces/responses.ts index 0628f302a..9cdbc0450 100644 --- a/apps/frontend/src/interfaces/responses.ts +++ b/apps/frontend/src/interfaces/responses.ts @@ -2,6 +2,7 @@ export interface IAutoAuthResponseData { readonly accessToken: string; readonly id: string; readonly name: string; + readonly createdAt: number; } export interface IRefreshTokenResponseData { diff --git a/apps/frontend/src/pages/AccountSettings.tsx b/apps/frontend/src/pages/AccountSettings.tsx new file mode 100644 index 000000000..330c4a4b4 --- /dev/null +++ b/apps/frontend/src/pages/AccountSettings.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import AccountSettings from '@/containers/AccountSettings'; + +interface IProps {} + +const AccountSettingsPage: React.FC = () => { + return ; +}; + +AccountSettingsPage.displayName = 'AccountSettingsPage'; +AccountSettingsPage.defaultProps = {}; + +export default AccountSettingsPage; diff --git a/apps/frontend/src/pages/UserSettings.tsx b/apps/frontend/src/pages/UserSettings.tsx deleted file mode 100644 index 65d354c7e..000000000 --- a/apps/frontend/src/pages/UserSettings.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import UserSettings from '@/containers/UserSettings'; - -interface IProps {} - -const UserSettingsPage: React.FC = () => { - return ; -}; - -UserSettingsPage.displayName = 'UserSettingsPage'; -UserSettingsPage.defaultProps = {}; - -export default UserSettingsPage; diff --git a/apps/frontend/src/store/app.ts b/apps/frontend/src/store/app.ts index 7ff040c92..d1a74e216 100644 --- a/apps/frontend/src/store/app.ts +++ b/apps/frontend/src/store/app.ts @@ -1,13 +1,17 @@ import { configureStore } from '@reduxjs/toolkit'; import authReducer from './reducers/auth'; +import uiReducer from './reducers/ui'; import authListenMiddleware from './middlewares/auth'; +import uiListenMiddleware from './middlewares/ui'; const store = configureStore({ reducer: { auth: authReducer, + ui: uiReducer, }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authListenMiddleware.middleware), + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(authListenMiddleware.middleware, uiListenMiddleware.middleware), devTools: process.env.REACT_APP_NODE_ENV === 'development', }); diff --git a/apps/frontend/src/store/interfaces/auth.ts b/apps/frontend/src/store/interfaces/auth.ts index 0d4f2016f..00c211a83 100644 --- a/apps/frontend/src/store/interfaces/auth.ts +++ b/apps/frontend/src/store/interfaces/auth.ts @@ -2,9 +2,11 @@ export interface IAuthState { isAuthenticated: boolean | null; id: string | null; name: string | null; + createdAt: number | null; } export interface IAuthPayload { id: string; name: string; + createdAt: number; } diff --git a/apps/frontend/src/store/interfaces/ui.ts b/apps/frontend/src/store/interfaces/ui.ts new file mode 100644 index 000000000..6ebb9b239 --- /dev/null +++ b/apps/frontend/src/store/interfaces/ui.ts @@ -0,0 +1,13 @@ +export type NotificationType = 'info' | 'checkmark' | 'warning' | 'error'; + +export interface IUiState { + notificationType: NotificationType | null; + notificationTitle: string | null; + notificationMessage: string | null; +} + +export interface IUiShowNotificationPayload { + notificationType: NotificationType; + notificationTitle: string; + notificationMessage: string; +} diff --git a/apps/frontend/src/store/middlewares/ui.ts b/apps/frontend/src/store/middlewares/ui.ts new file mode 100644 index 000000000..4b040032f --- /dev/null +++ b/apps/frontend/src/store/middlewares/ui.ts @@ -0,0 +1,26 @@ +import { clearAllListeners, createListenerMiddleware } from '@reduxjs/toolkit'; + +import { uiActions } from '../reducers/ui'; +import { NOTIFICATION_SHOW_TIMEOUT } from '../models/ui'; + +const listenerMiddleware = createListenerMiddleware(); + +listenerMiddleware.startListening({ + actionCreator: uiActions.closeNotification, + effect: (_, listnerApi) => { + listnerApi.dispatch(clearAllListeners()); + }, +}); + +listenerMiddleware.startListening({ + actionCreator: uiActions.showNotification, + effect: async (_, listnerApi) => { + listnerApi.dispatch(clearAllListeners()); + + await listnerApi.delay(NOTIFICATION_SHOW_TIMEOUT); + + listnerApi.dispatch(uiActions.closeNotification()); + }, +}); + +export default listenerMiddleware; diff --git a/apps/frontend/src/store/models/ui.ts b/apps/frontend/src/store/models/ui.ts new file mode 100644 index 000000000..6ad015856 --- /dev/null +++ b/apps/frontend/src/store/models/ui.ts @@ -0,0 +1,3 @@ +const SECOND = 1000; + +export const NOTIFICATION_SHOW_TIMEOUT = 8 * SECOND; diff --git a/apps/frontend/src/store/reducers/auth.ts b/apps/frontend/src/store/reducers/auth.ts index 8518657f1..6f06ef53b 100644 --- a/apps/frontend/src/store/reducers/auth.ts +++ b/apps/frontend/src/store/reducers/auth.ts @@ -6,6 +6,7 @@ const initialState: IAuthState = { isAuthenticated: null, id: null, name: null, + createdAt: null, }; const authSlice = createSlice({ @@ -16,11 +17,13 @@ const authSlice = createSlice({ state.isAuthenticated = true; state.id = action.payload.id; state.name = action.payload.name; + state.createdAt = action.payload.createdAt; }, setUnauthenticated(state) { state.isAuthenticated = false; state.id = null; state.name = null; + state.createdAt = null; }, }, }); diff --git a/apps/frontend/src/store/reducers/ui.ts b/apps/frontend/src/store/reducers/ui.ts new file mode 100644 index 000000000..97a743971 --- /dev/null +++ b/apps/frontend/src/store/reducers/ui.ts @@ -0,0 +1,30 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; + +import type { IUiState, IUiShowNotificationPayload } from '../interfaces/ui'; + +const initialState: IUiState = { + notificationType: null, + notificationTitle: null, + notificationMessage: null, +}; + +const uiSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + showNotification(state, action: PayloadAction) { + state.notificationType = action.payload.notificationType; + state.notificationTitle = action.payload.notificationTitle; + state.notificationMessage = action.payload.notificationMessage; + }, + closeNotification(state) { + state.notificationType = null; + state.notificationTitle = null; + state.notificationMessage = null; + }, + }, +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice.reducer; diff --git a/apps/frontend/src/styles/custom.scss b/apps/frontend/src/styles/custom.scss index c285a7d29..59066ad27 100644 --- a/apps/frontend/src/styles/custom.scss +++ b/apps/frontend/src/styles/custom.scss @@ -1,6 +1,10 @@ +@use 'sass:map'; + @import 'https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap'; @import 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'; +@import './variables.scss'; + * { box-sizing: border-box; } @@ -8,7 +12,7 @@ html { width: 100%; height: 100%; - font-size: 10px; + font-size: map.get($sizes, root-font-size); } body { @@ -19,6 +23,7 @@ body { direction: ltr; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: map.get($colors, whites-white); } main { @@ -103,7 +108,7 @@ textarea { } mark { - padding: 4px; + padding: map.get($sizes, spacing-s); } menu { diff --git a/apps/frontend/src/styles/variables.scss b/apps/frontend/src/styles/variables.scss index 53b94259c..e7a2ce34e 100644 --- a/apps/frontend/src/styles/variables.scss +++ b/apps/frontend/src/styles/variables.scss @@ -1,4 +1,5 @@ $sizes: ( + root-font-size: 10px, spacing-xxs: 1px, spacing-xs: 2px, spacing-s: 4px, @@ -8,6 +9,7 @@ $sizes: ( spacing-xxl: 32px, spacing-xxl-2: 52px, spacing-xxl-3: 64px, + spacing-xxl-4: 128px, ); $colors: ( greens-japanese-laurel: #2f7d2d, @@ -35,4 +37,11 @@ $colors: ( blacks-dark-gunmetal: #202428, greens-nyanza: #e6ffdd, blues-light-cyan: #d7faff, + greys-backdrop: #16161680, + greens-sea-green: #24a1484d, + reds-lavender-blush: #fff1f1, + reds-maximum-red: #da1e284d, + yellows-deep-lemon: #f1c21b, + yellows-deep-lemon-30-p: #f1c21b4d, + yellows-papaya-whip: #fcf4d6, ); diff --git a/apps/frontend/stylelint.config.cjs b/apps/frontend/stylelint.config.cjs index 01bc07dee..7a4094967 100644 --- a/apps/frontend/stylelint.config.cjs +++ b/apps/frontend/stylelint.config.cjs @@ -20,8 +20,8 @@ module.exports = { 'scss/at-import-partial-extension': null, 'scale-unlimited/declaration-strict-value': [ - ['/color/', '/padding/', '/top/', '/bottom/', '/margin/', 'font-size'], - { ignoreVariables: false, ignoreValues: ['transparent', '/rem/'] }, + ['/color/', '/padding/', 'top', 'bottom', '/margin/', 'font-size', 'fill'], + { ignoreVariables: false, ignoreValues: ['transparent', '/rem/', '0'] }, ], }, };