Commit Graph

82 Commits

Author SHA1 Message Date
Arnas Donauskas
323e0f06cb
Merge pull request #121 from ArnasDon/feat/flows-clickable-validation-issues
feat(flows): clickable validation issues jump to node
2026-05-22 15:10:30 +03:00
Arnas Donauskas
b95ce8b372
Merge pull request #120 from ArnasDon/feat/flows-node-card-previews
feat(flows): show content preview in collapsed node cards
2026-05-22 15:06:38 +03:00
Arnas Donauskas
edbbb894f6
feat(flows): make validation issues clickable to jump to node
The validation panel sits at the bottom of the editor. When it surfaced
an error ("node X is missing next_node_key") the user had to scroll up,
find that node, expand it, find the broken field. Long feedback loop.

Each node-scoped IssueLine becomes a button: click it and the editor
expands the offending node, scrolls it into view, and briefly rings
its border so the eye lands on the right card. Trigger-scoped issues
stay non-interactive (the trigger panel is already at the top of the
page).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:05:44 +03:00
Arnas Donauskas
9c7f635a4d
Merge pull request #119 from ArnasDon/feat/flows-send-list-add-section
feat(flows): add multi-section support to send_list builder
2026-05-22 15:02:20 +03:00
Arnas Donauskas
6079bf2046
feat(flows): show content preview in collapsed node cards
A flow with 8 send_message nodes used to show 8 identical-looking pills
in the editor — the only way to tell them apart was to expand each
one. Adds a one-line summary below the meta label when the card is
collapsed:

- send_message → first 80 chars of text
- send_buttons → text + button titles
- send_list → text + option count
- collect_input → prompt + target var
- condition → "var.email == 'support@…'"
- set_tag → "Add tag 1f8c…"
- handoff → first 80 chars of note

Empty nodes (just added, no fields set) keep the old compact look so
you can tell at a glance what still needs configuration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:01:53 +03:00
Arnas Donauskas
5a9a69a3c5
feat(flows): add multi-section support to send_list builder
The data model and the runner already supported multiple sections in a
WhatsApp interactive list, but the builder UI exposed no way to add one
— it only let you add rows within the single default section. So users
couldn't build a categorized menu (Billing / Support / Sales) without
editing the DB directly.

Adds an "Add section" button below the sections list (capped at the
WhatsApp spec's 10) and a per-section remove button (shown when more
than one section exists).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:56:52 +03:00
Arnas Donauskas
268ae7076f
feat(flows): hide internal node_key + reply_id behind Advanced disclosure
Node key and per-button/row reply IDs are auto-generated and consumed
only by the runner — exposing them in the main form read as developer
leakage. Move them behind a per-node "Show advanced" toggle so the
default UI shows only customer-facing fields. Power users who need
stable IDs for analytics can still get them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:52:44 +03:00
Arnas Donauskas
ed15493f95
feat(flows): templates + v1.5 nodes (collect_input/condition/set_tag) + run-history viewer (PR 4/4)
Final feature PR before GA cleanup. Closes the gap between "flow
runner works" (PR 2) and "users have everything they need to build
real automation" — three new node types, plain-text sends actually
sending, three starter templates, a run-history page for debugging.

