Porto Chapter — Group Calendar

A single-file, interactive web calendar that turns a sprawling group-trip Slack channel into a four-week, at-a-glance planner. Built for a friend group travelling in Porto so nobody misses an activity and time collisions are obvious before they become double-bookings.

Deliverable: porto-chapter-calendar.html — one self-contained file, no build step, no dependencies. Open it in any modern browser.


1. Original goals

The brief was to analyse the channel message list and generate a calendar that differentiates activities by commitment level. The explicit requirements, and how each is met:

Requirement from the brief How it’s addressed
Differentiate confirmed / pending / placeholder Three visual statuses — colour + fill + border style — plus filter toggles.
Main goal: see what’s available at a glance; avoid missing things Whole trip fits on one screen as a 4-week grid; a “replies due today” banner surfaces time-critical RSVPs.
Prevent collisions, or see them clearly when they exist Automatic clash detection by time-of-day slot, shown as per-day badges and per-tile markers, with a drill-down that lists exactly what conflicts.
Relevant for ~4 weeks, may span two months Fixed range Mon 6 Jul → Sun 2 Aug 2026; renders month boundaries (Aug days are muted/out-of-range). No multi-month scaling needed.
Favour the overall picture but allow dynamic drill-down Grid gives the overview; clicking any tile or day badge opens a detail drawer with the full context.
Tell apart long vs short activities (full-day vs a block vs a 1-hour/deadline item) Duration is encoded independently of status: full-width bars, timed blocks, and slim pills.

2. Source data & date reconstruction

The Slack export gives message timestamps and relative language (“tonight”, “tomorrow”, “next week”) but no absolute dates on most events. Dates were reconstructed from internal evidence rather than guessed:

Two interpretive calls are documented as assumptions in §7 because the thread doesn’t state them outright.


3. Information model

Every activity is classified on two independent axes, plus an optional third (deadlines).

3.1 Status — how locked-in is it?

Status Meaning Visual
Confirmed Booked or decided (tickets being purchased, slot fixed) Solid green fill, green left bar
Pending A binding post still inside its RSVP window Solid ochre fill, ochre left bar
Interest Interest post, poll, or floated idea — not committed Dashed blue outline, no fill

The fill weight itself signals commitment: interest reads as a “sketched, unglazed tile”, pending as warm, confirmed as fully glazed-in.

3.2 Length — how much of the day does it eat?

Length Examples Visual
Full day Douro tour, Chefs on Fire, day trips Full-width bar labelled “all day”
Part day (morning / afternoon / evening) Food tour, fado, cocktails Standard block with a time and a period label
Short Majestic coffee, Livraria slot Slim pill with just a start time

Time-of-day isn’t given its own colour to avoid overloading the eye; instead events sort chronologically within a cell (all-day → morning → afternoon → evening), so the day reads top-to-bottom.

3.3 Deadlines

Events carrying an RSVP cut-off (deadline) drive the red “replies due today” banner and a highlighted callout in the detail drawer. Three land on Wed 8 Jul (Douro 10:00, Livraria 12:00, Chefs on Fire menu 17:00).


4. Collision detection

Each event occupies time-of-day slots: full-day = morning + afternoon (evenings stay free, so a day tour followed by a night out is not flagged); a part/short event occupies only its own period; a poll with undecided timing occupies nothing.

Two events on the same date that share a slot produce a conflict, at one of two severities:

Conflicts surface at three levels of zoom: a day badge in the grid, an inset outline on each affected tile, and a cross-linked list inside the detail drawer.


5. Design decisions

5.1 Aesthetic direction — Porto azulejos

The visual identity is drawn from Porto’s own material world: the blue-and-white glazed azulejo tilework that covers the city (São Bento station being the famous example). This grounds the design in the subject rather than a generic calendar look, and deliberately avoids the default AI-design palette (cream background + high-contrast serif + terracotta accent), which would read as templated.

5.2 Layout

Overview-first: a fixed weekday header over four stacked week-rows. Multi-day events (the villa weekend) render as a single spanning bar in a dedicated lane above the day cells, so a continuous stay reads as one thing rather than three separate chips. Today is ringed; past days are muted; the Aug tail is shown as out-of-range hatching so the calendar edges are honest about the range.

5.3 Copy

Interface language names things by what the user controls (“Show past days”, “Replies due today”, “Reply by 12 PM”), states clash meaning plainly, and keeps the footer honest about provenance and the need to verify times before spending money.


6. Technical decisions


7. Assumptions & known limitations

These are judgement calls the thread doesn’t fully settle — worth confirming with the group:


8. Maintaining / updating the calendar

All content lives in the <script> block. To change the calendar, edit the data arrays — no other code needs touching.

Event object shape (EVENTS):

{
  id:      "douro",                 // unique string, used for cross-links
  title:   "Douro Valley boat tour & wine tasting",
  tag:     "DOURO",                 // short mono category label shown on the tile
  org:     "Emeraude L",            // who posted it
  status:  "pending",               // "confirmed" | "pending" | "interest"
  date:    "2026-07-11",            // ISO date (omit for floating items)
  len:     "fullday",               // "fullday" | "part" | "short"
  start:   "08:30",                 // 24h "HH:MM" (drives morning/afternoon/evening)
  end:     "16:30",                 // optional
  price:   "€90",                   // optional
  loc:     "Meet: Largo da Lapa 1", // optional
  link:    { label:"getyourguide.com", url:"https://..." },   // optional
  deadline:{ text:"Commit so the booking can go in",
             date:"2026-07-08", time:"10:00" },               // optional → feeds today-banner
  options: ["Mon 13 Jul", "Wed 15 Jul"],                      // optional, for polls
  noSlot:  true                     // optional: undecided time → excluded from clash detection
}

8.1 Add a new activity (step by step)

  1. Open porto-chapter-calendar.html and find the const EVENTS = [ ... ] array in the <script>.
  2. Copy an existing object and paste it as a new entry, then edit the fields. Only id, title, tag, org, status, and len are required — drop any optional field you don’t need.
  3. Give it a unique id (used for cross-links). Save and refresh the browser — that’s it. The tile, filters, deadline banner, and clash detection all pick it up automatically.
// Example: a confirmed morning pastel-de-nata run on Fri 17 Jul
{
  id:     "natas",
  title:  "Pastel de nata crawl",
  tag:    "FOOD",
  org:    "You",
  status: "confirmed",
  date:   "2026-07-17",
  len:    "short",
  start:  "10:00",
  loc:    "Manteigaria + Fábrica da Nata"
}

8.2 Add a new field, category, or status


9. File structure

porto-chapter-calendar.html   The entire app: markup, CSS, data, and logic in one file
README.md                     This document

Reconstructed from the #porto Slack thread. Statuses reflect the thread at the inferred “today” (Wed 8 Jul 2026); verify times and prices against the channel before committing money.