Skip to content

Commit 1b9ab97

Browse files
committed
feat: add notifications localization and enhance NotificationBell component with translation support for improved user experience
1 parent 56c2b67 commit 1b9ab97

File tree

6 files changed

+79
-36
lines changed

6 files changed

+79
-36
lines changed

apps/jobboard-frontend/public/locales/ar/common.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1806,6 +1806,15 @@
18061806
"authRequired": "يجب تسجيل الدخول لاستخدام الدردشة",
18071807
"noConversationsDesc": "تحتاج إلى وجود إرشاد نشط لبدء الدردشة."
18081808
},
1809+
"notifications": {
1810+
"title": "الإشعارات",
1811+
"ariaLabel": "الإشعارات",
1812+
"loading": "جاري تحميل الإشعارات...",
1813+
"empty": "أنت مواكب لكل شيء. لا توجد إشعارات بعد.",
1814+
"unread": "{{count}} غير مقروء",
1815+
"upToDate": "محدَّث",
1816+
"footer": "أحدث الإشعارات تظهر أولاً."
1817+
},
18091818
"errors": {
18101819
"DEFAULT": "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
18111820
"VALIDATION_FAILED": "بعض الحقول تحتاج إلى انتباهك.",

apps/jobboard-frontend/public/locales/en/common.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,15 @@
18241824
"authRequired": "You must be logged in to use chat",
18251825
"noConversationsDesc": "You need to have an active mentorship to start chatting."
18261826
},
1827+
"notifications": {
1828+
"title": "Notifications",
1829+
"ariaLabel": "Notifications",
1830+
"loading": "Loading notifications...",
1831+
"empty": "You're all caught up. No notifications yet.",
1832+
"unread": "{{count}} unread",
1833+
"upToDate": "Up to date",
1834+
"footer": "Newest notifications appear first."
1835+
},
18271836
"errors": {
18281837
"DEFAULT": "Something went wrong. Please try again.",
18291838
"VALIDATION_FAILED": "Some fields need your attention.",

apps/jobboard-frontend/public/locales/tr/common.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,6 +1834,15 @@
18341834
"authRequired": "Sohbeti kullanmak için giriş yapmalısınız",
18351835
"noConversationsDesc": "Sohbete başlamak için aktif bir mentorluk lazım."
18361836
},
1837+
"notifications": {
1838+
"title": "Bildirimler",
1839+
"ariaLabel": "Bildirimler",
1840+
"loading": "Bildirimler yükleniyor...",
1841+
"empty": "Harika! Hiç bildirimin yok.",
1842+
"unread": "{{count}} okunmamış",
1843+
"upToDate": "Güncel",
1844+
"footer": "En yeni bildirimler önce görünür."
1845+
},
18371846
"errors": {
18381847
"DEFAULT": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
18391848
"VALIDATION_FAILED": "Bazı alanların düzeltilmesi gerekiyor.",

apps/jobboard-frontend/src/modules/mentorship/pages/MentorProfilePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ const MentorProfilePage = () => {
619619
className="w-full"
620620
asChild
621621
>
622-
<Link to="/mentor/requests">
622+
<Link to="/mentorship/mentor/requests">
623623
{t('mentorship.profile.viewAllRequests') || 'View All Requests'}
624624
</Link>
625625
</Button>
@@ -634,7 +634,7 @@ const MentorProfilePage = () => {
634634
className="w-full"
635635
asChild
636636
>
637-
<Link to="/mentor/requests">
637+
<Link to="/mentorship/mentor/requests">
638638
{t('mentorship.profile.viewAllRequests') || 'View All Requests'}
639639
</Link>
640640
</Button>

apps/jobboard-frontend/src/modules/notifications/components/NotificationBell.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { Bell } from 'lucide-react';
22
import {
33
DropdownMenu,
44
DropdownMenuContent,
5+
DropdownMenuItem,
56
DropdownMenuLabel,
67
DropdownMenuSeparator,
78
DropdownMenuTrigger,
89
} from '@shared/components/ui/dropdown-menu';
910
import { Button } from '@shared/components/ui/button';
1011
import { Separator } from '@shared/components/ui/separator';
12+
import { useTranslation } from 'react-i18next';
1113
import { useNotifications } from '../hooks/useNotifications';
1214
import type { NotificationItem } from '@shared/types/notification.types';
1315

@@ -19,7 +21,7 @@ const formatTimestamp = (timestamp: number) => {
1921
hour: '2-digit',
2022
minute: '2-digit',
2123
}).format(new Date(timestamp));
22-
} catch (err) {
24+
} catch (_err: unknown) {
2325
return '';
2426
}
2527
};
@@ -34,10 +36,9 @@ const NotificationListItem = ({
3436
const isUnread = !notification.read;
3537

3638
return (
37-
<button
38-
type="button"
39-
onClick={() => onClick(notification)}
40-
className={`flex w-full gap-3 px-4 py-3 text-left transition-colors hover:bg-muted ${
39+
<DropdownMenuItem
40+
onSelect={() => onClick(notification)}
41+
className={`items-start px-0 py-3 text-left transition-colors hover:bg-muted ${
4142
isUnread ? 'bg-muted/60' : ''
4243
}`}
4344
>
@@ -48,17 +49,18 @@ const NotificationListItem = ({
4849
<div className="flex-1 min-w-0">
4950
<div className="flex items-center justify-between gap-2">
5051
<span className="font-medium line-clamp-1">{notification.title}</span>
51-
<span className="text-xs text-muted-foreground whitespace-nowrap">
52+
<span className="text-xs text-muted-foreground whitespace-nowrap px-3">
5253
{formatTimestamp(Number(notification.timestamp))}
5354
</span>
5455
</div>
5556
<p className="text-sm text-muted-foreground line-clamp-2">{notification.message}</p>
5657
</div>
57-
</button>
58+
</DropdownMenuItem>
5859
);
5960
};
6061

6162
export const NotificationBell = () => {
63+
const { t } = useTranslation('common');
6264
const {
6365
notifications,
6466
hasUnread,
@@ -71,13 +73,17 @@ export const NotificationBell = () => {
7173

7274
const renderList = () => {
7375
if (isLoading) {
74-
return <div className="p-4 text-sm text-muted-foreground">Loading notifications...</div>;
76+
return (
77+
<div className="p-4 text-sm text-muted-foreground">
78+
{t('notifications.loading', 'Loading notifications...')}
79+
</div>
80+
);
7581
}
7682

7783
if (notifications.length === 0) {
7884
return (
79-
<div className="p-4 text-sm text-muted-foreground">
80-
You&apos;re all caught up. No notifications yet.
85+
<div className="p-6 text-sm text-muted-foreground">
86+
{t('notifications.empty', "You're all caught up. No notifications yet.")}
8187
</div>
8288
);
8389
}
@@ -102,7 +108,7 @@ export const NotificationBell = () => {
102108
variant="ghost"
103109
size="icon"
104110
className="relative"
105-
aria-label="Notifications"
111+
aria-label={t('notifications.ariaLabel', 'Notifications')}
106112
aria-haspopup="menu"
107113
>
108114
<Bell className="h-5 w-5" />
@@ -111,20 +117,27 @@ export const NotificationBell = () => {
111117
)}
112118
</Button>
113119
</DropdownMenuTrigger>
114-
<DropdownMenuContent align="end" className="w-80 p-0">
120+
<DropdownMenuContent className="w-100 p-0">
115121
<DropdownMenuLabel className="flex items-center justify-between px-3 py-2">
116-
<span className="font-semibold">Notifications</span>
122+
<span className="font-semibold">{t('notifications.title', 'Notifications')}</span>
117123
{hasUnread ? (
118-
<span className="text-xs text-destructive">{unreadCount} unread</span>
124+
<span className="text-xs text-destructive">
125+
{t('notifications.unread', {
126+
count: unreadCount,
127+
defaultValue: `${unreadCount} unread`,
128+
})}
129+
</span>
119130
) : (
120-
<span className="text-xs text-muted-foreground">Up to date</span>
131+
<span className="text-xs text-muted-foreground">
132+
{t('notifications.upToDate', 'Up to date')}
133+
</span>
121134
)}
122135
</DropdownMenuLabel>
123136
<DropdownMenuSeparator />
124137
{renderList()}
125138
<Separator />
126139
<div className="px-3 py-2 text-xs text-muted-foreground">
127-
Newest notifications appear first.
140+
{t('notifications.footer', 'Newest notifications appear first.')}
128141
</div>
129142
</DropdownMenuContent>
130143
</DropdownMenu>

apps/jobboard-frontend/src/modules/notifications/hooks/useNotifications.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,22 @@ const resolveNotificationLink = (notification: NotificationItem): string => {
1111
const type = notification.notificationType?.toUpperCase() || '';
1212
const linkId = notification.linkId;
1313

14-
if (type.includes('CHAT') || type === 'NEW_MESSAGE') {
15-
return linkId ? `/mentorship/chat/${linkId}` : '/mentorship/chat';
16-
}
17-
18-
if (type.includes('JOB_APPLICATION')) {
19-
return '/jobs/applications';
20-
}
21-
22-
if (type.includes('MENTORSHIP')) {
23-
return '/mentorship/my';
24-
}
25-
26-
if (type.includes('FORUM')) {
27-
return '/forum';
28-
}
14+
// links must be finalized TODO
15+
const NOTIFICATION_TYPE_TO_LINK = {
16+
'NEW_MESSAGE': '/mentorship/chat',
17+
'MENTORSHIP_REQUEST': '/mentorship/mentor/requests',
18+
'MENTORSHIP_APPROVED': '/mentorship/my',
19+
'MENTORSHIP_REJECTED': '/mentorship/my',
20+
'JOB_APPLICATION_CREATED': '/employer/jobs/{linkId}/applications',
21+
'JOB_APPLICATION_APPROVED': '/jobs/applications',
22+
'JOB_APPLICATION_REJECTED': '/jobs/applications',
23+
'FORUM_COMMENT': '/forum',
24+
'SYSTEM_BROADCAST': '/',
25+
'BROADCAST': '/',
26+
'GLOBAL': '/',
27+
};
2928

30-
return '/';
29+
return NOTIFICATION_TYPE_TO_LINK[type as keyof typeof NOTIFICATION_TYPE_TO_LINK]?.replace('{linkId}', linkId?.toString() ?? '');
3130
};
3231

3332
export const useNotifications = () => {
@@ -79,14 +78,18 @@ export const useNotifications = () => {
7978
[sortNotifications]
8079
);
8180

82-
const { isLoading: isLoadingInitial } = useQuery({
81+
const { data: initialNotifications, isLoading: isLoadingInitial } = useQuery({
8382
queryKey: notificationKeys.me,
8483
queryFn: fetchMyNotifications,
8584
enabled: isAuthenticated,
8685
staleTime: 30_000,
87-
onSuccess: (data) => mergeInitialNotifications(data),
8886
});
8987

88+
useEffect(() => {
89+
if (!initialNotifications || !isAuthenticated) return;
90+
mergeInitialNotifications(initialNotifications);
91+
}, [initialNotifications, isAuthenticated, mergeInitialNotifications]);
92+
9093
const markReadMutation = useMutation({
9194
mutationFn: (id: number) => markNotificationAsRead(id),
9295
onSuccess: (_data, id) => {

0 commit comments

Comments
 (0)