Skip to content

feat(academy): add the chapter-wise worksheet site - #350

Merged
kartikeya-27 merged 8 commits into
mainfrom
feat/academy
Sep 19, 2026
Merged

kartikeya-27 merged 8 commits into
mainfrom
feat/academy

Conversation

@pratyush618

@pratyush618 pratyush618 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Adds apps/academy — a fifth app for academy.byteveda.org, built from the Design-Claude prototype in ../Assignment delivery platform design/. It sells chapter-wise CBSE and ICSE worksheets for Class 9 and 10: a stocked inventory of 36 chapters with answer keys, and a made-to-order quote for anything not on the shelf.

The prototype's structure is the spec; its mint-on-green palette is not. The app uses the shared Midnight tokens from @byteveda/ui so it reads as part of the family.

Buying is not open yet

Payment is deliberately unbuilt. Rather than ship a dead Pay button, the request is for a free sample sheet — the visitor picks a chapter, leaves an email, and gets the sheet back by hand within 24 hours. Every place the prototype printed a total now prints a "coming soon" notice instead: under the inventory bar, and on /sample.

No rupee figure appears anywhere in the request flow — not on the line, not on the submit button, not in the receipt mail. Prices stay where they inform rather than charge: the inventory table, where the list figure is struck through beside Free, and the live quote panel. The one exception is the team's work order, which keeps the figure under a "List value" heading, because the person filling the request is the only reader who wants it.

One free sample per email address

A sample costs nothing, so the email address is the whole of the price. academy.sample_requests in @byteveda/db is both the record of what was sent and the lock that stops the catalogue being collected one free sheet at a time.

  • Its own Postgres schema, not public. Nothing in academy references the blog or console tables and nothing references it. pgSchema("academy") says so in the one place that cannot drift.
  • The unique constraint is the rule. hasClaimedSample runs first only to turn the common case into a sentence; two requests racing each other both pass it, and the insert decides which is second. claimSample reads Postgres 23505 as "already had one".
  • Claim, send, release on failure. Sending first hands a second sheet to anything that arrives mid-flight. Claiming first alone would let a Resend outage permanently spend someone's sample, so a work order that never lands releases the row again.
  • No DATABASE_URL → 503. The cap cannot be enforced without it, and being briefly offline beats giving the catalogue away.
  • The request is one line, not a list: ResolvedOrder.line, a single-select store, and validateOrder refusing a second item. The table says so too — Pick becomes Swap once something is chosen.

Migration 0007_academy_samples is additive: one new schema, one enum, one table. Nothing existing is touched.

Prices never cross the wire

The browser posts {kind: "chapter", chapterId} or {kind: "custom", request} and never a price. validateOrder re-derives every rupee server-side from the same pure functions the quote panel uses, so there is one pricing rule and a tampered payload cannot buy a chapter pack for one rupee. That stays true even though nothing is charged today — it is the rule that has to be right on the day payment opens, not something to wire up then. An e2e test asserts the exact request body.

Layout

  • / — hero, inventory table with filters and paging, made-to-order form with a live quote, delivery steps.
  • /sample — the request: the picked chapter, email, send. Its own route rather than a modal, so the pick is persisted to sessionStorage and survives a reload or a pasted link. Not localStorage: a pick resurrected next week would name a catalogue that has moved on. It is re-validated through parseCartItem on restore, and a ready flag stops the page flashing "nothing picked yet" at someone who has already chosen.
  • POST /api/orders — validates, records the claim, then mails a work order to ACADEMY_ORDER_INBOX and a price-free receipt to the visitor through Resend.

