Skip to content

Latest commit

Β 

History

1,437 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Unit Tests Playwright Tests

Version node npm License: AGPL-3.0 Contributions Welcome Powered by Vercel

Open Issues Good First Issues Open Pull Requests Contributors Last Commit

VWC Logo

Welcome to Vets Who Code Web App πŸŽ‰

Hey there, Soldier! Welcome to the Vets Who Code Web App. This project serves as a communal code base where military vets and their spouses can sharpen their coding skills. πŸš€

What's Under The Hood 🧰

This app is built using a modern tech stack including:

  • Next.js 15 (Pages Router)
  • TypeScript
  • Tailwind CSS
  • Prisma + Postgres
  • NextAuth (GitHub OAuth)
  • MDX for content
  • Vitest and Playwright for testing

Our Mission 🎯

  1. Empower Veterans and Military Spouses: We're creating a production-grade app that addresses the unique needs of our community.

  2. Ever-Evolving Platform: New features are continuously added to provide valuable tools for our users.

  3. Learn By Doing: The project serves as a hands-on experience for our community to learn and grow their coding skills.

Getting Started πŸš€

To get a local copy up and running, you'll need a few things installed on your machine.

Prerequisites πŸ› οΈ

  • Git
  • Node.js v20 (see .nvmrc)
  • NVM
  • npm β€” the only supported package manager; preinstall rejects yarn, pnpm, and bun

Installation Steps πŸ”§

Fire up your terminal and run:

$ git clone https://github.com/Vets-Who-Code/vets-who-code-app.git
$ cd vets-who-code-app
$ nvm use
$ npm install
$ npm run dev

Navigate to http://localhost:3000/ to see the app in action.

Environment Variables πŸ”

Copy the template, then fill in the values you need:

$ cp .env.example .env.local

Set DATABASE_URL to a Postgres server you can reach before you bootstrap the database β€” the schema's provider is postgresql, so a SQLite file: URL is rejected outright. Run one locally, or point at a free Neon branch:

# one way to get a local Postgres β€” skip if you are using Neon
$ docker run --rm -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=vwc_dev -p 5432:5432 postgres:16-alpine
# DATABASE_URL="postgresql://postgres:postgres@localhost:5432/vwc_dev"

# then, with DATABASE_URL set in .env.local:
$ npm run dev:setup   # first-time database bootstrap (prisma generate && prisma db push)

.env.local and .env are gitignored β€” never commit them. .env.example is the template and the list of every variable the app reads; each entry there is annotated with whether it is required, whether it is a secret, and which file consumes it.

Required

Variable Purpose
DATABASE_URL Prisma connection string (prisma/schema.prisma). Must be postgresql:// β€” the datasource provider is postgresql, so a file: SQLite URL fails validation with P1012.
NEXTAUTH_SECRET Session encryption key. Generate one with openssl rand -base64 32.
NEXTAUTH_URL Base URL of the app. http://localhost:3000 in development.
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET GitHub OAuth app credentials. Create an app at github.com/settings/developers with the callback URL http://localhost:3000/api/auth/callback/github.
GITHUB_ORG GitHub organization that gates sign-in. Membership is the sole login gate β€” there is no allowlist and no dev bypass.

/api/health reports the environment as unhealthy when DATABASE_URL, NEXTAUTH_SECRET, or NEXTAUTH_URL is missing.

Optional, by feature

Every variable below is optional. The feature that reads it stays off, or falls back to a default, when it is unset.

Feature Variables
AI assistant and content scripts PRIMARY_AI_PROVIDER, GOOGLE_GENERATIVE_AI_API_KEY, GEMINI_API_KEY, GOOGLE_PRIVATE_KEY, GEMINI_MODEL, TECH_PATHWAYS_MODEL, AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT, OPENAI_API_KEY, PHI3_ENDPOINT, PHI3_API_KEY
J0dI3 AI backend J0DI3_API_URL, J0DI3_API_KEY
Slack form notifications APPLY_WEBHOOK_ID, CONTACT_WEBHOOK_ID, MENTOR_WEBHOOK_ID
GitHub API reads (org, repos, PRs) GITHUB_TOKEN
Cloudinary media NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET
Email (Resend) RESEND_API_KEY, EMAIL_FROM
Shopify commerce SHOPIFY_STORE_DOMAIN, SHOPIFY_STOREFRONT_ACCESS_TOKEN, SHOPIFY_WEBHOOK_SECRET (or SHOPIFY_API_SECRET / SHOPIFY_CLIENT_SECRET)
Labor-market data LIGHTCAST_CLIENT_ID, LIGHTCAST_CLIENT_SECRET, CENSUS_API_KEY
Public site config NEXT_PUBLIC_GOOGLE_ANALYTICS_ID, NEXT_PUBLIC_COHORT_START_DATE, NEXT_PUBLIC_SITE_URL
Database seeding ALLOW_DESTRUCTIVE_SEED β€” "true" lets npx prisma db seed wipe a non-local database (prisma/seed-guard.ts)

