Build a Weekly Content Calendar: platform-colour-coded posts an operator edits in place
- Level
- intermediate
- Time to complete
- 40 min
- Prerequisites
- 4
What you will buildA seven-day grid where each drafted post is a clickable block, colour-coded by platform, edited in a modal, and copied out to post. About 40 minutes.
What You Will Build
You will build a weekly content calendar: a seven-day grid where each drafted post is a clickable block, colour-coded by platform, that a non-technical operator clicks to edit in a modal and then copies out to post by hand.
It is deliberately small and framework-light. The examples are React with plain inline styles so the structure is visible; drop them into whatever component system you already use. By the end you will have a week grid, platform-coloured post chips that stay readable without colour, an overflow collapse for busy days, a click-to-edit modal, and designed empty states - the parts the best calendars share.
Auto-publishing is out of scope. This calendar plans and edits; posting is a copy-paste the operator does themselves.
Prerequisites
- A React setup you can render a component in (any bundler, or a sandbox).
- Comfort with component state - the whole calendar is driven by one array of post objects.
- A handful of sample posts to render. We will define them inline.
- No backend. Persisting posts to a database is a later, separate step.
Tip
Build it with two or three sample posts first, hard-coded. Everything here - the grid, the chip, the modal - is just a function of that array. Once it renders, swapping the array for data from your own store is a one-line change, not a rewrite.
Mental Model
A calendar is four visual jobs done at once. Position carries when: a post in the Wednesday column, partway down, reads as Wednesday afternoon without anyone parsing a date. An icon carries what platform it is for. Colour carries a category - here, the platform - but colour never travels alone, because a meaning carried by colour alone is invisible to a colour-blind operator and illegal under the accessibility standard. And a click carries the depth: the chip shows one line, the full post opens on demand.
So the data model is the spine. One array of posts, each with a day, a time, a platform, a caption, and a status. Every visible thing - a chip, a column, the modal - is a pure function of that array. Get the array right and the UI is just rendering.
Steps
Eight steps. The first two are free; the colour system, overflow, and modal unlock with Supporter. Build them in order - each one renders from the same posts array.
Lay out the week grid
Start with the data and a seven-column grid. The grid columns are the days; position in a column is the time.
const POSTS = [
{ id: "p1", day: 2, time: "09:30", platform: "linkedin", caption: "Launch recap", status: "scheduled" },
{ id: "p2", day: 2, time: "12:00", platform: "x", caption: "Thread on memory", status: "drafted" },
{ id: "p3", day: 4, time: "08:00", platform: "substack", caption: "Weekly essay", status: "idea" },
];
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
function WeekGrid({ posts }) {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 8 }}>
{DAYS.map((label, day) => (
<div key={day} style={{ border: "1px solid #E9E6F0", borderRadius: 12, minHeight: 320, padding: 8 }}>
<div style={{ fontWeight: 700, marginBottom: 8 }}>{label}</div>
{posts.filter((p) => p.day === day).sort((a, b) => a.time.localeCompare(b.time))
.map((p) => <Chip key={p.id} post={p} />)}
</div>
))}
</div>
);
}
You know it worked when you see seven day columns and the three sample posts land in the Tuesday and Thursday columns, in time order.
Render one post as a chip
A chip shows four things on one line: the platform icon, the time, a one-line caption, and a status marker. Keep it to a single line; the rest opens on click.
function Chip({ post, onOpen }) {
return (
<button
onClick={() => onOpen && onOpen(post)}
style={{ display: "flex", alignItems: "center", gap: 6, width: "100%",
textAlign: "left", border: "1px solid #E9E6F0", borderRadius: 8,
padding: "6px 8px", marginBottom: 6, background: "#FFFFFF", cursor: "pointer" }}
>
<PlatformIcon platform={post.platform} />
<span style={{ fontWeight: 700 }}>{post.time}</span>
<span style={{ flex: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{post.caption}
</span>
<StatusDot status={post.status} />
</button>
);
}
You know it worked when each post is one tidy line that truncates a long caption with an ellipsis instead of wrapping.
Sources
- NN/g: 2D position is the strongest pre-attentive channel; colour encodes categoriesnngroup.com
- WCAG 2.1 SC 1.4.1: colour must not be the only means of conveying informationw3.org
- Okabe-Ito: an 8-colour categorical palette that stays distinct for colour-vision deficiencysci-draw.com
- Google Calendar: +N more overflow collapses extra events in a day cellsupport.google.com
- NN/g: empty states must communicate status, teach what belongs, and offer the next actionnngroup.com
- Modal vs drawer: a drawer preserves context; a modal suits focused or destructive actionsonething.design
- ClickUp: re-encode the same grid by a selectable colour source (the colour-by toggle)help.clickup.com
- Buffer: a No Date side rail holds undated drafts you drag onto the calendarsupport.buffer.com