The vocabulary is split on purpose: src/features/sample/* is the UI, src/lib/orders/* is the server-side work order that comes back into its own when payment lands.

src/components/ui/* (field, input, listbox select, segmented radio group) are app-local because @byteveda/ui has no form primitives. Promoting them needs a rename first — .input and .field would collide with apps/admin. The select is a custom combobox over a listbox rather than a native <select>, whose option popup ignores data-theme and renders unreadable on dark.

Full-bleed bands keep their contents in the page's column. The made-to-order band and the three-up are the only sections whose rules reach the screen edges, and they were bleeding their contents too — on a wide screen the form began 331px left of the nav, the inventory and the footer. A .band-inner capped at --maxw fixes it, and an e2e test measures both ends against .inventory-head, which is a plain .wrap and therefore is the column.

A new brand mark

Wordmark in @byteveda/ui swaps the bracket glyph for the leaf logo, so all five sites pick it up. No tile behind it — the bracket was a white glyph that needed the accent square to sit on; this one brings its own colour.

The supplied artwork had 432,046 pixels at alpha 250–254 and only 1,103 at 255, so the whole mark was faintly translucent and picked up whatever sat behind it. packages/ui/brand/ holds the original untouched and records how the shipped asset was derived from it.

Worth a second opinion: the mark averages 3.10:1 against the dark background and 5.87:1 against the light one. It clears the 3:1 WCAG asks of a graphical object, but the dark stem does sink into the dark theme and only the pale leaves carry it. apps/admin's rail, login and email pages draw their own .mark tile and never went through Wordmark, so they still show the bracket.

Notes for review

  • Ports: dev 3003, Playwright 3014, Lighthouse 3103.
  • RESEND_API_KEY and DATABASE_URL are both required at runtime; without either the route returns 503 rather than silently dropping the request or handing out a second sample. ACADEMY_ORDER_FROM and ACADEMY_ORDER_INBOX are optional overrides, declared in turbo.json's passThroughEnv.
  • The academy project needs the transaction-mode pooler (:6543). :5432 is session mode and is for db:migrate only.
  • Still to do: a Vercel project rooted at apps/academy, the academy CNAME, and the sender domain verified in Resend.
  • Follow-ups, not here: Razorpay (order id, signed webhook, payment state machine — apps/admin/src/lib/email/webhook.ts is the pattern).

Verification

22 unit tests in apps/academy, 31 in @byteveda/db, 26 Playwright tests (desktop plus Pixel 7 and iPhone 14), Biome clean, all 22 turbo tasks green. / and /sample both prerender static. Screenshotted in both themes.

The claim layer was also exercised against the real database — first claim, repeat claim under a different case, release — which is how the 23505 handling turned out to be wrong: drizzle wraps the pg error in a DrizzleQueryError, so a check against the top-level code never matched and "you already have one" surfaced as a 500. isUniqueViolation now walks the cause chain, and a unit test pins both shapes.

@vercel

vercel Bot commented Sep 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
byteveda-site Ready Ready Preview Sep 19, 2026 1:33pm UTC
byteveda-site-admin Ready Ready Preview Sep 19, 2026 1:33pm UTC
byteveda-site-flexiq Ready Ready Preview Sep 19, 2026 1:33pm UTC

academy.byteveda.org joins the canonical list, so the new app derives its
site config the same way main and docs do.
A fifth app at academy.byteveda.org: a stocked inventory of CBSE and ICSE
worksheets for Class 9-10, plus a made-to-order quote for chapters we do
not hold.

Buying is not open yet, so the request is for a free sample rather than a
purchase, and no price appears in it. The browser posts what was asked
for and never a rupee; the server re-prices from the same functions the
quote panel uses, and mails the work order through Resend.
Installs browsers when the academy test task is affected, collects its
Playwright report, and adds it to the Lighthouse matrix.
Nothing in academy references the blog or console tables and nothing
references it, so it gets a namespace rather than a corner of public.

The unique constraint on the email is the one-free-sample rule. The
lookup beside it is only there to turn the common case into a sentence;
two requests racing each other both pass it.
A bitmap rather than an inline SVG: the mark is a multi-tone gradient,
so it cannot take currentColor. No tile behind it either — the bracket
was a white glyph that needed one, this one brings its own colour.

The supplied file had 432,046 pixels at alpha 250-254 and 1,103 at 255,
so the whole mark was faintly translucent; packages/ui/brand holds the
original untouched and records how the asset was derived from it.
The closing section repeated the hero's heading and both its buttons,
and the 01/02/03 three-up said what the delivery steps say lower down.
With no payment the address is the whole of the price, so the request
is capped at one chapter and the server re-checks it: the page is a
single-select, validateOrder refuses a second item, and the claim is
taken before the mail goes out and released again if it fails.

The table says so too — Pick becomes Swap once something is chosen, and
the price column shows the list figure struck through beside Free.

Carries the layout fixes that came with it: the full-bleed bands keep
their contents in the page's column, and the quote panel is narrower
and tighter than the form beside it.
next-env.d.ts carries the next/image-types reference, but it is
generated by next build and gitignored — so a typecheck that runs
before any build, which is what CI does, had no idea what a PNG was.
The reference belongs in the file that imports one.
@kartikeya-27
kartikeya-27 merged commit 3755f5f into main Sep 19, 2026
15 checks passed
@kartikeya-27
kartikeya-27 deleted the feat/academy branch September 19, 2026 13:43

This branch was successfully deployed

3 active deployments
Preview – byteveda-site-admin 20b03eb5 Deployed Sep 19, 2026 by vercel[bot]
Preview – byteveda-site 20b03eb5 Deployed Sep 19, 2026 by vercel[bot]
Preview – byteveda-site-flexiq 20b03eb5 Deployed Sep 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants