ReviewOS

also looking at this

stacks/lastsoulultra

LSU's own coaching surface, and what auditing the site turned up

#12
Merged glennmichael123 wants to merge claude/last-soul-ultra-web-6e0718 into main
86 files +3943 -1190

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

public/assets/styles/production/coaching.cssmodified+1-0
Changes to public/assets/styles/production/coaching.css
@@ -77,6 +77,7 @@
7777.coaching-surface .coach-offer-price {text-align:right;white-space:nowrap}
7878.coaching-surface .coach-offer-price b {display:block;font:400 clamp(22px,2.4vw,30px)/1.1 var(--lsu-display);font-weight:400}
7979.coaching-surface .coach-offer-trial {display:block;margin-top:8px;color:var(--lsu-dim-3);font:400 13px/1.4 var(--lsu-body)}
80.coaching-surface .coach-offer-method {color:var(--lsu-dim-2);font-size:15px}
8081.coaching-surface .coach-offer-note {margin:14px 0 0;color:var(--lsu-dim-3);font:400 13px/1.5 var(--lsu-body)}
8182
8283/* The hub's index of modules. A ruled list rather than a card grid: eight
resources/assets/styles/site/coaching.cssmodified+1-0
Changes to resources/assets/styles/site/coaching.css
@@ -77,6 +77,7 @@
7777.coaching-surface .coach-offer-price {text-align:right;white-space:nowrap}
7878.coaching-surface .coach-offer-price b {display:block;font:400 clamp(22px,2.4vw,30px)/1.1 var(--lsu-display);font-weight:400}
7979.coaching-surface .coach-offer-trial {display:block;margin-top:8px;color:var(--lsu-dim-3);font:400 13px/1.4 var(--lsu-body)}
80.coaching-surface .coach-offer-method {color:var(--lsu-dim-2);font-size:15px}
8081.coaching-surface .coach-offer-note {margin:14px 0 0;color:var(--lsu-dim-3);font:400 13px/1.5 var(--lsu-body)}
8182
8283/* The hub's index of modules. A ruled list rather than a card grid: eight
resources/components/CoachingOffers.stxmodified+18-2
Changes to resources/components/CoachingOffers.stx
@@ -23,10 +23,21 @@ const offerHost = portalHost()
2323 Getting it wrong is an h1 followed by an h3, which is how a screen-reader
2424 user loses the outline of the page. */
2525const offerTop = String(level) === '2'
26
27/* Two different things, and a storefront needs both: what this tier gives you
28 (the coach's product description) and how the coach trains people (their
29 published method summary). HQ sometimes carries the same sentence in both,
30 so the second is dropped when it repeats the first rather than printing a
31 paragraph twice. */
32const offerRows = offers.map((offer) => {
33 const includes = offer.product.description.trim()
34 const approach = offer.method.summary.trim()
35 return { ...offer, includes, approach: approach && approach !== includes ? approach : '' }
36})
2637</script>
2738@if(offers.length > 0)
2839<ul class="coach-offers">
29 @foreach(offers as offer)
40 @foreach(offerRows as offer)
3041 <li class="coach-offer">
3142 <div>
3243 @if(offerTop)
@@ -35,7 +46,12 @@ const offerTop = String(level) === '2'
3546 <h3 class="coach-offer-name">{{ offer.label }}</h3>
3647 @endif
3748 <p class="coach-offer-by">{{ offer.coach }}</p>
38 <p>{{ offer.method.summary }}</p>
49 @if(offer.includes)
50 <p>{{ offer.includes }}</p>
51 @endif
52 @if(offer.approach)
53 <p class="coach-offer-method">{{ offer.approach }}</p>
54 @endif
3955 <p class="coach-offer-note">{{ offer.method.name }}, version {{ offer.method.version }}. Bought and billed at {{ offerHost }}.</p>
4056 </div>
4157 <div class="coach-offer-price">
tests/coaching-views.test.tsmodified+20-0
Changes to tests/coaching-views.test.ts
@@ -196,3 +196,23 @@ describe('warming the connection to HQ', () => {
196196 expect(html.indexOf('rel="preconnect"')).toBeLessThan(html.indexOf('hq-coaching.js'))
197197 })
198198})
199
200describe('what a tier says it is', () => {
201 it('prints what the tier includes and how the coach trains', async () => {
202 stubHq()
203 const plans = await render('plans')
204
205 expect(plans).toContain('€19.99 is a sample price')
206 expect(plans).toContain('Demonstration content, not an approved LSU coach method.')
207 })
208
209 it('prints one paragraph when HQ carries the same sentence in both', async () => {
210 const same = structuredClone(payload)
211 same.offers[0].method.summary = same.offers[0].product.description
212 stubHq(same)
213 const plans = await render('plans')
214
215 expect(plans).not.toContain('coach-offer-method')
216 expect([...plans.matchAll(/sample price/g)]).toHaveLength(1)
217 })
218})