diff --git a/app/frontend/src/javascript/components/base/error-boundary.tsx b/app/frontend/src/javascript/components/base/error-boundary.tsx new file mode 100644 index 000000000..742d9d690 --- /dev/null +++ b/app/frontend/src/javascript/components/base/error-boundary.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; +} + +/** + * This component will catch javascript errors anywhere in their child component tree and display a message to the user. + * @see https://reactjs.org/docs/error-boundaries.html + */ +export class ErrorBoundary extends React.Component { + constructor (props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError () { + return { hasError: true }; + } + + componentDidCatch (error, errorInfo) { + console.error(error, errorInfo); + } + + render () { + if (this.state.hasError) { + return

Something went wrong.

; + } + + return this.props.children; + } +} diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx new file mode 100644 index 000000000..7f2118ee0 --- /dev/null +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { react2angular } from 'react2angular'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { User } from '../../models/user'; +import { IApplication } from '../../models/application'; +import { Loader } from '../base/loader'; +import { FormInput } from '../form/form-input'; + +declare const Application: IApplication; + +interface UserProfileFormProps { + action: 'create' | 'update', + user: User; + className?: string; +} + +export const UserProfileForm: React.FC = ({ action, user, className }) => { + const { handleSubmit, register } = useForm({ defaultValues: { ...user } }); + + /** + * Callback triggered when the form is submitted: process with the user creation or update. + */ + const onSubmit: SubmitHandler = (data: User) => { + console.log(action, data); + }; + + return ( +
+ + + ); +}; + +const UserProfileFormWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'user', 'className'])); diff --git a/app/frontend/src/javascript/controllers/dashboard.js b/app/frontend/src/javascript/controllers/dashboard.js index 5fbb80328..48f8b7be2 100644 --- a/app/frontend/src/javascript/controllers/dashboard.js +++ b/app/frontend/src/javascript/controllers/dashboard.js @@ -41,6 +41,20 @@ Application.Controllers.controller('DashboardController', ['$scope', 'memberProm return trainingsPromise.find(t => t.id === trainingId).name; }; + /** + * Callback used in PaymentScheduleDashboard, in case of error + */ + $scope.onError = function (message) { + growl.error(message); + }; + + /** + * Callback triggered when the user has successfully updated his card + */ + $scope.onCardUpdateSuccess = function (message) { + growl.success(message); + }; + /* PRIVATE SCOPE */ /** @@ -63,20 +77,6 @@ Application.Controllers.controller('DashboardController', ['$scope', 'memberProm return networks; }; - /** - * Callback used in PaymentScheduleDashboard, in case of error - */ - $scope.onError = function (message) { - growl.error(message); - }; - - /** - * Callback triggered when the user has successfully updated his card - */ - $scope.onCardUpdateSuccess = function (message) { - growl.success(message); - }; - // !!! MUST BE CALLED AT THE END of the controller return initialize(); } diff --git a/app/frontend/src/javascript/controllers/members.js b/app/frontend/src/javascript/controllers/members.js index 0b308def2..042146706 100644 --- a/app/frontend/src/javascript/controllers/members.js +++ b/app/frontend/src/javascript/controllers/members.js @@ -86,7 +86,7 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco $scope.method = 'patch'; // Current user's profile - $scope.user = memberPromise; + $scope.user = cleanUser(memberPromise); // default : do not show the group changing form $scope.group = @@ -293,6 +293,13 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco return angular.forEach(activeProviderPromise.mapping, map => $scope.preventField[map] = true); }; + // prepare the user for the react-hook-form + function cleanUser (user) { + delete user.$promise; + delete user.$resolved; + return user; + } + // !!! MUST BE CALLED AT THE END of the controller return initialize(); } diff --git a/app/frontend/src/javascript/models/user.ts b/app/frontend/src/javascript/models/user.ts index 9b079a410..eb3ce02db 100644 --- a/app/frontend/src/javascript/models/user.ts +++ b/app/frontend/src/javascript/models/user.ts @@ -1,4 +1,5 @@ import { Plan } from './plan'; +import { TDateISO } from '../typings/date-iso'; export enum UserRole { Member = 'member', @@ -63,13 +64,13 @@ export interface User { statistic_profile: { id: number, gender: string, - birthday: Date + birthday: TDateISO }, subscribed_plan: Plan, subscription: { id: number, - expired_at: Date, - canceled_at: Date, + expired_at: TDateISO, + canceled_at: TDateISO, stripe: boolean, plan: { id: number, @@ -82,5 +83,5 @@ export interface User { }, training_credits: Array, machine_credits: Array<{machine_id: number, hours_used: number}>, - last_sign_in_at: Date + last_sign_in_at: TDateISO } diff --git a/app/frontend/src/javascript/typings/date-iso.d.ts b/app/frontend/src/javascript/typings/date-iso.d.ts new file mode 100644 index 000000000..c86e84a06 --- /dev/null +++ b/app/frontend/src/javascript/typings/date-iso.d.ts @@ -0,0 +1,28 @@ +// from https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054 + +type TYear = `${number}${number}${number}${number}`; +type TMonth = `${number}${number}`; +type TDay = `${number}${number}`; +type THours = `${number}${number}`; +type TMinutes = `${number}${number}`; +type TSeconds = `${number}${number}`; +type TMilliseconds = `${number}${number}${number}`; + +/** + * Represent a string like `2021-01-08` + */ +type TDateISODate = `${TYear}-${TMonth}-${TDay}`; + +/** + * Represent a string like `14:42:34.678` + */ +type TDateISOTime = `${THours}:${TMinutes}:${TSeconds}.${TMilliseconds}`; + +/** + * Represent a string like `2021-01-08T14:42:34.678Z` (format: ISO 8601). + * + * It is not possible to type more precisely (list every possible values for months, hours etc) as + * it would result in a warning from TypeScript: + * "Expression produces a union type that is too complex to represent. ts(2590) + */ +export type TDateISO = `${TDateISODate}T${TDateISOTime}Z`; diff --git a/app/frontend/templates/dashboard/settings.html b/app/frontend/templates/dashboard/settings.html index 4890f310f..4d8e804b6 100644 --- a/app/frontend/templates/dashboard/settings.html +++ b/app/frontend/templates/dashboard/settings.html @@ -121,6 +121,7 @@
+