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.

Seven day columns; a post lands in its day and sorts by time. Position carries when.

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

  1. NN/g: 2D position is the strongest pre-attentive channel; colour encodes categoriesnngroup.com
  2. WCAG 2.1 SC 1.4.1: colour must not be the only means of conveying informationw3.org
  3. Okabe-Ito: an 8-colour categorical palette that stays distinct for colour-vision deficiencysci-draw.com
  4. Google Calendar: +N more overflow collapses extra events in a day cellsupport.google.com
  5. NN/g: empty states must communicate status, teach what belongs, and offer the next actionnngroup.com
  6. Modal vs drawer: a drawer preserves context; a modal suits focused or destructive actionsonething.design
  7. ClickUp: re-encode the same grid by a selectable colour source (the colour-by toggle)help.clickup.com
  8. Buffer: a No Date side rail holds undated drafts you drag onto the calendarsupport.buffer.com