diff --git a/package-lock.json b/package-lock.json index 3ef4561e..dff86281 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", + "@hookform/resolvers": "^5.0.1", "@reduxjs/toolkit": "^1.9.5", "@tanstack/react-table": "^8.9.1", "@testing-library/jest-dom": "^5.16.5", @@ -24,7 +25,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -33,12 +34,13 @@ "react-chartjs-2": "^5.2.0", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.56.0", "react-i18next": "^14.1.0", "react-icons": "^4.9.0", "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -2531,6 +2533,18 @@ "react": ">=16.3" } }, + "node_modules/@hookform/resolvers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz", + "integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3045,6 +3059,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3369,6 +3389,12 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -5760,9 +5786,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -13944,6 +13977,22 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "node_modules/react-hook-form": { + "version": "7.56.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.0.tgz", + "integrity": "sha512-U2QQgx5z2Y8Z0qlXv3W19hWHJgfKdWMz0O/osuY+o+CYq568V2R/JhzC6OAXfR8k24rIN0Muan2Qliaq9eKs/g==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-i18next": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz", @@ -16603,6 +16652,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/package.json b/package.json index a1ed9d64..2a200b18 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", + "@hookform/resolvers": "^5.0.1", "@reduxjs/toolkit": "^1.9.5", "@tanstack/react-table": "^8.9.1", "@testing-library/jest-dom": "^5.16.5", @@ -20,7 +21,6 @@ "axios": "^1.4.0", "bootstrap": "^5.3.3", "chart.js": "^4.1.1", - "recharts": "^2.0.0", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -29,11 +29,13 @@ "react-chartjs-2": "^5.2.0", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.56.0", "react-i18next": "^14.1.0", "react-icons": "^4.9.0", "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", diff --git a/src/pages/Profile/Edit.tsx b/src/pages/Profile/Edit.tsx index db20f9dc..b7c7e838 100644 --- a/src/pages/Profile/Edit.tsx +++ b/src/pages/Profile/Edit.tsx @@ -1,185 +1,308 @@ -import React from 'react'; -import { Formik, Form, Field, ErrorMessage } from 'formik'; +import React, {useState, useEffect} from "react"; import * as Yup from 'yup'; -import './Edit.css'; // Importing custom CSS styles -import { Button } from 'react-bootstrap'; // Importing Button component from react-bootstrap +import './Edit.css'; +import { Button, Form } from 'react-bootstrap'; +import axios, { AxiosError } from 'axios'; +import { alertActions } from "../../store/slices/alertSlice"; +import { useDispatch } from "react-redux"; +import { useForm, Controller } from "react-hook-form"; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useSelector } from "react-redux"; +import { RootState } from "../../store/store"; +// import { Formik, Form, Field, ErrorMessage } from 'formik'; + // Define initial form values and validation schema using Yup const Edit: React.FC = () => { const initialValues = { - fullName: 'Admin', + full_name: '', password: '', confirmPassword: '', email: '', - institution: 'Other', - actionPreference: 'cannotShowActions', + institution: { + id: 0, + name: 'Other' + }, + can_show_actions: 'cannotShowActions', handle: '', - timeZone: 'GMT-05:00', + time_zone: 'GMT-05:00', language: 'No Preference', - emailOptions: { - reviewNotification: true, - submissionNotification: true, - metaReviewNotification: true, - }, + email_on_review: true, + email_on_submission: true, + email_on_review_of_review: true, }; const validationSchema = Yup.object().shape({ - fullName: Yup.string().required('Full name is required'), - password: Yup.string().required('Password is required'), - confirmPassword: Yup.string() - .oneOf([Yup.ref('password')], 'Passwords must match') - .required('Confirm Password is required'), + full_name: Yup.string().required('Full name is required'), + password: Yup.string(), + confirmPassword: Yup.string().oneOf([Yup.ref('password')], 'Passwords must match'), email: Yup.string().email('Invalid email address').required('Email is required'), handle: Yup.string().required('Handle is required'), + institution: Yup.object().shape({ + id: Yup.number().required('Institution ID is required'), + name: Yup.string().required('Institution name is required'), + }), + time_zone: Yup.string().required(), + language: Yup.string().required(), + email_on_review: Yup.boolean(), + email_on_submission: Yup.boolean(), + email_on_review_of_review: Yup.boolean(), + can_show_actions: Yup.string().required(), }); - // Handle form submission - const handleSubmit = (values: any, { setSubmitting }: any) => { - setTimeout(() => { - alert(JSON.stringify(values, null, 2)); // Display form values as JSON - setSubmitting(false); - }, 400); + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); + + const dispatch = useDispatch(); + const [institutions, setInstitutions] = useState<{ id: number; name: string }[]>([]); + + const { + register, + handleSubmit, + reset, + control, + formState: {errors}, + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues: initialValues, + }) + + // Fetch user profile + useEffect(() => { + axios.get(`http://localhost:3002/api/v1/users/${auth.user.id}/get_profile`, { + headers: { + Authorization: `Bearer ${auth.authToken}` + } + }) + .then((res) => { + // Normalize data to fit form structure + const data = { + full_name: res.data.full_name, + email: res.data.email, + password: '', + confirmPassword: '', + institution: { + id: res.data.institution?.id || 0, + name: res.data.institution?.name || 'Other', + }, + handle: res.data.handle || '', + can_show_actions: res.data.can_show_actions ? 'canShowActions' : 'cannotShowActions', + time_zone: res.data.time_zone || 'GMT-05:00', + language: res.data.language || 'No Preference', + email_on_review: res.data.email_on_review ?? true, + email_on_submission: res.data.email_on_submission ?? true, + email_on_review_of_review: res.data.email_on_review_of_review ?? true, + }; + console.log(res.data); + console.log(data); + reset(data); + }) + .catch((error) => { + dispatch(alertActions.showAlert({ + variant: "danger", + message: "Failed to fetch user profile.", + })); + }); + }, [auth.user.id, reset, dispatch]); + + useEffect(() => { + axios.get(`http://localhost:3002/api/v1/institutions`, { + headers: { + Authorization: `Bearer ${auth.authToken}` + } + }) + .then(res => { + const names = res.data.map((institution: any) => ({id: institution.id, name: institution.name})); + setInstitutions([{id: 0, name: 'Other'}, ...names]); + console.log(institutions); + }) + .catch(() => { + dispatch(alertActions.showAlert({ + variant: "danger", + message: "Failed to load institutions list.", + })); + }); + }, [auth.authToken, dispatch]); + + const onSubmit = async (data: any) => { + try { + // Update profile + await axios.patch(`http://localhost:3002/api/v1/users/${auth.user.id}`, data, { + headers: { + Authorization: `Bearer ${auth.authToken}` + }, + }); + + // Update password if provided + if (data.password) { + await axios.post(`http://localhost:3002/api/v1/users/${auth.user.id}/update_password`, { + password: data.password, + confirmPassword: data.confirmPassword, + },{ + headers: { + Authorization: `Bearer ${auth.authToken}` + }}); + } + dispatch(alertActions.showAlert({ + variant: "success", + message: "Profile updated successfully!", + })); + } catch (error) { + if (error instanceof AxiosError && error.response?.data?.error) { + dispatch(alertActions.showAlert({ + variant: "danger", + message: error.response.data.error, + })); + } + } }; return ( -
{/* Container for the entire form */} -

User Profile Information

{/* Heading for user profile */} - - {({ isSubmitting }) => ( // Formik render prop function -
{/* Form component */} - {/* Form fields with labels, inputs, and error messages */} -
- - - -
+
+

User Profile Information

+ {/* Form Component */} - {/* Password and Confirm Password fields with validation */} -
- - - -
+ {/* Full Name */} +
+ + +

{errors.full_name?.message}

+
-
- - - -
+ {/* Passwords */} +
+ + +
- {/* Note regarding password field */} -
-

If password field is blank, the password will not be updated

-
+
+ + +

{errors.confirmPassword?.message}

+
- {/* Email field */} -
- - - -
+ {/* Note regarding password field */} +
+

If password field is blank, the password will not be updated

+
- {/* Institution field */} -
- - - - - - - - -
+ {/* Email field */} +
+ + +

{errors.email?.message}

+
+ + {/* Institution field */} +
+ + ( + + {institutions.map((institution) => ( + + ))} + + )} + /> +

{errors.institution?.id?.message}

+
- {/* Action Preference radio buttons */} -
- -
- - -
- + {/* Action Preference radio buttons */} +
+ +
+ +
+

{errors.can_show_actions?.message}

+
-
{/* Horizontal rule for visual separation */} +
{/* Horizontal rule for visual separation */} - {/* Handle field with instructions */} -
- -
A "handle" can be used to conceal your username from people who view your wiki pages. If you have a handle, your wiki account should be named after your handle instead of after your user-ID. If you do not have a handle, your Expertiza user-ID will be used instead. A blank entry in the field below will cause the handle to be set back to your Expertiza user-ID.

- Note: By using this form, you are changing your default handle, which will be used for all future assignments. To change your handle for a specific assignment, select that assignment and choose the Change Handle action.
+ {/* Handle field with instructions */} +
+ +
+ A "handle" can be used to conceal your username from people who view your wiki pages. If you have a handle, your wiki account should be named after your handle instead of after your user-ID. If you do not have a handle, your Expertiza user-ID will be used instead. A blank entry in the field below will cause the handle to be set back to your Expertiza user-ID.

+ Note: By using this form, you are changing your default handle, which will be used for all future assignments. To change your handle for a specific assignment, select that assignment and choose the Change Handle action.
+
- {/* Handle input field */} -
- - - -
+ {/* Handle input field */} +
+ + +

{errors.handle?.message}

+
- {/* Email Options checkboxes */} -
-
- -

Check the boxes representing the times when you want to receive e-mail.

-
-
- - - -
+ {/* Email Options checkboxes */} +
+
+ +

Check the boxes representing the times when you want to receive e-mail.

- - {/* Preferred Time Zone field */} -
- - - - - - +
+ + +
+
- {/* Preferred Language field */} -
- - - - - - - -
+ {/* Preferred Time Zone field */} +
+ + + + + + +
- {/* Submit button */} -
- -
- - )} - -
+ {/* Preferred Language field */} +
+ + + + + + +
+ + {/* Submit button */} +
+ +
+ +
); };