Skip to content

Commit

Permalink
polish settings panel
Browse files Browse the repository at this point in the history
  • Loading branch information
steeeee0223 committed Dec 31, 2024
1 parent 33d457b commit f70eb46
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 75 deletions.
16 changes: 8 additions & 8 deletions packages/notion/src/common/base-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useLayoutEffect, useRef, useTransition } from "react";
import { useLayoutEffect, useRef } from "react";

import { useTransition } from "@swy/ui/hooks";
import {
Button,
Dialog,
Expand All @@ -16,7 +17,7 @@ interface BaseModalProps {
title: string;
primary: string;
secondary: string;
onTrigger?: () => void;
onTrigger?: () => void | Promise<void>;
}

export const BaseModal = ({
Expand All @@ -26,13 +27,12 @@ export const BaseModal = ({
onTrigger,
}: BaseModalProps) => {
const { isOpen, setClose } = useModal();
const [loading, startTransition] = useTransition();
const [trigger, loading] = useTransition(() => onTrigger?.());

const reset = () =>
startTransition(() => {
onTrigger?.();
setClose();
});
const reset = async () => {
await trigger();
setClose();
};

const buttonRef = useRef<HTMLButtonElement>(null);
useLayoutEffect(() => {
Expand Down
35 changes: 21 additions & 14 deletions packages/notion/src/settings-panel/body/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChevronRight, X } from "lucide-react";
import { useHover } from "usehooks-ts";

import { useTranslation } from "@swy/i18n";
import { useTransition } from "@swy/ui/hooks";
import { cn } from "@swy/ui/lib";
import {
Avatar,
Expand Down Expand Up @@ -41,17 +42,21 @@ export const Account = () => {
deleteAccount,
} = useSettings();
const onUpdateAvatar = () => avatarInputRef.current?.click();
const onRemoveAvatar = () => void update({ account: { avatarUrl: "" } });
const onSelectImage = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const replaceTargetUrl = account.avatarUrl;
const url = URL.createObjectURL(file);
update({ account: { avatarUrl: url } });
const res = await uploadFile?.(file, { replaceTargetUrl });
if (res?.url) update({ account: { avatarUrl: res.url } });
}
};
const [removeAvatar, isRemoving] = useTransition(() =>
update({ account: { avatarUrl: "" } }),
);
const [selectImage, isUploading] = useTransition(
async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const replaceTargetUrl = account.avatarUrl;
const url = URL.createObjectURL(file);
await update({ account: { avatarUrl: url } });
const res = await uploadFile?.(file, { replaceTargetUrl });
if (res?.url) await update({ account: { avatarUrl: res.url } });
}
},
);
const onUpdateName = (e: ChangeEvent<HTMLInputElement>) =>
update({ account: { preferredName: e.target.value } });
/** Modals */
Expand Down Expand Up @@ -94,18 +99,19 @@ export const Account = () => {
>
<AvatarImage src={account.avatarUrl} />
<AvatarFallback className="bg-primary/5">
image
{account.name[0]}
</AvatarFallback>
</Avatar>
<div
ref={avatarCancelRef}
role="button"
onClick={onRemoveAvatar}
onClick={removeAvatar}
className={cn(
buttonVariants({ variant: "subitem" }),
"absolute -right-0.5 -top-0.5 z-10 hidden size-auto rounded-full border border-border-button bg-main p-1",
(avatarIsHover || avatarCancelIsHover) && "block",
)}
aria-disabled={isRemoving || isUploading}
>
<X size={8} strokeWidth={2} />
</div>
Expand All @@ -115,7 +121,8 @@ export const Account = () => {
ref={avatarInputRef}
className="hidden"
accept="image/*"
onChange={onSelectImage}
onChange={selectImage}
disabled={isRemoving || isUploading}
/>
</div>
<div className="ml-5 w-[250px]">
Expand Down
6 changes: 3 additions & 3 deletions packages/notion/src/settings-panel/body/region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export const Region = () => {
title={t("language-region.modals.language.title", {
language: langLabel,
})}
onTrigger={() => {
updateSettings({ account: { language } });
void i18n.changeLanguage(language);
onTrigger={async () => {
await updateSettings({ account: { language } });
await i18n.changeLanguage(language);
}}
/>,
);
Expand Down
25 changes: 15 additions & 10 deletions packages/notion/src/settings-panel/body/settings2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from "react";

import { useTranslation } from "@swy/i18n";
import { useTransition } from "@swy/ui/hooks";
import { Button, Input, Separator, Switch } from "@swy/ui/shadcn";
import { IconBlock, IconMenu, useModal, type IconInfo } from "@swy/ui/shared";

Expand All @@ -29,18 +30,21 @@ export const Settings2 = () => {
/** Handlers */
const onUpdateName = (e: React.ChangeEvent<HTMLInputElement>) =>
update({ workspace: { name: e.target.value } });
const onUpdateIcon = (icon: IconInfo) => update({ workspace: { icon } });
const onRemoveIcon = () =>
update({ workspace: { icon: { type: "text", text: workspace.name } } });
const onUploadIcon = async (file: File) => {
const [updateIcon, isUpdatingIcon] = useTransition((icon: IconInfo) =>
update({ workspace: { icon } }),
);
const [removeIcon, isRemoving] = useTransition(() =>
update({ workspace: { icon: { type: "text", text: workspace.name } } }),
);
const [uploadIcon, isUploading] = useTransition(async (file: File) => {
const replaceTargetUrl =
workspace.icon.type === "file" ? workspace.icon.url : undefined;
const url = URL.createObjectURL(file);
update({ workspace: { icon: { type: "file", url } } });
await update({ workspace: { icon: { type: "file", url } } });
const res = await uploadFile?.(file, { replaceTargetUrl });
if (res?.url)
update({ workspace: { icon: { type: "file", url: res.url } } });
};
await update({ workspace: { icon: { type: "file", url: res.url } } });
});
const onUpdateDomain = (e: React.ChangeEvent<HTMLInputElement>) =>
update({ workspace: { domain: e.target.value } });
const onDeleteWorkspace = () =>
Expand All @@ -61,9 +65,10 @@ export const Settings2 = () => {
<Content {...workspaceSettings.icon}>
<div className="rounded-md border border-border p-0.5">
<IconMenu
onSelect={onUpdateIcon}
onRemove={onRemoveIcon}
onUpload={onUploadIcon}
disabled={isUpdatingIcon || isRemoving || isUploading}
onSelect={updateIcon}
onRemove={removeIcon}
onUpload={uploadIcon}
>
<IconBlock icon={workspace.icon} size="lg" />
</IconMenu>
Expand Down
15 changes: 7 additions & 8 deletions packages/notion/src/settings-panel/modals/delete-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,10 @@ const userSchema = z.object({

interface DeleteAccountProps {
email: string;
onSubmit?: (email: string) => void;
onSubmit?: (email: string) => void | Promise<void>;
}

export const DeleteAccount = ({
email,
onSubmit: $onSubmit,
}: DeleteAccountProps) => {
export const DeleteAccount = ({ email, onSubmit }: DeleteAccountProps) => {
const { isOpen, setClose } = useModal();
const form = useForm<z.infer<typeof userSchema>>({
resolver: zodResolver(userSchema),
Expand All @@ -41,10 +38,10 @@ export const DeleteAccount = ({
form.reset();
form.clearErrors();
};
const onSubmit = (value: z.infer<typeof userSchema>) => {
const submit = async (value: z.infer<typeof userSchema>) => {
if (value.email === email) {
onClose();
$onSubmit?.(email);
await onSubmit?.(email);
} else {
form.reset();
}
Expand Down Expand Up @@ -74,7 +71,7 @@ export const DeleteAccount = ({
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
onSubmit={form.handleSubmit(submit)}
className="flex w-full flex-col"
style={{ marginTop: 0 }}
>
Expand Down Expand Up @@ -105,6 +102,7 @@ export const DeleteAccount = ({
variant="red:fill"
size="sm"
className="mt-6 w-full"
disabled={form.formState.isSubmitting}
>
Permanently delete account
</Button>
Expand All @@ -114,6 +112,7 @@ export const DeleteAccount = ({
variant="hint"
size="sm"
className="mt-3 h-7 w-fit"
disabled={form.formState.isSubmitting}
>
Cancel
</Button>
Expand Down
15 changes: 7 additions & 8 deletions packages/notion/src/settings-panel/modals/delete-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,10 @@ const workspaceSchema = z.object({

interface DeleteWorkspaceProps {
name: string;
onSubmit?: (name: string) => void;
onSubmit?: (name: string) => void | Promise<void>;
}

export const DeleteWorkspace = ({
name,
onSubmit: $onSubmit,
}: DeleteWorkspaceProps) => {
export const DeleteWorkspace = ({ name, onSubmit }: DeleteWorkspaceProps) => {
const { isOpen, setClose } = useModal();
const form = useForm<z.infer<typeof workspaceSchema>>({
resolver: zodResolver(workspaceSchema),
Expand All @@ -40,10 +37,10 @@ export const DeleteWorkspace = ({
form.reset();
form.clearErrors();
};
const onSubmit = (value: z.infer<typeof workspaceSchema>) => {
const submit = async (value: z.infer<typeof workspaceSchema>) => {
if (value.name === name) {
onClose();
$onSubmit?.(name);
await onSubmit?.(name);
} else {
form.reset();
}
Expand Down Expand Up @@ -73,7 +70,7 @@ export const DeleteWorkspace = ({
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
onSubmit={form.handleSubmit(submit)}
className="flex w-full flex-col"
style={{ marginTop: 0 }}
>
Expand All @@ -98,6 +95,7 @@ export const DeleteWorkspace = ({
variant="red:fill"
size="sm"
className="mt-6 w-full"
disabled={form.formState.isSubmitting}
>
Permanently delete workspace
</Button>
Expand All @@ -107,6 +105,7 @@ export const DeleteWorkspace = ({
variant="hint"
size="sm"
className="mt-3 h-7 w-fit"
disabled={form.formState.isSubmitting}
>
Cancel
</Button>
Expand Down
14 changes: 6 additions & 8 deletions packages/notion/src/settings-panel/modals/password-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,10 @@ const passwordSchema = z

interface PasswordFormProps {
hasPassword?: boolean;
onSubmit?: (pass: string, original?: string | null) => void;
onSubmit?: (pass: string, original?: string | null) => void | Promise<void>;
}

export const PasswordForm = ({
hasPassword,
onSubmit: __onSubmit,
}: PasswordFormProps) => {
export const PasswordForm = ({ hasPassword, onSubmit }: PasswordFormProps) => {
const { isOpen, setClose, setOpen } = useModal();
const form = useForm<z.infer<typeof passwordSchema>>({
resolver: zodResolver(passwordSchema),
Expand All @@ -57,11 +54,11 @@ export const PasswordForm = ({
setClose();
form.reset();
};
const onSubmit = ({
const submit = async ({
password,
currentPassword,
}: z.infer<typeof passwordSchema>) => {
__onSubmit?.(password, currentPassword);
await onSubmit?.(password, currentPassword);
onClose();
setOpen(<PasswordSuccess />);
};
Expand All @@ -80,7 +77,7 @@ export const PasswordForm = ({
noTitle
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="relative">
<form onSubmit={form.handleSubmit(submit)} className="relative">
<div className="my-4 flex justify-center">
<Icon.Password className="size-[27px] flex-shrink-0 fill-primary/85" />
</div>
Expand Down Expand Up @@ -151,6 +148,7 @@ export const PasswordForm = ({
variant="blue"
size="sm"
className="mt-4 w-full"
disabled={form.formState.isSubmitting}
>
{hasPassword ? "Change password" : "Set a password"}
</Button>
Expand Down
4 changes: 2 additions & 2 deletions packages/notion/src/sidebar/modals/settings-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export const SettingsModal = (props: SettingsPanelProps) => {
...props,
onDeleteAccount: (data) => {
setClose();
props.onDeleteAccount?.(data);
void props.onDeleteAccount?.(data);
},
onDeleteWorkspace: (data) => {
setClose();
props.onDeleteWorkspace?.(data);
void props.onDeleteWorkspace?.(data);
},
};

Expand Down
35 changes: 21 additions & 14 deletions packages/ui/src/hooks/use-transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@

import { useCallback, useState } from "react";

type Action<T> = (() => T) | (() => Promise<T>);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Action<T, Args extends any[]> =
| ((...args: Args) => T)
| ((...args: Args) => Promise<T>);

export const useTransition = <T>(fn: Action<T>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useTransition = <T, Args extends any[]>(fn: Action<T, Args>) => {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<Error>();

const action = useCallback(async () => {
try {
setIsPending(true);
const response = fn();
if (response instanceof Promise) return await response;
return response;
} catch (error) {
setError(error as Error);
} finally {
setIsPending(false);
}
}, [fn]);
const action = useCallback<Action<T | undefined, Args>>(
async (...args) => {
try {
setIsPending(true);
const response = fn(...args);
if (response instanceof Promise) return await response;
return response;
} catch (error) {
setError(error as Error);
} finally {
setIsPending(false);
}
},
[fn],
);

return [action, isPending, error] as const;
};

0 comments on commit f70eb46

Please sign in to comment.