Initialisation
BIN
Beginner Exercises and Answers/CSS_Beginner_Answer_Sheet.docx
Normal file
BIN
Beginner Exercises and Answers/CSS_Beginner_Exercises.docx
Normal file
BIN
Beginner Exercises and Answers/HTML_Beginner_Answer_Sheet.docx
Normal file
BIN
Beginner Exercises and Answers/HTML_Beginner_Exercises.docx
Normal file
546
CONTEXT.md
Normal file
@@ -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.*
|
||||||
BIN
Exercises and Answers/CSS_Answer_Sheet.docx
Normal file
BIN
Exercises and Answers/CSS_Exercises.docx
Normal file
BIN
Exercises and Answers/HTML_Answer_Sheet.docx
Normal file
BIN
Exercises and Answers/HTML_Exercises.docx
Normal file
BIN
Exercises and Answers/JavaScript_Answer_Sheet.docx
Normal file
BIN
Exercises and Answers/JavaScript_Exercises.docx
Normal file
BIN
Gumroad_Sales_Strategy.docx
Normal file
BIN
Monetising_Your_Programming_Knowledge.docx
Normal file
BIN
Programming Notes/CSS_Fundamentals.docx
Normal file
BIN
Programming Notes/HTML_Fundamentals.docx
Normal file
BIN
Programming Notes/JavaScript_Fundamentals.docx
Normal file
BIN
images/covers/cover_bundle_beginner.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
images/covers/cover_bundle_complete.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/covers/cover_bundle_css.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
images/covers/cover_bundle_html.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
images/covers/cover_bundle_intermediate.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/covers/cover_bundle_js.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/covers/cover_bundle_notes.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/covers/cover_css_answers.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/covers/cover_css_beginner_answers.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
images/covers/cover_css_beginner_exercises.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/covers/cover_css_exercises.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
images/covers/cover_css_fundamentals.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
images/covers/cover_gumroad_strategy.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
images/covers/cover_html_answers.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/covers/cover_html_beginner_answers.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/covers/cover_html_beginner_exercises.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
images/covers/cover_html_exercises.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
images/covers/cover_html_fundamentals.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
images/covers/cover_js_answers.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/covers/cover_js_beginner_answers.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
images/covers/cover_js_beginner_exercises.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/covers/cover_js_exercises.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/covers/cover_js_fundamentals.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/covers/cover_monetising_guide.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
204
scripts/gen_covers.py
Normal file
@@ -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}")
|
||||||