v0.2.0:39 — Buy page: render tier card for single-public-policy products
Previously the tier picker gated on `policies.len() < 2` and returned an empty string when a product had only one public policy. Buyers saw just the price card + form — none of the entitlements, marketing bullets, or description the operator had carefully authored on that tier. Reported against the Recap product, which has 3 policies but only Pro public; Pro's bullets were invisible to buyers. Fixed: - render_tier_picker gate flipped from `< 2` to `is_empty()`. A single public policy now renders a single tier card. - New `.tiers-1` grid class: one centered column at ~480px max-width. Keeps the single card from stretching to the full 1040px container. - `n` computation extends to handle 1 in the existing match arm. The price card below the picker still renders unchanged for the single-policy case — acts as the buy-confirmation summary. Operators keeping most tiers private and only exposing one to buyers now get the same rich tier-card render that multi-tier products always had. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -305,6 +305,10 @@ h1 {{
|
|||||||
1fr /* row 7: features (fills) */
|
1fr /* row 7: features (fills) */
|
||||||
auto; /* row 8: button */
|
auto; /* row 8: button */
|
||||||
}}
|
}}
|
||||||
|
/* Single-policy products: one centered card at a comfortable width.
|
||||||
|
Wide enough to read entitlements + marketing bullets clearly without
|
||||||
|
stretching across the full container. */
|
||||||
|
.tiers-1 {{ grid-template-columns:minmax(0, 480px); justify-content:center; }}
|
||||||
.tiers-2 {{ grid-template-columns:repeat(2, 1fr); }}
|
.tiers-2 {{ grid-template-columns:repeat(2, 1fr); }}
|
||||||
.tiers-3 {{ grid-template-columns:repeat(3, 1fr); }}
|
.tiers-3 {{ grid-template-columns:repeat(3, 1fr); }}
|
||||||
.tiers-4 {{ grid-template-columns:repeat(2, 1fr); }}
|
.tiers-4 {{ grid-template-columns:repeat(2, 1fr); }}
|
||||||
@@ -1093,19 +1097,23 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the server-rendered tier-picker HTML. Returns an empty string
|
/// Build the server-rendered tier-picker HTML. Returns an empty string
|
||||||
/// when the product has fewer than 2 public policies (i.e., the existing
|
/// only when the product has zero public policies (the bare price-card +
|
||||||
/// single-price view is sufficient).
|
/// form fallback covers that case). For one public policy, we still
|
||||||
|
/// render a single tier card so the operator-configured entitlements
|
||||||
|
/// and marketing bullets surface — without this, single-tier products
|
||||||
|
/// showed only price + form, eating the operator's tier copy.
|
||||||
fn render_tier_picker(
|
fn render_tier_picker(
|
||||||
policies: &[crate::models::Policy],
|
policies: &[crate::models::Policy],
|
||||||
initial: &Option<crate::models::Policy>,
|
initial: &Option<crate::models::Policy>,
|
||||||
product: &crate::models::Product,
|
product: &crate::models::Product,
|
||||||
featured_by_policy: &std::collections::HashMap<String, crate::models::DiscountCode>,
|
featured_by_policy: &std::collections::HashMap<String, crate::models::DiscountCode>,
|
||||||
) -> String {
|
) -> String {
|
||||||
if policies.len() < 2 {
|
if policies.is_empty() {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
let n = policies.len().min(4);
|
let n = policies.len().min(4);
|
||||||
let class_n = match n {
|
let class_n = match n {
|
||||||
|
1 => "tiers-1",
|
||||||
2 => "tiers-2",
|
2 => "tiers-2",
|
||||||
3 => "tiers-3",
|
3 => "tiers-3",
|
||||||
_ => "tiers-4",
|
_ => "tiers-4",
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ const RELEASE_NOTES = [
|
|||||||
// in RELEASE_NOTES above (the milestone). Subsequent revisions
|
// in RELEASE_NOTES above (the milestone). Subsequent revisions
|
||||||
// append here.
|
// append here.
|
||||||
const ROUTINE_NOTES = [
|
const ROUTINE_NOTES = [
|
||||||
|
'0.2.0:39 — **Buy page now renders a tier card for single-public-policy products.** Previously the tier picker only rendered when a product had two or more public policies; single-public-policy products fell back to a bare price card + form, swallowing all the operator-configured entitlements, marketing bullets, and tier descriptions. Fixed: render a single centered tier card (new `.tiers-1` grid class, ~480px max-width) whenever there\'s at least one public policy. Operators who keep most tiers private and only expose one (e.g. "Pro" public, "Core" and "Max" admin-only) now see the same rich tier-card render that multi-tier products get. The price card below still renders unchanged as the buy-confirmation summary.',
|
||||||
|
'',
|
||||||
'0.2.0:38 — **Admin UI: Create-product Cancel button + modal-overflow fix across all dialogs.** Two operator-reported bugs.',
|
'0.2.0:38 — **Admin UI: Create-product Cancel button + modal-overflow fix across all dialogs.** Two operator-reported bugs.',
|
||||||
'',
|
'',
|
||||||
'**Create product: Cancel button.** The "Create a new product" disclosure had a Create button but no way to back out without scrolling up to the chevron. Added a secondary Cancel button alongside Create — collapses the disclosure (returns to the products list) without clearing typed input, so re-expanding picks up where the operator left off.',
|
'**Create product: Cancel button.** The "Create a new product" disclosure had a Create button but no way to back out without scrolling up to the chevron. Added a secondary Cancel button alongside Create — collapses the disclosure (returns to the products list) without clearing typed input, so re-expanding picks up where the operator left off.',
|
||||||
@@ -505,7 +507,7 @@ const ROUTINE_NOTES = [
|
|||||||
].join('\n\n')
|
].join('\n\n')
|
||||||
|
|
||||||
export const v0_2_0 = VersionInfo.of({
|
export const v0_2_0 = VersionInfo.of({
|
||||||
version: '0.2.0:38',
|
version: '0.2.0:39',
|
||||||
releaseNotes: { en_US: ROUTINE_NOTES },
|
releaseNotes: { en_US: ROUTINE_NOTES },
|
||||||
// No on-disk transformation needed — v0.2.0:0 is a label change.
|
// No on-disk transformation needed — v0.2.0:0 is a label change.
|
||||||
// SQLite-level migrations live separately under
|
// SQLite-level migrations live separately under
|
||||||
|
|||||||
Reference in New Issue
Block a user