mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-04 01:20:01 -06:00
feat: enable weekly summary & support for callbacks on login (#1885)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com> Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5471324cfe
commit
70fe0fb7a7
@@ -12,8 +12,8 @@
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint"]
|
||||
}
|
||||
"extensions": ["dbaeumer.vscode-eslint"],
|
||||
},
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
@@ -25,5 +25,5 @@
|
||||
"postAttachCommand": "pnpm dev --filter=web... --filter=demo...",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
"remoteUser": "node",
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/surve
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { deleteSurvey, duplicateSurvey, getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const createShortUrlAction = async (url: string) => {
|
||||
@@ -44,10 +45,25 @@ export async function createTeamAction(teamName: string): Promise<Team> {
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await createProduct(newTeam.id, {
|
||||
const product = await createProduct(newTeam.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings = {
|
||||
...session.user.notificationSettings,
|
||||
alert: {
|
||||
...session.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...session.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
@@ -244,6 +260,20 @@ export const createProductAction = async (environmentId: string, productName: st
|
||||
const product = await createProduct(team.id, {
|
||||
name: productName,
|
||||
});
|
||||
const updatedNotificationSettings = {
|
||||
...session.user.notificationSettings,
|
||||
alert: {
|
||||
...session.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...session.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
// get production environment
|
||||
const productionEnvironment = product.environments.find((environment) => environment.type === "production");
|
||||
|
||||
@@ -504,7 +504,7 @@ export default function Navigation({
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
await signOut({ callbackUrl: "/auth/login" });
|
||||
await formbricksLogout();
|
||||
}}>
|
||||
<div className="flex h-full w-full items-center">
|
||||
|
||||
@@ -98,8 +98,11 @@ export const leaveTeamAction = async (teamId: string) => {
|
||||
};
|
||||
|
||||
export const createInviteTokenAction = async (inviteId: string) => {
|
||||
const { email } = await getInvite(inviteId);
|
||||
const inviteToken = createInviteToken(inviteId, email, {
|
||||
const invite = await getInvite(inviteId);
|
||||
if (!invite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
const inviteToken = createInviteToken(inviteId, invite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ export default async function ProfileSettingsPage({ params }) {
|
||||
</SettingsCard>
|
||||
<IntegrationsTip environmentId={params.environmentId} />
|
||||
<SettingsCard
|
||||
beta
|
||||
title="Weekly summary (Products)"
|
||||
description="Stay up-to-date with a Weekly every Monday.">
|
||||
<EditWeeklySummary memberships={memberships} user={user} environmentId={params.environmentId} />
|
||||
|
||||
@@ -55,7 +55,7 @@ function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps)
|
||||
try {
|
||||
setDeleting(true);
|
||||
await deleteUserAction();
|
||||
await signOut();
|
||||
await signOut({ callbackUrl: "/auth/login" });
|
||||
await formbricksLogout();
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong");
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function Onboarding({ session, environmentId, user, product }: On
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const updatedProfile = { ...user, onboardingCompleted: true };
|
||||
const updatedProfile = { onboardingCompleted: true };
|
||||
await updateUserAction(updatedProfile);
|
||||
|
||||
if (environmentId) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
const ContentLayout = ({ headline, description, children }) => {
|
||||
interface ContentLayoutProps {
|
||||
headline: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ContentLayout = ({ headline, description, children }: ContentLayoutProps) => {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="m-auto flex flex-col gap-7 text-center text-slate-700">
|
||||
@@ -57,9 +63,17 @@ export const ExpiredContent = () => {
|
||||
return (
|
||||
<ContentLayout
|
||||
headline="Invite expired 😥"
|
||||
description="Invites are valid for 7 days. Please request a new invite.">
|
||||
<div></div>
|
||||
</ContentLayout>
|
||||
description="Invites are valid for 7 days. Please request a new invite."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InvitationNotFound = () => {
|
||||
return (
|
||||
<ContentLayout
|
||||
headline="Invite not found 😥"
|
||||
description="The invitation code cannot be found or has already been used."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { sendInviteAcceptedEmail } from "@formbricks/lib/emails/emails";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import { deleteInvite, getInvite } from "@formbricks/lib/invite/service";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
|
||||
import {
|
||||
ExpiredContent,
|
||||
InvitationNotFound,
|
||||
NotLoggedInContent,
|
||||
RightAccountContent,
|
||||
UsedContent,
|
||||
WrongAccountContent,
|
||||
} from "./components/InviteContentComponents";
|
||||
|
||||
export default async function JoinTeam({ searchParams }) {
|
||||
const currentUser = await getServerSession(authOptions);
|
||||
export default async function InvitePage({ searchParams }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
try {
|
||||
const { inviteId, email } = verifyInviteToken(searchParams.token);
|
||||
|
||||
const invite = await getInvite(inviteId);
|
||||
|
||||
if (!invite) {
|
||||
return <InvitationNotFound />;
|
||||
}
|
||||
|
||||
const isInviteExpired = new Date(invite.expiresAt) < new Date();
|
||||
|
||||
if (!invite || isInviteExpired) {
|
||||
if (isInviteExpired) {
|
||||
return <ExpiredContent />;
|
||||
} else if (invite.accepted) {
|
||||
return <UsedContent />;
|
||||
} else if (!currentUser) {
|
||||
const redirectUrl = env.NEXTAUTH_URL + "/invite?token=" + searchParams.token;
|
||||
} else if (!session) {
|
||||
const redirectUrl = WEBAPP_URL + "/invite?token=" + searchParams.token;
|
||||
return <NotLoggedInContent email={email} token={searchParams.token} redirectUrl={redirectUrl} />;
|
||||
} else if (currentUser.user?.email !== email) {
|
||||
} else if (session.user?.email !== email) {
|
||||
return <WrongAccountContent />;
|
||||
} else {
|
||||
await createMembership(invite.teamId, currentUser.user.id, { accepted: true, role: invite.role });
|
||||
await createMembership(invite.teamId, session.user.id, { accepted: true, role: invite.role });
|
||||
await deleteInvite(inviteId);
|
||||
|
||||
sendInviteAcceptedEmail(invite.creator.name ?? "", currentUser.user?.name ?? "", invite.creator.email);
|
||||
sendInviteAcceptedEmail(invite.creator.name ?? "", session.user?.name ?? "", invite.creator.email);
|
||||
|
||||
return <RightAccountContent />;
|
||||
}
|
||||
} catch (e) {
|
||||
return <ExpiredContent />;
|
||||
console.error(e);
|
||||
return <InvitationNotFound />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,30 +159,27 @@ const createSurveyFields = (surveyResponses: SurveyResponse[]) => {
|
||||
return surveyFields;
|
||||
};
|
||||
|
||||
const notificationFooter = () => {
|
||||
const notificationFooter = (environmentId: string) => {
|
||||
return `
|
||||
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
|
||||
<p style="margin-top:0px;">The Formbricks Team 🤍</p>
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:8px; padding:0.01em 1.6em; text-align:center; font-size:0.8em; line-height:1.2em;"><p><i>This is a Beta feature. If you experience any issues, please let us know by replying to this email 🙏</i></p></div>
|
||||
`;
|
||||
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
|
||||
<p style="margin-top:0px;">The Formbricks Team 🤍</p>
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:8px; padding:0.01em 1.6em; text-align:center; font-size:0.8em; line-height:1.2em;">
|
||||
<p><i>To halt Weekly Updates, <a href="${WEBAPP_URL}/environments/${environmentId}/settings/notifications">please turn them off</a> in your settings 🙏</i></p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const createReminderNotificationBody = (notificationData: NotificationResponse, webUrl) => {
|
||||
const createReminderNotificationBody = (notificationData: NotificationResponse) => {
|
||||
return `
|
||||
<p>We’d love to send you a Weekly Summary, but currently there are no surveys running for ${notificationData.productName}.</p>
|
||||
|
||||
<p style="font-weight: bold; padding-top:1em;">Don’t let a week pass without learning about your users:</p>
|
||||
|
||||
<a class="button" href="${webUrl}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA">Setup a new survey</a>
|
||||
|
||||
<a class="button" href="${WEBAPP_URL}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA">Setup a new survey</a>
|
||||
|
||||
<br/>
|
||||
<p style="padding-top:1em;">Need help finding the right survey for your product? Pick a 15-minute slot <a href="https://cal.com/johannes/15">in our CEOs calendar</a> or reply to this email :)</p>
|
||||
|
||||
|
||||
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
|
||||
<p style="margin-top:0px;">The Formbricks Team</p>
|
||||
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:99px; margin:1em; padding:0.01em 1.6em; text-align:center;"><p><i>This is a Beta feature. If you experience any issues, please let us know by replying to this email 🙏</i></p></div>
|
||||
${notificationFooter(notificationData.environmentId)}
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -207,7 +204,7 @@ export const sendWeeklySummaryNotificationEmail = async (
|
||||
${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
|
||||
${notificationInsight(notificationData.insights)}
|
||||
${notificationLiveSurveys(notificationData.surveys, notificationData.environmentId)}
|
||||
${notificationFooter()}
|
||||
${notificationFooter(notificationData.environmentId)}
|
||||
`),
|
||||
});
|
||||
};
|
||||
@@ -231,7 +228,7 @@ export const sendNoLiveSurveyNotificationEmail = async (
|
||||
subject: getEmailSubject(notificationData.productName),
|
||||
html: withEmailTemplate(`
|
||||
${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
|
||||
${createReminderNotificationBody(notificationData, WEBAPP_URL)}
|
||||
${createReminderNotificationBody(notificationData)}
|
||||
`),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { createTeam, getTeam } from "@formbricks/lib/team/service";
|
||||
import { createUser } from "@formbricks/lib/user/service";
|
||||
import { createUser, updateUser } from "@formbricks/lib/user/service";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let { inviteToken, ...user } = await request.json();
|
||||
@@ -76,7 +76,22 @@ export async function POST(request: Request) {
|
||||
else {
|
||||
const team = await createTeam({ name: user.name + "'s Team" });
|
||||
await createMembership(team.id, user.id, { role: "owner", accepted: true });
|
||||
await createProduct(team.id, { name: "My Product" });
|
||||
const product = await createProduct(team.id, { name: "My Product" });
|
||||
|
||||
const updatedNotificationSettings = {
|
||||
...user.notificationSettings,
|
||||
alert: {
|
||||
...user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
}
|
||||
// send verification email amd return user
|
||||
if (!EMAIL_VERIFICATION_DISABLED) {
|
||||
|
||||
@@ -13,3 +13,6 @@ export const shareUrlRoute = (url: string): boolean => {
|
||||
const regex = /\/share\/[A-Za-z0-9]+\/(summary|responses)/;
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
export const isWebAppRoute = (url: string): boolean =>
|
||||
url.startsWith("/environments") && url !== "/api/auth/signout";
|
||||
|
||||
@@ -6,14 +6,31 @@ import {
|
||||
} from "@/app/middleware/bucket";
|
||||
import {
|
||||
clientSideApiRoute,
|
||||
isWebAppRoute,
|
||||
loginRoute,
|
||||
shareUrlRoute,
|
||||
signupRoute,
|
||||
} from "@/app/middleware/endpointValidator";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const token = await getToken({ req: request });
|
||||
|
||||
if (isWebAppRoute(request.nextUrl.pathname) && !token) {
|
||||
return NextResponse.redirect(
|
||||
WEBAPP_URL + "/auth/login?callbackUrl=" + WEBAPP_URL + request.nextUrl.pathname
|
||||
);
|
||||
}
|
||||
|
||||
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
||||
if (token && callbackUrl) {
|
||||
return NextResponse.redirect(WEBAPP_URL + callbackUrl);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
@@ -54,5 +71,7 @@ export const config = {
|
||||
"/api/v1/js/actions",
|
||||
"/api/v1/client/storage",
|
||||
"/share/(.*)/:path",
|
||||
"/environments/:path*",
|
||||
"/api/auth/signout",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@ test.describe("JS Package Test", async () => {
|
||||
|
||||
// Formbricks Modal is visible
|
||||
await expect(page.getByRole("link", { name: "Powered by Formbricks" })).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
test("Admin checks Display", async ({ page }) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ export const skipOnboarding = async (page: Page): Promise<void> => {
|
||||
await page.waitForURL("/onboarding");
|
||||
await expect(page).toHaveURL("/onboarding");
|
||||
await page.getByRole("button", { name: "I'll do it later" }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByRole("button", { name: "I'll do it later" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page).toHaveURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
@@ -31,17 +31,12 @@ export const AddMemberRole = ({ control, canDoRoleManagement }: AddMemberRolePro
|
||||
<div>
|
||||
<Label>Role</Label>
|
||||
<Select
|
||||
defaultValue="admin"
|
||||
value={value}
|
||||
onValueChange={(v) => onChange(v as MembershipRole)}
|
||||
disabled={!canDoRoleManagement}>
|
||||
<SelectTrigger className="capitalize">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
<span className="text-slate-400">
|
||||
{canDoRoleManagement ? "Select role" : "Select role (Pro Feature)"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
|
||||
@@ -248,7 +248,22 @@ export const authOptions: NextAuthOptions = {
|
||||
...account,
|
||||
userId: userProfile.id,
|
||||
});
|
||||
await createProduct(team.id, { name: "My Product" });
|
||||
const product = await createProduct(team.id, { name: "My Product" });
|
||||
const updatedNotificationSettings = {
|
||||
...userProfile.notificationSettings,
|
||||
alert: {
|
||||
...userProfile.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...userProfile.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(userProfile.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export const deleteInvite = async (inviteId: string): Promise<TInvite> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getInvite = async (inviteId: string): Promise<InviteWithCreator> => {
|
||||
export const getInvite = async (inviteId: string): Promise<InviteWithCreator | null> => {
|
||||
const invite = await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([inviteId, ZString]);
|
||||
@@ -140,18 +140,18 @@ export const getInvite = async (inviteId: string): Promise<InviteWithCreator> =>
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite) {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
}
|
||||
return invite;
|
||||
},
|
||||
[`getInvite-${inviteId}`],
|
||||
{ tags: [inviteCache.tag.byId(inviteId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
|
||||
)();
|
||||
return {
|
||||
...formatDateFields(invite, ZInvite),
|
||||
creator: invite.creator,
|
||||
};
|
||||
|
||||
return invite
|
||||
? {
|
||||
...formatDateFields(invite, ZInvite),
|
||||
creator: invite.creator,
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const resendInvite = async (inviteId: string): Promise<TInvite> => {
|
||||
|
||||
@@ -357,7 +357,7 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
|
||||
});
|
||||
|
||||
if (!responsePrisma) {
|
||||
throw new ResourceNotFoundError("Response", responseId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const response: TResponse = {
|
||||
@@ -382,10 +382,12 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
|
||||
}
|
||||
)();
|
||||
|
||||
return {
|
||||
...formatDateFields(response, ZResponse),
|
||||
notes: response.notes.map((note) => formatDateFields(note, ZResponseNote)),
|
||||
} as TResponse;
|
||||
return response
|
||||
? ({
|
||||
...formatDateFields(response, ZResponse),
|
||||
notes: response.notes.map((note) => formatDateFields(note, ZResponseNote)),
|
||||
} as TResponse)
|
||||
: null;
|
||||
};
|
||||
|
||||
export const getResponses = async (
|
||||
|
||||
@@ -280,7 +280,8 @@ describe("Tests for getResponse service", () => {
|
||||
|
||||
it("Throws ResourceNotFoundError if no response is found", async () => {
|
||||
prismaMock.response.findUnique.mockResolvedValue(null);
|
||||
await expect(getResponse(mockResponse.id)).rejects.toThrow(ResourceNotFoundError);
|
||||
const response = await getResponse(mockResponse.id);
|
||||
expect(response).toBeNull();
|
||||
});
|
||||
|
||||
it("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"downlevelIteration": true
|
||||
}
|
||||
"downlevelIteration": true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const responseSelection = {
|
||||
twoFactorEnabled: true,
|
||||
identityProvider: true,
|
||||
objective: true,
|
||||
notificationSettings: true,
|
||||
};
|
||||
|
||||
// function to retrive basic information about a user's user
|
||||
|
||||
@@ -13,6 +13,13 @@ export const ZUserObjective = z.enum([
|
||||
|
||||
export type TUserObjective = z.infer<typeof ZUserObjective>;
|
||||
|
||||
export const ZUserNotificationSettings = z.object({
|
||||
alert: z.record(z.boolean()),
|
||||
weeklySummary: z.record(z.boolean()),
|
||||
});
|
||||
|
||||
export type TUserNotificationSettings = z.infer<typeof ZUserNotificationSettings>;
|
||||
|
||||
export const ZUser = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
@@ -25,6 +32,7 @@ export const ZUser = z.object({
|
||||
updatedAt: z.date(),
|
||||
onboardingCompleted: z.boolean(),
|
||||
objective: ZUserObjective.nullable(),
|
||||
notificationSettings: ZUserNotificationSettings,
|
||||
});
|
||||
|
||||
export type TUser = z.infer<typeof ZUser>;
|
||||
@@ -37,6 +45,7 @@ export const ZUserUpdateInput = z.object({
|
||||
role: ZRole.optional(),
|
||||
objective: ZUserObjective.nullish(),
|
||||
imageUrl: z.string().url().nullish(),
|
||||
notificationSettings: ZUserNotificationSettings.optional(),
|
||||
});
|
||||
|
||||
export type TUserUpdateInput = z.infer<typeof ZUserUpdateInput>;
|
||||
@@ -53,10 +62,3 @@ export const ZUserCreateInput = z.object({
|
||||
});
|
||||
|
||||
export type TUserCreateInput = z.infer<typeof ZUserCreateInput>;
|
||||
|
||||
export const ZUserNotificationSettings = z.object({
|
||||
alert: z.record(z.boolean()),
|
||||
weeklySummary: z.record(z.boolean()),
|
||||
});
|
||||
|
||||
export type TUserNotificationSettings = z.infer<typeof ZUserNotificationSettings>;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"include": ["."],
|
||||
"exclude": ["build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021.String"]
|
||||
}
|
||||
"lib": ["ES2021.String"],
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user