Structured Course Builder & PPT Outline Generator

Structured Course Builder & PPT Outline Generator

I built a browser‑only course blueprinting tool that lets instructional designers rapidly structure modules, topics, objectives, notes, and media requirements—and then export clean outlines (PPT, agenda table, CSV, objective reports) without a backend. It uses Vue + Bootstrap 4, a small in‑memory schema, and the Web File & Storage APIs for persistence and export. Autosave, validation (word counts, flagged terms, spacing), and lightweight reporting make iteration fast while keeping content consistent.

Use it here: Course Builder

Problem / Context

Early course design sessions lived in scattered spreadsheets, Word outlines, and PPT drafts. Version drift and missing objective mapping slowed reviews. Needed: a single structured workspace to (1) capture hierarchical data (modules → topics → notes/media) (2) enforce basic quality rules (word counts, flagged verbs) (3) generate multiple delivery artifacts (PPT outline, agenda table, CSV, objectives report) (4) run entirely client‑side.

Constraints / Goals

  • Zero backend: all state serializes to JSON and can download directly.
  • Rapid bulk entry: paste newline lists for modules/topics/objectives.
  • Safe iteration: autosave snapshots in localStorage + manual file/CSV export.
  • Reusable exports: same schema drives every output format.
  • Accessibility basics: keyboardable Bootstrap components, sensible heading levels, alt‑text fields.

Core Approach

State lives in one Vue data.course2 object following a normalized schema. Bulk add operations parse newline‑separated input. Validation & reporting compute on demand. Exports traverse the structure and build strings (HTML outline, CSV rows) then download via a generated data URI.

Data Schema (Excerpt)

var courseSchema = {
  title: '', client: '', duration: '',
  modules: [{
    title: '', subtitle: '', instructor: '', duration: '', description: '',
    topics: [{
      title: '', slide_text: '', participant_notes: '', instructor_notes: '',
      id_objective: -1,
      exercise: { has_exercise: false, title: '', description: '' },
      media: { details: '', alttext: '', required: [], filename: '' },
      review_question: ''
    }],
    topicTextToAdd: ''
  }],
  objectives: [{ objective_text: '' }],
  flaggedValues: 'TODO'
};

Bulk Creation (Modules & Topics)

addModules: function () {
  let tempModuleObj = JSON.parse(JSON.stringify(courseSchema.modules[0]));
  tempModuleObj.topics = [];
  let lines = this.addModulesText.split(/\r|\n/);
  this.addModulesText = '';
  lines.forEach(title => {
    title = this.validateAllCaps(title);
    if (title) {
      tempModuleObj.title = title.trim();
      this.course2.modules.push(JSON.parse(JSON.stringify(tempModuleObj)));
    }
  });
  this.showToast('Modules Created', this.course2.modules.length + ' module(s) created.', '');
},
addTopics: function (m) {
  let tempTopicObj = courseSchema.modules[0].topics[0];
  let lines = this.course2.modules[m].topicTextToAdd.split(/\r|\n/);
  this.course2.modules[m].topicTextToAdd = '';
  lines.forEach(title => {
    title = this.validateAllCaps(title);
    if (title) {
      tempTopicObj.title = title.trim();
      this.course2.modules[m].topics.push(JSON.parse(JSON.stringify(tempTopicObj)));
    }
  });
},

Objectives Management

createObjectives: function () {
  let lines = this.addObjectivesText.split(/\r|\n/); let added = 0;
  lines.forEach(text => {
    text = this.validateAllCaps(text);
    if (text) { added++; this.course2.objectives.push({ objective_text: text.trim() }); }
  });
  this.addObjectivesText = '';
  this.showToast('Objectives Created', added + ' objective(s) created.', '');
},
deleteObjective: function (i) {
  this.course2.modules.forEach(module => {
    module.topics.forEach(topic => {
      if (topic.id_objective === i) topic.id_objective = -1;
      else if (topic.id_objective > i) topic.id_objective -= 1;
    });
  });
  this.course2.objectives.splice(i, 1);
  this.showToast('Objective Deleted', '1 objective deleted.', '');
},

Validation & Quality Flags

