web: edit profile directly

This commit is contained in:
Abdullah Atta
2024-03-01 16:42:26 +05:00
parent 1efae55a85
commit b9cd40937d
5 changed files with 167 additions and 116 deletions

View File

@@ -481,8 +481,8 @@ export function showAttachmentsDialog() {
));
}
export function showEditProfileDialog(profile?: Profile) {
return showDialog("EditProfileDialog", (Dialog, perform) => (
export function showEditProfilePictureDialog(profile?: Profile) {
return showDialog("EditProfilePictureDialog", (Dialog, perform) => (
<Dialog onClose={(res: boolean) => perform(res)} profile={profile} />
));
}

View File

@@ -20,9 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Perform } from "../common/dialog-controller";
import Dialog from "../components/dialog";
import { Profile } from "@notesnook/core";
import Field from "../components/field";
import AvatarEditor from "react-avatar-editor";
import { Avatar, Button, Flex, Slider } from "@theme-ui/components";
import { Button, Flex, Slider } from "@theme-ui/components";
import { User } from "../components/icons";
import { useRef, useState } from "react";
import { showFilePicker } from "../utils/file-picker";
@@ -30,20 +29,19 @@ import { db } from "../common/db";
import { useStore as useUserStore } from "../stores/user-store";
import { showToast } from "../utils/toast";
export type EditProfileDialogProps = {
export type EditProfilePictureDialogProps = {
onClose: Perform;
profile?: Profile;
};
export default function EditProfileDialog(props: EditProfileDialogProps) {
export default function EditProfilePictureDialog(
props: EditProfilePictureDialogProps
) {
const { profile } = props;
const [profilePicture, setProfilePicture] = useState<
File | string | undefined
>(profile?.profilePicture);
const profileRef = useRef<AvatarEditor>(null);
const [fullName, setFullName] = useState<string | undefined>(
profile?.fullName
);
const [scale, setScale] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [clearProfilePicture, setClearProfilePicture] = useState(false);
@@ -51,7 +49,7 @@ export default function EditProfileDialog(props: EditProfileDialogProps) {
return (
<Dialog
isOpen={true}
title={"Edit profile"}
title={"Edit profile picture"}
description="Your profile data is stored 100% end-to-end encrypted."
onClose={() => props.onClose(false)}
positiveButton={{
@@ -61,17 +59,19 @@ export default function EditProfileDialog(props: EditProfileDialogProps) {
onClick: async () => {
setIsLoading(true);
try {
await db.user.setProfile({
fullName,
profilePicture: profileRef.current
? profileRef.current
.getImageScaledToCanvas()
.toDataURL("image/jpeg", 1)
: clearProfilePicture
const pic = profileRef.current
? profileRef.current
.getImageScaledToCanvas()
.toDataURL("image/jpeg", 0.8)
: clearProfilePicture
? undefined
: profile?.profilePicture
: profile?.profilePicture;
await db.user.setProfile({
profilePicture: pic
});
useUserStore.setState({
profile: { ...profile, profilePicture: pic }
});
await useUserStore.getState().refreshUser();
showToast("success", "Profile updated!");
props.onClose(true);
} catch (e) {
@@ -82,84 +82,86 @@ export default function EditProfileDialog(props: EditProfileDialogProps) {
}
}
}}
width={400}
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
>
<Flex sx={{ gap: 2, mt: 2 }}>
<Flex sx={{ flexDirection: "column" }}>
<Flex
sx={{
flexDirection: "column"
}}
>
{profilePicture ? (
<AvatarEditor
ref={profileRef}
image={profilePicture}
width={256}
height={256}
border={1}
color={[255, 255, 255]}
borderRadius={256}
scale={scale}
style={{
width: 150,
height: 150,
alignSelf: "center"
}}
/>
) : (
<Flex
variant="columnCenter"
sx={{
bg: "shade",
mr: 2,
size: 150,
borderRadius: 200,
alignSelf: "center"
}}
>
<User size={60} />
</Flex>
)}
<Flex sx={{ gap: 1, alignItems: "center", mt: 2 }}>
<Button
sx={{ flex: 1 }}
variant="secondary"
onClick={async () => {
const file = await showFilePicker({
acceptedFileTypes: "image/*"
});
if (!file) return;
setProfilePicture(file);
setScale(1);
}}
>
{profilePicture
? "Change profile picture"
: "Select profile picture"}
</Button>
{profilePicture ? (
<AvatarEditor
ref={profileRef}
image={profilePicture}
width={150}
height={150}
border={0}
color={[255, 255, 255]}
borderRadius={100}
scale={scale}
style={{
width: 150,
height: 150
}}
/>
) : (
<Flex
variant="columnCenter"
sx={{
bg: "shade",
mr: 2,
size: 150,
borderRadius: 200,
alignSelf: "center"
}}
>
<User size={60} />
</Flex>
)}
<Flex sx={{ gap: 1, alignItems: "center", mt: 2 }}>
<Button
sx={{ flex: 1 }}
variant="secondary"
onClick={async () =>
setProfilePicture(
await showFilePicker({ acceptedFileTypes: "image/*" })
)
}
>
{profilePicture ? "Change" : "Set picture"}
</Button>
{profilePicture ? (
<Button
sx={{ flex: 1 }}
variant="secondary"
onClick={async () => {
if (profile?.profilePicture) setClearProfilePicture(true);
onClick={async () => {
if (typeof profilePicture === "string") {
setClearProfilePicture(true);
setProfilePicture(undefined);
setScale(1);
}}
>
{profile?.profilePicture ? "Clear" : "Reset"}
</Button>
) : null}
</Flex>
{profilePicture ? (
<Slider
max={5}
min={1}
step={0.1}
value={scale}
sx={{ color: "accent" }}
onChange={(e) => setScale(e.target.valueAsNumber)}
/>
} else setProfilePicture(profile?.profilePicture);
setScale(1);
}}
>
{typeof profilePicture === "string" ? "Clear" : "Reset"}
</Button>
) : null}
</Flex>
<Field
label="Full name"
maxLength={200}
value={fullName}
autoFocus
onChange={(e) => setFullName(e.target.value)}
/>
{profilePicture ? (
<Slider
max={5}
min={1}
step={0.1}
value={scale}
sx={{ color: "accent" }}
onChange={(e) => setScale(e.target.valueAsNumber)}
/>
) : null}
</Flex>
</Dialog>
);

View File

@@ -56,7 +56,7 @@ const BackupPasswordDialog = React.lazy(
() => import("./backup-password-dialog")
);
const CreateColorDialog = React.lazy(() => import("./create-color-dialog"));
const EditProfileDialog = React.lazy(() => import("./edit-profile-dialog"));
const EditProfilePictureDialog = React.lazy(() => import("./edit-profile-picture-dialog"));
export const Dialogs = {
AddNotebookDialog,
@@ -87,5 +87,5 @@ export const Dialogs = {
ThemeDetailsDialog,
BackupPasswordDialog,
CreateColorDialog,
EditProfileDialog
EditProfilePictureDialog
};

View File

@@ -44,7 +44,14 @@ export default function Prompt(props: PromptDialogProps) {
}}
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
>
<Field inputRef={inputRef} defaultValue={props.defaultValue} autoFocus />
<Field
inputRef={inputRef}
defaultValue={props.defaultValue}
autoFocus
onKeyUp={(e) => {
if (e.key == "Enter") props.onSave(inputRef.current?.value || "");
}}
/>
</Dialog>
);
}

View File

@@ -25,7 +25,13 @@ import { getFormattedDate } from "@notesnook/common";
import { SUBSCRIPTION_STATUS } from "../../../common/constants";
import dayjs from "dayjs";
import { useMemo } from "react";
import { showEditProfileDialog } from "../../../common/dialog-controller";
import {
showEditProfileDialog,
showEditProfilePictureDialog,
showPromptDialog
} from "../../../common/dialog-controller";
import { db } from "../../../common/db";
import { showToast } from "../../../utils/toast";
export function UserProfile() {
const user = useUserStore((store) => store.user);
@@ -104,7 +110,11 @@ export function UserProfile() {
mr: 2,
size: 60,
borderRadius: 80,
overflow: "hidden"
overflow: "hidden",
position: "relative",
":hover #profile-picture-edit": {
visibility: "visible"
}
}}
>
{profile?.profilePicture ? (
@@ -115,6 +125,30 @@ export function UserProfile() {
) : (
<User size={30} />
)}
<Flex
id="profile-picture-edit"
sx={{
position: "absolute",
width: "100%",
height: "100%",
bg: "#000000aa",
top: 0,
left: 0,
alignItems: "center",
alignContent: "center",
justifyContent: "center",
visibility: "collapse",
cursor: "pointer"
}}
title="Edit profile picture"
onClick={async () => {
await showEditProfilePictureDialog(profile);
}}
>
<Text variant="body" sx={{ color: "white" }}>
Edit
</Text>
</Flex>
</Flex>
<Flex sx={{ flexDirection: "column" }}>
<Text
@@ -126,32 +160,40 @@ export function UserProfile() {
{remainingDays > 0 && (isPro || isProCancelled)
? `PRO`
: remainingDays > 0 && isTrial
? "TRIAL"
: isBeta
? "BETA TESTER"
: "BASIC"}
? "TRIAL"
: isBeta
? "BETA TESTER"
: "BASIC"}
</Text>
{profile?.fullName ? (
<>
<Text variant={"title"}>{profile?.fullName}</Text>
<Text variant={"subBody"}>
{user.email} Member since{" "}
{getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</>
) : (
<>
<Text variant={"title"}>{user.email}</Text>
<Text variant={"subBody"}>
Member since
{getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</>
)}
<Text variant={"title"}>
{profile?.fullName || "Your full name"}{" "}
<Edit
sx={{ display: "inline-block", cursor: "pointer" }}
size={12}
title="Edit full name"
onClick={async () => {
const fullName = await showPromptDialog({
title: "Edit your full name",
description:
"Your profile data is 100% end-to-end encrypted.",
defaultValue: profile?.fullName
});
useUserStore.setState({
profile: { ...profile, fullName: fullName || undefined }
});
await db.user.setProfile({ fullName: fullName || undefined });
showToast("success", "Full name updated!");
}}
/>
</Text>
<Text variant={"subBody"}>
{user.email} Member since{" "}
{getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</Flex>
</Flex>
<Button
{/* <Button
variant="icon"
sx={{
borderRadius: 50,
@@ -165,7 +207,7 @@ export function UserProfile() {
onClick={() => showEditProfileDialog(profile)}
>
<Edit size={18} />
</Button>
</Button> */}
</Flex>
);
}