Shipping the Projects Feature: From Schema to Showcase
Shipping the Projects Feature: From Schema to Showcase
March 19, 2026 — One of the items on my roadmap since the very beginning was a proper projects showcase. Today it shipped — from empty database table to fully functional admin UI and public-facing portfolio grid. Here’s how it came together.
The Requirements
Before writing any code, I mapped out what a “project” actually needs to represent in the context of a professional portfolio:
- Title and slug for public URLs
- Description (Markdown) and excerpt for cards
- Status: active, inactive, completed, archived — because not every project is eternal
- Start/end dates — with “active” projects having no end date
- External URL — many projects live elsewhere (GitHub, company sites)
- Logo/hero image — selected from the media library, not just a URL field
- Tags — freeform labels like “open-source”, “web”, “data”
- Skills — linked to the global skills table via many-to-many
- Work Experience link — optionally associate a project with a company/institution
- Image gallery — for project screenshots and visuals
The Schema Design
Three tables were needed:
projects
The main table with all the metadata — title, slug, description, excerpt, external_url, status (Ecto Enum), dates, logo_url, tags (Postgres array), published flag, display_order, and an optional work_experience_id foreign key.
project_skills
A join table for the many-to-many relationship with skills. Just two foreign keys with a unique index — simple but powerful.
project_images
A gallery table that supports both media library references (media_id) and direct URLs.
The Ecto Schema: Associations Done Right
many_to_many :skills, Skill,
join_through: "project_skills",
on_replace: :delete
This is the key trick — when you update a project’s skills, Ecto automatically handles inserting and deleting join table rows.
The Admin UI: Media Library Picker
The most interesting UI challenge was the logo image selector — a preview thumbnail, a “Choose Image” button opening a modal with URL input and a scrollable grid of media library images, plus a “Remove” button.
The skills selector uses checkbox cards in a responsive grid — each skill gets a checkbox, and checked skills are tracked in a @selected_skill_ids assign.
The Public Pages
Projects Index (/projects)
A responsive grid (1→2→3 columns) where each project card shows logo, title, date range, excerpt, skill pills (purple), and tag pills (teal).
Project Detail (/projects/:slug)
A single-column article-style layout with title, metadata, skills, tags, Markdown description, and image gallery.
What I Learned
-
Many-to-many in Ecto is elegant with
put_assocandon_replace: :delete -
HTML forms send
""for empty selects — scrub before Ecto cast -
URL validation must accept local
/uploads/paths when serving media internally - Reusable admin patterns accelerate every new feature
From empty table to public portfolio in one session — that’s the Phoenix developer experience.