Copy + UX: fear hook on FindJobs no-scan state, WinStatsBanner zero-state, TrustCenter accuracy - #504
Conversation
…Banner zero-state, TrustCenter accuracy fix
- FindJobsPage: Replace SVG map + "CHECK THE CURRENT PUBLIC-TENDER FEED" with fear→proof→control hook ("Every week you don't scan is work your competitors are pricing")
- WinStatsBanner: Add zero-state placeholder ("No wins logged in your area yet — be first to track a job won") so the component shows after a scan even with no wins data yet
- TrustCenterPage: Fix misleading "One exclusive territory lock — your patch, nobody else in it" claim → accurate "Your scan is private — other trades scanning the same postcode see nothing of yours"
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b69e70aac4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… on postcode change Add AbortController to cancel previous fetch when postcode prop updates. Prevents a late-resolving response from an old postcode overwriting the current result now that zero-win state is also committed (not silently dropped).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0ead09a8f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Reset stats and loaded state when postcode is empty, so the banner hides immediately rather than showing the previous area's zero-win message.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92dd758fb4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ro-state readOutcomeRows() applied a LIMIT 1000 globally before postcode filtering, so older area wins could be excluded and WinStatsBanner would falsely show "no wins logged". New readWonRowsByArea() queries with status=won and ilike postcode filter pushed to Supabase, no truncation risk.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa20291428
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Editing from one non-empty postcode to another left the previous area's result visible during the pending request. Now resets stats/loaded at the start of every new fetch so the banner hides until fresh data arrives.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fbcffc3a3e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
readWonRowsByArea fetched rows client-side, still subject to PostgREST's server-side 1000-row cap. New readWonStatsByArea uses count()+won_value.sum() in the select, so aggregation happens in the DB and a single response returns exact totals regardless of the number of matching rows.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8559879721
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
PostgREST returned count()/sum() under keys 'count'/'sum', not the names the reader expected — so wonCount and totalValue always fell back to 0. Aliased the aggregates in the select as won_count and won_value_sum so the response key matches what the reader unwraps.
|
✅ Meticulous spotted 0 visual differences across 169 screens tested: view results. Meticulous evaluated ~4 hours of user flows against your PR. Expected differences? Click here. Last updated for commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9109c33377
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… slice Prior slice(0,2) turned B14 into B1 (wrong: matches B10-B19, misses B2-B9, B20+), so a Birmingham user could get a false zero-state when wins exist elsewhere in area B. Now uses outwardFromPostcode + /^[A-Z]+/ to extract the actual area, matching how server/utils/postcode.ts already handles it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: caf166b0fe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The prior `ilike '${area}%'` bled into neighbouring areas whose codes
start with the same letter — a B14 request also counted BT (Belfast), BA
(Bath), BD, BH, BL, BN, BR, BS wins; E1 counted EC/EH/EN/EX; S1 counted
SA/SE/SG/SK/SL/SM/SN/SO/SP/SR/SS/ST/SW/SY. UK outward codes always follow
letters with a digit, so switching from ilike to a POSIX regex
(`^${area}[0-9]`) via PostgREST's imatch operator enforces that boundary
and confines the aggregate to the actual postcode area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cMAaJrqSu6Bbrtyn9gCPU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a318d8b273
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add .not('user_id', 'is', null) to the aggregate query in
readWonStatsByArea so it operates on the same population as
countDistinctContributors. Prevents legacy null-user_id rows from
inflating the released aggregate under a k=3 privacy claim built from
identified contributors only.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5455580acf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Filter .not('won_value', 'is', null) on both the aggregate and the
distinct-contributor gate in readWonStatsByArea so k=3 measures
distinct users who recorded a value, and the released totalValue
covers exactly those users' values. Prevents a k=3 release from
publishing a single user's exact recorded won_value when the other
two contributors saved wins without an amount.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 015ebe4834
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add distinctValueContributors as a second gate scoped to wins with a recorded won_value. distinctWinContributors gates suppression and the wonCount publish; distinctValueContributors gates the totalValue publish separately. An area whose wins were all logged without an amount now surfaces the count with a no-amount message rather than falsely claiming no wins are logged.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74dbb5eb43
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
toMoneyInt preserves explicit 0 rather than nulling it, so
won_value = 0 rows pass .not('won_value', 'is', null). An
area with three contributors where only one supplied a
positive amount could satisfy the k=3 value threshold and
publish that single user's exact sum as totalValue.
Switch both the distinct-value-contributor gate and the sum
aggregate from IS NOT NULL to > 0. Zero-value rows no longer
count toward the value cohort, and the sum stays k=3
protected against exact-amount inference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cMAaJrqSu6Bbrtyn9gCPU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b4a08c64b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Every keystroke changed `postcode` and reran the effect, firing a fresh /api/wins/stats request even when the parsed outward was identical. Aborting fetch on the client does not cancel the server-side Express handler or its Supabase queries, so a full "SW1A 1AA" entry triggered ~7 wasted request cascades — each up to eight sequential Supabase queries via countDistinctContributors + aggregate. Derive `outward` with useMemo and depend the effect on outward, so the request only fires when the parsed area actually changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cMAaJrqSu6Bbrtyn9gCPU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b9c53e4fe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
split(' ')[0] produces successive outward strings for compact
postcodes — M11AA → M1, M11, M11AA as the user types, firing
three requests. Use the same UK postcode regex as the server's
outwardFromPostcode: clean non-alphanumerics, match the leading
outward pattern. M11AA now produces '' → M1 → M11 → '' → M1,
so the effect only fires when a structurally valid outward is
derivable, not on every intermediate character.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cMAaJrqSu6Bbrtyn9gCPU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 989d605fd1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two fixes for the unauthenticated /api/wins/stats endpoint:
1. Add rateLimit middleware to /api/wins/stats — consistent with
all other public DB-backed routes (/api/waitlist/count etc).
Previously unthrottled despite triggering up to 8 Supabase
queries per call via countDistinctContributors + aggregates.
2. Key the WinStatsBanner effect on the postcode area (leading
letters) rather than the outward. B1, B14, B147 all share
area B — the server queries ^B[0-9] regardless of which
outward is sent. Changing the effect dep from outward to area
means typing B14 7QH fires one request (when area first
becomes B on B1), never again until the area changes. Send
${area}1 as a minimal valid outward so the server can parse it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cMAaJrqSu6Bbrtyn9gCPU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8af8310d75
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The shared rateLimit middleware keys only on IP, so banner requests against /api/wins/stats consumed the same 20-req/min bucket as /api/leads/search. Twenty page loads or area queries could 429 a subsequent scan even though the user never hit the scan limit. Add makeRateLimit(limit) factory to server/middleware/rateLimit.ts — each call returns a middleware with its own isolated Map and configurable limit. Preserve the original rateLimit export (including its scan-specific 429 body) for backward compatibility. Wire /api/wins/stats to rateLimitStats = makeRateLimit(60): a generous separate bucket (banner fires once per area) that never interferes with scan quota. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cMAaJrqSu6Bbrtyn9gCPU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 121949eb61
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Remove the unverifiable claim that JobFilter surfaces jobs before Checkatrade/Bark/MyBuilder. Replace with factually accurate copy: "from official sources, before the job reaches the quote sites." Addresses Codex r3841840108.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e9aa273b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .from('lead_outcomes') | ||
| .select('user_id') | ||
| .eq('status', 'won') | ||
| .filter('postcode_outward', 'imatch', areaFilter) |
There was a problem hiding this comment.
Avoid asserting zero when wins lack outward postcodes
When a win comes from a feed without an exact postcode, deriveOutward() stores UK (leadEngine/normaliser.ts:150-158), and FindJobsPage forwards that value unchanged when recording the outcome (src/pages/FindJobsPage.tsx:1405-1418). This area regex excludes every such win from the contributor check, so an area whose tracked wins came from those supported contract feeds receives distinctWinContributors === 0 and the new banner states “No wins logged,” even though the database contains wins that cannot be geographically attributed. Preserve the user's searched outward when logging the outcome, or treat unlocalized outcome data as unavailable rather than an exhaustive zero.
Useful? React with 👍 / 👎.
Summary
FindJobsPage — no-scan state: Replaced generic SVG map illustration and corporate "CHECK THE CURRENT PUBLIC-TENDER FEED" headline with a fear→proof→control hook: "Every week you don't scan is work your competitors are pricing" + specific proof line naming planning, energy, and contract signals. Removes decorative noise, adds urgency that a tradesman reads in under 2 seconds.
WinStatsBanner — zero-state: Component previously returned
nullwhenwonCount === 0, so users never saw the banner after a scan in an area with no logged wins. Now shows a subdued placeholder: "No wins logged in your area yet — be the first to track a job won." This surfaces the concept of win-tracking to users who haven't seen it before.TrustCenterPage — accuracy fix:
guaranteeFeatureslisted "One exclusive territory lock — your patch, nobody else in it" which implies no other tradesperson can scan the same postcode — that's not true and would damage trust if a user investigated. Changed to "Your scan is private — other trades scanning the same postcode see nothing of yours" which is accurate and still strong.Test plan
/find-jobswithout scanning — confirm fear-hook headline shows, SVG map is gone/trust— confirm guarantee features no longer mention "exclusive territory lock"npm run build— passesnpx tsc --noEmit— passes (0 errors)Generated by Claude Code