Development vs production

  • NEXTAUTH_URL is http://localhost:3000 locally and the deployed origin in production.
  • DATABASE_URL is Postgres in both environments β€” your own local or Neon branch in development, the project's Neon database in production.
  • Production values live in the Vercel project settings, not in any file in this repo.
  • NEXT_PUBLIC_* values are inlined into the browser bundle at build time. Never put a secret behind that prefix.

Development using Dev Container (Optional) 🐳

We support development containers for an easier setup experience.

Requirements

Steps

  1. Clone the repository

    git clone https://github.com/Vets-Who-Code/vets-who-code-app.git
    cd vets-who-code-app
  2. Open in VS Code

    • Open the root directory in VS Code.
    • When prompted, choose "Reopen in Container"
    • Or use Command Palette (F1) and run Remote-Containers: Reopen in Container.
  3. Start Developing

    • Once the container is built and running, you're ready to code!

Remember, this is optional. If you prefer to set up your development environment manually, you can continue to do so.

Blog Media πŸ–ΌοΈ

Blog headers, audio overviews, and inline graphics are generated from the post's markdown and uploaded to Cloudinary. The media files never live in git; the post references them by Cloudinary path.

Every script reads NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, and GEMINI_API_KEY from .env.

Header image and audio

npm run generate:blog-media <slug>   # header image + audio overview in one run
npm run generate:blog-image <slug>   # header image only

The header lands at blog-images/<slug>; reference it in the post's front matter as image.src: "blog-images/<slug>.png". The audio lands at blog-audio/<slug>.wav, and the post page picks it up by slug.

How the header image is made

  • Scrapes/reads the blog markdown file in system blog folder.
  • Blog title, content and summary are returned to inject in a dynamic prompt that is given to Google Gemini.
  • Gemini returns JSON that becomes the prompt for a Gemini image model, which builds the image.
  • After the image is generated it is uploaded to Cloudinary into the blog-images folder.

Inline graphics

Inline graphics are HTML artboards styled by src/data/blog-graphics/_brand.css, rendered to PNG with Playwright, then uploaded.

mkdir -p src/data/blog-graphics/<slug>
npm run generate:blog-graphic <slug> -- --draft <name> "<what the graphic should say>"  # Gemini drafts <name>.html
npm run generate:blog-graphic <slug> -- --dry   # render PNGs to out/ for review, no upload
npm run generate:blog-graphic <slug>            # render and upload every graphic in the folder

Edit the drafted HTML until the --dry render reads right, then upload. Embed each one in the post as ![alt text](blog-graphics/<slug>-<name>). The per-post folders are gitignored: the sources stay on your machine and only the PNGs are published.

Audio staging

public/audio/blogs/ is a local staging directory β€” it is gitignored and never committed. npm run generate:blog-audio writes WAVs there on the way to Cloudinary; the site itself always plays from Cloudinary (see src/lib/blog.ts), and .vercelignore keeps the directory out of deploys. The newer per-post script (npm run generate:blog-media) streams straight to Cloudinary and does not use this directory at all.

Code Quality 🧹

Biome is the only linter and formatter β€” there is no ESLint or Prettier config.

npm run typecheck   # tsc, no emit
npm run lint        # Biome lint, no writes
npm run lint:fix    # typecheck, then biome check --write
npm run format      # Biome format only
npm run check       # biome ci β€” what to run before pushing

A pre-commit hook runs biome check --write on staged files, and a commit-msg hook enforces Conventional Commits.

Testing πŸ§ͺ

Unit and component tests run on Vitest. End-to-end tests run on Playwright.

Running Unit Tests

npm test                                           # run every unit and component test once
npx vitest                                         # watch mode
npx vitest run src/utils/__tests__/string.test.ts  # a single file
npx vitest run -t "falls back to the post title"   # a single test, matched by name
npx vitest run --coverage                          # with a coverage report

npm test is an alias for vitest run.

Test Structure

__tests__/                 # mirrors src/: api, components, data, lib, pages, prisma, scripts, utils
src/**/__tests__/          # co-located: src/hooks, src/lib/interactive-lessons,
                           #             src/lib/lesson-sandbox, src/utils
