Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions e2e/add-person-role.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test("Add person submits the role that was selected", async ({ page }) => {
const url = page.url();
test.skip(
!url.includes("/admin") || url.includes("/sign-in") || url.includes("/auth/"),
"Dev bypass unavailable (production or flag off) — skipping.",
"Dev bypass unavailable (production or flag off), skipping.",
);

await page.goto("/admin/users");
Expand All @@ -37,7 +37,7 @@ test("Add person submits the role that was selected", async ({ page }) => {

// Any POST to this route carries the form. A hydrated page sends a server
// action (Next-Action header, multipart body); before hydration the same
// form posts natively (url-encoded), so both shapes are accepted — the point
// form posts natively (url-encoded), so both shapes are accepted, the point
// is what `role` the form submitted, not which transport carried it.
let submittedBody: string | null = null;
await page.route("**/admin/users**", async (route) => {
Expand Down
8 changes: 4 additions & 4 deletions e2e/authenticated.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function assertRouteHealthy(page: Page, route: string) {

expect(
response?.status() ?? 0,
`${route} returned a server error — this is the signature of a missing migration or a broken query.`,
`${route} returned a server error, this is the signature of a missing migration or a broken query.`,
).toBeLessThan(500);

const body = (await page.locator("body").innerText()).toLowerCase();
Expand All @@ -113,7 +113,7 @@ async function assertRouteHealthy(page: Page, route: string) {
test.describe("operator surfaces", () => {
test.beforeEach(async ({ page }) => {
const signedIn = await signInAs(page, "admin");
test.skip(!signedIn, "Dev bypass unavailable (production or flag off) — skipping.");
test.skip(!signedIn, "Dev bypass unavailable (production or flag off), skipping.");
});

for (const route of ADMIN_ROUTES) {
Expand All @@ -132,7 +132,7 @@ test.describe("operator surfaces", () => {
// environment it means migrations are pending and must be applied.
expect(
body,
"Site content editor is in its degraded state — the database is missing migrations. Run `npm run db:migrate:deploy`.",
"Site content editor is in its degraded state, the database is missing migrations. Run `npm run db:migrate:deploy`.",
).not.toContain("Site content unavailable");

await expect(page.getByText("Search & SEO")).toBeVisible();
Expand All @@ -142,7 +142,7 @@ test.describe("operator surfaces", () => {
test.describe("buyer portal", () => {
test.beforeEach(async ({ page }) => {
const signedIn = await signInAs(page, "buyer");
test.skip(!signedIn, "Dev bypass unavailable (production or flag off) — skipping.");
test.skip(!signedIn, "Dev bypass unavailable (production or flag off), skipping.");
});

for (const route of PORTAL_ROUTES) {
Expand Down
4 changes: 2 additions & 2 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ for (const route of PUBLIC_ROUTES) {
// A 5xx is the exact signature of the production incident.
expect(
response?.status() ?? 0,
`${route} returned a server error — check the server logs for the underlying exception.`,
`${route} returned a server error, check the server logs for the underlying exception.`,
).toBeLessThan(500);

const body = (await page.locator("body").innerText()).toLowerCase();
Expand Down Expand Up @@ -145,7 +145,7 @@ test("footer navigation links stack instead of running together", async ({ page
expect(result.linkCount, "expected grouped footer navigation links").toBeGreaterThan(2);
expect(
result.collisions,
"footer links in the same column share a line — inline-level boxes in a non-flex container again",
"footer links in the same column share a line, inline-level boxes in a non-flex container again",
).toEqual([]);

// The 44px tap target applies on phones only: above `sm` the links
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-migrations.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Fails (exit 1) if the target database has migrations that have not been applied,
// or has drifted from the Prisma schema. Run this against a database BEFORE serving
// application code that depends on the latest schema — it is the guard that would
// application code that depends on the latest schema, it is the guard that would
// have caught the production "column does not exist" error.
//
// Usage (reads DATABASE_URL/DIRECT_URL from the environment):
Expand Down
4 changes: 2 additions & 2 deletions scripts/generate-migration-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* live database against a list of expected migrations. That list used to be
* hand-maintained and silently went stale: it stopped at 0035 while the repo
* grew to 0049, so readyz reported `migrations: ok` while production was
* missing 14 migrations — including 0042, whose absent columns produced a
* missing 14 migrations, including 0042, whose absent columns produced a
* P2022 "column does not exist" 500 in production.
*
* A generated manifest cannot go stale. `--check` fails CI when someone adds
Expand Down Expand Up @@ -47,7 +47,7 @@ function readMigrationNames() {
function renderManifest(names) {
const entries = names.map((name) => ` ${JSON.stringify(name)},`).join("\n");

return `// GENERATED FILE — DO NOT EDIT BY HAND.
return `// GENERATED FILE, DO NOT EDIT BY HAND.
// Regenerate with: node scripts/generate-migration-manifest.mjs
//
// Every migration directory in prisma/migrations, used by the readiness
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/announcements/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function AdminAnnouncementsPage() {
<DashboardShell
area="admin"
title="Announcements"
subtitle="Broadcast notices to your buyers — shown at the top of their portal until dismissed or expired."
subtitle="Broadcast notices to your buyers, shown at the top of their portal until dismissed or expired."
>
<div className="grid gap-6 lg:grid-cols-[minmax(0,380px)_minmax(0,1fr)]">
<AnnouncementComposer />
Expand Down
16 changes: 8 additions & 8 deletions src/app/(admin)/admin/benchmarks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function BenchmarkBar({
if (band.sampleSize < 3) {
return (
<p className="text-xs text-[var(--ink-400)]">
Platform data unavailable — not enough agencies in distribution yet.
Platform data unavailable, not enough agencies in distribution yet.
</p>
);
}
Expand Down Expand Up @@ -181,7 +181,7 @@ function MetricComparisonPanel({
Percentile
</div>
<div className="mt-1 text-xl font-semibold text-[var(--ink-500)]">
{band.sampleSize >= 3 ? `${band.tenantPercentileRank}th` : "—"}
{band.sampleSize >= 3 ? `${band.tenantPercentileRank}th` : "-"}
</div>
</div>
</div>
Expand Down Expand Up @@ -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`
Expand All @@ -373,7 +373,7 @@ export default async function AdminBenchmarksPage() {
<DashboardShell
area="admin"
title="Performance benchmarks"
subtitle={`How your agency compares to anonymised platform averages — ${BENCHMARK_WINDOW_DAYS}-day rolling window.`}
subtitle={`How your agency compares to anonymised platform averages, ${BENCHMARK_WINDOW_DAYS}-day rolling window.`}
>
{/* Anonymisation notice */}
<AdminStateBanner
Expand Down Expand Up @@ -436,23 +436,23 @@ export default async function AdminBenchmarksPage() {
<li className="flex gap-2">
<span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-[var(--ink-950)]" />
<span>
<strong className="text-[var(--ink-900)]">Conversion rate</strong> — above
<strong className="text-[var(--ink-900)]">Conversion rate</strong>, above
the median means your team qualifies and closes buyer interest more
effectively than most.
</span>
</li>
<li className="flex gap-2">
<span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-[var(--ink-950)]" />
<span>
<strong className="text-[var(--ink-900)]">Deal velocity</strong> — lower
<strong className="text-[var(--ink-900)]">Deal velocity</strong>, lower
is better. Faster closures reduce buyer drop-off risk and improve cash
collection.
</span>
</li>
<li className="flex gap-2">
<span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-[var(--ink-950)]" />
<span>
<strong className="text-[var(--ink-900)]">Default rate</strong> — lower
<strong className="text-[var(--ink-900)]">Default rate</strong>, lower
is better. A high default rate signals collection problems or over-stretched
buyers.
</span>
Expand All @@ -461,7 +461,7 @@ export default async function AdminBenchmarksPage() {
<span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-[var(--brand-300)]" />
<span>
The shaded bar is the <strong className="text-[var(--ink-900)]">
interquartile range</strong> — the middle 50% of all agencies. Being
interquartile range</strong>, the middle 50% of all agencies. Being
inside the band is normal; above p75 is strong performance.
</span>
</li>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/contracts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function UploadContractForm({ transactions }: { transactions: TransactionWithout
const buyer = [tx.user.firstName, tx.user.lastName].filter(Boolean).join(" ") || "Buyer";
return (
<option key={tx.id} value={tx.id}>
{ref} — {buyer} — {tx.property.title}
{ref}, {buyer}, {tx.property.title}
</option>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default async function AdminFeasibilityReportPage({
}
aiNarrative = chunks.join("");
} catch {
// non-critical — omit from report if generation fails
// non-critical, omit from report if generation fails
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/invoices/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function AdminInvoicesPage() {
<DashboardShell
area="admin"
title="Invoices"
subtitle="Issue property invoices buyers can download or print — branded with your company details."
subtitle="Issue property invoices buyers can download or print, branded with your company details."
>
<div className="space-y-6">
<InvoiceCreateForm />
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/leads/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function sendDraftReplyAction(
html: `<p style="white-space:pre-wrap">${draftText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</p>`,
});

// Sending a reply is a follow-up action — update lastFollowedUpAt on any
// Sending a reply is a follow-up action, update lastFollowedUpAt on any
// active transaction belonging to this inquiry's buyer.
if (inquiryId && featureFlags.hasDatabase) {
const inquiry = await prisma.inquiry.findFirst({
Expand Down
6 changes: 3 additions & 3 deletions src/app/(admin)/admin/listings/marketplace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default async function AdminMarketplacePage() {
<DashboardShell
area="admin"
title="Marketplace listings"
subtitle="Control which of your verified properties appear in the cross-company EstateOS marketplace. Only publicly visible, verified or stale properties are eligible — toggle opt-in per property."
subtitle="Control which of your verified properties appear in the cross-company EstateOS marketplace. Only publicly visible, verified or stale properties are eligible, toggle opt-in per property."
>
<AdminToolbar>
<div className="flex flex-wrap items-center gap-3">
Expand All @@ -40,7 +40,7 @@ export default async function AdminMarketplacePage() {

<AdminPanel
title="Property opt-in"
description="Enable marketplace listing per property. Only eligible properties (publicly visible with verified or stale verification) will actually appear — others are accepted but held until they pass the gate."
description="Enable marketplace listing per property. Only eligible properties (publicly visible with verified or stale verification) will actually appear, others are accepted but held until they pass the gate."
className="px-0 py-0"
>
{properties.length > 0 ? (
Expand Down Expand Up @@ -76,7 +76,7 @@ export default async function AdminMarketplacePage() {
{property.title}
</div>
<div className="text-xs text-[var(--ink-500)]">
{property.locationSummary ?? "—"}
{property.locationSummary ?? "-"}
</div>
</div>
</td>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/marketer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default async function AdminMarketerPage() {
<DashboardShell
area="admin"
title="My Dashboard"
subtitle="Your assigned leads and viewings — follow up fast and keep deals moving."
subtitle="Your assigned leads and viewings, follow up fast and keep deals moving."
>
<MarketerDashboardView data={data} />
</DashboardShell>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/overview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function AdminOverviewPage() {
<DashboardShell
area="admin"
title="Executive Overview"
subtitle="Company-wide health across sales, finance, and the front desk — at a glance."
subtitle="Company-wide health across sales, finance, and the front desk, at a glance."
>
<ExecutiveOverviewBoard overview={overview} />
<OwnerActivityFeed items={activity} />
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
<TenantReadinessChecklist
title="Tenant readiness"
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/settings/site-content/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default async function AdminSiteContentPage() {
<DashboardShell
area="admin"
title="Site content"
subtitle="Edit your public site copy — hero, calls to action, footer, and about — with draft preview and manual publish. Blank fields use a smart default."
subtitle="Edit your public site copy, hero, calls to action, footer, and about, with draft preview and manual publish. Blank fields use a smart default."
>
<SiteContentManagement state={state} fallback={fallback} />
</DashboardShell>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/team/commission-rules/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function AdminUsersPage() {
<DashboardShell
area="admin"
title="Users"
subtitle="Manage staff accounts for your company — view profiles, suspend access, or remove accounts."
subtitle="Manage staff accounts for your company, view profiles, suspend access, or remove accounts."
>
<UsersManagement users={users} hasClerkPassword={featureFlags.hasClerkPassword} />
</DashboardShell>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(marketing)/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/app/(marketing)/brochure/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,7 +32,7 @@ export default function BrochurePage() {
<SectionHeading
eyebrow="Brochure"
title="Get the full company brochure."
description="A clear, printable overview of our properties, process, and the safeguards that protect every buyer — ideal to share with family or advisors before you decide."
description="A clear, printable overview of our properties, process, and the safeguards that protect every buyer, ideal to share with family or advisors before you decide."
/>
</Reveal>

Expand Down
70 changes: 70 additions & 0 deletions src/app/(marketing)/how-it-works/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container className="space-y-12 py-16">
<Reveal>
<div className="max-w-2xl space-y-4">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--ink-500)]">
{presentation.companyName}
</div>
<h1 className="font-serif text-4xl leading-tight text-[var(--ink-950)] sm:text-5xl">
{content.journey.heading}
</h1>
<p className="text-base leading-8 text-[var(--ink-600)]">{content.hero.subhead}</p>
</div>
</Reveal>

<Reveal>
<ol className="grid gap-x-10 gap-y-12 md:grid-cols-3">
{content.journey.steps.map((step, index) => (
<li key={step.title} className="border-t-2 border-[var(--brand-700)]/25 pt-5">
<div className="numeric text-sm font-semibold tracking-[0.18em] text-[var(--brand-700)]">
0{index + 1}
</div>
<h2 className="mt-3 font-serif text-2xl leading-snug text-[var(--ink-950)]">
{step.title}
</h2>
<p className="mt-3 text-sm leading-7 text-[var(--ink-600)]">{step.description}</p>
</li>
))}
</ol>
</Reveal>

<Reveal>
<div className="flex flex-wrap gap-3 border-t border-[var(--line)] pt-10">
<Link href="/properties">
<Button size="lg">{content.hero.primaryCta.label}</Button>
</Link>
<Link href="/contact">
<Button variant="outline">Talk to the team</Button>
</Link>
</div>
</Reveal>
</Container>
);
}
Loading
Loading