140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { WithLabel } from '@/components/forms/input-with-label';
|
|
import FullPageLoadingState from '@/components/full-page-loading-state';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
|
import { useTRPC } from '@/integrations/trpc/react';
|
|
import { handleError } from '@/integrations/trpc/react';
|
|
import { emailCategories } from '@openpanel/constants';
|
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { createFileRoute } from '@tanstack/react-router';
|
|
import { SaveIcon } from 'lucide-react';
|
|
import { Controller, useForm } from 'react-hook-form';
|
|
import { toast } from 'sonner';
|
|
import { z } from 'zod';
|
|
|
|
const validator = z.object({
|
|
categories: z.record(z.string(), z.boolean()),
|
|
});
|
|
|
|
type IForm = z.infer<typeof validator>;
|
|
|
|
/**
|
|
* Build explicit boolean values for every key in emailCategories.
|
|
* Uses saved preferences when available, falling back to true (opted-in).
|
|
*/
|
|
function buildCategoryDefaults(
|
|
savedPreferences?: Record<string, boolean>,
|
|
): Record<string, boolean> {
|
|
return Object.keys(emailCategories).reduce(
|
|
(acc, category) => {
|
|
acc[category] = savedPreferences?.[category] ?? true;
|
|
return acc;
|
|
},
|
|
{} as Record<string, boolean>,
|
|
);
|
|
}
|
|
|
|
export const Route = createFileRoute(
|
|
'/_app/$organizationId/profile/_tabs/email-preferences',
|
|
)({
|
|
component: Component,
|
|
pendingComponent: FullPageLoadingState,
|
|
});
|
|
|
|
function Component() {
|
|
const trpc = useTRPC();
|
|
const queryClient = useQueryClient();
|
|
|
|
const preferencesQuery = useSuspenseQuery(
|
|
trpc.email.getPreferences.queryOptions(),
|
|
);
|
|
|
|
const { control, handleSubmit, formState, reset } = useForm<IForm>({
|
|
defaultValues: {
|
|
categories: buildCategoryDefaults(preferencesQuery.data),
|
|
},
|
|
});
|
|
|
|
const mutation = useMutation(
|
|
trpc.email.updatePreferences.mutationOptions({
|
|
onSuccess: async () => {
|
|
toast('Email preferences updated', {
|
|
description: 'Your email preferences have been saved.',
|
|
});
|
|
await queryClient.invalidateQueries(
|
|
trpc.email.getPreferences.pathFilter(),
|
|
);
|
|
// Reset form with fresh data after refetch
|
|
const freshData = await queryClient.fetchQuery(
|
|
trpc.email.getPreferences.queryOptions(),
|
|
);
|
|
reset({
|
|
categories: buildCategoryDefaults(freshData),
|
|
});
|
|
},
|
|
onError: handleError,
|
|
}),
|
|
);
|
|
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit((values) => {
|
|
mutation.mutate(values);
|
|
})}
|
|
>
|
|
<Widget className="max-w-screen-md w-full">
|
|
<WidgetHead>
|
|
<span className="title">Email Preferences</span>
|
|
</WidgetHead>
|
|
<WidgetBody className="gap-4 col">
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Choose which types of emails you want to receive. Uncheck a category
|
|
to stop receiving those emails.
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
{Object.entries(emailCategories).map(([category, label]) => (
|
|
<Controller
|
|
key={category}
|
|
name={`categories.${category}`}
|
|
control={control}
|
|
render={({ field }) => (
|
|
<div className="flex items-center justify-between gap-4 px-4 py-4 rounded-md border border-border hover:bg-def-200 transition-colors">
|
|
<div className="flex-1">
|
|
<div className="font-medium">{label}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{category === 'onboarding' &&
|
|
'Get started tips and guidance emails'}
|
|
{category === 'billing' &&
|
|
'Subscription updates and payment reminders'}
|
|
</div>
|
|
</div>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
disabled={mutation.isPending}
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<Button
|
|
size="sm"
|
|
type="submit"
|
|
disabled={!formState.isDirty || mutation.isPending}
|
|
className="self-end mt-4"
|
|
icon={SaveIcon}
|
|
loading={mutation.isPending}
|
|
>
|
|
Save
|
|
</Button>
|
|
</WidgetBody>
|
|
</Widget>
|
|
</form>
|
|
);
|
|
}
|