Skip to content

Commit

Permalink
Add frontend for user cabinet
Browse files Browse the repository at this point in the history
  • Loading branch information
kirill-stupakov committed Apr 22, 2024
1 parent 9cc8ee4 commit f9eb756
Show file tree
Hide file tree
Showing 53 changed files with 1,383 additions and 0 deletions.
9 changes: 9 additions & 0 deletions web-app/client/src/graphql/operations/mutations/updateUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';

export const UPDATE_USER = gql`
mutation updateUser($props: UpdatingUserProps!) {
updateUser(props: $props) {
message
}
}
`;
34 changes: 34 additions & 0 deletions web-app/client/src/graphql/operations/queries/getOwnDatasets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { gql } from '@apollo/client';

export const GET_OWN_DATASETS = gql`
query getOwnDatasets($props: DatasetsQueryProps!) {
user {
datasets(props: $props) {
total
data {
fileID
fileName
hasHeader
delimiter
supportedPrimitives
rowsCount
fileSize
fileFormat {
inputFormat
tidColumnIndex
itemColumnIndex
hasTid
}
user {
fullName
}
countOfColumns
isBuiltIn
createdAt
originalFileName
numberOfUses
}
}
}
}
`;
40 changes: 40 additions & 0 deletions web-app/client/src/graphql/operations/queries/getOwnTasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { gql } from '@apollo/client';

export const GET_OWN_TASKS = gql`
query getOwnTasks($props: TasksQueryProps!) {
user {
tasks(props: $props) {
total
data {
taskID
state {
... on TaskState {
user {
fullName
}
processStatus
phaseName
currentPhase
progress
maxPhase
isExecuted
elapsedTime
createdAt
}
}
data {
baseConfig {
algorithmName
type
}
}
dataset {
originalFileName
}
}
}
}
}
`;
42 changes: 42 additions & 0 deletions web-app/client/src/pages/me/[tab].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRouter } from 'next/router';
import UserIcon from '@assets/icons/user.svg?component';
import TabsLayout from '@components/TabsLayout';
import { useAuthContext } from '@hooks/useAuthContext';
import styles from '@styles/Me.module.scss';
import tabs from 'src/routes/UserCabinet/tabs';
import { NextPageWithLayout } from 'types/pageWithLayout';

const AdminPanel: NextPageWithLayout = () => {
const router = useRouter();
const { user } = useAuthContext();

const currentTab =
tabs.find((tab) => router.query.tab === tab.pathname) ?? tabs[0];

const Component = currentTab.component;

return (
<TabsLayout
tabs={tabs}
selectedTab={currentTab.pathname}
onTabSelect={(pathname) => router.push(`/me/${pathname}`)}
beforeTabs={
<div className={styles.beforeMenu}>
<div className={styles.iconContainer}>
<div className={styles.iconBackground}>
<UserIcon className={styles.icon} />
</div>
</div>
<div className={styles.nameContainer}>
<h5>{user?.name}</h5>
<small>{user?.email}</small>
</div>
</div>
}
>
<Component />
</TabsLayout>
);
};

export default AdminPanel;
17 changes: 17 additions & 0 deletions web-app/client/src/pages/me/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GetServerSideProps, NextPage } from 'next';
import tabs from 'src/routes/UserCabinet/tabs';

const UserCabinet: NextPage = () => {
return null;
};

export const getServerSideProps: GetServerSideProps = async () => {
return {
redirect: {
destination: `/me/${tabs[0].pathname}`,
permanent: true,
},
};
};

export default UserCabinet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import "styles/common";

