A single-screen marketing landing page built with Nuxt 4.1.1, Nuxt UI v4, Tailwind CSS v4 and TypeScript. Optimized to route visitors to a live demo, with clear value props, confidence cues, accessible visuals, and fast interactions.
Presentation in presentation folder - Cursly.pdf
- Nuxt.js 4.1.1 (TypeScript, SSG-ready)
- Nuxt UI v4 (UApp, UContainer, UCard, UButton, UAccordion, UBadge, UIcon, useToast)
- Tailwind CSS v4 (CSS-first design tokens via @theme)
- Icons via Nuxt UI/Iconify
- Pre-render enabled for a fast, static landing page
- Node: 18.20+ (recommended: latest LTS)
- Package manager: npm (pnpm/yarn/bun also work)
Install dependencies:
npm install
Start dev server:
npm run dev
# http://localhost:3010
Build for production:
npm run build
Preview the production build locally:
npm run preview
# http://localhost:3010
Deployment (typical):
- Vercel or Netlify: Connect the repo, set Build Command to
npm run build
, and let Nuxt handle the output directory. - Cloud Run/other: Build and serve the generated output via a Node server or static hosting (pre-rendered).
- app.vue — Wraps the app with
UApp
and configures a global toaster (position: 'top-right'
,duration: 3500
,expand: true
). - nuxt.config.ts — Registers
@nuxt/ui
, includes global CSS, setsui.theme.defaultVariants
for consistent sizes/color defaults, enables pre-render rules. - app.config.ts — Nuxt UI runtime color aliases (maps semantic colors to base palettes).
- assets/css/main.css — Tailwind v4 + Nuxt UI imports and CSS variables for brand tokens, type scale, border radius, container width, and focus states.
- pages/index.vue — The marketing landing page (Hero, Feature highlights, How it works, Integrations, Proof strip, FAQs, Footer).
- pages/privacy.vue — Minimal privacy page to avoid broken footer links.
- package.json — Scripts and dependencies.
- Landing/marketing routes are SSR/SSG (pre-rendered) for speed and SEO.
- Application (Teacher Hub) routes under
/app/**
are client-rendered only (no SSR).- Enforced via
definePageMeta({ ssr: false })
on app pages androuteRules['/app/**'] = { ssr: false, prerender: false }
innuxt.config.ts
. - Server middleware avoids SSR redirects on
/app/**
; auth checks happen client-side after Supabase session initializes.
- Enforced via
New authenticated area for teachers:
GET /app/dashboard
— Dashboard with stats, welcome CTA, and global "Create Course" action.GET /app/courses/list
— Courses table (title, progress, status, created/updated) with "Create Course".GET /app/courses/[id]
— Course detail view with overview and modules; includes edit actions.
Both pages use a shared layout layouts/app.vue
(topbar with logo/search/Create Course/avatar + slim left sidebar navigation) powered by Nuxt UI v4 Dashboard components.
Backend is a FastAPI service located in backend/
(see below).
npm run dev
— Start Nuxt in developmentnpm run build
— Production buildnpm run preview
— Preview the production build locally
- None required for the static landing page.
- For auth and user accounts, fill these in
.env
(copy fromsample.env
):
SUPABASE_PROJECT_URL="https://<your-project-ref>.supabase.co"
SUPABASE_API_KEY="<your-supabase-anon-key>"
# Teacher Hub backend
BACKEND_URL="http://localhost:8000"
# Optional Convex configuration for backend
CONVEX_URL="https://<your-space>.convex.cloud"
CONVEX_DEPLOY_KEY=""
CONVEX_USER_BEARER=""
# Optional if you implement your own Google OAuth (not required when using Supabase's Google provider)
GOOGLE_OAUTH_ID=""
GOOGLE_OAUTH_SECRET=""
Do not commit real secrets. .env
is already gitignored.
This project integrates Supabase Auth using @nuxtjs/supabase
:
- Email sign up with confirmation link
- Email sign in with password
- Google sign-in via Supabase OAuth provider
Routes:
GET /auth
— UI for sign up/sign in (email + Google)GET /confirm
— Callback to exchange the authcode
for a session
Supabase module reads runtime config from runtimeConfig.public.supabase
(wired to SUPABASE_PROJECT_URL
and SUPABASE_API_KEY
).
- Auth > URL Configuration:
- Add Redirect URLs:
http://localhost:3010/confirm
(dev) and your productionhttps://your-domain/confirm
.
- Add Redirect URLs:
- Auth > Providers > Google:
- Click Enable and follow instructions to set the Google client ID/secret in the Supabase dashboard.
- You do NOT need
GOOGLE_OAUTH_ID
/GOOGLE_OAUTH_SECRET
in this app when using Supabase-managed Google OAuth.
-
Copy vars:
cp sample.env .env
and fill values. -
Install deps and start:
npm install npm run dev # http://localhost:3010
-
Visit
/auth
to sign up/sign in. After confirming email or Google OAuth, you will be redirected to/confirm
and then to/
.
We store newsletter signups in public.newsletter_subscribers
via the API route POST /api/newsletter/subscribe
.
Migration file added:
supabase/migrations/20250913_154251_newsletter_subscribers.sql
This migration:
- Creates the
newsletter_subscribers
table withid
,email
,created_at
. - Adds a case-insensitive unique index on
email
. - Enables RLS and allows
anon
inserts (so the API route can upsert with the anon key). - Blocks
anon
selects by default.
- Open your Supabase project > SQL Editor.
- Paste the contents of
supabase/migrations/20250913_154251_newsletter_subscribers.sql
and run. - Verify the table exists under
Table Editor
.
Prereqs: Supabase CLI installed and logged in.
# install (choose one)
npm i -g supabase
# or: brew install supabase/tap/supabase
# authenticate in your terminal
supabase login
# link your project (use the project ref from SUPABASE_PROJECT_URL)
supabase link --project-ref <your-project-ref>
# apply local migrations in ./supabase/migrations to your linked project
supabase db push
After this, the footer newsletter form will upsert emails into public.newsletter_subscribers
.
This repo includes a docker-compose.yml
configured with profiles to run three modes using the same file:
- Backend only
- Frontend only
- Full stack (backend + frontend)
Prerequisites:
- Docker Desktop 4.30+ or Docker Engine 24+
- Copy env:
cp sample.env .env
and fill at least:SUPABASE_PROJECT_URL
,SUPABASE_API_KEY
(for auth-enabled pages)BACKEND_URL
(frontend uses this; default ishttp://localhost:8000
)- Optional Convex:
CONVEX_URL
(+CONVEX_USER_BEARER
orCONVEX_DEPLOY_KEY
if needed)
Notes:
- The frontend runs
npm run dev
on port 3010 with hot reload. - The backend runs Uvicorn on port 8000 with
--reload
and excludestmp/manim_runs
from watcher. - CORS is controlled by
FRONTEND_ORIGIN
(defaults tohttp://localhost:3010
).
Profiles and commands:
-
Backend only
docker compose --profile backend up --build # API: http://localhost:8000
-
Frontend only (expects a reachable backend at BACKEND_URL)
# Ensure .env has BACKEND_URL set (defaults to http://localhost:8000) docker compose --profile frontend up --build # App: http://localhost:3010
-
Full stack (backend + frontend)
docker compose --profile full up --build # Frontend: http://localhost:3010 # Backend: http://localhost:8000
Environment variables used in Docker:
-
Frontend container
BACKEND_URL
is injected into Nuxt runtime config (seenuxt.config.ts
runtimeConfig.public.backendUrl
).- Default in compose is
http://localhost:8000
so the browser can reach the backend mapped on the host.
- Default in compose is
SUPABASE_PROJECT_URL
,SUPABASE_API_KEY
are forwarded to the Nuxt module.
-
Backend container
FRONTEND_ORIGIN
defaults tohttp://localhost:3010
(CORS allowlist).FRONTEND_ORIGINS
can accept a comma-separated list for multi-origin dev.- Convex envs (
CONVEX_URL
,CONVEX_USER_BEARER
,CONVEX_DEPLOY_KEY
) are forwarded as-is. - Manim tuning envs are supported (see below) but Docker-in-Docker is off by default in compose.
Development volumes:
- The entire repo is mounted into both containers at
/app
for hot reload. - A named volume
frontend_node_modules
prevents host/guest permission issues and speeds up installs.
Manim in Docker (video generation):
- The backend can attempt to run Manim via Docker or locally. Inside a container you typically cannot spawn nested Docker.
- For compose usage, set the following in
.env
:MANIM_ENABLE_DOCKER=0 MANIM_ENABLE_LOCAL=1 MANIM_LOCAL_FIRST=1 # Optionally set MANIM_LOCAL_PYTHON or MANIM_LOCAL_BIN if you bake Manim into the image
- If you do want Docker-based Manim, mount the host Docker socket (advanced, not recommended for general dev):
You may also need to install docker CLI in the backend image.
services: backend: volumes: - /var/run/docker.sock:/var/run/docker.sock
Stop & clean up:
# Stop containers
docker compose --profile full down
# Remove volumes if needed (clears node_modules cache volume)
docker volume rm cursly_frontend_node_modules || true
All backend code lives in backend/
.
Endpoints used by the Teacher Hub UI:
GET /courses
— Returns an array of Course objects (see schema below)POST /courses
— Creates a new course with{ title }
GET /courses/{course_id}
— Returns CourseDetail including optional metadata and modulesPATCH /courses/{course_id}
— Updates course basics (title, status, description, etc.)GET /courses/{course_id}/modules
— Lists modules for a coursePATCH /courses/{course_id}/modules/{module_id}
— Upserts a module (title, text, outline, etc.)GET /stats
— Returns dashboard stats with recent activity
Auth note: Every Teacher Hub request must include the current Supabase session token (
Authorization: Bearer <access_token>
). The backend derives the user ID from that token and only returns courses/modules owned by that teacher.
Course object schema:
{
"id": "string",
"owner_id": "string",
"title": "string",
"progress": 0,
"created_at": "ISO 8601",
"updated_at": "ISO 8601",
"status": "draft|published"
}
python3 -m venv .venv && source .venv/bin/activate
pip install -r backend/requirements.txt
# Exclude Manim temp output to prevent reload loops during /ai/build
uvicorn backend.app.main:app --reload --port 8000 --env-file .env --reload-exclude tmp/manim_runs
Frontend expects BACKEND_URL
(defaults to http://localhost:8000
).
Tip: set MANIM_TMP_DIR
in .env
to a path outside the repo, e.g. ~/.cache/cursly/manim_runs
, so Manim compiles don’t trigger the dev reload.
Convex HTTP API docs: https://docs.convex.dev/http-api/
Set CONVEX_URL
in .env
to your deployment (e.g., https://abc-123.convex.cloud
). The backend will call:
courses:list
(query)courses:create
(mutation)stats:get
(query)
If CONVEX_URL
is not set, the backend falls back to an in‑memory list so you can try the UI immediately.
See backend/README.md
for detailed examples and curl commands.
This repo includes a minimal Convex app in convex/
implementing the functions the backend expects:
courses:list
(query)courses:create
(mutation)courses:get
(query) — fetch a course by publicid
with detailed fieldscourses:updateBasic
(mutation) — update basic fields (title/status/metadata)courses:createDetailed
(mutation)courses:updateProgress
(mutation)courses:finalize
(mutation)modules:listByCourse
(query)modules:upsert
(mutation)stats:get
(query)files:generateUploadUrl
(action)
Schema is defined in convex/schema.ts
with courses
and modules
tables (and indexes used by functions).
Setup and deploy:
# 1) Install dependencies (adds Convex CLI)
npm install
# 2) Initialize Convex (login/select or create a project)
npm run convex:dev
# This starts a local dev server and creates project config; keep it running in another terminal
# 3) Deploy functions & schema to your Convex project
npm run convex:deploy
# 4) Copy envs and set your deployment URL so the backend can call Convex
cp sample.env .env
# Edit .env and set:
# CONVEX_URL="https://<your-space>.convex.cloud"
Verify connectivity:
- Start the backend:
uvicorn backend.app.main:app --reload --port 8000 --env-file .env --reload-exclude tmp/manim_runs
- Open
GET /convex/diagnostics
— it should showquery_ok: true
andrun_ok: true
once deployed. - Try
GET /courses
,POST /courses
, and the new detail routesGET /courses/{id}
,PATCH /courses/{id}
from the UI or curl.
- Navigate to
Teacher Hub → Courses
and click a row to open/app/courses/{id}
. - Use the pencil icon in the top-right to edit title, status, and metadata.
- In the Modules section, use the per-row pencil to edit module title/text. Additional fields (outline/manim/media) can be added similarly.
- Static pre-render is enabled via
routeRules
and works well on:- Vercel (recommended)
- Netlify
- Any static hosting (after build)
- For SSR features, adjust
routeRules
in nuxt.config.ts.
Auth routes (/auth
, /confirm
) are excluded from prerender to enable dynamic auth flows.
- “nuxt: command not found” when running
npm run dev
:- Run
npm install
in the project root first (this installs the local nuxt binary used by npm scripts).
- Run
npm ERR! code ETARGET @nuxt/ui@^4.0.0
:- Use a resolvable version (this project sets
@nuxt/ui
tolatest
). - Optionally run:
npm cache verify
and try again.
- Use a resolvable version (this project sets
- Icons not appearing:
- Use
UIcon
withname="i-lucide-..."
orlucide:...
. Ensure@nuxt/ui
is installed and listed undermodules
in nuxt.config.ts.
- Use
- Nitro dev error
Error: spawn EBADF
onnpm run dev
:- This often occurs on Node 22. Use Node 20 LTS. We include
.nvmrc
(20.18.0
) so you can runnvm use
. - Alternatively, set
engines.node
to Node 20 in your environment or switch via your version manager.
- This often occurs on Node 22. Use Node 20 LTS. We include
- Wire “Try Demo” to a live demo.
- Add waitlist integration (modal/API).
- Add analytics, cookie consent, and legal pages.
- Add user dashboard for course creators (requires authenticated area).
- Supabase documentation is indexed for fast lookup via NIA MCP: https://supabase.com/docs
- Ask the AI assistant for examples (e.g.,
exchangeCodeForSession
,signInWithOAuth
,emailRedirectTo
) and it can pull code-level references quickly.