This is the first delivered slice of the skilll platform. It covers the Instructor’s ability to create and edit a Course — the top-level learning unit in the domain.
What was built
The Instructor can now:
- Create a Course with four required fields: title, description, price (€0 = free), and language
- Set visibility to public or private
- Save the Course as draft (the default state)
- Edit any field after creation
The system enforces all four required fields and rejects blank submissions with live validation.
Domain model
erDiagram
COURSE {
int id
string title
string description
int price
string language
string visibility
string status
datetime inserted_at
datetime updated_at
}
status is an enum: draft | published. visibility is public | private. Both are orthogonal — all four combinations are valid. Price = 0 is a valid free Course.
Architecture
flowchart LR
subgraph Web
LV[CourseLive.New / Edit]
FC[FormComponent]
end
subgraph Domain
CTX[Skilll.Courses context]
SCH[Course schema + changeset]
end
subgraph DB
PG[(PostgreSQL\ncourses table)]
end
LV --> FC --> CTX --> SCH --> PG
The LiveView is intentionally thin — it delegates all business rules to Skilll.Courses. The FormComponent is shared between New and Edit, receiving action: :new | :edit.
Screenshots
Empty state

The index page before any Course exists.
New Course form

Four required fields + visibility toggle. Price defaults to 0.
Validation

Submitting with blank title, description, or language shows inline errors. Price doesn't error because it defaults to 0.
Courses list

After creating two courses: one free, one at €149. Price=0 renders as "Free".
Edit Course

The edit page loads existing data. The header shows the current title and Draft badge.
Acceptance criteria
- Create a Course with title, description, price, and language (all required)
- Set visibility to public or private
- Draft is the default state on creation
- Edit Course fields after creation
- System prevents saving if any required field is missing
- Price = 0 is valid (free Course)
Key decisions
Price stored as integer cents — price is stored as a plain integer in PostgreSQL representing euro cents (e.g. 4900 = €49). Zero is a valid free Course. When Paddle comes in (PBI 0060), no migration is needed — Paddle also works in cents.
FormComponent shared between New and Edit — a single LiveComponent receives action: :new | :edit and dispatches to create_course/1 or update_course/2. No duplication in the web layer.
Next up: PBI 0015 — Build and Publish a Course (Chapters, Text Lessons, reordering, publish action).