Skip to content

Add memo engagement, Local Docker-Compose rig (frontend)#16

Open
jnmclarty wants to merge 2 commits into
BuildCanada:mainfrom
jnmclarty:jeff/add-endorse-critique
Open

Add memo engagement, Local Docker-Compose rig (frontend)#16
jnmclarty wants to merge 2 commits into
BuildCanada:mainfrom
jnmclarty:jeff/add-endorse-critique

Conversation

@jnmclarty
Copy link
Copy Markdown

Memo engagement: Endorse + Critique buttons

Adds an engagement section at the bottom of every memo page with Endorse and Critique buttons. Both modals route the user through a LinkedIn OIDC popup hosted by york_factory; on success the modal collects a Canadian postal code (and a critique body) and submits to the new york_factory endpoints. Counts and the 5 most recent endorsers/approved critiques render server-side; the user's own action is reflected immediately via
optimistic update + on-demand cache revalidation.

What's in this PR

  • MemoEngagement.tsx (new client island, src/app/memos/[slug]/) — buttons, dialog, popup-driven LinkedIn OAuth, postal-code validation, optimistic insert, sonner toasts. Critique bodies are cropped to the first sentence with a small expand-icon that opens the full body in a secondary modal — handles long critiques without dwarfing the page.
  • api/revalidate/route.ts (new) — Bearer-token-protected endpoint that calls revalidateTag(). Allows york_factory to bust the per-memo cache tag immediately when an endorsement is added or a critique is approved.
  • fetchMemo now tags its fetch with memo:<slug> so revalidation can target a single memo.
  • apiPost helper added to src/lib/api/client.ts (JSON, cache: 'no-store', throws typed ApiPostError on non-2xx so the modal can branch on 409).
  • Type updates: YFMemoDetail extended with endorsements_count, critiques_count, recent_endorsers, critiques. Mapped through fetchMemo.
  • Hydration-safe date formatting (timeZone: "UTC") so SSR and client render identical strings.
  • Re-uses the existing @base-ui/react/dialog pattern from SubscribeModal.tsx. No new dependencies.

New environment variables

Variable Example Purpose
REVALIDATE_SECRET (secret) Bearer token accepted by /api/revalidate. Must match york_factory's NEXTJS_REVALIDATE_SECRET.
NEXT_PUBLIC_YORK_FACTORY_ORIGIN https://yorkfactory.buildcanada.com Trusted origin for postMessage from the LinkedIn callback popup. Optional — defaults to the origin parsed from YORK_FACTORY_API_URL, override only if the API is reverse-proxied under a different public origin.

Existing YORK_FACTORY_API_URL is unchanged.

Required york_factory env vars (cross-repo)

