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