-
Notifications
You must be signed in to change notification settings - Fork 24
fix: harden EduDesk API handlers and dashboard state #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
4bf2dbf
786f196
f8497d9
e2e78e6
6eece45
bcdb097
c6311ad
b226041
8914ff4
0d8e23c
fc6af7b
e82b944
9ea4918
f530169
a7cdbb9
ab2575d
ff55180
8b88cc5
055487b
43c256a
ee7249d
7a21f8a
7ee4890
dfc35bb
4fccff5
6219828
b8eb247
267235d
52ddc21
2b81c1d
c7c40a8
463ad4d
65fc704
4b13671
d5774df
fa49c8a
d861074
2485abf
d47ddd2
db2900a
5924714
95010df
84bd692
5c776e3
ac290d6
eec1e9b
923e838
dc33710
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,18 @@ import mongoose from 'mongoose' | |
| import { connectDB } from '@/lib/mongodb' | ||
| import { Grade } from '@/models/Grade' | ||
|
|
||
| const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade'] | ||
| const ALLOWED_UPDATE_FIELDS = ['studentId', 'studentName', 'subject', 'term', 'marks', 'maxMarks'] | ||
|
|
||
| function calcGrade(marks: number, max: number): string { | ||
| const pct = (marks / max) * 100 | ||
| if (pct >= 90) return 'A+' | ||
| if (pct >= 80) return 'A' | ||
| if (pct >= 70) return 'B+' | ||
| if (pct >= 60) return 'B' | ||
| if (pct >= 50) return 'C' | ||
| if (pct >= 40) return 'D' | ||
| return 'F' | ||
| } | ||
|
|
||
| export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { | ||
| const { userId } = await auth() | ||
|
|
@@ -34,17 +45,30 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string | |
| } | ||
|
|
||
| await connectDB() | ||
|
|
||
| const existingGrade = await Grade.findOne({ _id: id, teacherId: userId }) | ||
| if (!existingGrade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
|
|
||
| const nextMarks = | ||
| typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existingGrade.marks | ||
| const nextMaxMarks = | ||
| typeof sanitizedBody.maxMarks === 'number' ? sanitizedBody.maxMarks : existingGrade.maxMarks | ||
|
|
||
| sanitizedBody.grade = calcGrade(nextMarks, nextMaxMarks) | ||
|
Comment on lines
+76
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recalculated grade can drift from the persisted The Either reject non-numeric ♻️ Suggested direction- const nextMarks =
- typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existingGrade.marks
- const nextMaxMarks =
- typeof sanitizedBody.maxMarks === 'number' ? sanitizedBody.maxMarks : existingGrade.maxMarks
+ if ('marks' in sanitizedBody && typeof sanitizedBody.marks !== 'number') {
+ return NextResponse.json({ error: '`marks` must be a number' }, { status: 400 })
+ }
+ if ('maxMarks' in sanitizedBody && typeof sanitizedBody.maxMarks !== 'number') {
+ return NextResponse.json({ error: '`maxMarks` must be a number' }, { status: 400 })
+ }
+ const nextMarks = (sanitizedBody.marks as number | undefined) ?? existingGrade.marks
+ const nextMaxMarks = (sanitizedBody.maxMarks as number | undefined) ?? existingGrade.maxMarks
+ if (!nextMaxMarks || nextMaxMarks <= 0) {
+ return NextResponse.json({ error: '`maxMarks` must be greater than 0' }, { status: 400 })
+ }The explicit 🤖 Prompt for AI Agents |
||
|
|
||
| const grade = await Grade.findOneAndUpdate( | ||
| { _id: id }, | ||
| sanitizedBody, | ||
| { new: true } | ||
| { _id: id, teacherId: userId }, | ||
| { $set: sanitizedBody }, | ||
| { new: true, runValidators: true, context: 'query' } | ||
| ) | ||
| if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| return NextResponse.json(grade) | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| console.error('PUT /api/grades/[id] error:', error.message) | ||
| } | ||
| if ((error as { code?: number }).code === 11000) { | ||
| return NextResponse.json({ error: 'A grade already exists for this student, subject, and term' }, { status: 409 }) | ||
| } | ||
| return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) | ||
| } | ||
| } | ||
|
|
@@ -55,8 +79,13 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str | |
|
|
||
| try { | ||
| const { id } = await ctx.params | ||
|
|
||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| } | ||
|
|
||
| await connectDB() | ||
| const deleted = await Grade.findOneAndDelete({ _id: id }) | ||
| const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId }) | ||
|
|
||
| if (!deleted) { | ||
| return NextResponse.json({ error: 'Grade not found' }, { status: 404 }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,22 +8,13 @@ interface NavbarProps { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function Navbar({ onMenuClick, title }: NavbarProps) { | ||||||||||||||||||||||||||||||||
| const [dark, setDark] = useState(false); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Initialize theme from localStorage and system preference on client-side only | ||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const stored = localStorage.getItem("theme"); | ||||||||||||||||||||||||||||||||
| const prefersDark = window.matchMedia( | ||||||||||||||||||||||||||||||||
| "(prefers-color-scheme: dark)", | ||||||||||||||||||||||||||||||||
| ).matches; | ||||||||||||||||||||||||||||||||
| const isDark = stored ? stored === "dark" : prefersDark; | ||||||||||||||||||||||||||||||||
| setDark(isDark); | ||||||||||||||||||||||||||||||||
| document.documentElement.classList.toggle("dark", isDark); | ||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||
| // Silently fail if localStorage is not available | ||||||||||||||||||||||||||||||||
| const [dark, setDark] = useState(() => { | ||||||||||||||||||||||||||||||||
| if (typeof document === 'undefined') { | ||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return document.documentElement.classList.contains('dark') | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential hydration mismatch from lazy initializer. During SSR, Consider initializing to Proposed fix- const [dark, setDark] = useState(() => {
- if (typeof document === 'undefined') {
- return false
- }
-
- return document.documentElement.classList.contains('dark')
- })
+ const [dark, setDark] = useState(false)
+
+ // Sync from DOM after mount to avoid hydration mismatch with the
+ // pre-hydration theme script in app/layout.tsx.
+ useEffect(() => {
+ setDark(document.documentElement.classList.contains('dark'))
+ }, [])📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Sync dark class to <html> whenever dark state changes | ||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.