This PR depends on the corresponding york_factory PR being deployed and configured with:

  • LINKEDIN_CLIENT_ID
  • LINKEDIN_CLIENT_SECRET
  • LINKEDIN_REDIRECT_URI — must point to the york_factory linkedin/callback route
  • LINKEDIN_POSTMESSAGE_ORIGIN — must equal the production TradingPost origin (e.g. https://buildcanada.com)
  • NEXTJS_REVALIDATE_URL — must point to this app's /api/revalidate
  • NEXTJS_REVALIDATE_SECRET — must equal REVALIDATE_SECRET here

LinkedIn app registration steps live in the york_factory PR.

Deployment

This PR can ship independently of york_factory but the engagement buttons will display zero counts until york_factory's migrations + endpoints are live. Recommended order:

  1. Merge & deploy the york_factory PR (run migrations).
  2. Set REVALIDATE_SECRET and NEXT_PUBLIC_YORK_FACTORY_ORIGIN (if needed) in Vercel project env.
  3. Set NEXTJS_REVALIDATE_URL and NEXTJS_REVALIDATE_SECRET in york_factory (Kamal secrets), then redeploy york_factory so the revalidator picks them up.
  4. Merge & deploy this PR.
  5. Open any memo page in production, smoke-test the flow (see below).

Testing

Local (against live york_factory dev)

# in TradingPost
cp .env.local.example .env.local   # if not present
echo 'REVALIDATE_SECRET=dev-revalidate-secret' >> .env.local
pnpm dev:remote

Then in york_factory:

# .env additions
LINKEDIN_CLIENT_ID=...
LINKEDIN_CLIENT_SECRET=...
LINKEDIN_REDIRECT_URI=http://localhost:3000/api/v1/auth/linkedin/callback
LINKEDIN_POSTMESSAGE_ORIGIN=http://localhost:5050
NEXTJS_REVALIDATE_URL=http://localhost:5050/api/revalidate
NEXTJS_REVALIDATE_SECRET=dev-revalidate-secret

bin/rails s

Open http://localhost:5050/memos/<any-slug> and walk through the steps below.

Manual flow

  1. Section renders at the bottom of the memo, after the body, with current counts and "No endorsements yet" / "No critiques yet" if empty.
  2. Endorse: click → modal opens with "Continue with LinkedIn" → popup completes → modal swaps to read-only LinkedIn payload + postal-code field → submit M5V 3A8 → toast "Thanks for endorsing this memo.", count increments, name appears at the top of the recent list.
  3. Duplicate endorsement: click Endorse again as the same LinkedIn user → toast "You've already endorsed this memo."
  4. Critique: click → fill body, submit → toast "Critique submitted for review.", optimistic row appears with yellow "Pending review" badge. Refresh the page — the pending row disappears (only approved critiques are public).
  5. Admin approve (in york_factory /admin/critiques) → approve the critique → within ~1s the TradingPost memo page, on next refresh, shows the critique without the pending badge. Verify revalidateTag fired by checking the TradingPost server logs for POST /api/revalidate with 200 { ok: true, revalidated: ["memo:<slug>"] }.
  6. Long critique: critique with multiple sentences shows only the first sentence followed by a small expand icon. Click → modal opens with the full body, scrollable.
  7. Invalid postal code: enter 12345 → client-side error "Please enter a valid Canadian postal code (e.g. A1A 1A1).", no network call.
  8. Popup blocked: block popups in the browser → click Endorse → toast "Popup blocked. Please allow popups for this site and try again."
  9. Wrong origin postMessage: in DevTools console run window.postMessage({type:"linkedin-verified", verifiedTicket:"x", payload:{}}, "*") from a tab on a different origin (or simulate by editing NEXT_PUBLIC_YORK_FACTORY_ORIGIN) — the modal should ignore it.

Build / lint

pnpm lint
pnpm build

Should be clean. The new route lives under src/app/api/revalidate/route.ts and is opted out of static rendering (dynamic = "force-dynamic").

Cache-bust smoke test

# Hit the endpoint directly to confirm it works:
curl -i -X POST http://localhost:5050/api/revalidate \
  -H "Authorization: Bearer dev-revalidate-secret" \
  -H "Content-Type: application/json" \
  -d '{"tag":"memo:test-memo-title"}'

# Expect: 200 { ok: true, revalidated: ["memo:test-memo-title"] }
# Without the bearer: 401
# With REVALIDATE_SECRET unset: 503

Rollback

git revert <merge-commit> and redeploy. The york_factory engagement endpoints can stay live — they're harmless without a frontend consumer. The /api/revalidate route disappearing means cache-bust calls from york_factory will start failing with 404; logged as a warning by NextjsRevalidator, no impact on user-facing requests.

Notes

  • The memo page remains statically generated via generateStaticParams(). The 5-minute ISR window (revalidate: 300 in fetchMemo) is now made fresh on demand by tag-based revalidation when engagement changes — so users see new endorsements/critiques within seconds, not minutes.
  • The fetchMemoEngagement helper in src/lib/api/memos.ts is exported but currently unused; it's there for a future variant of the page that wants always-fresh counts at SSR time.

Screenshots

At the bottom of the memo:

image

Note that critiques are cropped to the first sentence.

Expanded Critique (pop-out modal)

image

Endorse/Critique Modals

image image image image

@jnmclarty
Copy link
Copy Markdown
Author

See BuildCanada/york_factory#38 for backend.

variant="ghost"
onClick={() => setOpenKind("critique")}
>
Critique this memo
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some substansive copy about what makes a good critique?

We won't publish ad hominens.

In that vein, they should also directly address the memo content and propose a different alternative to the problem specified or challenge the premise of the memo itself.

We will not publish anything that is self-promotional or that tries to leverage the platform for someone's own personal gain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants