better looking SettingsSection

This commit is contained in:
m5r 2021-09-30 23:36:47 +02:00
parent 49f11a16e2
commit 13ac4a5580
9 changed files with 327 additions and 371 deletions

View File

@ -2,10 +2,10 @@ import { useRef, useState } from "react";
import { useMutation } from "blitz"; import { useMutation } from "blitz";
import clsx from "clsx"; import clsx from "clsx";
import Button from "./button"; import Button from "../button";
import SettingsSection from "./settings-section"; import SettingsSection from "../settings-section";
import Modal, { ModalTitle } from "./modal"; import Modal, { ModalTitle } from "../modal";
import deleteUser from "../mutations/delete-user"; import deleteUser from "../../mutations/delete-user";
export default function DangerZone() { export default function DangerZone() {
const deleteUserMutation = useMutation(deleteUser)[0]; const deleteUserMutation = useMutation(deleteUser)[0];
@ -26,9 +26,8 @@ export default function DangerZone() {
}; };
return ( return (
<SettingsSection title="Danger Zone" description="Highway to the Danger Zone 𝅘𝅥𝅮"> <SettingsSection className="border border-red-300">
<div className="shadow border border-red-300 sm:rounded-md sm:overflow-hidden"> <div className="flex justify-between items-center flex-row space-x-2">
<div className="flex justify-between items-center flex-row px-4 py-5 space-x-2 bg-white sm:p-6">
<p> <p>
Once you delete your account, all of its data will be permanently deleted and any ongoing Once you delete your account, all of its data will be permanently deleted and any ongoing
subscription will be cancelled. subscription will be cancelled.
@ -40,7 +39,6 @@ export default function DangerZone() {
</Button> </Button>
</span> </span>
</div> </div>
</div>
<Modal initialFocus={modalCancelButtonRef} isOpen={isConfirmationModalOpen} onClose={closeModal}> <Modal initialFocus={modalCancelButtonRef} isOpen={isConfirmationModalOpen} onClose={closeModal}>
<div className="md:flex md:items-start"> <div className="md:flex md:items-start">

View File

@ -0,0 +1,115 @@
import type { FunctionComponent } from "react";
import { useEffect } from "react";
import { useMutation } from "blitz";
import { useForm } from "react-hook-form";
import updateUser from "../../mutations/update-user";
import Alert from "../../../core/components/alert";
import Button from "../button";
import SettingsSection from "../settings-section";
import useCurrentUser from "../../../core/hooks/use-current-user";
import appLogger from "../../../../integrations/logger";
type Form = {
fullName: string;
email: string;
};
const logger = appLogger.child({ module: "profile-settings" });
const ProfileInformations: FunctionComponent = () => {
const { user } = useCurrentUser();
const [updateUserMutation, { error, isError, isSuccess }] = useMutation(updateUser);
const {
register,
handleSubmit,
setValue,
formState: { isSubmitting },
} = useForm<Form>();
useEffect(() => {
setValue("fullName", user?.fullName ?? "");
setValue("email", user?.email ?? "");
}, [setValue, user]);
const onSubmit = handleSubmit(async ({ fullName, email }) => {
if (isSubmitting) {
return;
}
await updateUserMutation({ email, fullName });
});
const errorMessage = parseErrorMessage(error as Error | null);
return (
<form onSubmit={onSubmit}>
<SettingsSection
footer={
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
<Button variant="default" type="submit" isDisabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</Button>
</div>
}
>
{isError ? (
<div className="mb-8">
<Alert title="Oops, there was an issue" message={errorMessage} variant="error" />
</div>
) : null}
{isSuccess ? (
<div className="mb-8">
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
</div>
) : null}
<div className="col-span-3 sm:col-span-2">
<label htmlFor="fullName" className="block text-sm font-medium leading-5 text-gray-700">
Full name
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="fullName"
type="text"
tabIndex={1}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("fullName")}
required
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium leading-5 text-gray-700">
Email address
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="email"
type="email"
tabIndex={2}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("email")}
required
/>
</div>
</div>
</SettingsSection>
</form>
);
};
export default ProfileInformations;
function parseErrorMessage(error: Error | null): string {
if (!error) {
return "";
}
if (error.name === "ZodError") {
return JSON.parse(error.message)[0].message;
}
return error.message;
}

View File

@ -0,0 +1,112 @@
import type { FunctionComponent } from "react";
import { useMutation } from "blitz";
import { useForm } from "react-hook-form";
import Alert from "../../../core/components/alert";
import Button from "../button";
import SettingsSection from "../settings-section";
import appLogger from "../../../../integrations/logger";
import changePassword from "../../mutations/change-password";
const logger = appLogger.child({ module: "update-password" });
type Form = {
currentPassword: string;
newPassword: string;
};
const UpdatePassword: FunctionComponent = () => {
const [changePasswordMutation, { error, isError, isSuccess }] = useMutation(changePassword);
const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm<Form>();
const onSubmit = handleSubmit(async ({ currentPassword, newPassword }) => {
if (isSubmitting) {
return;
}
await changePasswordMutation({ currentPassword, newPassword });
});
const errorMessage = parseErrorMessage(error as Error | null);
return (
<form onSubmit={onSubmit}>
<SettingsSection
footer={
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
<Button variant="default" type="submit" isDisabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</Button>
</div>
}
>
{isError ? (
<div className="mb-8">
<Alert title="Oops, there was an issue" message={errorMessage} variant="error" />
</div>
) : null}
{isSuccess ? (
<div className="mb-8">
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
</div>
) : null}
<div>
<label
htmlFor="currentPassword"
className="flex justify-between text-sm font-medium leading-5 text-gray-700"
>
<div>Current password</div>
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="currentPassword"
type="password"
tabIndex={3}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("currentPassword")}
required
/>
</div>
</div>
<div>
<label
htmlFor="newPassword"
className="flex justify-between text-sm font-medium leading-5 text-gray-700"
>
<div>New password</div>
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="newPassword"
type="password"
tabIndex={4}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("newPassword")}
required
/>
</div>
</div>
</SettingsSection>
</form>
);
};
export default UpdatePassword;
function parseErrorMessage(error: Error | null): string {
if (!error) {
return "";
}
if (error.name === "ZodError") {
return JSON.parse(error.message)[0].message;
}
return error.message;
}

