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 3 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
2 changes: 1 addition & 1 deletion apps/formbricks-com/pages/api/oss-friends/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
"Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.",
href: "https://typebot.io",
},
{
{
name: "Twenty",
description:
"A modern CRM offering the flexibility of open-source, advanced features and sleek design.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +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, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";

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

const { triggerProfileMutate, isMutatingProfile } = useProfileMutation();

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")} />

<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}>
Update
</Button>
</form>
);
}
import { profileDeleteAction } from "./action";
import { TProfile } from "@formbricks/types/v1/profile";

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

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

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

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 @@ -155,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,27 @@
"use client";

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

export function EditAvatar({ session }) {
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,38 @@
"use client"

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

export function EditName({ profile }:{ profile: TProfile}) {
const { register, handleSubmit , formState: { isSubmitting } } = useForm();

return (
<>
{profile && <form
className="w-full max-w-sm items-center"
onSubmit={handleSubmit((data) => {
profileEditAction(profile.id, 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 ? 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>}
</>
);
}
Dhruwang marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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 EditProfile({ session }) {
const profile = await getProfile(session.user.id);
return (
<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>
);
}
Dhruwang marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use server";

import { updateProfile } from "@formbricks/lib/services/profile";
import { deleteProfile } from "@formbricks/lib/services/profile";
Dhruwang marked this conversation as resolved.
Show resolved Hide resolved

export async function profileEditAction(userId: string, data: Object) {
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,11 @@
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 EditProfile from "./EditProfile";

export default async function ProfileSettingsPage() {
const session = await getServerSession(authOptions);
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>
);
return <>{session && <EditProfile session={session} />}</>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ export default function PreviewSurvey({
switch (logic.condition) {
case "equals":
return (
(Array.isArray(responseValue) && responseValue.length === 1 && responseValue.includes(logic.value)) ||
(Array.isArray(responseValue) &&
responseValue.length === 1 &&
responseValue.includes(logic.value)) ||
responseValue.toString() === logic.value
);
case "notEquals":
Expand Down
4 changes: 3 additions & 1 deletion apps/web/components/preview/MultipleChoiceMultiQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export default function MultipleChoiceMultiQuestion({
.map((choice) => choice.label);

useEffect(() => {
const nonOtherSavedChoices = storedResponseValue?.filter((answer) => nonOtherChoiceLabels.includes(answer));
const nonOtherSavedChoices = storedResponseValue?.filter((answer) =>
nonOtherChoiceLabels.includes(answer)
);
const savedOtherSpecified = storedResponseValue?.find((answer) => !nonOtherChoiceLabels.includes(answer));

setSelectedChoices(nonOtherSavedChoices ?? []);
Expand Down
4 changes: 3 additions & 1 deletion apps/web/components/preview/MultipleChoiceSingleQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export default function MultipleChoiceSingleQuestion({
goToNextQuestion,
goToPreviousQuestion,
}: MultipleChoiceSingleProps) {
const storedResponseValueValue = question.choices.find((choice) => choice.label === storedResponseValue)?.id;
const storedResponseValueValue = question.choices.find(
(choice) => choice.label === storedResponseValue
)?.id;
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
const [savedOtherAnswer, setSavedOtherAnswer] = useState<string | null>(null);
const [questionChoices, setQuestionChoices] = useState<TSurveyChoice[]>(
Expand Down
4 changes: 3 additions & 1 deletion apps/web/components/preview/NPSQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export default function NPSQuestion({
/>
)}
<div></div>
{(!question.required || storedResponseValue) && <SubmitButton {...{ question, lastQuestion, brandColor }} />}
{(!question.required || storedResponseValue) && (
<SubmitButton {...{ question, lastQuestion, brandColor }} />
)}
</div>
</form>
);
Expand Down
4 changes: 3 additions & 1 deletion apps/web/components/preview/RatingQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ export default function RatingQuestion({
/>
)}
<div></div>
{(!question.required || storedResponseValue) && <SubmitButton {...{ question, lastQuestion, brandColor }} />}
{(!question.required || storedResponseValue) && (
<SubmitButton {...{ question, lastQuestion, brandColor }} />
)}
</div>
</form>
);
Expand Down
5 changes: 4 additions & 1 deletion apps/web/lib/linkSurvey/linkSurvey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,10 @@ const storeResponse = (surveyId: string, answer: Response["data"]) => {
const storedResponses = localStorage.getItem(`formbricks-${surveyId}-responses`);
if (storedResponses) {
const parsedResponses = JSON.parse(storedResponses);
localStorage.setItem(`formbricks-${surveyId}-responses`, JSON.stringify({ ...parsedResponses, ...answer }));
localStorage.setItem(
`formbricks-${surveyId}-responses`,
JSON.stringify({ ...parsedResponses, ...answer })
);
} else {
localStorage.setItem(`formbricks-${surveyId}-responses`, JSON.stringify(answer));
}
Expand Down
4 changes: 3 additions & 1 deletion packages/js/src/components/MultipleChoiceMultiQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export default function MultipleChoiceMultiQuestion({
.map((choice) => choice.label);

useEffect(() => {
const nonOtherSavedChoices = storedResponseValue?.filter((answer) => nonOtherChoiceLabels.includes(answer));
const nonOtherSavedChoices = storedResponseValue?.filter((answer) =>
nonOtherChoiceLabels.includes(answer)
);
const savedOtherSpecified = storedResponseValue?.find((answer) => !nonOtherChoiceLabels.includes(answer));

setSelectedChoices(nonOtherSavedChoices ?? []);
Expand Down
4 changes: 3 additions & 1 deletion packages/js/src/components/MultipleChoiceSingleQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export default function MultipleChoiceSingleQuestion({
goToNextQuestion,
goToPreviousQuestion,
}: MultipleChoiceSingleProps) {
const storedResponseValueValue = question.choices.find((choice) => choice.label === storedResponseValue)?.id;
const storedResponseValueValue = question.choices.find(
(choice) => choice.label === storedResponseValue
)?.id;
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
const [savedOtherAnswer, setSavedOtherAnswer] = useState<string | null>(null);
const [questionChoices, setQuestionChoices] = useState<TSurveyChoice[]>(
Expand Down
Loading