Fix landing page routing, add logged-in nav integration

- App.jsx: simplify to single Routes block with Outlet layout pattern — fixes 404
- App.jsx: /home always accessible regardless of auth state
- LandingPage: show Dashboard/Quizzes/Question Bank nav links + Go to App CTA when logged in
- LandingPage: hero shows app navigation buttons when signed in
- LandingPage: fix apostrophe JSX syntax error in textarea placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-04-04 01:41:39 +02:00
parent ee47fb07a2
commit 5403d78a3e
3 changed files with 101 additions and 114 deletions

View file

@ -1,30 +1,20 @@
"""Contact form — stores submissions and emails admin."""
from datetime import datetime
"""Contact form — stores submissions and emails admin.
Table is created via raw SQL in setup_pgvector() to avoid race conditions with multiple workers.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.database import get_db, Base
from app.database import get_db
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
@ -70,13 +60,9 @@ async def submit_contact(req: ContactRequest, db: Session = Depends(get_db)):
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.execute(text(
"INSERT INTO contact_submissions (name, email, type, message) VALUES (:name, :email, :type, :message)"
), {"name": name, "email": req.email.lower().strip(), "type": req.type, "message": message})
db.commit()
# Email admin
@ -112,15 +98,13 @@ def list_submissions(
):
"""Admin: list all contact submissions."""
from app.utils.auth import require_admin
rows = db.query(ContactSubmission).order_by(ContactSubmission.created_at.desc()).all()
rows = db.execute(text(
"SELECT id, name, email, type, message, read, created_at FROM contact_submissions ORDER BY created_at DESC"
)).fetchall()
return [
{
"id": r.id,
"name": r.name,
"email": r.email,
"type": r.type,
"message": r.message,
"read": r.read,
"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
@ -132,9 +116,8 @@ 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
result = db.execute(text("UPDATE contact_submissions SET read=1 WHERE id=:id"), {"id": submission_id})
db.commit()
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="Not found")
return {"success": True}

View file

@ -1,4 +1,4 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import { ThemeProvider } from './context/ThemeContext'
import Navbar from './components/Navbar'
@ -23,68 +23,72 @@ 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="/home" />
if (requireModerator && user.role !== 'admin' && user.role !== 'moderator') return <Navigate to="/" />
return children
// Layout wrapper for authenticated app pages (Navbar + container + footer)
function AppLayout() {
return (
<>
<Navbar />
<div className="container">
<Outlet />
</div>
<footer className="site-footer">
<div className="container">© {new Date().getFullYear()} PedsHub</div>
</footer>
</>
)
}
function Footer() {
return (
<footer className="site-footer">
<div className="container">
© {new Date().getFullYear()} PedQuiz
</div>
</footer>
)
// Guard: redirect to /home if not logged in, or to / if not moderator
function RequireAuth({ moderator = false }) {
const { user, loading } = useAuth()
if (loading) return <div className="loading"><div className="spinner" /></div>
if (!user) return <Navigate to="/home" replace />
if (moderator && user.role !== 'admin' && user.role !== 'moderator') return <Navigate to="/" replace />
return <Outlet />
}
function AppRoutes() {
const { user, loading } = useAuth()
if (loading) return <div className="loading"><div className="spinner"></div></div>
if (loading) return <div className="loading"><div className="spinner" /></div>
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={<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 />} />
<Route path="/" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
<Route path="/upload" element={<ProtectedRoute requireModerator><UploadPage /></ProtectedRoute>} />
<Route path="/documents/:id" element={<ProtectedRoute><DocumentDetailPage /></ProtectedRoute>} />
<Route path="/quizzes" element={<ProtectedRoute><QuizzesPage /></ProtectedRoute>} />
<Route path="/quizzes/:id" element={<ProtectedRoute><QuizPage /></ProtectedRoute>} />
<Route path="/results/:id" element={<ProtectedRoute><ResultsPage /></ProtectedRoute>} />
<Route path="/admin" element={<ProtectedRoute><AdminPage /></ProtectedRoute>} />
<Route path="/quizzes/:id/edit" element={<ProtectedRoute requireModerator><QuizEditPage /></ProtectedRoute>} />
<Route path="/account" element={<ProtectedRoute><AccountPage /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
<Route path="/question-bank" element={<ProtectedRoute><QuestionBankPage /></ProtectedRoute>} />
<Route path="/jobs" element={<ProtectedRoute requireModerator><JobsPage /></ProtectedRoute>} />
<Route path="/trash" element={<ProtectedRoute requireModerator><TrashPage /></ProtectedRoute>} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</div>
<Footer />
</>}
</>
<Routes>
{/* Always public */}
<Route path="/home" element={<LandingPage />} />
<Route path="/login" element={user ? <Navigate to="/" replace /> : <LoginPage />} />
<Route path="/register" element={user ? <Navigate to="/" replace /> : <RegisterPage />} />
<Route path="/verify-email" element={<VerifyEmailPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/reset-password" element={<ResetPasswordPage />} />
{/* Authenticated app — wrapped in AppLayout */}
<Route element={<RequireAuth />}>
<Route element={<AppLayout />}>
<Route path="/" element={<DashboardPage />} />
<Route path="/quizzes" element={<QuizzesPage />} />
<Route path="/quizzes/:id" element={<QuizPage />} />
<Route path="/results/:id" element={<ResultsPage />} />
<Route path="/documents/:id" element={<DocumentDetailPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/question-bank" element={<QuestionBankPage />} />
<Route path="/admin" element={<AdminPage />} />
</Route>
</Route>
{/* Moderator-only */}
<Route element={<RequireAuth moderator />}>
<Route element={<AppLayout />}>
<Route path="/upload" element={<UploadPage />} />
<Route path="/quizzes/:id/edit" element={<QuizEditPage />} />
<Route path="/jobs" element={<JobsPage />} />
<Route path="/trash" element={<TrashPage />} />
</Route>
</Route>
{/* Catch-all */}
<Route path="*" element={user ? <NotFoundPage /> : <Navigate to="/home" replace />} />
</Routes>
)
}

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import api from '../api/client'
import { useAuth } from '../context/AuthContext'
// Turnstile widget
const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
@ -138,7 +139,7 @@ function ContactForm() {
<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...'}
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' }}
/>
@ -157,7 +158,11 @@ function ContactForm() {
}
// Main landing page
const navLink = { fontSize: '0.85rem', color: '#94a3b8', textDecoration: 'none', padding: '6px 10px', borderRadius: 6 }
export default function LandingPage() {
const { user } = useAuth()
// Load Turnstile script once
useEffect(() => {
if (!TURNSTILE_SITE_KEY || document.getElementById('cf-turnstile-script')) return
@ -182,25 +187,18 @@ export default function LandingPage() {
<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 style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
{user && <>
<Link to="/" style={navLink}>Dashboard</Link>
<Link to="/quizzes" style={navLink}>Quizzes</Link>
<Link to="/question-bank" style={navLink}>Question Bank</Link>
</>}
<a href="https://peds.danvics.com" target="_blank" rel="noopener noreferrer" style={navLink}>AI Scribe </a>
<a href="#contact" style={navLink}>Contact</a>
{user
? <Link to="/" className="btn btn-primary" style={{ fontSize: '0.85rem', padding: '7px 16px', borderRadius: 8, textDecoration: 'none' }}>Go to App</Link>
: <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)' }}>Sign In</Link>
}
</div>
</div>
</nav>
@ -235,12 +233,14 @@ export default function LandingPage() {
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>
{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: 'white', 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: 'white', border: '1px solid rgba(255,255,255,0.18)', textDecoration: 'none' }}>Question Bank</Link>
</> : <>
<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>