Skip to content

Commit

Permalink
Rewrite profile settings page to server component (formbricks#642)
Browse files Browse the repository at this point in the history
* Chore: moved profile settings to server component

* ran pnpm format

* fisxed a build issue

* made requested changes

* made some refactors

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
  • Loading branch information
Dhruwang and mattinannt committed Aug 9, 2023
1 parent 2ff508a commit 3439220
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 92 deletions.
Original file line number Diff line number Diff line change
@@ -1,81 +1,16 @@
"use client";

import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import AvatarPlaceholder from "@/images/avatar-placeholder.png";
import { formbricksLogout } from "@/lib/formbricks";
import { useProfileMutation } from "@/lib/profile/mutateProfile";
import { useProfile } from "@/lib/profile/profile";
import { deleteProfile } from "@/lib/users/users";
import { Button, ErrorComponent, Input, Label, ProfileAvatar } from "@formbricks/ui";
import { Button, Input, ProfileAvatar } from "@formbricks/ui";
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
import Image from "next/image";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";

export function EditName() {
const { register, handleSubmit, control, setValue } = useForm();
const { profile, isLoadingProfile, isErrorProfile } = useProfile();

const { triggerProfileMutate, isMutatingProfile } = useProfileMutation();

const profileName = useWatch({
control,
name: "name",
});
const isProfileNameInputEmpty = !profileName?.trim();
const currentProfileName = profileName?.trim().toLowerCase() ?? "";
const previousProfileName = profile?.name?.trim().toLowerCase() ?? "";

useEffect(() => {
setValue("name", profile?.name ?? "");
}, [profile?.name]);

if (isLoadingProfile) {
return <LoadingSpinner />;
}
if (isErrorProfile) {
return <ErrorComponent />;
}

return (
<form
className="w-full max-w-sm items-center"
onSubmit={handleSubmit((data) => {
triggerProfileMutate(data)
.then(() => {
toast.success("Your name was updated successfully.");
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
})}>
<Label htmlFor="fullname">Full Name</Label>
<Input
type="text"
id="fullname"
defaultValue={profile.name}
{...register("name")}
className={isProfileNameInputEmpty ? "border-red-300 focus:border-red-300" : ""}
/>

<div className="mt-4">
<Label htmlFor="email">Email</Label>
<Input type="email" id="fullname" defaultValue={profile.email} disabled />
</div>
<Button
type="submit"
variant="darkCTA"
className="mt-4"
loading={isMutatingProfile}
disabled={isProfileNameInputEmpty || currentProfileName === previousProfileName}>
Update
</Button>
</form>
);
}
import { profileDeleteAction } from "./actions";
import { TProfile } from "@formbricks/types/v1/profile";

export function EditAvatar({ session }) {
return (
Expand Down Expand Up @@ -103,9 +38,10 @@ interface DeleteAccountModalProps {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
session: Session;
profile: TProfile;
}

function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps) {
function DeleteAccountModal({ setOpen, open, session, profile }: DeleteAccountModalProps) {
const [deleting, setDeleting] = useState(false);
const [inputValue, setInputValue] = useState("");

Expand All @@ -116,7 +52,7 @@ function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps)
const deleteAccount = async () => {
try {
setDeleting(true);
await deleteProfile();
await profileDeleteAction(profile.id);
await signOut();
await formbricksLogout();
} catch (error) {
Expand Down Expand Up @@ -169,7 +105,7 @@ function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps)
);
}

export function DeleteAccount({ session }: { session: Session | null }) {
export function DeleteAccount({ session, profile }: { session: Session | null; profile: TProfile }) {
const [isModalOpen, setModalOpen] = useState(false);

if (!session) {
Expand All @@ -178,7 +114,7 @@ export function DeleteAccount({ session }: { session: Session | null }) {

return (
<div>
<DeleteAccountModal open={isModalOpen} setOpen={setModalOpen} session={session} />
<DeleteAccountModal open={isModalOpen} setOpen={setModalOpen} session={session} profile={profile} />
<p className="text-sm text-slate-700">
Delete your account with all personal data. <strong>This cannot be undone!</strong>
</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import AvatarPlaceholder from "@/images/avatar-placeholder.png";
import { Button, ProfileAvatar } from "@formbricks/ui";
import Image from "next/image";
import { Session } from "next-auth";

export function EditAvatar({ session }:{session: Session | null}) {
return (
<div>
{session?.user?.image ? (
<Image
src={AvatarPlaceholder}
width="100"
height="100"
className="h-24 w-24 rounded-full"
alt="Avatar placeholder"
/>
) : (
<ProfileAvatar userId={session!.user.id} />
)}

<Button className="mt-4" variant="darkCTA" disabled={true}>
Upload Image
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { Button, Input, Label } from "@formbricks/ui";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { profileEditAction } from "./actions";
import { TProfile } from "@formbricks/types/v1/profile";

export function EditName({ profile }: { profile: TProfile }) {

const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm<{name:string}>()

return (
<>
<form
className="w-full max-w-sm items-center"
onSubmit={handleSubmit(async(data) => {
try {
await profileEditAction(profile.id, data);
toast.success("Your name was updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);
}
})}>
<Label htmlFor="fullname">Full Name</Label>
<Input
type="text"
id="fullname"
defaultValue={profile.name ? profile.name : ""}
{...register("name")}
/>

<div className="mt-4">
<Label htmlFor="email">Email</Label>
<Input type="email" id="fullname" defaultValue={profile.email} disabled />
</div>
<Button type="submit" variant="darkCTA" className="mt-4" loading={isSubmitting}>
Update
</Button>
</form>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use server";

import { updateProfile, deleteProfile } from "@formbricks/lib/services/profile";
import { Prisma } from "@prisma/client";

export async function profileEditAction(userId: string, data: Prisma.UserUpdateInput) {
return await updateProfile(userId, data);
}

export async function profileDeleteAction(userId: string) {
return await deleteProfile(userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
function LoadingCard({ title, description, skeletonLines }) {
return (
<div className="my-4 rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-100 px-6 py-5 text-left text-slate-900">
<h3 className="text-lg font-medium leading-6">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<div className="w-full">
<div className="rounded-lg px-6 py-5 hover:bg-slate-100">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-gray-200 ${line.classes}`}></div>
</div>
))}
</div>
</div>
</div>
);
}

export default function Loading() {
const cards = [
{
title: "Personal Information",
description: "Update your personal information",
skeletonLines: [
{ classes: "h-4 w-28" },
{ classes: "h-6 w-64" },
{ classes: "h-4 w-28" },
{ classes: "h-6 w-64" },
{ classes: "h-8 w-24" },
],
},
{
title: "Avatar",
description: "Assist your team in identifying you on Formbricks.",
skeletonLines: [{ classes: "h-10 w-10" }, { classes: "h-8 w-24" }],
},
{
title: "Delete account",
description: "Delete your account with all of your personal information and data.",
skeletonLines: [{ classes: "h-4 w-60" }, { classes: "h-8 w-24" }],
},
];

return (
<div>
<h2 className="my-4 text-2xl font-medium leading-6 text-slate-800">Profile</h2>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
import SettingsCard from "../SettingsCard";
import SettingsTitle from "../SettingsTitle";
export const revalidate = REVALIDATION_INTERVAL;

import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getServerSession } from "next-auth";
import { EditName, EditAvatar, DeleteAccount } from "./editProfile";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import SettingsCard from "../SettingsCard";
import SettingsTitle from "../SettingsTitle";
import { DeleteAccount } from "./DeleteAccount";
import { EditName } from "./EditName";
import { EditAvatar } from "./EditAvatar";
import { getProfile } from "@formbricks/lib/services/profile";

export default async function ProfileSettingsPage() {
const session = await getServerSession(authOptions);
const profile = session ? await getProfile(session.user.id) : null;

return (
<div>
<SettingsTitle title="Profile" />
<SettingsCard title="Personal Information" description="Update your personal information.">
<EditName />
</SettingsCard>
<SettingsCard title="Avatar" description="Assist your team in identifying you on Formbricks.">
<EditAvatar session={session} />
</SettingsCard>
<SettingsCard
title="Delete account"
description="Delete your account with all of your personal information and data.">
<DeleteAccount session={session} />
</SettingsCard>
</div>
<>
{profile && (
<div>
<SettingsTitle title="Profile" />
<SettingsCard title="Personal Information" description="Update your personal information.">
<EditName profile={profile} />
</SettingsCard>
<SettingsCard title="Avatar" description="Assist your team in identifying you on Formbricks.">
<EditAvatar session={session} />
</SettingsCard>
<SettingsCard
title="Delete account"
description="Delete your account with all of your personal information and data.">
<DeleteAccount session={session} profile={profile} />
</SettingsCard>
</div>
)}
</>
);
}
Loading

0 comments on commit 3439220

Please sign in to comment.