Add PedsHub landing page with contact form and AI Scribe section

Frontend:
- LandingPage.jsx: hero, 6 feature cards, AI Scribe section (links peds.danvics.com), contact form
- Contact form: name/email/type toggle (question vs moderator app), Turnstile anti-bot (optional)
- App.jsx: /home route for unauthenticated users, auth pages reachable pre-login, logged-in /home → /

Backend:
- contact.py: POST /api/contact (public), stores in DB, emails admin; GET /submissions (admin)
- main.py: create contact_submissions table, register contact router
- main.py: add pedshub.com + www.pedshub.com to CORS origins
- config.py: TURNSTILE_SECRET_KEY, ADMIN_EMAIL settings

Env vars to set:
  VITE_TURNSTILE_SITE_KEY=<from cloudflare.com/turnstile>  (frontend)
  TURNSTILE_SECRET_KEY=<secret>  (backend .env)
  ADMIN_EMAIL=you@example.com  (backend .env)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-04-04 01:34:44 +02:00
parent 4cbaa529f0
commit ee47fb07a2
5 changed files with 547 additions and 5 deletions

View file

@ -38,5 +38,8 @@ class Settings(BaseSettings):
UPLOAD_DIR: str = "./uploads"
MAX_UPLOAD_SIZE: int = 524288000 # 500MB
TURNSTILE_SECRET_KEY: str = "" # Cloudflare Turnstile — leave blank to disable captcha
ADMIN_EMAIL: str = "" # Where contact form submissions are emailed
settings = Settings()

View file

