DEV Community

Cover image for I've Been Building an LMS for a Year. Finish-Up-A-Thon Made Me Actually Finish It.
Anuforo Okechukwu Deede
Anuforo Okechukwu Deede

Posted on

I've Been Building an LMS for a Year. Finish-Up-A-Thon Made Me Actually Finish It.

GitHub “Finish-Up-A-Thon” Challenge Submission

This is a submission for the GitHub Finish-Up-A-Thon Challenge

What I Built

Training Theatre (TTT) is a multi-tenant LMS where instructors can build a course in three different shapes, depending on how they teach:

  • Story — long-form written content for instructors who teach the way essayists write.
  • Video — the conventional LMS path everyone already knows: upload a video, chapter it, ship it. The Udemy/Coursera shape.
  • Slide — composable decks built from a catalog of 20+ templates. This is where most of TTT's design budget goes, and it's the part of the product that doesn't look like every other LMS.

Before a slide is ever built, they walk through a setup flow: what they're building (a standalone course or a bundled program), how the course is structured (multi-format modules or a repeating pattern), and which teaching pattern fits their content. By the time they land in the slide editor, the course shape is already decided.

On the slide side, the workflow goes like this: an instructor opens the slide editor, picks a layout from the catalog (hero-text, three-column, comparison, team, closing-cta, and so on), and lands on a canvas. Each template exposes a set of named zones — a headline zone, an items zone, a cta zone, whatever the layout needs — and each zone declares which section types it accepts (text, list, image, callout, quote, stats, table) and how many sections it can hold. The instructor fills the zones, the editor persists the result as a SlideGroup row (template choice, zone assignments, background, accent color, speaker notes), and the learner sees that same data render through SlideCanvas inside LearnerSlideDeck. The whole catalog is declarative — adding a new template means defining its zones, building its editor canvas, and building its learner renderer.

The stack is Next.js (App Router) + TypeScript + Prisma/PostgreSQL, with Zustand and React Query on the frontend, Tiptap for rich text, and Tailwind for styling.

TTT is also my first real-world professional project. I've been building it alongside my 9-5 work as a technical support — for over a year now, and most of what I currently know about shipping software I learned by getting it wrong here first: the bad schema choices I had to migrate out of, the audit logging I retrofitted across the codebase, the moderation flow I rebuilt twice. So finishing the half-done parts isn't just about cleaning up the codebase — it's about turning TTT into the portfolio piece I can confidently point to when I'm pitching for contracts or interviewing for a full-time engineering role. Finish-Up-A-Thon was a deadline I was glad to have.

For the challenge, I focused on the slide template system — the part of the product I'm proudest of. The goal was to give instructors more layout options to design with, so I worked on three templates: closing-cta, feature-list, and agenda-grid. Some were new builds, some were modifications to existing templates to support layout patterns the original implementations couldn't handle.

Demo

🔗 Project: Training Theatre on GitHub

🔗 Live URL: Training Theatre

Before & After — Slide Templates

Before After
Templates Before Templates After
Config Schema Before Config Schema After

🎥 Video Walkthrough

The Comeback Story

The shape of the problem

TTT's slide system has an opinionated architecture. Every template is a triple:

  1. A config schema that declares the editable zones (headline, body, items, icons, tags, CTA, etc.) and feeds the editor's PerItemConfigSlot so each item gets its own inline controls.
  2. An editor canvas that renders the same component the learner will see, but wired to the Zustand store so edits are live.
  3. A learner renderer consumed by SlideCanvas inside LearnerSlideDeck, with preserveAspect for desktop, single-column stack on mobile, and accent/background tokens pulled from getSlideColors(group).

All three of the templates I worked on had to fit this triple pattern. For some, that meant building all three pieces fresh; for others, it meant going back into the existing config schema or renderer and extending it to support layout shapes the original implementation hadn't planned for. Either way, the shape of the work was the same: zones, canvas, renderer, ship.

What I built for each

