mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-24 06:28:49 -05:00
refactor: Move Onboarding to server components (#816)
* added data fetching to server side and also added some actions * made refactors * replaced router.push with redirect * updated profileUpdateInput * move components in components folder * update import for product update action --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
041bf36f20
commit
e741f7d8e5
@@ -0,0 +1,14 @@
|
||||
"use server";
|
||||
|
||||
import { updateProduct } from "@formbricks/lib/services/product";
|
||||
import { updateProfile } from "@formbricks/lib/services/profile";
|
||||
import { TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
import { TProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
|
||||
export async function updateProfileAction(personId: string, updatedProfile: Partial<TProfileUpdateInput>) {
|
||||
return await updateProfile(personId, updatedProfile);
|
||||
}
|
||||
|
||||
export async function updateProductAction(productId: string, updatedProduct: Partial<TProductUpdateInput>) {
|
||||
return await updateProduct(productId, updatedProduct);
|
||||
}
|
||||
+10
-8
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { formbricksEnabled, updateResponse } from "@/lib/formbricks";
|
||||
import { useProfile } from "@/lib/profile";
|
||||
import { useProfileMutation } from "@/lib/profile/mutateProfile";
|
||||
import { ResponseId } from "@formbricks/js";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Objective } from "@formbricks/types/templates";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -15,6 +15,7 @@ type ObjectiveProps = {
|
||||
next: () => void;
|
||||
skip: () => void;
|
||||
formbricksResponseId?: ResponseId;
|
||||
profile: TProfile;
|
||||
};
|
||||
|
||||
type ObjectiveChoice = {
|
||||
@@ -22,7 +23,7 @@ type ObjectiveChoice = {
|
||||
id: Objective;
|
||||
};
|
||||
|
||||
const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId }) => {
|
||||
const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId, profile }) => {
|
||||
const objectives: Array<ObjectiveChoice> = [
|
||||
{ label: "Increase conversion", id: "increase_conversion" },
|
||||
{ label: "Improve user retention", id: "improve_user_retention" },
|
||||
@@ -32,19 +33,20 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId
|
||||
{ label: "Other", id: "other" },
|
||||
];
|
||||
|
||||
const { profile } = useProfile();
|
||||
const { triggerProfileMutate, isMutatingProfile } = useProfileMutation();
|
||||
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const [isProfileUpdating, setIsProfileUpdating] = useState(false);
|
||||
|
||||
const handleNextClick = async () => {
|
||||
if (selectedChoice) {
|
||||
const selectedObjective = objectives.find((objective) => objective.label === selectedChoice);
|
||||
if (selectedObjective) {
|
||||
try {
|
||||
setIsProfileUpdating(true);
|
||||
const updatedProfile = { ...profile, objective: selectedObjective.id };
|
||||
await triggerProfileMutate(updatedProfile);
|
||||
await updateProfileAction(profile.id, updatedProfile);
|
||||
setIsProfileUpdating(false);
|
||||
} catch (e) {
|
||||
setIsProfileUpdating(false);
|
||||
console.error(e);
|
||||
toast.error("An error occured saving your settings");
|
||||
}
|
||||
@@ -116,7 +118,7 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId
|
||||
<Button
|
||||
size="lg"
|
||||
variant="darkCTA"
|
||||
loading={isMutatingProfile}
|
||||
loading={isProfileUpdating}
|
||||
disabled={!selectedChoice}
|
||||
onClick={handleNextClick}
|
||||
id="objective-next">
|
||||
+28
-34
@@ -1,38 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { Logo } from "@/components/Logo";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useProfile } from "@/lib/profile";
|
||||
import { useProfileMutation } from "@/lib/profile/mutateProfile";
|
||||
import { fetcher } from "@formbricks/lib/fetcher";
|
||||
import { ProgressBar } from "@formbricks/ui";
|
||||
import { Session } from "next-auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import useSWR from "swr";
|
||||
import Greeting from "./Greeting";
|
||||
import Objective from "./Objective";
|
||||
import Product from "./Product";
|
||||
import Role from "./Role";
|
||||
import { ResponseId } from "@formbricks/js";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
|
||||
|
||||
const MAX_STEPS = 6;
|
||||
|
||||
interface OnboardingProps {
|
||||
session: Session | null;
|
||||
environmentId: string;
|
||||
profile: TProfile;
|
||||
product: TProduct;
|
||||
}
|
||||
|
||||
export default function Onboarding({ session }: OnboardingProps) {
|
||||
const {
|
||||
data: environment,
|
||||
error: isErrorEnvironment,
|
||||
isLoading: isLoadingEnvironment,
|
||||
} = useSWR(`/api/v1/environments/find-first`, fetcher);
|
||||
|
||||
const { profile } = useProfile();
|
||||
|
||||
const { triggerProfileMutate } = useProfileMutation();
|
||||
export default function Onboarding({ session, environmentId, profile, product }: OnboardingProps) {
|
||||
const [formbricksResponseId, setFormbricksResponseId] = useState<ResponseId | undefined>();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -42,18 +34,6 @@ export default function Onboarding({ session }: OnboardingProps) {
|
||||
return currentStep / MAX_STEPS;
|
||||
}, [currentStep]);
|
||||
|
||||
if (!profile || isLoadingEnvironment) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorEnvironment) {
|
||||
return <div className="flex h-full w-full items-center justify-center">An error occurred</div>;
|
||||
}
|
||||
|
||||
const skipStep = () => {
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
@@ -75,10 +55,10 @@ export default function Onboarding({ session }: OnboardingProps) {
|
||||
|
||||
try {
|
||||
const updatedProfile = { ...profile, onboardingCompleted: true };
|
||||
await triggerProfileMutate(updatedProfile);
|
||||
await updateProfileAction(profile.id, updatedProfile);
|
||||
|
||||
if (environment) {
|
||||
router.push(`/environments/${environment.id}/surveys`);
|
||||
if (environmentId) {
|
||||
router.push(`/environments/${environmentId}/surveys`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -105,14 +85,28 @@ export default function Onboarding({ session }: OnboardingProps) {
|
||||
<div className="col-span-2" />
|
||||
</div>
|
||||
<div className="flex grow items-center justify-center">
|
||||
{currentStep === 1 && <Greeting next={next} skip={doLater} name={profile.name} session={session} />}
|
||||
{currentStep === 1 && (
|
||||
<Greeting next={next} skip={doLater} name={profile.name ? profile.name : ""} session={session} />
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<Role next={next} skip={skipStep} setFormbricksResponseId={setFormbricksResponseId} />
|
||||
<Role
|
||||
next={next}
|
||||
skip={skipStep}
|
||||
setFormbricksResponseId={setFormbricksResponseId}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<Objective next={next} skip={skipStep} formbricksResponseId={formbricksResponseId} />
|
||||
<Objective
|
||||
next={next}
|
||||
skip={skipStep}
|
||||
formbricksResponseId={formbricksResponseId}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<Product done={done} environmentId={environmentId} isLoading={isLoading} product={product} />
|
||||
)}
|
||||
{currentStep === 4 && <Product done={done} environmentId={environment.id} isLoading={isLoading} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
+9
-10
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { updateProductAction } from "@/app/(app)/onboarding/actions";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useProductMutation } from "@/lib/products/mutateProducts";
|
||||
import { useProduct } from "@/lib/products/products";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { Button, ColorPicker, ErrorComponent, Input, Label } from "@formbricks/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -11,12 +11,11 @@ type Product = {
|
||||
done: () => void;
|
||||
environmentId: string;
|
||||
isLoading: boolean;
|
||||
product: TProduct;
|
||||
};
|
||||
|
||||
const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
|
||||
const Product: React.FC<Product> = ({ done, isLoading, environmentId, product }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
|
||||
const { triggerProductMutate } = useProductMutation(environmentId);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [color, setColor] = useState("##4748b");
|
||||
@@ -30,7 +29,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingProduct) {
|
||||
if (!product) {
|
||||
return;
|
||||
} else if (product && product.name !== "My Product") {
|
||||
done(); // when product already exists, skip product step entirely
|
||||
@@ -40,7 +39,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}, [product, done, isLoadingProduct]);
|
||||
}, [product, done]);
|
||||
|
||||
const dummyChoices = ["❤️ Love it!"];
|
||||
|
||||
@@ -50,7 +49,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await triggerProductMutate({ name, brandColor: color });
|
||||
await updateProductAction(product.id, { name, brandColor: color });
|
||||
} catch (e) {
|
||||
toast.error("An error occured saving your settings");
|
||||
console.error(e);
|
||||
@@ -63,11 +62,11 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
|
||||
done();
|
||||
};
|
||||
|
||||
if (isLoadingProduct || loading) {
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorProduct) {
|
||||
if (!product) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
+11
-10
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/../../packages/lib/cn";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { createResponse, formbricksEnabled } from "@/lib/formbricks";
|
||||
import { useProfile } from "@/lib/profile";
|
||||
import { useProfileMutation } from "@/lib/profile/mutateProfile";
|
||||
import { ResponseId, SurveyId } from "@formbricks/js";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -14,6 +14,7 @@ type RoleProps = {
|
||||
next: () => void;
|
||||
skip: () => void;
|
||||
setFormbricksResponseId: (id: ResponseId) => void;
|
||||
profile: TProfile;
|
||||
};
|
||||
|
||||
type RoleChoice = {
|
||||
@@ -21,11 +22,9 @@ type RoleChoice = {
|
||||
id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other";
|
||||
};
|
||||
|
||||
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
|
||||
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profile }) => {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
|
||||
const { profile } = useProfile();
|
||||
const { triggerProfileMutate, isMutatingProfile } = useProfileMutation();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const roles: Array<RoleChoice> = [
|
||||
{ label: "Project Manager", id: "project_manager" },
|
||||
@@ -40,9 +39,12 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
|
||||
const selectedRole = roles.find((role) => role.label === selectedChoice);
|
||||
if (selectedRole) {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
const updatedProfile = { ...profile, role: selectedRole.id };
|
||||
await triggerProfileMutate(updatedProfile);
|
||||
await updateProfileAction(profile.id, updatedProfile);
|
||||
setIsUpdating(false);
|
||||
} catch (e) {
|
||||
setIsUpdating(false);
|
||||
toast.error("An error occured saving your settings");
|
||||
console.error(e);
|
||||
}
|
||||
@@ -68,7 +70,6 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
|
||||
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
What is your role?
|
||||
</label>
|
||||
∏
|
||||
<label className="block text-sm font-normal leading-6 text-slate-500">
|
||||
Make your Formbricks experience more personalised.
|
||||
</label>
|
||||
@@ -114,7 +115,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
|
||||
<Button
|
||||
size="lg"
|
||||
variant="darkCTA"
|
||||
loading={isMutatingProfile}
|
||||
loading={isUpdating}
|
||||
disabled={!selectedChoice}
|
||||
onClick={handleNextClick}
|
||||
id="role-next">
|
||||
@@ -0,0 +1,13 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex h-[100vh] w-[80vw] animate-pulse flex-col items-center justify-between p-12 text-white">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="h-12 w-1/6 rounded-lg bg-gray-200"></div>
|
||||
<div className="h-12 w-1/3 rounded-lg bg-gray-200"></div>
|
||||
<div className="h-0 w-1/6"></div>
|
||||
</div>
|
||||
<div className="h-1/3 w-1/2 rounded-lg bg-gray-200"></div>
|
||||
<div className="h-10 w-1/2 rounded-lg bg-gray-200"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,23 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Onboarding from "./Onboarding";
|
||||
import Onboarding from "./components/Onboarding";
|
||||
import { getEnvironmentByUser } from "@formbricks/lib/services/environment";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
return <Onboarding session={session} />;
|
||||
const environment = await getEnvironmentByUser(session?.user);
|
||||
const profile = await getProfile(session?.user.id!);
|
||||
const product = await getProductByEnvironmentId(environment?.id!);
|
||||
|
||||
if (!environment || !profile || !product) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
return <Onboarding session={session} environmentId={environment?.id} profile={profile} product={product} />;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import "server-only";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { ZEnvironment, ZEnvironmentUpdateInput, ZId } from "@formbricks/types/v1/environment";
|
||||
import { Prisma, EnvironmentType } from "@prisma/client";
|
||||
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors";
|
||||
import type { TEnvironment, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment";
|
||||
import type { TEnvironment, TEnvironmentId, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment";
|
||||
import { populateEnvironment } from "../utils/createDemoProductHelpers";
|
||||
import { ZEnvironment, ZEnvironmentUpdateInput, ZId } from "@formbricks/types/v1/environment";
|
||||
import { cache } from "react";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
@@ -101,3 +102,87 @@ export const updateEnvironment = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnvironmentByUser = async (user: any): Promise<TEnvironment | TEnvironmentId | null> => {
|
||||
const firstMembership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!firstMembership) {
|
||||
// create a new team and return environment
|
||||
const membership = await prisma.membership.create({
|
||||
data: {
|
||||
accepted: true,
|
||||
role: "owner",
|
||||
user: { connect: { id: user.id } },
|
||||
team: {
|
||||
create: {
|
||||
name: `${user.name}'s Team`,
|
||||
products: {
|
||||
create: {
|
||||
name: "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: EnvironmentType.production,
|
||||
...populateEnvironment,
|
||||
},
|
||||
{
|
||||
type: EnvironmentType.development,
|
||||
...populateEnvironment,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
products: {
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const environment = membership.team.products[0].environments[0];
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
const firstProduct = await prisma.product.findFirst({
|
||||
where: {
|
||||
teamId: firstMembership.teamId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (firstProduct === null) {
|
||||
return null;
|
||||
}
|
||||
const firstEnvironment = await prisma.environment.findFirst({
|
||||
where: {
|
||||
productId: firstProduct.id,
|
||||
type: "production",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (firstEnvironment === null) {
|
||||
return null;
|
||||
}
|
||||
return firstEnvironment;
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ export const updateProduct = async (
|
||||
productId: string,
|
||||
inputProduct: Partial<TProductUpdateInput>
|
||||
): Promise<TProduct> => {
|
||||
validateInputs([productId, ZId], [inputProduct, ZProductUpdateInput]);
|
||||
validateInputs([productId, ZId], [inputProduct, ZProductUpdateInput.partial()]);
|
||||
let updatedProduct;
|
||||
try {
|
||||
updatedProduct = await prisma.product.update({
|
||||
|
||||
@@ -16,6 +16,7 @@ const responseSelection = {
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
onboardingCompleted: true,
|
||||
};
|
||||
|
||||
// function to retrive basic information about a user's profile
|
||||
@@ -62,8 +63,11 @@ const getAdminMemberships = (memberships: TMembership[]) =>
|
||||
memberships.filter((membership) => membership.role === MembershipRole.admin);
|
||||
|
||||
// function to update a user's profile
|
||||
export const updateProfile = async (personId: string, data: TProfileUpdateInput): Promise<TProfile> => {
|
||||
validateInputs([personId, ZId], [data, ZProfileUpdateInput]);
|
||||
export const updateProfile = async (
|
||||
personId: string,
|
||||
data: Partial<TProfileUpdateInput>
|
||||
): Promise<TProfile> => {
|
||||
validateInputs([personId, ZId], [data, ZProfileUpdateInput.partial()]);
|
||||
try {
|
||||
const updatedProfile = await prisma.user.update({
|
||||
where: {
|
||||
|
||||
@@ -11,6 +11,12 @@ export const ZEnvironment = z.object({
|
||||
|
||||
export type TEnvironment = z.infer<typeof ZEnvironment>;
|
||||
|
||||
export const ZEnvironmentId = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export type TEnvironmentId = z.infer<typeof ZEnvironmentId>;
|
||||
|
||||
export const ZEnvironmentUpdateInput = z.object({
|
||||
type: z.enum(["development", "production"]),
|
||||
productId: z.string(),
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
import z from "zod";
|
||||
|
||||
const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]);
|
||||
const ZObjective = z.enum([
|
||||
"increase_conversion",
|
||||
"improve_user_retention",
|
||||
"increase_user_adoption",
|
||||
"sharpen_marketing_messaging",
|
||||
"support_sales",
|
||||
"other",
|
||||
]);
|
||||
export const ZProfile = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
email: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
onboardingCompleted: z.boolean(),
|
||||
});
|
||||
|
||||
export type TProfile = z.infer<typeof ZProfile>;
|
||||
|
||||
export const ZProfileUpdateInput = z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
name: z.string().nullable(),
|
||||
email: z.string(),
|
||||
onboardingCompleted: z.boolean(),
|
||||
role: ZRole.nullable(),
|
||||
objective: ZObjective.nullable(),
|
||||
});
|
||||
|
||||
export type TProfileUpdateInput = z.infer<typeof ZProfileUpdateInput>;
|
||||
|
||||
Reference in New Issue
Block a user