commit 5efaf58707fff74c473dcc3bc26fcb29b0f88842 Author: Baccar Date: Tue Jun 2 18:48:14 2026 +0400 Initialisation diff --git a/Beginner Exercises and Answers/CSS_Beginner_Answer_Sheet.docx b/Beginner Exercises and Answers/CSS_Beginner_Answer_Sheet.docx new file mode 100644 index 0000000..9c27b7a Binary files /dev/null and b/Beginner Exercises and Answers/CSS_Beginner_Answer_Sheet.docx differ diff --git a/Beginner Exercises and Answers/CSS_Beginner_Exercises.docx b/Beginner Exercises and Answers/CSS_Beginner_Exercises.docx new file mode 100644 index 0000000..f81640d Binary files /dev/null and b/Beginner Exercises and Answers/CSS_Beginner_Exercises.docx differ diff --git a/Beginner Exercises and Answers/HTML_Beginner_Answer_Sheet.docx b/Beginner Exercises and Answers/HTML_Beginner_Answer_Sheet.docx new file mode 100644 index 0000000..45fd380 Binary files /dev/null and b/Beginner Exercises and Answers/HTML_Beginner_Answer_Sheet.docx differ diff --git a/Beginner Exercises and Answers/HTML_Beginner_Exercises.docx b/Beginner Exercises and Answers/HTML_Beginner_Exercises.docx new file mode 100644 index 0000000..97ce3e3 Binary files /dev/null and b/Beginner Exercises and Answers/HTML_Beginner_Exercises.docx differ diff --git a/Beginner Exercises and Answers/JavaScript_Beginner_Answer_Sheet.docx b/Beginner Exercises and Answers/JavaScript_Beginner_Answer_Sheet.docx new file mode 100644 index 0000000..c4b046f Binary files /dev/null and b/Beginner Exercises and Answers/JavaScript_Beginner_Answer_Sheet.docx differ diff --git a/Beginner Exercises and Answers/JavaScript_Beginner_Exercises.docx b/Beginner Exercises and Answers/JavaScript_Beginner_Exercises.docx new file mode 100644 index 0000000..e16d434 Binary files /dev/null and b/Beginner Exercises and Answers/JavaScript_Beginner_Exercises.docx differ diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..3b66fca --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,546 @@ +# Basic Web Development Series — Project Context +> Drop this file into your project folder and tell Claude Code: +> **"Read CONTEXT.md and continue the project."** + +--- + +## What This Project Is + +A complete programming education product catalogue being built to sell on **Gumroad** and **Etsy**. It covers HTML, CSS, and JavaScript across three learning tiers: reference notes, beginner micro-exercises, and intermediate exercises — each with matching answer sheets. Two strategy documents accompany the catalogue. + +All documents are Word (.docx) files generated using **Python + python-docx**. The Node.js `docx` library was tried first but produced files Word couldn't open. python-docx 1.2.0 is confirmed working and must be used for all future documents. + +--- + +## Project Folder Structure + +Place files like this inside `C:\projects\Docs Re\`: + +``` +C:\projects\Docs Re\ +│ +├── CONTEXT.md ← this file +│ +├── scripts\ +│ ├── dochelper.py ← shared helper library (all generators import this) +│ ├── gen_notes.py ← generates 3 fundamentals reference notes +│ ├── gen_strategy.py ← generates 2 strategy documents +│ ├── gen_exercises.py ← generates 6 intermediate exercise docs +│ ├── gen_beginner.py ← generates 6 beginner exercise docs +│ ├── gen_micro_images.py ← generates 6 PIL preview images for HTML beginner exercises +│ └── gen_images.py ← generates 3 PIL preview images for HTML intermediate exercises +│ +├── images\ +│ ├── bex1.png through bex6.png ← HTML beginner exercise browser-window previews +│ └── html_ex1_preview.png ← HTML intermediate exercise previews +│ html_ex2_preview.png +│ html_ex3_preview.png +│ +└── output\ + ├── HTML_Fundamentals.docx + ├── CSS_Fundamentals.docx + ├── JavaScript_Fundamentals.docx + ├── HTML_Beginner_Exercises.docx + ├── HTML_Beginner_Answer_Sheet.docx + ├── CSS_Beginner_Exercises.docx + ├── CSS_Beginner_Answer_Sheet.docx + ├── JavaScript_Beginner_Exercises.docx + ├── JavaScript_Beginner_Answer_Sheet.docx + ├── HTML_Exercises.docx + ├── HTML_Answer_Sheet.docx + ├── CSS_Exercises.docx + ├── CSS_Answer_Sheet.docx + ├── JavaScript_Exercises.docx + ├── JavaScript_Answer_Sheet.docx + ├── Monetising_Your_Programming_Knowledge.docx + └── Gumroad_Sales_Strategy.docx +``` + +> **Note:** Update the output paths at the top of each generator script to match wherever you want files saved on your machine. + +--- + +## All 17 Completed Documents + +### Tier 1 — Reference Notes (3 docs) +| File | Accent Colour | Sections | +|------|--------------|----------| +| HTML_Fundamentals.docx | Orange `#E44D26` | What is HTML, Tags & Elements, Lists, Tables, Forms, Semantic HTML, Attributes, Best Practices, Cheat Sheet | +| CSS_Fundamentals.docx | Blue `#264DE4` | What is CSS, Selectors, Box Model, Typography & Colour, Flexbox, CSS Grid, Positioning, Media Queries, Variables & Animations, Cheat Sheet | +| JavaScript_Fundamentals.docx | Yellow `#B8A800` | What is JS, Variables & Data Types, Operators, Control Flow, Loops, Functions, Arrays, Objects, DOM Manipulation, Fetch API, Cheat Sheet | + +### Tier 2 — Beginner Micro-Exercises (6 docs) +Each language has an exercises doc (6 exercises × 5 pts = 30 pts) and a matching green answer sheet. + +| Language | Exercises Cover | Difficulty Spread | +|----------|----------------|-------------------| +| HTML | Headings & Paragraphs, Images, Lists, Tables, Hyperlinks, Semantic Layout | 3 × First Steps / 3 × Getting There | +| CSS | Colours & Fonts, Box Model, Backgrounds & Links, Flexbox Centring, Hover & Transitions, CSS Grid | 3 × First Steps / 3 × Getting There | +| JavaScript | Variables & console.log, if/else, Arrays & for Loop, Functions, DOM Manipulation, Objects | 3 × First Steps / 3 × Getting There | + +HTML beginner exercises include real browser-window preview images (generated by PIL). CSS exercises use inline text preview boxes. JS exercises show expected Console output in green-on-dark code blocks. + +### Tier 3 — Intermediate Exercises (6 docs) +Each language has 3 exercises worth 10 pts each = 30 pts total, with a matching answer sheet. + +| Language | Exercises | +|----------|-----------| +| HTML | Personal Bio Page, Recipe Page with Table, Contact Form | +| CSS | Blog Post Card Styling, Flexbox Navigation Bar, Responsive CSS Grid | +| JavaScript | Grade Calculator (functions & conditionals), DOM Array Builder, Interactive Quiz App | + +HTML intermediate exercises include real PIL preview images. + +### Strategy Documents (2 docs) +| File | Contents | +|------|----------| +| Monetising_Your_Programming_Knowledge.docx | Online courses, tutoring, YouTube/newsletter, selling digital docs, corporate training, recommended starting path, strategy comparison table | +| Gumroad_Sales_Strategy.docx | Full pricing table, bundle structure, Gumroad setup steps, Etsy guidance, product description templates, 8-week launch plan | + +--- + +## Pricing Strategy (Finalised) + +### Individual Products +| Product | Price | +|---------|-------| +| Fundamentals notes (each) | £7.00 | +| Beginner exercises + answers (per language) | £5.00 | +| Intermediate exercises + answers (per language) | £8.00 | +| Monetising guide | Free lead magnet or £4.00 | + +### Bundles +| Bundle | Includes | Price | +|--------|----------|-------| +| Complete HTML Pack | All 5 HTML docs | £22.00 | +| Complete CSS Pack | All 5 CSS docs | £22.00 | +| Complete JavaScript Pack | All 5 JS docs | £22.00 | +| Notes Bundle | All 3 fundamentals guides | £16.00 | +| Beginner Exercises Bundle | All 6 beginner docs (3 languages) | £18.00 | +| Intermediate Exercises Bundle | All 6 intermediate docs (3 languages) | £20.00 | +| **Complete Series** | **All 16 teaching docs** | **£45.00** | + +> After the first 20–30 reviews, raise prices 20–30%. Early buyers got a founder's discount; this is standard and well-accepted on Gumroad. + +--- + +## Design Decisions (Important — Keep These Consistent) + +### Brand Colours +- **HTML** → Orange `#E44D26` +- **CSS** → Blue `#264DE4` +- **JavaScript** → Yellow `#B8A800` +- **Answer sheets** → Green `#1A7A3C` (all languages) +- **Strategy docs** → Teal `#2E86AB` + +### Document Design Rules +- Font: **Arial** throughout (body, headings, tables) +- Code blocks: dark background `#1E1E1E`, monospace `Courier New` + - HTML code → amber `#F8C555` + - CSS code → blue `#9CDCFE` + - JavaScript code → yellow `#F0DB4F` + - Console output → green `#98FB98` on `#2D2D2D` +- Exercise badges: 4-column table (title | section ref | time | score) +- Answer headers: full-width green banner with tick mark +- Info boxes: 2-column table (coloured label | tinted text area) +- Reference tables: coloured header row, alternating white/`#F8F8F8` rows + +### Scoring +- Beginner exercises: 5 pts each × 6 = 30 pts per language +- Intermediate exercises: 10 pts each × 3 = 30 pts per language + +--- + +## Technical Setup + +### Requirements +``` +python-docx==1.2.0 pip install python-docx +Pillow pip install Pillow +``` + +> **Do NOT use the Node.js `docx` library** (v9.6.1 was tried — it produces structurally valid XML but Word rejects the files on open). + +### How to Run +```bash +# From your project folder, run scripts in this order: +python scripts/gen_micro_images.py # generates bex1-6.png +python scripts/gen_images.py # generates html_ex1-3_preview.png +python scripts/gen_notes.py # HTML/CSS/JS Fundamentals +python scripts/gen_strategy.py # Monetising guide + Gumroad Strategy +python scripts/gen_exercises.py # 6 intermediate exercise docs +python scripts/gen_beginner.py # 6 beginner exercise docs +``` + +### Output Path Variable +Each generator script has `OUT = "/mnt/user-data/outputs"` near the top. Change this to your local output folder, e.g.: +```python +OUT = r"C:\projects\Docs Re\output" +``` + +### Validating Files +Run this to confirm all generated files are Word-compatible: +```python +import zipfile, xml.etree.ElementTree as ET, os + +folder = r"C:\projects\Docs Re\output" +for f in sorted(os.listdir(folder)): + if f.endswith('.docx'): + path = os.path.join(folder, f) + try: + with zipfile.ZipFile(path) as z: + ET.fromstring(z.read('word/document.xml')) + print(f"OK {f}") + except Exception as e: + print(f"FAIL {f} -> {e}") +``` + +--- + +## Shared Helper Library (dochelper.py) + +This file must exist before any generator runs. All generators do `import dochelper` at the top. + +```python +""" +Shared helpers for generating Word documents with python-docx. +Produces clean, Word-compatible .docx files. +""" +from docx import Document +from docx.shared import Pt, RGBColor, Inches, Cm, Emu +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL +from docx.oxml.ns import qn +from docx.oxml import OxmlElement +import copy + +# ── Colour palette ───────────────────────────────────────────────────────── +def rgb(hex6): + h = hex6.lstrip('#') + return RGBColor(int(h[0:2],16), int(h[2:4],16), int(h[4:6],16)) + +WHITE = rgb("FFFFFF") +BLACK = rgb("1A1A2E") +GRAY = rgb("666666") +LGRAY = rgb("F5F5F7") +DGRAY = rgb("333333") +ORANGE = rgb("E44D26") +ORANGE_L = rgb("FDF0EC") +BLUE = rgb("264DE4") +BLUE_L = rgb("EEF2FD") +YELLOW = rgb("B8A800") +YELLOW_L = rgb("FFFCE0") +GREEN = rgb("1A7A3C") +GREEN_L = rgb("E8F8EE") + +# ── Document setup ────────────────────────────────────────────────────────── +def new_doc(): + doc = Document() + for section in doc.sections: + section.top_margin = Cm(2.5) + section.bottom_margin = Cm(2.5) + section.left_margin = Cm(2.5) + section.right_margin = Cm(2.5) + for p in doc.paragraphs: + p._element.getparent().remove(p._element) + return doc + +# ── Paragraph helpers ─────────────────────────────────────────────────────── +def add_para(doc_or_cell, text="", bold=False, italic=False, size=11, + color=None, align=WD_ALIGN_PARAGRAPH.LEFT, + space_before=0, space_after=6, font_name="Arial"): + if hasattr(doc_or_cell, 'add_paragraph'): + p = doc_or_cell.add_paragraph() + else: + p = doc_or_cell.paragraphs[0] if doc_or_cell.paragraphs else doc_or_cell.add_paragraph() + p.clear() + p.alignment = align + pf = p.paragraph_format + pf.space_before = Pt(space_before) + pf.space_after = Pt(space_after) + if text: + run = p.add_run(text) + run.bold = bold; run.italic = italic + run.font.name = font_name; run.font.size = Pt(size) + run.font.color.rgb = color if color else BLACK + return p + +def add_heading(doc, text, level=1, color=None, size=None, border_color=None, + space_before=14, space_after=4): + sizes = {1: 18, 2: 14, 3: 12} + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.LEFT + p.paragraph_format.space_before = Pt(space_before) + p.paragraph_format.space_after = Pt(space_after) + run = p.add_run(text) + run.bold = True; run.font.name = "Arial" + run.font.size = Pt(size or sizes.get(level, 11)) + run.font.color.rgb = color or BLACK + if border_color: + set_para_border(p, bottom=border_color) + return p + +def add_code_block(doc, lines, bg="1E1E1E", fg="F0DB4F", left_bar=None): + for line in lines: + p = doc.add_paragraph() + p.paragraph_format.space_before = Pt(0) + p.paragraph_format.space_after = Pt(0) + p.paragraph_format.left_indent = Cm(0.5) + set_para_shading(p, bg) + if left_bar: + set_para_border(p, left=left_bar) + run = p.add_run(line if line else " ") + run.font.name = "Courier New"; run.font.size = Pt(9.5) + run.font.color.rgb = rgb(fg) + sp = doc.add_paragraph(" ") + sp.paragraph_format.space_before = Pt(0) + sp.paragraph_format.space_after = Pt(4) + +def add_bullet(doc, text, size=11, color=None): + p = doc.add_paragraph(style='List Bullet') + p.paragraph_format.space_before = Pt(2) + p.paragraph_format.space_after = Pt(2) + run = p.add_run(text) + run.font.name = "Arial"; run.font.size = Pt(size) + run.font.color.rgb = color or BLACK + return p + +def add_numbered(doc, text, size=11, color=None): + p = doc.add_paragraph(style='List Number') + p.paragraph_format.space_before = Pt(2) + p.paragraph_format.space_after = Pt(2) + run = p.add_run(text) + run.font.name = "Arial"; run.font.size = Pt(size) + run.font.color.rgb = color or BLACK + return p + +def add_checkbox(doc, text): + p = doc.add_paragraph(style='List Bullet') + p.paragraph_format.space_before = Pt(2) + p.paragraph_format.space_after = Pt(2) + run = p.add_run("☐ " + text) + run.font.name = "Arial"; run.font.size = Pt(10.5) + run.font.color.rgb = BLACK + return p + +def add_tick(doc, text): + p = doc.add_paragraph(style='List Bullet') + p.paragraph_format.space_before = Pt(2) + p.paragraph_format.space_after = Pt(2) + run = p.add_run("✓ " + text) + run.font.name = "Arial"; run.font.size = Pt(10.5) + run.font.color.rgb = GREEN + return p + +def add_spacer(doc, pts=8): + p = doc.add_paragraph(" ") + p.paragraph_format.space_before = Pt(0) + p.paragraph_format.space_after = Pt(pts) + for run in p.runs: + run.font.size = Pt(2) + +def add_page_break(doc): + doc.add_page_break() + +# ── Table helpers ─────────────────────────────────────────────────────────── +def cell_text(cell, text, bold=False, italic=False, size=10, color=None, + align=WD_ALIGN_PARAGRAPH.LEFT, bg=None, font="Arial"): + cell.text = "" + p = cell.paragraphs[0] + p.alignment = align + p.paragraph_format.space_before = Pt(3) + p.paragraph_format.space_after = Pt(3) + run = p.add_run(text) + run.bold = bold; run.italic = italic + run.font.name = font; run.font.size = Pt(size) + run.font.color.rgb = color or BLACK + if bg: + set_cell_shading(cell, bg) + return cell + +def set_cell_shading(cell, hex_color): + tc = cell._tc; tcPr = tc.get_or_add_tcPr() + shd = OxmlElement('w:shd') + shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto') + shd.set(qn('w:fill'), hex_color.lstrip('#')) + tcPr.append(shd) + +def set_cell_margins(cell, top=60, bottom=60, left=100, right=100): + tc = cell._tc; tcPr = tc.get_or_add_tcPr() + tcMar = OxmlElement('w:tcMar') + for side, val in [('top',top),('bottom',bottom),('left',left),('right',right)]: + el = OxmlElement(f'w:{side}') + el.set(qn('w:w'), str(val)); el.set(qn('w:type'), 'dxa') + tcMar.append(el) + tcPr.append(tcMar) + +# ── XML styling helpers ───────────────────────────────────────────────────── +def set_para_shading(para, hex_color): + pPr = para._p.get_or_add_pPr() + shd = OxmlElement('w:shd') + shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto') + shd.set(qn('w:fill'), hex_color.lstrip('#')) + pPr.append(shd) + +def set_para_border(para, bottom=None, left=None, top=None): + pPr = para._p.get_or_add_pPr() + pBdr = OxmlElement('w:pBdr') + for side, color in [('bottom',bottom),('left',left),('top',top)]: + if color: + el = OxmlElement(f'w:{side}') + el.set(qn('w:val'), 'single') + el.set(qn('w:sz'), '6' if side in ('bottom','top') else '18') + el.set(qn('w:space'), '4') + el.set(qn('w:color'), color.lstrip('#') if isinstance(color,str) + else '{:02X}{:02X}{:02X}'.format(*color)) + pBdr.append(el) + pPr.append(pBdr) + +# ── Compound elements ─────────────────────────────────────────────────────── +def add_info_box(doc, label, text, bg_hex, accent_hex, label_color_hex="FFFFFF"): + table = doc.add_table(rows=1, cols=2) + table.style = 'Table Grid' + lc = table.rows[0].cells[0] + lc.width = Cm(1.5) + set_cell_shading(lc, accent_hex); set_cell_margins(lc) + p = lc.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER + p.paragraph_format.space_before = Pt(4); p.paragraph_format.space_after = Pt(4) + run = p.add_run(label); run.bold = True; run.font.name = "Arial" + run.font.size = Pt(9); run.font.color.rgb = rgb(label_color_hex) + tc = table.rows[0].cells[1] + set_cell_shading(tc, bg_hex); set_cell_margins(tc, left=120) + p2 = tc.paragraphs[0] + p2.paragraph_format.space_before = Pt(4); p2.paragraph_format.space_after = Pt(4) + run2 = p2.add_run(text); run2.font.name = "Arial"; run2.font.size = Pt(10) + run2.font.color.rgb = BLACK + add_spacer(doc, 6) + return table + +def add_cover(doc, title_top, title_main, title_sub, subtitle, part_note, accent_hex): + add_spacer(doc, 40) + if title_top: + add_para(doc, title_top, bold=True, size=14, color=GRAY, + align=WD_ALIGN_PARAGRAPH.CENTER, space_after=2) + add_para(doc, title_main, bold=True, size=36, color=rgb(accent_hex), + align=WD_ALIGN_PARAGRAPH.CENTER, space_after=4) + if title_sub: + add_para(doc, title_sub, bold=True, size=20, color=BLACK, + align=WD_ALIGN_PARAGRAPH.CENTER, space_after=6) + add_para(doc, subtitle, size=12, color=GRAY, + align=WD_ALIGN_PARAGRAPH.CENTER, space_after=4) + if part_note: + p = add_para(doc, part_note, size=10, italic=True, color=GRAY, + align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8) + set_para_border(p, bottom=accent_hex) + add_spacer(doc, 20) + +def add_answer_header(doc, num, title, accent_hex="1A7A3C"): + table = doc.add_table(rows=1, cols=1) + table.style = 'Table Grid' + cell = table.rows[0].cells[0] + set_cell_shading(cell, accent_hex); set_cell_margins(cell, top=80, bottom=80, left=150) + p = cell.paragraphs[0] + r1 = p.add_run(f"✓ Answer — Exercise {num}: ") + r1.bold = True; r1.font.name = "Arial"; r1.font.size = Pt(13); r1.font.color.rgb = WHITE + r2 = p.add_run(title) + r2.font.name = "Arial"; r2.font.size = Pt(11); r2.font.color.rgb = rgb("C8F0D4") + add_spacer(doc, 6) + +def add_exercise_badge(doc, num, title, section_ref, difficulty, minutes, accent_hex, score_denom=5): + table = doc.add_table(rows=1, cols=4) + table.style = 'Table Grid' + c0 = table.rows[0].cells[0]; c0.width = Cm(8) + set_cell_shading(c0, accent_hex); set_cell_margins(c0, top=80, bottom=80, left=150) + p = c0.paragraphs[0] + r1 = p.add_run(f"Ex {num} "); r1.bold=True; r1.font.name="Arial" + r1.font.size=Pt(13); r1.font.color.rgb=WHITE + r2 = p.add_run(title); r2.font.name="Arial"; r2.font.size=Pt(11); r2.font.color.rgb=rgb("E0E0E0") + c1 = table.rows[0].cells[1]; set_cell_shading(c1, "F5F5F7"); set_cell_margins(c1) + p1a = c1.paragraphs[0]; p1a.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p1a.add_run(f"After Section {section_ref}"); r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=GRAY + p1b = c1.add_paragraph(); p1b.alignment = WD_ALIGN_PARAGRAPH.CENTER + r2 = p1b.add_run(difficulty); r2.bold=True; r2.font.name="Arial"; r2.font.size=Pt(9); r2.font.color.rgb=rgb(accent_hex) + c2 = table.rows[0].cells[2]; set_cell_shading(c2, "F5F5F7"); set_cell_margins(c2) + p2a = c2.paragraphs[0]; p2a.alignment = WD_ALIGN_PARAGRAPH.CENTER + p2b = c2.add_paragraph(); p2b.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p2b.add_run(f"{minutes} min"); r.bold=True; r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=BLACK + c3 = table.rows[0].cells[3]; set_cell_shading(c3, "F5F5F7"); set_cell_margins(c3) + p3a = c3.paragraphs[0]; p3a.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p3a.add_run("Score"); r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=GRAY + p3b = c3.add_paragraph(); p3b.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p3b.add_run(f"__ / {score_denom}"); r.bold=True; r.font.name="Arial"; r.font.size=Pt(10); r.font.color.rgb=BLACK + add_spacer(doc, 6) + +def add_ref_table(doc, headers, rows, accent_hex, col_widths_cm=None): + table = doc.add_table(rows=1+len(rows), cols=len(headers)) + table.style = 'Table Grid' + if col_widths_cm: + for row in table.rows: + for j, cell in enumerate(row.cells): + if j < len(col_widths_cm): + cell.width = Cm(col_widths_cm[j]) + for j, h in enumerate(headers): + c = table.rows[0].cells[j] + set_cell_shading(c, accent_hex); set_cell_margins(c) + cell_text(c, h, bold=True, size=10, color=WHITE) + for i, row in enumerate(rows): + bg = "FFFFFF" if i % 2 == 0 else "F8F8F8" + for j, val in enumerate(row): + c = table.rows[i+1].cells[j] + set_cell_shading(c, bg); set_cell_margins(c) + font = "Courier New" if j < 2 else "Arial" + clr = rgb("C0392B") if j == 0 else (GRAY if j == 1 else BLACK) + cell_text(c, val, size=9, color=clr, font=font) + add_spacer(doc, 8) + +print("dochelper loaded OK") +``` + +--- + +## Ideas for What to Build Next + +These were discussed but not yet created. Claude Code can pick any of these up: + +### Additional Documents +- **Advanced Exercise Tier** — 3 more challenging exercises per language (e.g. building a full responsive landing page in HTML/CSS, a full CRUD app in JS). Worth £12–15 each. +- **jQuery Basics** — a notes + exercise pack for the legacy library still widely used in older codebases +- **Git & GitHub Guide** — version control reference doc, highly searched on Etsy +- **CSS Animations Deep Dive** — keyframes, transitions, scroll animations +- **JavaScript Interview Prep** — Q&A format covering closures, hoisting, event loop, promises + +### Product Improvements +- Add a **table of contents** page to each fundamentals notes doc +- Add **page numbers** in the footer of each document +- Create **PDF versions** of all documents (some buyers prefer PDFs for printing) +- Add a **"Common Mistakes"** callout box to every exercise (not just answer sheets) + +### Marketing Assets +- A **free sample PDF** — 2-page teaser combining one page from each fundamentals guide +- **Gumroad cover images** — 1280×720px branded thumbnails for each product listing +- A **product comparison table** one-pager showing all bundles side by side + +### Platform Expansion +- Reformat notes as **Notion templates** (sold separately on Gumroad) +- Create a **Teachable or Udemy course outline** based on the existing document structure +- Package documents as a **Payhip bundle** for a second sales channel + +--- + +## Key Decisions Made (Do Not Change Without Reason) + +| Decision | What Was Decided | +|----------|-----------------| +| File format | Word (.docx) via python-docx 1.2.0 — NOT Node.js docx library | +| Image generation | PIL (Pillow) for browser-window preview images | +| Fonts | Arial body, Courier New for all code | +| Code block style | Dark background #1E1E1E, language-specific fg colours | +| Answer sheet colour | Green #1A7A3C for all languages | +| Scoring | Beginner: 5 pts × 6 = 30 | Intermediate: 10 pts × 3 = 30 | +| Pricing anchor | Complete Series £45 is the hero product | +| Lead magnet | Monetising guide offered free to build email list | +| Launch platform | Gumroad first, then Etsy in parallel | + +--- + +*This context file was generated from a claude.ai chat session. Last updated: June 2026.* diff --git a/Exercises and Answers/CSS_Answer_Sheet.docx b/Exercises and Answers/CSS_Answer_Sheet.docx new file mode 100644 index 0000000..d926af6 Binary files /dev/null and b/Exercises and Answers/CSS_Answer_Sheet.docx differ diff --git a/Exercises and Answers/CSS_Exercises.docx b/Exercises and Answers/CSS_Exercises.docx new file mode 100644 index 0000000..b734407 Binary files /dev/null and b/Exercises and Answers/CSS_Exercises.docx differ diff --git a/Exercises and Answers/HTML_Answer_Sheet.docx b/Exercises and Answers/HTML_Answer_Sheet.docx new file mode 100644 index 0000000..2ce3169 Binary files /dev/null and b/Exercises and Answers/HTML_Answer_Sheet.docx differ diff --git a/Exercises and Answers/HTML_Exercises.docx b/Exercises and Answers/HTML_Exercises.docx new file mode 100644 index 0000000..045cf0a Binary files /dev/null and b/Exercises and Answers/HTML_Exercises.docx differ diff --git a/Exercises and Answers/JavaScript_Answer_Sheet.docx b/Exercises and Answers/JavaScript_Answer_Sheet.docx new file mode 100644 index 0000000..a6627ef Binary files /dev/null and b/Exercises and Answers/JavaScript_Answer_Sheet.docx differ diff --git a/Exercises and Answers/JavaScript_Exercises.docx b/Exercises and Answers/JavaScript_Exercises.docx new file mode 100644 index 0000000..424d192 Binary files /dev/null and b/Exercises and Answers/JavaScript_Exercises.docx differ diff --git a/Gumroad_Sales_Strategy.docx b/Gumroad_Sales_Strategy.docx new file mode 100644 index 0000000..44f7253 Binary files /dev/null and b/Gumroad_Sales_Strategy.docx differ diff --git a/Monetising_Your_Programming_Knowledge.docx b/Monetising_Your_Programming_Knowledge.docx new file mode 100644 index 0000000..3a32567 Binary files /dev/null and b/Monetising_Your_Programming_Knowledge.docx differ diff --git a/Programming Notes/CSS_Fundamentals.docx b/Programming Notes/CSS_Fundamentals.docx new file mode 100644 index 0000000..7c14de9 Binary files /dev/null and b/Programming Notes/CSS_Fundamentals.docx differ diff --git a/Programming Notes/HTML_Fundamentals.docx b/Programming Notes/HTML_Fundamentals.docx new file mode 100644 index 0000000..1cf2eb3 Binary files /dev/null and b/Programming Notes/HTML_Fundamentals.docx differ diff --git a/Programming Notes/JavaScript_Fundamentals.docx b/Programming Notes/JavaScript_Fundamentals.docx new file mode 100644 index 0000000..f3fc393 Binary files /dev/null and b/Programming Notes/JavaScript_Fundamentals.docx differ diff --git a/images/covers/cover_bundle_beginner.png b/images/covers/cover_bundle_beginner.png new file mode 100644 index 0000000..3ce7efb Binary files /dev/null and b/images/covers/cover_bundle_beginner.png differ diff --git a/images/covers/cover_bundle_complete.png b/images/covers/cover_bundle_complete.png new file mode 100644 index 0000000..dddf0cb Binary files /dev/null and b/images/covers/cover_bundle_complete.png differ diff --git a/images/covers/cover_bundle_css.png b/images/covers/cover_bundle_css.png new file mode 100644 index 0000000..fe4eec9 Binary files /dev/null and b/images/covers/cover_bundle_css.png differ diff --git a/images/covers/cover_bundle_html.png b/images/covers/cover_bundle_html.png new file mode 100644 index 0000000..e629e9b Binary files /dev/null and b/images/covers/cover_bundle_html.png differ diff --git a/images/covers/cover_bundle_intermediate.png b/images/covers/cover_bundle_intermediate.png new file mode 100644 index 0000000..ce9be40 Binary files /dev/null and b/images/covers/cover_bundle_intermediate.png differ diff --git a/images/covers/cover_bundle_js.png b/images/covers/cover_bundle_js.png new file mode 100644 index 0000000..1d05d8b Binary files /dev/null and b/images/covers/cover_bundle_js.png differ diff --git a/images/covers/cover_bundle_notes.png b/images/covers/cover_bundle_notes.png new file mode 100644 index 0000000..441961f Binary files /dev/null and b/images/covers/cover_bundle_notes.png differ diff --git a/images/covers/cover_css_answers.png b/images/covers/cover_css_answers.png new file mode 100644 index 0000000..bdad515 Binary files /dev/null and b/images/covers/cover_css_answers.png differ diff --git a/images/covers/cover_css_beginner_answers.png b/images/covers/cover_css_beginner_answers.png new file mode 100644 index 0000000..c75d1e3 Binary files /dev/null and b/images/covers/cover_css_beginner_answers.png differ diff --git a/images/covers/cover_css_beginner_exercises.png b/images/covers/cover_css_beginner_exercises.png new file mode 100644 index 0000000..cf5fdd2 Binary files /dev/null and b/images/covers/cover_css_beginner_exercises.png differ diff --git a/images/covers/cover_css_exercises.png b/images/covers/cover_css_exercises.png new file mode 100644 index 0000000..111ed9f Binary files /dev/null and b/images/covers/cover_css_exercises.png differ diff --git a/images/covers/cover_css_fundamentals.png b/images/covers/cover_css_fundamentals.png new file mode 100644 index 0000000..5d02876 Binary files /dev/null and b/images/covers/cover_css_fundamentals.png differ diff --git a/images/covers/cover_gumroad_strategy.png b/images/covers/cover_gumroad_strategy.png new file mode 100644 index 0000000..c641fe0 Binary files /dev/null and b/images/covers/cover_gumroad_strategy.png differ diff --git a/images/covers/cover_html_answers.png b/images/covers/cover_html_answers.png new file mode 100644 index 0000000..b880559 Binary files /dev/null and b/images/covers/cover_html_answers.png differ diff --git a/images/covers/cover_html_beginner_answers.png b/images/covers/cover_html_beginner_answers.png new file mode 100644 index 0000000..870d38a Binary files /dev/null and b/images/covers/cover_html_beginner_answers.png differ diff --git a/images/covers/cover_html_beginner_exercises.png b/images/covers/cover_html_beginner_exercises.png new file mode 100644 index 0000000..c3d8b6d Binary files /dev/null and b/images/covers/cover_html_beginner_exercises.png differ diff --git a/images/covers/cover_html_exercises.png b/images/covers/cover_html_exercises.png new file mode 100644 index 0000000..a584e2c Binary files /dev/null and b/images/covers/cover_html_exercises.png differ diff --git a/images/covers/cover_html_fundamentals.png b/images/covers/cover_html_fundamentals.png new file mode 100644 index 0000000..e583c99 Binary files /dev/null and b/images/covers/cover_html_fundamentals.png differ diff --git a/images/covers/cover_js_answers.png b/images/covers/cover_js_answers.png new file mode 100644 index 0000000..9214950 Binary files /dev/null and b/images/covers/cover_js_answers.png differ diff --git a/images/covers/cover_js_beginner_answers.png b/images/covers/cover_js_beginner_answers.png new file mode 100644 index 0000000..6d1f788 Binary files /dev/null and b/images/covers/cover_js_beginner_answers.png differ diff --git a/images/covers/cover_js_beginner_exercises.png b/images/covers/cover_js_beginner_exercises.png new file mode 100644 index 0000000..c3e38e4 Binary files /dev/null and b/images/covers/cover_js_beginner_exercises.png differ diff --git a/images/covers/cover_js_exercises.png b/images/covers/cover_js_exercises.png new file mode 100644 index 0000000..2a0d261 Binary files /dev/null and b/images/covers/cover_js_exercises.png differ diff --git a/images/covers/cover_js_fundamentals.png b/images/covers/cover_js_fundamentals.png new file mode 100644 index 0000000..eebc331 Binary files /dev/null and b/images/covers/cover_js_fundamentals.png differ diff --git a/images/covers/cover_monetising_guide.png b/images/covers/cover_monetising_guide.png new file mode 100644 index 0000000..506f381 Binary files /dev/null and b/images/covers/cover_monetising_guide.png differ diff --git a/scripts/gen_covers.py b/scripts/gen_covers.py new file mode 100644 index 0000000..ee9740b --- /dev/null +++ b/scripts/gen_covers.py @@ -0,0 +1,204 @@ +""" +Generate 1280x720 Gumroad cover images for the Basic Web Development Series. +Produces 24 PNG files: 17 individual product covers + 7 bundle covers. + +Requirements: pip install Pillow +Run from any directory: python scripts/gen_covers.py +""" +from PIL import Image, ImageDraw, ImageFont +import os + +OUT = r"C:\projects\Docs Re\images\covers" +os.makedirs(OUT, exist_ok=True) + +W, H = 1280, 720 +MARGIN = 80 + +FONT_BOLD = r"C:\Windows\Fonts\arialbd.ttf" +FONT_REG = r"C:\Windows\Fonts\arial.ttf" + + +def load_font(bold=False, size=32): + try: + return ImageFont.truetype(FONT_BOLD if bold else FONT_REG, size) + except OSError: + return ImageFont.load_default() + + +BRAND = { + "html": (228, 77, 38), # #E44D26 + "css": ( 38, 77, 228), # #264DE4 + "js": (184, 168, 0), # #B8A800 + "green": ( 26, 122, 60), # #1A7A3C + "teal": ( 46, 134, 171), # #2E86AB +} + + +def _lighter(rgb, factor): + return tuple(min(255, int(c + (255 - c) * factor)) for c in rgb) + + +def _badge_colours(accent): + """ + Returns (badge_bg, badge_fg). + Yellow background gets a dark badge so text stays readable. + """ + r, g, b = accent + if r > 140 and g > 130 and b < 60: # yellow (#B8A800) + return (26, 26, 46), (r, g, b) + return (255, 255, 255), accent # white bg, accent text + + +def _rounded_rect(draw, x0, y0, x1, y1, radius, fill): + r = radius + draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill) + draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill) + for cx, cy in [(x0, y0), (x1 - 2*r, y0), (x0, y1 - 2*r), (x1 - 2*r, y1 - 2*r)]: + draw.ellipse([cx, cy, cx + 2*r, cy + 2*r], fill=fill) + + +def make_cover(filename, badge, title, subtitle, accent): + """ + badge – short text for the top-left pill (e.g. 'HTML', 'CSS', 'SERIES') + title – main title; use '\\n' for a two-line title + subtitle – type label below the title + accent – RGB tuple for background colour + """ + img = Image.new("RGB", (W, H), accent) + d = ImageDraw.Draw(img) + + f_title = load_font(bold=True, size=54) + f_sub = load_font(bold=False, size=34) + f_series = load_font(bold=False, size=22) + f_badge = load_font(bold=True, size=40) + + LINE_GAP = 8 # gap between title lines when title wraps + RULE_PAD = 22 # space between rule and first title line + GAP_T_S = 14 # gap between title block and subtitle + GAP_S_SR = 10 # gap between subtitle and series name + BOTTOM_PAD = 72 # padding below series name + + SERIES_LABEL = "Basic Web Development Series" + + def bb(text, fnt): + return d.textbbox((0, 0), text, font=fnt) + + title_lines = title.split("\n") + t_bboxes = [bb(ln, f_title) for ln in title_lines] + t_heights = [b[3] - b[1] for b in t_bboxes] + total_title_h = sum(t_heights) + LINE_GAP * (len(t_heights) - 1) + + sub_b = bb(subtitle, f_sub) + series_b = bb(SERIES_LABEL, f_series) + sub_h = sub_b[3] - sub_b[1] + series_h = series_b[3] - series_b[1] + + content_h = RULE_PAD + total_title_h + GAP_T_S + sub_h + GAP_S_SR + series_h + rule_y = H - BOTTOM_PAD - content_h - 3 + + # ── Horizontal rule ──────────────────────────────────────────────────── + rule_col = _lighter(accent, 0.55) + d.rectangle([MARGIN, rule_y, W - MARGIN, rule_y + 3], fill=rule_col) + + # ── Title ────────────────────────────────────────────────────────────── + y = rule_y + RULE_PAD + for line, lh, line_bb in zip(title_lines, t_heights, t_bboxes): + d.text((MARGIN - line_bb[0], y - line_bb[1]), line, + font=f_title, fill=(255, 255, 255)) + y += lh + LINE_GAP + y -= LINE_GAP # remove trailing gap after last line + + # ── Subtitle ─────────────────────────────────────────────────────────── + y += GAP_T_S + sub_bb2 = bb(subtitle, f_sub) + d.text((MARGIN - sub_bb2[0], y - sub_bb2[1]), subtitle, + font=f_sub, fill=(255, 255, 255)) + y += sub_h + + # ── Series name ──────────────────────────────────────────────────────── + y += GAP_S_SR + series_col = _lighter(accent, 0.45) + sr_bb = bb(SERIES_LABEL, f_series) + d.text((MARGIN - sr_bb[0], y - sr_bb[1]), SERIES_LABEL, + font=f_series, fill=series_col) + + # ── Badge (top-left) ─────────────────────────────────────────────────── + badge_bg, badge_fg = _badge_colours(accent) + b_bb = bb(badge, f_badge) + bw, bh = b_bb[2] - b_bb[0], b_bb[3] - b_bb[1] + px, py = 28, 14 + bx0, by0 = MARGIN, 58 + bx1, by1 = bx0 + bw + 2*px, by0 + bh + 2*py + _rounded_rect(d, bx0, by0, bx1, by1, radius=14, fill=badge_bg) + d.text((bx0 + px - b_bb[0], by0 + py - b_bb[1]), + badge, font=f_badge, fill=badge_fg) + + out_path = os.path.join(OUT, filename) + img.save(out_path) + print(f" {filename}") + + +# ── Run ──────────────────────────────────────────────────────────────────────── +print("Generating covers...\n") + +# Fundamentals — reference notes +make_cover("cover_html_fundamentals.png", + "HTML", "HTML Fundamentals", "Reference Guide", BRAND["html"]) +make_cover("cover_css_fundamentals.png", + "CSS", "CSS Fundamentals", "Reference Guide", BRAND["css"]) +make_cover("cover_js_fundamentals.png", + "JS", "JavaScript Fundamentals", "Reference Guide", BRAND["js"]) + +# Beginner exercises +make_cover("cover_html_beginner_exercises.png", + "HTML", "HTML Beginner Exercises", "Beginner Exercises", BRAND["html"]) +make_cover("cover_html_beginner_answers.png", + "HTML", "HTML Beginner Answers", "Beginner Answer Sheet", BRAND["green"]) +make_cover("cover_css_beginner_exercises.png", + "CSS", "CSS Beginner Exercises", "Beginner Exercises", BRAND["css"]) +make_cover("cover_css_beginner_answers.png", + "CSS", "CSS Beginner Answers", "Beginner Answer Sheet", BRAND["green"]) +make_cover("cover_js_beginner_exercises.png", + "JS", "JavaScript Beginner Exercises", "Beginner Exercises", BRAND["js"]) +make_cover("cover_js_beginner_answers.png", + "JS", "JavaScript Beginner Answers", "Beginner Answer Sheet", BRAND["green"]) + +# Intermediate exercises +make_cover("cover_html_exercises.png", + "HTML", "HTML Exercises", "Intermediate Exercises", BRAND["html"]) +make_cover("cover_html_answers.png", + "HTML", "HTML Answer Sheet", "Intermediate Answer Sheet", BRAND["green"]) +make_cover("cover_css_exercises.png", + "CSS", "CSS Exercises", "Intermediate Exercises", BRAND["css"]) +make_cover("cover_css_answers.png", + "CSS", "CSS Answer Sheet", "Intermediate Answer Sheet", BRAND["green"]) +make_cover("cover_js_exercises.png", + "JS", "JavaScript Exercises", "Intermediate Exercises", BRAND["js"]) +make_cover("cover_js_answers.png", + "JS", "JavaScript Answer Sheet", "Intermediate Answer Sheet", BRAND["green"]) + +# Strategy docs +make_cover("cover_monetising_guide.png", + "GUIDE", "Monetising Your\nProgramming Knowledge", "Strategy Guide", BRAND["teal"]) +make_cover("cover_gumroad_strategy.png", + "GUIDE", "Gumroad Sales Strategy", "Sales Strategy", BRAND["teal"]) + +# Language bundles (single-language, use language colour) +make_cover("cover_bundle_html.png", + "HTML", "Complete HTML Pack", "5-Document Bundle", BRAND["html"]) +make_cover("cover_bundle_css.png", + "CSS", "Complete CSS Pack", "5-Document Bundle", BRAND["css"]) +make_cover("cover_bundle_js.png", + "JS", "Complete JavaScript Pack", "5-Document Bundle", BRAND["js"]) + +# Multi-language bundles (teal) +make_cover("cover_bundle_notes.png", + "NOTES", "Notes Bundle", "All 3 Fundamentals Guides", BRAND["teal"]) +make_cover("cover_bundle_beginner.png", + "STARTER", "Beginner Exercises Bundle", "All 6 Beginner Documents", BRAND["teal"]) +make_cover("cover_bundle_intermediate.png", + "LEVEL 2", "Intermediate Exercises Bundle", "All 6 Intermediate Documents", BRAND["teal"]) +make_cover("cover_bundle_complete.png", + "SERIES", "Complete Series", "All 16 Teaching Documents", BRAND["teal"]) + +print(f"\nDone — 24 covers saved to:\n{OUT}")