Skip to content
This repository was archived by the owner on Sep 19, 2025. It is now read-only.

Commit d2b1f51

Browse files
committed
added notifications on TopNavbar
1 parent bab46dc commit d2b1f51

7 files changed

Lines changed: 371 additions & 43 deletions

File tree

bun.lock

Lines changed: 48 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link rel="icon" type="image/svg+xml" href="/logo.png" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>CloudNotes</title>
8-
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
8+
<!-- <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script> -->
99
</head>
1010
<body>
1111
<div id="root"></div>

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
"@radix-ui/react-tabs": "^1.1.11",
3939
"@radix-ui/react-toggle": "^1.1.8",
4040
"@radix-ui/react-tooltip": "^1.2.6",
41-
"@tailwindcss/vite": "^4.1.6",
41+
"@tailwindcss/vite": "^4.1.7",
4242
"class-variance-authority": "^0.7.1",
4343
"clsx": "^2.1.1",
44-
"framer-motion": "^12.11.0",
44+
"framer-motion": "^12.11.3",
4545
"i18next": "^24.2.3",
4646
"i18next-http-backend": "^3.0.2",
4747
"input-otp": "^1.4.2",
@@ -64,20 +64,20 @@
6464
},
6565
"devDependencies": {
6666
"@eslint/js": "^9.26.0",
67-
"@faker-js/faker": "^9.7.0",
68-
"@types/node": "^22.15.17",
67+
"@faker-js/faker": "^9.8.0",
68+
"@types/node": "^22.15.18",
6969
"@types/react": "^19.1.4",
7070
"@types/react-dom": "^19.1.5",
7171
"@vitejs/plugin-react": "^4.4.1",
7272
"cross-env": "^7.0.3",
73-
"electron": "^34.5.5",
73+
"electron": "^34.5.6",
7474
"electron-builder": "^25.1.8",
7575
"eslint": "^9.26.0",
7676
"eslint-plugin-react-hooks": "^5.2.0",
7777
"eslint-plugin-react-refresh": "^0.4.20",
7878
"globals": "^15.15.0",
7979
"npm-run-all": "^4.1.5",
80-
"tailwindcss": "^4.1.6",
80+
"tailwindcss": "^4.1.7",
8181
"typescript": "~5.7.3",
8282
"typescript-eslint": "^8.32.1",
8383
"vite": "^6.3.5"

src/frontend/components/layout/LeftSidebar.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { AppLink } from "../ui/app-link";
1515
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
1616
import SettingsModal from '../modals/SettingsModal';
1717
import SessionExpiredModal from '../modals/SessionExpiredModal';
18+
import { Separator } from '../ui/separator';
1819

1920
interface NavItemProps {
2021
icon: React.ReactNode;
@@ -78,10 +79,8 @@ const LeftSidebar: React.FC = () => {
7879
{/* Top Navigation Items - Centered */}
7980
<div className="flex-grow flex flex-col items-center justify-center">
8081
<NavItem icon={<HomeIcon size={32} />} label="HOME" to="/home" active />
81-
<NavItem icon={<FileTextIcon size={32} />} label="READER" to="/reader" />
82+
<Separator className="w-full my-2 border-1" />
8283
<NavItem icon={<BookmarkIcon size={32} />} label="SAVED" to="/saved" />
83-
<NavItem icon={<BellIcon size={32} />} label="NOTICE" to="/notifications" />
84-
<NavItem icon={<MoreHorizontalIcon size={32} />} label="MORE" to="/more" />
8584
</div>
8685

8786
{/* Bottom Items - Settings and Profile */}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import type React from 'react';
2+
import { useState, useCallback } from 'react';
3+
import { BellIcon, SettingsIcon, CheckIcon, MailIcon, FileTextIcon, MessageSquareIcon } from 'lucide-react';
4+
import { Button } from "../ui/button";
5+
import { Badge } from "../ui/badge";
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuLabel,
11+
DropdownMenuSeparator,
12+
DropdownMenuTrigger
13+
} from "../ui/dropdown-menu";
14+
import { cn } from '@/lib/utils';
15+
16+
// Types
17+
export interface NotificationItem {
18+
id: string;
19+
title: string;
20+
message: string;
21+
timestamp: Date;
22+
isRead: boolean;
23+
type: 'document' | 'comment' | 'share' | 'system';
24+
actionUrl?: string;
25+
}
26+
27+
interface NotificationsProps {
28+
unreadCount?: number;
29+
notifications?: NotificationItem[];
30+
onOpenSettings?: () => void;
31+
onMarkAllAsRead?: () => void;
32+
onNotificationClick?: (notification: NotificationItem) => void;
33+
}
34+
35+
const Notifications: React.FC<NotificationsProps> = ({
36+
unreadCount = 0,
37+
notifications = [],
38+
onOpenSettings,
39+
onMarkAllAsRead,
40+
onNotificationClick
41+
}) => {
42+
const [isOpen, setIsOpen] = useState(false);
43+
44+
// Format the timestamp to a relative time string
45+
const formatTime = (date: Date): string => {
46+
const now = new Date();
47+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
48+
49+
if (diffInSeconds < 60) return 'Just now';
50+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
51+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
52+
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
53+
54+
return date.toLocaleDateString();
55+
};
56+
57+
// Get icon based on notification type
58+
const getNotificationIcon = (type: NotificationItem['type']): React.ReactNode => {
59+
switch (type) {
60+
case 'document':
61+
return <FileTextIcon size={14} className="text-blue-500" />;
62+
case 'comment':
63+
return <MessageSquareIcon size={14} className="text-green-500" />;
64+
case 'share':
65+
return <MailIcon size={14} className="text-purple-500" />;
66+
default:
67+
return <BellIcon size={14} className="text-gray-500" />;
68+
}
69+
};
70+
71+
// Handle notification click
72+
const handleNotificationClick = useCallback((notification: NotificationItem) => {
73+
setIsOpen(false);
74+
onNotificationClick?.(notification);
75+
}, [onNotificationClick]);
76+
77+
// Handle mark all as read
78+
const handleMarkAllAsRead = useCallback(() => {
79+
setIsOpen(false);
80+
onMarkAllAsRead?.();
81+
}, [onMarkAllAsRead]);
82+
83+
// Handle settings click
84+
const handleSettingsClick = useCallback(() => {
85+
setIsOpen(false);
86+
onOpenSettings?.();
87+
}, [onOpenSettings]);
88+
89+
return (
90+
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
91+
<DropdownMenuTrigger asChild>
92+
<Button
93+
variant="ghost"
94+
size="icon"
95+
className="p-1 h-8 w-8 rounded-full transition-all duration-200 hover:bg-primary/10 hover:text-primary relative"
96+
title="Notifications"
97+
>
98+
<BellIcon size={16} />
99+
{unreadCount > 0 && (
100+
<Badge
101+
variant="destructive"
102+
className="absolute -top-1 -right-1 flex items-center justify-center h-4 min-w-4 text-[10px] px-[5px] py-0 rounded-full"
103+
>
104+
{unreadCount > 99 ? '99+' : unreadCount}
105+
</Badge>
106+
)}
107+
</Button>
108+
</DropdownMenuTrigger>
109+
110+
<DropdownMenuContent align="end" className="w-80">
111+
<DropdownMenuLabel className="flex items-center justify-between">
112+
<span>Notifications</span>
113+
{unreadCount > 0 && (
114+
<Button
115+
variant="ghost"
116+
size="sm"
117+
className="h-7 text-xs flex items-center gap-1 hover:text-primary"
118+
onClick={handleMarkAllAsRead}
119+
>
120+
<CheckIcon size={12} />
121+
Mark all as read
122+
</Button>
123+
)}
124+
</DropdownMenuLabel>
125+
126+
<DropdownMenuSeparator />
127+
128+
{notifications.length === 0 ? (
129+
<div className="py-8 px-2 flex flex-col items-center justify-center text-center">
130+
<BellIcon size={32} className="text-muted-foreground mb-2 opacity-20" />
131+
<p className="text-sm text-muted-foreground">No notifications yet</p>
132+
<p className="text-xs text-muted-foreground/70">We'll notify you when something important happens</p>
133+
</div>
134+
) : (
135+
<div className="max-h-[350px] overflow-y-auto py-1">
136+
{notifications.map((notification) => (
137+
<DropdownMenuItem
138+
key={notification.id}
139+
className={cn(
140+
"flex flex-col items-start p-3 cursor-pointer relative transition-all duration-200",
141+
notification.isRead ? "opacity-80" : "bg-primary/5"
142+
)}
143+
onClick={() => handleNotificationClick(notification)}
144+
>
145+
<div className="flex w-full justify-between items-start">
146+
<div className="flex items-center gap-2">
147+
<div className={cn(
148+
"p-1 rounded-full",
149+
notification.isRead ? "bg-muted/70" : "bg-muted"
150+
)}>
151+
{getNotificationIcon(notification.type)}
152+
</div>
153+
<span className={cn(
154+
"text-sm",
155+
notification.isRead ? "font-normal" : "font-medium"
156+
)}>
157+
{notification.title}
158+
</span>
159+
</div>
160+
<span className={cn(
161+
"text-xs text-muted-foreground ml-2 transition-all duration-200",
162+
notification.isRead ? "mr-0" : "mr-5"
163+
)}>
164+
{formatTime(notification.timestamp)}
165+
</span>
166+
</div>
167+
<p className="text-xs text-muted-foreground pl-7 pt-1">
168+
{notification.message}
169+
</p>
170+
{!notification.isRead && (
171+
<div className="w-2 h-2 rounded-full bg-primary absolute top-3.5 right-2.5" />
172+
)}
173+
</DropdownMenuItem>
174+
))}
175+
</div>
176+
)}
177+
178+
<DropdownMenuSeparator />
179+
180+
<DropdownMenuItem
181+
className="p-2 cursor-pointer"
182+
onClick={handleSettingsClick}
183+
>
184+
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground w-full justify-center">
185+
<SettingsIcon size={14} />
186+
<span>Notification Settings</span>
187+
</div>
188+
</DropdownMenuItem>
189+
</DropdownMenuContent>
190+
</DropdownMenu>
191+
);
192+
};
193+
194+
export default Notifications;

src/frontend/components/layout/TopNavbar.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { Button } from "../ui/button";
1414
import { Input } from "../ui/input";
1515
import { Badge } from "../ui/badge";
1616
import SearchModal from '../modals/SearchModal';
17+
import SettingsModal from '../modals/SettingsModal';
18+
import Notifications, { type NotificationItem } from './Notifications';
1719
import { cn } from '@/lib/utils';
1820
import { goBack, goForward, reloadPage, getElectronAPI } from '@/lib/navigation';
1921

@@ -61,6 +63,36 @@ const TopNavbar: React.FC = () => {
6163
const [canGoForward, setCanGoForward] = useState(false);
6264
const [isMaximized, setIsMaximized] = useState(true); // Default to true as we maximize on startup
6365
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
66+
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
67+
const [settingsActiveTab, setSettingsActiveTab] = useState<string>("account");
68+
69+
// Mock notifications data (in a real app, this would come from a database or API)
70+
const [notifications, setNotifications] = useState<NotificationItem[]>([
71+
{
72+
id: '1',
73+
title: 'Document shared',
74+
message: 'Jane Doe shared "Project Budget.pdf" with you',
75+
timestamp: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
76+
isRead: false,
77+
type: 'share'
78+
},
79+
{
80+
id: '2',
81+
title: 'New comment',
82+
message: 'Alex commented on "Meeting Notes.pdf": "Great summary, thanks!"',
83+
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
84+
isRead: false,
85+
type: 'comment'
86+
},
87+
{
88+
id: '3',
89+
title: 'Document updated',
90+
message: 'Marketing Plan.docx has been updated with new revisions',
91+
timestamp: new Date(Date.now() - 8 * 60 * 60 * 1000), // 8 hours ago
92+
isRead: true,
93+
type: 'document'
94+
}
95+
]);
6496

6597
// CSS style objects
6698
const dragRegion: ElectronCSSProperties = { WebkitAppRegion: 'drag' };
@@ -72,7 +104,7 @@ const TopNavbar: React.FC = () => {
72104
if (api?.requestNavigationStateUpdate) {
73105
api.requestNavigationStateUpdate();
74106
}
75-
}, [window.location.pathname]);
107+
}, []);
76108