View File

@ -1,9 +0,0 @@
export default function Divider() {
return (
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
</div>
);
}

View File

@ -1,114 +0,0 @@
import type { FunctionComponent } from "react";
import { useEffect, useState } from "react";
import { useMutation } from "blitz";
import { useForm } from "react-hook-form";
import updateUser from "../mutations/update-user";
import Alert from "../../core/components/alert";
import Button from "./button";
import SettingsSection from "./settings-section";
import useCurrentUser from "../../core/hooks/use-current-user";
import appLogger from "../../../integrations/logger";
type Form = {
fullName: string;
email: string;
};
const logger = appLogger.child({ module: "profile-settings" });
const ProfileInformations: FunctionComponent = () => {
const { user } = useCurrentUser();
const updateUserMutation = useMutation(updateUser)[0];
const {
register,
handleSubmit,
setValue,
formState: { isSubmitting, isSubmitSuccessful },
} = useForm<Form>();
const [errorMessage, setErrorMessage] = useState("");
useEffect(() => {
setValue("fullName", user?.fullName ?? "");
setValue("email", user?.email ?? "");
}, [setValue, user]);
const onSubmit = handleSubmit(async ({ fullName, email }) => {
if (isSubmitting) {
return;
}
try {
await updateUserMutation({ email, fullName });
} catch (error: any) {
logger.error(error.response, "error updating user infos");
setErrorMessage(error.response.data.errorMessage);
}
});
return (
<SettingsSection
title="Profile Information"
description="Update your account's profile information and email address."
>
<form onSubmit={onSubmit}>
{errorMessage ? (
<div className="mb-8">
<Alert title="Oops, there was an issue" message={errorMessage} variant="error" />
</div>
) : null}
{isSubmitSuccessful ? (
<div className="mb-8">
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
</div>
) : null}
<div className="shadow sm:rounded-md sm:overflow-hidden">
<div className="px-4 py-5 bg-white space-y-6 sm:p-6">
<div className="col-span-3 sm:col-span-2">
<label htmlFor="fullName" className="block text-sm font-medium leading-5 text-gray-700">
Full name
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="fullName"
type="text"
tabIndex={1}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("fullName")}
required
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium leading-5 text-gray-700">
Email address
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="email"
type="email"
tabIndex={2}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("email")}
required
/>
</div>
</div>
</div>
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
<Button variant="default" type="submit" isDisabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</Button>
</div>
</div>
</form>
</SettingsSection>
);
};
export default ProfileInformations;

View File

