diff --git a/client/app/app/notifications/layout.jsx b/client/app/app/notifications/layout.jsx new file mode 100644 index 0000000..275b382 --- /dev/null +++ b/client/app/app/notifications/layout.jsx @@ -0,0 +1,7 @@ +export const metadata = { + title: 'bildirimler' +}; + +export default function Layout({ children }) { + return children; +} diff --git a/client/app/app/notifications/page.jsx b/client/app/app/notifications/page.jsx index e095606..e94549a 100644 --- a/client/app/app/notifications/page.jsx +++ b/client/app/app/notifications/page.jsx @@ -1,7 +1,111 @@ -export const metadata = { - title: 'bildirimler' -}; +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import Notification from '@/components/app/Notification'; +import NotificationSkeleton from '@/components/app/Notification/Skeleton'; +import { getNotifications } from '@/lib/api/me'; export default function Page() { - return
bu sayfa henüz hazır değil {':('}
; + const [notifications, setNotifications] = useState([]); + const [offset, setOffset] = useState(10); + const [hasMoreNotification, setHasMoreNotification] = useState(true); + const [loading, setLoading] = useState(false); + const { toast } = useToast(); + + const loadMoreNotifications = async () => { + if (!hasMoreNotification) return; + + const response = await getNotifications(11, offset); + if (!response || response.status === 429) { + toast({ + title: 'hay aksi, bir şeyler ters gitti!', + description: + 'sunucudan yanıt alınamadı. lütfen daha sonra tekrar deneyin.', + duration: 3000 + }); + return; + } + + const newNotifications = response.data.notifications || []; + + if (newNotifications.length > 10) { + setNotifications((prevNotifications) => [ + ...prevNotifications, + ...newNotifications.slice(0, 10) + ]); + } else { + setNotifications((prevNotifications) => [ + ...prevNotifications, + ...newNotifications + ]); + setHasMoreNotification(false); + } + + setOffset((prevOffset) => prevOffset + 10); + }; + + useEffect( + () => { + const fetchInitialNotifications = async () => { + setLoading(true); + const response = await getNotifications(11, 0); + + if (!response || response.status === 429) { + toast({ + title: 'hay aksi, bir şeyler ters gitti!', + description: + 'sunucudan yanıt alınamadı. Lütfen daha sonra tekrar deneyin.', + duration: 3000 + }); + return; + } + + const initialNotifications = response.data.notifications || []; + + if (initialNotifications.length > 10) { + setNotifications(initialNotifications.slice(0, 10)); + } else { + setNotifications(initialNotifications); + setHasMoreNotification(false); + } + + setLoading(false); + }; + + fetchInitialNotifications(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( +
+ {loading && ( + <> + + + + )} + {!loading && notifications.length > 0 ? ( + <> + {notifications.map((notification) => ( + + ))} + {hasMoreNotification && ( + + )} + + ) : ( + !loading && ( +
+ buralar şimdilik sessiz. +
+ ) + )} +
+ ); } diff --git a/client/components/app/Navbar/Item.jsx b/client/components/app/Navbar/Item.jsx index 9eeddf6..d9c4ba6 100644 --- a/client/components/app/Navbar/Item.jsx +++ b/client/components/app/Navbar/Item.jsx @@ -1,19 +1,46 @@ import Link from 'next/link'; +import { useState, useEffect } from 'react'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { getUnreadNotificationsCount } from '@/lib/api/me'; export default function Item({ pathname, href, icon: Icon, label }) { const isActive = pathname === href || (href !== '/app' && pathname.startsWith(href)); + const [unreadNotificationsCount, setUnreadNotificationsCount] = useState(0); + + useEffect( + () => { + const fetchUnreadNotificationsCount = async () => { + const response = await getUnreadNotificationsCount(); + if (response) { + setUnreadNotificationsCount(response.data.unreads); + } + }; + + fetchUnreadNotificationsCount(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return ( ); diff --git a/client/components/app/Notification/Skeleton.jsx b/client/components/app/Notification/Skeleton.jsx new file mode 100644 index 0000000..b71b13c --- /dev/null +++ b/client/components/app/Notification/Skeleton.jsx @@ -0,0 +1,16 @@ +import { Skeleton } from '@/components/ui/skeleton'; + +export default function NotificationSkeleton() { + return ( +
+
+ +
+ + +
+
+ +
+ ) +} \ No newline at end of file diff --git a/client/components/app/Notification/index.jsx b/client/components/app/Notification/index.jsx new file mode 100644 index 0000000..c9d759d --- /dev/null +++ b/client/components/app/Notification/index.jsx @@ -0,0 +1,88 @@ +import Link from 'next/link'; +import { useState } from 'react'; +import { Mail, MailOpen, SquareArrowOutUpRight } from 'lucide-react'; +import UserInfo from '@/components/app/User/Info'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import { updateNotification } from '@/lib/api/me'; + +function typeContent(notification) { + switch (notification.type) { + case 'user_followed': + return { + href: `/app/users/${notification.type_content}`, + message: 'seni takip etmeye başladı.' + }; + case 'post_liked': + return { + href: `/app/posts/${notification.type_content}`, + message: 'gönderini beğendi.' + }; + case 'comment_created': + return { + href: `/app/posts/${notification.type_content}`, + message: 'gönderine yorum yaptı.' + }; + case 'comment_liked': + return { + href: `/app/posts/${notification.type_content}`, + message: 'yorumunu beğendi.' + }; + } +} + +export default function Notification({ notification }) { + const [isRead, setIsRead] = useState(notification.read); + const { toast } = useToast(); + + const markAsRead = async ({ read }) => { + const response = await updateNotification({ + id: notification.id, + read: !read + }); + if (!response || response.status === 429) { + toast({ + title: 'hay aksi, bir şeyler ters gitti!', + description: + 'sunucudan yanıt alınamadı. lütfen daha sonra tekrar deneyin.', + duration: 3000 + }); + return; + } + + setIsRead(!read); + }; + + return ( +
+
+ +
+ + +
+
+
+ + {notification.source_user.display_name} ( + {notification.source_user.username}) + {' '} + {typeContent(notification).message} +
+
+ ); +} diff --git a/client/lib/api/me/index.js b/client/lib/api/me/index.js index 11c2167..3005965 100644 --- a/client/lib/api/me/index.js +++ b/client/lib/api/me/index.js @@ -25,4 +25,33 @@ export async function getFeed(limit, offset) { } catch (error) { return error.response; } -} \ No newline at end of file +} + +export async function getNotifications(limit, offset) { + try { + const response = await api.get('/me/notifications', { + params: { limit, offset } + }); + return response; + } catch (error) { + return error.response; + } +} + +export async function getUnreadNotificationsCount() { + try { + const response = await api.get('/me/notifications/unread'); + return response; + } catch (error) { + return error.response; + } +} + +export async function updateNotification({ id, read }) { + try { + const response = await api.patch(`/me/notifications/${id}`, { read }); + return response; + } catch (error) { + return error.response; + } +}