534 lines
28 KiB
JavaScript
534 lines
28 KiB
JavaScript
import { useState, useEffect, useRef } from 'react'
|
|
import { Link, useNavigate } from 'react-router-dom'
|
|
import api from '../api/client'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import Navbar from '../components/Navbar'
|
|
|
|
// ── Turnstile widget ──────────────────────────────────────────────────────────
|
|
const TURNSTILE_SITE_KEY = window.__APP_CONFIG__?.TURNSTILE_SITE_KEY || ''
|
|
|
|
function TurnstileWidget({ onVerify }) {
|
|
const ref = useRef(null)
|
|
const widgetId = useRef(null)
|
|
useEffect(() => {
|
|
if (!TURNSTILE_SITE_KEY) return
|
|
let timer
|
|
const tryRender = () => {
|
|
if (!window.turnstile || !ref.current) { timer = setTimeout(tryRender, 200); return }
|
|
widgetId.current = window.turnstile.render(ref.current, {
|
|
sitekey: TURNSTILE_SITE_KEY,
|
|
callback: onVerify,
|
|
})
|
|
}
|
|
tryRender()
|
|
return () => { clearTimeout(timer); if (widgetId.current != null) try { window.turnstile.remove(widgetId.current) } catch {} }
|
|
}, [])
|
|
if (!TURNSTILE_SITE_KEY) return null
|
|
return <div ref={ref} style={{ margin: '8px 0' }} />
|
|
}
|
|
|
|
// ── Feature cards data ────────────────────────────────────────────────────────
|
|
const FEATURES = [
|
|
{
|
|
icon: '📄',
|
|
title: 'Quiz from Any PDF',
|
|
desc: 'Upload PREP materials, textbook chapters, or lecture slides. AI extracts questions with answers and explanations — no formatting required.',
|
|
},
|
|
{
|
|
icon: '🎓',
|
|
title: 'AI Tutor Built In',
|
|
desc: 'Ask anything mid-study. The AI tutor knows the current question, the correct answer, and pulls in related questions from your bank for context.',
|
|
},
|
|
{
|
|
icon: '🔊',
|
|
title: 'Audio Mode',
|
|
desc: 'Every question read aloud with natural-sounding voices. Study during rounds, commuting, or on a run — no screen needed.',
|
|
},
|
|
{
|
|
icon: '🏦',
|
|
title: 'Question Bank',
|
|
desc: 'All questions in one searchable place. Filter by category, topic, or search semantically — "hyponatremia in neonates" just works.',
|
|
},
|
|
{
|
|
icon: '📊',
|
|
title: 'Track Your Progress',
|
|
desc: 'Every attempt saved. Review past scores, see where you struggled, and focus your study time where it counts.',
|
|
},
|
|
{
|
|
icon: '⚡',
|
|
title: 'Fast by Design',
|
|
desc: 'Study mode loads instantly. The AI works in the background — it never holds up the quiz. No spinners on the critical path.',
|
|
},
|
|
]
|
|
|
|
// ── Contact form ──────────────────────────────────────────────────────────────
|
|
function ContactForm() {
|
|
const [form, setForm] = useState({ name: '', email: '', type: 'question', message: '' })
|
|
const [turnstileToken, setTurnstileToken] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [sent, setSent] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const hasTurnstile = !!TURNSTILE_SITE_KEY
|
|
const canSubmit = form.name && form.email && form.message && (!hasTurnstile || turnstileToken)
|
|
|
|
const submit = async (e) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
try {
|
|
await api.post('/contact', { ...form, turnstile_token: turnstileToken || null })
|
|
setSent(true)
|
|
} catch (err) {
|
|
setError(err.response?.data?.detail || 'Something went wrong. Please try again.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (sent) return (
|
|
<div style={{ textAlign: 'center', padding: '32px 0' }}>
|
|
<div style={{ fontSize: '2.5rem', marginBottom: 12 }}>✓</div>
|
|
<h3 style={{ fontSize: '1.2rem', marginBottom: 8 }}>Message received</h3>
|
|
<p style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>
|
|
{form.type === 'moderator'
|
|
? "We'll review your application and get back to you."
|
|
: "We'll get back to you shortly."}
|
|
</p>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 200px), 1fr))', gap: 12 }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: 5, color: 'var(--text-muted)' }}>Name</label>
|
|
<input
|
|
value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
|
placeholder="Your name" required maxLength={120}
|
|
style={{ width: '100%', padding: '9px 12px', borderRadius: 8, border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text)', fontSize: '0.9rem', boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: 5, color: 'var(--text-muted)' }}>Email</label>
|
|
<input
|
|
type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))}
|
|
placeholder="you@example.com" required
|
|
style={{ width: '100%', padding: '9px 12px', borderRadius: 8, border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text)', fontSize: '0.9rem', boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: 5, color: 'var(--text-muted)' }}>Type</label>
|
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
{[['question', '💬 Question'], ['moderator', '🛡 Apply as Moderator']].map(([val, label]) => (
|
|
<button
|
|
key={val} type="button"
|
|
onClick={() => setForm(f => ({ ...f, type: val }))}
|
|
style={{
|
|
flex: 1, padding: '8px 12px', borderRadius: 8, fontSize: '0.85rem', cursor: 'pointer',
|
|
fontWeight: form.type === val ? 700 : 400,
|
|
background: form.type === val ? 'var(--primary)' : 'var(--bg)',
|
|
color: form.type === val ? 'var(--primary-fg)' : 'var(--text)',
|
|
border: `1px solid ${form.type === val ? 'var(--primary)' : 'var(--border)'}`,
|
|
transition: 'all 0.15s',
|
|
}}
|
|
>{label}</button>
|
|
))}
|
|
</div>
|
|
{form.type === 'moderator' && (
|
|
<p style={{ fontSize: '0.78rem', color: 'var(--text-muted)', marginTop: 6 }}>
|
|
Moderators can upload PDFs and manage quiz content. Tell us about your background and how you'd like to contribute.
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: 5, color: 'var(--text-muted)' }}>Message</label>
|
|
<textarea
|
|
value={form.message} onChange={e => setForm(f => ({ ...f, message: e.target.value }))}
|
|
placeholder={form.type === 'moderator' ? "Tell us about your background, specialty, and how you'd like to help..." : 'Your question or message...'}
|
|
required rows={4} maxLength={2000}
|
|
style={{ width: '100%', padding: '9px 12px', borderRadius: 8, border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text)', fontSize: '0.9rem', resize: 'vertical', fontFamily: 'inherit', boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
<TurnstileWidget onVerify={setTurnstileToken} />
|
|
{error && <div style={{ color: 'var(--wrong-fg)', fontSize: '0.85rem', background: 'var(--wrong-bg)', padding: '8px 12px', borderRadius: 6 }}>{error}</div>}
|
|
<button
|
|
type="submit" disabled={loading || !canSubmit}
|
|
className="btn btn-primary"
|
|
style={{ alignSelf: 'flex-start', minWidth: 140 }}
|
|
>
|
|
{loading ? 'Sending…' : 'Send Message'}
|
|
</button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
// ── Auth modal (Sign In / Register as overlay) ──────────────────────────────
|
|
function AuthModal({ mode, onClose, onSwitch }) {
|
|
const { login, loginWithToken } = useAuth()
|
|
const navigate = useNavigate()
|
|
const [name, setName] = useState('')
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [unverified, setUnverified] = useState(false)
|
|
const [turnstileToken, setTurnstileToken] = useState('')
|
|
const [resendSent, setResendSent] = useState(false)
|
|
const [resending, setResending] = useState(false)
|
|
const [registered, setRegistered] = useState(false)
|
|
|
|
const reset = () => { setError(''); setUnverified(false); setResendSent(false); setRegistered(false) }
|
|
const switchMode = (m) => { reset(); setName(''); setEmail(''); setPassword(''); onSwitch(m) }
|
|
|
|
const handleLogin = async (e) => {
|
|
e.preventDefault()
|
|
setError(''); setUnverified(false)
|
|
setLoading(true)
|
|
try {
|
|
await login(email, password, turnstileToken)
|
|
onClose()
|
|
navigate('/')
|
|
} catch (err) {
|
|
if (err.response?.status === 403) setUnverified(true)
|
|
else setError(err.response?.data?.detail || 'Login failed')
|
|
} finally { setLoading(false) }
|
|
}
|
|
|
|
const handleRegister = async (e) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
try {
|
|
const res = await api.post('/auth/register', { email, password, name, turnstile_token: turnstileToken || null })
|
|
if (res.data.requires_verification) {
|
|
setRegistered(true)
|
|
} else {
|
|
await loginWithToken(res.data.access_token)
|
|
onClose()
|
|
navigate('/')
|
|
}
|
|
} catch (err) {
|
|
const detail = err.response?.data?.detail
|
|
if (typeof detail === 'string') setError(detail)
|
|
else if (Array.isArray(detail)) setError(detail.some(e => e.loc?.includes('email')) ? 'Invalid email address.' : 'Please check your input and try again.')
|
|
else setError('Registration failed')
|
|
} finally { setLoading(false) }
|
|
}
|
|
|
|
const resendVerification = async () => {
|
|
setResending(true)
|
|
try { await api.post('/auth/resend-verification', { email }) } catch {}
|
|
setResendSent(true)
|
|
setResending(false)
|
|
}
|
|
|
|
const isLogin = mode === 'login'
|
|
|
|
return (
|
|
<div onClick={onClose} style={{
|
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 1000,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
}}>
|
|
<div onClick={e => e.stopPropagation()} style={{
|
|
background: 'var(--card-bg)', borderRadius: 'var(--card-radius, 14px)',
|
|
padding: '28px 24px', maxWidth: 400, width: '100%',
|
|
boxShadow: '0 20px 60px rgba(0,0,0,0.3)', position: 'relative',
|
|
}}>
|
|
<button onClick={onClose} style={{
|
|
position: 'absolute', top: 12, right: 14, background: 'none',
|
|
border: 'none', cursor: 'pointer', color: 'var(--text-muted)', fontSize: '1.2rem',
|
|
}}>✕</button>
|
|
|
|
{/* Tabs */}
|
|
<div style={{ display: 'flex', gap: 0, marginBottom: 20, borderBottom: '2px solid var(--border)' }}>
|
|
{[['login', 'Sign In'], ['register', 'Register']].map(([m, label]) => (
|
|
<button key={m} onClick={() => switchMode(m)} style={{
|
|
flex: 1, padding: '10px 0', background: 'none', border: 'none', cursor: 'pointer',
|
|
fontWeight: mode === m ? 700 : 400, fontSize: '0.95rem',
|
|
color: mode === m ? 'var(--primary)' : 'var(--text-muted)',
|
|
borderBottom: mode === m ? '2px solid var(--primary)' : '2px solid transparent',
|
|
marginBottom: -2,
|
|
}}>{label}</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Registration success */}
|
|
{registered && (
|
|
<div style={{ textAlign: 'center', padding: '12px 0' }}>
|
|
<div style={{ fontSize: '2.5rem', marginBottom: 12 }}>📧</div>
|
|
<h3 style={{ marginBottom: 8 }}>Check your email</h3>
|
|
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', marginBottom: 16 }}>
|
|
We sent a verification link to <strong>{email}</strong>. Click it to activate your account.
|
|
</p>
|
|
<button className="btn btn-primary" onClick={() => { setRegistered(false); switchMode('login') }}>
|
|
Go to Sign In
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Login form */}
|
|
{isLogin && !registered && (
|
|
<>
|
|
{unverified && !resendSent && (
|
|
<div style={{ background: '#fffbeb', border: '1px solid #fcd34d', borderRadius: 8, padding: '12px 14px', marginBottom: 14 }}>
|
|
<p style={{ margin: '0 0 8px', fontSize: '0.85rem', color: '#92400e', fontWeight: 500 }}>
|
|
Email not verified. Check your inbox.
|
|
</p>
|
|
<button className="btn btn-sm btn-primary" onClick={resendVerification} disabled={resending}>
|
|
{resending ? 'Sending…' : 'Resend verification email'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
{resendSent && <div className="alert alert-success" style={{ marginBottom: 14 }}>Verification email sent.</div>}
|
|
{error && <div className="alert alert-error" style={{ marginBottom: 14 }}>{error}</div>}
|
|
<form onSubmit={handleLogin}>
|
|
<div className="form-group">
|
|
<label>Email</label>
|
|
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Password</label>
|
|
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
|
</div>
|
|
<TurnstileWidget onVerify={setTurnstileToken} />
|
|
<button className="btn btn-primary" style={{ width: '100%' }} disabled={loading || (TURNSTILE_SITE_KEY && !turnstileToken)}>
|
|
{loading ? 'Signing in…' : 'Sign In'}
|
|
</button>
|
|
</form>
|
|
<div style={{ textAlign: 'center', marginTop: 12, fontSize: '0.82rem' }}>
|
|
<Link to="/forgot-password" onClick={onClose} style={{ color: 'var(--text-muted)' }}>Forgot password?</Link>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Register form */}
|
|
{!isLogin && !registered && (
|
|
<>
|
|
{error && <div className="alert alert-error" style={{ marginBottom: 14 }}>{error}</div>}
|
|
<form onSubmit={handleRegister}>
|
|
<div className="form-group">
|
|
<label>Name</label>
|
|
<input type="text" value={name} onChange={e => setName(e.target.value)} required autoFocus />
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Email</label>
|
|
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required />
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Password</label>
|
|
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required minLength={8} />
|
|
</div>
|
|
<TurnstileWidget onVerify={setTurnstileToken} />
|
|
<button className="btn btn-primary" style={{ width: '100%' }} disabled={loading || (TURNSTILE_SITE_KEY && !turnstileToken)}>
|
|
{loading ? 'Creating account…' : 'Sign Up'}
|
|
</button>
|
|
</form>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ── Main landing page ─────────────────────────────────────────────────────────
|
|
|
|
export default function LandingPage() {
|
|
const { user } = useAuth()
|
|
const [authModal, setAuthModal] = useState(null) // null | 'login' | 'register'
|
|
|
|
// Load Turnstile script once
|
|
useEffect(() => {
|
|
if (!TURNSTILE_SITE_KEY || document.getElementById('cf-turnstile-script')) return
|
|
const s = document.createElement('script')
|
|
s.id = 'cf-turnstile-script'
|
|
s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
|
|
s.async = true
|
|
document.head.appendChild(s)
|
|
}, [])
|
|
|
|
return (
|
|
<div style={{ minHeight: '100vh', background: 'var(--bg)' }}>
|
|
|
|
{/* Auth modal overlay */}
|
|
{authModal && (
|
|
<AuthModal
|
|
mode={authModal}
|
|
onClose={() => setAuthModal(null)}
|
|
onSwitch={setAuthModal}
|
|
/>
|
|
)}
|
|
|
|
{/* ── Shared Navbar (handles auth state) ─────────────────────────────── */}
|
|
<style>{`.navbar { margin-bottom: 0 !important; }`}</style>
|
|
<Navbar onSignIn={() => setAuthModal('login')} onRegister={() => setAuthModal('register')} />
|
|
|
|
{/* ── Hero ───────────────────────────────────────────────────────────── */}
|
|
<section style={{
|
|
background: 'var(--navbar-bg)',
|
|
color: 'var(--navbar-fg)',
|
|
padding: '96px 24px 88px',
|
|
textAlign: 'center',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
}}>
|
|
{/* subtle grid */}
|
|
<div style={{
|
|
position: 'absolute', inset: 0, opacity: 0.04,
|
|
backgroundImage: 'linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px)',
|
|
backgroundSize: '48px 48px',
|
|
}} />
|
|
<div style={{ position: 'relative', maxWidth: 720, margin: '0 auto' }}>
|
|
<div style={{ display: 'inline-block', background: 'rgba(96,165,250,0.15)', border: '1px solid rgba(96,165,250,0.3)', borderRadius: 20, padding: '4px 14px', fontSize: '0.78rem', fontWeight: 600, color: '#93c5fd', marginBottom: 28, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
|
|
Pediatric Learning Platform
|
|
</div>
|
|
<h1 style={{ fontSize: 'clamp(2.2rem, 5vw, 3.4rem)', fontWeight: 800, lineHeight: 1.15, margin: '0 0 22px', letterSpacing: '-0.03em' }}>
|
|
Turn your textbooks<br />
|
|
<span style={{ background: 'linear-gradient(90deg, #60a5fa, #a78bfa)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
|
|
into tests
|
|
</span>
|
|
</h1>
|
|
<p style={{ fontSize: '1.15rem', color: 'rgba(226,232,240,0.75)', lineHeight: 1.7, marginBottom: 40, maxWidth: 560, margin: '0 auto 40px' }}>
|
|
Upload any PDF — PREP materials, lecture notes, textbook chapters.
|
|
AI extracts questions, reads them aloud, and explains every answer.
|
|
</p>
|
|
<div style={{ display: 'flex', gap: 14, justifyContent: 'center', flexWrap: 'wrap' }}>
|
|
{user ? <>
|
|
<Link to="/" className="btn btn-primary" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10 }}>Dashboard</Link>
|
|
<Link to="/quizzes" className="btn" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10, background: 'rgba(255,255,255,0.08)', color: 'var(--navbar-fg)', border: '1px solid rgba(255,255,255,0.18)', textDecoration: 'none' }}>My Quizzes</Link>
|
|
<Link to="/question-bank" className="btn" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10, background: 'rgba(255,255,255,0.08)', color: 'var(--navbar-fg)', border: '1px solid rgba(255,255,255,0.18)', textDecoration: 'none' }}>Question Bank</Link>
|
|
</> : <>
|
|
<button onClick={() => setAuthModal('login')} className="btn btn-primary" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10 }}>Sign In</button>
|
|
<button onClick={() => setAuthModal('register')} className="btn" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10, background: 'rgba(255,255,255,0.08)', color: 'var(--navbar-fg)', border: '1px solid rgba(255,255,255,0.18)' }}>Register</button>
|
|
</>}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Features ───────────────────────────────────────────────────────── */}
|
|
<section style={{ maxWidth: 1100, margin: '0 auto', padding: '80px 24px' }}>
|
|
<div style={{ textAlign: 'center', marginBottom: 56 }}>
|
|
<h2 style={{ fontSize: 'clamp(1.6rem, 3vw, 2.2rem)', fontWeight: 800, letterSpacing: '-0.02em', margin: '0 0 12px' }}>
|
|
Everything you need to study smarter
|
|
</h2>
|
|
<p style={{ color: 'var(--text-muted)', fontSize: '1.05rem', maxWidth: 480, margin: '0 auto' }}>
|
|
Built specifically for pediatric board prep and clinical learning.
|
|
</p>
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))', gap: 20 }}>
|
|
{FEATURES.map((f, i) => (
|
|
<div key={i} className="card" style={{ padding: '24px', transition: 'transform 0.2s, box-shadow 0.2s' }}
|
|
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-3px)'; e.currentTarget.style.boxShadow = '0 8px 32px rgba(0,0,0,0.12)' }}
|
|
onMouseLeave={e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = '' }}
|
|
>
|
|
<div style={{ fontSize: '2rem', marginBottom: 12 }}>{f.icon}</div>
|
|
<h3 style={{ fontWeight: 700, fontSize: '1rem', margin: '0 0 8px' }}>{f.title}</h3>
|
|
<p style={{ color: 'var(--text-muted)', fontSize: '0.9rem', lineHeight: 1.65, margin: 0 }}>{f.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── AI Scribe section ──────────────────────────────────────────────── */}
|
|
<section style={{ background: 'var(--navbar-bg)', color: 'var(--navbar-fg)', padding: '80px 24px' }}>
|
|
<div style={{ maxWidth: 1000, margin: '0 auto', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))', gap: 40, alignItems: 'center' }}>
|
|
<div>
|
|
<div style={{ display: 'inline-block', background: 'rgba(96,165,250,0.15)', border: '1px solid rgba(96,165,250,0.3)', borderRadius: 20, padding: '4px 14px', fontSize: '0.78rem', fontWeight: 600, color: '#93c5fd', marginBottom: 20, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
|
|
Also from PedsHub
|
|
</div>
|
|
<h2 style={{ fontSize: 'clamp(1.6rem, 3vw, 2rem)', fontWeight: 800, letterSpacing: '-0.02em', margin: '0 0 16px', color: '#f1f5f9' }}>
|
|
Pediatric AI Scribe
|
|
</h2>
|
|
<p style={{ color: '#94a3b8', lineHeight: 1.7, marginBottom: 28, fontSize: '0.95rem' }}>
|
|
A clinical assistant built for pediatric providers. Voice-to-note documentation,
|
|
age-specific well visit workflows, developmental milestones, vaccine schedules,
|
|
catch-up planners, and automatic ICD-10 billing codes — all in one tool.
|
|
</p>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 32, overflow: 'hidden' }}>
|
|
{[
|
|
'🩺 Well visit planner from newborn through adolescence',
|
|
'📋 Developmental milestone tracking across 4 domains',
|
|
'💉 Full AAP/ACIP vaccine schedule with catch-up planner',
|
|
'🎙 AI scribe — speak, get a structured note',
|
|
'💊 Automatic ICD-10 and CPT billing codes',
|
|
].map((item, i) => (
|
|
<div key={i} style={{ fontSize: '0.88rem', color: '#cbd5e1', display: 'flex', gap: 8, alignItems: 'flex-start', overflowWrap: 'break-word', wordBreak: 'break-word', minWidth: 0 }}>
|
|
<span>{item}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<a
|
|
href="https://app.pedshub.com"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="btn btn-primary"
|
|
style={{ display: 'inline-block', textDecoration: 'none', padding: '11px 24px', borderRadius: 10 }}
|
|
>
|
|
Open AI Scribe ↗
|
|
</a>
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 130px), 1fr))', gap: 10 }}>
|
|
{[
|
|
{ icon: '📅', label: 'Well Visits', sub: '2wk → 18yr' },
|
|
{ icon: '🧠', label: 'Milestones', sub: '2mo → 5yr' },
|
|
{ icon: '💉', label: 'Vaccines', sub: 'Full schedule' },
|
|
{ icon: '🎙', label: 'AI Scribe', sub: 'Voice-to-note' },
|
|
{ icon: '💊', label: 'ICD-10', sub: 'Auto-suggested' },
|
|
{ icon: '📋', label: 'SOAP Notes', sub: 'Structured' },
|
|
].map((item, i) => (
|
|
<div key={i} style={{
|
|
background: 'rgba(255,255,255,0.05)',
|
|
border: '1px solid rgba(255,255,255,0.08)',
|
|
borderRadius: 10, padding: '14px',
|
|
transition: 'background 0.2s',
|
|
}}
|
|
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.09)'}
|
|
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
|
|
>
|
|
<div style={{ fontSize: '1.5rem', marginBottom: 6 }}>{item.icon}</div>
|
|
<div style={{ fontWeight: 700, fontSize: '0.85rem', color: '#e2e8f0' }}>{item.label}</div>
|
|
<div style={{ fontSize: '0.75rem', color: '#64748b' }}>{item.sub}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Contact form ───────────────────────────────────────────────────── */}
|
|
<section id="contact" style={{ maxWidth: 680, margin: '0 auto', padding: '80px 24px' }}>
|
|
<div style={{ textAlign: 'center', marginBottom: 40 }}>
|
|
<h2 style={{ fontSize: 'clamp(1.5rem, 3vw, 2rem)', fontWeight: 800, letterSpacing: '-0.02em', margin: '0 0 12px' }}>
|
|
Get in touch
|
|
</h2>
|
|
<p style={{ color: 'var(--text-muted)', fontSize: '0.95rem' }}>
|
|
Have a question? Want to contribute as a moderator? We'd love to hear from you.
|
|
</p>
|
|
</div>
|
|
<div className="card" style={{ padding: '32px' }}>
|
|
<ContactForm />
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Footer ─────────────────────────────────────────────────────────── */}
|
|
<footer style={{
|
|
background: 'var(--navbar-bg)',
|
|
padding: '32px 24px',
|
|
borderTop: '1px solid rgba(255,255,255,0.05)',
|
|
}}>
|
|
<div style={{ maxWidth: 1100, margin: '0 auto', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
|
<span style={{ fontWeight: 700, color: 'rgba(226,232,240,0.6)', fontSize: '0.9rem' }}>
|
|
🏥 PedsHub
|
|
<span style={{ fontWeight: 400, marginLeft: 16 }}>© {new Date().getFullYear()}</span>
|
|
</span>
|
|
<div style={{ display: 'flex', gap: 24, fontSize: '0.85rem' }}>
|
|
<button onClick={() => setAuthModal('login')} style={{ color: 'rgba(226,232,240,0.45)', background: 'none', border: 'none', cursor: 'pointer', fontSize: 'inherit' }}>Sign In</button>
|
|
<a href="#contact" style={{ color: 'rgba(226,232,240,0.45)', textDecoration: 'none' }}>Contact</a>
|
|
<a href="https://app.pedshub.com" target="_blank" rel="noopener noreferrer" style={{ color: 'rgba(226,232,240,0.45)', textDecoration: 'none' }}>AI Scribe</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
</div>
|
|
)
|
|
}
|