Skip to content

Commit 5af5018

Browse files
committed
feat: Add boolean fields for Discord notification settings + updated schema accordingly
1 parent 3180651 commit 5af5018

7 files changed

+241
-23
lines changed

app/lib/db/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { sql } from "drizzle-orm";
22
import {
3+
boolean,
34
index,
45
integer,
56
numeric,
@@ -136,6 +137,11 @@ export const userSettings = pgTable(identifyTable("UserSettings"), {
136137
onUpdate: "cascade",
137138
}),
138139
font: text("font").notNull().default("Default"),
140+
discordCommunication: boolean("discord_communication")
141+
.notNull()
142+
.default(false),
143+
discordTransactions: boolean("discord_transactions").notNull().default(true),
144+
discordSecurity: boolean("discord_security").notNull().default(true),
139145
});
140146
export type UserSetting = typeof userSettings.$inferSelect;
141147
export type NewUserSetting = typeof userSettings.$inferInsert;

app/lib/get.queries.server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,28 @@ export async function getAppearanceSettings(
124124
};
125125
}
126126

127+
/**
128+
* Get notification settings for a user.
129+
*
130+
* @param userId The ID of the user.
131+
* @returns The notification settings.
132+
*/
133+
export async function getNotificationSettings(
134+
userId: number,
135+
): Promise<Partial<UserSetting>> {
136+
const userSettings = await getUserSettingsById(userId);
137+
138+
if (!userSettings) {
139+
throw new Error("User settings not found");
140+
}
141+
142+
return {
143+
discordCommunication: userSettings.discordCommunication,
144+
discordTransactions: userSettings.discordTransactions,
145+
discordSecurity: userSettings.discordSecurity,
146+
};
147+
}
148+
127149
/**
128150
* Get transactions based on the input provided. This function is used to
129151
* fetch transactions for the transactions table using the search parameters

app/lib/post.queries.server.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,30 @@ export async function updateAccountSettings(
253253
*/
254254
export async function updateAppearanceSettings(
255255
userId: number,
256-
settings: Omit<Partial<UserSetting>, "id" | "user_id">,
256+
settings: Omit<
257+
Partial<UserSetting>,
258+
| "id"
259+
| "user_id"
260+
| "discordCommunication"
261+
| "discordTransactions"
262+
| "discordSecurity"
263+
>,
264+
): Promise<void> {
265+
await db
266+
.update(userSettings)
267+
.set(settings)
268+
.where(eq(userSettings.userId, userId));
269+
}
270+
271+
/**
272+
* Get a user's notification settings.
273+
*
274+
* @param userId The ID of the user.
275+
* @returns The user's notification settings.
276+
*/
277+
export async function updateNotificationSettings(
278+
userId: number,
279+
settings: Omit<Partial<UserSetting>, "id" | "user_id" | "font">,
257280
): Promise<void> {
258281
await db
259282
.update(userSettings)

app/routes/app.$server.transactions.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import {
22
type ActionFunctionArgs,
33
json,
44
type LoaderFunctionArgs,
5-
unstable_parseMultipartFormData,
65
type TypedResponse,
6+
unstable_parseMultipartFormData,
77
} from "@remix-run/node";
88
import { Await, defer, useLoaderData } from "@remix-run/react";
99
import { Suspense } from "react";
@@ -29,18 +29,20 @@ import { searchParamsSchema } from "~/lib/validations";
2929
export type DepositActionData = { success: boolean; message: string };
3030
export type WithdrawActionData = { success: boolean; message: string };
3131
export type TransferActionData = { success: boolean; message: string };
32-
export type ExportActionData = { success: boolean; message: string; data?: Transaction[] };
32+
export type ExportActionData = {
33+
success: boolean;
34+
message: string;
35+
data?: Transaction[];
36+
};
3337

34-
type ActionData =
35-
| DepositActionData
36-
| WithdrawActionData
37-
| TransferActionData
38+
type ActionData =
39+
| DepositActionData
40+
| WithdrawActionData
41+
| TransferActionData
3842
| ExportActionData;
3943

4044
export type ActionReturn = TypedResponse<ActionData>;
4145

42-
export type ActionFunction = (args: ActionFunctionArgs) => Promise<ActionReturn>;
43-
4446
export async function loader({ request, params }: LoaderFunctionArgs) {
4547
const url = new URL(request.url);
4648
const search = searchParamsSchema.parse(Object.fromEntries(url.searchParams));

app/routes/app.settings.account.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { compare } from "@node-rs/bcrypt";
22
import {
3-
ActionFunctionArgs,
3+
type ActionFunctionArgs,
44
defer,
55
json,
66
type LoaderFunctionArgs,
@@ -23,11 +23,6 @@ import {
2323
} from "~/lib/post.queries.server";
2424
import { authenticator } from "~/lib/services/auth.server";
2525

26-
export type AccountSettingsFetcherResponse = {
27-
success: boolean;
28-
message?: string;
29-
};
30-
3126
export async function loader({ request }: LoaderFunctionArgs) {
3227
const userId = (
3328
await authenticator.isAuthenticated(request, {
@@ -178,7 +173,7 @@ export async function action({ request }: ActionFunctionArgs) {
178173

179174
export default function AccountSettings() {
180175
const { accountSettings } = useLoaderData<typeof loader>();
181-
const fetcher = useFetcher<AccountSettingsFetcherResponse>();
176+
const fetcher = useFetcher<typeof action>();
182177

183178
const isSubmitting = fetcher.state === "submitting";
184179

app/routes/app.settings.appearance.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {
2-
ActionFunctionArgs,
2+
type ActionFunctionArgs,
33
defer,
44
json,
55
type LoaderFunctionArgs,
66
} from "@remix-run/node";
77
import { Await, useFetcher, useLoaderData } from "@remix-run/react";
8-
import { Suspense } from "react";
8+
import { Suspense, useEffect } from "react";
99
import { namedAction } from "remix-utils/named-action";
10+
import { toast } from "sonner";
1011

1112
import AppSettingsLayout from "~/components/Layout/AppSettingsLayout";
1213
import SettingsSelect from "~/components/Select/SettingsSelect";
@@ -75,10 +76,22 @@ export async function action({ request }: ActionFunctionArgs) {
7576

7677
export default function AppearanceSettings() {
7778
const { appearanceSettings } = useLoaderData<typeof loader>();
78-
const fetcher = useFetcher();
79+
const fetcher = useFetcher<typeof action>();
7980

8081
const isSubmitting = fetcher.state === "submitting";
8182

83+
useEffect(() => {
84+
if (fetcher.data && !fetcher.data.success && fetcher.state === "idle") {
85+
toast.error(fetcher.data.message);
86+
} else if (
87+
fetcher.data &&
88+
fetcher.data.success &&
89+
fetcher.state === "idle"
90+
) {
91+
toast.success(fetcher.data.message);
92+
}
93+
}, [fetcher.data, fetcher.state]);
94+
8295
return (
8396
<AppSettingsLayout title="Appearance" desc="Customize the look of the app.">
8497
<Suspense
Lines changed: 161 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,175 @@
1-
import { type LoaderFunctionArgs } from "@remix-run/node";
1+
import {
2+
type ActionFunctionArgs,
3+
defer,
4+
json,
5+
type LoaderFunctionArgs,
6+
} from "@remix-run/node";
7+
import { Await, useFetcher, useLoaderData } from "@remix-run/react";
8+
import { Suspense, useEffect } from "react";
9+
import { namedAction } from "remix-utils/named-action";
10+
import { toast } from "sonner";
211

312
import AppSettingsLayout from "~/components/Layout/AppSettingsLayout";
13+
import SettingsSwitch from "~/components/Switch/SettingsSwitch";
14+
import { Button } from "~/components/ui/button";
15+
import { SpokeSpinner } from "~/components/ui/spinner";
16+
import { getNotificationSettings } from "~/lib/get.queries.server";
17+
import { updateNotificationSettings } from "~/lib/post.queries.server";
18+
import { authenticator } from "~/lib/services/auth.server";
419

520
export async function loader({ request }: LoaderFunctionArgs) {
6-
return null;
21+
const userId = (
22+
await authenticator.isAuthenticated(request, {
23+
failureRedirect: "/login",
24+
})
25+
).id;
26+
27+
const notificationSettings = getNotificationSettings(userId);
28+
29+
return defer(
30+
{ notificationSettings },
31+
{
32+
status: 200,
33+
headers: {
34+
"Cache-Control": "private, max-age=86400", // 1 day
35+
},
36+
},
37+
);
38+
}
39+
40+
export async function action({ request }: ActionFunctionArgs) {
41+
const formData = await request.formData();
42+
43+
const userId = (
44+
await authenticator.isAuthenticated(request, {
45+
failureRedirect: "/login",
46+
})
47+
).id;
48+
49+
return namedAction(request, {
50+
async save_notification_settings() {
51+
const communication = formData.get("communication") === "on";
52+
const transactions = formData.get("transactions") === "on";
53+
const security = formData.get("security") === "on";
54+
55+
try {
56+
await updateNotificationSettings(userId, {
57+
discordCommunication: communication,
58+
discordTransactions: transactions,
59+
discordSecurity: security,
60+
});
61+
} catch (error) {
62+
console.error("Error saving notification settings:", error);
63+
return json(
64+
{
65+
success: false,
66+
message: "Error saving notification settings.",
67+
},
68+
{
69+
status: 500,
70+
},
71+
);
72+
}
73+
74+
return json({
75+
success: true,
76+
message: "Notification settings saved successfully.",
77+
});
78+
},
79+
});
780
}
881

982
export default function NotificationSettings() {
83+
const { notificationSettings } = useLoaderData<typeof loader>();
84+
const fetcher = useFetcher<typeof action>();
85+
86+
const isSubmitting = fetcher.state === "submitting";
87+
88+
useEffect(() => {
89+
if (fetcher.data && !fetcher.data.success && fetcher.state === "idle") {
90+
toast.error(fetcher.data.message);
91+
} else if (
92+
fetcher.data &&
93+
fetcher.data.success &&
94+
fetcher.state === "idle"
95+
) {
96+
toast.success(fetcher.data.message);
97+
}
98+
}, [fetcher.data, fetcher.state]);
99+
10100
return (
11101
<AppSettingsLayout
12102
title="Notifications"
13-
desc="Customize the look of the app."
103+
desc="Manage your notifications settings"
14104
>
15-
<p>Notifications page</p>
105+
<Suspense
106+
fallback={
107+
<div className="flex h-64 items-center justify-center">
108+
<SpokeSpinner color="white" />
109+
</div>
110+
}
111+
>
112+
<Await
113+
resolve={notificationSettings}
114+
errorElement={
115+
<div className="flex h-48 items-center justify-center">
116+
<p className="text-red-500">
117+
Error loading appearance settings, please try again later.
118+
</p>
119+
</div>
120+
}
121+
>
122+
{(notificationSettings) => (
123+
<fetcher.Form
124+
method="post"
125+
action="?/save_notification_settings"
126+
className="space-y-4"
127+
>
128+
<div>
129+
<h3 className="mb-2 text-lg font-semibold">
130+
Discord Notifications
131+
</h3>
132+
<div className="space-y-4">
133+
<SettingsSwitch
134+
name="communication"
135+
label="Communication"
136+
defaultChecked={notificationSettings.discordCommunication}
137+
description="Receive discord notifications for announcements, new features, etc."
138+
/>
139+
140+
<SettingsSwitch
141+
name="transactions"
142+
label="Transactions"
143+
defaultChecked={notificationSettings.discordTransactions}
144+
description="Receive discord notifications for transactions."
145+
/>
146+
147+
<SettingsSwitch
148+
name="security"
149+
label="Security"
150+
defaultChecked={notificationSettings.discordSecurity}
151+
description="Receive discord notifications for suspicious login attempts, etc."
152+
/>
153+
154+
<Button
155+
type="submit"
156+
name="_action"
157+
value="save_notification_settings"
158+
variant="default"
159+
className="!mt-8 w-1/2"
160+
disabled={isSubmitting}
161+
>
162+
{isSubmitting && (
163+
<SpokeSpinner size="sm" className="mr-1" />
164+
)}
165+
Save Changes
166+
</Button>
167+
</div>
168+
</div>
169+
</fetcher.Form>
170+
)}
171+
</Await>
172+
</Suspense>
16173
</AppSettingsLayout>
17174
);
18175
}

0 commit comments

Comments
 (0)