feature: onboarding emails
* wip * wip * wip * fix coderabbit comments * remove template
This commit is contained in:
committed by
GitHub
parent
67301d928c
commit
e645c094b2
@@ -0,0 +1,139 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user