From 44d9f4cccac4b11a8ed0ae841d687852e4a1105a Mon Sep 17 00:00:00 2001 From: Asapteejo Date: Fri, 18 Sep 2026 12:10:08 +0100 Subject: [PATCH] feat(marketing): restructure the tenant homepage around one hero message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero carried the eyebrow, headline, subhead, three CTAs, a four-field search, three stat cards, the image, an overlay panel AND the how-it-works steps inside one bordered box. Everything competed for width, which is what made individual pieces look broken rather than any one of them being badly styled. Hero now holds the headline, subhead and a single primary CTA, with the search spanning its full width underneath. That alone fixed the clipped placeholders ("City, state, or estate r", "Max budget ("), which were clipped because the four-column form sat in the 1.2fr text column; the placeholders are also shorter now. How it works moved out of the hero's 0.8fr sidebar into its own full-width section below the featured listings, three across at md+ and stacked on mobile. In the sidebar each step card had ~121px and the copy broke to one word per line; they now measure 395px at 1440 and 335px at 375. The trust strip never renders a zero. A brand-new tenant's homepage read "EXPERT MARKETERS 0 / HAPPY CLIENTS 0" in large type above the fold — on a site whose entire pitch is trust, that is worse than showing nothing. Cards with a zero count are dropped and the strip disappears entirely if fewer than two survive. Labels and notes stay CMS-editable. Property cards are flex columns with clamped location/title/description, a 4:3 aspect-ratio image instead of a fixed h-72, and the price/CTA row pinned to the bottom, so one verbose listing no longer stretches every sibling in its row. Two notes on keeping copy CMS-driven: SectionHeading's description is now optional, because the journey section has a CMS heading but no supporting sentence and inventing one would hardcode copy tenants cannot edit; and the journey section has no eyebrow badge, because the CMS heading is itself the section name and the badge only repeated it. Measured at 1440 and 375: step cards 121px -> 395px, no clipped placeholders, footer stacked, hero down to one primary CTA. Co-Authored-By: Claude Opus 5 --- src/components/marketing/property-card.tsx | 25 +-- src/components/marketing/tenant-homepage.tsx | 183 ++++++++++++------- src/components/shared/section-heading.tsx | 11 +- 3 files changed, 135 insertions(+), 84 deletions(-) diff --git a/src/components/marketing/property-card.tsx b/src/components/marketing/property-card.tsx index 177577a..9f4c859 100644 --- a/src/components/marketing/property-card.tsx +++ b/src/components/marketing/property-card.tsx @@ -20,8 +20,11 @@ export function PropertyCard({ property }: { property: PropertySummary }) { .filter(Boolean); return ( - -
+ // Flex column + clamped text: card height used to track content length, so + // one verbose listing stretched every sibling in the row. The price/CTA row + // is pushed to the bottom (mt-auto below) so cards line up. + +
-
+
-
{property.locationSummary}
-

{property.title}

-

{property.shortDescription}

+
{property.locationSummary}
+

{property.title}

+

{property.shortDescription}

{/* verification.label carries internal states ("Listing hidden", "Verification required"). Only the verified case is shown. */} {property.verification.status === "VERIFIED" ? (

{property.verification.label}

) : null}
-
+
{isLand ? ( <> - {property.landSizeSqm ? `${property.landSizeSqm} sqm` : "Land"} - {property.numberOfPlots ? `${property.numberOfPlots} plot${property.numberOfPlots === 1 ? "" : "s"}` : "Plots"} - {landSizes.length > 0 ? landSizes.slice(0, 2).join(", ") : "Flexible sizes"} + {property.landSizeSqm ? `${property.landSizeSqm} sqm` : "Land"} + {property.numberOfPlots ? `${property.numberOfPlots} plot${property.numberOfPlots === 1 ? "" : "s"}` : "Plots"} + {landSizes.length > 0 ? landSizes.slice(0, 2).join(", ") : "Flexible sizes"} ) : ( <> @@ -66,7 +69,7 @@ export function PropertyCard({ property }: { property: PropertySummary }) { )}
-
+
Starting from diff --git a/src/components/marketing/tenant-homepage.tsx b/src/components/marketing/tenant-homepage.tsx index edce099..14a6bc4 100644 --- a/src/components/marketing/tenant-homepage.tsx +++ b/src/components/marketing/tenant-homepage.tsx @@ -105,69 +105,20 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) { {siteContent.hero.subhead}

+ {/* One primary action. The secondary route (marketers) lives + in its own section further down, and the header already + carries the portal link — three CTAs here competed with the + search, which is the actual front door. */}
- + - - - - +
- {/* Property search — the front door of a real estate site. - Plain GET form to /properties, matching its filter params. */} -
- - - - -
-
-
- {[ - [siteContent.heroStats.inventoryLabel, latestInventory.total, siteContent.heroStats.inventoryNote], - [siteContent.heroStats.marketersLabel, leaderboard.length, siteContent.heroStats.marketersNote], - [siteContent.heroStats.trustLabel, testimonials.length, siteContent.heroStats.trustNote], - ].map(([label, value, note]) => ( - -
- {label} -
-
- {value} -
-

{note}

-
- ))}
@@ -194,25 +145,89 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) {
- -
-
- {siteContent.journey.heading} -
-
-
- {siteContent.journey.steps.map((step, index) => ( -
-
0{index + 1}
-
{step.title}
-

{step.description}

-
- ))} -
-
+ + {/* Search spans the hero's full width instead of sharing the text + column with the headline, which is what clipped the placeholders + ("City, state, or estate r", "Max budget ("). */} +
+
+ + + + +
+
+ + {/* Trust strip — only real numbers. A brand-new tenant otherwise + advertised "EXPERT MARKETERS 0 / HAPPY CLIENTS 0" in large type + above the fold, which is worse than showing nothing. A card with a + zero count is dropped, and the strip disappears entirely if fewer + than two survive. Labels and notes stay CMS-editable. */} + {(() => { + const stats = [ + { + label: siteContent.heroStats.inventoryLabel, + value: latestInventory.total, + note: siteContent.heroStats.inventoryNote, + }, + { + label: siteContent.heroStats.marketersLabel, + value: leaderboard.length, + note: siteContent.heroStats.marketersNote, + }, + { + label: siteContent.heroStats.trustLabel, + value: testimonials.length, + note: siteContent.heroStats.trustNote, + }, + ].filter((stat) => stat.value > 0); + + if (stats.length < 2) { + return null; + } + + return ( +
+ {stats.map((stat) => ( + +
+ {stat.label} +
+
{stat.value}
+

{stat.note}

+
+ ))} +
+ ); + })()} @@ -246,6 +261,32 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) { + {/* How it works — moved out of the hero's 0.8fr sidebar, where three + cards shared ~121px each and the copy broke to one word per line. + A primary trust explainer earns the full measure. */} + +
+ {/* No eyebrow badge here: the CMS heading is itself the section + name ("How it works" by default), so a badge above it just + repeats the words. Typography matches SectionHeading's title. */} +

+ {siteContent.journey.heading} +

+
+ {siteContent.journey.steps.map((step, index) => ( + +
0{index + 1}
+
{step.title}
+

{step.description}

+
+ ))} +
+
+
+ @@ -16,7 +21,9 @@ export function SectionHeading({

{title}

-

{description}

+ {description ? ( +

{description}

+ ) : null}
);