What landed:

  1. engineSendText helper (src/lib/flows/meta-send.ts) — wraps
     sendTextMessage with the phone-variant retry + DB persistence
     pattern. Unblocks send_message + collect_input nodes that
     previously auto-advanced without sending.

  2. Three new node types in the engine:
     - collect_input: sends a prompt, suspends, captures the next
       text reply into flow_runs.vars[var_key], advances. Supports
       {{vars.X}} interpolation in downstream prompts + handoff
       notes.
     - condition: branches on subject (var / tag / contact_field)
       with operators (equals / contains / present / absent).
       Auto-advancing, no Meta call. Pure predicate logic exported
       as evaluateConditionPredicate with 7 new unit tests.
     - set_tag: upsert/delete on contact_tags then advance. Non-
       fatal on write failure — logged + advances anyway so a tag
       glitch doesn't strand the customer.

  3. Real send_message — calls engineSendText, logs the result,
     handles errors by ending the run with status='failed'. Same
     {{vars.X}} interpolation.

  4. Three starter templates (src/lib/flows/templates.ts):
     - Welcome menu (the owner's brief example)
     - FAQ bot (keyword → list → answers → end)
     - Lead capture (first_inbound → collect_input chain → handoff
       with vars in the note)
     POST /api/flows now accepts {template_slug} to clone a
     template into a fresh draft. GET /api/flows/templates exposes
     the gallery shape (no nodes payload — fetched on actual clone).

  5. Builder UI extensions:
     - NODE_META + config sub-forms for collect_input, condition,
       set_tag.
     - Template gallery on the /flows New-flow dialog (loads from
       /api/flows/templates, falls back gracefully if absent).
     - "Runs" button in the editor header → /flows/[id]/runs.

  6. Run-history viewer at /flows/[id]/runs — newest-first list of
     the 50 most recent runs, with expandable per-run event
     timeline (started, node_entered, message_sent,
     reply_received, fallback_fired, handoff, timeout, error,
     completed). Useful for debugging "why didn't my flow advance?"
     from the brief's use case.

  7. Validator updates: new error rules for each new node type
     (prompt_text required, var_key alphanumeric, operator valid,
     true_next/false_next exist, etc.). outgoingEdges helper now
     covers collect_input/condition/set_tag for reachability.

All gated behind isFlowsEnabled(profile). Non-beta accounts see no
change. Run-history API is also 404 for non-beta.

Validation: 173/173 tests pass (added 7 new for the predicate);
typecheck + lint + build all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:27:01 +03:00
Arnas Donauskas
4871d7aa06
feat(flows): no-code builder UI + flows API + validator (PR 3/4)
Ships the surface that lets opted-in beta accounts build, save, and
activate WhatsApp conversation flows entirely from the web UI — no
SQL fixtures needed. Sidebar gets a "Flows" entry (hidden for
non-beta accounts), the /flows list page surfaces existing flows,
/flows/[id] is the linear-list editor.

Wraps the runner from PR 2 with everything a real user needs to
build the welcome-menu example end-to-end.

What landed:

  1. `src/lib/flows/validate.ts` — pure validator. Checks entry
     node exists, all next_node_key refs resolve, no unreachable
     nodes, Meta-limit violations (button title ≤20, ≤3 buttons,
     ≤10 list rows, ≤24 chars per list row). 24 unit tests.

  2. `src/app/api/flows/route.ts` — GET (list user's flows),
     POST (create draft).

  3. `src/app/api/flows/[id]/route.ts` — GET (load with nodes),
     PUT (replace name + trigger + nodes; delete-then-insert),
     DELETE (cascade clean-up).

  4. `src/app/api/flows/[id]/activate/route.ts` — status toggle.
     Activation runs the validator; returns 422 + the full issue
     list when blocked.

  5. `src/app/(dashboard)/flows/page.tsx` — list of cards, "New
     flow" dialog, delete with confirm.

  6. `src/app/(dashboard)/flows/[id]/page.tsx` — editor shell
     that loads data and hands it to FlowBuilder.

  7. `src/components/flows/flow-builder.tsx` — the editor itself.
     Header with name + save/activate/pause/delete; trigger
     panel; entry-node picker; node list with expandable per-node
     config forms (start, send_buttons, send_list, send_message,
     handoff, end); bottom validation panel that mirrors what the
     server's activate endpoint would reject.

  8. `src/components/layout/sidebar.tsx` — Flows entry between
     Automations and Settings, rendered only when isFlowsEnabled.

Every API route + page is gated server-side by isFlowsEnabled —
non-beta accounts get 404 (not 403) so the routes don't even hint
at existing. UI gate is for snappy redirect; security is at the
API layer.

Validation: 166/166 tests pass (added 24 validator tests);
typecheck + lint + build all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:57:38 +03:00
Arnas Donauskas
208b096cb1
chore(flows): defensive gate on interactive bubble render
Wraps the new `case 'interactive'` rendering with `isFlowsEnabled(profile)`
from the beta-features module that landed in #113. Non-beta accounts
fall back to the plain-text rendering for `content_type='interactive'`
rows — they get the option title as text, no special "↩ Button reply"
affordance.

Why this gate exists even though the case is unreachable today:
  - No code path on this branch produces `content_type='interactive'`
    rows. The flow runner that does is upcoming (PR #2), and it'll be
    behind the same gate.
  - But: if a stray interactive row ever shows up in a non-beta
    account (manual SQL insert, data import, future bug), we'd rather
    the account see boring plain text than UI for a feature they
    haven't opted into.
  - The cost is ~3 LoC and one hook call. Pure paranoia insurance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:21:22 +03:00
Arnas Donauskas
dc245d5914
feat(flows): foundation for interactive WhatsApp messages (PR 1/4)
Foundation for the upcoming WhatsApp chatbot flow builder. No runner,
no UI yet — just the plumbing that lets the codebase SEND interactive
messages (button replies + lists) and RECEIVE the taps as structured
message rows. Plan: ~/.claude/plans/understood-research-such-functionality-binary-lake.md

## What's in this PR

### Migration 010_flows.sql
- New tables: flows, flow_nodes, flow_runs, flow_run_events
- Linchpin partial unique index on flow_runs(user_id, contact_id) WHERE status='active' — at most one active run per contact, makes concurrent webhook deliveries idempotent at the DB layer
- Widens messages.content_type CHECK to allow 'interactive'
- New messages.interactive_reply_id column for the tapped button/row id
- RLS on every table; flow_runs added to supabase_realtime publication
- Idempotent throughout — matches the 009_message_actions.sql template

### Interactive Meta senders (src/lib/whatsapp/meta-api.ts)
- sendInteractiveButtons: type='interactive', interactive.type='button', up to 3 reply buttons
- sendInteractiveList: type='interactive', interactive.type='list', up to 10 rows across sections
- Limits exported as INTERACTIVE_LIMITS so flow validation can share the constants
- All Meta caps validated BEFORE the network call (button title ≤20 chars, list row title ≤24 chars, ≤10 total rows, no duplicate row ids, body ≤1024 chars, etc.) — misconfigured flows fail at save time instead of as a customer-facing 400 mid-conversation

### Webhook (src/app/api/whatsapp/webhook/route.ts)
- Extends WhatsAppMessage interface with `interactive?: { type, button_reply?, list_reply? }`
- parseMessageContent returns a new `interactiveReplyId` field; refactored to use a shared `empty` base so every return site picks up the new field without 9 explicit copies
- New `case 'interactive'`: uses the tapped option's TITLE as content_text (so the inbox bubble reads "Existing customer", not a UUID) and stashes the id separately
- 'interactive' added to ALLOWED_CONTENT_TYPES so the messages insert no longer coerces it to 'text'

### Inbox bubble (src/components/inbox/message-bubble.tsx)
- New `case "interactive"` in MessageContent renders the tap with a small "↩ Button reply" affordance so agents reading the inbox can tell at a glance that this was a tap rather than the customer typing the same words

### Types (src/types/index.ts)
- ContentType union extended with 'interactive'
- Message.interactive_reply_id field

### Engine helpers (src/lib/flows/)
- admin-client.ts: thin service-role client wrapper, mirrors automations/admin-client.ts
- meta-send.ts: engineSendInteractiveButtons / engineSendInteractiveList — adapts the Meta senders for service-role use, persists messages with sender_type='bot' content_type='interactive', uses the same phone-variant retry as automations/meta-send.ts. Unused in PR #1; ready for the runner in PR #2

### Tests
- 13 new vitest cases in meta-api.test.ts covering Meta's limit validation + payload-shape assertions for both interactive senders

## Validation
- npm test → 102/102 (was 89; 13 new)
- npm run lint → 0 errors
- npm run typecheck → clean
- npm run build → clean

## What's NOT in this PR
- The runner (dispatchInboundToFlows) — PR #2
- Builder UI / API surface — PR #3
- collect_input / condition / set_tag nodes + templates — PR #4
- react-flow canvas + http_fetch — v2 (PR #5)

## Risks tracked from the plan
- messages.content_type CHECK widening: grep'd every consumer; message-bubble.tsx, reply-quote.tsx, send/route.ts, automations/meta-send.ts all handle the new variant correctly (or accept it as a defaulted fallthrough)
- engineSendText / engineSendInteractive share ~80% of code; deliberately not pre-factored yet — let both stabilize, extract a shared base in PR #2 once the shape settles

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:12:27 +03:00
Arnas Donauskas
a701d932d2
Merge pull request #111 from ArnasDon/feat/inbox-doodle-background
feat(inbox): WhatsApp-style doodle background in the chat area
2026-05-22 10:09:16 +03:00
Arnas Donauskas
598ac7e66d
feat(inbox): WhatsApp-style doodle background in the chat area
Adds a subtle, repeating SVG pattern behind the message thread and the
"select a conversation" empty state — same visual language WhatsApp uses
in their own chat UI, themed for a CRM (message bubble, phone, mail,
calendar, lightbulb, briefcase, trophy, dollar sign, percent, etc).

Asset: public/inbox-doodle.svg, 6 KB, 240×240 tile that repeats
seamlessly. Icon shapes are taken verbatim from lucide-react (already
a dependency, every other icon in the UI uses it) so the doodle reads
as part of the same visual system, not a separate clip-art import.
Stroke is slate-500 at 22% opacity over bg-slate-950 — visible but
not competing with message text.

Wiring: a single DOODLE_BG_CLASSES constant in message-thread.tsx
applied to both the empty-state and active-thread containers, so they
can't drift. The header keeps bg-slate-900 (solid) and the composer
keeps its own background, so the doodle only shows in the actual
message scroll area — exactly like WhatsApp.

No JS, no perf hit beyond a one-off 6 KB asset fetch that's then
browser-cached forever (it's served from /public with Next's default
caching).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:06:34 +03:00
Arnas Donauskas
c35f2a1cb4
feat(inbox): add a refresh button to the conversation header
A small icon button (lucide RefreshCw) in the message-thread header,
sitting just left of the Status dropdown. Clicking it bumps the
parent's `resyncToken` — the same channel that already drives the
WS-reconnect and tab-visibility-resume refetches. So the button goes
through the existing dedupe + fetch plumbing in ConversationList and
MessageThread; no parallel code path to keep in sync.

The icon spins for 700ms as click feedback (the refetch itself is
fire-and-forget — the children just refire their fetch effects). The
button is gated behind the optional `onRefresh` prop, so existing
callers of MessageThread (and tests, if any are added later) keep
working unchanged.

Useful when the agent suspects realtime missed something — or just
wants to be sure the list and thread reflect the latest DB state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:34:39 +03:00
Arnas Donauskas
138ae8d6f4
fix(inbox): replace broken updater-closure flag, add reconnect/visibility resync
Addresses #106 (issue stays open until the reporter confirms in prod).

#105's `let foundInList = false; setState(p => { foundInList = ...; ... })`
pattern is broken: setState updaters run during reconciliation, *after*
the synchronous handler returns, so `foundInList` reads `false` in the
same tick and `hydrateConversation()` fires on every code path the
flag was meant to gate. Every incoming message and every conversation
UPDATE (read receipts, status flips, agent assignment) triggered a
redundant DB fetch, queuing HTTP behind realtime WS frames on the same
supabase client and starving the channel — which surfaced as exactly
what #106 reports: messages that never arrive, threads stuck on "No
messages yet", events landing out of order.

The fix:

1. Synchronous known-IDs ref. `knownConvIdsRef` mirrors the conv ids
   currently in state, kept in sync via a useEffect on `conversations`.
   Event handlers check it synchronously to decide between a cheap
   in-place patch and a hydrate refetch — no more closure-flag pattern.

2. Reconnect + visibility resync. Realtime is best-effort: events sent
   while the WS was disconnected or while the tab was background-throttled
   are gone. A `resyncToken` state bumped on (a) `isConnected` false→true
   transitions after the initial connect and (b) tab visibility →
   visible drives `ConversationList` and `MessageThread` to refire their
   fetch effects. The reactions effect in MessageThread is split into
   fetch + channel-subscribe so a resync only refetches the rows
   without tearing down and rebuilding the realtime channel.

3. Keep the conv-INSERT hydrate. Still the right fix for #104 (realtime
   payloads never carry the `contact` join); now it only fires when the
   conversation is genuinely unknown to state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:30:48 +03:00
Arnas Donauskas
d41652d748
fix(inbox): scope hover to the bubble; fix stale-closure reaction flow
Two follow-ups to the chat-actions PR:

1. Action toolbar was revealing across the whole message row, not just
   over the bubble. The `group/actions` div in MessageActions wrapped
   the bubble's full-width `flex w-full justify-end/start` row, so
   anywhere in that row triggered group-hover. Moved row alignment +
   max-w-[75%] cap onto MessageActions and reduced MessageBubble to a
   content-sized stack. Hover area now matches the bubble shape.

2. Agent reactions weren't appearing locally. `handleAddReaction` was
   wrapped in `useCallback` with a `[]` dep array, so it captured the
   first-render `postReaction` — whose own closure held a stale
   `reactions = []`, stale `conversation`, and stale `user`. Every
   optimistic update replaced the reactions list with `[onlyMyTemp]`
   and the API POST may have hit a null conversation. Dropped both
   wrappers; postReaction is now called directly from JSX and uses a
   functional `setReactions((prev) => ...)` so each call sees fresh
   state. Toggle semantics for pill-click are computed at the call site
   where `msgReactions` and `user?.id` are already in scope. Realtime
   INSERT now swaps a matching optimistic temp row instead of stacking
   a duplicate.

Also: reject `temp-*` message IDs with a friendly toast so users can't
react to a bubble that hasn't reached Meta yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:32:34 +03:00
Arnas Donauskas
339b1fe94f
feat(inbox): reactions, reply-to, and copy on chat messages
Adds three WhatsApp-standard affordances to the inbox: per-message
emoji reactions, reply-with-quote, and copy-text. Bubbles stay pure
presenters — actions layer on by composition (MessageActions wraps
MessageBubble). Hover toolbar on desktop, long-press (onContextMenu)
on touch.

Data model (migration 009):
- messages.reply_to_message_id (self-FK, ON DELETE SET NULL) so replies
  survive a deleted parent
- message_reactions table — one row per (message, actor) with a unique
  constraint, conversation_id denormalised so Supabase Realtime can
  filter without a join
- RLS scoped via conversation ownership; service-role bypass for the
  webhook

API:
- /api/whatsapp/send accepts reply_to_message_id, resolves the parent's
  Meta message_id and forwards it as context.message_id
- /api/whatsapp/react (new) sends Meta reaction payloads and mirrors
  the upsert/delete into message_reactions
- Webhook stops storing inbound reactions as fake text messages and
  instead upserts/deletes message_reactions; inbound replies (context.id)
  resolve to internal UUID and populate reply_to_message_id

UI:
- MessageActions hover toolbar with inline 6-emoji Popover (static set;
  no emoji-picker library)
- ReplyQuote shared between the in-bubble quote and the composer chip
- MessageReactions pill row with click-to-toggle
- Thread subscribes to message_reactions filtered by conversation_id;
  optimistic upsert with rollback on failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:17:26 +03:00
Arnas Donauskas
fbb95eb2a7
Merge branch 'main' of https://github.com/ArnasDon/wacrm into feat/inbox-template-picker
# Conflicts:
#	src/components/inbox/message-thread.tsx
2026-05-18 15:43:39 +03:00
Arnas Donauskas
149a4e67ef
Merge pull request #91 from ArnasDon/chore/remove-broadcast-schedule-stub
chore(broadcasts): drop the inert Schedule for Later tile
2026-05-18 15:39:54 +03:00
Arnas Donauskas
8d83220df4
chore(broadcasts): drop the inert Schedule for Later tile
The Pro-badged Schedule for Later tile in step 4 was a non-interactive
div — no handler, no scheduling backend, and no Pro tier in the product.
Send Immediately was the only path that did anything, so the picker
itself was decorative. Remove the section (and the now-unused timing
state / Clock / Lock imports). The wizard still completes Send
Immediately end-to-end via the existing Send Broadcast button below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:26:47 +03:00
Arnas Donauskas
2a86781c85
feat(inbox): wire up the template picker dialog
handleOpenTemplates flipped templateModalOpen but nothing rendered when
true, so both composer template buttons were dead. Add a TemplatePicker
dialog that lists Approved templates, prompts for any {{N}} variables,
then sends through the existing /api/whatsapp/send route with
message_type=template. The rendered body is stored as content_text so
the thread bubble and conversation preview show the actual message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:25:45 +03:00
Arnas Donauskas
5d0944e087
feat(inbox): wire conversation assignment dropdown
Replace the no-op Assign button in the thread header with a dropdown that
reads/writes conversations.assigned_agent_id. Lists profiles visible
under RLS (single-tenant today, ready for shared workspaces) and offers
Unassign when the conversation is already claimed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:14:42 +03:00
Arnas Donauskas
15a05eb280
Extract marketing site to ArnasDon/wacrm-site
Forks of this repo no longer inherit the wacrm.tech landing page
and docs — those move to the dedicated marketing-site repo at
https://github.com/ArnasDon/wacrm-site (deployed to wacrm.tech).

Changes here:
- Replace src/app/page.tsx with a redirect to /dashboard.
  Middleware already handles the unauth → /login bounce, so this
  works for both auth states.
- Drop src/app/{docs,opengraph-image.tsx,robots.ts,sitemap.ts}.
- Drop src/components/{landing,docs,seo} and src/lib/{seo,docs}.
- Drop the top-level docs/ markdown — moved to wacrm-site/docs.
- Slim src/app/layout.tsx metadata down to product chrome
  (no public OG cards, robots: noindex).
- Drop `marked` from dependencies; refresh package-lock.
- Refocus README on self-hosting; point readers at wacrm.tech/docs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:21:13 +03:00
Arnas Donauskas
9bdbfead6f
Merge pull request #86 from ArnasDon/chore/footer-trademark-disclaimer
chore: add Meta trademark / non-affiliation disclaimer to public footers
2026-05-06 13:54:43 +03:00
Arnas Donauskas
1f2849df75
Merge remote-tracking branch 'origin/main' into chore/whatsapp-trademark-symbol
# Conflicts:
#	README.md
#	docs/README.md
#	docs/automations-and-cron.md
#	docs/supabase-setup.md
#	docs/whatsapp-setup.md
#	src/app/opengraph-image.tsx
#	src/components/landing/hero.tsx
#	src/lib/seo/site-config.ts
2026-05-06 13:50:03 +03:00
Arnas Donauskas
4bb157c705
Merge remote-tracking branch 'origin/main' into chore/hostinger-accent-palette
# Conflicts:
#	src/app/icon.tsx
#	src/components/docs/shell.tsx
#	src/components/landing/footer.tsx
#	src/components/landing/nav.tsx
2026-05-06 13:35:46 +03:00
Arnas Donauskas
c3dde01229
chore: add Meta trademark / non-affiliation disclaimer to public footers
Per Meta's brand-use guidance for third-party products, sites that
reference WhatsApp must include a clear non-affiliation disclaimer in
the footer. Adds the standard line:

  "WhatsApp® is a registered trademark of Meta Platforms, Inc.
   Hostinger is not affiliated with, endorsed by, or sponsored by
   Meta Platforms, Inc."

Placed in:
- src/components/landing/footer.tsx — marketing footer (every public
  landing page).
- src/components/docs/shell.tsx — public docs pages had no footer at
  all; added a slim one carrying the same disclaimer.

Out of scope:
- Authed dashboard pages don't have a footer (full-screen workspace
  layout). The trademark-fair-use risk is concentrated on public
  surfaces — crawlers, visitors who haven't signed up, social shares.
  Skipping the dashboard keeps the workspace layout intact and the
  disclaimer where it does the most work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:15:08 +03:00
Arnas Donauskas
e46844d527
chore: add WhatsApp® on first mention across user-facing surfaces
Per Meta's brand-use guidance for third-party products, the registered
trademark symbol (®) should appear on the FIRST mention of "WhatsApp"
on each user-facing page. Subsequent mentions on the same page are
left plain.

Surfaces touched (one-per-page first mention):
- Landing hero badge ("Built on the official WhatsApp® Business API").
- src/lib/seo/site-config.ts: SITE_TAGLINE and SITE_DESCRIPTION
  (rendered into <title> and <meta description> on every page).
- src/app/opengraph-image.tsx: hero text in social-share image.
- Settings, Inbox connection banner, Automations page subtitle.
- README.md tagline.
- All public docs pages (docs/README.md, docs/whatsapp-setup.md,
  docs/getting-started.md, docs/supabase-setup.md,
  docs/environment-variables.md, docs/deployment-hostinger.md,
  docs/automations-and-cron.md, docs/architecture.md,
  docs/troubleshooting.md).

Out of scope: SITE_KEYWORDS (the meta-keywords array isn't
user-rendered text — putting ® on every keyword would look broken in
the meta tag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:13:26 +03:00
Arnas Donauskas
77dfa4f165
chore: switch accent palette to Hostinger violet, drop emerald green
The legal brief on third-party trademark use says: avoid the WhatsApp
teal/green palette and any visual treatment that could read as a
WhatsApp/Meta product. Emerald-500 was used as the primary brand colour
across hero, CTAs, sidebar, charts, badges, and the favicon — close
enough in hue to WhatsApp's signature green to muddy the line.

Swapping to Tailwind violet (which matches the Hostinger brand purple
#7c3aed) makes the visual identity unambiguously ours.

Scope:
- Mechanical Tailwind class swap: emerald-* → violet-* across src/.
- src/app/globals.css: --primary, --ring, --sidebar-primary, --sidebar-ring,
  --chart-1 oklch tokens swapped from emerald (oklch 0.696 0.17 162) to
  Hostinger-aligned violet (oklch 0.526 0.247 293).
- src/app/icon.tsx: favicon background #10b981 → #7c3aed.
- src/app/opengraph-image.tsx: badge bg, accent text, gradient.
- src/components/landing/hero.tsx + cta-banner.tsx: radial-gradient hex.
- src/components/landing/mock/analytics-mock.tsx: marketing chart line.
- src/components/dashboard/conversations-chart.tsx + response-time-chart.tsx:
  "Outgoing" series colour. Distinguishable from "Incoming" blue at a
  glance; we can revisit the chart pair later for stronger colourblind
  contrast (separate concern).
- README.md: license badge colour parameter.

Out of scope:
- src/components/settings/tag-manager.tsx still offers "Emerald" as one
  of the user-pickable contact-tag colours alongside Blue / Yellow / Red
  / etc. — that's a user data choice, not a brand mark.

Contrast check: white text on #7c3aed measures ~4.78:1 (WCAG AA passing
for normal body text). Violet-400 (#a78bfa) on slate-950 measures ~10:1
for accent text, well clear of AA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:57:12 +03:00
Arnas Donauskas
faee4eea44
chore: rename product to "CRM Template for WhatsApp"
Drops the "WaCRM" branded name across user-facing surfaces in favour
of a descriptive name. Branded names that imply WhatsApp ownership or
sponsorship are a trademark fair-use risk; a relationship-based name
("CRM Template for WhatsApp") sidesteps that.

Scope:
- Brand text in nav, sidebar, footer, docs shell, mock window chrome.
- SITE_NAME / SITE_DESCRIPTION / legalName in src/lib/seo/site-config.ts.
- README, CONTRIBUTING, docs/, .github/ templates and policies.
- package.json description.
- Auth pages, settings copy, FAQ, code comments.

Out of scope (intentionally — these are infra renames the project
owner should drive separately so links and the package lockfile stay
consistent):
- The github.com/ArnasDon/wacrm repo URL.
- The wacrm.tech domain / homepage field.
- The package.json "name" field.
- The Hostinger application name and Meta system-user name in deploy
  docs (existing-deployment instructions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:52:11 +03:00
Arnas Donauskas
047b981821
ci: clear lint error backlog, drop continue-on-error on lint step
Fixes the 14 pre-existing lint errors surfaced by React 19's new
hook rules so CI can enforce lint on every PR instead of tolerating
failures. The errors split roughly into three camps:

1) react-hooks/refs — "Cannot access refs during render"
   4 occurrences of the "latest ref" pattern that assigned
   `ref.current = x` during render. Moved each assignment into a
   `useEffect(() => { ref.current = x })` — the canonical React-19
   pattern. Consumers only read `.current` inside async callbacks
   that run after commit, so the very slight delay to the update is
   fine.
   Touched: src/hooks/use-realtime.ts (onMessageRef,
   onConversationRef), src/components/inbox/message-thread.tsx
   (onMessagesLoadedRef), src/components/inbox/conversation-list.tsx
   (onConversationsLoadedRef).

2) react-hooks/set-state-in-effect — genuine issues
   - contacts/page.tsx: the `setPage(0)` on search change moved from
     a useEffect into the `onChange` handler of the search input,
     which is the cleaner pattern anyway.
   - settings/page.tsx: the local `tab` state + sync effect were
     removed entirely; the URL query param is now the single source
     of truth, and the `onChange` handler just calls
     `router.replace(...)`. Simpler and fixes the lint error.

3) react-hooks/set-state-in-effect — false positives for
   load-on-mount and prop-driven sync
   Suppressed per-call (or per-block for effects with many setters)
   with explanatory comments. These effects use setters inside
   async Supabase-query callbacks, not synchronously in the effect
   body, so the cascade the rule warns about doesn't apply. Files:
   contacts/page.tsx, pipelines/page.tsx, pipelines/deal-form.tsx,
   pipelines/pipeline-settings.tsx,
   components/inbox/contact-sidebar.tsx.

4) react-hooks/preserve-manual-memoization — one useCallback
   Rule complained that the inferred dep was `contact` but the
   source dep list was `[contact?.phone]`. Broadened the manual dep
   to `[contact]` to match — callback behaviour is unchanged (it
   still only reads `contact?.phone`), and the React Compiler can
   now preserve the memoization.

And:
  - .github/workflows/ci.yml: dropped `continue-on-error: true` on
    the lint step. Lint now blocks merge on any new error.

Verified: `npm run lint` → 0 errors (24 warnings remain; all
pre-existing unused-import / missing-dep / <img> warnings that don't
fail CI). `npm run typecheck` clean. Landing, /docs, /login, and
/settings redirects all still respond correctly from the dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:28:35 +03:00
Arnas Donauskas
6cceb81c46
fix(inbox): don't drop messages when ?c= URL param updates
Reports: clicking a conversation sometimes left the message thread
stuck on "No messages yet" until the user hit browser refresh.

Root cause — a three-way race kicked off by any click on a conversation
in the list:

1. handleSelectConversation runs:
      setActiveConversation(B)
      setMessages([])
      router.replace("/inbox?c=B")

2. The URL change flips `deepLinkConvId` (from useSearchParams).
   handleConversationsLoaded is a useCallback with
   [deepLinkConvId], so its identity changes. That cascades into
   ConversationList, whose fetchConversations useCallback has
   [onConversationsLoaded], whose useEffect has [fetchConversations] —
   so the list refetches conversations purely because the URL changed.

3. When the refetch lands, handleConversationsLoaded runs its deep-link
   auto-select branch. autoSelectedForDeepLinkRef still points at its
   previous value (null, or the previous deep-link), so the guard
   `ref !== deepLinkConvId` passes and the branch re-applies B:
      setActiveConversation(B)     // no-op, same id
      setMessages([])              // BLOWS AWAY the messages
                                   // MessageThread just fetched

4. MessageThread's fetch effect only re-runs when conversationId
   changes. B is still B. It does not refetch. Messages stuck at [].

A hard reload unsticks things because state starts from scratch and
the initial deep-link flow is the only path that runs the auto-select.

Three defense-in-depth fixes:

- handleSelectConversation now writes the clicked id into
  autoSelectedForDeepLinkRef BEFORE the router.replace, so by the
  time any downstream refetch lands, the auto-select branch sees
  ref === deepLinkConvId and skips.

- handleConversationsLoaded early-returns if the deep-linked
  conversation is already the active one. This is redundant with
  the ref update above but protects against any other code path
  we don't foresee triggering a refetch.

- ConversationList's fetch is now driven by a plain useEffect with
  [] deps, using a ref for the latest onConversationsLoaded
  callback. URL changes no longer cause a conversations refetch.
  Matches the same stabilisation pattern used in
  src/hooks/use-auth.tsx and src/components/inbox/message-thread.tsx.

Verified: typecheck passes; lint count unchanged (37 pre-existing
problems, zero added). Couldn't exercise the rendered /inbox from the
preview sandbox — it's auth-gated and no test credentials are
available. Please confirm in the authenticated inbox:
  - Click back and forth between two conversations with different
    message histories; messages should populate every time.
  - Open /inbox?c=<id> directly; thread opens with messages.
  - Switch conversations after a deep-link landing; still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:01:59 +03:00
Arnas Donauskas
a90ae1d991
fix(inbox): align composer send and template buttons with the textarea
The \"Type '/' for quick replies\" hint used to live inside the
textarea's flex-1 column. With items-end on the parent row, the
column's height grew by the hint's line box and the buttons aligned
to *that* bottom — sitting one row below the textarea on screen.

Move the hint out to a sibling of the flex row. The row now only
measures the textarea, items-end aligns the buttons level with the
textarea's bottom, and the hint drops neatly below with a pl-11
indent so it stays aligned under the textarea's left edge
(w-9 button + gap-2 = 44px).

Verified structurally via source; the /inbox route itself requires
auth so the preview couldn't exercise the rendered UI. Typecheck
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:35:06 +03:00
Arnas Donauskas
72a5e4cf70
feat(settings): user profile management
Adds a profile tab to /settings with the five things every B2B SaaS
user expects:

- Display name — edits profiles.full_name.
- Avatar — upload / replace / remove, stored in a new `avatars`
  Supabase Storage bucket with RLS scoped to the owner's folder.
  Migration 008_profile_avatars_storage.sql creates the bucket (public
  read, 2 MB cap, image mimes only) and the four storage.objects
  policies.
- Email — goes through supabase.auth.updateUser({ email }), which
  confirms to both the old and new addresses. UI surfaces a "check
  both inboxes" banner after save.
- Password — three-field form with a current-password gate implemented
  via a throwaway signInWithPassword() before calling updateUser.
- Active sessions — one-click "sign out of all devices" using
  supabase.auth.signOut({ scope: 'global' }), with a confirm dialog.

Plus read-only role, joined date, and user id for operator debugging.

The header avatar and sidebar user row are now both dropdown menus
linking to /settings?tab=profile and /settings?tab=whatsapp; the
settings page reads ?tab= to land on the right tab. useAuth gets a
refreshProfile() callback so the header/sidebar update immediately
after a save without a full reload.

Deferred (separate work): account deletion, 2FA, phone/timezone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:40:39 +03:00
Arnas Donauskas
45fd2a9845
feat(docs): in-app docs viewer at /docs
Renders every file in docs/*.md as a proper documentation site styled
like Stripe / Vercel / Linear docs: sticky header, grouped left sidebar
(Setup / Deploy / Operate), dark prose with inline code pills, syntax
-ready code blocks, responsive table styling, and prev/next pagination.

- src/lib/docs/content.ts — parses md with marked and decorates the
  HTML with Tailwind utility classes via post-processing regex. Also
  rewrites inter-doc `./other.md` links to `/docs/other` and extracts
  an h2/h3 TOC for future use.
- src/app/docs/layout.tsx + src/components/docs/shell.tsx — shared
  frame with docs-scoped header (GitHub, Home, Get started), collapsing
  sidebar on mobile.
- src/app/docs/page.tsx — grouped index cards per section.
- src/app/docs/[slug]/page.tsx — individual doc + prev/next nav.
- Adds "Docs" to landing nav, footer, and a "Read the full
  documentation" CTA under the open-source cards.
- Adds `marked` dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:28:36 +03:00
Arnas Donauskas
029a20e407
fix(hostinger): recommend Managed Node.js Hosting, not VPS
Swaps the Hostinger recommendation on the landing page and throughout the
docs from VPS to Managed Node.js Hosting
(https://www.hostinger.com/web-apps-hosting).

The deploy walkthrough is rewritten around hPanel's Node.js app flow
(Git-based deploy, env vars panel, AutoSSL, cron via Advanced → Cron Jobs)
instead of a raw Ubuntu VPS with nginx + PM2 + certbot. VPS stays
mentioned at the bottom of the deploy page as an alternative when a
user actually needs one (background workers, custom binaries, etc.).

Also touched:
  - docs/README.md, environment-variables.md, automations-and-cron.md,
    troubleshooting.md to drop VPS-specific assumptions.
  - footer link URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:02:10 +03:00
Arnas Donauskas
dd65e532a1
feat(landing): open-source + Hostinger deploy section
Adds a #self-host landing section with two cards — one linking to
the GitHub repo (fork / clone) and one recommending Hostinger VPS
for deploying the template. Also surfaces the GitHub link from the
nav (desktop icon + mobile drawer) and the footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:45:00 +03:00
Arnas Donauskas
9173e8e4aa
Merge pull request #51 from ArnasDon/feat/mobile-automation-builder
feat(mobile): automation builder fits + flows on narrow screens
2026-04-19 12:58:11 +03:00
Arnas Donauskas
9f4c34e945
Merge pull request #50 from ArnasDon/feat/mobile-pipelines-kanban
feat(mobile): snap-scroll kanban with viewport-sized columns
2026-04-19 12:56:59 +03:00
Arnas Donauskas
a7dec71589
Merge pull request #49 from ArnasDon/feat/mobile-inbox-single-pane
feat(mobile): inbox single-pane flow on phone, unchanged on desktop
2026-04-19 12:55:21 +03:00
Arnas Donauskas
08eb6e192d
feat(mobile): app shell drawer + responsive header
First of a series to make the app mobile-friendly. The sidebar used
to be a fixed 240px column always visible, eating most of a 375px
phone screen and leaving ~135px for content. Now:

- Sidebar becomes a slide-in drawer on <lg, static on ≥lg. Controlled
  via a drawer-open prop wired from the shell; transform + duration
  feels native on touch devices.
- Backdrop overlay closes it on tap; Escape closes it; body scroll
  locks while open; route changes auto-close it.
- Header gains a 44×44 hamburger button on <lg that opens the drawer,
  hidden on ≥lg. Title shrinks slightly on narrow widths so the
  hamburger + avatar + sign-out row never wraps.
- Nav rows are taller (py-2.5) on mobile for comfortable touch
  targets, slim (py-2) back on desktop.
- Main content padding is p-4 on mobile, p-6 on desktop so cards
  have room to breathe without shouldering into the viewport edge.

No desktop changes visible beyond tighter padding on very narrow
lg breakpoints (main content width is unchanged).

Next PRs in this series will adapt:
- Inbox (3-pane → single-pane with back)
- Pipelines (kanban snap-scroll)
- Automation builder (card widths + bottom-sheet edit)
- Tables + forms polish

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:44:31 +03:00
Arnas Donauskas
15e4b5366d
feat(seo): full SEO pass (reapply with OG-image build fix)
Re-applies the original SEO work from PR #47 that was reverted in
PR #54 because its opengraph-image.tsx crashed `next build` during
static prerender of /opengraph-image. Satori (next/og's renderer)
errored with:

  Error: Expected <div> to have explicit "display: flex",
         "display: contents", or "display: none" if it has more
         than one child node.
  Failed to load dynamic font for ✓ . Status: 400

Root cause in the OG image JSX:
- Several <div>s held text + a sibling element (e.g. "Run your
  WhatsApp business from <span>one inbox.</span>") without an
  explicit display value. Satori requires it whenever there is
  more than one child.
- The ✓ Unicode glyph isn't covered by the default font Satori
  loads, so next/og tried to download a dynamic font and failed.

Fix:
- Every <div> with more than one child now has display: flex.
- Multi-span headline sits in a flex-wrap row of two <span>s.
- ✓ replaced with plain text dot separators in the feature row.

Verified end-to-end: `npm run build` now completes with 27/27
pages prerendered, /opengraph-image among them.

All the other SEO-pass content is identical to the originally
merged-and-reverted work: metadataBase + title template, OpenGraph
+ Twitter Cards defaults, JSON-LD (WebSite + Organization +
SoftwareApplication + FAQPage), sitemap.ts, robots.ts, noindex on
auth + dashboard route-group layouts, shared faq-data + site-config
modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:25:59 +03:00
Arnas Donauskas
adf78bb2ee
Revert "feat(seo): full SEO pass — metadata, structured data, sitemap, robots" 2026-04-19 12:15:42 +03:00
Arnas Donauskas
aca0d3be8b
Revert "feat(mobile): app shell drawer + responsive header" 2026-04-19 12:14:24 +03:00
Arnas Donauskas
150b3832d3
feat(mobile): automation builder fits + flows on narrow screens
The automation flow builder had fixed-width cards (320px for steps,
400px for conditions) and a crammed top bar. On a 375px phone the
condition cards overflowed horizontally and the top bar's Active
switch label left the name input with almost no room.

- Step cards: w-full max-w-[320px] sm:w-80 — full-width on mobile,
  original 320px on sm+.
- Condition cards: w-full max-w-[400px] sm:w-[400px].
- Trigger card matches the step cards.
- Condition branches (Yes/No) stack vertically on mobile — two
  columns at 375 would cram each branch to ~170px. Two columns
  return on sm+.
- Top bar tightens on mobile: "Active" label hidden under sm so
  the switch + save button + name input all fit; back-arrow grows
  from h-8 w-8 → h-9 w-9 for a comfortable touch target; name input
  is text-sm on mobile, text-base on sm+.

Desktop unchanged — every mobile-specific class is overridden by
its sm: counterpart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:57:25 +03:00
Arnas Donauskas
4d901e16bd
feat(mobile): snap-scroll kanban board with viewport-sized columns
The pipelines board was already horizontally scrollable, but on a
phone it felt imprecise — swipes landed mid-column, there was no
visual hint that more columns existed past the viewport, and 260px
columns felt cramped at 375px width.

- Each StageColumn is now w-[85vw] min/max-clamped on mobile, so the
  next column's edge peeks in as a "more here" affordance. On lg+
  the columns revert to flex-1 basis-[260px] (share the row).
- Scroll container gets snap-x + snap-mandatory on mobile, snap-none
  on lg+. Each column has snap-start so swipes always land cleanly
  at the left edge of the next column.
- shrink-0 on mobile prevents flex from compressing the columns when
  space is tight.

No desktop visual change — all the mobile-specific classes are
overridden back by their lg: counterparts, and the original
flex-1 basis-[260px] behavior is preserved above lg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:53:10 +03:00
Arnas Donauskas
7612e6c781
feat(mobile): inbox single-pane flow on phone, unchanged on desktop
The inbox was a 3-pane flex row (list + thread + contact sidebar).
On a 375px phone both list and thread tried to share ~187px each
and the whole UI was unusable. Now:

- On <lg the inbox shows ONE pane at a time: list when nothing's
  selected, thread when a conversation is open.
- Thread gains a back-arrow in its header (mobile only) that pops
  back to the list and clears the ?c= URL param so refreshing lands
  on the list, not on the thread the user just backed out of.
- Conversation list grows to w-full on mobile (was fixed w-80)
  so each row uses the whole viewport width.
- Thread header tightens: name/phone truncate with min-w-0, session
  timer badge is hidden under sm to save room for back arrow + name.
- Dashboard shell's p-6 is already p-4 on mobile (PR #48); matched
  the -m-6 offset in the inbox to -m-4 sm:-m-6.

Desktop layout (lg+) is unchanged: list + thread + contact sidebar
side-by-side exactly as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:47:35 +03:00
Arnas Donauskas
73ae399a4d
feat(mobile): app shell drawer + responsive header
First of a series to make the app mobile-friendly. The sidebar used
to be a fixed 240px column always visible, eating most of a 375px
phone screen and leaving ~135px for content. Now:

- Sidebar becomes a slide-in drawer on <lg, static on ≥lg. Controlled
  via a drawer-open prop wired from the shell; transform + duration
  feels native on touch devices.
- Backdrop overlay closes it on tap; Escape closes it; body scroll
  locks while open; route changes auto-close it.
- Header gains a 44×44 hamburger button on <lg that opens the drawer,
  hidden on ≥lg. Title shrinks slightly on narrow widths so the
  hamburger + avatar + sign-out row never wraps.
- Nav rows are taller (py-2.5) on mobile for comfortable touch
  targets, slim (py-2) back on desktop.
- Main content padding is p-4 on mobile, p-6 on desktop so cards
  have room to breathe without shouldering into the viewport edge.

No desktop changes visible beyond tighter padding on very narrow
lg breakpoints (main content width is unchanged).

Next PRs in this series will adapt:
- Inbox (3-pane → single-pane with back)
- Pipelines (kanban snap-scroll)
- Automation builder (card widths + bottom-sheet edit)
- Tables + forms polish

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:39:08 +03:00
Arnas Donauskas
c2587f7466
feat(seo): full SEO pass — metadata, structured data, sitemap, robots
Brings the site from bare-bones metadata to production-ready SEO.
Covers technical crawlability, rich-result eligibility, social
sharing, and duplicate-content prevention.

Added:
- src/lib/seo/site-config.ts — single source of truth for base URL,
  brand, description, keywords. Referenced by every consumer so a
  domain / rename is a one-file edit.
- src/lib/seo/faq-data.ts — FAQ Q&A extracted from the component so
  the UI and the FAQPage JSON-LD render the same content.
- src/lib/seo/structured-data.ts — builders for WebSite,
  Organization, SoftwareApplication, and FAQPage JSON-LD nodes.
  FAQPage is eligible for Google's FAQ rich result; SoftwareApplication
  with a free Offer can earn a "Free" badge on SERP.
- src/components/seo/json-ld.tsx — emits one <script
  type="application/ld+json"> per node (Google's preferred shape).
- src/app/opengraph-image.tsx — Next auto-generates a 1200×630 PNG
  for the landing page's OG / Twitter previews.
- src/app/sitemap.ts — lists only public, indexable pages.
- src/app/robots.ts — Allow /, Disallow every app / auth route,
  references the sitemap.

Updated:
- src/app/layout.tsx — metadataBase, title template (%s — WaCRM),
  default OpenGraph + Twitter Cards (inherited everywhere), canonical,
  icons config, viewport (themeColor + colorScheme=dark for browser
  chrome on mobile).
- src/app/page.tsx — injects the 4 JSON-LD blocks, sets explicit
  canonical for /, overrides the title.
- src/app/(auth)/layout.tsx — noindex+nofollow for login / signup /
  forgot-password (no SEO value; would compete with landing page).
- src/app/(dashboard)/layout.tsx — split into thin server layout
  (exports noindex metadata) + DashboardShell client component. App
  routes won't be indexed even if a URL leaks.
- src/components/landing/faq.tsx — consumes the shared FAQ data
  module (no more drift between UI and JSON-LD).

Verified end-to-end against the running dev server:
- GET / HTML contains: canonical, description, keywords, robots,
  og:title/url/image/type, twitter:card/title, theme-color, + 4
  JSON-LD blocks (WebSite, Organization, SoftwareApplication,
  FAQPage).
- GET /opengraph-image → 200 OK, content-type image/png.
- GET /login HTML contains <meta name="robots" content="noindex,
  nofollow, nocache">.
- GET /sitemap.xml serves valid XML with /.
- GET /robots.txt serves Allow /, Disallow all app + auth paths,
  sitemap reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:17:48 +03:00
Arnas Donauskas
d5d406fe8e
feat(marketing): full public landing page at /
Replaces the root redirect to /login with a real marketing landing so
first-time visitors can learn about WaCRM and convert. Authed users
still see it and get a "Go to Dashboard" CTA instead of "Sign in" /
"Get started".

Sections, in order:
- Sticky nav with smooth scroll to Features / How it works / FAQ.
  Auth-aware CTA: pending/signed-in/signed-out states render the
  right buttons without layout shift.
- Hero with clear one-liner, benefit subhead, primary + secondary
  CTA, trust cues, and a miniature inbox preview (pure CSS/SVG
  recreation of the real UI — no screenshot to age out).
- Features grid: 6 benefit-first cards.
- 4x feature spotlights (alternating): shared inbox, automations,
  pipelines, analytics. Each with a bespoke product miniature.
- How It Works: 3 steps framed around "live in 30 minutes".
- FAQ: 6 questions answering the real objections (BSP access, team
  sharing, setup speed, data ownership, templates, broadcasts).
- Final CTA banner + footer with nav + product/account link columns.

Implementation notes:
- All visuals are CSS/SVG miniatures in src/components/landing/mock —
  intentionally not real screenshots so the page doesn't drift from
  the product over time.
- Dark theme throughout to preserve the app's brand (per the repo's
  dark-only convention). No light sections.
- Per-viewport verified: desktop (1440x900) two-column hero with
  mock; mobile (375x812) stacks hero and reveals a hamburger menu.
- Middleware untouched — the page itself did the /login redirect;
  replacing page.tsx is enough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:51:30 +03:00