Skip to content

Commit fb484cf

Browse files
authored
Feature/user settings (boundlessfi#68)
* feat: implement /api/user/settings route with GET and PUT support using Prisma * feat(settings): connect frontend notification toggle to backend API * PR fixes
1 parent b54128b commit fb484cf

4 files changed

Lines changed: 168 additions & 47 deletions

File tree

app/(dashboard)/settings/notification-settings.tsx

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
CardTitle,
1414
} from "@/components/ui/card";
1515
import { Label } from "@/components/ui/label";
16-
import { useState } from "react";
16+
import { updateUserSettings } from "@/lib/actions/settings";
17+
import { useEffect, useState } from "react";
1718

1819
const toast = (props: { title: string; description: string }) => {
1920
console.log(`Toast: ${props.title} - ${props.description}`);
@@ -22,6 +23,20 @@ const toast = (props: { title: string; description: string }) => {
2223

2324
export function NotificationSettings() {
2425
const [isLoading, setIsLoading] = useState(false);
26+
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
27+
28+
useEffect(() => {
29+
const fetchSettings = async () => {
30+
try {
31+
const res = await fetch("/api/user/settings");
32+
const data = await res.json();
33+
setNotificationsEnabled(data.notificationsEnabled);
34+
} catch (err) {
35+
console.error("Failed to fetch notification settings", err);
36+
}
37+
};
38+
fetchSettings();
39+
}, []);
2540

2641
const [emailNotifications, setEmailNotifications] = useState({
2742
projectUpdates: true,
@@ -37,19 +52,22 @@ export function NotificationSettings() {
3752
importantAnnouncements: true,
3853
});
3954

40-
function handleSave() {
55+
async function handleSave() {
4156
setIsLoading(true);
4257

43-
// Simulate API call
44-
setTimeout(() => {
45-
setIsLoading(false);
58+
try {
59+
await updateUserSettings({ notificationsEnabled });
60+
4661
toast({
4762
title: "Notification preferences updated",
48-
description:
49-
"Your notification preferences have been updated successfully.",
63+
description: "Your preferences were successfully saved.",
5064
});
51-
console.log({ emailNotifications, pushNotifications });
52-
}, 1000);
65+
} catch (err) {
66+
console.error(err);
67+
toast({ title: "Error", description: "Failed to update settings." });
68+
} finally {
69+
setIsLoading(false);
70+
}
5371
}
5472

5573
return (
@@ -211,16 +229,26 @@ export function NotificationSettings() {
211229
updates
212230
</p>
213231
</div>
232+
233+
{/*
234+
Only `notificationsEnabled` is saved to the database.
235+
Other toggles are local UI state with no backend mapping (yet).
236+
*/}
214237
<Switch
215238
id="push-announcements"
216-
checked={pushNotifications.importantAnnouncements}
217-
onCheckedChange={(checked) =>
218-
setPushNotifications({
219-
...pushNotifications,
220-
importantAnnouncements: checked,
221-
})
222-
}
239+
checked={notificationsEnabled}
240+
onCheckedChange={(checked) => setNotificationsEnabled(checked)}
223241
/>
242+
{/* <Switch
243+
id="push-announcements"
244+
checked={pushNotifications.importantAnnouncements}
245+
onCheckedChange={(checked) =>
246+
setPushNotifications({
247+
...pushNotifications,
248+
importantAnnouncements: checked,
249+
})
250+
}
251+
/> */}
224252
</div>
225253
</div>
226254
</div>

app/api/user/settings/route.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { authOptions } from '@/lib/auth.config';
2+
import prisma from '@/lib/prisma';
3+
import { getServerSession } from 'next-auth';
4+
import { NextResponse } from 'next/server';
5+
import { z } from 'zod';
6+
7+
const settingsSchema = z.object({
8+
language: z.string().optional(),
9+
notificationsEnabled: z.boolean().optional(),
10+
});
11+
12+
export async function GET() {
13+
const session = await getServerSession(authOptions);
14+
15+
if (!session?.user?.id) {
16+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
17+
}
18+
19+
const user = await prisma.user.findUnique({
20+
where: { id: session.user.id },
21+
select: {
22+
theme: true,
23+
language: true,
24+
notificationsEnabled: true,
25+
},
26+
});
27+
28+
return NextResponse.json(user);
29+
}
30+
31+
export async function PUT(request: Request) {
32+
const session = await getServerSession(authOptions);
33+
34+
if (!session?.user?.id) {
35+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
36+
}
37+
38+
try {
39+
const body = await request.json();
40+
const data = settingsSchema.parse(body);
41+
42+
const updated = await prisma.user.update({
43+
where: { id: session.user.id },
44+
data,
45+
select: {
46+
theme: true,
47+
language: true,
48+
notificationsEnabled: true,
49+
},
50+
});
51+
52+
return NextResponse.json(updated);
53+
} catch (error) {
54+
if (error instanceof z.ZodError) {
55+
return NextResponse.json({ error: 'Invalid input', details: error.flatten() }, { status: 400 });
56+
}
57+
58+
return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 });
59+
}
60+
}

lib/actions/settings.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type UserSettings = {
2+
theme?: string;
3+
language?: string;
4+
notificationsEnabled?: boolean;
5+
};
6+
7+
export async function getUserSettings(): Promise<UserSettings> {
8+
const res = await fetch('/api/user/settings');
9+
10+
if (!res.ok) {
11+
throw new Error('Failed to fetch settings');
12+
}
13+
14+
return res.json();
15+
}
16+
17+
export async function updateUserSettings(data: UserSettings): Promise<UserSettings> {
18+
const res = await fetch('/api/user/settings', {
19+
method: 'PUT',
20+
headers: { 'Content-Type': 'application/json' },
21+
body: JSON.stringify(data),
22+
});
23+
24+
if (!res.ok) {
25+
throw new Error('Failed to update settings');
26+
}
27+
28+
return res.json();
29+
}

prisma/schema.prisma

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,26 @@ model Session {
3535
}
3636

3737
model User {
38-
id String @id @default(cuid())
38+
id String @id @default(cuid())
3939
name String?
40-
email String? @unique
40+
email String? @unique
4141
emailVerified DateTime?
4242
image String?
4343
password String?
44-
role Role @default(USER)
45-
44+
role Role @default(USER)
45+
4646
// Profile fields
47-
username String? @unique
48-
bio String? @db.Text
49-
bannerImage String?
50-
twitter String?
51-
linkedin String?
47+
username String? @unique
48+
bio String? @db.Text
49+
bannerImage String?
50+
twitter String?
51+
linkedin String?
5252
totalContributions Int @default(0)
53-
53+
54+
// Settings fields
55+
language String? @default("en")
56+
notificationsEnabled Boolean? @default(true)
57+
5458
accounts Account[]
5559
sessions Session[]
5660
OTP OTP[]
@@ -153,23 +157,23 @@ enum ReactionType {
153157
}
154158

155159
model Comment {
156-
id String @id @default(cuid())
157-
content String @db.Text
158-
createdAt DateTime @default(now())
159-
updatedAt DateTime @updatedAt
160-
161-
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
160+
id String @id @default(cuid())
161+
content String @db.Text
162+
createdAt DateTime @default(now())
163+
updatedAt DateTime @updatedAt
164+
165+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
162166
projectId String
163-
164-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
165-
userId String
166-
167-
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: SetNull)
168-
parentId String?
169-
replies Comment[] @relation("CommentReplies")
170-
167+
168+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
169+
userId String
170+
171+
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: SetNull)
172+
parentId String?
173+
replies Comment[] @relation("CommentReplies")
174+
171175
reactions CommentReaction[]
172-
176+
173177
@@index([projectId])
174178
@@index([userId])
175179
@@index([parentId])
@@ -179,13 +183,13 @@ model CommentReaction {
179183
id String @id @default(cuid())
180184
type ReactionType
181185
createdAt DateTime @default(now())
182-
183-
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
186+
187+
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
184188
commentId String
185-
186-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
187-
userId String
188-
189+
190+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
191+
userId String
192+
189193
@@unique([commentId, userId])
190194
@@index([commentId])
191195
@@index([userId])

0 commit comments

Comments
 (0)