Skip to content

Commit 33efb81

Browse files
committed
combine notifications and subscriptions tabs
1 parent 530c6d7 commit 33efb81

6 files changed

Lines changed: 92 additions & 92 deletions

File tree

‎src/components/Pages/SettingsPage/UserSettingsForm/ApiSettings.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const ApiSettings = () => {
99
<FieldSet className="min-h-0 flex-auto">
1010
<FieldLegend>API</FieldLegend>
1111
<FieldDescription>Manage your API preferences.</FieldDescription>
12-
<FieldGroup className="min-h-0 flex-auto overflow-y-auto">
12+
<FieldGroup className="min-h-0 flex-auto overflow-y-auto px-1">
1313
<FieldApiKey apiKey={user?.apiKey ?? ''} userId={user?.id} />
1414
</FieldGroup>
1515
</FieldSet>

‎src/components/Pages/SettingsPage/UserSettingsForm/GeneralSettings.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const GeneralSettings = ({
3636
<FieldSet className="min-h-0 flex-auto">
3737
<FieldLegend>General</FieldLegend>
3838
<FieldDescription>Update your general account settings and preferences.</FieldDescription>
39-
<FieldGroup className="min-h-0 flex-auto overflow-y-auto">
39+
<FieldGroup className="min-h-0 flex-auto overflow-y-auto px-1">
4040
<FormField
4141
control={form.control}
4242
name="locale"

src/components/Pages/SettingsPage/UserSettingsForm/NotificationSubscriptionsSettings.tsx renamed to src/components/Pages/SettingsPage/UserSettingsForm/NotificationSubscriptionFields.tsx

Lines changed: 57 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { useEffect, useState } from 'react'
2-
import { toast } from 'sonner'
32
import { api } from '@/api'
4-
import { DocsLink } from '@/components/shared/DocsLink'
5-
import { Button } from '@/components/ui/Button'
6-
import { DisabledTooltip } from '@/components/ui/DisabledTooltip'
7-
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@/components/ui/Field'
3+
import { FieldDescription, FieldLegend, FieldSet } from '@/components/ui/Field'
84
import { Label } from '@/components/ui/Label'
95
import { Loader } from '@/components/ui/Loader'
106
import {
@@ -14,7 +10,6 @@ import {
1410
SelectTrigger,
1511
SelectValue,
1612
} from '@/components/ui/Select'
17-
import { Spinner } from '@/components/ui/Spinner'
1813
import {
1914
COUNT_FIELDS,
2015
type NotificationSubscriptions,
@@ -54,12 +49,21 @@ const SubscriptionRow = ({ id, label, description, value, options, onChange }: R
5449
</div>
5550
)
5651

52+
export interface SubscriptionsDraft {
53+
draft: NotificationSubscriptions | null
54+
setField: (key: keyof NotificationSubscriptions, value: number) => void
55+
isDirty: boolean
56+
save: () => Promise<unknown>
57+
isLoading: boolean
58+
isError: boolean
59+
}
60+
5761
/**
5862
* Per-notification delivery preferences. These live behind their own endpoint
59-
* rather than the user's settings object, so this tab keeps its own draft and
60-
* Save button instead of joining the account form's submit.
63+
* rather than the user's settings object, so they're kept as a draft here and
64+
* saved alongside the account form when it submits.
6165
*/
62-
export const NotificationSubscriptionsSettings = ({ userId }: { userId: number }) => {
66+
export const useNotificationSubscriptionsDraft = (userId: number): SubscriptionsDraft => {
6367
const { data, isLoading, isError } = api.user.notificationSubscriptions(userId)
6468
const updateMutation = api.user.useUpdateNotificationSubscriptions()
6569
const [draft, setDraft] = useState<NotificationSubscriptions | null>(null)
@@ -77,6 +81,26 @@ export const NotificationSubscriptionsSettings = ({ userId }: { userId: number }
7781
[...SUBSCRIPTION_FIELDS, ...COUNT_FIELDS].some(({ key }) => draft[key] !== saved[key])
7882
)
7983

84+
return {
85+
draft,
86+
setField: (key, value) => setDraft((prev) => (prev ? { ...prev, [key]: value } : prev)),
87+
isDirty,
88+
save: async () => {
89+
if (!draft) throw new Error('Notification subscriptions have not loaded yet')
90+
return updateMutation.mutateAsync({ userId, subscriptions: draft })
91+
},
92+
isLoading,
93+
isError,
94+
}
95+
}
96+
97+
export const NotificationSubscriptionFields = ({
98+
subscriptions,
99+
}: {
100+
subscriptions: SubscriptionsDraft
101+
}) => {
102+
const { draft, setField, isLoading, isError } = subscriptions
103+
80104
if (isLoading) return <Loader message="Loading notification preferences..." />
81105
if (isError || !draft) {
82106
return (
@@ -86,30 +110,10 @@ export const NotificationSubscriptionsSettings = ({ userId }: { userId: number }
86110
)
87111
}
88112

89-
const setField = (key: keyof NotificationSubscriptions, value: number) =>
90-
setDraft((prev) => (prev ? { ...prev, [key]: value } : prev))
91-
92-
const handleSave = async () => {
93-
try {
94-
await updateMutation.mutateAsync({ userId, subscriptions: draft })
95-
toast.success('Notification preferences updated')
96-
} catch {
97-
toast.error('Failed to update notification preferences')
98-
}
99-
}
100-
101113
return (
102-
<FieldSet className="min-h-0 flex-auto">
103-
<FieldLegend>Notification Subscriptions</FieldLegend>
104-
<FieldDescription>
105-
Decide which MapRoulette notifications you would like to receive, along with whether you
106-
would like to be sent an email informing you of the notification (either immediately or as a
107-
daily digest).{' '}
108-
<DocsLink page="notificationsAndEmail" icon={null}>
109-
Learn about notifications and email
110-
</DocsLink>
111-
</FieldDescription>
112-
<FieldGroup className="min-h-0 flex-auto overflow-y-auto">
114+
<>
115+
<FieldSet>
116+
<FieldLegend variant="label">Notify me about</FieldLegend>
113117
<div>
114118
{SUBSCRIPTION_FIELDS.map(({ key, label, description }) => (
115119
<SubscriptionRow
@@ -123,47 +127,27 @@ export const NotificationSubscriptionsSettings = ({ userId }: { userId: number }
123127
/>
124128
))}
125129
</div>
130+
</FieldSet>
126131

127-
<FieldSet>
128-
<FieldLegend>Periodic summaries</FieldLegend>
129-
<FieldDescription>
130-
These are sent on a schedule rather than when something happens.
131-
</FieldDescription>
132-
<div>
133-
{COUNT_FIELDS.map(({ key, label, description }) => (
134-
<SubscriptionRow
135-
key={key}
136-
id={`subscription-${key}`}
137-
label={label}
138-
description={description}
139-
value={draft[key] as number}
140-
options={subscriptionFrequencyOptions}
141-
onChange={(value) => setField(key, value)}
142-
/>
143-
))}
144-
</div>
145-
</FieldSet>
146-
</FieldGroup>
147-
148-
<div className="shrink-0 border-zinc-200 border-t pt-4 dark:border-slate-700">
149-
<DisabledTooltip show={!isDirty && !updateMutation.isPending} message="No changes to save">
150-
<Button
151-
type="button"
152-
size="lg"
153-
onClick={handleSave}
154-
disabled={updateMutation.isPending || !isDirty}
155-
>
156-
{updateMutation.isPending ? (
157-
<>
158-
<Spinner />
159-
Saving...
160-
</>
161-
) : (
162-
'Save Changes'
163-
)}
164-
</Button>
165-
</DisabledTooltip>
166-
</div>
167-
</FieldSet>
132+
<FieldSet>
133+
<FieldLegend variant="label">Periodic summaries</FieldLegend>
134+
<FieldDescription>
135+
These are sent on a schedule rather than when something happens.
136+
</FieldDescription>
137+
<div>
138+
{COUNT_FIELDS.map(({ key, label, description }) => (
139+
<SubscriptionRow
140+
key={key}
141+
id={`subscription-${key}`}
142+
label={label}
143+
description={description}
144+
value={draft[key] as number}
145+
options={subscriptionFrequencyOptions}
146+
onChange={(value) => setField(key, value)}
147+
/>
148+
))}
149+
</div>
150+
</FieldSet>
151+
</>
168152
)
169153
}

‎src/components/Pages/SettingsPage/UserSettingsForm/NotificationsSettings.tsx‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { UseFormReturn } from 'react-hook-form'
22
import type { z } from 'zod'
3+
import { DocsLink } from '@/components/shared/DocsLink'
34
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@/components/ui/Field'
45
import {
56
FormControl,
@@ -12,17 +13,30 @@ import {
1213
import { Input } from '@/components/ui/Input'
1314
import { FieldSubmit } from './FieldSubmit'
1415
import type { formSchema } from './formSchema'
16+
import {
17+
NotificationSubscriptionFields,
18+
type SubscriptionsDraft,
19+
} from './NotificationSubscriptionFields'
1520

1621
export const NotificationsSettings = ({
1722
form,
23+
subscriptions,
1824
}: {
1925
form: UseFormReturn<z.infer<typeof formSchema>>
26+
subscriptions: SubscriptionsDraft
2027
}) => {
2128
return (
2229
<FieldSet className="min-h-0 flex-auto">
2330
<FieldLegend>Notifications</FieldLegend>
24-
<FieldDescription>Manage your notification preferences.</FieldDescription>
25-
<FieldGroup className="min-h-0 flex-auto overflow-y-auto">
31+
<FieldDescription>
32+
Decide which MapRoulette notifications you would like to receive, along with whether you
33+
would like to be sent an email informing you of the notification (either immediately or as a
34+
daily digest).{' '}
35+
<DocsLink page="notificationsAndEmail" icon={null}>
36+
Learn about notifications and email
37+
</DocsLink>
38+
</FieldDescription>
39+
<FieldGroup className="min-h-0 flex-auto overflow-y-auto px-1">
2640
<FormField
2741
control={form.control}
2842
name="email"
@@ -33,14 +47,18 @@ export const NotificationsSettings = ({
3347
<Input type="email" {...field} />
3448
</FormControl>
3549
<FormDescription>
36-
If you request emails in your Notification Subscriptions, they will be sent here.
50+
If you request emails below, they will be sent here.
3751
</FormDescription>
3852
<FormMessage />
3953
</FormItem>
4054
)}
4155
/>
56+
<NotificationSubscriptionFields subscriptions={subscriptions} />
4257
</FieldGroup>
43-
<FieldSubmit isSubmitting={form.formState.isSubmitting} isDirty={form.formState.isDirty} />
58+
<FieldSubmit
59+
isSubmitting={form.formState.isSubmitting}
60+
isDirty={form.formState.isDirty || subscriptions.isDirty}
61+
/>
4462
</FieldSet>
4563
)
4664
}

‎src/components/Pages/SettingsPage/UserSettingsForm/index.tsx‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { User, UserSettings } from '@/types/User'
1111
import { ApiSettings } from './ApiSettings'
1212
import { formSchema } from './formSchema'
1313
import { GeneralSettings } from './GeneralSettings'
14-
import { NotificationSubscriptionsSettings } from './NotificationSubscriptionsSettings'
14+
import { useNotificationSubscriptionsDraft } from './NotificationSubscriptionFields'
1515
import { NotificationsSettings } from './NotificationsSettings'
1616
import { PluginSettings } from './PluginSettings'
1717
import { PluginUserSettingsFields } from './PluginUserSettingsFields'
@@ -31,6 +31,7 @@ const CORE_SETTINGS_KEYS = new Set([
3131

3232
export const UserSettingsForm = ({ user }: { user: User }) => {
3333
const updateSettingsMutation = api.user.useUpdateUserSettings()
34+
const subscriptions = useNotificationSubscriptionsDraft(user.id)
3435
const { locale } = useIntl()
3536

3637
const form = useForm<z.infer<typeof formSchema>>({
@@ -70,6 +71,10 @@ export const UserSettingsForm = ({ user }: { user: User }) => {
7071
...pluginSettings,
7172
} as unknown as UserSettings,
7273
})
74+
if (subscriptions.isDirty) {
75+
await subscriptions.save()
76+
}
77+
7378
toast.success('User settings updated')
7479
} catch {
7580
toast.error('Failed to update user settings')
@@ -80,18 +85,17 @@ export const UserSettingsForm = ({ user }: { user: User }) => {
8085
<Form {...form}>
8186
<form
8287
onSubmit={form.handleSubmit(onSubmit)}
83-
className="mx-auto flex max-h-full min-h-0 w-full max-w-3xl flex-col gap-4 py-4"
88+
className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col gap-4 py-4"
8489
>
8590
<h1 className="font-bold text-base">Account</h1>
86-
<Tabs defaultValue="general" className="flex min-h-0 flex-auto flex-col">
91+
<Tabs defaultValue="general" className="flex min-h-0 flex-1 flex-col">
8792
<TabsList>
8893
<TabsTrigger value="general">General</TabsTrigger>
8994
<TabsTrigger value="notifications">Notifications</TabsTrigger>
90-
<TabsTrigger value="subscriptions">Subscriptions</TabsTrigger>
9195
<TabsTrigger value="plugins">Plugins</TabsTrigger>
9296
<TabsTrigger value="api">API</TabsTrigger>
9397
</TabsList>
94-
<div className="flex min-h-0 flex-auto flex-col rounded-lg bg-zinc-50 p-4 lg:p-6 dark:bg-slate-900">
98+
<div className="flex max-h-full min-h-0 flex-initial flex-col overflow-hidden rounded-lg bg-zinc-50 p-4 lg:p-6 dark:bg-slate-900">
9599
<FieldGroup className="min-h-0 flex-auto">
96100
<TabsContent
97101
value="general"
@@ -105,13 +109,7 @@ export const UserSettingsForm = ({ user }: { user: User }) => {
105109
value="notifications"
106110
className="min-h-0 flex-auto data-[state=active]:flex data-[state=active]:flex-col"
107111
>
108-
<NotificationsSettings form={form} />
109-
</TabsContent>
110-
<TabsContent
111-
value="subscriptions"
112-
className="min-h-0 flex-auto data-[state=active]:flex data-[state=active]:flex-col"
113-
>
114-
<NotificationSubscriptionsSettings userId={user.id} />
112+
<NotificationsSettings form={form} subscriptions={subscriptions} />
115113
</TabsContent>
116114
<TabsContent
117115
value="plugins"

‎src/components/Pages/SettingsPage/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const SettingsPage = () => {
77

88
return (
99
<AuthGuard>
10-
<div className="flex min-h-0 flex-1 flex-col px-4">
10+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-4">
1111
{user && <UserSettingsForm user={user} />}
1212
</div>
1313
</AuthGuard>

0 commit comments

Comments
 (0)