Skip to content

Commit bfb8668

Browse files
Merge pull request #62 from guillermoscript/17-specific-view-for-reading-all-the-new-notifications
Refactor Notifications feature and add support for comment reactions
2 parents 8c59ecb + 14e342a commit bfb8668

File tree

18 files changed

+518
-67
lines changed

18 files changed

+518
-67
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use server'
2+
import { revalidatePath } from 'next/cache'
3+
4+
import { createResponse } from '@/utils/functions'
5+
import { createClient } from '@/utils/supabase/server'
6+
import { Tables } from '@/utils/supabase/supabase'
7+
8+
export async function notificationUpdate (notification: Tables<'notifications'>) {
9+
const supabase = createClient()
10+
const commentUpdate = await supabase
11+
.from('notifications')
12+
.update({
13+
...notification
14+
})
15+
.eq('notification_id', notification.notification_id)
16+
17+
if (commentUpdate.error) {
18+
console.log(commentUpdate.error)
19+
return createResponse('error', 'Error updating notification', null, 'Error updating notification')
20+
}
21+
22+
revalidatePath('/dashboard/student/', 'layout')
23+
return createResponse('success', 'Notification updated successfully', null, null)
24+
}

actions/dashboard/studentActions.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import { Tables } from '@/utils/supabase/supabase'
1010
export async function studentSubmitLessonComment (state: {
1111
comment: string
1212
lesson_id: number
13+
course_id: number
1314
parent_comment_id?: number
1415
}) {
1516
const supabase = createClient()
1617
const userData = await supabase.auth.getUser()
1718

1819
if (userData.error) {
19-
return createResponse('error', 'Error updating lesson', null, 'Error updating lesson')
20+
return createResponse('error', 'Error no user found', null, 'Error no user found')
2021
}
2122

2223
const studentId = userData.data.user?.id
@@ -30,7 +31,33 @@ export async function studentSubmitLessonComment (state: {
3031
})
3132

3233
if (commentInsert.error) {
33-
return createResponse('error', 'Error updating lesson', null, 'Error updating lesson')
34+
return createResponse('error', 'Error creating comment', null, 'Error creating comment')
35+
}
36+
37+
// if comment is a reply, then add a new notification for the parent comment owner
38+
if (state.parent_comment_id) {
39+
const parentComment = await supabase
40+
.from('lesson_comments')
41+
.select('user_id')
42+
.eq('id', state.parent_comment_id)
43+
.single()
44+
45+
if (parentComment.error) {
46+
return createResponse('error', 'Error searching for parent comment', null, 'Error searching for parent comment')
47+
}
48+
49+
const notificationInsert = await supabase.from('notifications').insert({
50+
user_id: parentComment.data.user_id,
51+
message: `${state.comment}`,
52+
link: `/dashboard/student/courses/${state.course_id}/lessons/${state.lesson_id}`,
53+
shrot_message: `**${userData.data.user?.email}** replied to your comment.`,
54+
created_at: new Date().toISOString(),
55+
notification_type: 'comment_reply'
56+
})
57+
58+
if (notificationInsert.error) {
59+
return createResponse('error', 'Error updating lesson', null, 'Error updating lesson')
60+
}
3461
}
3562

3663
revalidatePath('/dashboard/student/courses/[courseId]/lessons/[lessonId]', 'layout')

app/dashboard/loading.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ export default function Component () {
3030
</div>
3131
<div className="flex flex-col">
3232
<main className="flex-1 p-4 md:p-6">
33-
<div className="flex items-center">
34-
<Skeleton className="h-6 w-40 rounded-md" />
35-
<Skeleton className="h-8 w-20 ml-auto rounded-md" />
36-
</div>
3733
<div className="mt-4 border rounded-lg shadow-sm">
3834
<Skeleton className="h-[200px] w-full rounded-t-lg" />
3935
<div className="p-4 md:p-6">

app/dashboard/notifications/page.tsx

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1+
import dayjs from 'dayjs'
2+
import relativeTime from 'dayjs/plugin/relativeTime'
3+
import { Eye } from 'lucide-react'
4+
5+
import NotificationSidebarFilter from '@/components/dashboards/notifications/NotificationSidebarFilter'
6+
import NotificationsReadButton from '@/components/dashboards/notifications/NotificationsReadButton'
7+
import ViewMarkdown from '@/components/ui/markdown/ViewMarkdown'
18
import { createClient } from '@/utils/supabase/server'
29

3-
export default async function NotificationsPage () {
10+
dayjs.extend(relativeTime)
11+
12+
function formatNotificationTime (created_at: string): string {
13+
return dayjs(created_at).fromNow()
14+
}
15+
16+
export default async function NotificationsPage ({ searchParams }: {
17+
searchParams?: {
18+
filter?: string
19+
}
20+
}) {
421
const supabase = createClient()
522
const userData = await supabase.auth.getUser()
623

@@ -14,16 +31,69 @@ export default async function NotificationsPage () {
1431
.eq('user_id', userData.data.user.id)
1532
.order('created_at', { ascending: false })
1633

34+
if (notifications.error) {
35+
console.log(notifications.error)
36+
throw new Error(notifications.error.message)
37+
}
38+
39+
const filteredData = (searchParams.filter === 'all' || !searchParams.filter) ? notifications.data : notifications.data.filter((notification) => notification.notification_type === searchParams.filter)
40+
1741
return (
18-
<div>
19-
<h1>Notifications</h1>
20-
<ul>
21-
{notifications.data.map((notification) => (
22-
<li key={notification.notification_id}>
23-
{notification.message}
24-
</li>
25-
))}
26-
</ul>
27-
</div>
42+
<>
43+
<div className="flex justify-between items-center px-8 py-4 border-b border-gray-200 dark:border-gray-700">
44+
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
45+
Notifications
46+
</h1>
47+
<button className="text-lg text-blue-500">Settings</button>
48+
</div>
49+
<div className="grid md:grid-cols-3 grid-cols-1 gap-4 mt-4 px-8">
50+
<aside className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
51+
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">
52+
Filter by type
53+
</h2>
54+
<NotificationSidebarFilter />
55+
</aside>
56+
<div className="md:col-span-2">
57+
<ul className="space-y-4">
58+
{filteredData.map((notification) => (
59+
<li
60+
key={notification.notification_id}
61+
className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow"
62+
>
63+
<div className="flex items-center mb-2">
64+
<div className="text-sm">
65+
<p className="text-gray-500 dark:text-gray-400">
66+
{formatNotificationTime(notification.created_at)}
67+
</p>
68+
</div>
69+
</div>
70+
<div className="ml-13">
71+
<ViewMarkdown markdown={notification.message} />
72+
<div className="flex text-gray-600 dark:text-gray-400 mt-3 space-x-4">
73+
{/* <button className="flex items-center space-x-1">
74+
<ThumbsUpIcon className="h-5 w-5" />
75+
<span>Like</span>
76+
</button> */}
77+
<NotificationsReadButton notification={notification}>
78+
{notification.read ? (
79+
<>
80+
<Eye className="h-5 w-5" />
81+
<span>Viewed</span>
82+
</>
83+
) : (
84+
<>
85+
<Eye className="h-5 w-5" />
86+
<span>View</span>
87+
</>
88+
)}
89+
</NotificationsReadButton>
90+
</div>
91+
</div>
92+
</li>
93+
))}
94+
</ul>
95+
</div>
96+
</div>
97+
</>
2898
)
2999
}

app/dashboard/student/courses/[courseId]/lessons/[lessonsId]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default async function StudentLessonPage ({
7878
<TabsContent value="comments">
7979
<Suspense fallback={<div>Loading...</div>}>
8080
<CommentsSections
81+
course_id={Number(params.courseId)}
8182
lesson_id={lessonData.data.id}
8283
lesson_comments={lessonData.data.lesson_comments}
8384
/>

app/dashboard/teacher/courses/[courseId]/lessons/[lessonId]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default async function TeacherLessonPage ({
4646
sideBar={
4747
<CommentsSections
4848
lesson_id={lesson?.data?.id}
49+
course_id={lesson?.data?.courses?.course_id}
4950
lesson_comments={lesson?.data?.lesson_comments}
5051
/>
5152
}

components/dashboards/Common/CommandDialogComponent.tsx

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use client'
22

33
import {
4+
Bell,
45
Calculator,
5-
Calendar,
66
CreditCard,
7-
Settings,
87
Smile,
98
User
109
} from 'lucide-react'
11-
import * as React from 'react'
10+
import Link from 'next/link'
11+
import { useEffect, useState } from 'react'
1212

1313
import {
1414
CommandDialog,
@@ -17,14 +17,21 @@ import {
1717
CommandInput,
1818
CommandItem,
1919
CommandList,
20-
CommandSeparator,
21-
CommandShortcut
20+
CommandSeparator
2221
} from '@/components/ui/command'
22+
import { useToast } from '@/components/ui/use-toast'
23+
import { createClient } from '@/utils/supabase/client'
24+
import { getClientUserRole } from '@/utils/supabase/getClientUserRole'
2325

2426
export function CommandDialogComponent () {
25-
const [open, setOpen] = React.useState(false)
27+
const [open, setOpen] = useState(false)
28+
const [userRole, setUserRole] = useState('' as string)
29+
const [loading, setLoading] = useState(false)
30+
const { toast } = useToast()
31+
const [courses, setCourses] = useState([])
32+
const [lessons, setLessons] = useState([])
2633

27-
React.useEffect(() => {
34+
useEffect(() => {
2835
const down = (e: KeyboardEvent) => {
2936
if (e.key === 'j' && (e.metaKey || e.ctrlKey)) {
3037
e.preventDefault()
@@ -36,6 +43,45 @@ export function CommandDialogComponent () {
3643
return () => document.removeEventListener('keydown', down)
3744
}, [])
3845

46+
useEffect(() => {
47+
async function fetchUserRole () {
48+
const response = await getClientUserRole()
49+
setUserRole(response)
50+
}
51+
52+
fetchUserRole()
53+
}, [])
54+
55+
useEffect(() => {
56+
async function fetchCourses () {
57+
setLoading(true)
58+
try {
59+
const supabase = createClient()
60+
const { data: courses, error } = await supabase.from('courses').select('*').limit(5)
61+
62+
if (error) {
63+
console.error(error)
64+
return null
65+
}
66+
67+
setCourses(courses)
68+
} catch (error) {
69+
toast({
70+
title: 'Error fetching courses',
71+
description: error.message,
72+
variant: 'destructive'
73+
})
74+
} finally {
75+
setLoading(false)
76+
}
77+
}
78+
79+
fetchCourses()
80+
}
81+
, [userRole])
82+
83+
console.log(courses)
84+
3985
return (
4086
<>
4187
<div className="text-sm text-muted-foreground">
@@ -49,8 +95,13 @@ export function CommandDialogComponent () {
4995
<CommandEmpty>No results found.</CommandEmpty>
5096
<CommandGroup heading="Suggestions">
5197
<CommandItem>
52-
<Calendar className="mr-2 h-4 w-4" />
53-
<span>Calendar</span>
98+
<Link
99+
className='flex items-center gap-2'
100+
href="/dashboard/notifications"
101+
>
102+
<Bell className="mr-2 h-4 w-4" />
103+
<span>Notifications</span>
104+
</Link>
54105
</CommandItem>
55106
<CommandItem>
56107
<Smile className="mr-2 h-4 w-4" />
@@ -64,21 +115,41 @@ export function CommandDialogComponent () {
64115
<CommandSeparator />
65116
<CommandGroup heading="Settings">
66117
<CommandItem>
67-
<User className="mr-2 h-4 w-4" />
68-
<span>Profile</span>
69-
<CommandShortcut>⌘P</CommandShortcut>
70-
</CommandItem>
71-
<CommandItem>
72-
<CreditCard className="mr-2 h-4 w-4" />
73-
<span>Billing</span>
74-
<CommandShortcut>⌘B</CommandShortcut>
118+
<Link
119+
className='flex items-center gap-2'
120+
href={`/dashboard/${userRole}/profile`}
121+
>
122+
<User className="mr-2 h-4 w-4" />
123+
<span>Profile</span>
124+
</Link>
75125
</CommandItem>
76126
<CommandItem>
77-
<Settings className="mr-2 h-4 w-4" />
78-
<span>Settings</span>
79-
<CommandShortcut>⌘S</CommandShortcut>
127+
<Link
128+
className='flex items-center gap-2'
129+
href={`/dashboard/${userRole}/profile`}
130+
>
131+
<CreditCard className="mr-2 h-4 w-4" />
132+
<span>Billing</span>
133+
</Link>
80134
</CommandItem>
81135
</CommandGroup>
136+
<CommandGroup heading="Courses">
137+
{loading
138+
? <>Loading...</>
139+
: courses.map((course: any) => (
140+
<CommandItem key={course.course_id}>
141+
<Link
142+
className='flex items-center gap-2'
143+
href={`/dashboard/${userRole}/courses/${course.course_id}`}
144+
>
145+
<span>{course.title}</span>
146+
</Link>
147+
</CommandItem>
148+
))
149+
}
150+
151+
</CommandGroup>
152+
82153
</CommandList>
83154
</CommandDialog>
84155
</>

0 commit comments

Comments
 (0)