From ae5d16f8fd35cfb057ed70176898b65dba90a5e9 Mon Sep 17 00:00:00 2001 From: Asapteejo Date: Fri, 18 Sep 2026 19:17:38 +0100 Subject: [PATCH 1/3] feat(marketing): premium pass on the tenant homepage, plus a How it works page Three changes, all layout and hierarchy rather than a rebrand. Photography. A tenant that has not uploaded a hero image shipped a plain grey gradient where the photograph should be, which is the single biggest reason the page did not read as premium. The hero now falls back to the first featured listing's photo, so a tenant only sees the gradient when it has neither a hero image nor a published listing. Chrome. The trust strip and the buyer journey were bordered, shadowed cards sitting inside an already bordered section: outlines stacked on outlines read as clutter, not as quality. Both are now plain columns separated by hairline rules, with larger numerals and more space around them. Overlay label. The hero panel badge carries the company name, so long names wrapped to two lines inside a pill built for one and broke its shape. It is a plain uppercase label now, which wraps without looking broken. How it works also gets its own page at /how-it-works, driven by the same CMS journey content the homepage summarises, and a footer link beside About and Team. No new copy: a tenant edits the steps once and both surfaces follow. Deliberately not added to the header nav, which already carries six links and two CTAs; crowding it would undo the hero work. Co-Authored-By: Claude Opus 5 --- src/app/(marketing)/how-it-works/page.tsx | 70 +++++++++++++++++++ src/components/marketing/marketing-footer.tsx | 1 + src/components/marketing/tenant-homepage.tsx | 43 ++++++++---- 3 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 src/app/(marketing)/how-it-works/page.tsx diff --git a/src/app/(marketing)/how-it-works/page.tsx b/src/app/(marketing)/how-it-works/page.tsx new file mode 100644 index 0000000..dd427ee --- /dev/null +++ b/src/app/(marketing)/how-it-works/page.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; + +import { Container } from "@/components/shared/container"; +import { Reveal } from "@/components/shared/reveal"; +import { Button } from "@/components/ui/button"; +import { requirePublicTenantContext } from "@/lib/tenancy/context"; +import { getPublicTenantPresentation } from "@/modules/branding/service"; +import { resolveTenantSiteContent } from "@/modules/cms/site-content"; +import { getPublishedSiteContent } from "@/modules/cms/site-content-service"; + +export const dynamic = "force-dynamic"; + +/** + * The buyer journey on its own page, driven by the same CMS content the + * homepage summarises (siteContent.journey). No new copy is introduced here: + * a tenant edits the steps once and both surfaces follow. + */ +export default async function HowItWorksPage() { + const tenant = await requirePublicTenantContext(); + const presentation = await getPublicTenantPresentation(tenant); + const storedContent = await getPublishedSiteContent(tenant); + const content = resolveTenantSiteContent({ + companyName: presentation.companyName, + startPurchaseHref: "/portal", + stored: storedContent, + }); + + return ( + + +
+
+ {presentation.companyName} +
+

+ {content.journey.heading} +

+

{content.hero.subhead}

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

    + {step.title} +

    +

    {step.description}

    +
  2. + ))} +