@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles
from app.config import settings
from app.database import engine, Base, SessionLocal
from app.routers import auth, documents, quizzes, attempts, admin, tts, nextcloud, categories, questions, question_categories, favorites, teach
from app.routers import auth, documents, quizzes, attempts, admin, tts, nextcloud, categories, questions, question_categories, favorites, teach, contact
from app.utils.auth import get_password_hash
from app.utils.scheduler import start_scheduler, stop_scheduler
@ -230,6 +230,18 @@ def setup_pgvector():
"""))
# Unthrottle flag for users (exempt from AI/TTS rate limits)
conn.execute(text("ALTER TABLE users ADD COLUMN IF NOT EXISTS is_unthrottled INTEGER DEFAULT 0"))
# Contact form submissions table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS contact_submissions (
id SERIAL PRIMARY KEY,
name VARCHAR(120) NOT NULL,
email VARCHAR NOT NULL,
type VARCHAR NOT NULL,
message TEXT NOT NULL,
read INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
)
"""))
conn.commit()
@ -307,7 +319,7 @@ app = FastAPI(
app.add_middleware(
CORSMiddleware,
allow_origins=["https://quiz.danvics.com"],
allow_origins=["https://quiz.danvics.com", "https://pedshub.com", "https://www.pedshub.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -333,6 +345,7 @@ app.include_router(questions.router, prefix="/api/questions", tags=["questions"]
app.include_router(question_categories.router, prefix="/api/question-categories", tags=["question-categories"])
app.include_router(favorites.router, prefix="/api/favorites", tags=["favorites"])
app.include_router(teach.router, prefix="/api/teach", tags=["teach"])
app.include_router(contact.router, prefix="/api/contact", tags=["contact"])
@app.get("/api/health")

View file

@ -0,0 +1,140 @@
"""Contact form — stores submissions and emails admin."""
from datetime import datetime
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.orm import Session
from app.database import get_db, Base
from app.config import settings
router = APIRouter()
log = logging.getLogger(__name__)
class ContactSubmission(Base):
__tablename__ = "contact_submissions"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
email = Column(String, nullable=False)
type = Column(String, nullable=False) # "question" | "moderator"
message = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
read = Column(Integer, default=0)
class ContactRequest(BaseModel):
name: str
email: EmailStr
type: str # "question" | "moderator"
message: str
turnstile_token: str | None = None
async def _verify_turnstile(token: str) -> bool:
"""Verify Cloudflare Turnstile token. Returns True if valid or if Turnstile not configured."""
secret = getattr(settings, "TURNSTILE_SECRET_KEY", None)
if not secret:
return True # Not configured — allow in dev/no-captcha mode
try:
import httpx
resp = httpx.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": secret, "response": token},
timeout=10,
)
return resp.json().get("success", False)
except Exception as e:
log.warning(f"Turnstile verification failed: {e}")
return False
@router.post("")
async def submit_contact(req: ContactRequest, db: Session = Depends(get_db)):
"""Public endpoint — no auth required."""
if req.type not in ("question", "moderator"):
raise HTTPException(status_code=400, detail="Type must be 'question' or 'moderator'")
name = req.name.strip()[:120]
message = req.message.strip()[:2000]
if not name or not message:
raise HTTPException(status_code=400, detail="Name and message are required")
# Verify Turnstile if configured
secret = getattr(settings, "TURNSTILE_SECRET_KEY", None)
if secret:
if not req.turnstile_token:
raise HTTPException(status_code=400, detail="Bot verification required")
if not await _verify_turnstile(req.turnstile_token):
raise HTTPException(status_code=400, detail="Bot verification failed — please try again")
submission = ContactSubmission(
name=name,
email=req.email.lower().strip(),
type=req.type,
message=message,
)
db.add(submission)
db.commit()
# Email admin
_email_admin(name, req.email, req.type, message)
return {"success": True}
def _email_admin(name: str, email: str, type_: str, message: str):
try:
from app.services.email_service import send_email
label = "Moderator Application" if type_ == "moderator" else "Contact Form Question"
body = (
f"<h2>New {label} — PedsHub</h2>"
f"<p><strong>From:</strong> {name} &lt;{email}&gt;</p>"
f"<p><strong>Message:</strong></p>"
f"<blockquote style='border-left:4px solid #2563eb;padding-left:12px;color:#334155'>"
f"{message.replace(chr(10), '<br>')}"
f"</blockquote>"
)
admin_email = getattr(settings, "ADMIN_EMAIL", None) or getattr(settings, "SMTP_FROM", None)
if admin_email:
send_email(to=admin_email, subject=f"[PedsHub] {label} from {name}", html=body)
except Exception as e:
log.warning(f"Failed to email admin about contact submission: {e}")
# ── Admin view of submissions ─────────────────────────────────────────────────
@router.get("/submissions")
def list_submissions(
db: Session = Depends(get_db),
):
"""Admin: list all contact submissions."""
from app.utils.auth import require_admin
rows = db.query(ContactSubmission).order_by(ContactSubmission.created_at.desc()).all()
return [
{
"id": r.id,
"name": r.name,
"email": r.email,
"type": r.type,
"message": r.message,
"read": r.read,
"created_at": r.created_at.isoformat(),
}
for r in rows
]
@router.put("/submissions/{submission_id}/read")
def mark_read(
submission_id: int,
db: Session = Depends(get_db),
):
row = db.query(ContactSubmission).filter(ContactSubmission.id == submission_id).first()
if not row:
raise HTTPException(status_code=404, detail="Not found")
row.read = 1
db.commit()
return {"success": True}

View file

@ -21,11 +21,12 @@ import VerifyEmailPage from './pages/VerifyEmailPage'
import ForgotPasswordPage from './pages/ForgotPasswordPage'
import ResetPasswordPage from './pages/ResetPasswordPage'
import NotFoundPage from './pages/NotFoundPage'
import LandingPage from './pages/LandingPage'
function ProtectedRoute({ children, requireModerator = false }) {
const { user, loading } = useAuth()
if (loading) return <div className="loading"><div className="spinner"></div></div>
if (!user) return <Navigate to="/login" />
if (!user) return <Navigate to="/home" />
if (requireModerator && user.role !== 'admin' && user.role !== 'moderator') return <Navigate to="/" />
return children
}
@ -46,11 +47,22 @@ function AppRoutes() {
return (
<>
{!user && <Routes>
<Route path="/home" element={<LandingPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/verify-email" element={<VerifyEmailPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/reset-password" element={<ResetPasswordPage />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>}
{user && <>
<Navbar />
<div className="container">
<Routes>
<Route path="/login" element={user ? <Navigate to="/" /> : <LoginPage />} />
<Route path="/register" element={user ? <Navigate to="/" /> : <RegisterPage />} />
<Route path="/login" element={<Navigate to="/" />} />
<Route path="/register" element={<Navigate to="/" />} />
<Route path="/home" element={<Navigate to="/" />} />
<Route path="/verify-email" element={<VerifyEmailPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/reset-password" element={<ResetPasswordPage />} />
@ -71,6 +83,7 @@ function AppRoutes() {
</Routes>
</div>
<Footer />
</>}
</>
)
}

View file

@ -0,0 +1,373 @@
import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import api from '../api/client'
// Turnstile widget
const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
function TurnstileWidget({ onVerify }) {
const ref = useRef(null)
useEffect(() => {
if (!TURNSTILE_SITE_KEY || !window.turnstile) return
const id = window.turnstile.render(ref.current, {
sitekey: TURNSTILE_SITE_KEY,
callback: onVerify,
})
return () => { try { window.turnstile.remove(id) } 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: '1fr 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>
)
}
// Main landing page
export default function LandingPage() {
// 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)' }}>
{/* ── Navbar ─────────────────────────────────────────────────────────── */}
<nav style={{
position: 'sticky', top: 0, zIndex: 100,
background: 'var(--navbar-bg)', color: 'var(--navbar-fg)',
borderBottom: '1px solid rgba(255,255,255,0.06)',
backdropFilter: 'blur(12px)',
}}>
<div style={{ maxWidth: 1100, margin: '0 auto', padding: '0 24px', height: 56, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontWeight: 800, fontSize: '1.25rem', letterSpacing: '-0.02em', color: '#e2e8f0' }}>
Peds<span style={{ color: '#60a5fa' }}>Hub</span>
</span>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<a
href="https://peds.danvics.com"
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: '0.85rem', color: '#94a3b8', textDecoration: 'none', padding: '6px 12px', borderRadius: 6, transition: 'color 0.15s' }}
onMouseEnter={e => e.currentTarget.style.color = '#e2e8f0'}
onMouseLeave={e => e.currentTarget.style.color = '#94a3b8'}
>
AI Scribe
</a>
<a href="#contact" style={{ fontSize: '0.85rem', color: '#94a3b8', textDecoration: 'none', padding: '6px 12px', borderRadius: 6 }}
onMouseEnter={e => e.currentTarget.style.color = '#e2e8f0'}
onMouseLeave={e => e.currentTarget.style.color = '#94a3b8'}
>Contact</a>
<Link to="/login" style={{ fontSize: '0.875rem', color: '#e2e8f0', textDecoration: 'none', padding: '7px 16px', borderRadius: 8, border: '1px solid rgba(255,255,255,0.18)', transition: 'background 0.15s' }}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>Sign In</Link>
</div>
</div>
</nav>
{/* ── Hero ───────────────────────────────────────────────────────────── */}
<section style={{
background: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #0f172a 100%)',
color: 'white',
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: '#94a3b8', 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' }}>
<Link to="/login" className="btn btn-primary" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10 }}>
Sign In
</Link>
<a href="#contact" className="btn" style={{ padding: '12px 28px', fontSize: '1rem', borderRadius: 10, background: 'rgba(255,255,255,0.08)', color: 'white', border: '1px solid rgba(255,255,255,0.18)', textDecoration: 'none' }}>
Request Access
</a>
</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(300px, 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: '1fr 1fr', gap: 56, 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 }}>
{[
'🩺 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' }}>
<span style={{ flexShrink: 0 }}>{item}</span>
</div>
))}
</div>
<a
href="https://peds.danvics.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: '1fr 1fr', gap: 12 }}>
{[
{ 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: '16px',
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)', color: '#475569',
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: '#94a3b8' }}>
Peds<span style={{ color: '#60a5fa' }}>Hub</span>
<span style={{ fontWeight: 400, marginLeft: 16 }}>© {new Date().getFullYear()}</span>
</span>
<div style={{ display: 'flex', gap: 24, fontSize: '0.85rem' }}>
<Link to="/login" style={{ color: '#475569', textDecoration: 'none' }}>Sign In</Link>
<a href="#contact" style={{ color: '#475569', textDecoration: 'none' }}>Contact</a>
<a href="https://peds.danvics.com" target="_blank" rel="noopener noreferrer" style={{ color: '#475569', textDecoration: 'none' }}>AI Scribe</a>
</div>
</div>
</footer>
</div>
)
}