validateText: function (val, desired) {
  let count = this.wordCount(val), valid = true, msg = '';
  if (count < desired.min || count > desired.max) { valid = false; msg += 'Word Count out of range.'; }
  if (/[^^\S\r\n]{2,}|[\n]{2,}/.test(val)) { valid = false; msg += ' Extra spacing.'; }
  this.course2.flaggedValues.split(/,/).forEach(flag => {
    if (flag && new RegExp(flag.trim(), 'im').test(val)) { valid = false; msg += ' Contains ' + flag; }
  });
  return { message: msg, valid };
},

Word Count Reporting

wordCountByType: function (key) {
  let data = [];
  this.course2.modules.forEach(m =>
    m.topics.forEach(t => { if (key in t) data.push(this.wordCount(t[key])); })
  );
  if (!data.length) return { min:0, max:0, avg:0, sd:0 };
  data.sort((a,b)=>a-b);
  const mean = data.reduce((a,b)=>a+b,0)/data.length;
  const sd = Math.sqrt(data.reduce((sq,n)=> sq + Math.pow(n-mean,2),0)/(data.length-1));
  return { min:data[0], max:data[data.length-1], avg:mean.toFixed(1), sd:sd.toFixed(1) };
},

Autosave & Persistence

saveBeforeLeaving: function () {
  window.addEventListener('beforeunload', () => {
    const d = new Date();
    const stamp = [
      d.getFullYear(), d.getMonth()+1, d.getDate(), '_',
      d.getHours(), ':', d.getMinutes(), ':', d.getSeconds()
    ].join('');
    if ((this.course2.modules[0] || this.course2.objectives[0]) && this.course2.title) {
      this.saveToBrowser(this.course2.title + '_autosave_' + stamp);
    }
  }, false);
},

CSV Export (Excerpt)

exportCSV: function () {
  const headers = [
    'Course Title','Course Client','Course Duration (days)','Course Objectives',
    'Module Number','Module Title','Module Subtitle','Module Description','Module Instructor','Module Duration',
    'Module Objectives','Topic Number','Topic Title','Topic Objective','Topic Slide Text','Topic PG Notes',
    'Topic IG Notes','Topic Review Questions','Topic Has Exercise','Topic Exercise Title','Topic Exercise Description',
    'Topic Media Required','Topic Media Details','Topic Media File Name','Topic Media Alt Text'
  ];
  let csvArr = [ headers.join(',') ];
  const cleanup = item => item.trim()
    .replace(/\n/gi,'%0A')
    .replace(/[\u2018\u2019]/g,"'")
    .replace(/[^\x20-\x7E]/g,'')
    .replace(/‐/g,'-')
    .replace(/,/g,'%2C')
    .replace(/[\u201C\u201D]/g,'"');
  // Traverse modules/topics pushing rows...
  // download(filename, csvArr.join('\n'));
},

Exports & Reuse

PPT outline, agenda table, objectives report reuse the same traversal logic (only formatting wrappers change). Deterministic outputs from a normalized schema mean reliable downstream formatting.

Impact

  • Single structured source → multiple artifacts (PPT, agenda, CSV, objective report).
  • Reduced manual cleanup of presentations and agendas.
  • Early detection of weak or vague text (flagged verbs, out‑of‑range word counts).
  • Fully portable (HTML + assets) and offline‑friendly.

Trade‑offs / Limitations

  • No real‑time collaboration; sharing requires exported JSON/CSV.
  • Validation is intentionally shallow (counts + flags) not pedagogical scoring.
  • jQuery for collapses/modals (Bootstrap 4); could migrate to Vue components later.

Possible Next Steps

Objective coverage heatmap; diff view vs prior autosave; optional sync API; Markdown export.

Tech Summary

Vue, Bootstrap 4, jQuery (UI behaviors), LocalStorage, FileReader API, Data URI downloads, regex validation, CSV assembly.

Summary Pattern Example

Problem: Fragmented course planning artifacts causing version drift.
Approach: Unified Vue schema model + bulk paste + client‑side exports.
Result: Faster design sessions; consistent multi‑format outputs from one JSON source.