This repository has no description
1import React, { useContext, useState, useRef, useEffect } from 'react';
2import { Link, useNavigate } from 'react-router-dom';
3import { ThemeContext } from '../../contexts/ThemeContext';
4import './Navbar.css';
5
6// Dropdown Menu Component
7const DropdownMenu = ({ title, path, items }) => {
8 const [isOpen, setIsOpen] = useState(false);
9 const dropdownRef = useRef(null);
10
11 // Handle click outside to close the dropdown
12 useEffect(() => {
13 const handleClickOutside = (event) => {
14 if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
15 setIsOpen(false);
16 }
17 };
18
19 // Add event listener only when dropdown is open
20 if (isOpen) {
21 document.addEventListener('mousedown', handleClickOutside);
22 }
23
24 return () => {
25 document.removeEventListener('mousedown', handleClickOutside);
26 };
27 }, [isOpen]);
28
29 // Add event listener to close dropdown on scroll
30 useEffect(() => {
31 const handleScroll = () => {
32 if (isOpen) {
33 setIsOpen(false);
34 }
35 };
36
37 window.addEventListener('scroll', handleScroll);
38 return () => {
39 window.removeEventListener('scroll', handleScroll);
40 };
41 }, [isOpen]);
42
43 // Detect if we're on mobile based on window width
44 const [windowWidth, setWindowWidth] = useState(window.innerWidth);
45 const isMobile = windowWidth <= 940;
46
47 // Update window width on resize
48 useEffect(() => {
49 const handleResize = () => {
50 setWindowWidth(window.innerWidth);
51 // Close dropdown on resize to prevent positioning issues
52 if (isOpen) {
53 setIsOpen(false);
54 }
55 };
56
57 window.addEventListener('resize', handleResize);
58 return () => {
59 window.removeEventListener('resize', handleResize);
60 };
61 }, [isOpen]);
62
63 return (
64 <li
65 className={`dropdown-container ${isOpen ? 'active' : ''}`}
66 ref={dropdownRef}
67 onMouseEnter={() => !isMobile && setIsOpen(true)}
68 onMouseLeave={() => !isMobile && setIsOpen(false)}
69 >
70 <Link
71 to={path}
72 className="dropdown-trigger"
73 onClick={(e) => {
74 if (isMobile) {
75 e.preventDefault();
76 setIsOpen(!isOpen);
77 }
78 }}
79 >
80 {title}
81 </Link>
82 {isOpen && (
83 <ul className="dropdown-menu">
84 {items.map((item, index) => (
85 <li key={index}>
86 <Link
87 to={item.path}
88 onClick={() => setIsOpen(false)}
89 >
90 {item.title}
91 </Link>
92 </li>
93 ))}
94 </ul>
95 )}
96 </li>
97 );
98};
99
100// Regular menu item component to ensure consistent styling
101const MenuItem = ({ title, path }) => {
102 return (
103 <li>
104 <Link to={path}>{title}</Link>
105 </li>
106 );
107};
108
109const Navbar = () => {
110 const { isDarkMode, toggleDarkMode } = useContext(ThemeContext);
111 const navigate = useNavigate();
112
113 // Define dropdown menus structure
114 const scoreDropdown = {
115 title: "score",
116 path: "/",
117 items: [
118 { title: "score", path: "/" },
119 { title: "compare", path: "/compare" },
120 { title: "leaderboard", path: "/leaderboard" },
121 { title: "alt text rating", path: "/alt-text" },
122 { title: "shortcut", path: "/shortcut" }
123 ]
124 };
125
126 const aboutDropdown = {
127 title: "about",
128 path: "/about",
129 items: [
130 { title: "about", path: "/about" },
131 { title: "methodology", path: "/methodology" },
132 { title: "definitions", path: "/definitions" }
133 ]
134 };
135
136 return (
137 <header className="navbar">
138 <div className="navbar-container">
139 <div className="navbar-left">
140 <div className="navbar-logo">
141 <Link to="/">
142 <span className="cred">cred</span>
143 <span className="period">.</span>
144 <span className="blue">blue</span>
145 </Link>
146 <div className="beta-badge">
147 beta
148 </div>
149 </div>
150 <nav className="navbar-links">
151 <ul>
152 <DropdownMenu {...scoreDropdown} />
153 <DropdownMenu {...aboutDropdown} />
154 <MenuItem title="resources" path="/resources" />
155 </ul>
156 </nav>
157 </div>
158 <div className="navbar-actions">
159 {/* Social Links */}
160 <a
161 href="https://discord.gg/95ypHb2qPE"
162 target="_blank"
163 rel="noopener noreferrer"
164 className="nav-icon-discord"
165 aria-label="Discord Profile"
166 >
167 <svg className="icon" fill="currentColor">
168 <use href="/icons/icons-sprite.svg#icon-discord" />
169 </svg>
170 </a>
171 <a
172 href="https://bsky.app/profile/cred.blue"
173 target="_blank"
174 rel="noopener noreferrer"
175 className="nav-icon"
176 aria-label="Bluesky Profile"
177 >
178 <svg className="icon" fill="currentColor">
179 <use href="/icons/icons-sprite.svg#icon-bluesky" />
180 </svg>
181 </a>
182 <a
183 href="https://github.com/dame-is/cred.blue"
184 target="_blank"
185 rel="noopener noreferrer"
186 className="nav-icon"
187 aria-label="GitHub Profile"
188 >
189 <svg className="icon" fill="currentColor">
190 <use href="/icons/icons-sprite.svg#icon-github" />
191 </svg>
192 </a>
193 {/* Theme Toggle */}
194 <button
195 className="theme-toggle-button"
196 onClick={toggleDarkMode}
197 aria-label="Toggle dark mode"
198 >
199 <svg className="icon" fill="currentColor">
200 <use href={`/icons/icons-sprite.svg#icon-${isDarkMode ? 'sun' : 'moon'}`} />
201 </svg>
202 </button>
203 <div className="navbar-support-button-container">
204 <button
205 className="navbar-support-button"
206 type="button"
207 onClick={() => navigate(`/supporter`)}
208 >
209 become a supporter
210 </button>
211 </div>
212 </div>
213 </div>
214 </header>
215 );
216};
217
218export default Navbar;