diff --git a/app/settings/components/danger-zone.tsx b/app/settings/components/account/danger-zone.tsx similarity index 75% rename from app/settings/components/danger-zone.tsx rename to app/settings/components/account/danger-zone.tsx index 694b9de..f29c848 100644 --- a/app/settings/components/danger-zone.tsx +++ b/app/settings/components/account/danger-zone.tsx @@ -2,10 +2,10 @@ import { useRef, useState } from "react"; import { useMutation } from "blitz"; import clsx from "clsx"; -import Button from "./button"; -import SettingsSection from "./settings-section"; -import Modal, { ModalTitle } from "./modal"; -import deleteUser from "../mutations/delete-user"; +import Button from "../button"; +import SettingsSection from "../settings-section"; +import Modal, { ModalTitle } from "../modal"; +import deleteUser from "../../mutations/delete-user"; export default function DangerZone() { const deleteUserMutation = useMutation(deleteUser)[0]; @@ -26,20 +26,18 @@ export default function DangerZone() { }; return ( - -
-
-

- Once you delete your account, all of its data will be permanently deleted and any ongoing - subscription will be cancelled. -

+ +
+

+ Once you delete your account, all of its data will be permanently deleted and any ongoing + subscription will be cancelled. +

- - - -
+ + +
diff --git a/app/settings/components/account/profile-informations.tsx b/app/settings/components/account/profile-informations.tsx new file mode 100644 index 0000000..93d4581 --- /dev/null +++ b/app/settings/components/account/profile-informations.tsx @@ -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
(); + + 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 ( + + + +
+ } + > + {isError ? ( +
+ +
+ ) : null} + + {isSuccess ? ( +
+ +
+ ) : null} +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + ); +}; + +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; +} diff --git a/app/settings/components/account/update-password.tsx b/app/settings/components/account/update-password.tsx new file mode 100644 index 0000000..0f2bb44 --- /dev/null +++ b/app/settings/components/account/update-password.tsx @@ -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
(); + + const onSubmit = handleSubmit(async ({ currentPassword, newPassword }) => { + if (isSubmitting) { + return; + } + + await changePasswordMutation({ currentPassword, newPassword }); + }); + const errorMessage = parseErrorMessage(error as Error | null); + + return ( + + + + + } + > + {isError ? ( +
+ +
+ ) : null} + + {isSuccess ? ( +
+ +
+ ) : null} +
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+ ); +}; + +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; +} diff --git a/app/settings/components/divider.tsx b/app/settings/components/divider.tsx deleted file mode 100644 index 8c78520..0000000 --- a/app/settings/components/divider.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function Divider() { - return ( -
-
-
-
-
- ); -} diff --git a/app/settings/components/profile-informations.tsx b/app/settings/components/profile-informations.tsx deleted file mode 100644 index d4fbcaf..0000000 --- a/app/settings/components/profile-informations.tsx +++ /dev/null @@ -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
(); - 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 ( - - - {errorMessage ? ( -
- -
- ) : null} - - {isSubmitSuccessful ? ( -
- -
- ) : null} - -
-
-
- -
- -
-
- -
- -
- -
-
-
- -
- -
-
- -
- ); -}; - -export default ProfileInformations; diff --git a/app/settings/components/settings-section.tsx b/app/settings/components/settings-section.tsx index 0860f02..6086eff 100644 --- a/app/settings/components/settings-section.tsx +++ b/app/settings/components/settings-section.tsx @@ -1,18 +1,16 @@ import type { FunctionComponent, ReactNode } from "react"; +import clsx from "clsx"; type Props = { - title: string; - description?: ReactNode; + className?: string; + footer?: ReactNode; }; -const SettingsSection: FunctionComponent = ({ children, title, description }) => ( -
-
-

{title}

- {description ?

{description}

: null} -
-
{children}
-
+const SettingsSection: FunctionComponent = ({ children, footer, className }) => ( +
+
{children}
+ {footer ?? null} +
); export default SettingsSection; diff --git a/app/settings/components/update-password.tsx b/app/settings/components/update-password.tsx deleted file mode 100644 index 93f481b..0000000 --- a/app/settings/components/update-password.tsx +++ /dev/null @@ -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
(); - 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 ( - - - {errorMessage ? ( -
- -
- ) : null} - - {!isSubmitting && isSubmitSuccessful && !errorMessage ? ( -
- -
- ) : null} - -
-
-
- -
- -
-
- -
- -
- -
-
-
- -
- -
-
- -
- ); -}; - -export default UpdatePassword; diff --git a/app/settings/pages/settings/billing.tsx b/app/settings/pages/settings/billing.tsx index 7b20bfb..e94bd70 100644 --- a/app/settings/pages/settings/billing.tsx +++ b/app/settings/pages/settings/billing.tsx @@ -1,5 +1,7 @@ import type { BlitzPage } 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 useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; @@ -7,10 +9,7 @@ import SettingsLayout from "../../components/settings-layout"; import appLogger from "../../../../integrations/logger"; import PaddleLink from "../../components/paddle-link"; import SettingsSection from "../../components/settings-section"; -import Divider from "../../components/divider"; 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" }); @@ -76,111 +75,91 @@ const Billing: BlitzPage = () => { if (!subscription) { return ( -
-
-
-
-

