Skip to content

Commit

Permalink
feat: Improve error handling for settings modals (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker authored Nov 30, 2024
1 parent 98a5d49 commit 957070c
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/silent-apricots-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Improve error handling for changes to user settings
18 changes: 14 additions & 4 deletions src/components/common/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,25 @@ export function Modal(props: ModalOverlayProps) {

type ModalHeaderProps = {
title: string;
description?: string;
children?: React.ReactNode;
};

export function ModalHeader({ title, children }: ModalHeaderProps) {
export function ModalHeader({
title,
description,
children,
}: ModalHeaderProps) {
return (
<header className="flex items-center justify-between w-full">
<Heading className="text-xl font-medium text-gray-normal" slot="title">
{title}
</Heading>
<div className="flex flex-col gap-1">
<Heading className="text-xl font-medium text-gray-normal" slot="title">
{title}
</Heading>
{description && (
<p className="text-gray-dim text-sm text-pretty">{description}</p>
)}
</div>
{children}
</header>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { Button, Modal, ModalFooter, ModalHeader } from "@/components/common";
import {
Banner,
Button,
Form,
Modal,
ModalFooter,
ModalHeader,
TextField,
} from "@/components/common";
import { SettingsItem } from "@/components/settings";
import { useAuthActions } from "@convex-dev/auth/react";
import { api } from "@convex/_generated/api";
import { useMutation } from "convex/react";
import { Trash } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";

type DeleteAccountModalProps = {
isOpen: boolean;
Expand All @@ -18,29 +27,74 @@ const DeleteAccountModal = ({
onSubmit,
}: DeleteAccountModalProps) => {
const { signOut } = useAuthActions();
const [value, setValue] = useState("");
const [error, setError] = useState<string>();
const [isDeleting, setIsDeleting] = useState(false);

const clearLocalStorage = () => {
localStorage.removeItem("theme");
};
const deleteAccount = useMutation(api.users.deleteCurrentUser);

const handleSubmit = () => {
clearLocalStorage();
deleteAccount();
signOut();
onSubmit();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError(undefined);

if (value !== "DELETE") {
setError("Please type DELETE to confirm.");
return;
}

try {
setIsDeleting(true);
await deleteAccount();
clearLocalStorage();
signOut();
onSubmit();
toast.success("Account deleted.");
} catch (err) {
setError("Failed to delete account. Please try again.");
} finally {
setIsDeleting(false);
}
};

return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalHeader title="Delete account?" />
<p>This will permanently erase your account and all data.</p>
<ModalFooter>
<Button onPress={() => onOpenChange(false)}>Cancel</Button>
<Button variant="destructive" onPress={handleSubmit}>
Delete
</Button>
</ModalFooter>
<ModalHeader
title="Delete account?"
description="This will permanently erase your account and all data."
/>
<Form onSubmit={handleSubmit} className="w-full">
{error ? (
<Banner variant="danger">{error}</Banner>
) : (
<Banner variant="warning">This action cannot be undone.</Banner>
)}
<TextField
label="Type DELETE to confirm"
isRequired
value={value}
onChange={(value) => {
setValue(value);
setError(undefined);
}}
className="w-full"
autoComplete="off"
/>
<ModalFooter>
<Button
variant="secondary"
onPress={() => onOpenChange(false)}
isDisabled={isDeleting}
>
Cancel
</Button>
<Button type="submit" variant="destructive" isDisabled={isDeleting}>
Delete account
</Button>
</ModalFooter>
</Form>
</Modal>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Banner,
Button,
Form,
Modal,
Expand All @@ -7,13 +8,14 @@ import {
Select,
SelectItem,
} from "@/components/common";
import { SettingsItem } from "@/components/settings";
import { api } from "@convex/_generated/api";
import type { Doc } from "@convex/_generated/dataModel";
import { JURISDICTIONS, type Jurisdiction } from "@convex/constants";
import { useMutation } from "convex/react";
import { Pencil } from "lucide-react";
import { useState } from "react";
import { SettingsItem } from "../SettingsItem";
import { toast } from "sonner";

type EditBirthplaceModalProps = {
defaultBirthplace: Jurisdiction;
Expand All @@ -28,26 +30,52 @@ const EditBirthplaceModal = ({
onOpenChange,
onSubmit,
}: EditBirthplaceModalProps) => {
const [birthplace, setBirthplace] = useState<Jurisdiction>(defaultBirthplace);
const [error, setError] = useState<string>();
const [isSubmitting, setIsSubmitting] = useState(false);

const updateBirthplace = useMutation(api.users.setBirthplace);
const [birthplace, setBirthplace] = useState(defaultBirthplace);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
updateBirthplace({ birthplace });
onSubmit();
setError(undefined);

if (!birthplace) {
setError("Please select a state.");
return;
}

try {
setIsSubmitting(true);
await updateBirthplace({ birthplace });
onSubmit();
toast.success("Birthplace updated.");
} catch (err) {
setError("Failed to update birthplace. Please try again.");
} finally {
setIsSubmitting(false);
}
};

return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalHeader title="Edit birthplace" />
<ModalHeader
title="Edit birthplace"
description="Where were you born? This location is used to select the forms for your birth certificate."
/>
<Form onSubmit={handleSubmit} className="w-full">
{error && <Banner variant="danger">{error}</Banner>}
<Select
aria-label="Birthplace"
label="State"
name="birthplace"
selectedKey={birthplace}
onSelectionChange={(key) => setBirthplace(key as Jurisdiction)}
placeholder="Select state"
onSelectionChange={(key) => {
setBirthplace(key as Jurisdiction);
setError(undefined);
}}
isRequired
className="w-full"
placeholder="Select state"
>
{Object.entries(JURISDICTIONS).map(([value, label]) => (
<SelectItem key={value} id={value}>
Expand All @@ -56,10 +84,14 @@ const EditBirthplaceModal = ({
))}
</Select>
<ModalFooter>
<Button variant="secondary" onPress={() => onOpenChange(false)}>
<Button
variant="secondary"
onPress={() => onOpenChange(false)}
isDisabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" variant="primary">
<Button type="submit" variant="primary" isDisabled={isSubmitting}>
Save
</Button>
</ModalFooter>
Expand All @@ -86,9 +118,9 @@ export const EditBirthplaceSetting = ({ user }: EditBirthplaceSettingProps) => {
: "Set birthplace"}
</Button>
<EditBirthplaceModal
defaultBirthplace={user.birthplace as Jurisdiction}
isOpen={isBirthplaceModalOpen}
onOpenChange={setIsBirthplaceModalOpen}
defaultBirthplace={user.birthplace as Jurisdiction}
onSubmit={() => setIsBirthplaceModalOpen(false)}
/>
</SettingsItem>
Expand Down
50 changes: 39 additions & 11 deletions src/components/settings/EditNameSetting/EditNameSetting.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Banner,
Button,
Form,
Modal,
Expand All @@ -11,6 +12,7 @@ import type { Doc } from "@convex/_generated/dataModel";
import { useMutation } from "convex/react";
import { Pencil } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { SettingsItem } from "../SettingsItem";

type EditNameModalProps = {
Expand All @@ -28,29 +30,58 @@ const EditNameModal = ({
}: EditNameModalProps) => {
const updateName = useMutation(api.users.setName);
const [name, setName] = useState(defaultName);
const [error, setError] = useState<string>();
const [isSubmitting, setIsSubmitting] = useState(false);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
updateName({ name });
onSubmit();
setError(undefined);

if (name.length > 100) {
setError("Name must be less than 100 characters");
return;
}

try {
setIsSubmitting(true);
await updateName({ name: name.trim() });
onSubmit();
toast.success("Name updated.");
} catch (err) {
setError("Failed to update name. Please try again.");
} finally {
setIsSubmitting(false);
}
};

return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalHeader title="Edit name" />
<ModalHeader
title="Edit name"
description="How should Namesake refer to you? This can be different from your legal name."
/>
<Form onSubmit={handleSubmit} className="w-full">
{error && <Banner variant="danger">{error}</Banner>}
<TextField
name="name"
label="Name"
value={name}
onChange={setName}
onChange={(value) => {
setName(value);
setError(undefined);
}}
className="w-full"
isRequired
/>
<ModalFooter>
<Button variant="secondary" onPress={() => onOpenChange(false)}>
<Button
variant="secondary"
isDisabled={isSubmitting}
onPress={() => onOpenChange(false)}
>
Cancel
</Button>
<Button type="submit" variant="primary">
<Button type="submit" variant="primary" isDisabled={isSubmitting}>
Save
</Button>
</ModalFooter>
Expand All @@ -67,10 +98,7 @@ export const EditNameSetting = ({ user }: EditNameSettingProps) => {
const [isNameModalOpen, setIsNameModalOpen] = useState(false);

return (
<SettingsItem
label="Name"
description="How should Namesake refer to you? This can be different from your legal name."
>
<SettingsItem label="Name" description="How should Namesake refer to you?">
<Button icon={Pencil} onPress={() => setIsNameModalOpen(true)}>
{user?.name ?? "Set name"}
</Button>
Expand Down
Loading

0 comments on commit 957070c

Please sign in to comment.