@ -1,18 +1,16 @@
import type { FunctionComponent, ReactNode } from "react"; import type { FunctionComponent, ReactNode } from "react";
import clsx from "clsx";
type Props = { type Props = {
title: string; className?: string;
description?: ReactNode; footer?: ReactNode;
}; };
const SettingsSection: FunctionComponent<Props> = ({ children, title, description }) => ( const SettingsSection: FunctionComponent<Props> = ({ children, footer, className }) => (
<div className="px-4 sm:px-6 lg:px-0 lg:grid lg:grid-cols-4 lg:gap-6"> <section className={clsx(className, "shadow sm:rounded-md sm:overflow-hidden")}>
<div className="lg:col-span-1"> <div className="bg-white space-y-6 py-6 px-4 sm:p-6">{children}</div>
<h3 className="text-lg font-medium leading-6 text-gray-900">{title}</h3> {footer ?? null}
{description ? <p className="mt-1 text-sm text-gray-600">{description}</p> : null} </section>
</div>
<div className="mt-5 lg:mt-0 lg:col-span-3">{children}</div>
</div>
); );
export default SettingsSection; export default SettingsSection;

View File

@ -1,114 +0,0 @@
import type { FunctionComponent } from "react";
import { useState } from "react";
import { useMutation } from "blitz";
import { useForm } from "react-hook-form";
import Alert from "../../core/components/alert";
import Button from "./button";
import SettingsSection from "./settings-section";
import appLogger from "../../../integrations/logger";
import changePassword from "../mutations/change-password";
const logger = appLogger.child({ module: "update-password" });
type Form = {
currentPassword: string;
newPassword: string;
};
const UpdatePassword: FunctionComponent = () => {
const changePasswordMutation = useMutation(changePassword)[0];
const {
register,
handleSubmit,
formState: { isSubmitting, isSubmitSuccessful },
} = useForm<Form>();
const [errorMessage, setErrorMessage] = useState("");
const onSubmit = handleSubmit(async ({ currentPassword, newPassword }) => {
if (isSubmitting) {
return;
}
setErrorMessage("");
try {
await changePasswordMutation({ currentPassword, newPassword });
} catch (error: any) {
logger.error(error, "error updating user infos");
setErrorMessage(error.message);
}
});
return (
<SettingsSection
title="Update Password"
description="Make sure you are using a long, random password to stay secure."
>
<form onSubmit={onSubmit}>
{errorMessage ? (
<div className="mb-8">
<Alert title="Oops, there was an issue" message={errorMessage} variant="error" />
</div>
) : null}
{!isSubmitting && isSubmitSuccessful && !errorMessage ? (
<div className="mb-8">
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
</div>
) : null}
<div className="shadow sm:rounded-md sm:overflow-hidden">
<div className="px-4 py-5 bg-white space-y-6 sm:p-6">
<div>
<label
htmlFor="currentPassword"
className="flex justify-between text-sm font-medium leading-5 text-gray-700"
>
<div>Current password</div>
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="currentPassword"
type="password"
tabIndex={3}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("currentPassword")}
required
/>
</div>
</div>
<div>
<label
htmlFor="newPassword"
className="flex justify-between text-sm font-medium leading-5 text-gray-700"
>
<div>New password</div>
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id="newPassword"
type="password"
tabIndex={4}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
{...register("newPassword")}
required
/>
</div>
</div>
</div>
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
<Button variant="default" type="submit" isDisabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</Button>
</div>
</div>
</form>
</SettingsSection>
);
};
export default UpdatePassword;

View File

@ -1,5 +1,7 @@
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { GetServerSideProps, Link, Routes } from "blitz"; import { GetServerSideProps, Link, Routes } from "blitz";
import * as Panelbear from "@panelbear/panelbear-js";
import clsx from "clsx";
import useSubscription from "../../hooks/use-subscription"; import useSubscription from "../../hooks/use-subscription";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
@ -7,10 +9,7 @@ import SettingsLayout from "../../components/settings-layout";
import appLogger from "../../../../integrations/logger"; import appLogger from "../../../../integrations/logger";
import PaddleLink from "../../components/paddle-link"; import PaddleLink from "../../components/paddle-link";
import SettingsSection from "../../components/settings-section"; import SettingsSection from "../../components/settings-section";
import Divider from "../../components/divider";
import { HiCheck } from "react-icons/hi"; import { HiCheck } from "react-icons/hi";
import * as Panelbear from "@panelbear/panelbear-js";
import clsx from "clsx";
const logger = appLogger.child({ page: "/account/settings/billing" }); const logger = appLogger.child({ page: "/account/settings/billing" });
@ -76,16 +75,12 @@ const Billing: BlitzPage = () => {
if (!subscription) { if (!subscription) {
return ( return (
<section aria-labelledby="subscribe-heading"> <SettingsSection>
<div className="shadow sm:rounded-md sm:overflow-hidden">
<div className="bg-white py-6 px-4 sm:p-6">
<div> <div>
<h2 id="subscribe-heading" className="text-lg leading-6 font-medium text-gray-900"> <h2 className="text-lg leading-6 font-medium text-gray-900">Subscribe</h2>
Subscribe
</h2>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Update your billing information. Please note that updating your location could affect Update your billing information. Please note that updating your location could affect your tax
your tax rates. rates.
</p> </p>
</div> </div>
@ -96,18 +91,14 @@ const Billing: BlitzPage = () => {
className="relative p-4 mb-4 bg-white border border-gray-200 rounded-xl shadow-sm flex flex-grow w-1/3 flex-col" className="relative p-4 mb-4 bg-white border border-gray-200 rounded-xl shadow-sm flex flex-grow w-1/3 flex-col"
> >
<div className="flex-1"> <div className="flex-1">
<h3 className="text-xl font-mackinac font-semibold text-gray-900"> <h3 className="text-xl font-mackinac font-semibold text-gray-900">{tier.title}</h3>
{tier.title}
</h3>
{tier.yearly ? ( {tier.yearly ? (
<p className="absolute top-0 py-1.5 px-4 bg-primary-500 rounded-full text-xs font-semibold uppercase tracking-wide text-white transform -translate-y-1/2"> <p className="absolute top-0 py-1.5 px-4 bg-primary-500 rounded-full text-xs font-semibold uppercase tracking-wide text-white transform -translate-y-1/2">
Get 2 months free! Get 2 months free!
</p> </p>
) : null} ) : null}
<p className="mt-4 flex items-baseline text-gray-900"> <p className="mt-4 flex items-baseline text-gray-900">
<span className="text-2xl font-extrabold tracking-tight"> <span className="text-2xl font-extrabold tracking-tight">{tier.price}</span>
{tier.price}
</span>
<span className="ml-1 text-lg font-semibold">{tier.frequency}</span> <span className="ml-1 text-lg font-semibold">{tier.frequency}</span>
</p> </p>
{tier.yearly ? ( {tier.yearly ? (
@ -153,34 +144,22 @@ const Billing: BlitzPage = () => {
</div> </div>
))} ))}
</div> </div>
</div> </SettingsSection>
</div>
</section>
); );
} }
return ( return (
<> <>
<SettingsSection title="Payment method"> <SettingsSection>
<PaddleLink <PaddleLink
onClick={() => updatePaymentMethod({ updateUrl: subscription.updateUrl })} onClick={() => updatePaymentMethod({ updateUrl: subscription.updateUrl })}
text="Update payment method on Paddle" text="Update payment method on Paddle"
/> />
</SettingsSection> </SettingsSection>
<div className="hidden lg:block"> <SettingsSection>{/*<BillingPlans activePlanId={subscription.paddlePlanId} />*/}</SettingsSection>
<Divider />
</div>
<SettingsSection title="Plan"> <SettingsSection>
{/*<BillingPlans activePlanId={subscription.paddlePlanId} />*/}
</SettingsSection>
<div className="hidden lg:block">
<Divider />
</div>
<SettingsSection title="Cancel subscription">
<PaddleLink <PaddleLink
onClick={() => cancelSubscription({ cancelUrl: subscription.cancelUrl })} onClick={() => cancelSubscription({ cancelUrl: subscription.cancelUrl })}
text="Cancel subscription on Paddle" text="Cancel subscription on Paddle"

View File

@ -2,10 +2,9 @@ import type { BlitzPage } from "blitz";
import { Routes } from "blitz"; import { Routes } from "blitz";
import SettingsLayout from "../../components/settings-layout"; import SettingsLayout from "../../components/settings-layout";
import ProfileInformations from "../../components/profile-informations"; import ProfileInformations from "../../components/account/profile-informations";
import Divider from "../../components/divider"; import UpdatePassword from "../../components/account/update-password";
import UpdatePassword from "../../components/update-password"; import DangerZone from "../../components/account/danger-zone";
import DangerZone from "../../components/danger-zone";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
const Account: BlitzPage = () => { const Account: BlitzPage = () => {
@ -15,16 +14,8 @@ const Account: BlitzPage = () => {
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<ProfileInformations /> <ProfileInformations />
<div className="hidden lg:block">
<Divider />
</div>
<UpdatePassword /> <UpdatePassword />
<div className="hidden lg:block">
<Divider />
</div>
<DangerZone /> <DangerZone />
</div> </div>
); );