closing-cta — A closing slide built around three zones: a two-part headline (main statement + subhead), an optional call-to-action with an instructor-set button label, and optional contact info that can render as a single line or a list of items. The kind of slide a course ends on when the instructor wants the learner to enroll in the next thing, book a session, or just walk away with a way to reach them.

feature-list — An image panel alongside a scrollable vertical list of 3–8 feature rows, each with an icon, a heading, a short body, and pill tags for stats or labels. The right pick when an instructor wants to walk through a set of features, plan inclusions, or capabilities and has a screenshot or visual to anchor the slide. It sits between feature-grid (which tiles features into a grid) and split-image (which works for mixed prose rather than uniform itemized rows).

agenda-grid — A denser cousin of the existing agenda template. Where agenda is a vertical list, agenda-grid is a card grid: up to 8 agenda items, each card carrying a topic heading, a description, and a list of meta lines (durations, presenters, room numbers — whatever the instructor needs). It also exposes a header-bar title and a dual footer (left and right text), so it works equally well as a session agenda, a workshop schedule, or a module table-of-contents.

Each one slots into the existing system without special-casing: the editor picks them up via the registry, the moderation preview renders them via the same SlideCanvas, the learner deck navigates through them with the same keyboard handlers, and the mobile breakpoint collapses them the same way every other multi-column template collapses.

Why this is the finish

The catalog was solid but had thin spots — places where instructors were forcing a layout that didn't quite fit what they were trying to say. There wasn't a clean way to end a course on a strong call-to-action, the feature list couldn't carry the kind of structured content people were trying to put in it, and there was no good dense-grid layout for agendas with a lot of items. These three templates close those gaps. Instructors have more shapes to design with now, and that's all I wanted from this work.

My Experience with GitHub Copilot

I used Copilot in two places for this: Copilot Web for generating the new templates, and the Copilot VS Code extension for fixing TypeScript errors as they came up. They played different roles, and the honest version of the story is that Copilot is dramatically more useful when you stop asking it to invent your architecture and start asking it to follow one you've already built.

My Copilot Web workflow looked like this:

Show, don't tell. Before asking Copilot to write feature-list, I'd paste the entire triple for a sibling template — feature-grid's config schema, its editor canvas, and its learner renderer. Copilot needs to see the pattern before it can extend it. Without the existing files in context, it would invent plausible-but-wrong conventions; with them, it tracked my naming, my zone-assignment patterns, and my PerItemConfigSlot wiring.

Scope tightly. Instead of "build the closing-cta template," I'd ask for the config schema first, review it, then the canvas, review it, then the learner renderer. Three small, well-defined slots beats one big "build me a template" prompt every time.

Iterate against the convention. When Copilot drifted — forgetting that tagsConfig needs config.tags ?? [] as a fallback, or returning a hardcoded color instead of pulling from getSlideColors, or skipping the mobile stack breakpoint — I'd quote the convention back and ask for a fix. That's much cheaper than rewriting from scratch.

The VS Code extension played a different role: TypeScript triage. TTT has its share of generic-heavy types — Prisma's generated types, Zustand stores with typed slices, discriminated unions on section types — and when something didn't compile, having an inline assistant that could read the actual error against the surrounding types saved a lot of context-switching to the browser. Most of my use there was on the order of "why is this SlideSection not assignable to that SlideSection" or "how do I narrow this discriminated union without writing five type guards by hand." Quick, targeted, in-the-flow.

The main friction across both surfaces was token usage. Pasting the full triple for a sibling template — config schema, editor canvas, learner renderer — plus the registry, plus the type definitions, plus the helper utilities, adds up fast. I burned through context faster than I expected on the first template, then learned to trim ruthlessly: just the one most-relevant sibling, just the types I'd actually reference, just the helper I was about to call. By the third template the prompts were leaner and better-targeted, and the responses got sharper too. The constraint forced better prompt discipline — a roundabout win.

What Copilot did for me was speed up the typing. I still had to figure out what each template should look like, what to make configurable, what it should do on mobile. Those weren't decisions the tool could make for me. But the boilerplate moved faster.


Top comments (0)