+
+ + +
+ + + + + + +
+
+
+ ); +} diff --git a/src/components/marketing/marketing-footer.tsx b/src/components/marketing/marketing-footer.tsx index 370d2a3..bd37daa 100644 --- a/src/components/marketing/marketing-footer.tsx +++ b/src/components/marketing/marketing-footer.tsx @@ -110,6 +110,7 @@ export function MarketingFooter({
About + How it works Team {showCareers ? ( Careers diff --git a/src/components/marketing/tenant-homepage.tsx b/src/components/marketing/tenant-homepage.tsx index 14a6bc4..9ba97b4 100644 --- a/src/components/marketing/tenant-homepage.tsx +++ b/src/components/marketing/tenant-homepage.tsx @@ -73,7 +73,13 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) { featuredInventory.items.length > 0 ? featuredInventory.items.slice(0, 3) : latestInventory.items.slice(0, 3); - const heroImageUrl = presentation.branding.heroImageUrl; + // Photography is what makes a property site feel premium, and a tenant that + // has not uploaded a hero image was shipping a plain grey gradient. Fall back + // to the first featured listing's photo so no tenant launches without one. + const heroImageUrl = + presentation.branding.heroImageUrl ?? + featuredProperties.find((property) => property.images[0])?.images[0] ?? + null; const startPurchaseHref = buildAuthRedirect(runtimeConfig, { returnTo: "/portal", tenantSlug: tenant.companySlug, @@ -134,7 +140,12 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) { ) : null}
- {siteContent.heroPanel.badge} + {/* Plain label rather than a Badge: this string includes the + company name, so long names wrapped to two lines inside a + pill built for one and broke its rounded shape. */} +
+ {siteContent.heroPanel.badge} +

{siteContent.heroPanel.title} @@ -215,15 +226,18 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) { } return ( -
+ // Plain columns divided by hairlines rather than three bordered, + // shadowed cards: the strip already sits inside a bordered section, + // and stacked outlines read as clutter rather than as quality. +
{stats.map((stat) => ( - +
{stat.label}
-
{stat.value}
-

{stat.note}

- +
{stat.value}
+

{stat.note}

+
))}
); @@ -272,16 +286,15 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) {

{siteContent.journey.heading}

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

{step.description}

- +
))}
From 8ab1cf9392963e0c13d0248570c23ac80895ef54 Mon Sep 17 00:00:00 2001 From: Asapteejo Date: Fri, 18 Sep 2026 19:35:36 +0100 Subject: [PATCH 2/3] style(copy): remove em dashes from user-facing text Em dashes read as machine-written, so every string a user can see now uses ordinary punctuation. Replacements were chosen per context rather than applied blindly: a comma where the dash joined a clause, a full stop where a comma would have produced a splice ("before it goes live. No stale or hidden inventory."), a colon where the dash separated a label from its value ("Payment overdue: action required"), a pipe in the SEO title, and a plain hyphen for the "no value yet" placeholders in tables. Two automated replacements were wrong and were rewritten by hand: a WhatsApp message had become a title-style pipe, and a notification subject had ended up with two colons. Code comments still contain em dashes and are handled separately, so this commit is only the text a user actually reads. Co-Authored-By: Claude Opus 5 --- src/app/(admin)/admin/announcements/page.tsx | 2 +- src/app/(admin)/admin/benchmarks/page.tsx | 16 ++++++------- src/app/(admin)/admin/contracts/page.tsx | 2 +- src/app/(admin)/admin/invoices/page.tsx | 2 +- .../admin/listings/marketplace/page.tsx | 6 ++--- src/app/(admin)/admin/marketer/page.tsx | 2 +- src/app/(admin)/admin/overview/page.tsx | 2 +- src/app/(admin)/admin/settings/page.tsx | 2 +- .../admin/settings/site-content/page.tsx | 2 +- .../admin/team/commission-rules/actions.ts | 2 +- src/app/(admin)/admin/users/page.tsx | 2 +- src/app/(marketing)/about/page.tsx | 2 +- src/app/(marketing)/brochure/page.tsx | 4 ++-- .../platform/properties/page.tsx | 4 ++-- src/app/(portal)/portal/contracts/page.tsx | 2 +- .../[companyId]/page.tsx | 2 +- .../[inquiryId]/draft-reply/route.ts | 6 ++--- src/app/app/access/page.tsx | 2 +- src/app/layout.tsx | 2 +- src/app/manifest.ts | 2 +- src/components/admin/announcement-list.tsx | 2 +- .../admin/executive-overview-board.tsx | 2 +- .../admin/feasibility-ai-narrative.tsx | 2 +- .../admin/feasibility-project-hub.tsx | 2 +- .../admin/inspection-management.tsx | 2 +- src/components/admin/invoice-list.tsx | 2 +- .../admin/payment-account-setup.tsx | 4 ++-- .../admin/provision-result-modal.tsx | 2 +- .../admin/site-content-management.tsx | 24 +++++++++---------- src/components/marketing/inquiry-form.tsx | 2 +- src/components/marketing/property-card.tsx | 2 +- src/components/marketing/tenant-homepage.tsx | 6 ++--- src/components/messaging/conversation.tsx | 2 +- .../platform/marketplace-property-card.tsx | 2 +- src/components/platform/platform-home.tsx | 8 +++---- .../portal/dashboard-mobile-nav.tsx | 2 +- src/components/portal/dashboard-shell.tsx | 2 +- src/components/ui/data-table.tsx | 2 +- src/lib/notifications/templates.ts | 24 +++++++++---------- src/lib/tenancy/context.ts | 2 +- src/modules/automation/service.ts | 2 +- src/modules/cms/site-content.test.ts | 2 +- src/modules/cms/site-content.ts | 20 ++++++++-------- .../recommendations.ts | 12 +++++----- src/modules/inquiries/service.ts | 2 +- src/modules/inspections/service.ts | 2 +- src/modules/invoices/service.ts | 2 +- src/modules/morning-briefing/aggregator.ts | 6 ++--- src/modules/transactions/mutations.ts | 4 ++-- 49 files changed, 107 insertions(+), 107 deletions(-) diff --git a/src/app/(admin)/admin/announcements/page.tsx b/src/app/(admin)/admin/announcements/page.tsx index 68d823a..28b9dfe 100644 --- a/src/app/(admin)/admin/announcements/page.tsx +++ b/src/app/(admin)/admin/announcements/page.tsx @@ -15,7 +15,7 @@ export default async function AdminAnnouncementsPage() {
diff --git a/src/app/(admin)/admin/benchmarks/page.tsx b/src/app/(admin)/admin/benchmarks/page.tsx index 02125a8..d75a672 100644 --- a/src/app/(admin)/admin/benchmarks/page.tsx +++ b/src/app/(admin)/admin/benchmarks/page.tsx @@ -75,7 +75,7 @@ function BenchmarkBar({ if (band.sampleSize < 3) { return (

- Platform data unavailable — not enough agencies in distribution yet. + Platform data unavailable, not enough agencies in distribution yet.

); } @@ -181,7 +181,7 @@ function MetricComparisonPanel({ Percentile
- {band.sampleSize >= 3 ? `${band.tenantPercentileRank}th` : "—"} + {band.sampleSize >= 3 ? `${band.tenantPercentileRank}th` : "-"}
@@ -361,7 +361,7 @@ export default async function AdminBenchmarksPage() { value: report.topPropertyTypes[0]?.type ? report.topPropertyTypes[0].type.replaceAll("_", " ").toLowerCase() - : "—", + : "-", hint: report.topPropertyTypes[0] ? `${report.topPropertyTypes[0].tenantDealCount} deal${report.topPropertyTypes[0].tenantDealCount === 1 ? "" : "s"} · ${formatCurrency(report.topPropertyTypes[0].tenantRevenue)} revenue` @@ -373,7 +373,7 @@ export default async function AdminBenchmarksPage() { {/* Anonymisation notice */} - Conversion rate — above + Conversion rate, above the median means your team qualifies and closes buyer interest more effectively than most. @@ -444,7 +444,7 @@ export default async function AdminBenchmarksPage() {
  • - Deal velocity — lower + Deal velocity, lower is better. Faster closures reduce buyer drop-off risk and improve cash collection. @@ -452,7 +452,7 @@ export default async function AdminBenchmarksPage() {
  • - Default rate — lower + Default rate, lower is better. A high default rate signals collection problems or over-stretched buyers. @@ -461,7 +461,7 @@ export default async function AdminBenchmarksPage() { The shaded bar is the - interquartile range — the middle 50% of all agencies. Being + interquartile range, the middle 50% of all agencies. Being inside the band is normal; above p75 is strong performance.
  • diff --git a/src/app/(admin)/admin/contracts/page.tsx b/src/app/(admin)/admin/contracts/page.tsx index cc41e25..bf1c49c 100644 --- a/src/app/(admin)/admin/contracts/page.tsx +++ b/src/app/(admin)/admin/contracts/page.tsx @@ -94,7 +94,7 @@ function UploadContractForm({ transactions }: { transactions: TransactionWithout const buyer = [tx.user.firstName, tx.user.lastName].filter(Boolean).join(" ") || "Buyer"; return ( ); })} diff --git a/src/app/(admin)/admin/invoices/page.tsx b/src/app/(admin)/admin/invoices/page.tsx index f927734..0ef4f3a 100644 --- a/src/app/(admin)/admin/invoices/page.tsx +++ b/src/app/(admin)/admin/invoices/page.tsx @@ -15,7 +15,7 @@ export default async function AdminInvoicesPage() {
    diff --git a/src/app/(admin)/admin/listings/marketplace/page.tsx b/src/app/(admin)/admin/listings/marketplace/page.tsx index 0d50b32..9f62e44 100644 --- a/src/app/(admin)/admin/listings/marketplace/page.tsx +++ b/src/app/(admin)/admin/listings/marketplace/page.tsx @@ -22,7 +22,7 @@ export default async function AdminMarketplacePage() {
    @@ -40,7 +40,7 @@ export default async function AdminMarketplacePage() { {properties.length > 0 ? ( @@ -76,7 +76,7 @@ export default async function AdminMarketplacePage() { {property.title}
    - {property.locationSummary ?? "—"} + {property.locationSummary ?? "-"}
    diff --git a/src/app/(admin)/admin/marketer/page.tsx b/src/app/(admin)/admin/marketer/page.tsx index 83ac8c8..d6ca290 100644 --- a/src/app/(admin)/admin/marketer/page.tsx +++ b/src/app/(admin)/admin/marketer/page.tsx @@ -12,7 +12,7 @@ export default async function AdminMarketerPage() { diff --git a/src/app/(admin)/admin/overview/page.tsx b/src/app/(admin)/admin/overview/page.tsx index 3a62b19..617beb1 100644 --- a/src/app/(admin)/admin/overview/page.tsx +++ b/src/app/(admin)/admin/overview/page.tsx @@ -24,7 +24,7 @@ export default async function AdminOverviewPage() { diff --git a/src/app/(admin)/admin/settings/page.tsx b/src/app/(admin)/admin/settings/page.tsx index 5cd0907..37686c1 100644 --- a/src/app/(admin)/admin/settings/page.tsx +++ b/src/app/(admin)/admin/settings/page.tsx @@ -45,7 +45,7 @@ export default async function AdminSettingsPage() { title="Settings" subtitle="Manage tenant branding, defaults, payment display rules, and public staff visibility without developer intervention." > - {/* Domain card — top priority */} + {/* Domain card, top priority */} {readiness ? ( diff --git a/src/app/(admin)/admin/team/commission-rules/actions.ts b/src/app/(admin)/admin/team/commission-rules/actions.ts index ec8aa96..c301929 100644 --- a/src/app/(admin)/admin/team/commission-rules/actions.ts +++ b/src/app/(admin)/admin/team/commission-rules/actions.ts @@ -21,7 +21,7 @@ export async function createCommissionRuleAction(formData: FormData) { const propertyId = (formData.get("propertyId") as string | null)?.trim() || null; if (!name || !feeType || !["FLAT", "PERCENTAGE"].includes(feeType)) { - return; // basic guard — real validation happens client-side + return; // basic guard, real validation happens client-side } const flatAmount = diff --git a/src/app/(admin)/admin/users/page.tsx b/src/app/(admin)/admin/users/page.tsx index 2847acf..db6a6e2 100644 --- a/src/app/(admin)/admin/users/page.tsx +++ b/src/app/(admin)/admin/users/page.tsx @@ -15,7 +15,7 @@ export default async function AdminUsersPage() { diff --git a/src/app/(marketing)/about/page.tsx b/src/app/(marketing)/about/page.tsx index 2bb815b..3eb3251 100644 --- a/src/app/(marketing)/about/page.tsx +++ b/src/app/(marketing)/about/page.tsx @@ -15,7 +15,7 @@ const VALUES: { icon: LucideIcon; title: string; body: string }[] = [ { icon: Eye, title: "Clarity", - body: "Listing discovery, transaction progress, payments, and documents stay visible and structured — for buyers and the team alike.", + body: "Listing discovery, transaction progress, payments, and documents stay visible and structured, for buyers and the team alike.", }, { icon: Zap, diff --git a/src/app/(marketing)/brochure/page.tsx b/src/app/(marketing)/brochure/page.tsx index f553e74..8424330 100644 --- a/src/app/(marketing)/brochure/page.tsx +++ b/src/app/(marketing)/brochure/page.tsx @@ -16,7 +16,7 @@ const INSIDE = [ { icon: ShieldCheck, title: "How we protect buyers", - body: "Our verification, documentation, and payment process — so you know exactly how your money and paperwork are handled.", + body: "Our verification, documentation, and payment process, so you know exactly how your money and paperwork are handled.", }, { icon: FileText, @@ -32,7 +32,7 @@ export default function BrochurePage() { diff --git a/src/app/(platform-marketing)/platform/properties/page.tsx b/src/app/(platform-marketing)/platform/properties/page.tsx index fcf405b..c53a607 100644 --- a/src/app/(platform-marketing)/platform/properties/page.tsx +++ b/src/app/(platform-marketing)/platform/properties/page.tsx @@ -74,7 +74,7 @@ export default async function MarketplacePage({ ) : ( "top agencies" )}{" "} - across Nigeria — all on one platform. + across Nigeria, all on one platform.

    @@ -231,7 +231,7 @@ export default async function MarketplacePage({

    EstateOS agencies can opt any verified listing into the marketplace from - their admin dashboard — zero extra cost. + their admin dashboard, zero extra cost.

    diff --git a/src/app/(portal)/portal/contracts/page.tsx b/src/app/(portal)/portal/contracts/page.tsx index 6511bd3..c38ac00 100644 --- a/src/app/(portal)/portal/contracts/page.tsx +++ b/src/app/(portal)/portal/contracts/page.tsx @@ -72,7 +72,7 @@ function ContractCard({ contract }: { contract: BuyerContractRow }) { {contract.document.fileName}

    - Sent {contract.sentAt ? formatDate(contract.sentAt, "PP") : "—"} + Sent {contract.sentAt ? formatDate(contract.sentAt, "PP") : "-"}

    {entry.type} {entry.amount > 0 ? `+${entry.amount}` : entry.amount} {entry.balanceAfter} - {entry.reference ?? "—"} + {entry.reference ?? "-"} {formatDate(entry.createdAt, "PPP p")} ))} diff --git a/src/app/api/admin/inquiries/[inquiryId]/draft-reply/route.ts b/src/app/api/admin/inquiries/[inquiryId]/draft-reply/route.ts index fe4ddba..28bc665 100644 --- a/src/app/api/admin/inquiries/[inquiryId]/draft-reply/route.ts +++ b/src/app/api/admin/inquiries/[inquiryId]/draft-reply/route.ts @@ -64,7 +64,7 @@ export async function POST( const propertyContext = inquiry.property ? `Property enquired about: "${inquiry.property.title}"${inquiry.property.shortDescription ? `\nProperty summary: ${inquiry.property.shortDescription}` : ""}` - : "General inquiry — no specific property selected."; + : "General inquiry, no specific property selected."; const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel({ @@ -76,10 +76,10 @@ Guidelines: - Acknowledge their specific inquiry and the property if applicable - Be helpful and express genuine interest in assisting them - Propose a clear next step (e.g. schedule a viewing or call) -- Keep it concise — 150 to 200 words +- Keep it concise, 150 to 200 words - Sign off as "The ${companyName} Team" -Output only the email body text starting with "Dear [Name]," — no subject line, no extra commentary.`, +Output only the email body text starting with "Dear [Name],", no subject line, no extra commentary.`, }); const prompt = `Buyer name: ${inquiry.fullName} diff --git a/src/app/app/access/page.tsx b/src/app/app/access/page.tsx index b36a6a8..7a51cce 100644 --- a/src/app/app/access/page.tsx +++ b/src/app/app/access/page.tsx @@ -41,7 +41,7 @@ function resolveCopy(status: string | undefined, reason: string | undefined): Ac // The default bullets describe a SUSPENDED company, which is wrong here: // nothing is blocked for this user, they just aren't a platform owner. points: [ - "Your company workspace is unaffected — admin, portal, and payment actions all still work.", + "Your company workspace is unaffected, admin, portal, and payment actions all still work.", "The platform owner dashboard is reserved for EstateOS staff.", "If you reached this by mistake, continue in your own admin dashboard.", ], diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1668009..cec9881 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -72,7 +72,7 @@ export default function RootLayout({ {/* Pre-hydration theme bootstrap. The text lives in @/lib/security/csp - because the enforced CSP authorizes it by SHA-256 hash — keeping the + because the enforced CSP authorizes it by SHA-256 hash, keeping the script and its hash in one module (verified by csp.test.ts) means editing one without the other fails tests instead of breaking prod. */}