Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: Rewrite profile settings to server component #642

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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