diff --git a/apps/website/i18n/course-ladder.ru.json b/apps/website/i18n/course-ladder.ru.json index 1058317c47..beff301171 100644 --- a/apps/website/i18n/course-ladder.ru.json +++ b/apps/website/i18n/course-ladder.ru.json @@ -12,6 +12,8 @@ "SAY_PAID_LINE": "Клуб оплачивается звёздами Telegram в боте, там же цена. Уже в клубе? Та же кнопка выдаст ключ.", "SAY_PAID_PRESS": "Вступить в клуб в Telegram", "SAY_PAID_LATER": "Оплата здесь в рублях, долларах или крипте ещё не сделана.", + "SAY_CLUB_TITLE": "Клуб открывает остальную лестницу", + "SAY_CLUB_LINE": "Четвёртый и все следующие курсы открываются с клубом: наша единственная подписка, оплачивается звёздами Telegram в боте, там же цена. Часть каждого платежа возвращается вам токенами.", "SAY_CHECKING": "Проверяем ключ...", "SAY_KEY_BAD": "Этот ключ не открыл курс ({0}). Нажмите кнопку ещё раз — бот выдаст новый.", "SAY_SHARED": "Ваши пройденные уроки видит автор курса." diff --git a/apps/website/specs/ladder/course-ladder.t27 b/apps/website/specs/ladder/course-ladder.t27 index 2e056e265c..ebefbd2f1c 100644 --- a/apps/website/specs/ladder/course-ladder.t27 +++ b/apps/website/specs/ladder/course-ladder.t27 @@ -38,6 +38,11 @@ pub const SAY_PAID_TITLE : str = "This course opens with the club"; pub const SAY_PAID_LINE : str = "The club is paid in Telegram Stars in the bot, which shows the price. Already in the club? The same button hands you the key."; pub const SAY_PAID_PRESS : str = "Join the club in Telegram"; pub const SAY_PAID_LATER : str = "Paying here in roubles, dollars or crypto is not built yet."; +; The teaser a reader meets INSIDE an open course (lesson one, below the pager): the gate +; above names the club only when a course is locked, and most readers never climb that far. +; No price here either -- the bot's card says it, same honesty as SAY_PAID_LINE. +pub const SAY_CLUB_TITLE : str = "The club opens the rest of the ladder"; +pub const SAY_CLUB_LINE : str = "Every course from the fourth on opens with the club: our one subscription, paid in Telegram Stars in the bot, which shows the price. Part of every payment returns to you as tokens."; pub const SAY_CHECKING : str = "Checking your key..."; pub const SAY_KEY_BAD : str = "This key did not open the course ({0}). Press the button again for a fresh one."; pub const SAY_SHARED : str = "Your finished lessons are shared with the course author."; @@ -53,3 +58,12 @@ test the_order_and_the_keys_stay_in_the_browser { assert KEYS_KEY == "t27-course-keys"; assert OPEN_KEY != KEYS_KEY; } + +; The gate's words answer "why is this locked"; the teaser answers "what is next" +; for a reader mid-course. If they ever become the same sentence, one of the two +; pages is saying the wrong thing. +test the_teaser_speaks_for_itself_not_for_the_gate { + assert SAY_CLUB_TITLE != SAY_PAID_TITLE; + assert SAY_CLUB_LINE != SAY_PAID_LINE; + assert SAY_CLUB_LINE != SAY_LADDER; +} diff --git a/apps/website/src/lib/courseLadder.generated.ts b/apps/website/src/lib/courseLadder.generated.ts index f056c1fa1e..8ef2c69a2a 100644 --- a/apps/website/src/lib/courseLadder.generated.ts +++ b/apps/website/src/lib/courseLadder.generated.ts @@ -22,6 +22,8 @@ export const LADDER = { "PAID_LINE": "The club is paid in Telegram Stars in the bot, which shows the price. Already in the club? The same button hands you the key.", "PAID_PRESS": "Join the club in Telegram", "PAID_LATER": "Paying here in roubles, dollars or crypto is not built yet.", + "CLUB_TITLE": "The club opens the rest of the ladder", + "CLUB_LINE": "Every course from the fourth on opens with the club: our one subscription, paid in Telegram Stars in the bot, which shows the price. Part of every payment returns to you as tokens.", "CHECKING": "Checking your key...", "KEY_BAD": "This key did not open the course ({0}). Press the button again for a fresh one.", "SHARED": "Your finished lessons are shared with the course author." @@ -40,6 +42,8 @@ export const LADDER = { "PAID_LINE": "Клуб оплачивается звёздами Telegram в боте, там же цена. Уже в клубе? Та же кнопка выдаст ключ.", "PAID_PRESS": "Вступить в клуб в Telegram", "PAID_LATER": "Оплата здесь в рублях, долларах или крипте ещё не сделана.", + "CLUB_TITLE": "Клуб открывает остальную лестницу", + "CLUB_LINE": "Четвёртый и все следующие курсы открываются с клубом: наша единственная подписка, оплачивается звёздами Telegram в боте, там же цена. Часть каждого платежа возвращается вам токенами.", "CHECKING": "Проверяем ключ...", "KEY_BAD": "Этот ключ не открыл курс ({0}). Нажмите кнопку ещё раз — бот выдаст новый.", "SHARED": "Ваши пройденные уроки видит автор курса." diff --git a/apps/website/src/pages/Course.tsx b/apps/website/src/pages/Course.tsx index 33a814e7ad..4bd31ce76e 100644 --- a/apps/website/src/pages/Course.tsx +++ b/apps/website/src/pages/Course.tsx @@ -234,7 +234,7 @@ function framesOf(l: CourseLesson): Frame[] { return [main, ...spec, ...also] } -function Lesson({ C, lesson, lang, say, done, toggle, shared }: { C: CourseT; lesson: CourseLesson; lang: Lang; say: Say; done: string[]; toggle: (id: string) => void; shared?: string }) { +function Lesson({ C, lesson, lang, say, done, toggle, shared, ladder }: { C: CourseT; lesson: CourseLesson; lang: Lang; say: Say; done: string[]; toggle: (id: string) => void; shared?: string; ladder: ReturnType }) { const LESSONS = C.lessons as readonly CourseLesson[] const TOTAL = LESSONS.length const frames = useMemo(() => framesOf(lesson), [lesson]) @@ -362,6 +362,8 @@ function Lesson({ C, lesson, lang, say, done, toggle, shared }: { C: CourseT; le )} + {lesson.n === 1 && !ladder.checking && !ladder.inClub ? : null} +

{fmt(say.PROGRESS, done.length, TOTAL)}

@@ -434,7 +436,29 @@ function useLadder(C: CourseT, allDone: string[]) { const open = () => setOpened((o) => openCourse(C.id, o, valid)) // The key a lesson report travels with: the paid one while it holds, else the bot's. const reportKey = valid.paid ? keys.paid : valid.bot ? keys.bot : undefined - return { state, open, checking, bad, reportKey } + return { state, open, checking, bad, reportKey, inClub: valid.paid } +} + +/** + * The club, named where a reader actually is. The gate below says the club only when a course + * is locked, and a reader who never climbs past their first open course never meets it at all; + * the bot's thirty-day funnel counted 1 club card against 56 new people. So lesson one carries + * the teaser: its words are the ladder spec's (SAY_CLUB_*), its button is the same start link + * the gate uses, and a reader already holding a paid key sees nothing (owner, 2026-10-13). + */ +function ClubTeaser({ C, lang }: { C: CourseT; lang: Lang }) { + const say = LADDER.say[lang] as LadderSay + return ( +
+

{say.CLUB_TITLE}

+

{say.CLUB_LINE}

+ +
+ ) } function LadderGate({ C, lang, ladder }: { C: CourseT; lang: Lang; ladder: ReturnType }) { @@ -512,7 +536,7 @@ export default function Course() { ) : lesson ? ( - + ) : ( )}