- Subscribe -

-

- Update your billing information. Please note that updating your location could affect - your tax rates. -

-
- -
- {pricing.tiers.map((tier) => ( -
-
-

- {tier.title} -

- {tier.yearly ? ( -

- Get 2 months free! -

- ) : null} -

- - {tier.price}€ - - {tier.frequency} -

- {tier.yearly ? ( -

Billed yearly ({tier.price * 12}€)

- ) : null} -

{tier.description}

- -
    - {tier.features.map((feature) => ( -
  • -
  • - ))} - {tier.unavailableFeatures.map((feature) => ( -
  • - - {~feature.indexOf("(coming soon)") - ? feature.slice(0, feature.indexOf("(coming soon)")) - : feature} - -
  • - ))} -
-
- - - Panelbear.track("redirect-to-join-waitlist")} - className={clsx( - tier.yearly - ? "bg-primary-500 text-white hover:bg-primary-600" - : "bg-primary-50 text-primary-700 hover:bg-primary-100", - "mt-8 block w-full py-3 px-6 border border-transparent rounded-md text-center font-medium", - )} - > - {tier.cta} - - -
- ))} -
-
+ +
+

Subscribe

+

+ Update your billing information. Please note that updating your location could affect your tax + rates. +

-
+ +
+ {pricing.tiers.map((tier) => ( +
+
+

{tier.title}

+ {tier.yearly ? ( +

+ Get 2 months free! +

+ ) : null} +

+ {tier.price}€ + {tier.frequency} +

+ {tier.yearly ? ( +

Billed yearly ({tier.price * 12}€)

+ ) : null} +

{tier.description}

+ +
    + {tier.features.map((feature) => ( +
  • +
  • + ))} + {tier.unavailableFeatures.map((feature) => ( +
  • + + {~feature.indexOf("(coming soon)") + ? feature.slice(0, feature.indexOf("(coming soon)")) + : feature} + +
  • + ))} +
+
+ + + Panelbear.track("redirect-to-join-waitlist")} + className={clsx( + tier.yearly + ? "bg-primary-500 text-white hover:bg-primary-600" + : "bg-primary-50 text-primary-700 hover:bg-primary-100", + "mt-8 block w-full py-3 px-6 border border-transparent rounded-md text-center font-medium", + )} + > + {tier.cta} + + +
+ ))} +
+ ); } return ( <> - + updatePaymentMethod({ updateUrl: subscription.updateUrl })} text="Update payment method on Paddle" /> -
- -
+ {/**/} - - {/**/} - - -
- -
- - + cancelSubscription({ cancelUrl: subscription.cancelUrl })} text="Cancel subscription on Paddle" diff --git a/app/settings/pages/settings/index.tsx b/app/settings/pages/settings/index.tsx index aa00063..e3a9a54 100644 --- a/app/settings/pages/settings/index.tsx +++ b/app/settings/pages/settings/index.tsx @@ -2,10 +2,9 @@ import type { BlitzPage } from "blitz"; import { Routes } from "blitz"; import SettingsLayout from "../../components/settings-layout"; -import ProfileInformations from "../../components/profile-informations"; -import Divider from "../../components/divider"; -import UpdatePassword from "../../components/update-password"; -import DangerZone from "../../components/danger-zone"; +import ProfileInformations from "../../components/account/profile-informations"; +import UpdatePassword from "../../components/account/update-password"; +import DangerZone from "../../components/account/danger-zone"; import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; const Account: BlitzPage = () => { @@ -15,16 +14,8 @@ const Account: BlitzPage = () => {
-
- -
- -
- -
-
);