feat: broaden from postcards to greeting cards (Grußkarten) — rebrand + card types #10

Closed
opened 2026-06-16 15:47:18 +00:00 by mAi · 3 comments
Collaborator

What

The whole app is currently framed as Postkarte (title 'Postkarte – eine Grußkarte zum Verschicken', postcard-centric copy/themes). m wants it broadened to greeting cards in general (Grußkarten) — birthdays, Christmas, invitations, thank-you, get-well, etc. — not just postcards.

The app is already generic underneath (text + media + layouts + custom links). This is mostly framing + a card-type concept + occasion-fitting design, not a rewrite.

New home domain is sendmy.cards (generic), so the branding is no longer tied to 'post'.

Depth — OPEN DECISION (m being asked via PWA chips)

Three levels, each building on the previous:

  • (a) Rebrand only — swap 'Postkarte' framing for 'Grußkarte' across copy/branding (title, landing, labels). Minimal; the app already sends any text+media.
  • (b) + card-type / occasion selector — at compose, sender picks a type (Geburtstag, Weihnachten, Einladung, Dankeschön, Gute Besserung, Liebe, Allgemein, Postkarte, …). Drives copy/labels and a sensible default theme per occasion. Persist the type with the card. Medium.
  • (c) + occasion-specific designs — each occasion gets its own themes/templates/visual identity (birthday, christmas, wedding…). Design-heavy.

This issue will be scoped to m's chosen depth before building.

Sequencing

Overlaps the compose flow + templates + branding → after #8 (/to/ /from/) and #9 (link lifecycle) to avoid colliding with the active short-link work. Filed now to capture the direction; otto-head scopes + assigns once the short-link track lands and m has picked the depth.

Constraints (carry over)

  • Mobile-first, German UI + Umlaute, renamable. Coexists with everything shipped: layouts (single/gallery/flip), broadcast, video, image lightbox, custom /to//from short links.
  • If a card-type column is added: idempotent migration, postgres-role ownership (see #1-#4 history).

DoD (placeholder — finalized once depth is chosen)

  • App reads as a general greeting-card tool (not postcard-only); existing cards keep working.
  • (If b/c) sender can pick a card type/occasion at compose; it's stored + reflected in the rendered card.
  • build/vet/test green; e2e verified; test data cleaned. Commit references this issue.
## What The whole app is currently framed as **Postkarte** (title 'Postkarte – eine Grußkarte zum Verschicken', postcard-centric copy/themes). m wants it broadened to **greeting cards in general** (Grußkarten) — birthdays, Christmas, invitations, thank-you, get-well, etc. — not just postcards. The app is already generic underneath (text + media + layouts + custom links). This is mostly **framing + a card-type concept + occasion-fitting design**, not a rewrite. New home domain is **sendmy.cards** (generic), so the branding is no longer tied to 'post'. ## Depth — OPEN DECISION (m being asked via PWA chips) Three levels, each building on the previous: - **(a) Rebrand only** — swap 'Postkarte' framing for 'Grußkarte' across copy/branding (title, landing, labels). Minimal; the app already sends any text+media. - **(b) + card-type / occasion selector** — at compose, sender picks a type (Geburtstag, Weihnachten, Einladung, Dankeschön, Gute Besserung, Liebe, Allgemein, Postkarte, …). Drives copy/labels and a sensible default theme per occasion. Persist the type with the card. Medium. - **(c) + occasion-specific designs** — each occasion gets its own themes/templates/visual identity (birthday, christmas, wedding…). Design-heavy. **This issue will be scoped to m's chosen depth before building.** ## Sequencing Overlaps the compose flow + templates + branding → **after #8 (/to/ /from/) and #9 (link lifecycle)** to avoid colliding with the active short-link work. Filed now to capture the direction; otto-head scopes + assigns once the short-link track lands and m has picked the depth. ## Constraints (carry over) - Mobile-first, German UI + Umlaute, renamable. Coexists with everything shipped: layouts (single/gallery/flip), broadcast, video, image lightbox, custom /to//from short links. - If a card-type column is added: idempotent migration, postgres-role ownership (see #1-#4 history). ## DoD (placeholder — finalized once depth is chosen) - App reads as a general greeting-card tool (not postcard-only); existing cards keep working. - (If b/c) sender can pick a card type/occasion at compose; it's stored + reflected in the rendered card. - build/vet/test green; e2e verified; test data cleaned. Commit references this issue.
Author
Collaborator

m's decision on depth (PWA, 2026-06-16): Option (b) + Custom.

Scope is now: a card-type / occasion selector at compose with a preset starter set (e.g. Geburtstag, Weihnachten, Einladung, Dankeschön, Gute Besserung, Liebe, Allgemein, Postkarte) plus a 'Custom' / 'Eigener Anlass' option where the sender types their own occasion label freely.

  • Preset occasions: drive copy/labels + a sensible default theme per occasion (occasion-specific designs = the deeper (c), out of scope here — themes from the existing set are fine).
  • Custom: free-text occasion name, neutral/default theme. Persist the chosen type (and the custom label) with the card; reflect it in the rendered card.
  • Plus the overall rebrand from 'Postkarte'-only framing to general 'Grußkarte' (the app is now on sendmy.cards).

This makes #10 buildable. Still sequenced after #8/#9 (compose-flow overlap).

**m's decision on depth (PWA, 2026-06-16): Option (b) + Custom.** Scope is now: a **card-type / occasion selector** at compose with a preset starter set (e.g. Geburtstag, Weihnachten, Einladung, Dankeschön, Gute Besserung, Liebe, Allgemein, Postkarte) **plus a 'Custom' / 'Eigener Anlass' option** where the sender types their own occasion label freely. - Preset occasions: drive copy/labels + a sensible default theme per occasion (occasion-specific *designs* = the deeper (c), out of scope here — themes from the existing set are fine). - **Custom**: free-text occasion name, neutral/default theme. Persist the chosen type (and the custom label) with the card; reflect it in the rendered card. - Plus the overall rebrand from 'Postkarte'-only framing to general 'Grußkarte' (the app is now on sendmy.cards). This makes #10 buildable. Still sequenced after #8/#9 (compose-flow overlap).
mAi self-assigned this 2026-06-16 16:13:24 +00:00
Author
Collaborator

#10 done — greeting-card broadening, depth (b)+Custom

Built to m's PWA decision (2026-06-16): occasion selector (presets + free-text custom) driving copy/default-theme, persisted + reflected on the card, plus the Postkarte → Grußkarte rebrand. Deeper occasion-specific designs = (c) stay out of scope.

Commit: a325d01d28
Branch: mai/hermes/issue-10-feat-broaden (pushed; not merged — otto-head merges/deploys)

What shipped

1. Rebrand (Postkarte → Grußkarte). User-facing framing is now general greeting cards across landing, compose, rendered card and not-found — brand 💌 Grußkarte, hero/steps copy mentioning Geburtstag/Weihnachten/Einladung/Dankeschön, page titles + meta description. Internal names (postcards schema, bucket, Go module) stay as-is — only the framing changed, so no infra churn.

2. Occasion / card-type concept. New Occasion domain type with a preset set + a free-text custom escape hatch:

Occasion Badge label Card noun Icon Default theme
Allgemein (default) (no badge) Grußkarte 💌 classic
Geburtstag Geburtstag Geburtstagskarte 🎂 sky
Weihnachten Weihnachten Weihnachtskarte 🎄 classic
Einladung Einladung Einladung 🎉 dusk
Dankeschön Dankeschön Dankeskarte 🙏 classic
Gute Besserung Gute Besserung Genesungskarte 🍀 sky
Liebe Liebe Liebeskarte ❤️ classic
Postkarte Postkarte Postkarte 📮 sky
Eigener Anlass sender's free text Grußkarte classic
  • Compose offers an occasion picker (icon tiles, 2/row mobile · 3/row wider). Picking an occasion pre-selects its default colour world (JS enhancement — the sender can still override the theme; with no JS the theme is simply whatever radio is checked). Choosing Eigener Anlass reveals a free-text label field.
  • The occasion is reflected on the rendered card: a small accent badge above the message (icon + label) and the occasion noun in the page <title> + OG title (e.g. „Du hast eine Geburtstagskarte bekommen 🎂). The neutral Allgemein default renders no badge, so old cards look exactly as before.

3. Persistence — idempotent migration 006_occasion.sql. occasion text NOT NULL DEFAULT 'allgemein' + nullable occasion_label. Additive ALTER keeps the table's postgres owner (same note as 002/003/005), so it's safe for the old running binary, and existing rows backfill to the neutral allgemein. Occasion is normalised on both write and read, so an unknown/hand-edited value can never break the renderer (falls back to general). Custom-with-empty-label degrades cleanly to general.

Verification

  • go build / go vet / go test ./... -race — all green. New unit tests (NormalizeOccasion, DefaultThemeFor, Occasions, OccasionView) + render tests (compose selector present, error-re-render prefill, rendered badge/title, general = no badge).
  • E2e against a throwaway local Postgres (no prod touch): all 6 migrations applied on startup; composed a preset card (→ occasion=geburtstag, badge 🎂 Geburtstag, title Geburtstagskarte), a custom card (→ occasion=custom + label Schulabschluss, badge ✨ Schulabschluss, generic noun), and a general card (→ occasion=allgemein, no badge); an old-binary-style insert with no occasion defaulted to allgemein; a hand-edited bogus occasion still returns 200 with no badge. Test container + temp files removed afterwards.

DoD

  • App reads as a general greeting-card tool; existing cards keep working (neutral default, no badge).
  • Sender picks a card type/occasion (preset or custom) at compose; stored + reflected in the rendered card.
  • build/vet/test green; e2e verified; test data cleaned; commit references this issue.

Notes for otto-head

  • Not merged/deployed — that's yours. On deploy, migration 006 applies automatically on startup (additive, safe with the old binary still serving).
  • No new env, no Shlink/Traefik changes; the occasion feature is unconditional (no feature flag).
  • sendmy.cards DNS/TLS is still the open infra item carried from #8 — unrelated to this change.

(m/postcards has no Gitea labels — no label state machine to update; signalling via this comment + commit link + task status, per prior shifts.)

## #10 done — greeting-card broadening, depth (b)+Custom Built to m's PWA decision (2026-06-16): **occasion selector (presets + free-text custom) driving copy/default-theme, persisted + reflected on the card, plus the Postkarte → Grußkarte rebrand.** Deeper occasion-specific *designs* = (c) stay out of scope. **Commit:** https://mgit.msbls.de/m/postcards/commit/a325d01d2845a439dde0448590600cd46afd056d **Branch:** `mai/hermes/issue-10-feat-broaden` (pushed; not merged — otto-head merges/deploys) ### What shipped **1. Rebrand (Postkarte → Grußkarte).** User-facing framing is now general greeting cards across landing, compose, rendered card and not-found — brand `💌 Grußkarte`, hero/steps copy mentioning Geburtstag/Weihnachten/Einladung/Dankeschön, page titles + meta description. Internal names (`postcards` schema, bucket, Go module) stay as-is — only the framing changed, so no infra churn. **2. Occasion / card-type concept.** New `Occasion` domain type with a preset set + a free-text custom escape hatch: | Occasion | Badge label | Card noun | Icon | Default theme | |---|---|---|---|---| | Allgemein *(default)* | — *(no badge)* | Grußkarte | 💌 | classic | | Geburtstag | Geburtstag | Geburtstagskarte | 🎂 | sky | | Weihnachten | Weihnachten | Weihnachtskarte | 🎄 | classic | | Einladung | Einladung | Einladung | 🎉 | dusk | | Dankeschön | Dankeschön | Dankeskarte | 🙏 | classic | | Gute Besserung | Gute Besserung | Genesungskarte | 🍀 | sky | | Liebe | Liebe | Liebeskarte | ❤️ | classic | | Postkarte | Postkarte | Postkarte | 📮 | sky | | **Eigener Anlass** | *sender's free text* | Grußkarte | ✨ | classic | - Compose offers an occasion picker (icon tiles, 2/row mobile · 3/row wider). Picking an occasion **pre-selects its default colour world** (JS enhancement — the sender can still override the theme; with no JS the theme is simply whatever radio is checked). Choosing *Eigener Anlass* reveals a free-text label field. - The occasion is **reflected on the rendered card**: a small accent badge above the message (icon + label) and the occasion noun in the page `<title>` + OG title (e.g. *„Du hast eine Geburtstagskarte bekommen 🎂“*). The neutral *Allgemein* default renders **no badge**, so old cards look exactly as before. **3. Persistence — idempotent migration `006_occasion.sql`.** `occasion text NOT NULL DEFAULT 'allgemein'` + nullable `occasion_label`. Additive `ALTER` keeps the table's `postgres` owner (same note as 002/003/005), so it's safe for the old running binary, and existing rows backfill to the neutral `allgemein`. Occasion is normalised on **both write and read**, so an unknown/hand-edited value can never break the renderer (falls back to general). Custom-with-empty-label degrades cleanly to general. ### Verification - `go build` / `go vet` / `go test ./... -race` — all green. New unit tests (`NormalizeOccasion`, `DefaultThemeFor`, `Occasions`, `OccasionView`) + render tests (compose selector present, error-re-render prefill, rendered badge/title, general = no badge). - **E2e** against a throwaway local Postgres (no prod touch): all 6 migrations applied on startup; composed a preset card (→ `occasion=geburtstag`, badge `🎂 Geburtstag`, title *Geburtstagskarte*), a custom card (→ `occasion=custom` + label `Schulabschluss`, badge `✨ Schulabschluss`, generic noun), and a general card (→ `occasion=allgemein`, **no badge**); an old-binary-style insert with no occasion defaulted to `allgemein`; a hand-edited bogus occasion still returns 200 with no badge. Test container + temp files removed afterwards. ### DoD - [x] App reads as a general greeting-card tool; existing cards keep working (neutral default, no badge). - [x] Sender picks a card type/occasion (preset or custom) at compose; stored + reflected in the rendered card. - [x] build/vet/test green; e2e verified; test data cleaned; commit references this issue. ### Notes for otto-head - Not merged/deployed — that's yours. On deploy, migration 006 applies automatically on startup (additive, safe with the old binary still serving). - No new env, no Shlink/Traefik changes; the occasion feature is unconditional (no feature flag). - sendmy.cards DNS/TLS is still the open infra item carried from #8 — unrelated to this change. *(m/postcards has no Gitea labels — no label state machine to update; signalling via this comment + commit link + task status, per prior shifts.)*
Author
Collaborator

Merged & deployed — greeting-card broadening live on sendmy.cards

Merged a325d01 (merge 2354e70), deployed. Migration 006 applied. Verified live: landing now reads Grußkarte, compose has the occasion picker, a Geburtstag card renders title „eine Geburtstagskarte“ + 🎂 badge. Neutral Allgemein default = no badge (old cards unchanged). Custom free-text occasion works. Test data cleaned. build/vet/test -race green.

Built exactly to m's depth decision (b)+Custom. Not closing — m closes.

## ✅ Merged & deployed — greeting-card broadening live on sendmy.cards Merged `a325d01` (merge `2354e70`), deployed. Migration 006 applied. Verified live: landing now reads **Grußkarte**, compose has the occasion picker, a Geburtstag card renders title *„eine Geburtstagskarte“* + 🎂 badge. Neutral *Allgemein* default = no badge (old cards unchanged). Custom free-text occasion works. Test data cleaned. build/vet/test -race green. Built exactly to m's depth decision (b)+Custom. Not closing — m closes.
mAi closed this issue 2026-06-23 15:48:02 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/postcards#10
No description provided.