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 with live counts.
Main goal: see what’s available at a glance; avoid missing things Whole trip fits on one screen as a 4-week grid; a collapsible “Ideas TBD” section surfaces undated activities.
Prevent collisions, or see them clearly when they exist Automatic overlap 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.

Note: The “pending” status is rarely used in practice. Most events are either confirmed (green) or interest (blue). The filter buttons show live counts for each status.

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 Category emojis

Each event title is prefixed with a category emoji for quick visual scanning. These represent the kind of activity (duration and context), not the specific content:

Emoji Category Examples
Sports / Match World Cup matches
🏄 Sports / Water Kayak
🏖️ Sports / Leisure Beach day
🧗 Sports / Climbing Via Ferrata
🏛️ City / Tour Food tour, bookstore, café
🚶 City / Walk Walking tour
🚌 Full-day trip Day tours, Chefs on Fire
🍽️ Food / Meal Brunch, francesinha lunch
🍸 Food / Drinks Cocktails, bar crawl
🎵 Entertainment / Music Fado concerts
🎭 Entertainment / Theatre Stand-up comedy
🎬 Entertainment / Cinema IMAX movie
💼 Professional / Skill share B2B sales, autism talk, SEO
🧳 Travel / Logistics Checkout
🏡 Accommodation Villa weekend
🎨 Workshop Tile-painting

3.4 Deadlines

Events carrying an RSVP cut-off (deadline) drive a highlighted callout in the detail drawer.


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:

All overlaps are treated as “competing options” rather than “hard clashes” since these are group activities where the group simply needs to choose. 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: each 7-day week splits into two grid rows, Mon–Thu (4 cells) then Fri–Sun (3 cells, centered at 85% width), so no row ever needs to shrink cells below a readable size. Weekday names live inside each day cell (no separate header row) since the two row shapes don’t share one column layout. 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; a fully-past row disappears entirely once “Show past rows” is off (default), rather than leaving a blank gap. The Aug tail is shown as out-of-range hatching so the calendar edges are honest about the range. Below 640px, both row shapes collapse to 2 columns; below 820px the page opens straight into Agenda view.

Weekend styling: Saturday and Sunday day numbers are displayed in firebrick red to visually distinguish weekends from weekdays.

Collapsible sections: The “Ideas TBD” section at the top is collapsed by default with a counter showing the number of undated activities. Click to expand and see the floating ideas.

Legend icon: A small blue ⓘ icon in the bottom-right of the hero section reveals a popover with clash indicator meanings on hover.

Status filters: The filter buttons (Confirmed / Pending / Interest) are consolidated into a single row with live counts, replacing the separate tally pills that were previously in the hero.

5.3 Copy

Interface language names things by what the user controls (“Show past rows”, “Reply by 12 PM”), states overlap 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",  // prefix with category emoji
  tag:     "DOURO",                 // short mono category label shown on the tile
  org:     "Emeraude L",            // who posted it
  status:  "confirmed",             // "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
  post:    "https://wifitribe.slack.com/archives/C0.../p123...", // optional, permalink to the source Slack post
  deadline:{ text:"Commit so the booking can go in",
             date:"2026-07-08", time:"10:00" },               // optional → feeds drawer callout
  options: ["Mon 13 Jul", "Wed 15 Jul"],                      // optional, for polls
  noSlot:  true                     // optional: undecided time → excluded from overlap 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. Prefix the title with a category emoji from the table in §3.3.
  4. Give it a unique id (used for cross-links). Save and refresh the browser — that’s it. The tile, filters, and overlap 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
Notes and ideas _jul26.md     Working scratchpad with decisions and troubleshooting
development_runbook_jul26.md  Detailed chronological log of changes

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.