From e69e665db1ceca4f71b3161b0b0e2956b4925744 Mon Sep 17 00:00:00 2001 From: 0xkkonrad Date: Thu, 23 Jul 2026 08:11:51 +0000 Subject: [PATCH 1/4] fix(card): build rejection-share captions from the live door tally The numbers-flex caption in the appeal-tweet pool hardcoded the original '213 tried, 7 got in' placeholder, so shared tweets contradicted the rejection screen itself, which already renders the real computeDoorTally counts. Build the pool per share from the same tally so the two surfaces always agree. --- src/components/Card/CardRejectionScreen.tsx | 2 +- .../__tests__/rejectionCaptions.test.ts | 19 ++++++ .../Card/share-asset/rejectionCaptions.ts | 66 +++++++++++-------- 3 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts diff --git a/src/components/Card/CardRejectionScreen.tsx b/src/components/Card/CardRejectionScreen.tsx index c2525f60c5..6004bfb38a 100644 --- a/src/components/Card/CardRejectionScreen.tsx +++ b/src/components/Card/CardRejectionScreen.tsx @@ -103,7 +103,7 @@ const CardRejectionScreen: FC = ({ // Clear any stale "failed to join" error from an earlier handleJoin so // the success state can't render alongside it (CodeRabbit). setJoinError(null) - const caption = pickRejectionCaption() + const caption = pickRejectionCaption({ applicants, admitted }) // Appeal = tweet AND join the waitlist. Joining is NOT access โ€” release // stays manual (admin grant) โ€” it just drops the appealer into the diff --git a/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts b/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts new file mode 100644 index 0000000000..af908f903e --- /dev/null +++ b/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts @@ -0,0 +1,19 @@ +import { buildRejectionCaptions, pickRejectionCaption } from '../rejectionCaptions' + +describe('rejectionCaptions', () => { + test('numbers-flex caption uses the live door tally, not a hardcoded 213/7', () => { + const captions = buildRejectionCaptions({ applicants: 275, admitted: 12 }) + expect(captions).toContain('275 tried, 12 got in. @joinpeanut said not me. yet.') + expect(captions.some((c) => c.includes('213 tried'))).toBe(false) + }) + + test('large tallies are en-US formatted to match the screen copy', () => { + const captions = buildRejectionCaptions({ applicants: 1500, admitted: 42 }) + expect(captions).toContain('1,500 tried, 42 got in. @joinpeanut said not me. yet.') + }) + + test('pickRejectionCaption returns a caption from the pool', () => { + const tally = { applicants: 275, admitted: 12 } + expect(buildRejectionCaptions(tally)).toContain(pickRejectionCaption(tally)) + }) +}) diff --git a/src/components/Card/share-asset/rejectionCaptions.ts b/src/components/Card/share-asset/rejectionCaptions.ts index 45ed1ab393..87d863f56e 100644 --- a/src/components/Card/share-asset/rejectionCaptions.ts +++ b/src/components/Card/share-asset/rejectionCaptions.ts @@ -6,37 +6,45 @@ * doesn't fill with one identical tweet. Every caption tags @joinpeanut so * the rejection itself markets the door's exclusivity โ€” the handle rides the * caption (it is intentionally not drawn on the asset image). + * + * The pool is built per share from the live door tally so the numbers-flex + * caption matches the "{tried} tried ยท {got in} got in" the screen renders. */ -export const REJECTION_CAPTIONS: readonly string[] = [ - 'rejected by @joinpeanut ๐Ÿšซ the door policy is insane.', - '@joinpeanut has a bouncer now??', - "@joinpeanut told me i'm not on the list ๐Ÿ’€ the AUDACITY", - "the @joinpeanut bouncer said come back when i'm somebody. ok bet.", - 'got read for filth by the @joinpeanut door', - 'officially rejected by @joinpeanut.', - 'collected my first @joinpeanut badge: REJECTED ๐Ÿ’€', - 'took an L at the @joinpeanut door. building my comeback arc.', - "denied at the @joinpeanut door. the bouncer didn't even blink ๐Ÿšซ", - "@joinpeanut said NOT TONIGHT. guess i'll fix my whole life", - 'turned away from @joinpeanut ๐Ÿ’€', - "couldn't get past the @joinpeanut velvet rope. humbling.", - '@joinpeanut rejection #1. framing this one.', - 'the @joinpeanut door looked me up and down and said no. respect.', - 'not on the @joinpeanut list tonight. villain arc starts now.', - "got bounced from @joinpeanut. it's giving exclusive.", - '@joinpeanut denied me with zero explanation. iconic behavior honestly', - '213 tried, 7 got in. @joinpeanut said not me. yet.', - "@joinpeanut said come back when i'm somebody. challenge accepted ๐Ÿฅœ", - 'the @joinpeanut bouncer has no notes. just no. devastating.', - 'tried the @joinpeanut door. NOT TONIGHT. comeback arc loading.', - "@joinpeanut rejected me and i've never wanted in more ๐Ÿ’€", - 'took an L at the @joinpeanut door tonight. tomorrow we ride again.', - "remember @joinpeanut from devconnect?? they've rejected me from their beta programme omg", - "rember @joinpeanut from devconnect?? seems they're back but also they don't want users ??? WHAT?", -] +import type { DoorTally } from '@/components/Card/doorTally.utils' + +export function buildRejectionCaptions(tally: DoorTally): readonly string[] { + return [ + 'rejected by @joinpeanut ๐Ÿšซ the door policy is insane.', + '@joinpeanut has a bouncer now??', + "@joinpeanut told me i'm not on the list ๐Ÿ’€ the AUDACITY", + "the @joinpeanut bouncer said come back when i'm somebody. ok bet.", + 'got read for filth by the @joinpeanut door', + 'officially rejected by @joinpeanut.', + 'collected my first @joinpeanut badge: REJECTED ๐Ÿ’€', + 'took an L at the @joinpeanut door. building my comeback arc.', + "denied at the @joinpeanut door. the bouncer didn't even blink ๐Ÿšซ", + "@joinpeanut said NOT TONIGHT. guess i'll fix my whole life", + 'turned away from @joinpeanut ๐Ÿ’€', + "couldn't get past the @joinpeanut velvet rope. humbling.", + '@joinpeanut rejection #1. framing this one.', + 'the @joinpeanut door looked me up and down and said no. respect.', + 'not on the @joinpeanut list tonight. villain arc starts now.', + "got bounced from @joinpeanut. it's giving exclusive.", + '@joinpeanut denied me with zero explanation. iconic behavior honestly', + `${tally.applicants.toLocaleString('en-US')} tried, ${tally.admitted} got in. @joinpeanut said not me. yet.`, + "@joinpeanut said come back when i'm somebody. challenge accepted ๐Ÿฅœ", + 'the @joinpeanut bouncer has no notes. just no. devastating.', + 'tried the @joinpeanut door. NOT TONIGHT. comeback arc loading.', + "@joinpeanut rejected me and i've never wanted in more ๐Ÿ’€", + 'took an L at the @joinpeanut door tonight. tomorrow we ride again.', + "remember @joinpeanut from devconnect?? they've rejected me from their beta programme omg", + "rember @joinpeanut from devconnect?? seems they're back but also they don't want users ??? WHAT?", + ] +} /** Pick a random caption. Browser-only (Math.random) โ€” fine at share time. */ -export function pickRejectionCaption(): string { - return REJECTION_CAPTIONS[Math.floor(Math.random() * REJECTION_CAPTIONS.length)] +export function pickRejectionCaption(tally: DoorTally): string { + const captions = buildRejectionCaptions(tally) + return captions[Math.floor(Math.random() * captions.length)] } From e3852d1053d95b7e9dccc092db9dc235b9fa9899 Mon Sep 17 00:00:00 2001 From: 0xkkonrad Date: Thu, 23 Jul 2026 10:43:16 +0000 Subject: [PATCH 2/4] fix(card): drop hardcoded-stats captions instead of templating them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Konrad's call: rather than interpolating the live tally into the share caption, remove the fake-stats caption entirely ('213 tried, 7 got in') and de-number 'rejection #1' โ†’ 'my first rejection'. A no-digits test locks the invariant so stat-flavored captions can't sneak back in. --- src/components/Card/CardRejectionScreen.tsx | 2 +- .../__tests__/rejectionCaptions.test.ts | 17 ++--- .../Card/share-asset/rejectionCaptions.ts | 67 +++++++++---------- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/components/Card/CardRejectionScreen.tsx b/src/components/Card/CardRejectionScreen.tsx index 6004bfb38a..c2525f60c5 100644 --- a/src/components/Card/CardRejectionScreen.tsx +++ b/src/components/Card/CardRejectionScreen.tsx @@ -103,7 +103,7 @@ const CardRejectionScreen: FC = ({ // Clear any stale "failed to join" error from an earlier handleJoin so // the success state can't render alongside it (CodeRabbit). setJoinError(null) - const caption = pickRejectionCaption({ applicants, admitted }) + const caption = pickRejectionCaption() // Appeal = tweet AND join the waitlist. Joining is NOT access โ€” release // stays manual (admin grant) โ€” it just drops the appealer into the diff --git a/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts b/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts index af908f903e..9fe555957b 100644 --- a/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts +++ b/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts @@ -1,19 +1,16 @@ -import { buildRejectionCaptions, pickRejectionCaption } from '../rejectionCaptions' +import { REJECTION_CAPTIONS, pickRejectionCaption } from '../rejectionCaptions' describe('rejectionCaptions', () => { - test('numbers-flex caption uses the live door tally, not a hardcoded 213/7', () => { - const captions = buildRejectionCaptions({ applicants: 275, admitted: 12 }) - expect(captions).toContain('275 tried, 12 got in. @joinpeanut said not me. yet.') - expect(captions.some((c) => c.includes('213 tried'))).toBe(false) + test('no caption hardcodes numbers/stats โ€” they drift from the live door tally', () => { + const withDigits = REJECTION_CAPTIONS.filter((c) => /\d/.test(c)) + expect(withDigits).toEqual([]) }) - test('large tallies are en-US formatted to match the screen copy', () => { - const captions = buildRejectionCaptions({ applicants: 1500, admitted: 42 }) - expect(captions).toContain('1,500 tried, 42 got in. @joinpeanut said not me. yet.') + test('every caption tags @joinpeanut so the appeal rides the handle', () => { + expect(REJECTION_CAPTIONS.every((c) => c.includes('@joinpeanut'))).toBe(true) }) test('pickRejectionCaption returns a caption from the pool', () => { - const tally = { applicants: 275, admitted: 12 } - expect(buildRejectionCaptions(tally)).toContain(pickRejectionCaption(tally)) + expect(REJECTION_CAPTIONS).toContain(pickRejectionCaption()) }) }) diff --git a/src/components/Card/share-asset/rejectionCaptions.ts b/src/components/Card/share-asset/rejectionCaptions.ts index 87d863f56e..5c276b1d4e 100644 --- a/src/components/Card/share-asset/rejectionCaptions.ts +++ b/src/components/Card/share-asset/rejectionCaptions.ts @@ -7,44 +7,39 @@ * the rejection itself markets the door's exclusivity โ€” the handle rides the * caption (it is intentionally not drawn on the asset image). * - * The pool is built per share from the live door tally so the numbers-flex - * caption matches the "{tried} tried ยท {got in} got in" the screen renders. + * No caption may hardcode numbers/stats (waitlist size, admitted count) โ€” + * those drift from the live door tally the screen renders and read as + * fabricated data in a public tweet. The no-digits test enforces this. */ -import type { DoorTally } from '@/components/Card/doorTally.utils' - -export function buildRejectionCaptions(tally: DoorTally): readonly string[] { - return [ - 'rejected by @joinpeanut ๐Ÿšซ the door policy is insane.', - '@joinpeanut has a bouncer now??', - "@joinpeanut told me i'm not on the list ๐Ÿ’€ the AUDACITY", - "the @joinpeanut bouncer said come back when i'm somebody. ok bet.", - 'got read for filth by the @joinpeanut door', - 'officially rejected by @joinpeanut.', - 'collected my first @joinpeanut badge: REJECTED ๐Ÿ’€', - 'took an L at the @joinpeanut door. building my comeback arc.', - "denied at the @joinpeanut door. the bouncer didn't even blink ๐Ÿšซ", - "@joinpeanut said NOT TONIGHT. guess i'll fix my whole life", - 'turned away from @joinpeanut ๐Ÿ’€', - "couldn't get past the @joinpeanut velvet rope. humbling.", - '@joinpeanut rejection #1. framing this one.', - 'the @joinpeanut door looked me up and down and said no. respect.', - 'not on the @joinpeanut list tonight. villain arc starts now.', - "got bounced from @joinpeanut. it's giving exclusive.", - '@joinpeanut denied me with zero explanation. iconic behavior honestly', - `${tally.applicants.toLocaleString('en-US')} tried, ${tally.admitted} got in. @joinpeanut said not me. yet.`, - "@joinpeanut said come back when i'm somebody. challenge accepted ๐Ÿฅœ", - 'the @joinpeanut bouncer has no notes. just no. devastating.', - 'tried the @joinpeanut door. NOT TONIGHT. comeback arc loading.', - "@joinpeanut rejected me and i've never wanted in more ๐Ÿ’€", - 'took an L at the @joinpeanut door tonight. tomorrow we ride again.', - "remember @joinpeanut from devconnect?? they've rejected me from their beta programme omg", - "rember @joinpeanut from devconnect?? seems they're back but also they don't want users ??? WHAT?", - ] -} +export const REJECTION_CAPTIONS: readonly string[] = [ + 'rejected by @joinpeanut ๐Ÿšซ the door policy is insane.', + '@joinpeanut has a bouncer now??', + "@joinpeanut told me i'm not on the list ๐Ÿ’€ the AUDACITY", + "the @joinpeanut bouncer said come back when i'm somebody. ok bet.", + 'got read for filth by the @joinpeanut door', + 'officially rejected by @joinpeanut.', + 'collected my first @joinpeanut badge: REJECTED ๐Ÿ’€', + 'took an L at the @joinpeanut door. building my comeback arc.', + "denied at the @joinpeanut door. the bouncer didn't even blink ๐Ÿšซ", + "@joinpeanut said NOT TONIGHT. guess i'll fix my whole life", + 'turned away from @joinpeanut ๐Ÿ’€', + "couldn't get past the @joinpeanut velvet rope. humbling.", + 'my first @joinpeanut rejection. framing this one.', + 'the @joinpeanut door looked me up and down and said no. respect.', + 'not on the @joinpeanut list tonight. villain arc starts now.', + "got bounced from @joinpeanut. it's giving exclusive.", + '@joinpeanut denied me with zero explanation. iconic behavior honestly', + "@joinpeanut said come back when i'm somebody. challenge accepted ๐Ÿฅœ", + 'the @joinpeanut bouncer has no notes. just no. devastating.', + 'tried the @joinpeanut door. NOT TONIGHT. comeback arc loading.', + "@joinpeanut rejected me and i've never wanted in more ๐Ÿ’€", + 'took an L at the @joinpeanut door tonight. tomorrow we ride again.', + "remember @joinpeanut from devconnect?? they've rejected me from their beta programme omg", + "rember @joinpeanut from devconnect?? seems they're back but also they don't want users ??? WHAT?", +] /** Pick a random caption. Browser-only (Math.random) โ€” fine at share time. */ -export function pickRejectionCaption(tally: DoorTally): string { - const captions = buildRejectionCaptions(tally) - return captions[Math.floor(Math.random() * captions.length)] +export function pickRejectionCaption(): string { + return REJECTION_CAPTIONS[Math.floor(Math.random() * REJECTION_CAPTIONS.length)] } From 865b64f2b96f499d6b6dddcf318a8b25b33ad898 Mon Sep 17 00:00:00 2001 From: 0xkkonrad Date: Thu, 23 Jul 2026 11:20:26 +0000 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20drop=20the=20caption-pool=20test?= =?UTF-8?q?=20=E2=80=94=20keep=20the=20PR=20to=20the=20caption=20removal?= =?UTF-8?q?=20itself?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/rejectionCaptions.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts diff --git a/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts b/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts deleted file mode 100644 index 9fe555957b..0000000000 --- a/src/components/Card/share-asset/__tests__/rejectionCaptions.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { REJECTION_CAPTIONS, pickRejectionCaption } from '../rejectionCaptions' - -describe('rejectionCaptions', () => { - test('no caption hardcodes numbers/stats โ€” they drift from the live door tally', () => { - const withDigits = REJECTION_CAPTIONS.filter((c) => /\d/.test(c)) - expect(withDigits).toEqual([]) - }) - - test('every caption tags @joinpeanut so the appeal rides the handle', () => { - expect(REJECTION_CAPTIONS.every((c) => c.includes('@joinpeanut'))).toBe(true) - }) - - test('pickRejectionCaption returns a caption from the pool', () => { - expect(REJECTION_CAPTIONS).toContain(pickRejectionCaption()) - }) -}) From 30eaddb0b65657965eefa142045fa0d0e511d0fd Mon Sep 17 00:00:00 2001 From: 0xkkonrad Date: Thu, 23 Jul 2026 11:20:48 +0000 Subject: [PATCH 4/4] chore: drop stale test reference from the doc comment --- src/components/Card/share-asset/rejectionCaptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Card/share-asset/rejectionCaptions.ts b/src/components/Card/share-asset/rejectionCaptions.ts index 5c276b1d4e..0f15481a62 100644 --- a/src/components/Card/share-asset/rejectionCaptions.ts +++ b/src/components/Card/share-asset/rejectionCaptions.ts @@ -9,7 +9,7 @@ * * No caption may hardcode numbers/stats (waitlist size, admitted count) โ€” * those drift from the live door tally the screen renders and read as - * fabricated data in a public tweet. The no-digits test enforces this. + * fabricated data in a public tweet. */ export const REJECTION_CAPTIONS: readonly string[] = [