.accountSettingsTab {
max-width: 464px;

.title {
margin: 0 0 32px;
}

.additionalActions {
display: flex;
gap: 16px;

button {
width: 100%;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useQuery } from '@apollo/client';
import { FC, useState } from 'react';
import Button from '@components/Button';
import { getUser } from '@graphql/operations/queries/__generated__/getUser';
import { GET_USER } from '@graphql/operations/queries/getUser';
import ChangePasswordModal from './components/ChangePasswordModal';
import ProfileForm from './components/ProfileForm';
import styles from './AccountSettings.module.scss';

const AccountSettings: FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { data } = useQuery<getUser>(GET_USER, {
variables: {
userID: undefined,
},
});

const user = data?.user;

if (!user) {
return null;
}

return (
<div className={styles.accountSettingsTab}>
<h5 className={styles.title}>Account Settings</h5>
<ProfileForm user={user} />
<div className={styles.additionalActions}>
<Button variant="secondary" onClick={() => setIsModalOpen(true)}>
Change Password
</Button>
<Button variant="secondary-danger" disabled>
Delete Account
</Button>
</div>
{isModalOpen && (
<ChangePasswordModal onClose={() => setIsModalOpen(false)} />
)}
</div>
);
};

export default AccountSettings;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import "styles/common";

.changePasswordModal {
.title {
text-align: center;
margin: 0 0 32px;
}

form {
display: flex;
flex-direction: column;
gap: 24px;
margin: 0 0 32px;
}

.actions {
display: flex;
justify-content: center;
gap: 16px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useMutation } from '@apollo/client';
import { FC, useId } from 'react';
import { useForm } from 'react-hook-form';
import isStrongPassword from 'validator/lib/isStrongPassword';
import Button from '@components/Button';
import { Text } from '@components/Inputs';
import ModalContainer, { ModalProps } from '@components/ModalContainer';
import {
changePassword,
changePasswordVariables,
} from '@graphql/operations/mutations/__generated__/changePassword';
import { CHANGE_PASSWORD } from '@graphql/operations/mutations/changePassword';
import hashPassword from '@utils/hashPassword';
import styles from './ChangePasswordModal.module.scss';

type Inputs = {
oldPassword: string;
newPassword: string;
repeatPassword: string;
};

const ChangePasswordModal: FC<ModalProps> = ({ onClose }) => {
const {
register,
formState: { errors },
handleSubmit,
} = useForm<Inputs>();
const formId = useId();
const [changePassword] = useMutation<changePassword, changePasswordVariables>(
CHANGE_PASSWORD,
);

const onSubmit = handleSubmit(async (values) => {
try {
await changePassword({
variables: {
currentPwdHash: hashPassword(values.oldPassword),
newPwdHash: hashPassword(values.newPassword),
},
});
location.reload();
} catch (e) {}
});

return (
<ModalContainer onClose={onClose} className={styles.changePasswordModal}>
<h4 className={styles.title}>Change password</h4>
<form onSubmit={onSubmit} id={formId}>
<Text
label="Old password"
type="password"
placeholder="admin1234"
{...register('oldPassword', {
required: 'Required',
})}
error={errors.oldPassword?.message}
/>
<Text
label="New password"
type="password"
placeholder="admin1234"
{...register('newPassword', {
required: 'Required',
validate: (value) => isStrongPassword(value) || 'Weak password',
})}
error={errors.newPassword?.message}
/>
<Text
label="Repeat password"
type="password"
placeholder="admin1234"
{...register('repeatPassword', {
required: 'Required',
validate: {
isStrong: (value) => isStrongPassword(value) || 'Weak password',
isSame: (value, { newPassword }) =>
value === newPassword || 'Passwords do not match',
},
})}
error={errors.repeatPassword?.message}
/>
</form>
<div className={styles.actions}>
<Button variant="secondary">Cancel</Button>
<Button type="submit" form={formId}>
Update password
</Button>
</div>
</ModalContainer>
);
};

export default ChangePasswordModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ChangePasswordModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import "styles/common";

.profileForm {
display: flex;
flex-direction: column;
gap: 24px;
margin: 0 0 16px;
}
Loading

0 comments on commit f9eb756

Please sign in to comment.