Skip to content

Commit

Permalink
Merge pull request #4 from brunomous/feature/user-profile
Browse files Browse the repository at this point in the history
Implement User Profile workflow
  • Loading branch information
brunomous authored Feb 13, 2025
2 parents ecb8b0b + fd3c9c3 commit 3b660d8
Show file tree
Hide file tree
Showing 10 changed files with 18,080 additions and 19 deletions.
17,671 changes: 17,671 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/frontend/cypress/e2e/auth/sign-in.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ describe("Sign In", () => {
cy.get("#root_username").type(Cypress.env("signInEmail"));
cy.get("#root_password").type(Cypress.env("signInPassword"));
cy.contains("Send").click();
cy.url().should("include", "/settings");
cy.url().should("include", "/users");
});
});
20 changes: 10 additions & 10 deletions packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { useNavigate } from "react-router-dom";
import { RocketsProvider, createConfig } from "@concepta/react-material-ui";
import { Router, Resource, ChildRoutes } from "@concepta/react-navigation";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import PersonOutlinedIcon from "@mui/icons-material/PersonOutline";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

import UsersScreen from "./pages/Users";
import SettingsScreen from "./pages/Settings";
import ProfileScreen from "./pages/Profile";
import {
signInProps,
forgotPasswordProps,
Expand Down Expand Up @@ -50,19 +50,19 @@ const Routes = () => {
logo: Logo,
}}
>
<Resource
id="/settings"
name="Settings"
icon={<SettingsOutlinedIcon />}
page={<SettingsScreen />}
/>

<Resource
id="/users"
name="Users"
icon={<PeopleAltOutlinedIcon />}
page={<UsersScreen />}
/>

<Resource
id="/profile"
name="Profile"
icon={<PersonOutlinedIcon />}
page={<ProfileScreen />}
/>
</ChildRoutes>
);
};
Expand Down Expand Up @@ -115,7 +115,7 @@ const App = () => (
<Router
rootElement={<AdminProvider />}
childRoutes={<Routes />}
initialRoute="/settings"
initialRoute="/users"
/>
);

Expand Down
80 changes: 80 additions & 0 deletions packages/frontend/src/pages/Profile/ChangePasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { FC, useState } from "react";
import { SchemaForm } from "@concepta/react-material-ui";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import useTheme from "@mui/material/styles/useTheme";

import { type PasswordChangeFormData } from "./types";
import {
passwordChangeFormSchema,
passwordChangeUiSchema,
customValidate,
} from "./constants";

interface ChangePasswordFormProps {
closePasswordChangeModal: () => void;
openConfirmationModal: () => void;
}

const ChangePasswordForm: FC<ChangePasswordFormProps> = ({
closePasswordChangeModal,
openConfirmationModal,
}) => {
const theme = useTheme();

const [formData, setFormData] = useState<PasswordChangeFormData>({
oldPassword: "",
newPassword: "",
confirmNewPassword: "",
});

const handleFormSubmit = async () => {
closePasswordChangeModal();
openConfirmationModal();
};

return (
<SchemaForm.Form
schema={passwordChangeFormSchema}
uiSchema={passwordChangeUiSchema}
onSubmit={handleFormSubmit}
noHtml5Validate={true}
showErrorList={false}
customValidate={customValidate}
formData={formData}
onChange={({ formData }) => {
setFormData(formData);
}}
>
<Box
display="flex"
flexDirection="row"
alignItems="center"
justifyContent="flex-end"
mt={4}
gap={2}
>
<Button
variant="outlined"
sx={{
color: theme.palette.text.primary,
borderColor: theme.palette.text.primary,
textTransform: "capitalize",
}}
onClick={() => closePasswordChangeModal()}
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
sx={{ textTransform: "capitalize" }}
>
Save
</Button>
</Box>
</SchemaForm.Form>
);
};

export default ChangePasswordForm;
43 changes: 43 additions & 0 deletions packages/frontend/src/pages/Profile/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FC } from "react";
import { Text } from "@concepta/react-material-ui";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import CheckIcon from "@mui/icons-material/Check";

interface Props {
handleClose: () => void;
}

const ConfirmationModal: FC<Props> = ({ handleClose }) => {
return (
<Box display="flex" flexDirection="column" alignItems="center" padding={2}>
<Box
display="flex"
sx={{
backgroundColor: "#d1fae5",
padding: "12px",
borderRadius: "50%",
mb: 2,
}}
>
<CheckIcon fontSize="small" />
</Box>
<Text fontSize={18} fontWeight={500} color="text.primary">
Password Changed
</Text>
<Text
fontSize={14}
fontWeight={400}
color="text.secondary"
sx={{ mb: 3 }}
>
Your Password has been changed successfully
</Text>
<Button variant="contained" onClick={handleClose} fullWidth>
Go back to profile
</Button>
</Box>
);
};

export default ConfirmationModal;
67 changes: 67 additions & 0 deletions packages/frontend/src/pages/Profile/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { UiSchema, RJSFSchema, CustomValidator } from "@rjsf/utils";
import { CustomTextFieldWidget } from "@concepta/react-material-ui/dist/styles/CustomWidgets";
import { CustomPasswordFieldWidget } from "@concepta/react-material-ui/dist/styles/CustomWidgets";
import { validatePasswordScore } from "@concepta/react-material-ui/dist/components/TextField/utils";

type PasswordUpdateForm = {
oldPassword: string;
newPassword: string;
confirmNewPassword: string;
};

export const widgets = {
TextWidget: CustomTextFieldWidget,
};

export const profileFormSchema: RJSFSchema = {
type: "object",
required: ["fullName", "nickname", "email"],
properties: {
fullName: { type: "string", title: "Full Name", minLength: 3 },
nickname: { type: "string", title: "Nickname", minLength: 3 },
email: { type: "string", title: "Email", minLength: 3, readOnly: true },
},
};

export const passwordChangeFormSchema: RJSFSchema = {
type: "object",
required: ["oldPassword", "newPassword", "confirmNewPassword"],
properties: {
oldPassword: { type: "string", title: "Current password" },
newPassword: { type: "string", title: "New password" },
confirmNewPassword: { type: "string", title: "Confirm new password" },
},
};

export const passwordChangeUiSchema: UiSchema = {
oldPassword: {
"ui:widget": CustomPasswordFieldWidget,
},
newPassword: {
"ui:widget": CustomPasswordFieldWidget,
"ui:passwordStrengthConfig": {
hideStrengthBar: false,
hideRulesText: false,
},
},
confirmNewPassword: {
"ui:widget": CustomPasswordFieldWidget,
},
};

export const customValidate: CustomValidator<
PasswordUpdateForm,
RJSFSchema,
Record<string, unknown>
> = (formData, errors) => {
if (formData?.confirmNewPassword !== formData?.newPassword) {
errors?.confirmNewPassword?.addError("Your passwords don't match.");
}
if (!validatePasswordScore(formData?.newPassword ?? "")) {
errors?.newPassword?.addError(
"Your password do not meet the security criteria."
);
}

return errors;
};
Loading

0 comments on commit 3b660d8

Please sign in to comment.