Free Grades 6-8 math resources — self-paced study paths, video walkthroughs, worksheets, and quizzes — layered under a paid 1-on-1 tutoring business. Built with Next.js 15, React 19, and Tailwind CSS v4.
Live at https://www.joinascentmath.com
npm install
cp .env.example .env.local # then fill in your Resend API key
npm run devOpen http://localhost:3000.
The booking form lives on the homepage (#book-session) and sends real emails to Adam and Alan via Resend. Both adamissac08@gmail.com and alanmozhoor@gmail.com receive submissions. The form posts to the in-app API route src/app/api/contact/route.ts — no mailto: links anywhere, no third-party widgets.
Fields collected: inquiry type (Student Tutoring / Class Demo / School Partnership / General), name, email, school or org (optional), grade level, and message.
There's no instant booking or published pricing on the site. A submission goes to Adam and Alan, who personally review and reply within 1-2 days to set up a free consultation call over Zoom before any paid sessions begin. Tutoring itself is Zoom-only — no in-person option.
-
Create a free Resend account at https://resend.com.
-
Verify both destination addresses (
adamissac08@gmail.comandalanmozhoor@gmail.com) under Settings → Verified Emails. This lets you send to those inboxes without owning a custom domain. -
Create an API key under API Keys → Create API Key. Copy the
re_…string. -
Copy
.env.exampleto.env.localand paste the key:RESEND_API_KEY=re_your_real_key_here BOOKING_RECIPIENT_EMAIL=adamissac08@gmail.com,alanmozhoor@gmail.com BOOKING_FROM_EMAIL="Ascent Math <onboarding@resend.dev>" -
Restart the dev server. Bookings now flow to both tutors' inboxes.
Add the same three keys in Project Settings → Environment Variables. Redeploy.
Once joinascentmath.com is verified in Resend (Settings → Domains), switch BOOKING_FROM_EMAIL to something like "Ascent Math <booking@joinascentmath.com>". Replies will still route to whoever submitted the form, because the API sets replyTo to the visitor's address.
- Validates every field server-side with Zod (
src/app/api/contact/route.ts). - Rate-limits to 5 submissions per minute per IP.
- Honeypot field hidden in the form catches bots.
- Templates the email (HTML + plain text) in
src/lib/contactEmail.ts. - Returns structured JSON:
{ ok: true }on success, or{ ok: false, error, fieldErrors? }on failure — the form surfaces these inline.
Account pages live at /signup, /login, /forgot-password, and /dashboard. The system uses Firebase Authentication (email + password and Google) with optional Firestore for storing user profiles.
Note: the free study paths themselves require no account — this auth system is separate, opt-in infrastructure. Worth double-checking before public launch whether the site's "no account needed" messaging and this dashboard coexist cleanly, especially given COPPA considerations for a site with under-13 visitors.
-
Create a Firebase project at https://console.firebase.google.com.
-
Enable sign-in methods:
- Authentication → Sign-in method → enable Email/Password.
- Authentication → Sign-in method → enable Google (set a support email).
-
(Optional) Enable Firestore to persist user profiles:
-
Build → Firestore Database → Create database (Production mode → pick a region).
-
Replace the default rules with the snippet below so a user can only read/write their own profile:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{uid} { allow read, write: if request.auth != null && request.auth.uid == uid; } } }
-
-
Register a web app: Project settings (gear) → General → "Your apps" → Web (
</>) → register. Copy the sixfirebaseConfigvalues into.env.local:NEXT_PUBLIC_FIREBASE_API_KEY=... NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789012 NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789012:web:abc...
These keys are public by design — the Firebase Web SDK requires them in the browser bundle. Security is enforced by the rules above, not by hiding the key.
-
Authorized domains: still in Authentication → Settings → Authorized domains, add
localhost(already there) and any production domain you'll deploy on (e.g.joinascentmath.com,ascent-math.vercel.app). -
Restart
npm run dev. Sign up at/signup, log out from the navbar avatar menu, and try/forgot-password.
| Concern | Where |
|---|---|
| SDK init (lazy, env-guarded) | src/lib/firebase.ts |
| Friendly error messages | src/lib/auth-errors.ts |
| Session + actions context | src/components/AuthProvider.tsx |
| Route guards | src/components/ProtectedRoute.tsx, src/components/RedirectIfAuthed.tsx |
| Shared auth-page chrome | src/components/AuthShell.tsx |
| Form primitives (Google btn, password meter, etc.) | src/components/AuthFormParts.tsx |
| Signup / login / forgot / dashboard pages | src/app/{signup,login,forgot-password,dashboard}/page.tsx |
| Navbar user menu | src/components/Navbar.tsx |
Key behaviors:
- Persistent sessions —
browserLocalPersistenceby default. Uncheck "Remember me" on the login screen to switch tobrowserSessionPersistence. - Protected routes —
/dashboardredirects to/login?next=/dashboardif not authenticated. After login the user is sent back to the originalnextpath. - Already-logged-in guard —
/login,/signup,/forgot-passwordredirect to/dashboardif the visitor already has a session. - Graceful degradation — if
NEXT_PUBLIC_FIREBASE_*aren't set, every auth screen and the dashboard show a friendly "Firebase not configured" banner instead of crashing. - Profiles — on first sign-in a
users/{uid}document is created in Firestore (name, email, role, createdAt). If Firestore isn't enabled the auth still works; we just synthesize the profile from the Auth record. - Google sign-in resilience — tries
signInWithPopupfirst; if the browser blocks the popup or refuses third-party storage, automatically falls back tosignInWithRedirect.getRedirectResultis consumed on mount so the user lands back in/dashboardwithout an extra click.
If "Continue with Google" doesn't work, walk this checklist:
- Method enabled? Firebase Console → Authentication → Sign-in method → confirm Google shows "Enabled". Setting a support email is required to turn it on. If you haven't done this you'll see
auth/operation-not-allowed. - Domain authorized? Authentication → Settings → Authorized domains must include the host the user is on.
localhostis added by default; your production host (e.g.joinascentmath.com,ascent-math.vercel.app) must be added manually. Missing entries throwauth/unauthorized-domain. - Env vars filled? All six
NEXT_PUBLIC_FIREBASE_*values must be set in.env.localand the dev server restarted afterwards (Next.js bakes them in at build time). - Popup blocker? Some browsers (especially Safari + Firefox in strict mode) block OAuth popups. The app will auto-fall-back to a full-page redirect — if you'd rather use the popup, allow popups for this site.
- Strict tracking protection? Brave / Safari ITP can disable the storage Firebase needs. You'll see
auth/web-storage-unsupported. Disable strict tracking for the site or use the redirect flow. - Verified support email? If you change the support email in GCP, Google sign-in can briefly stop working — re-saving the Sign-in method config refreshes it.
src/
├── app/
│ ├── layout.tsx # Root layout (Nav + Footer + fonts)
│ ├── page.tsx # Landing page (credentials, booking form, study paths)
│ ├── globals.css # Design tokens + base styles
│ ├── loading.tsx, error.tsx, not-found.tsx
│ ├── about/page.tsx
│ ├── privacy/page.tsx
│ ├── terms/page.tsx
│ ├── accessibility/page.tsx
│ └── mathematics/
│ ├── page.tsx # Curriculum hub / "Find your start"
│ ├── curriculum-frameworks/page.tsx
│ └── [grade]/[unit]/page.tsx # Dynamic unit page (Grades 6, 7, 8)
├── components/ # Reusable UI primitives
│ ├── Navbar, Footer, Container, Section, SectionHeader
│ ├── Button, Card, Badge, Breadcrumbs, ProgressBar
│ ├── VideoEmbed, WorksheetCard, ResourceLinkCard
│ ├── Quiz, UnitProgressPanel
├── data/
│ └── units.ts # Curriculum source of truth (Grades 6-8)
└── hooks/
└── useUnitProgress.ts # localStorage-backed progress
All curriculum data — units, videos, worksheets, quiz questions — lives in src/data/units.ts, covering Grade 6 (7 units, 22 topics), Grade 7 (6 units, 17 topics), and Grade 8 (7 units, 16 topics). Edit that file and every page (home, math hub, unit pages, frameworks page) updates automatically.
videos: [
{ videoId: "abc123XYZ", title: "Adding fractions", source: "Khan Academy", description: "..." },
// ...
]worksheets: [
{ driveFileId: "1abc...", title: "Worksheet 1", description: "..." },
]quiz: [
{
id: "u1-q5",
prompt: "What is 1/2 + 1/3?",
type: "multiple-choice",
options: ["2/5", "5/6", "1/5", "2/6"],
answer: "5/6",
explanation: "Common denominator 6: 3/6 + 2/6 = 5/6.",
difficulty: "easy",
},
]Design tokens are defined as CSS custom properties in src/app/globals.css under the @theme block. Component primitives map to these tokens via utility classes like btn, btn-primary, card, pill, etc.
Tokens include:
- Brand palette (
--color-brand-50…--color-brand-900) — teal-green - Accent palette (
--color-accent-*) — warm amber - Surface, ink, border tokens (background
#FBFAF7) - Semantic colors (info, success, warning, danger)
- Radii, shadows, focus rings
| Command | Description |
|---|---|
npm run dev |
Start dev server |
npm run build |
Production build |
npm run start |
Serve production build |
npm run lint |
Lint check |
Free educational use. Not affiliated with Khan Academy, GADOE, Georgia Tech, or any specific school district.
Never commit .env.local. Use .env.example as the template for Resend and related keys.