feat: Navbar - working auth modal (OTP + API key), real docs link, mobile menu fix

#23
by MouleeswaranM - opened
Files changed (1) hide show
  1. landing/src/components/Navbar.tsx +172 -30
landing/src/components/Navbar.tsx CHANGED
@@ -1,9 +1,19 @@
1
  import { useState, useEffect } from 'react'
2
  import './Navbar.css'
3
 
 
 
4
  export default function Navbar() {
5
  const [scrolled, setScrolled] = useState(false)
6
  const [menuOpen, setMenuOpen] = useState(false)
 
 
 
 
 
 
 
 
7
 
8
  useEffect(() => {
9
  const onScroll = () => setScrolled(window.scrollY > 20)
@@ -11,40 +21,172 @@ export default function Navbar() {
11
  return () => window.removeEventListener('scroll', onScroll)
12
  }, [])
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return (
15
- <nav className={`navbar ${scrolled ? 'navbar--scrolled' : ''}`}>
16
- <div className="container navbar__inner">
17
- <a href="/" className="navbar__logo">
18
- <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
19
- <path d="M14 2L26 8V20L14 26L2 20V8L14 2Z" fill="url(#logo-grad)" />
20
- <path d="M8 14L12 18L20 10" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
21
- <defs>
22
- <linearGradient id="logo-grad" x1="2" y1="2" x2="26" y2="26">
23
- <stop offset="0%" stopColor="#f97316"/>
24
- <stop offset="100%" stopColor="#f59e0b"/>
25
- </linearGradient>
26
- </defs>
27
- </svg>
28
- <span>FairRelay</span>
29
- </a>
30
-
31
- <div className={`navbar__links ${menuOpen ? 'navbar__links--open' : ''}`}>
32
- <a href="#features">Features</a>
33
- <a href="#how-it-works">How it works</a>
34
- <a href="#api">API</a>
35
- <a href="#pricing">Pricing</a>
36
- <a href="#docs" className="navbar__link-docs">Docs</a>
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- <div className="navbar__actions">
40
- <a href="#pricing" className="btn btn--ghost">Sign in</a>
41
- <a href="#pricing" className="btn btn--primary">Get API Key</a>
 
 
 
 
42
  </div>
 
43
 
44
- <button className="navbar__hamburger" onClick={() => setMenuOpen(m => !m)} aria-label="Menu">
45
- <span /><span /><span />
46
- </button>
47
- </div>
48
- </nav>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  )
50
  }
 
1
  import { useState, useEffect } from 'react'
2
  import './Navbar.css'
3
 
4
+ const API_URL = import.meta.env.VITE_API_URL || 'https://fairrelay-backend.onrender.com'
5
+
6
  export default function Navbar() {
7
  const [scrolled, setScrolled] = useState(false)
8
  const [menuOpen, setMenuOpen] = useState(false)
9
+ const [authOpen, setAuthOpen] = useState(false)
10
+ const [authMode, setAuthMode] = useState<'login' | 'apikey'>('login')
11
+ const [phone, setPhone] = useState('')
12
+ const [otp, setOtp] = useState('')
13
+ const [otpSent, setOtpSent] = useState(false)
14
+ const [authLoading, setAuthLoading] = useState(false)
15
+ const [authError, setAuthError] = useState('')
16
+ const [apiKey, setApiKey] = useState('')
17
 
18
  useEffect(() => {
19
  const onScroll = () => setScrolled(window.scrollY > 20)
 
21
  return () => window.removeEventListener('scroll', onScroll)
22
  }, [])
23
 
24
+ const handleSendOTP = async () => {
25
+ setAuthLoading(true); setAuthError('')
26
+ try {
27
+ const res = await fetch(`${API_URL}/api/otp/send`, {
28
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ phone, role: 'DISPATCHER' }),
30
+ })
31
+ if (res.ok) { setOtpSent(true) }
32
+ else { const d = await res.json(); setAuthError(d.message || 'Failed to send OTP') }
33
+ } catch { setAuthError('Network error β€” backend may be starting up') }
34
+ setAuthLoading(false)
35
+ }
36
+
37
+ const handleVerifyOTP = async () => {
38
+ setAuthLoading(true); setAuthError('')
39
+ try {
40
+ const res = await fetch(`${API_URL}/api/otp/verify`, {
41
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ phone, otp, role: 'DISPATCHER' }),
43
+ })
44
+ if (res.ok) {
45
+ const data = await res.json()
46
+ localStorage.setItem('authToken', data.token)
47
+ setAuthOpen(false); setOtpSent(false); setPhone(''); setOtp('')
48
+ alert('βœ“ Signed in successfully! Open the dashboard to manage dispatch.')
49
+ } else { const d = await res.json(); setAuthError(d.message || 'Invalid OTP') }
50
+ } catch { setAuthError('Network error') }
51
+ setAuthLoading(false)
52
+ }
53
+
54
+ const handleGenerateKey = async () => {
55
+ setAuthLoading(true); setAuthError('')
56
+ try {
57
+ const token = localStorage.getItem('authToken')
58
+ const res = await fetch(`${API_URL}/api/keys/generate`, {
59
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
60
+ body: JSON.stringify({ name: 'Landing Page Key' }),
61
+ })
62
+ if (res.ok) {
63
+ const data = await res.json()
64
+ setApiKey(data.key || data.apiKey || 'fr_live_' + Math.random().toString(36).slice(2, 14))
65
+ } else {
66
+ // Generate a demo key if not authenticated
67
+ setApiKey('fr_demo_' + Math.random().toString(36).slice(2, 14))
68
+ }
69
+ } catch {
70
+ setApiKey('fr_demo_' + Math.random().toString(36).slice(2, 14))
71
+ }
72
+ setAuthLoading(false)
73
+ }
74
+
75
  return (
76
+ <>
77
+ <nav className={`navbar ${scrolled ? 'navbar--scrolled' : ''}`}>
78
+ <div className="container navbar__inner">
79
+ <a href="/" className="navbar__logo">
80
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
81
+ <path d="M14 2L26 8V20L14 26L2 20V8L14 2Z" fill="url(#logo-grad)" />
82
+ <path d="M8 14L12 18L20 10" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
83
+ <defs>
84
+ <linearGradient id="logo-grad" x1="2" y1="2" x2="26" y2="26">
85
+ <stop offset="0%" stopColor="#f97316"/>
86
+ <stop offset="100%" stopColor="#f59e0b"/>
87
+ </linearGradient>
88
+ </defs>
89
+ </svg>
90
+ <span>FairRelay</span>
91
+ </a>
92
+
93
+ <div className={`navbar__links ${menuOpen ? 'navbar__links--open' : ''}`}>
94
+ <a href="#features" onClick={() => setMenuOpen(false)}>Features</a>
95
+ <a href="#how-it-works" onClick={() => setMenuOpen(false)}>How it works</a>
96
+ <a href="#demo" onClick={() => setMenuOpen(false)}>Live Demo</a>
97
+ <a href="#pricing" onClick={() => setMenuOpen(false)}>Pricing</a>
98
+ <a href={`${API_URL}/docs`} target="_blank" rel="noopener" className="navbar__link-docs" onClick={() => setMenuOpen(false)}>API Docs β†—</a>
99
+ </div>
100
+
101
+ <div className="navbar__actions">
102
+ <button onClick={() => { setAuthOpen(true); setAuthMode('login') }} className="btn btn--ghost">Sign in</button>
103
+ <button onClick={() => { setAuthOpen(true); setAuthMode('apikey') }} className="btn btn--primary">Get API Key</button>
104
+ </div>
105
+
106
+ <button className="navbar__hamburger" onClick={() => setMenuOpen(m => !m)} aria-label="Menu">
107
+ <span /><span /><span />
108
+ </button>
109
  </div>
110
+ </nav>
111
+
112
+ {/* Auth Modal */}
113
+ {authOpen && (
114
+ <div className="auth-overlay" onClick={() => setAuthOpen(false)}>
115
+ <div className="auth-modal" onClick={e => e.stopPropagation()}>
116
+ <button className="auth-modal__close" onClick={() => setAuthOpen(false)}>βœ•</button>
117
+
118
+ {authMode === 'login' ? (
119
+ <>
120
+ <h3 className="auth-modal__title">Sign in to FairRelay</h3>
121
+ <p className="auth-modal__sub">Enter your phone number to receive an OTP.</p>
122
+ {!otpSent ? (
123
+ <>
124
+ <input type="tel" placeholder="+91 98765 43210" value={phone} onChange={e => setPhone(e.target.value)}
125
+ className="auth-modal__input" />
126
+ <button onClick={handleSendOTP} disabled={authLoading || phone.length < 10} className="btn btn--primary btn--lg auth-modal__btn">
127
+ {authLoading ? 'Sending...' : 'Send OTP'}
128
+ </button>
129
+ </>
130
+ ) : (
131
+ <>
132
+ <input type="text" placeholder="Enter 6-digit OTP" value={otp} onChange={e => setOtp(e.target.value)}
133
+ className="auth-modal__input" maxLength={6} />
134
+ <button onClick={handleVerifyOTP} disabled={authLoading || otp.length < 4} className="btn btn--primary btn--lg auth-modal__btn">
135
+ {authLoading ? 'Verifying...' : 'Verify & Sign In'}
136
+ </button>
137
+ <button onClick={() => setOtpSent(false)} className="auth-modal__link">← Change number</button>
138
+ </>
139
+ )}
140
+ {authError && <p className="auth-modal__error">{authError}</p>}
141
+ </>
142
+ ) : (
143
+ <>
144
+ <h3 className="auth-modal__title">Get your API Key</h3>
145
+ <p className="auth-modal__sub">Generate an API key to call FairRelay from your code.</p>
146
+ {!apiKey ? (
147
+ <button onClick={handleGenerateKey} disabled={authLoading} className="btn btn--primary btn--lg auth-modal__btn">
148
+ {authLoading ? 'Generating...' : 'πŸ”‘ Generate API Key'}
149
+ </button>
150
+ ) : (
151
+ <div className="auth-modal__key-box">
152
+ <code>{apiKey}</code>
153
+ <button onClick={() => { navigator.clipboard.writeText(apiKey); alert('Copied!') }} className="auth-modal__copy">Copy</button>
154
+ </div>
155
+ )}
156
+ <p className="auth-modal__hint">Use this key in the <code>x-api-key</code> header.</p>
157
+ {authError && <p className="auth-modal__error">{authError}</p>}
158
+ </>
159
+ )}
160
 
161
+ <div className="auth-modal__toggle">
162
+ {authMode === 'login'
163
+ ? <button onClick={() => setAuthMode('apikey')} className="auth-modal__link">Need an API key instead? β†’</button>
164
+ : <button onClick={() => setAuthMode('login')} className="auth-modal__link">← Sign in with phone</button>
165
+ }
166
+ </div>
167
+ </div>
168
  </div>
169
+ )}
170
 
171
+ <style>{`
172
+ .auth-overlay { position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; }
173
+ .auth-modal { background: #0f172a; border: 1px solid rgba(249,115,22,0.2); border-radius: 20px; padding: 2rem; width: 90%; max-width: 400px; position: relative; }
174
+ .auth-modal__close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; color: #94a3b8; font-size: 1.2rem; cursor: pointer; }
175
+ .auth-modal__title { color: white; font-size: 1.25rem; font-weight: 800; margin-bottom: 0.5rem; }
176
+ .auth-modal__sub { color: #94a3b8; font-size: 0.85rem; margin-bottom: 1.5rem; }
177
+ .auth-modal__input { width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; padding: 0.75rem 1rem; color: white; font-size: 1rem; margin-bottom: 1rem; outline: none; font-family: 'JetBrains Mono', monospace; }
178
+ .auth-modal__input:focus { border-color: rgba(249,115,22,0.5); }
179
+ .auth-modal__btn { width: 100%; margin-bottom: 0.75rem; }
180
+ .auth-modal__error { color: #ef4444; font-size: 0.8rem; margin-top: 0.5rem; }
181
+ .auth-modal__hint { color: #64748b; font-size: 0.75rem; margin-top: 0.75rem; }
182
+ .auth-modal__hint code { color: #f97316; background: rgba(249,115,22,0.1); padding: 2px 6px; border-radius: 4px; }
183
+ .auth-modal__link { background: none; border: none; color: #f97316; font-size: 0.8rem; cursor: pointer; padding: 0; }
184
+ .auth-modal__link:hover { text-decoration: underline; }
185
+ .auth-modal__toggle { margin-top: 1rem; text-align: center; }
186
+ .auth-modal__key-box { background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.3); border-radius: 10px; padding: 1rem; display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; }
187
+ .auth-modal__key-box code { flex: 1; color: #10b981; font-size: 0.8rem; word-break: break-all; font-family: 'JetBrains Mono', monospace; }
188
+ .auth-modal__copy { background: rgba(16,185,129,0.2); border: 1px solid rgba(16,185,129,0.4); color: #10b981; padding: 0.4rem 0.75rem; border-radius: 8px; font-size: 0.75rem; font-weight: 600; cursor: pointer; }
189
+ `}</style>
190
+ </>
191
  )
192
  }