feat(dashboard-v2): implement data mutations for user accounts
This commit is contained in:
parent
d27ef442f4
commit
93809d5428
|
@ -0,0 +1,78 @@
|
|||
import * as Yup from "yup";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Formik, Form } from "formik";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { TextField } from "../Form/TextField";
|
||||
import accountsService from "../../services/accountsService";
|
||||
|
||||
const accountRemovalSchema = Yup.object().shape({
|
||||
confirm: Yup.string().oneOf(["delete"], `Type "delete" to confirm`),
|
||||
});
|
||||
|
||||
export const AccountRemovalForm = ({ abort, onSuccess }) => {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
confirm: "",
|
||||
}}
|
||||
validationSchema={accountRemovalSchema}
|
||||
onSubmit={async () => {
|
||||
try {
|
||||
setError(false);
|
||||
await accountsService.delete("user");
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isValid, dirty }) => (
|
||||
<Form className="flex flex-col gap-4">
|
||||
<div>
|
||||
<h4>Delete account</h4>
|
||||
<p>
|
||||
This will completely delete your account. <strong>This process can't be undone.</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-palette-200/50" />
|
||||
|
||||
<p>Type "delete" in the field below to remove your account.</p>
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
name="confirm"
|
||||
placeholder="delete"
|
||||
error={errors.confirm}
|
||||
touched={touched.confirm}
|
||||
className="text-center"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 justify-center mt-4">
|
||||
<Button $primary onClick={abort}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!isValid || !dirty}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="px-3 py-2 sm:px-6 sm:py-4 rounded border bg-red-100 border-red-200 text-error">
|
||||
There was an error processing your request. Please try again later.
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
AccountRemovalForm.propTypes = {
|
||||
abort: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
import * as Yup from "yup";
|
||||
import PropTypes from "prop-types";
|
||||
import { Formik, Form } from "formik";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { TextField } from "../Form/TextField";
|
||||
import accountsService from "../../services/accountsService";
|
||||
|
||||
const isPopulated = (value) => value?.length > 0;
|
||||
|
||||
const emailUpdateSchema = Yup.object().shape({
|
||||
email: Yup.string().email("Please provide a valid email address"),
|
||||
confirmEmail: Yup.string()
|
||||
.oneOf([Yup.ref("email"), null], "Emails must match")
|
||||
.when("email", {
|
||||
is: isPopulated,
|
||||
then: (schema) => schema.required("Please confirm new email address"),
|
||||
}),
|
||||
password: Yup.string().min(6, "Password has to be at least 6 characters long"),
|
||||
confirmPassword: Yup.string()
|
||||
.oneOf([Yup.ref("password"), null], "Passwords must match")
|
||||
.when("password", {
|
||||
is: isPopulated,
|
||||
then: (schema) => schema.required("Please confirm new password"),
|
||||
}),
|
||||
});
|
||||
|
||||
export const AccountSettingsForm = ({ user, onSuccess, onFailure }) => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: "",
|
||||
confirmEmail: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
}}
|
||||
validationSchema={emailUpdateSchema}
|
||||
onSubmit={async ({ email, password }, { resetForm }) => {
|
||||
try {
|
||||
await accountsService.put("user", {
|
||||
json: { email, password },
|
||||
});
|
||||
|
||||
resetForm();
|
||||
onSuccess();
|
||||
} catch {
|
||||
onFailure();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isValid, dirty }) => (
|
||||
<Form>
|
||||
<div className="flex flex-col w-full gap-8">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<TextField
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder={user?.email}
|
||||
error={errors.email}
|
||||
touched={touched.email}
|
||||
/>
|
||||
<TextField
|
||||
type="text"
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
label="Confirm new email address"
|
||||
error={errors.confirmEmail}
|
||||
touched={touched.confirmEmail}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<TextField
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
label="New password"
|
||||
error={errors.password}
|
||||
touched={touched.password}
|
||||
/>
|
||||
<TextField
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
label="Confirm new password"
|
||||
error={errors.confirmPassword}
|
||||
touched={touched.confirmPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex mt-2 sm:mt-0 pt-5 justify-center">
|
||||
<Button type="submit" className="px-24" disabled={!isValid || !dirty}>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
AccountSettingsForm.propTypes = {
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
onFailure: PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,19 +1,31 @@
|
|||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useMedia } from "react-use";
|
||||
import styled from "styled-components";
|
||||
import { navigate } from "gatsby";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import theme from "../../lib/theme";
|
||||
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
|
||||
import { TextInputBasic } from "../../components/TextInputBasic/TextInputBasic";
|
||||
import { Button } from "../../components/Button";
|
||||
import { AvatarUploader } from "../../components/AvatarUploader";
|
||||
import { AccountSettingsForm } from "../../components/forms/AccountSettingsForm";
|
||||
import { Modal } from "../../components/Modal/Modal";
|
||||
import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm";
|
||||
import { Alert } from "../../components/Alert";
|
||||
|
||||
const FormGroup = styled.div.attrs({
|
||||
className: "grid sm:grid-cols-[1fr_min-content] w-full gap-y-2 gap-x-4 items-end",
|
||||
})``;
|
||||
const State = {
|
||||
Pure: "PURE",
|
||||
Success: "SUCCESS",
|
||||
Failure: "FAILURE",
|
||||
};
|
||||
|
||||
const AccountPage = () => {
|
||||
const isLargeScreen = useMedia(`(min-width: ${theme.screens.xl})`);
|
||||
const { user, mutate: reloadUser } = useUser();
|
||||
const [state, setState] = useState(State.Pure);
|
||||
const [removalInitiated, setRemovalInitiated] = useState(false);
|
||||
|
||||
const prompt = () => setRemovalInitiated(true);
|
||||
const abort = () => setRemovalInitiated(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col xl:flex-row">
|
||||
|
@ -32,28 +44,18 @@ const AccountPage = () => {
|
|||
</section>
|
||||
)}
|
||||
<section className="flex flex-col gap-8">
|
||||
<FormGroup>
|
||||
<TextInputBasic label="Display name" placeholder="John Doe" />
|
||||
<div className="flex mt-2 sm:mt-0 justify-center">
|
||||
<Button>Update</Button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<TextInputBasic label="Email" placeholder="john.doe@example.com" />
|
||||
<div className="flex mt-2 sm:mt-0 justify-center">
|
||||
<Button>Update</Button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<TextInputBasic type="password" label="Password" placeholder="dbf3htf*efh4pcy@PXB" />
|
||||
<div className="flex mt-2 sm:mt-0 justify-center order-last sm:order-none">
|
||||
<Button>Update</Button>
|
||||
</div>
|
||||
<small className="text-palette-400">
|
||||
The password must be at least 6 characters long. Significantly different from the email and old
|
||||
password.
|
||||
</small>
|
||||
</FormGroup>
|
||||
{state === State.Failure && (
|
||||
<Alert $variant="error">There was an error processing your request. Please try again later.</Alert>
|
||||
)}
|
||||
{state === State.Success && <Alert $variant="success">Changes saved successfully.</Alert>}
|
||||
<AccountSettingsForm
|
||||
user={user}
|
||||
onSuccess={() => {
|
||||
reloadUser();
|
||||
setState(State.Success);
|
||||
}}
|
||||
onFailure={() => setState(State.Failure)}
|
||||
/>
|
||||
</section>
|
||||
<hr />
|
||||
<section>
|
||||
|
@ -61,7 +63,7 @@ const AccountPage = () => {
|
|||
<p>This will completely delete your account. This process can't be undone.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.confirm("TODO: confirmation modal")}
|
||||
onClick={prompt}
|
||||
className="text-error underline decoration-1 hover:decoration-dashed"
|
||||
>
|
||||
Delete account
|
||||
|
@ -71,6 +73,11 @@ const AccountPage = () => {
|
|||
<div className="flex w-full justify-start xl:justify-end">
|
||||
{isLargeScreen && <AvatarUploader className="flex flex-col gap-4" />}
|
||||
</div>
|
||||
{removalInitiated && (
|
||||
<Modal onClose={abort} className="text-center">
|
||||
<AccountRemovalForm abort={abort} onSuccess={() => navigate("/auth/login")} />
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
Reference in New Issue