77109
// Set up event listeners
78110
useEffect(() => {
@@ -176,6 +208,41 @@ const TopNavbar: React.FC = () => {
176208
const openSearchModal = () => setIsSearchModalOpen(true);
177209
const closeSearchModal = () => setIsSearchModalOpen(false);
178210

211+
// Settings modal handlers
212+
const openSettingsModal = (tab = "account") => {
213+
setSettingsActiveTab(tab);
214+
setIsSettingsModalOpen(true);
215+
};
216+
const closeSettingsModal = () => setIsSettingsModalOpen(false);
217+
218+
// Notifications handlers
219+
const getUnreadNotificationsCount = () => {
220+
return notifications.filter(notification => !notification.isRead).length;
221+
};
222+
223+
const handleOpenNotificationSettings = () => {
224+
openSettingsModal("notifications");
225+
};
226+
227+
const handleMarkAllAsRead = () => {
228+
setNotifications(notifications.map(notification => ({
229+
...notification,
230+
isRead: true
231+
})));
232+
};
233+
234+
const handleNotificationClick = (notification: NotificationItem) => {
235+
// Mark the notification as read
236+
setNotifications(notifications.map(n =>
237+
n.id === notification.id
238+
? { ...n, isRead: true }
239+
: n
240+
));
241+
242+
// Handle specific action based on notification type (could navigate to a page, etc.)
243+
console.log('Notification clicked:', notification);
244+
};
245+
179246
return (
180247
<>
181248
<header className="flex h-12 bg-sidebar text-sidebar-foreground items-center justify-between select-none" style={dragRegion}>
@@ -228,6 +295,17 @@ const TopNavbar: React.FC = () => {
228295
<Badge variant="secondary" className="text-[10px] bg-muted border-0 shadow-none">CTRL + F</Badge>
229296
</div>
230297
</div>
298+
299+
{/* Notifications */}
300+
<div className="ml-2" style={noDragRegion}>
301+
<Notifications
302+
unreadCount={getUnreadNotificationsCount()}
303+
notifications={notifications}
304+
onOpenSettings={handleOpenNotificationSettings}
305+
onMarkAllAsRead={handleMarkAllAsRead}
306+
onNotificationClick={handleNotificationClick}
307+
/>
308+
</div>
231309
</div>
232310

233311
{/* Right section - Window controls */}
@@ -269,6 +347,13 @@ const TopNavbar: React.FC = () => {
269347
isOpen={isSearchModalOpen}
270348
onClose={closeSearchModal}
271349
/>
350+
351+
{/* Settings Modal */}
352+
<SettingsModal
353+
isOpen={isSettingsModalOpen}
354+
onClose={closeSettingsModal}
355+
activeTab={settingsActiveTab}
356+
/>
272357
</>
273358
);
274359
};

0 commit comments

Comments
 (0)