Skip to content

Commit 1e89c85

Browse files
committed
feat(user-settings): add user settings page for email notification preferences
1 parent da0e78c commit 1e89c85

File tree

11 files changed

+104
-170
lines changed

11 files changed

+104
-170
lines changed

apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { z } from 'zod';
66
// Adjust safe-action import for colocalized structure
77
import { authActionClient } from '@/actions/safe-action';
88
import type { ActionResponse } from '@/actions/types';
9-
import { sendUnassignedItemsNotificationEmail, type UnassignedItem } from '@comp/email';
9+
import {
10+
isUserUnsubscribed,
11+
sendUnassignedItemsNotificationEmail,
12+
type UnassignedItem,
13+
} from '@comp/email';
1014

1115
const removeMemberSchema = z.object({
1216
memberId: z.string(),
@@ -253,29 +257,22 @@ export const removeMember = authActionClient
253257

254258
if (owner) {
255259
// Check if owner is unsubscribed from unassigned items notifications
256-
const ownerUser = await db.user.findUnique({
257-
where: { email: owner.user.email },
258-
select: { emailNotificationsUnsubscribed: true, emailPreferences: true },
259-
});
260+
const unsubscribed = await isUserUnsubscribed(
261+
db,
262+
owner.user.email,
263+
'unassignedItemsNotifications',
264+
);
260265

261-
if (ownerUser) {
262-
const isUnsubscribed =
263-
ownerUser.emailNotificationsUnsubscribed ||
264-
(ownerUser.emailPreferences &&
265-
typeof ownerUser.emailPreferences === 'object' &&
266-
(ownerUser.emailPreferences as Record<string, boolean>).unassignedItemsNotifications === false);
267-
268-
if (!isUnsubscribed) {
269-
// Send email to the org owner
270-
sendUnassignedItemsNotificationEmail({
271-
email: owner.user.email,
272-
userName: owner.user.name || owner.user.email || 'Owner',
273-
organizationName: organization.name,
274-
organizationId: ctx.session.activeOrganizationId,
275-
removedMemberName,
276-
unassignedItems,
277-
});
278-
}
266+
if (!unsubscribed) {
267+
// Send email to the org owner
268+
sendUnassignedItemsNotificationEmail({
269+
email: owner.user.email,
270+
userName: owner.user.name || owner.user.email || 'Owner',
271+
organizationName: organization.name,
272+
organizationId: ctx.session.activeOrganizationId,
273+
removedMemberName,
274+
unassignedItems,
275+
});
279276
}
280277
}
281278
}

apps/app/src/app/(app)/[orgId]/settings/layout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ export default async function Layout({ children }: { children: React.ReactNode }
2424
path: `/${orgId}/settings`,
2525
label: 'General',
2626
},
27-
{
28-
path: `/${orgId}/settings/notifications`,
29-
label: 'Notifications',
30-
},
3127
{
3228
path: `/${orgId}/settings/trust-portal`,
3329
label: 'Trust Portal',
@@ -44,6 +40,10 @@ export default async function Layout({ children }: { children: React.ReactNode }
4440
path: `/${orgId}/settings/secrets`,
4541
label: 'Secrets',
4642
},
43+
{
44+
path: `/${orgId}/settings/user`,
45+
label: 'User Settings',
46+
},
4747
]}
4848
/>
4949
</Suspense>

apps/app/src/app/(app)/[orgId]/settings/notifications/actions/update-email-preferences.ts renamed to apps/app/src/app/(app)/[orgId]/settings/user/actions/update-email-preferences.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use server';
22

3-
import { db } from '@db';
43
import { authActionClient } from '@/actions/safe-action';
5-
import { z } from 'zod';
4+
import { db } from '@db';
65
import { revalidatePath } from 'next/cache';
6+
import { z } from 'zod';
77

88
const emailPreferencesSchema = z.object({
99
preferences: z.object({
@@ -50,7 +50,7 @@ export const updateEmailPreferencesAction = authActionClient
5050

5151
// Revalidate the settings page
5252
if (ctx.session.activeOrganizationId) {
53-
revalidatePath(`/${ctx.session.activeOrganizationId}/settings/notifications`);
53+
revalidatePath(`/${ctx.session.activeOrganizationId}/settings/user`);
5454
}
5555

5656
return {
@@ -64,4 +64,3 @@ export const updateEmailPreferencesAction = authActionClient
6464
};
6565
}
6666
});
67-

apps/app/src/app/(app)/[orgId]/settings/notifications/components/EmailNotificationPreferences.tsx renamed to apps/app/src/app/(app)/[orgId]/settings/user/components/EmailNotificationPreferences.tsx

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
'use client';
22

3-
import { Checkbox } from '@comp/ui/checkbox';
43
import { Button } from '@comp/ui/button';
5-
import { useState } from 'react';
6-
import { updateEmailPreferencesAction } from '../actions/update-email-preferences';
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardFooter,
9+
CardHeader,
10+
CardTitle,
11+
} from '@comp/ui/card';
12+
import { Checkbox } from '@comp/ui/checkbox';
713
import { useAction } from 'next-safe-action/hooks';
14+
import { useState } from 'react';
815
import { toast } from 'sonner';
16+
import { updateEmailPreferencesAction } from '../actions/update-email-preferences';
917

1018
interface EmailPreferences {
1119
policyNotifications: boolean;
@@ -20,6 +28,7 @@ interface Props {
2028
}
2129

2230
export function EmailNotificationPreferences({ initialPreferences, email }: Props) {
31+
// Normal logic: true = subscribed (checked), false = unsubscribed (unchecked)
2332
const [preferences, setPreferences] = useState<EmailPreferences>(initialPreferences);
2433
const [saving, setSaving] = useState(false);
2534

@@ -42,6 +51,8 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
4251
};
4352

4453
const handleSelectAll = () => {
54+
// If all are enabled (all true), disable all (set all to false)
55+
// If any are disabled (some false), enable all (set all to true)
4556
const allEnabled = Object.values(preferences).every((v) => v === true);
4657
setPreferences({
4758
policyNotifications: !allEnabled,
@@ -56,28 +67,26 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
5667
execute({ preferences });
5768
};
5869

70+
// Check if all are disabled (all false)
5971
const allDisabled = Object.values(preferences).every((v) => v === false);
6072

6173
return (
62-
<div className="space-y-6">
63-
<div>
64-
<h2 className="text-lg font-semibold">Email Notification Preferences</h2>
65-
<p className="text-sm text-muted-foreground">
66-
Manage which email notifications you receive at <span className="font-medium">{email}</span>. These preferences apply to all organizations you're a member of.
67-
</p>
68-
</div>
69-
70-
<div className="space-y-4">
74+
<Card>
75+
<CardHeader>
76+
<CardTitle>Email Notifications</CardTitle>
77+
<CardDescription>
78+
Manage which email notifications you receive at{' '}
79+
<span className="font-medium">{email}</span>. These preferences apply to all organizations
80+
you're a member of.
81+
</CardDescription>
82+
</CardHeader>
83+
<CardContent className="space-y-4">
7184
<div className="flex items-center justify-between border-b pb-4">
7285
<div>
7386
<label className="text-base font-medium text-foreground">Enable All</label>
74-
<p className="text-sm text-muted-foreground">Toggle all notifications at once</p>
87+
<p className="text-sm text-muted-foreground">Toggle all notifications</p>
7588
</div>
76-
<Button
77-
onClick={handleSelectAll}
78-
variant="outline"
79-
size="sm"
80-
>
89+
<Button onClick={handleSelectAll} variant="outline" size="sm">
8190
{Object.values(preferences).every((v) => v === true) ? 'Disable All' : 'Enable All'}
8291
</Button>
8392
</div>
@@ -87,7 +96,7 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
8796
<Checkbox
8897
checked={preferences.policyNotifications}
8998
onCheckedChange={(checked) => handleToggle('policyNotifications', checked === true)}
90-
className="mt-1 flex-shrink-0"
99+
className="mt-1 shrink-0"
91100
/>
92101
<div className="flex-1 min-w-0">
93102
<div className="font-medium text-foreground">Policy Notifications</div>
@@ -101,7 +110,7 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
101110
<Checkbox
102111
checked={preferences.taskReminders}
103112
onCheckedChange={(checked) => handleToggle('taskReminders', checked === true)}
104-
className="mt-1 flex-shrink-0"
113+
className="mt-1 shrink-0"
105114
/>
106115
<div className="flex-1 min-w-0">
107116
<div className="font-medium text-foreground">Task Reminders</div>
@@ -115,7 +124,7 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
115124
<Checkbox
116125
checked={preferences.weeklyTaskDigest}
117126
onCheckedChange={(checked) => handleToggle('weeklyTaskDigest', checked === true)}
118-
className="mt-1 flex-shrink-0"
127+
className="mt-1 shrink-0"
119128
/>
120129
<div className="flex-1 min-w-0">
121130
<div className="font-medium text-foreground">Weekly Task Digest</div>
@@ -128,8 +137,10 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
128137
<label className="flex cursor-pointer items-start gap-4 rounded-lg border p-4 hover:bg-muted/50 transition-colors">
129138
<Checkbox
130139
checked={preferences.unassignedItemsNotifications}
131-
onCheckedChange={(checked) => handleToggle('unassignedItemsNotifications', checked === true)}
132-
className="mt-1 flex-shrink-0"
140+
onCheckedChange={(checked) =>
141+
handleToggle('unassignedItemsNotifications', checked === true)
142+
}
143+
className="mt-1 shrink-0"
133144
/>
134145
<div className="flex-1 min-w-0">
135146
<div className="font-medium text-foreground">Unassigned Items Notifications</div>
@@ -139,28 +150,16 @@ export function EmailNotificationPreferences({ initialPreferences, email }: Prop
139150
</div>
140151
</label>
141152
</div>
142-
</div>
143-
144-
{allDisabled && (
145-
<div className="rounded-md bg-yellow-500/10 border border-yellow-500/20 p-4 text-sm text-yellow-700 dark:text-yellow-400">
146-
You have disabled all notifications. You won't receive any email notifications from any organization.
153+
</CardContent>
154+
<CardFooter className="flex justify-between">
155+
<div className="text-muted-foreground text-xs">
156+
You can also manage these preferences by clicking the unsubscribe link in any email
157+
notification.
147158
</div>
148-
)}
149-
150-
<div className="flex justify-end">
151-
<Button
152-
onClick={handleSave}
153-
disabled={saving}
154-
>
155-
{saving ? 'Saving...' : 'Save Preferences'}
159+
<Button onClick={handleSave} disabled={saving}>
160+
{saving ? 'Saving...' : 'Save'}
156161
</Button>
157-
</div>
158-
159-
<div className="rounded-md bg-muted p-4 text-sm text-muted-foreground">
160-
<p className="font-medium mb-1">Note:</p>
161-
<p>You can also manage these preferences by clicking the unsubscribe link in any email notification.</p>
162-
</div>
163-
</div>
162+
</CardFooter>
163+
</Card>
164164
);
165165
}
166-

apps/app/src/app/(app)/[orgId]/settings/notifications/page.tsx renamed to apps/app/src/app/(app)/[orgId]/settings/user/page.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Metadata } from 'next';
44
import { headers } from 'next/headers';
55
import { EmailNotificationPreferences } from './components/EmailNotificationPreferences';
66

7-
export default async function NotificationSettings() {
7+
export default async function UserSettings() {
88
const session = await auth.api.getSession({
99
headers: await headers(),
1010
});
@@ -28,6 +28,21 @@ export default async function NotificationSettings() {
2828
unassignedItemsNotifications: true,
2929
};
3030

31+
// If user has the old all-or-nothing unsubscribe flag, convert to preferences
32+
if (user?.emailNotificationsUnsubscribed) {
33+
const preferences = {
34+
policyNotifications: false,
35+
taskReminders: false,
36+
weeklyTaskDigest: false,
37+
unassignedItemsNotifications: false,
38+
};
39+
return (
40+
<div className="space-y-4">
41+
<EmailNotificationPreferences initialPreferences={preferences} email={session.user.email} />
42+
</div>
43+
);
44+
}
45+
3146
const preferences =
3247
user?.emailPreferences && typeof user.emailPreferences === 'object'
3348
? { ...DEFAULT_PREFERENCES, ...(user.emailPreferences as Record<string, boolean>) }
@@ -42,6 +57,6 @@ export default async function NotificationSettings() {
4257

4358
export async function generateMetadata(): Promise<Metadata> {
4459
return {
45-
title: 'Email Notifications',
60+
title: 'User Settings',
4661
};
4762
}

apps/app/src/app/api/unsubscribe/route.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)