source: chapterx-frontend/src/components/layout/Navbar.tsx@ b62cefc

main
Last change on this file since b62cefc was b62cefc, checked in by kikisrbinoska <srbinoskakristina07@…>, 4 months ago

Added frontend and imporoved AI Suggestions service

  • Property mode set to 100644
File size: 10.1 KB
RevLine 
[b62cefc]1import React, { useState, useRef, useEffect } from 'react'
2import { Link, useNavigate, useLocation } from 'react-router-dom'
3import { Bell, Menu, X, ChevronDown, BookOpen, PenLine, Shield, List } from 'lucide-react'
4import logo from '../../assets/chapterX-removebg-preview.png'
5import { useAuthStore } from '../../store/authStore'
6import { useNotificationStore } from '../../store/notificationStore'
7import { Avatar } from '../ui/Avatar'
8import { RoleBadge } from '../ui/Badge'
9import { Button } from '../ui/Button'
10import { NotificationDropdown } from '../notifications/NotificationDropdown'
11
12export const Navbar: React.FC = () => {
13 const navigate = useNavigate()
14 const location = useLocation()
15 const { currentUser, logout } = useAuthStore()
16 const { notifications } = useNotificationStore()
17 const unread = notifications.filter(n => !n.is_read).length
18
19 const [mobileOpen, setMobileOpen] = useState(false)
20 const [userMenuOpen, setUserMenuOpen] = useState(false)
21 const [notifOpen, setNotifOpen] = useState(false)
22 const userMenuRef = useRef<HTMLDivElement>(null)
23 const notifRef = useRef<HTMLDivElement>(null)
24
25 useEffect(() => {
26 const handler = (e: MouseEvent) => {
27 if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) setUserMenuOpen(false)
28 if (notifRef.current && !notifRef.current.contains(e.target as Node)) setNotifOpen(false)
29 }
30 document.addEventListener('mousedown', handler)
31 return () => document.removeEventListener('mousedown', handler)
32 }, [])
33
34 const navLinks = [
35 { to: '/browse', label: 'Browse', icon: <BookOpen size={16} /> },
36 { to: '/genres', label: 'Genres', icon: null },
37 { to: '/community-lists', label: 'Reading Lists', icon: <List size={16} /> },
38 ]
39
40 const isActive = (path: string) => location.pathname.startsWith(path)
41
42 return (
43 <nav className="sticky top-0 z-40 glass border-b border-white/10">
44 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
45 <div className="flex items-center justify-between h-16">
46 {/* Logo */}
47 <Link to="/" className="flex items-center gap-2 flex-shrink-0 group">
48 <img src={logo} alt="ChapterX" className="h-14 w-14 object-contain group-hover:scale-110 transition-transform" />
49 </Link>
50
51 {/* Desktop nav */}
52 <div className="hidden md:flex items-center gap-1">
53 {navLinks.map(link => (
54 <Link
55 key={link.to}
56 to={link.to}
57 className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
58 isActive(link.to)
59 ? 'bg-indigo-500/20 text-indigo-300'
60 : 'text-slate-400 hover:text-white hover:bg-slate-800'
61 }`}
62 >
63 {link.icon}
64 {link.label}
65 </Link>
66 ))}
67 {currentUser?.role === 'writer' || currentUser?.role === 'admin' ? (
68 <Link
69 to="/writer"
70 className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
71 isActive('/writer')
72 ? 'bg-indigo-500/20 text-indigo-300'
73 : 'text-slate-400 hover:text-white hover:bg-slate-800'
74 }`}
75 >
76 <PenLine size={16} />
77 Write
78 </Link>
79 ) : null}
80 {currentUser?.role === 'admin' && (
81 <Link
82 to="/admin"
83 className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
84 isActive('/admin')
85 ? 'bg-rose-500/20 text-rose-300'
86 : 'text-slate-400 hover:text-white hover:bg-slate-800'
87 }`}
88 >
89 <Shield size={16} />
90 Admin
91 </Link>
92 )}
93 </div>
94
95 {/* Right side */}
96 <div className="flex items-center gap-2">
97 {currentUser ? (
98 <>
99 {/* Notification bell */}
100 <div className="relative" ref={notifRef}>
101 <button
102 onClick={() => { setNotifOpen(!notifOpen); setUserMenuOpen(false) }}
103 className="relative p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
104 >
105 <Bell size={18} />
106 {unread > 0 && (
107 <span className="absolute -top-0.5 -right-0.5 w-4 h-4 bg-rose-500 text-white text-xs rounded-full flex items-center justify-center font-medium">
108 {unread > 9 ? '9+' : unread}
109 </span>
110 )}
111 </button>
112 {notifOpen && <NotificationDropdown onClose={() => setNotifOpen(false)} />}
113 </div>
114
115 {/* User menu */}
116 <div className="relative" ref={userMenuRef}>
117 <button
118 onClick={() => { setUserMenuOpen(!userMenuOpen); setNotifOpen(false) }}
119 className="flex items-center gap-2 px-2 py-1.5 rounded-xl hover:bg-slate-800 transition-colors"
120 >
121 <Avatar name={currentUser.name} size="sm" />
122 <span className="hidden sm:block text-sm text-slate-300">{currentUser.username}</span>
123 <ChevronDown size={14} className="text-slate-500" />
124 </button>
125
126 {userMenuOpen && (
127 <div className="absolute right-0 top-full mt-2 w-52 bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl z-50 overflow-hidden py-1">
128 <div className="px-4 py-3 border-b border-slate-700">
129 <p className="text-sm font-medium text-white">{currentUser.name} {currentUser.surname}</p>
130 <div className="flex items-center gap-2 mt-1">
131 <p className="text-xs text-slate-500">@{currentUser.username}</p>
132 <RoleBadge role={currentUser.role} />
133 </div>
134 </div>
135 <div className="py-1">
136 <button onClick={() => { navigate(`/profile/${currentUser.username}`); setUserMenuOpen(false) }}
137 className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white transition-colors">
138 My Profile
139 </button>
140 <button onClick={() => { navigate('/reading-lists'); setUserMenuOpen(false) }}
141 className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white transition-colors">
142 My Reading Lists
143 </button>
144 {(currentUser.role === 'writer' || currentUser.role === 'admin') && (
145 <button onClick={() => { navigate('/writer'); setUserMenuOpen(false) }}
146 className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white transition-colors">
147 Writer Dashboard
148 </button>
149 )}
150 {currentUser.role === 'admin' && (
151 <button onClick={() => { navigate('/admin'); setUserMenuOpen(false) }}
152 className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white transition-colors">
153 Admin Panel
154 </button>
155 )}
156 </div>
157 <div className="border-t border-slate-700 py-1">
158 <button
159 onClick={() => { logout(); setUserMenuOpen(false); navigate('/') }}
160 className="w-full text-left px-4 py-2 text-sm text-rose-400 hover:bg-slate-800 transition-colors"
161 >
162 Sign Out
163 </button>
164 </div>
165 </div>
166 )}
167 </div>
168 </>
169 ) : (
170 <div className="hidden sm:flex items-center gap-2">
171 <Button variant="ghost" size="sm" onClick={() => navigate('/login')}>
172 Sign In
173 </Button>
174 <Button size="sm" onClick={() => navigate('/register')}>
175 Get Started
176 </Button>
177 </div>
178 )}
179
180 {/* Mobile hamburger */}
181 <button
182 className="md:hidden p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800"
183 onClick={() => setMobileOpen(!mobileOpen)}
184 >
185 {mobileOpen ? <X size={20} /> : <Menu size={20} />}
186 </button>
187 </div>
188 </div>
189
190 {/* Mobile menu */}
191 {mobileOpen && (
192 <div className="md:hidden pb-4 border-t border-slate-800 mt-2 pt-4 space-y-1">
193 {navLinks.map(link => (
194 <Link
195 key={link.to}
196 to={link.to}
197 onClick={() => setMobileOpen(false)}
198 className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
199 isActive(link.to)
200 ? 'bg-indigo-500/20 text-indigo-300'
201 : 'text-slate-400 hover:text-white hover:bg-slate-800'
202 }`}
203 >
204 {link.icon}
205 {link.label}
206 </Link>
207 ))}
208 {!currentUser && (
209 <div className="flex gap-2 pt-2">
210 <Button variant="ghost" size="sm" onClick={() => { navigate('/login'); setMobileOpen(false) }} className="flex-1">
211 Sign In
212 </Button>
213 <Button size="sm" onClick={() => { navigate('/register'); setMobileOpen(false) }} className="flex-1">
214 Get Started
215 </Button>
216 </div>
217 )}
218 </div>
219 )}
220 </div>
221 </nav>
222 )
223}
Note: See TracBrowser for help on using the repository browser.