From 5403d78a3e294a261c4b5be24526a3b79f17dfca Mon Sep 17 00:00:00 2001
From: Daniel
Date: Sat, 4 Apr 2026 01:41:39 +0200
Subject: [PATCH] Fix landing page routing, add logged-in nav integration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
backend/app/routers/contact.py | 49 ++++---------
frontend/src/App.jsx | 114 +++++++++++++++--------------
frontend/src/pages/LandingPage.jsx | 52 ++++++-------
3 files changed, 101 insertions(+), 114 deletions(-)
diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py
index c3ad3ff..85a4069 100644
--- a/backend/app/routers/contact.py
+++ b/backend/app/routers/contact.py
@@ -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}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 20ca3e3..66184b3 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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
- if (!user) return
- if (requireModerator && user.role !== 'admin' && user.role !== 'moderator') return
- return children
+// Layout wrapper for authenticated app pages (Navbar + container + footer)
+function AppLayout() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
}
-function Footer() {
- return (
-
- )
+// Guard: redirect to /home if not logged in, or to / if not moderator
+function RequireAuth({ moderator = false }) {
+ const { user, loading } = useAuth()
+ if (loading) return
+ if (!user) return
+ if (moderator && user.role !== 'admin' && user.role !== 'moderator') return
+ return
}
function AppRoutes() {
const { user, loading } = useAuth()
- if (loading) return
+ if (loading) return
return (
- <>
- {!user &&
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- }
- {user && <>
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
- >}
- >
+
+ {/* Always public */}
+ } />
+ : } />
+ : } />
+ } />
+ } />
+ } />
+
+ {/* Authenticated app — wrapped in AppLayout */}
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ {/* Moderator-only */}
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+
+
+
+ {/* Catch-all */}
+ : } />
+
)
}
diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx
index 5046c12..c30b80d 100644
--- a/frontend/src/pages/LandingPage.jsx
+++ b/frontend/src/pages/LandingPage.jsx
@@ -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() {
Message