Commit Graph

7 Commits

Author SHA1 Message Date
Arnas Donauskas
2fbeeee3ac
Revert "feat(i18n): re-attempt next-intl with hardened artifact tracing" 2026-05-31 21:05:34 +03:00
Arnas Donauskas
8124640d80
Revert "Merge pull request #183 from ArnasDon/revert/i18n-pr-182"
This reverts commit 872f751c07, reversing
changes made to 5e8076cee9.
2026-05-31 20:56:54 +03:00
Arnas Donauskas
a30f04b5a4
Revert "Merge pull request #182 from ArnasDon/feat/i18n-next-intl"
This reverts commit 5e8076cee9, reversing
changes made to 962e4fb27d.
2026-05-31 20:42:14 +03:00
Arnas Donauskas
8a8758a487
feat(i18n): scaffold next-intl with [locale] routing + extract auth/layout
Introduces an internationalization layer so wacrm can ship in multiple
languages — initially en (canonical), es, lt (untranslated mirrors of
en, ready for translators).

Foundation
- next-intl ^4.13 (Next 16 + React 19 compatible)
- src/i18n/{routing,navigation,request}.ts — locale config, locale-aware
  Link/router/redirect, server-side message loader
- next.config.ts wired with `createNextIntlPlugin`
- src/app/* moved under src/app/[locale]/ (excluding /api and globals.css)
- [locale]/layout.tsx is now the root layout: <html lang={locale}>,
  hasLocale guard, setRequestLocale, NextIntlClientProvider,
  generateStaticParams for build-time prerendering of all locales
- middleware.ts composes next-intl's createMiddleware with the existing
  Supabase auth check: intl middleware runs first to handle locale
  prefix; auth redirects retain the active locale (e.g. signed-out user
  on /es/dashboard → /es/login); intl cookies (NEXT_LOCALE) are carried
  onto auth-issued responses so the sticky preference survives

Strings extracted (~80 keys across en/es/lt.json)
- common, nav (sidebar + header), roles (Owner/Admin/Agent/Viewer chips)
- auth.login, auth.signup, auth.forgotPassword (all forms + success states)
- join (all four UI states: loading, fail, accept, sign-up CTA + conflict modal)
- localeSwitcher

UI
- New `<LocaleSwitcher />` in the header — globe icon + locale dropdown
  using next-intl's useLocale + locale-aware useRouter

Out of scope for this PR (intentional — keeps diff reviewable)
- Dashboard feature pages (dashboard, inbox, contacts, pipelines,
  broadcasts, automations, flows, settings) and their components/
  namespaces still have hardcoded English; they continue to render
  fine because next/link redirects through the locale middleware
- Shared UI a11y strings (dialog close, etc.)
- Translating es.json / lt.json — they currently mirror en.json so the
  app runs in any locale without crashes (English fallback)
- Renaming middleware.ts → proxy.ts per Next 16 deprecation

Verification
- npm run build → 62 static pages prerendered across en/es/lt
- npm run typecheck → clean
- npm run test → 366 passing
- npm run lint → 0 errors (18 pre-existing warnings untouched)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 20:17:49 +03:00
Arnas Donauskas
03d4c26b89
feat(ui): visual polish from multi-user review (P1-P5, P7, P10)
PR D — the final pass of the post-review fix series. Bundles the
seven visual / UX polish items the design review flagged.

P1 + P5 — sidebar account strip (`src/components/layout/sidebar.tsx`)
- Owner used to be invisible in the role-chip slot (the previous
  check was `accountRole && accountRole !== "owner"`), so owners
  looked indistinguishable from admins. Now the chip always
  renders, with a per-role icon + colour pulled from a new
  `ROLE_CHIP` table.
- Added `title={account.name}` on the truncated name so the full
  account name is visible on hover when it's clipped (long names
  + narrow sidebars).

P2 + P3 + P10 — members tab (`members-tab.tsx`)
- **P2** — destructive Remove / Revoke buttons used a neutral
  default that only turned red on hover. The destructive intent
  was invisible until the user moused over. Both buttons now have
  a red default state (`border-red-500/40 bg-red-500/10
  text-red-300`) with a darker shade on hover.
- **P3** — role chips were all slate. Replaced with a per-role
  colour scale: owner amber (scarce, immutable), admin primary
  (significant), agent slate (operational default), viewer muted
  slate (read-only). Mirrors the sidebar's `ROLE_CHIP` table so
  the two surfaces don't drift; once the surface stabilises this
  should hoist into a shared module.
- **P10** — pending invitations section now opens with a one-line
  hint: "The plaintext invite URL is only shown once at creation
  for security — to re-share, revoke the invite below and create
  a new one." Pre-fix, admins kept looking for a "copy link"
  button on the pending rows; the design decision (don't store
  plaintext) is now self-explanatory.
- Cleanup: removed the now-unused `ROLE_LABELS` constant
  (subsumed by `ROLE_CHIP[role].label`).

P4 — amber "Save this link now" callout contrast
- Pre-fix: `bg-amber-500/10 + text-amber-200` on slate-900 was
  borderline for WCAG AAA body-text contrast.
- Post-fix: `bg-amber-500/15`, border bumped to `/50`, strong
  intro promoted to `amber-100` so the urgent copy reads with
  proper weight against the dark backdrop.

P7 — Try-again button on join page server_error
- Pre-fix: the only recourse for a server_error peek was to
  follow "Create a new account" / "Sign in" links — both lose
  the invite token.
- Post-fix: extracted the peek+auth fetch as `loadPeekAndAuth`,
  call it from a new "Try again" primary button on the
  server_error card. Other failure reasons (not_found / used /
  expired) are terminal for this token so they keep the
  signup/sign-in escape hatches; only server_error gets retry.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds

Closes out the post-review fix series. Combined with PRs A
(#178, backend perf), B (#179, UI blocking), and C (#180,
front-end correctness), every item from the consolidated review
report is addressed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 19:39:41 +03:00
Arnas Donauskas
bff8ec6765
feat(ui): user-facing fixes from multi-user review (M3, M4, S3)
PR B of the post-review fix series. Closes the three user-blocking
UI gaps the design / UX review flagged.

M3 — Charlie's 409 redeem dead-end (join page)
- Pre-fix: a user with existing data clicking an invite saw a
  3-second toast then the Accept button re-enabled. The same
  retry would 409 forever.
- Post-fix: 409 status is detected explicitly and surfaces a
  blocking dialog with an explanation + "Sign out & use a
  different email" action. The action calls supabase.signOut()
  then reloads the page; the page rebuilds on the signed-out
  branch with the "Create account & join" CTA. The invite token
  stays in the URL through the reload so the recovery is one
  click. Non-409 errors still take the regular toast path.
- Files: `src/app/join/[token]/page.tsx` — new state
  `conflictMessage`, new handler `handleSignOutAndRetry`, new
  Dialog component in the auth-in render branch.

M4 — WhatsApp share message includes the account name
- Pre-fix: "Join our wacrm account using this link…" — generic,
  no team context. In multi-team contexts the recipient
  couldn't tell which team they were being invited to.
- Post-fix: "Join <Account Name> on wacrm using this link…".
  The account name is snapshotted into the `CreatedInvite`
  result at creation time so a later rename can't retroactively
  rewrite the wa.me message.
- Files: `src/components/settings/invite-member-dialog.tsx` —
  read `account` from `useAuth()`, thread `accountName` through
  `CreatedInvite`, update `whatsappShareUrl` copy.

S3 — Members tab roster mobile reflow
- Pre-fix: the member row used a single flex-row with a fixed-
  width 128px role Select and a 36px Remove button. On a 375px
  viewport the name + email got crushed to ~50px.
- Post-fix: at base width the row stacks vertically — identity
  (avatar + name + email) on top, actions (role + Remove) below.
  At sm+ it returns to the original inline layout. The joined-
  date column stays desktop-only.
- Files: `src/components/settings/members-tab.tsx` — `flex-col`
  base, `sm:flex-row sm:items-center` for desktop. Actions
  wrapped in their own flex container so the role Select and
  Remove button cluster together on both breakpoints.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 19:25:50 +03:00
Arnas Donauskas
47104042fd
feat(join): /join/[token] page + signup/login invite redirects
PR 7 of the multi-user accounts series. Closes the invite loop —
PR 6 lets admins generate links, this PR is what the recipient
sees when they click one.

What lands
- `src/app/join/layout.tsx` — minimal centered dark shell. Sits
  outside both `(auth)` and `(dashboard)` route groups because
  the page is hybrid: it renders for anonymous AND signed-in
  visitors. Reusing either neighbouring layout would funnel one
  audience through the wrong middleware redirect.
- `src/app/join/[token]/page.tsx` — client component. Calls
  /api/invitations/[token]/peek + supabase.auth.getUser() in
  parallel on mount and routes through four UI states:

    peek          | auth         | render
    --------------|--------------|----------------------------------
    loading       | —            | spinner
    ok:false (any)| —            | friendly error + signup/signin CTAs
    ok:true       | signed out   | "Sign up & join" + "Sign in" links
    ok:true       | signed in    | "Accept invitation" button → redeem

  Redeem is intentionally manual — auto-accepting on mount would
  race with the signup → email-verification → return flow and
  hide what's being accepted from the visitor.

  AuthContext doesn't reach this route group (AuthProvider lives
  inside (dashboard)), so we call Supabase directly here the same
  way /login and /signup already do.

  After a successful accept, the page does a full reload to
  /dashboard (not router.push) so AuthProvider re-fetches the
  profile with the new account_id and account_role.

- `src/app/(auth)/signup/page.tsx`:
  - Reads `?invite=<token>` via useSearchParams.
  - Passes emailRedirectTo: `${origin}/join/<token>` to
    supabase.auth.signUp so the verification email returns the
    user to the redeem step.
  - Re-skins the card header for invite signups ("Create account
    & join", different icon).
  - Preserves the token through the "Sign in" link and the
    "Back to sign in" button on the success screen.
  - Default export now wraps the form in `<Suspense>` — Next 16
    refuses to prerender pages calling useSearchParams without
    it.

- `src/app/(auth)/login/page.tsx`:
  - Reads `?invite=<token>`.
  - On successful sign-in, routes to /join/<token> instead of
    /dashboard.
  - Re-skins the card header for invite logins.
  - Preserves the token through the "Create account" link.
  - Default export wrapped in `<Suspense>` for the same prerender
    reason.

- `src/middleware.ts`:
  - When an authenticated user hits /login?invite=<t> or
    /signup?invite=<t>, redirect to /join/<t> instead of the
    default /dashboard. Without this, a forwarded invite link
    landing on an already-signed-in browser would silently drop
    on /dashboard and the visitor would never see the accept
    prompt.
  - Both pre-existing redirect cases (no invite present, OR
    /forgot-password with an invite — which doesn't make sense)
    fall through to /dashboard as before. The search string is
    cleared on both paths so we don't leak the invite token
    into the dashboard URL bar.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds:
                          ƒ /join/[token]   — dynamic (peek + auth fetch)
                          ○ /login          — static, hydrates the form
                          ○ /signup         — static, hydrates the form

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