tests/                     # Playwright only β€” excluded from Vitest
  a11y/*.spec.ts           # axe-core WCAG A/AA scan of public routes (chromium only)
  e2e/*.spec.ts
  security/*.spec.ts

Vitest picks up __tests__/**/*.{test,tests}.{ts,tsx} and src/**/__tests__/**/*.{test,tests}.{ts,tsx}. The top-level tests/ directory is excluded from Vitest and belongs to Playwright, whose specs are named *.spec.ts.

Writing Tests

House style, set by vitest.config.mts and vitest.setup.ts:

  • globals: true is on, so do not import describe, it, or expect.
  • DOM matchers come from vitest-dom, so toHaveAttribute and toBeInTheDocument work with no extra import.
  • Import through the path aliases (@components/..., @lib/...), never deep relative paths across feature boundaries.
  • Mock with vi.fn() and vi.mock().
  • For interactions, use fireEvent from @testing-library/react.

A component test (__tests__/components/blog-card.test.tsx):

import BlogCard from "@components/blog-card/blog-03";
import { render, screen } from "@testing-library/react";

const PROPS = {
    path: "/blog/combat-to-code",
    title: "From Combat to Code",
    category: { title: "Career", slug: "career", path: "/blog/category/career" },
    postedAt: "Jan 1, 2026",
};

describe("BlogCard (blog-03)", () => {
    it("falls back to the post title for the image alt when none is provided", () => {
        render(<BlogCard {...PROPS} image={{ src: "/images/combat-to-code.png" }} />);

        expect(screen.getByRole("img")).toHaveAttribute("alt", "From Combat to Code");
    });
});

A utility test (src/utils/__tests__/string.test.ts):

import { capitalize } from "../string";

describe("capitalize", () => {
    it("capitalizes each word", () => {
        expect(capitalize("hello world")).toBe("Hello World");
    });
});

Coverage

No thresholds are enforced, but we aim for:

Metric Target
Statements 70%+
Branches 60%+
Functions 70%+
Lines 70%+

Generate and open the HTML report:

npx vitest run --coverage
open coverage/index.html

End-to-End Tests

npx playwright install                                    # first time only
npx playwright test                                       # every spec
npx playwright test --project=chromium                    # one browser
npx playwright test tests/e2e/interactive-lesson.spec.ts  # one file

Projects defined in playwright.config.ts: chromium, firefox, Mobile Chrome, Microsoft Edge, Google Chrome.

The Microsoft Edge and Google Chrome projects drive system-installed branded browsers, which plain npx playwright install does not download β€” a bare npx playwright test fails those two projects with Chromium distribution 'msedge' is not found. Either run npx playwright install msedge chrome first, or stay on the bundled browsers:

npx playwright test --project=chromium --project=firefox --project="Mobile Chrome"

Locally, Playwright runs npm run build && npm run start before the specs, so the first run takes a couple of minutes. Specs that need Shopify or NextAuth credentials skip themselves when those environment variables are missing.

Best Practices

  • Add tests with new behavior, and a regression test with every bug fix.
  • Test observable behavior β€” rendered output, state changes, API responses β€” not internals.
  • Keep one assertion focus per test.
  • Give tests descriptive names that read as sentences.
  • Mock external services with vi.mock(); never call a real API from a unit test.
  • For API routes, cover success, validation failure, and external-service failure.

Continuous Integration

Both suites run on every pull request to master. .github/workflows/vitest.yml runs npx vitest run --reporter=verbose, and .github/workflows/playwright.yml runs a chromium + firefox matrix. The tests/a11y axe scan runs on the chromium leg only and skips itself on every other project, so an accessibility violation on a public route fails the test (chromium) check with the route, rule id and selector in the log.

Resources

Project Docs πŸ“š

  • AGENTS.md β€” architecture, where new code goes, path aliases, auth guards, and conventions. Written for AI coding agents, and the fastest orientation for humans too.
  • docs/ β€” deep dives: design system, brand style guide, database, deployment, Shopify, email, and more.
  • /api-docs β€” Swagger UI for every API route, generated at build time from @swagger JSDoc blocks. The raw spec is served at /api/docs.

Contributing 🀝

We love contributions! Please read our Contributing Guidelines to get started.

Roadmap πŸ—ΊοΈ

Curious about upcoming features? Check our Roadmap.

License πŸ“œ

This project is under the GNU Affero General Public License v3.0 β€” see the LICENSE for details.


Made with β™₯ by veterans, for veterans

Releases

Sponsor this project

Packages

Used by

Contributors

Languages