Skip to content

Commit

Permalink
feat(client): implement notification system
Browse files Browse the repository at this point in the history
  • Loading branch information
lareii committed Sep 19, 2024
1 parent ef91bd6 commit 7de0db7
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 8 deletions.
7 changes: 7 additions & 0 deletions client/app/app/notifications/layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const metadata = {
title: 'bildirimler'
};

export default function Layout({ children }) {
return children;
}
112 changes: 108 additions & 4 deletions client/app/app/notifications/page.jsx
Original file line number Diff line number Diff line change
@@ -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 <div className="text-center text-sm">bu sayfa henüz hazır değil {':('}</div>;
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 (
<div className='flex flex-col gap-2'>
{loading && (
<>
<NotificationSkeleton />
<NotificationSkeleton />
</>
)}
{!loading && notifications.length > 0 ? (
<>
{notifications.map((notification) => (
<Notification key={notification.id} notification={notification} />
))}
{hasMoreNotification && (
<Button onClick={loadMoreNotifications} className='w-full'>
daha fazla bildirim yükle
</Button>
)}
</>
) : (
!loading && (
<div className='flex flex-col items-center justify-center text-sm'>
buralar şimdilik sessiz.
</div>
)
)}
</div>
);
}
33 changes: 30 additions & 3 deletions client/components/app/Navbar/Item.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
variant={isActive ? 'secondary' : 'ghost'}
className='justify-start'
asChild
>
<Link href={href}>
<Icon className='mr-5 h-4 w-4' />
{label}
<Link href={href} className='flex gap-5'>
<Icon className=' h-4 w-4' />
{href === '/app/notifications' && unreadNotificationsCount ? (
<div className='flex justify-between items-center grow'>
{label}
<Badge>{unreadNotificationsCount}</Badge>
</div>
) : (
label
)}
</Link>
</Button>
);
Expand Down
16 changes: 16 additions & 0 deletions client/components/app/Notification/Skeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Skeleton } from '@/components/ui/skeleton';

export default function NotificationSkeleton() {
return (
<div className='p-5 flex flex-col'>
<div className='flex items-center'>
<Skeleton className='mr-3 w-10 h-10 rounded-lg bg-zinc-800'></Skeleton>
<div>
<Skeleton className='w-20 h-4 mb-1' />
<Skeleton className='w-10 h-4' />
</div>
</div>
<Skeleton className='mt-3 w-1/2 h-4'></Skeleton>
</div>
)
}
88 changes: 88 additions & 0 deletions client/components/app/Notification/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`p-5 rounded-lg ${isRead ? '' : 'bg-zinc-900'}`}>
<div className='flex justify-between items-center mb-3'>
<UserInfo user={notification.source_user} />
<div>
<Button
variant='ghost'
size='icon'
onClick={() => markAsRead({ read: isRead })}
>
{isRead ? (
<MailOpen className='w-4 h-4' />
) : (
<Mail className='w-4 h-4' />
)}
</Button>
<Button variant='ghost' size='icon' asChild>
<Link href={typeContent(notification).href}>
<SquareArrowOutUpRight className='w-4 h-4' />
</Link>
</Button>
</div>
</div>
<div className='text-sm text-muted-foreground'>
<span className='text-primary'>
{notification.source_user.display_name} (
{notification.source_user.username})
</span>{' '}
{typeContent(notification).message}
</div>
</div>
);
}
31 changes: 30 additions & 1 deletion client/lib/api/me/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,33 @@ export async function getFeed(limit, offset) {
} catch (error) {
return error.response;
}
}
}

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;
}
}

0 comments on commit 7de0db7

Please sign in to comment.