No announcements yet.
- Posted broadcasts will appear here — publish or unpublish anytime.
+ Posted broadcasts will appear here, publish or unpublish anytime.
);
diff --git a/src/components/admin/contracts-tables.tsx b/src/components/admin/contracts-tables.tsx
index 2e468f4..032df94 100644
--- a/src/components/admin/contracts-tables.tsx
+++ b/src/components/admin/contracts-tables.tsx
@@ -10,7 +10,7 @@ import { DataTable, type ColumnDef } from "@/components/ui/data-table";
* sortable columns, search, pagination. The server page flattens service rows
* into these serializable shapes and passes the server actions down as props
* (Next serializes action references), so the send/regenerate forms keep
- * working exactly as before — just inside table cells.
+ * working exactly as before, just inside table cells.
*/
type ServerAction = (formData: FormData) => void | Promise;
diff --git a/src/components/admin/executive-overview-board.tsx b/src/components/admin/executive-overview-board.tsx
index 47b454c..e973ac7 100644
--- a/src/components/admin/executive-overview-board.tsx
+++ b/src/components/admin/executive-overview-board.tsx
@@ -26,7 +26,7 @@ export function ExecutiveOverviewBoard({ overview }: { overview: ExecutiveOvervi
Company pulse
- Live across sales, finance, and the front desk — refreshes automatically.
+ Live across sales, finance, and the front desk, refreshes automatically.
diff --git a/src/components/admin/feasibility-ai-narrative.tsx b/src/components/admin/feasibility-ai-narrative.tsx
index 792d481..ea5a8b1 100644
--- a/src/components/admin/feasibility-ai-narrative.tsx
+++ b/src/components/admin/feasibility-ai-narrative.tsx
@@ -38,7 +38,7 @@ export function FeasibilityAiNarrative({ calculationId }: Props) {
setNarrative((prev) => prev + decoder.decode(value, { stream: true }));
}
} catch {
- setError("Network error — please try again.");
+ setError("Network error, please try again.");
} finally {
setLoading(false);
}
diff --git a/src/components/admin/feasibility-project-hub.tsx b/src/components/admin/feasibility-project-hub.tsx
index cc22190..bd12d2a 100644
--- a/src/components/admin/feasibility-project-hub.tsx
+++ b/src/components/admin/feasibility-project-hub.tsx
@@ -70,7 +70,7 @@ function DecisionBadge({
});
onStatusChange(calculationId, next);
} catch {
- // silent — stale UI at worst
+ // silent, stale UI at worst
}
}
@@ -167,7 +167,7 @@ function EmptyState({ onNew }: { onNew: () => void }) {
No feasibility projects yet
- The feasibility calculator helps land developers and real estate companies underwrite new projects — sizing land efficiency, modelling delivery cost, setting pricing targets, and stress-testing returns across scenarios before committing capital.
+ The feasibility calculator helps land developers and real estate companies underwrite new projects, sizing land efficiency, modelling delivery cost, setting pricing targets, and stress-testing returns across scenarios before committing capital.
Use it before acquiring a site, pitching to investors, or setting a pricing strategy for a new estate.
diff --git a/src/components/admin/inspection-management.tsx b/src/components/admin/inspection-management.tsx
index 476297b..147921a 100644
--- a/src/components/admin/inspection-management.tsx
+++ b/src/components/admin/inspection-management.tsx
@@ -386,7 +386,7 @@ export function InspectionManagement({
{booking.phone ? (
) : null}
resetBooking(booking)} disabled={!isDirty(booking) || pendingId === booking.id}>
diff --git a/src/components/admin/invoice-list.tsx b/src/components/admin/invoice-list.tsx
index 14ecb30..7f69498 100644
--- a/src/components/admin/invoice-list.tsx
+++ b/src/components/admin/invoice-list.tsx
@@ -57,7 +57,7 @@ export function InvoiceList({
accessorKey: "propertyTitle",
header: "Property",
cell: ({ row }) => (
- {row.original.propertyTitle ?? "—"}
+ {row.original.propertyTitle ?? "-"}
),
},
{
diff --git a/src/components/admin/marketer-ranking-table.tsx b/src/components/admin/marketer-ranking-table.tsx
index f3fadd5..192cf95 100644
--- a/src/components/admin/marketer-ranking-table.tsx
+++ b/src/components/admin/marketer-ranking-table.tsx
@@ -31,7 +31,7 @@ export type MarketerRankingRow = {
/**
* Ranked marketer list for /admin/marketers, on the shared DataTable. Raw
* numbers stay numbers (revenue, score, deals…) so column sorting is numeric
- * — the CEO can re-rank by monthly revenue or commission with one click —
+ *, the CEO can re-rank by monthly revenue or commission with one click,
* while cells format for display. Default order is the service's rank.
*/
export function MarketerRankingTable({ rows }: { rows: MarketerRankingRow[] }) {
diff --git a/src/components/admin/payment-account-setup.tsx b/src/components/admin/payment-account-setup.tsx
index 3719d8e..0b86a9d 100644
--- a/src/components/admin/payment-account-setup.tsx
+++ b/src/components/admin/payment-account-setup.tsx
@@ -236,7 +236,7 @@ export function PaymentAccountSetup() {
{ label: "Business name", value: account.displayName },
{
label: "Bank",
- value: String(meta?.["bankName"] ?? "—"),
+ value: String(meta?.["bankName"] ?? "-"),
},
{
label: "Account number",
@@ -248,7 +248,7 @@ export function PaymentAccountSetup() {
},
{
label: "Subaccount code",
- value: account.subaccountCode ?? "—",
+ value: account.subaccountCode ?? "-",
mono: true,
},
{
diff --git a/src/components/admin/payments-register-table.tsx b/src/components/admin/payments-register-table.tsx
index d5d9e9f..0615008 100644
--- a/src/components/admin/payments-register-table.tsx
+++ b/src/components/admin/payments-register-table.tsx
@@ -18,7 +18,7 @@ export type PaymentRegisterRow = {
};
/**
- * Deal payment register for /admin/payments — the FINANCE team's daily
+ * Deal payment register for /admin/payments, the FINANCE team's daily
* driver, migrated from a static bespoke table to the shared DataTable:
* sortable columns, search across references/buyers/marketers, pagination
* for long registers, and the receipt download link preserved as a cell.
diff --git a/src/components/admin/provision-result-modal.tsx b/src/components/admin/provision-result-modal.tsx
index d65bce8..e5ec197 100644
--- a/src/components/admin/provision-result-modal.tsx
+++ b/src/components/admin/provision-result-modal.tsx
@@ -11,7 +11,7 @@ import type { ProvisionUserResult } from "@/modules/provisioning/provision-user"
* One-time credential display after operator-provisioned account creation.
* For invite delivery: shows confirmation with the recipient email.
* For password delivery: shows the temporary password with copy-to-clipboard
- * and a prominent "shown once" warning — the password is never stored.
+ * and a prominent "shown once" warning, the password is never stored.
*/
export function ProvisionResultDialog({
result,
@@ -71,7 +71,7 @@ export function ProvisionResultDialog({
- Shown once — copy it now
+ Shown once, copy it now
This password will not be displayed again. Hand it directly to the person and
diff --git a/src/components/admin/schedule-agenda.tsx b/src/components/admin/schedule-agenda.tsx
index cf5c72b..4c8c0ed 100644
--- a/src/components/admin/schedule-agenda.tsx
+++ b/src/components/admin/schedule-agenda.tsx
@@ -40,7 +40,7 @@ function timeLabel(d: Date): string {
/**
* A front-desk agenda: upcoming property viewings from today onward, grouped by
- * day and ordered by time. Read-only overview — use the Bookings page to
+ * day and ordered by time. Read-only overview, use the Bookings page to
* reschedule or change status.
*/
export function ScheduleAgenda({ items }: { items: InspectionManagementItem[] }) {
diff --git a/src/components/admin/site-content-management.tsx b/src/components/admin/site-content-management.tsx
index f812750..08b2f93 100644
--- a/src/components/admin/site-content-management.tsx
+++ b/src/components/admin/site-content-management.tsx
@@ -11,9 +11,9 @@ import type { TenantSiteContent } from "@/modules/cms/site-content";
import type { TenantSiteContentState } from "@/modules/cms/site-content-service";
/**
- * Full-site CMS editor: every piece of public marketing copy — SEO, hero
+ * Full-site CMS editor: every piece of public marketing copy, SEO, hero
* (+ stat cards + side panel), the buyer journey, section headings, about,
- * contact extras, social links, footer — with draft → publish control.
+ * contact extras, social links, footer, with draft → publish control.
* Blank fields fall back to smart company-derived defaults (shown as
* placeholders), so tenants only override what they want to change.
*/
@@ -292,7 +292,7 @@ export function SiteContentManagement({
router.refresh();
}
- // The content columns could not be read — almost always a database that is
+ // The content columns could not be read, almost always a database that is
// behind the deployed code. Explain it instead of showing a broken editor.
if (state.unavailable) {
return (
@@ -305,7 +305,7 @@ export function SiteContentManagement({
Your site editor is temporarily unavailable.
- Your public website is still online and is showing its default copy — visitors are
+ Your public website is still online and is showing its default copy, visitors are
unaffected. The editor needs a pending database update before your saved content can
be loaded.
@@ -333,7 +333,7 @@ export function SiteContentManagement({
Every section of your public website is editable here. Changes stay in draft until
- you publish. Any field left blank uses a smart default derived from your company —
+ you publish. Any field left blank uses a smart default derived from your company,
shown as the placeholder.
@@ -417,7 +417,7 @@ export function SiteContentManagement({
@@ -501,32 +501,32 @@ export function SiteContentManagement({
>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -554,7 +554,7 @@ export function SiteContentManagement({
diff --git a/src/components/dashboard/live-refresh.tsx b/src/components/dashboard/live-refresh.tsx
index b2145c2..10989f4 100644
--- a/src/components/dashboard/live-refresh.tsx
+++ b/src/components/dashboard/live-refresh.tsx
@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
* An honest "Live" indicator that also DOES the refreshing. It periodically calls
* router.refresh() so server-rendered data (recent payments, balances, logs)
* updates without a manual reload. Polling pauses while the tab is hidden to
- * avoid wasted work, and resumes — with an immediate refresh — when the tab is
+ * avoid wasted work, and resumes, with an immediate refresh, when the tab is
* focused again. The dot pulses continuously; during an actual refresh the chip
* shows a brief "Updating…" state so the label reflects real activity.
*/
diff --git a/src/components/dashboard/quick-create-modal.tsx b/src/components/dashboard/quick-create-modal.tsx
index ff193f9..91d60a5 100644
--- a/src/components/dashboard/quick-create-modal.tsx
+++ b/src/components/dashboard/quick-create-modal.tsx
@@ -3,7 +3,7 @@
import { Dialog } from "@/components/ui/dialog";
/**
- * Quick-create modal — now a thin wrapper over the shared Dialog primitive
+ * Quick-create modal, now a thin wrapper over the shared Dialog primitive
* (portal to document.body, Escape/backdrop close, body scroll lock, focus
* trap + focus restore). Kept as a named export so the quick-action call
* sites don't change.
diff --git a/src/components/marketing/inquiry-form.tsx b/src/components/marketing/inquiry-form.tsx
index 37587e7..711523b 100644
--- a/src/components/marketing/inquiry-form.tsx
+++ b/src/components/marketing/inquiry-form.tsx
@@ -54,7 +54,7 @@ export function InquiryForm({ propertyId }: { propertyId?: string }) {
Inquiry received
- Thanks — our team will reach out shortly. Need to add something? Send another below.
+ Thanks, our team will reach out shortly. Need to add something? Send another below.
setSubmitted(false)}>
diff --git a/src/components/marketing/marketing-footer.tsx b/src/components/marketing/marketing-footer.tsx
index 370d2a3..d9f0d50 100644
--- a/src/components/marketing/marketing-footer.tsx
+++ b/src/components/marketing/marketing-footer.tsx
@@ -5,7 +5,7 @@ import { Container } from "@/components/shared/container";
import { Logo } from "@/components/shared/logo";
import type { TenantSiteContent } from "@/modules/cms/site-content";
-// Facebook, Instagram, LinkedIn were removed from lucide-react — minimal inline SVGs
+// Facebook, Instagram, LinkedIn were removed from lucide-react, minimal inline SVGs
function FacebookIcon({ className }: { className?: string }) {
return (
@@ -35,7 +35,7 @@ function LinkedinIcon({ className }: { className?: string }) {
type SocialLinks = TenantSiteContent["social"];
/**
- * Tenant site footer. Tagline and social links come from the tenant CMS —
+ * Tenant site footer. Tagline and social links come from the tenant CMS,
* icons render only for the profiles the company actually filled in.
* WhatsApp is stored as a phone number and linked via wa.me.
*/
@@ -110,6 +110,7 @@ export function MarketingFooter({
About
+ How it works
Team
{showCareers ? (
Careers
diff --git a/src/components/marketing/marketing-mobile-nav.tsx b/src/components/marketing/marketing-mobile-nav.tsx
index 75d2aef..3522b20 100644
--- a/src/components/marketing/marketing-mobile-nav.tsx
+++ b/src/components/marketing/marketing-mobile-nav.tsx
@@ -8,8 +8,8 @@ type MarketingNavLink = { href: string; label: string };
/**
* Mobile/tablet (below `lg`) navigation for the tenant public site header.
*
- * Implemented as an absolute-positioned dropdown anchored to the trigger —
- * matching the platform marketing header's mobile menu — rather than a fixed
+ * Implemented as an absolute-positioned dropdown anchored to the trigger,
+ * matching the platform marketing header's mobile menu, rather than a fixed
* full-screen drawer. This is deliberate: `MarketingHeader` uses
* `backdrop-blur-xl`, and an ancestor with `backdrop-filter` becomes the
* containing block for `position: fixed` descendants, which would clip a fixed
diff --git a/src/components/marketing/property-card.tsx b/src/components/marketing/property-card.tsx
index 9f4c859..3b705f1 100644
--- a/src/components/marketing/property-card.tsx
+++ b/src/components/marketing/property-card.tsx
@@ -15,7 +15,7 @@ export function PropertyCard({ property }: { property: PropertySummary }) {
return null;
}
- return `${label} — ${option.price ? formatCurrency(option.price, option.currency ?? property.currency) : "Price on request"}`;
+ return `${label}: ${option.price ? formatCurrency(option.price, option.currency ?? property.currency) : "Price on request"}`;
})
.filter(Boolean);
diff --git a/src/components/marketing/tenant-homepage.tsx b/src/components/marketing/tenant-homepage.tsx
index 14a6bc4..e7b9d32 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,
@@ -107,7 +113,7 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) {
{/* 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
+ carries the portal link, three CTAs here competed with the
search, which is the actual front door. */}
@@ -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}
@@ -186,7 +197,7 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) {
- {/* Trust strip — only real numbers. A brand-new tenant otherwise
+ {/* 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
@@ -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}
+
))}
);
@@ -261,7 +275,7 @@ export async function TenantHomepage({ tenant }: { tenant: TenantContext }) {
- {/* How it works — moved out of the hero's 0.8fr sidebar, where three
+ {/* 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. */}
@@ -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}
-
+
))}
diff --git a/src/components/messaging/conversation.tsx b/src/components/messaging/conversation.tsx
index ce9c5ca..e9e92fc 100644
--- a/src/components/messaging/conversation.tsx
+++ b/src/components/messaging/conversation.tsx
@@ -23,7 +23,7 @@ export function Conversation({
diff --git a/src/components/platform/marketplace-property-card.tsx b/src/components/platform/marketplace-property-card.tsx
index 54cd032..2c5aa6e 100644
--- a/src/components/platform/marketplace-property-card.tsx
+++ b/src/components/platform/marketplace-property-card.tsx
@@ -20,7 +20,7 @@ export function MarketplacePropertyCard({
return null;
}
- return `${label} — ${option.price ? formatCurrency(option.price, option.currency ?? property.currency) : "Price on request"}`;
+ return `${label}: ${option.price ? formatCurrency(option.price, option.currency ?? property.currency) : "Price on request"}`;
})
.filter(Boolean);
diff --git a/src/components/platform/platform-hero-preview.tsx b/src/components/platform/platform-hero-preview.tsx
index 6d7fcf5..8be75c5 100644
--- a/src/components/platform/platform-hero-preview.tsx
+++ b/src/components/platform/platform-hero-preview.tsx
@@ -5,7 +5,7 @@ import { CheckCircle2, ReceiptText } from "lucide-react";
*
* A light "operator view" panel that floats on the dark hero gradient so the
* landing page *shows* the product instead of only describing it. All data here
- * is illustrative sample UI (not real tenant data) — it exists purely to convey
+ * is illustrative sample UI (not real tenant data), it exists purely to convey
* what the deal board and payment flow look like.
*
* Purely presentational and server-rendered (no client JS). Entrance motion is
diff --git a/src/components/platform/platform-home.tsx b/src/components/platform/platform-home.tsx
index 25456e2..b77ae1f 100644
--- a/src/components/platform/platform-home.tsx
+++ b/src/components/platform/platform-home.tsx
@@ -57,7 +57,7 @@ export async function PlatformHome() {
EstateOS moves buyers from inquiry to inspection, reservation, payment, and
- receipt — and flags every overdue balance — so your team stops losing money to
+ receipt, and flags every overdue balance, so your team stops losing money to
spreadsheets, WhatsApp threads, and manual transfer checks.
@@ -174,7 +174,7 @@ export async function PlatformHome() {
@@ -203,7 +203,7 @@ export async function PlatformHome() {
@@ -221,7 +221,7 @@ export async function PlatformHome() {
Start a pilot
- Launch EstateOS for your sales team — on a platform that already understands deals,
+ Launch EstateOS for your sales team, on a platform that already understands deals,
payments, and collections.
diff --git a/src/components/platform/platform-pricing.tsx b/src/components/platform/platform-pricing.tsx
index ad9eb3f..04c9fc7 100644
--- a/src/components/platform/platform-pricing.tsx
+++ b/src/components/platform/platform-pricing.tsx
@@ -23,7 +23,7 @@ function intervalSuffix(interval: string) {
* Shared pricing block for the platform marketing site (landing + /platform/pricing).
*
* Adds the things a buyer actually needs to decide: a concrete what's-included
- * list, the annual saving made explicit (derived from the real plan amounts —
+ * list, the annual saving made explicit (derived from the real plan amounts,
* not invented), and a CTA on every card. The annual plan is highlighted as the
* best value when a monthly counterpart exists to compare against.
*/
diff --git a/src/components/portal/announcement-banner.tsx b/src/components/portal/announcement-banner.tsx
index d854936..39dba78 100644
--- a/src/components/portal/announcement-banner.tsx
+++ b/src/components/portal/announcement-banner.tsx
@@ -20,7 +20,7 @@ function readDismissed(): string[] {
/**
* Dismissible broadcast banner shown at the top of the dashboard (buyer portal
* or operator workspace). Dismissal is persisted in localStorage so a notice a
- * user has closed stays closed across navigation and reloads — until the
+ * user has closed stays closed across navigation and reloads, until the
* operator unpublishes it or it expires (both drop it server-side).
*/
export function AnnouncementBanner({ items }: { items: AnnouncementNotice[] }) {
diff --git a/src/components/portal/dashboard-mobile-nav.tsx b/src/components/portal/dashboard-mobile-nav.tsx
index dcf97be..f778236 100644
--- a/src/components/portal/dashboard-mobile-nav.tsx
+++ b/src/components/portal/dashboard-mobile-nav.tsx
@@ -10,8 +10,8 @@ import { Avatar } from "@/components/ui/avatar";
/**
* Mobile/tablet (below `lg`) navigation for the dashboard shell.
*
- * Replaces the stacked tile grid of nav links — which previously pushed the
- * actual page content far down the screen on small viewports — with a sticky
+ * Replaces the stacked tile grid of nav links, which previously pushed the
+ * actual page content far down the screen on small viewports, with a sticky
* top bar (company identity + menu button) plus a slide-in drawer that holds
* the full navigation. The drawer renders the same grouped, collapsible nav
* (SidebarNavGroups) as the desktop sidebar, so the two surfaces never drift.
@@ -101,7 +101,7 @@ export function DashboardMobileNav({
return (
<>
- {/* Sticky top bar — this element is itself the grid item, so `sticky`
+ {/* Sticky top bar, this element is itself the grid item, so `sticky`
pins it against the tall grid container as the page scrolls. Identity
on the left, menu trigger on the right. Hidden on desktop (`lg`). */}
diff --git a/src/components/portal/dashboard-shell.tsx b/src/components/portal/dashboard-shell.tsx
index 60e5ccd..f8b054b 100644
--- a/src/components/portal/dashboard-shell.tsx
+++ b/src/components/portal/dashboard-shell.tsx
@@ -28,7 +28,7 @@ import { cn } from "@/lib/utils";
/**
* Buyer portal navigation, grouped the same way the operator sidebar is: a
* few scannable clusters instead of a flat 15-link list. Grouping here is
- * presentation-only — every buyer sees every section.
+ * presentation-only, every buyer sees every section.
*/
const portalNavGroups: SidebarNavGroup[] = [
{
@@ -115,7 +115,7 @@ export async function DashboardShell({
// getAppSession reads request cookies (dynamic) and is never cached.
// Presentation, session, and announcements are mutually independent (all
- // derive from `tenant`), so resolve all three concurrently — one round trip
+ // derive from `tenant`), so resolve all three concurrently, one round trip
// instead of three sequential ones on every authenticated page load.
const [presentation, appSession, announcements] = await Promise.all([
loadPresentation(),
@@ -138,7 +138,7 @@ export async function DashboardShell({
// message count all depend only on profileTenant/notification ids, so fetch
// the three concurrently rather than sequentially. The notification count is
// per-user and changes frequently, so it is cached only very briefly (15s)
- // and keyed by companyId + userId (tenant- and user-scoped) — enough to
+ // and keyed by companyId + userId (tenant- and user-scoped), enough to
// absorb rapid re-navigation without showing a materially stale badge.
const [portalUserRows, unreadNotificationCount, messagesUnreadCount] = await Promise.all([
area === "portal" && featureFlags.hasDatabase && profileTenant.userId && profileTenant.companyId
@@ -278,7 +278,7 @@ export async function DashboardShell({
/>
- {/* Mobile bottom tab bar — buyers only; the drawer covers the long tail. */}
+ {/* Mobile bottom tab bar, buyers only; the drawer covers the long tail. */}
{area === "portal" ? (
clearTimeout(id);
@@ -108,7 +108,7 @@ export function SidebarNavGroups({
);
};
- // A single group renders as a plain list — no header, nothing to collapse.
+ // A single group renders as a plain list, no header, nothing to collapse.
if (groups.length <= 1) {
return (
diff --git a/src/components/realtime/live-surface-sync.tsx b/src/components/realtime/live-surface-sync.tsx
index 9604dde..d96b25c 100644
--- a/src/components/realtime/live-surface-sync.tsx
+++ b/src/components/realtime/live-surface-sync.tsx
@@ -50,7 +50,7 @@ export function LiveSurfaceSync({ channel, surface = "admin" }: LiveSurfaceSyncP
// /api/realtime/version endpoint (one Redis GET server-side) and only
// call router.refresh() when the change counter actually moved. Falls
// back to the old blind refresh when the counter backplane is
- // unavailable (no Redis) or the endpoint errors — shouldRefreshRealtime
+ // unavailable (no Redis) or the endpoint errors, shouldRefreshRealtime
// still rate-limits blind refreshes to the 30s cadence.
const versionSearch = new URLSearchParams({
channel,
@@ -95,12 +95,12 @@ export function LiveSurfaceSync({ channel, surface = "admin" }: LiveSurfaceSyncP
}
if (payload.data.version !== lastVersion) {
lastVersion = payload.data.version;
- // A real change was detected — refresh promptly (short debounce
+ // A real change was detected, refresh promptly (short debounce
// floor rather than the 30s blind-poll spacing).
refresh(REALTIME_CHANGE_REFRESH_MIN_INTERVAL_MS);
}
} catch {
- // Endpoint unreachable — degrade to blind refresh (rate-limited).
+ // Endpoint unreachable, degrade to blind refresh (rate-limited).
refresh();
}
};
diff --git a/src/components/shared/command-palette.tsx b/src/components/shared/command-palette.tsx
index 173a4c4..8b38c92 100644
--- a/src/components/shared/command-palette.tsx
+++ b/src/components/shared/command-palette.tsx
@@ -110,7 +110,7 @@ export function CommandPalette({
};
}, [openPalette, close]);
- // Open side-effects only (focus, scroll lock, focus restore) — no setState.
+ // Open side-effects only (focus, scroll lock, focus restore), no setState.
useEffect(() => {
if (!open) {
return;
diff --git a/src/components/shared/data-table-card.tsx b/src/components/shared/data-table-card.tsx
index c3e73a9..ca3d27e 100644
--- a/src/components/shared/data-table-card.tsx
+++ b/src/components/shared/data-table-card.tsx
@@ -4,7 +4,7 @@ import { SimpleDataTable } from "@/components/ui/simple-data-table";
/**
* Titled table card. Same external API as before (title + string columns +
* string rows, all serializable from server components), but the internals
- * now run on the shared TanStack DataTable — so every consumer (Audit Logs,
+ * now run on the shared TanStack DataTable, so every consumer (Audit Logs,
* Documents, Transactions) gains column sorting, an optional search box, and
* pagination for long lists instead of one unbounded static table.
*/
diff --git a/src/components/shared/page-transition.tsx b/src/components/shared/page-transition.tsx
index 9921bfd..7f31115 100644
--- a/src/components/shared/page-transition.tsx
+++ b/src/components/shared/page-transition.tsx
@@ -8,7 +8,7 @@ import type { ReactNode } from "react";
* Wraps page content in a subtle, GPU-friendly enter transition.
*
* Designed to live inside `app/template.tsx`, which Next.js re-mounts on every
- * navigation — so the enter animation replays on each route change without any
+ * navigation, so the enter animation replays on each route change without any
* pathname bookkeeping for the animation itself.
*
* Dashboard surfaces (portal / admin / superadmin) are intentionally skipped:
diff --git a/src/components/shared/reveal.tsx b/src/components/shared/reveal.tsx
index 072a04b..1c79b47 100644
--- a/src/components/shared/reveal.tsx
+++ b/src/components/shared/reveal.tsx
@@ -13,7 +13,7 @@ import { PREMIUM_EASE } from "@/components/shared/page-transition";
* hijacking). Honors `prefers-reduced-motion` by collapsing to a plain fade and
* animates only transform/opacity, so there is no layout shift.
*
- * Intended for marketing pages only — dashboards stay calm.
+ * Intended for marketing pages only, dashboards stay calm.
*/
export function Reveal({
children,
diff --git a/src/components/shared/tenant-readiness-checklist.tsx b/src/components/shared/tenant-readiness-checklist.tsx
index bff506c..0ce232f 100644
--- a/src/components/shared/tenant-readiness-checklist.tsx
+++ b/src/components/shared/tenant-readiness-checklist.tsx
@@ -18,8 +18,8 @@ const statusClasses = {
/**
* Readiness items owned by EstateOS itself (platform Paystack keys, R2 storage,
- * superadmin-only checks). A tenant admin can neither see nor action these — and
- * their action links point at /superadmin/*, which tenants cannot open — so they
+ * superadmin-only checks). A tenant admin can neither see nor action these, and
+ * their action links point at /superadmin/*, which tenants cannot open, so they
* are hidden from the tenant audience and shown only to superadmins.
*/
const PLATFORM_OWNERS = new Set(["Platform", "Superadmin"]);
diff --git a/src/components/shared/theme-toggle.tsx b/src/components/shared/theme-toggle.tsx
index b19c15e..2964974 100644
--- a/src/components/shared/theme-toggle.tsx
+++ b/src/components/shared/theme-toggle.tsx
@@ -30,7 +30,7 @@ export function setDarkTheme(dark: boolean) {
try {
localStorage.setItem(STORAGE_KEY, dark ? "dark" : "light");
} catch {
- // localStorage unavailable (private mode / blocked) — theme still applies for the session.
+ // localStorage unavailable (private mode / blocked), theme still applies for the session.
}
}
@@ -42,7 +42,7 @@ export function toggleTheme() {
* Light/dark theme toggle for the operator and buyer dashboards. Flips
* `theme-dark` on (the visual change is gated behind `.app-dark-scope`,
* so only the dashboards respond) and persists the choice. Until the user
- * makes an explicit choice, the theme follows the SYSTEM preference — applied
+ * makes an explicit choice, the theme follows the SYSTEM preference, applied
* pre-paint by the bootstrap script in the root layout, and kept in sync here
* if the OS preference changes while the app is open.
*/
diff --git a/src/components/shared/whatsapp-button.tsx b/src/components/shared/whatsapp-button.tsx
index 1bf896d..051e8c0 100644
--- a/src/components/shared/whatsapp-button.tsx
+++ b/src/components/shared/whatsapp-button.tsx
@@ -5,7 +5,7 @@ import { buildWhatsAppHref } from "@/modules/team/contact";
/**
* Click-to-chat WhatsApp button. Opens wa.me for the given phone with an
* optional prefilled message so an operator can message a buyer from their own
- * WhatsApp — no credentials or wallet required. Renders nothing if the phone is
+ * WhatsApp, no credentials or wallet required. Renders nothing if the phone is
* missing/invalid.
*/
export function WhatsAppButton({
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index b7b4431..6e0ab9a 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -19,7 +19,7 @@ const buttonVariants = cva(
size: {
default: "h-11 px-5",
lg: "h-12 px-6",
- // `sm` is the admin row-action size and was 36px everywhere — under the
+ // `sm` is the admin row-action size and was 36px everywhere, under the
// 44px touch minimum. It stays 44px on phones and keeps the denser
// 36px from `sm` up, where a mouse is doing the pointing.
sm: "h-11 px-4 sm:h-9",
@@ -37,7 +37,7 @@ export interface ButtonProps
VariantProps {
/**
* When true, shows an inline spinner, marks the button busy, and prevents
- * activation — the standard pattern for an action that is in flight.
+ * activation, the standard pattern for an action that is in flight.
*/
loading?: boolean;
}
diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx
index 7cdb3ce..2900ec4 100644
--- a/src/components/ui/data-table.tsx
+++ b/src/components/ui/data-table.tsx
@@ -16,7 +16,7 @@ import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
/**
- * Shared data table built on TanStack Table (headless) — the replacement for
+ * Shared data table built on TanStack Table (headless), the replacement for
* the bespoke `` implementations across the admin screens. One
* consistent look (tokens, sticky header, hover states) and one consistent
* behavior set (client-side sorting, optional search, optional pagination)
@@ -54,7 +54,7 @@ export function DataTable({
onRowClick?: (row: TData) => void;
className?: string;
/**
- * Drops the table's own border/radius/shadow chrome — for embedding inside
+ * Drops the table's own border/radius/shadow chrome, for embedding inside
* an existing Card (e.g. DataTableCard) without double borders.
*/
frameless?: boolean;
@@ -118,7 +118,7 @@ export function DataTable({
) : null}
{/* Phone layout: one card per row. A table narrower than the viewport is
- unreadable on a phone — these screens were 775-973px wide inside a
+ unreadable on a phone, these screens were 775-973px wide inside a
333px column, i.e. three screens of sideways scrolling. The first
column becomes the card heading and the rest become label/value
pairs, so the same columns stay legible in a single vertical scroll.
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index a879b10..0e91f7c 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -6,7 +6,7 @@ import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
/**
- * Accessible modal dialog primitive — the shared replacement for the bespoke
+ * Accessible modal dialog primitive, the shared replacement for the bespoke
* per-screen modals. Behavior mirrors the dashboard drawer (the app's most
* battle-tested overlay): body scroll lock, Escape to close, Tab focus trap,
* focus moves into the panel on open and back to the previously focused
@@ -31,7 +31,7 @@ export function Dialog({
}: {
open: boolean;
onClose: () => void;
- /** Required for accessibility — rendered as the dialog heading. */
+ /** Required for accessibility, rendered as the dialog heading. */
title: string;
/** Optional supporting copy under the title. */
description?: string;
@@ -164,7 +164,7 @@ export function Dialog({
);
}
-/** Standard footer row for dialog actions — right-aligned, gap-consistent. */
+/** Standard footer row for dialog actions, right-aligned, gap-consistent. */
export function DialogFooter({
children,
className,
diff --git a/src/components/ui/progress-bar.tsx b/src/components/ui/progress-bar.tsx
index 6a66522..88ba204 100644
--- a/src/components/ui/progress-bar.tsx
+++ b/src/components/ui/progress-bar.tsx
@@ -2,7 +2,7 @@ import { cn } from "@/lib/utils";
/**
* Determinate (or indeterminate) progress bar for operations whose duration is
- * observable — file uploads being the primary case.
+ * observable, file uploads being the primary case.
*
* Pass `value` (0–100) for a real progress fill; omit it for an indeterminate
* sweep when total work is unknown. Always provide a `label` for assistive tech.
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index 78b0437..3d628c4 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils";
/**
- * Styled native select — the shared replacement for the ~34 bespoke ``
+ * Styled native select, the shared replacement for the ~34 bespoke ``
* elements across the app. Deliberately built on the NATIVE element: it is the
* most accessible, mobile-friendly picker there is (iOS/Android render their
* platform pickers), needs no JS, and works inside plain server-rendered
diff --git a/src/components/ui/simple-data-table.tsx b/src/components/ui/simple-data-table.tsx
index 20ed403..20a87bf 100644
--- a/src/components/ui/simple-data-table.tsx
+++ b/src/components/ui/simple-data-table.tsx
@@ -6,7 +6,7 @@ import { DataTable, type ColumnDef } from "@/components/ui/data-table";
/**
* Client bridge for server components that hold plain string tables
- * (columns: string[], rows: string[][]) — the DataTableCard shape used by
+ * (columns: string[], rows: string[][]), the DataTableCard shape used by
* Audit Logs, Documents, and Transactions. Server pages can't construct
* TanStack ColumnDefs (cell renderers aren't serializable), so this wrapper
* takes the serializable shape and builds index-accessor columns client-side.
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
index e6899f3..75fa36d 100644
--- a/src/components/ui/spinner.tsx
+++ b/src/components/ui/spinner.tsx
@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils";
* that already conveys busy state (e.g. a Button with `aria-busy`), so it stays
* purely decorative and is not double-announced.
*
- * Stays spinning under `prefers-reduced-motion` — a loading spinner is
+ * Stays spinning under `prefers-reduced-motion`, a loading spinner is
* functional feedback, not decorative motion (see `.spinner` in globals.css).
*/
export function Spinner({
diff --git a/src/components/uploads/asset-picker-dialog.tsx b/src/components/uploads/asset-picker-dialog.tsx
index 0b06217..4e43ffb 100644
--- a/src/components/uploads/asset-picker-dialog.tsx
+++ b/src/components/uploads/asset-picker-dialog.tsx
@@ -8,7 +8,7 @@ import type { MediaLibraryAsset } from "@/modules/uploads/library";
import type { UploadPurpose } from "@/modules/uploads/config";
/**
- * Tenant media library picker — migrated to the shared Dialog primitive
+ * Tenant media library picker, migrated to the shared Dialog primitive
* (portal, Escape/backdrop close, scroll lock, focus trap + restore). Assets
* load lazily when the dialog opens, as before.
*/
diff --git a/src/inngest/functions.ts b/src/inngest/functions.ts
index c6dc98d..b4dfe93 100644
--- a/src/inngest/functions.ts
+++ b/src/inngest/functions.ts
@@ -54,14 +54,14 @@ import {
export const notificationFunctions = [
// ─────────────────────────────────────────────────────────────────────────
- // 1. OPERATIONAL SWEEP — hourly cron + manual event trigger
+ // 1. OPERATIONAL SWEEP, hourly cron + manual event trigger
//
// The sweep is intentionally kept as a single coordinated job rather
// than per-item fan-out so that the dedup mechanism in
// runScheduledOperationalJobs() (BackgroundJobLog) can prevent
// double-runs across manual API triggers and the cron.
//
- // retries: 1 — the sweep records its own BackgroundJobLog so a hard
+ // retries: 1, the sweep records its own BackgroundJobLog so a hard
// retry on failure is sufficient; aggressive retries would produce
// duplicate notifications.
// ─────────────────────────────────────────────────────────────────────────
@@ -75,7 +75,7 @@ export const notificationFunctions = [
],
},
async ({ event, step }) => {
- // event.data is BasicDataAny | CronEventData — cast to extract optional fields
+ // event.data is BasicDataAny | CronEventData, cast to extract optional fields
const data = event.data as Record;
return step.run("execute-sweep", () =>
runScheduledOperationalJobs({
@@ -90,7 +90,7 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // 2. WISHLIST REMINDER FAN-OUT — daily at 07:00 UTC
+ // 2. WISHLIST REMINDER FAN-OUT, daily at 07:00 UTC
//
// Step 1 (find-and-expire): atomically mark expired wishlists and
// collect items entering the 3-day reminder window.
@@ -101,7 +101,7 @@ export const notificationFunctions = [
// completed steps, so re-runs of this function after a partial
// failure won't re-dispatch already-dispatched events.
//
- // retries: 2 — if Prisma is unavailable the step will be retried
+ // retries: 2, if Prisma is unavailable the step will be retried
// before giving up on the whole batch.
// ─────────────────────────────────────────────────────────────────────────
inngest.createFunction(
@@ -133,13 +133,13 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // 3. PER-ITEM WISHLIST REMINDER — event triggered
+ // 3. PER-ITEM WISHLIST REMINDER, event triggered
//
// Handles one saved-property reminder: eligibility re-check (in case
// the buyer cancelled between fan-out and delivery), email send,
// in-app notification, and reminderSentAt stamp.
//
- // retries: 3 — transient email delivery failures should resolve
+ // retries: 3, transient email delivery failures should resolve
// within a few attempts without user impact.
// ─────────────────────────────────────────────────────────────────────────
inngest.createFunction(
@@ -160,7 +160,7 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // ANNOUNCEMENT BROADCAST — event-triggered on publish
+ // ANNOUNCEMENT BROADCAST, event-triggered on publish
//
// When an operator posts an announcement, fan out a WhatsApp message to
// everyone in the target audience (buyers / staff / all) who has a phone.
@@ -185,16 +185,16 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // 4. REVENUE RECOVERY SWEEP — daily at 09:00 UTC + manual event trigger
+ // 4. REVENUE RECOVERY SWEEP, daily at 09:00 UTC + manual event trigger
//
// Walks all OVERDUE transactions and advances each one to the next
// escalation stage (day 1 → day 3 → day 7 → day 14) based on how
// long ago nextPaymentDueAt passed.
//
- // Each transaction advances at most one stage per run — the
+ // Each transaction advances at most one stage per run, the
// overdueReminderStage high-water mark prevents re-sends.
//
- // retries: 1 — the stage field ensures idempotency; aggressive
+ // retries: 1, the stage field ensures idempotency; aggressive
// retries would not produce duplicate sends but a single retry is
// sufficient for transient DB / email failures.
// ─────────────────────────────────────────────────────────────────────────
@@ -218,14 +218,14 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // 5. MORNING BRIEFING FAN-OUT — daily at 08:00 UTC
+ // 5. MORNING BRIEFING FAN-OUT, daily at 08:00 UTC
//
// Fetches every active company that has at least one ADMIN user with an
// email address, then fans out one "morning-briefing/company.send" event
// per company. Each per-company delivery is independently retried so a
// single bad company record can't block all briefings.
//
- // retries: 1 — fan-out itself is idempotent; the child events carry the
+ // retries: 1, fan-out itself is idempotent; the child events carry the
// retry burden.
// ─────────────────────────────────────────────────────────────────────────
inngest.createFunction(
@@ -264,13 +264,13 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // 6. PER-COMPANY MORNING BRIEFING — event triggered
+ // 6. PER-COMPANY MORNING BRIEFING, event triggered
//
// Aggregates the four digest sections for one company and emails every
// ADMIN user. Two steps keep the aggregation and delivery observable
// and independently retried in Inngest's dashboard.
//
- // retries: 2 — transient DB or Resend failures should resolve on retry
+ // retries: 2, transient DB or Resend failures should resolve on retry
// without user impact.
// ─────────────────────────────────────────────────────────────────────────
inngest.createFunction(
@@ -325,7 +325,7 @@ export const notificationFunctions = [
),
// ─────────────────────────────────────────────────────────────────────────
- // 7. PROPERTY VERIFICATION SYNC — event triggered
+ // 7. PROPERTY VERIFICATION SYNC, event triggered
//
// Syncs verification states for one company (or all companies when
// companyId is omitted). Called from admin UI after bulk edits.
diff --git a/src/lib/auth/admin-sections.ts b/src/lib/auth/admin-sections.ts
index 6943517..a293b93 100644
--- a/src/lib/auth/admin-sections.ts
+++ b/src/lib/auth/admin-sections.ts
@@ -19,7 +19,7 @@ import type { QuickAction } from "@/components/dashboard/quick-actions";
/**
* Sidebar clusters, in canonical display order. Grouping ~30 flat links into
* labeled sections keeps the operator sidebar scannable as the surface grows;
- * every item belongs to exactly one group. Grouping is presentation-only —
+ * every item belongs to exactly one group. Grouping is presentation-only,
* route guards (rolesForAdminPath) are unaffected.
*/
export const ADMIN_NAV_GROUPS = [
@@ -44,7 +44,7 @@ export type AdminNavItem = {
roles: AppRole[];
/**
* Front-desk / hands-on operational tools (data entry, check-ins). These are
- * hidden from the owner/CEO's oversight nav — the owner monitors this activity
+ * hidden from the owner/CEO's oversight nav, the owner monitors this activity
* through the Executive Overview and Analytics rather than doing the entry work
* themselves. Owners retain direct route access (see rolesForAdminPath); this
* flag only curates the sidebar so the CEO gets an oversight-first cockpit,
@@ -102,7 +102,7 @@ export function canAccessAdminItem(roles: AppRole[], item: AdminNavItem): boolea
/**
* The nav items a given set of roles SEES in the sidebar, in canonical order.
* Operational (front-desk) tools are shown only to the roles that actually do
- * that work — never to the owner purely by virtue of full-admin — so the CEO
+ * that work, never to the owner purely by virtue of full-admin, so the CEO
* gets an oversight-first cockpit. Access itself is unaffected: rolesForAdminPath
* still lets the owner open any section directly.
*/
@@ -123,7 +123,7 @@ export type NavGroup = {
/**
* Groups the visible nav items into ordered sidebar clusters, dropping empty
* groups (a FINANCE-only user sees only Workspace + Finance + …). Items are
- * [label, href] tuples — the shape the sidebar/drawer components consume.
+ * [label, href] tuples, the shape the sidebar/drawer components consume.
*/
export function groupedAdminNav(roles: AppRole[]): NavGroup[] {
const visible = filterAdminNav(roles);
diff --git a/src/lib/media/images.ts b/src/lib/media/images.ts
index 47f9421..8e70a07 100644
--- a/src/lib/media/images.ts
+++ b/src/lib/media/images.ts
@@ -20,7 +20,7 @@ export function isRelativeAssetUrl(value: string) {
}
/**
- * Hosts the next/image optimizer is allowed to fetch from — must stay in sync
+ * Hosts the next/image optimizer is allowed to fetch from, must stay in sync
* with images.remotePatterns in next.config.ts. NEXT_PUBLIC_R2_PUBLIC_HOST is
* inlined at build time from R2_PUBLIC_BASE_URL, so tenant media on the
* public R2 domain gets real optimization (AVIF/WebP, resizing) instead of
diff --git a/src/lib/notifications/buyer-whatsapp.ts b/src/lib/notifications/buyer-whatsapp.ts
index 8253ad6..1cd45d0 100644
--- a/src/lib/notifications/buyer-whatsapp.ts
+++ b/src/lib/notifications/buyer-whatsapp.ts
@@ -7,7 +7,7 @@ import { sendWhatsAppMessage } from "@/lib/notifications/whatsapp";
* Best-effort WhatsApp notification to a buyer. Looks up their phone + first
* name, then dispatches through the shared Twilio + wallet layer, which no-ops
* safely when the buyer has no phone, Twilio is unconfigured, or the company's
- * messaging wallet is out of credit. Never throws — callers can fire-and-forget.
+ * messaging wallet is out of credit. Never throws, callers can fire-and-forget.
*/
export async function notifyBuyerWhatsApp(input: {
companyId: string;
diff --git a/src/lib/notifications/templates.ts b/src/lib/notifications/templates.ts
index b76ea1e..eb1632e 100644
--- a/src/lib/notifications/templates.ts
+++ b/src/lib/notifications/templates.ts
@@ -123,7 +123,7 @@ export function renderPaymentConfirmedEmail(data: {
amount: string;
companyName: string;
}): { subject: string; html: string } {
- const subject = "Payment confirmed — receipt issued";
+ const subject = "Payment confirmed: receipt issued";
const body = [
h1("Payment confirmed"),
p(`Hi ${esc(data.buyerName)} , your payment has been received and confirmed.`),
@@ -201,7 +201,7 @@ export function renderInspectionBookedEmail(data: {
propertyTitle: string;
companyName: string;
}): { subject: string; html: string } {
- const subject = `Inspection request received — ${data.propertyTitle}`;
+ const subject = `Inspection request received, ${data.propertyTitle}`;
const body = [
h1("Inspection request received"),
p(`Hi ${esc(data.fullName)} , we've received your request to inspect ${esc(data.propertyTitle)} .`),
@@ -222,7 +222,7 @@ export function renderInquiryReceivedEmail(data: {
companyName: string;
}): { subject: string; html: string } {
const subject = data.propertyTitle
- ? `Inquiry received — ${data.propertyTitle}`
+ ? `Inquiry received, ${data.propertyTitle}`
: "We received your inquiry";
const context = data.propertyTitle
@@ -249,7 +249,7 @@ export function renderPaymentOverdueEmail(data: {
outstandingBalance: string;
companyName: string;
}): { subject: string; html: string } {
- const subject = "Payment overdue — action required";
+ const subject = "Payment overdue: action required";
const body = [
h1("Payment overdue"),
p(`Hi ${esc(data.buyerName)} , a payment on your reservation is now overdue.`),
@@ -357,7 +357,7 @@ export function renderWishlistReminderEmail(data: {
cta("View property", data.propertyUrl),
divider(),
note(
- "If you no longer wish to pursue this property, no action is needed — your interest will expire automatically.",
+ "If you no longer wish to pursue this property, no action is needed. Your interest will expire automatically.",
),
].join("");
@@ -382,8 +382,8 @@ export function renderOverdueBuyerEmail(data: {
}): { subject: string; html: string } {
const urgent = data.daysOverdue >= 7;
const subject = urgent
- ? `Urgent: Payment overdue ${data.daysOverdue} days — ${data.reservationRef}`
- : `Reminder: Payment overdue — ${data.reservationRef}`;
+ ? `Urgent: Payment overdue ${data.daysOverdue} days, ${data.reservationRef}`
+ : `Reminder: Payment overdue, ${data.reservationRef}`;
const intro = data.daysOverdue === 1
? "This is a friendly reminder that a payment on your reservation was due yesterday."
@@ -421,7 +421,7 @@ export function renderMarketerEscalationEmail(data: {
whatsAppHref: string | null;
companyName: string;
}): { subject: string; html: string } {
- const subject = `Action required: ${data.buyerName} — ${data.reservationRef} overdue ${data.daysOverdue} days`;
+ const subject = `Action required: ${data.buyerName}, ${data.reservationRef} overdue ${data.daysOverdue} days`;
const body = [
h1("Deal escalated to you"),
p(`Hi ${esc(data.marketerName)} ,`),
@@ -467,7 +467,7 @@ export function renderOwnerEscalationEmail(data: {
}
const body = [
- h1("Revenue at risk — immediate attention needed"),
+ h1("Revenue at risk: immediate attention needed"),
p(`Hi ${esc(data.ownerName)} ,`),
p(`The following deal has been overdue for ${data.daysOverdue} days and has not been resolved. Your direct intervention may be required.`),
detailTable(rows),
@@ -563,8 +563,8 @@ export function renderMorningBriefingEmail(data: {
}): { subject: string; html: string } {
const hasUrgent = data.overdueCount > 0 || data.stalledCount > 0 || data.hiddenProperties > 0 || data.atRiskCount > 0;
const subject = hasUrgent
- ? `Morning briefing — ${data.overdueCount} overdue, ${data.atRiskCount} at risk — ${data.date}`
- : `Morning briefing — ${data.date}`;
+ ? `Morning briefing, ${data.overdueCount} overdue, ${data.atRiskCount} at risk, ${data.date}`
+ : `Morning briefing, ${data.date}`;
const alertRows: string[][] = [];
if (data.overdueCount > 0) alertRows.push([`${data.overdueCount} overdue payment${data.overdueCount === 1 ? "" : "s"}`, `${data.overdueTotalAtRisk} at risk`]);
@@ -575,7 +575,7 @@ export function renderMorningBriefingEmail(data: {
const body = [
h1("Daily briefing"),
- p(`Good morning ${esc(data.recipientName)} — here's your summary for ${esc(data.date)} .`),
+ p(`Good morning ${esc(data.recipientName)} , here's your summary for ${esc(data.date)} .`),
// Stat cards
statGrid([
diff --git a/src/lib/ops/health.test.ts b/src/lib/ops/health.test.ts
index 332f4dd..979b9fb 100644
--- a/src/lib/ops/health.test.ts
+++ b/src/lib/ops/health.test.ts
@@ -68,7 +68,7 @@ test("migration readiness reports every migration missing from the database", ()
/**
* Regression test for the production incident: the expected-migrations list
* was hand-maintained, stopped at 0035, and reported a false green while the
- * database was missing 0042 — whose absent columns produced P2022
+ * database was missing 0042, whose absent columns produced P2022
* "column does not exist" 500s.
*/
test("migration manifest covers the CMS migration that caused the P2022 incident", () => {
diff --git a/src/lib/ops/health.ts b/src/lib/ops/health.ts
index 0996bb3..dc528e7 100644
--- a/src/lib/ops/health.ts
+++ b/src/lib/ops/health.ts
@@ -15,7 +15,7 @@ export { EXPECTED_PRODUCTION_MIGRATIONS };
/**
* Migrations present in the repo but not yet applied to the connected
- * database — i.e. the deployed code expects columns the database does not
+ * database, i.e. the deployed code expects columns the database does not
* have. The expected list is GENERATED from prisma/migrations
* (scripts/generate-migration-manifest.mjs) rather than hand-maintained,
* because a hand-maintained list silently went stale at 0035 and let a
diff --git a/src/lib/ops/migration-manifest.ts b/src/lib/ops/migration-manifest.ts
index 663027b..6907561 100644
--- a/src/lib/ops/migration-manifest.ts
+++ b/src/lib/ops/migration-manifest.ts
@@ -1,4 +1,4 @@
-// GENERATED FILE — DO NOT EDIT BY HAND.
+// 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
diff --git a/src/lib/payments/reconciliation.ts b/src/lib/payments/reconciliation.ts
index 2cb5d89..0066028 100644
--- a/src/lib/payments/reconciliation.ts
+++ b/src/lib/payments/reconciliation.ts
@@ -57,7 +57,7 @@ function normalizePaystackStatus(status?: string): PaymentStatus {
return status === "success" ? "SUCCESS" : "FAILED";
}
-/** Prisma P2002 — unique constraint violation (concurrent duplicate webhook). */
+/** Prisma P2002, unique constraint violation (concurrent duplicate webhook). */
function isUniqueConstraintError(error: unknown) {
return (
typeof error === "object" &&
@@ -378,7 +378,7 @@ export async function reconcilePaystackWebhook(rawPayload: PaystackWebhookPayloa
},
});
} catch (error) {
- // Concurrent duplicate delivery lost the unique-index race — treat
+ // Concurrent duplicate delivery lost the unique-index race, treat
// exactly like the fast-path dedup above.
if (isUniqueConstraintError(error)) {
return { duplicate: true, companyId: company.id, providerEventId };
@@ -422,7 +422,7 @@ export async function reconcilePaystackWebhook(rawPayload: PaystackWebhookPayloa
})
: null;
- // Idempotency guards — see resolveReconciliationStatus for the rules.
+ // Idempotency guards, see resolveReconciliationStatus for the rules.
// Receipt/commission/settlement upserts stay unconditional below: they are
// keyed by paymentId and idempotent, so a re-delivery can self-heal a
// partially failed first attempt without double-applying money mutations.
@@ -590,7 +590,7 @@ export async function reconcilePaystackWebhook(rawPayload: PaystackWebhookPayloa
transactionUpdate = await prisma.$transaction(async (tx) => {
let updatedTransactionId = payment.transactionId;
let updatedTransactionStage: string | null = null;
- // Captured inside the tx, dispatched AFTER commit — never run external
+ // Captured inside the tx, dispatched AFTER commit, never run external
// WhatsApp/Twilio I/O inside a database transaction.
let paymentConfirmedBuyerUserId: string | null = null;
let persistedReceipt: {
@@ -894,7 +894,7 @@ export async function reconcilePaystackWebhook(rawPayload: PaystackWebhookPayloa
});
} catch (error) {
// A concurrent duplicate delivery lost the webhookEvent unique-index
- // race — the ENTIRE transaction (balance, receipt, notifications)
+ // race, the ENTIRE transaction (balance, receipt, notifications)
// rolled back atomically. Report it as the duplicate it is.
if (isUniqueConstraintError(error)) {
return { duplicate: true, companyId: company.id, providerEventId };
diff --git a/src/lib/realtime/change-counter.ts b/src/lib/realtime/change-counter.ts
index de6aa38..04995d2 100644
--- a/src/lib/realtime/change-counter.ts
+++ b/src/lib/realtime/change-counter.ts
@@ -2,12 +2,12 @@ import { redis } from "@/lib/cache/redis";
import { buildSafeErrorLogContext, logWarn } from "@/lib/ops/logger";
/**
- * Cross-instance change counters — the backplane for CONDITIONAL polling.
+ * Cross-instance change counters, the backplane for CONDITIONAL polling.
*
* Every published realtime event bumps a per-company counter (and the
* platform counter for the superadmin surface) in Upstash Redis. Dashboards
* poll the tiny /api/realtime/version endpoint (one Redis GET) and call
- * router.refresh() ONLY when the counter moved — instead of blindly
+ * router.refresh() ONLY when the counter moved, instead of blindly
* re-rendering every 30s. Correct across serverless instances by
* construction, unlike the in-process EventEmitter bus.
*
diff --git a/src/lib/realtime/events.ts b/src/lib/realtime/events.ts
index b98d668..19a1800 100644
--- a/src/lib/realtime/events.ts
+++ b/src/lib/realtime/events.ts
@@ -54,7 +54,7 @@ export function publishRealtimeEvent(
getRealtimeBus().emit("platform-event", event);
// Cross-instance signal for conditional polling: bump the per-company and
- // platform change counters in Redis. Fire-and-forget — publishing an event
+ // platform change counters in Redis. Fire-and-forget, publishing an event
// must never fail or slow down the mutation that triggered it (the bump
// catches internally and degrades to blind polling on the client).
void bumpChangeCounters(event.companyId);
diff --git a/src/lib/realtime/refresh-policy.ts b/src/lib/realtime/refresh-policy.ts
index 0d0cdfb..b3235a7 100644
--- a/src/lib/realtime/refresh-policy.ts
+++ b/src/lib/realtime/refresh-policy.ts
@@ -7,7 +7,7 @@ export const REALTIME_REFRESH_INTERVAL_MS = 30_000;
/**
* Minimum spacing between refreshes triggered by an actual version change.
* Shorter than the blind interval on purpose: when we KNOW something changed,
- * re-render promptly — the floor only debounces event bursts.
+ * re-render promptly, the floor only debounces event bursts.
*/
export const REALTIME_CHANGE_REFRESH_MIN_INTERVAL_MS = 5_000;
diff --git a/src/lib/security/csp.ts b/src/lib/security/csp.ts
index 612714a..8552364 100644
--- a/src/lib/security/csp.ts
+++ b/src/lib/security/csp.ts
@@ -1,5 +1,5 @@
/**
- * Content-Security-Policy — ENFORCED, nonce-based.
+ * Content-Security-Policy, ENFORCED, nonce-based.
*
* History: the CSP shipped as `Content-Security-Policy-Report-Only` from
* next.config.ts (static headers) starting 2026-06 so real traffic could be
@@ -35,7 +35,7 @@
/**
* Pre-hydration theme bootstrap. Runs before paint so users who chose the
- * dark theme — or whose SYSTEM is dark and who haven't chosen anything —
+ * dark theme, or whose SYSTEM is dark and who haven't chosen anything,
* don't get a light-mode flash. An explicit stored choice ("dark"/"light")
* always wins over prefers-color-scheme. Rendered in src/app/layout.tsx.
*
@@ -129,14 +129,14 @@ export function buildContentSecurityPolicy(options: {
// Web/Service workers (Mapbox GL spawns blob: workers).
"worker-src 'self' blob:",
"child-src 'self' blob:",
- // Media (audio/video) — self + blob + R2 objects (presigned, public
+ // Media (audio/video), self + blob + R2 objects (presigned, public
// bucket domains, and the configured tenant media domain).
`media-src 'self' blob: https://*.r2.cloudflarestorage.com https://*.r2.dev${mediaHostSource}`,
// App manifest.
"manifest-src 'self'",
// Form submissions only to our own origin (+ Paystack redirect target, defensively).
"form-action 'self' https://checkout.paystack.com",
- // Who may frame us — mirrors the X-Frame-Options: SAMEORIGIN header.
+ // Who may frame us, mirrors the X-Frame-Options: SAMEORIGIN header.
"frame-ancestors 'self'",
// Restrict and disallow plugins.
"base-uri 'self'",
diff --git a/src/lib/tenancy/context.ts b/src/lib/tenancy/context.ts
index b41a285..49c2d9d 100644
--- a/src/lib/tenancy/context.ts
+++ b/src/lib/tenancy/context.ts
@@ -145,7 +145,7 @@ async function logLocalTenantFallbackHint(host: string | null, devTenantSlug: st
const example = devTenantSlug ?? knownSlugs[0] ?? "";
const nextStep =
knownSlugs.length === 0
- ? "This database has no companies yet — run `npm run db:seed` first."
+ ? "This database has no companies yet, run `npm run db:seed` first."
: `Companies in this database: ${knownSlugs.join(", ")}.`;
logWarn(
diff --git a/src/lib/validations/site-content.ts b/src/lib/validations/site-content.ts
index 048d9bf..b14aa6b 100644
--- a/src/lib/validations/site-content.ts
+++ b/src/lib/validations/site-content.ts
@@ -1,7 +1,7 @@
import { z } from "zod";
/**
- * Validation for tenant-authored marketing copy — the full public site:
+ * Validation for tenant-authored marketing copy, the full public site:
* SEO, hero (+ stat cards + side panel), buyer journey, section headings,
* about, contact extras, social links, footer.
*
diff --git a/src/modules/admin/activity-feed.ts b/src/modules/admin/activity-feed.ts
index 919176a..b012daf 100644
--- a/src/modules/admin/activity-feed.ts
+++ b/src/modules/admin/activity-feed.ts
@@ -51,7 +51,7 @@ async function safeSource(fn: () => Promise): Promise {
/**
* A read-only, company-wide "what's happening" feed for the owner/CEO: recent
* visitors, leads, payments, buyer messages, and reservations merged into one
- * time-ordered timeline. Reinforces the oversight model — the owner monitors
+ * time-ordered timeline. Reinforces the oversight model, the owner monitors
* activity here rather than doing the operational entry work.
*/
export async function getOwnerActivityFeed(ctx: Ctx): Promise {
diff --git a/src/modules/admin/deal-board.ts b/src/modules/admin/deal-board.ts
index f501212..5a9127c 100644
--- a/src/modules/admin/deal-board.ts
+++ b/src/modules/admin/deal-board.ts
@@ -190,7 +190,7 @@ const transactionSelect = Prisma.validator()({
followUpNote: true,
lastFollowedUpAt: true,
nextFollowUpAt: true,
- // riskScore is added after prisma generate — cast via any below
+ // riskScore is added after prisma generate, cast via any below
updatedAt: true,
user: {
select: {
@@ -720,7 +720,7 @@ export async function getAdminDealBoard(context: TenantContext): Promise Promise)({
diff --git a/src/modules/admin/executive-overview.ts b/src/modules/admin/executive-overview.ts
index 93b0ae8..0ed5148 100644
--- a/src/modules/admin/executive-overview.ts
+++ b/src/modules/admin/executive-overview.ts
@@ -92,7 +92,7 @@ function emptyOverview(): ExecutiveOverview {
/**
* Company-wide snapshot for the owner: a single view spanning sales, finance, and
- * front-desk health. Composed from existing models and never throws — returns a
+ * front-desk health. Composed from existing models and never throws, returns a
* zeroed snapshot on any failure so the landing always renders.
*/
export async function getExecutiveOverview(context: {
diff --git a/src/modules/announcements/service.ts b/src/modules/announcements/service.ts
index 6de0531..28376a4 100644
--- a/src/modules/announcements/service.ts
+++ b/src/modules/announcements/service.ts
@@ -182,7 +182,7 @@ function audienceRoleNames(audience: AnnouncementAudience): AppRole[] {
* Fan out a published announcement to its audience over WhatsApp. Runs from a
* background job (see the `announcement/published` Inngest function) so a large
* recipient set never blocks the request. Each send goes through the shared
- * Twilio + wallet layer, which no-ops safely when unconfigured — so this is a
+ * Twilio + wallet layer, which no-ops safely when unconfigured, so this is a
* best-effort broadcast that activates once WhatsApp credentials + credit exist.
*/
export async function broadcastAnnouncementWhatsApp(
diff --git a/src/modules/automation/service.ts b/src/modules/automation/service.ts
index eda124c..415c6ae 100644
--- a/src/modules/automation/service.ts
+++ b/src/modules/automation/service.ts
@@ -195,7 +195,7 @@ export async function runOperationalAutomationSweep(input?: { companyId?: string
transactionId: transaction.id,
href: "/admin/payments",
} as Prisma.InputJsonValue,
- emailSubject: `Payment overdue — ${reservationRef}`,
+ emailSubject: `Payment overdue, ${reservationRef}`,
emailHtml: renderOperatorPaymentOverdueAlert({
reservationRef,
outstandingBalance: formattedBalance,
diff --git a/src/modules/benchmarks/aggregator.ts b/src/modules/benchmarks/aggregator.ts
index de402de..6d5e773 100644
--- a/src/modules/benchmarks/aggregator.ts
+++ b/src/modules/benchmarks/aggregator.ts
@@ -13,7 +13,7 @@
* Platform distributions are computed by running the same aggregations
* across ALL companies simultaneously, grouping by companyId, and deriving
* per-company values before extracting quartiles. No individual company
- * identity is surfaced — only anonymised distribution statistics.
+ * identity is surfaced, only anonymised distribution statistics.
*
* Minimum thresholds prevent thin-data workspaces from skewing benchmarks:
* - Conversion rate: company must have ≥ 5 inquiries in the window
@@ -176,7 +176,7 @@ export async function getBenchmarkReport(companyId: string): Promise
);
/**
- * Published branding only — what every public page, metadata and app shell
+ * Published branding only, what every public page, metadata and app shell
* renders. The layout, generateMetadata, the public shell and the page each ask
* for it; before this they issued 3-5 identical queries per render.
* - unstable_cache (cross-request, 60s) is keyed and tagged by companyId so a
diff --git a/src/modules/cms/site-content-service.ts b/src/modules/cms/site-content-service.ts
index 777d027..d12c780 100644
--- a/src/modules/cms/site-content-service.ts
+++ b/src/modules/cms/site-content-service.ts
@@ -16,7 +16,7 @@ import type { StoredSiteContent } from "@/modules/cms/site-content";
* consistent across the two CMS surfaces.
*
* The *public render* only ever reads `getPublishedSiteContent`, which returns
- * the stored overrides (or null) and never throws — so a DB hiccup degrades to
+ * the stored overrides (or null) and never throws, so a DB hiccup degrades to
* the company-derived fallback copy instead of breaking the marketing site.
*/
@@ -25,7 +25,7 @@ export type TenantSiteContentState = {
published: StoredSiteContent;
publishedAt: string | null;
/**
- * True when the content columns could not be read — in practice a database
+ * True when the content columns could not be read, in practice a database
* that is behind the deployed code (pending migration). The editor renders
* read-only with an explanatory banner rather than throwing.
*/
@@ -105,7 +105,7 @@ export type TenantPublicContact = {
/**
* Public contact details for a tenant, read from SiteSettings. Used to render the
* tenant's own email / phone / office on the public contact page instead of
- * hardcoded placeholders. Never throws — missing fields render as omitted rows.
+ * hardcoded placeholders. Never throws, missing fields render as omitted rows.
*/
export async function getPublicTenantContact(
context: TenantContext,
@@ -155,7 +155,7 @@ const loadPublishedSiteContentRow = cache((companyId: string) =>
)(),
);
-/** Public render entry point. Never throws — returns null so callers fall back. */
+/** Public render entry point. Never throws, returns null so callers fall back. */
export async function getPublishedSiteContent(
context: TenantContext,
): Promise {
diff --git a/src/modules/cms/site-content.test.ts b/src/modules/cms/site-content.test.ts
index ce55f61..0d169e3 100644
--- a/src/modules/cms/site-content.test.ts
+++ b/src/modules/cms/site-content.test.ts
@@ -118,7 +118,7 @@ test("social links must be https and whatsapp must be E.164", () => {
}).success,
true,
);
- // Blank socials are fine — they simply hide the icon.
+ // Blank socials are fine, they simply hide the icon.
assert.equal(siteContentSchema.safeParse({ social: { facebook: "" } }).success, true);
});
@@ -181,7 +181,7 @@ test("journey accepts at most three steps and bounded copy", () => {
heading: "How buying works",
steps: [{ title: "Find", description: "Browse verified homes." }],
},
- seo: { title: "Acme Realty — Homes in Lekki", description: "Verified homes in Lekki." },
+ seo: { title: "Acme Realty, Homes in Lekki", description: "Verified homes in Lekki." },
}).success,
true,
);
diff --git a/src/modules/cms/site-content.ts b/src/modules/cms/site-content.ts
index dcbb3a1..f9220ad 100644
--- a/src/modules/cms/site-content.ts
+++ b/src/modules/cms/site-content.ts
@@ -3,7 +3,7 @@
*
* Centralizes ALL editable text of a tenant's public marketing site behind
* one typed shape with sensible, company-derived fallbacks. Components read
- * from the resolved object, never from hardcoded strings — every tenant can
+ * from the resolved object, never from hardcoded strings, every tenant can
* make the site sound like *their* company without touching code.
*
* Coverage: SEO metadata, hero (copy + stat cards + side panel), the
@@ -48,7 +48,7 @@ export type TenantSiteContent = {
title: string;
body: string;
};
- /** "How it works" — the three-step buyer journey. */
+ /** "How it works", the three-step buyer journey. */
journey: {
heading: string;
steps: [JourneyStep, JourneyStep, JourneyStep];
@@ -63,7 +63,7 @@ export type TenantSiteContent = {
title: string;
intro: string;
};
- /** Careers page — hideable for companies that aren't hiring. */
+ /** Careers page, hideable for companies that aren't hiring. */
careers: {
visible: boolean;
eyebrow: string;
@@ -179,7 +179,7 @@ export function resolveTenantSiteContent(args: {
const fallback: TenantSiteContent = {
seo: {
- title: `${companyName} — Verified property listings`,
+ title: `${companyName} | Verified property listings`,
description:
trimmedDescription ||
`Browse verified listings from ${companyName}, book inspections, and buy your next property with confidence.`,
@@ -188,13 +188,13 @@ export function resolveTenantSiteContent(args: {
eyebrow: "Trusted property transactions",
headline: `Find your next home with ${companyName}.`,
subhead:
- "Browse verified listings, book inspections with our team, and move from first viewing to keys in hand — with clear pricing and no surprises.",
+ "Browse verified listings, book inspections with our team, and move from first viewing to keys in hand, with clear pricing and no surprises.",
primaryCta: { label: "View properties", href: "/properties" },
secondaryCta: { label: "Start purchase", href: startPurchaseHref },
},
heroStats: {
inventoryLabel: "Homes available",
- inventoryNote: "Every listing is verified by our team before it goes live — no stale or hidden inventory.",
+ inventoryNote: "Every listing is verified by our team before it goes live. No stale or hidden inventory.",
marketersLabel: "Expert marketers",
marketersNote: "Meet the people who will walk you from first viewing to handover.",
trustLabel: "Happy clients",
@@ -221,7 +221,7 @@ export function resolveTenantSiteContent(args: {
{
title: "Move in with peace of mind",
description:
- "Track payments, sign your documents, and collect your keys — all in one place.",
+ "Track payments, sign your documents, and collect your keys, all in one place.",
},
],
},
@@ -235,7 +235,7 @@ export function resolveTenantSiteContent(args: {
marketers: {
title: "Meet the team moving deals forward",
description:
- "Our marketers guide you from first viewing to handover — pick one and start a conversation.",
+ "Our marketers guide you from first viewing to handover. Pick one and start a conversation.",
},
testimonials: {
eyebrow: "Testimonials",
@@ -260,12 +260,12 @@ export function resolveTenantSiteContent(args: {
{
title: "Real ownership",
description:
- "Your work directly helps families find and secure their homes — you see the impact of every deal you close.",
+ "Your work directly helps families find and secure their homes. You see the impact of every deal you close.",
},
{
title: "Trust by default",
description:
- "We win by making property transactions feel safe and transparent — for buyers and for the people who work with us.",
+ "We win by making property transactions feel safe and transparent, for buyers and for the people who work with us.",
},
{
title: "Room to grow",
@@ -275,12 +275,12 @@ export function resolveTenantSiteContent(args: {
],
ctaHeading: "Want to work with us?",
ctaBody:
- "Tell us how you'd like to contribute — as a marketer, agent, or partner — and we'll get back to you.",
+ "Tell us how you'd like to contribute, as a marketer, agent, or partner, and we'll get back to you.",
ctaLabel: "Introduce yourself",
},
contact: {
hours: "Mon – Fri, 9:00 – 17:00",
- note: "Prefer WhatsApp? Message us any time — we usually reply within the hour.",
+ note: "Prefer WhatsApp? Message us any time. We usually reply within the hour.",
},
social: {
facebook: "",
@@ -293,7 +293,7 @@ export function resolveTenantSiteContent(args: {
footer: {
tagline:
trimmedDescription ||
- `${companyName} — verified listings, honest guidance, and a property journey you can track from start to finish.`,
+ `${companyName}: verified listings, honest guidance, and a property journey you can track from start to finish.`,
},
};
diff --git a/src/modules/commission/calculator.ts b/src/modules/commission/calculator.ts
index 10e6dc0..f073d9e 100644
--- a/src/modules/commission/calculator.ts
+++ b/src/modules/commission/calculator.ts
@@ -1,7 +1,7 @@
/**
* Marketer commission calculator and recorder.
*
- * calculateCommissionAmount() is a pure function — no I/O.
+ * calculateCommissionAmount() is a pure function, no I/O.
* recordMarketerCommission() persists one MarketerCommission row per payment.
* getMarketerCommissionTotals() returns a Map used by the performance dashboard.
*/
@@ -28,7 +28,7 @@ function getCommissionDelegate(): CommissionDelegate | null {
// ─── Calculation ──────────────────────────────────────────────────────────
/**
- * Pure function — returns the commission amount in the same currency as the rule.
+ * Pure function, returns the commission amount in the same currency as the rule.
* FLAT: fixed amount regardless of payment size.
* PERCENTAGE: percentage of the payment amount, rounded to 2dp.
*/
@@ -63,7 +63,7 @@ export async function recordMarketerCommission(input: {
const delegate = delegateInput ?? getCommissionDelegate();
if (!delegate) return;
- // @unique on paymentId — ignore duplicate (idempotent webhook re-delivery)
+ // @unique on paymentId, ignore duplicate (idempotent webhook re-delivery)
try {
await delegate.create({
data: {
@@ -78,7 +78,7 @@ export async function recordMarketerCommission(input: {
},
});
} catch (err: unknown) {
- // P2002 = unique constraint violation — commission already recorded for this payment
+ // P2002 = unique constraint violation, commission already recorded for this payment
if (
err instanceof Error &&
"code" in err &&
diff --git a/src/modules/commission/rules.ts b/src/modules/commission/rules.ts
index 05bd76b..7cef7c2 100644
--- a/src/modules/commission/rules.ts
+++ b/src/modules/commission/rules.ts
@@ -15,7 +15,7 @@ import { prisma } from "@/lib/db/prisma";
import { featureFlags } from "@/lib/env";
// ─── Delegate pattern (same as marketerRankingSnapshot in performance.ts) ───
-// Prisma client is not yet regenerated — access new models via typed cast.
+// Prisma client is not yet regenerated, access new models via typed cast.
type RuleRecord = {
id: string;
diff --git a/src/modules/contracts/service.ts b/src/modules/contracts/service.ts
index 1224afd..1f7ff51 100644
--- a/src/modules/contracts/service.ts
+++ b/src/modules/contracts/service.ts
@@ -3,7 +3,7 @@
*
* Lifecycle for a signed agreement:
* PENDING → admin uploaded a contract PDF and linked it to a transaction
- * ACTIVE → admin marked it as "sent" — buyer can now view and accept
+ * ACTIVE → admin marked it as "sent", buyer can now view and accept
* COMPLETED → buyer accepted in the portal (timestamp + IP recorded)
*
* All SignedAgreement queries use `as any` casts because the new schema
@@ -294,7 +294,7 @@ export async function createContract(input: {
/**
* Marks the agreement as ACTIVE (sent) and notifies the buyer via in-app
- * notification. Idempotent — calling it on an already-ACTIVE agreement
+ * notification. Idempotent, calling it on an already-ACTIVE agreement
* returns the existing row without re-notifying.
*/
export async function sendContract(input: {
diff --git a/src/modules/development-calculations/recommendations.ts b/src/modules/development-calculations/recommendations.ts
index 4f2c990..2012857 100644
--- a/src/modules/development-calculations/recommendations.ts
+++ b/src/modules/development-calculations/recommendations.ts
@@ -347,9 +347,9 @@ export async function generateFeasibilityNarrative(
const prompt = `You are a senior real estate development analyst. Write a 3-paragraph plain-English feasibility narrative for the project described below.
-Paragraph 1 — Viability summary: State whether the project is viable based on margin, ROI, and break-even cushion. Quote specific numbers.
-Paragraph 2 — Key risks and scenario triggers: Identify the 2–3 most significant risks from the warnings and downside scenario. Be specific about what numbers would change and why.
-Paragraph 3 — Recommended action with numbers: Give a direct recommendation — proceed, pause for revision, or abandon — with specific thresholds or conditions attached.
+Paragraph 1, Viability summary: State whether the project is viable based on margin, ROI, and break-even cushion. Quote specific numbers.
+Paragraph 2, Key risks and scenario triggers: Identify the 2–3 most significant risks from the warnings and downside scenario. Be specific about what numbers would change and why.
+Paragraph 3, Recommended action with numbers: Give a direct recommendation, proceed, pause for revision, or abandon, with specific thresholds or conditions attached.
Use short, direct sentences. No bullet points. No headers. No markdown. Output only the three paragraphs separated by blank lines.
@@ -365,9 +365,9 @@ ROI: ${results.revenue.roiPercent.toFixed(1)}% | Margin: ${results.revenue.margi
Target margin: ${form.requiredTargetProfitMarginRate}%
Peak funding gap: ${fmt(results.phasing.peakFundingGap)}
Payback month: ${results.phasing.paybackMonth != null ? `Month ${results.phasing.paybackMonth}` : "Not reached in plan window"}
-BASE scenario — ROI: ${baseScenario ? baseScenario.roiPercent.toFixed(1) + "%" : "n/a"}, Margin: ${baseScenario ? baseScenario.marginPercent.toFixed(1) + "%" : "n/a"}
-BEST scenario — ROI: ${bestScenario ? bestScenario.roiPercent.toFixed(1) + "%" : "n/a"}, Revenue: ${bestScenario ? fmt(bestScenario.estimatedRevenue) : "n/a"}
-WORST scenario — ROI: ${worstScenario ? worstScenario.roiPercent.toFixed(1) + "%" : "n/a"}, Margin: ${worstScenario ? worstScenario.marginPercent.toFixed(1) + "%" : "n/a"}
+BASE scenario, ROI: ${baseScenario ? baseScenario.roiPercent.toFixed(1) + "%" : "n/a"}, Margin: ${baseScenario ? baseScenario.marginPercent.toFixed(1) + "%" : "n/a"}
+BEST scenario, ROI: ${bestScenario ? bestScenario.roiPercent.toFixed(1) + "%" : "n/a"}, Revenue: ${bestScenario ? fmt(bestScenario.estimatedRevenue) : "n/a"}
+WORST scenario, ROI: ${worstScenario ? worstScenario.roiPercent.toFixed(1) + "%" : "n/a"}, Margin: ${worstScenario ? worstScenario.marginPercent.toFixed(1) + "%" : "n/a"}
Warnings: ${results.warnings.length > 0 ? results.warnings.join("; ") : "None"}`;
const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY!);
diff --git a/src/modules/finance/service.ts b/src/modules/finance/service.ts
index f089627..589eedf 100644
--- a/src/modules/finance/service.ts
+++ b/src/modules/finance/service.ts
@@ -78,7 +78,7 @@ function relativeTime(date: Date, now: Date): string {
/**
* Accountant snapshot: cash collected, what is still outstanding/overdue, and the
* latest successful payments. Built from existing Payment / Transaction data and
- * never throws — returns a zeroed snapshot on any failure so the page always
+ * never throws, returns a zeroed snapshot on any failure so the page always
* renders.
*/
export async function getFinanceOverview(context: {
diff --git a/src/modules/front-desk/logbook.ts b/src/modules/front-desk/logbook.ts
index d1c5dfa..ccfa613 100644
--- a/src/modules/front-desk/logbook.ts
+++ b/src/modules/front-desk/logbook.ts
@@ -66,14 +66,14 @@ export async function getLogbookTodayCounts(
]);
return { visitorsToday, callsToday };
} catch {
- // Tables not migrated yet — treat as zero rather than failing the caller.
+ // Tables not migrated yet, treat as zero rather than failing the caller.
return { visitorsToday: 0, callsToday: 0 };
}
}
/**
* Full front-desk logbook (recent visitors + calls and today's counts). Never
- * throws — if the Visitor/CallLog tables have not been migrated yet it returns an
+ * throws, if the Visitor/CallLog tables have not been migrated yet it returns an
* empty logbook so the page renders cleanly until the migration is applied.
*/
export async function getFrontDeskLogbook(context: {
diff --git a/src/modules/front-desk/service.ts b/src/modules/front-desk/service.ts
index 03b9575..5d1011c 100644
--- a/src/modules/front-desk/service.ts
+++ b/src/modules/front-desk/service.ts
@@ -78,7 +78,7 @@ function relativeTime(date: Date, now: Date): string {
/**
* Front-desk operations snapshot, assembled from existing CRM data (inquiries,
- * inspection bookings, reservations). Never throws — on any failure or when the
+ * inspection bookings, reservations). Never throws, on any failure or when the
* database is unavailable it returns a zeroed snapshot so the dashboard always
* renders.
*/
diff --git a/src/modules/inquiries/service.ts b/src/modules/inquiries/service.ts
index 9ac78ea..20a593a 100644
--- a/src/modules/inquiries/service.ts
+++ b/src/modules/inquiries/service.ts
@@ -458,7 +458,7 @@ export async function createInquiry(
propertyId: inquiry.propertyId,
snippet: buildSnippet(parsed.message),
} as Prisma.InputJsonValue,
- emailSubject: `New inquiry — ${inquiry.property?.title ?? inquiry.fullName}`,
+ emailSubject: `New inquiry, ${inquiry.property?.title ?? inquiry.fullName}`,
emailHtml: renderOperatorInquiryAlert({
buyerName: inquiry.fullName,
propertyTitle: inquiry.property?.title,
diff --git a/src/modules/inspections/service.ts b/src/modules/inspections/service.ts
index 3d83883..3930a04 100644
--- a/src/modules/inspections/service.ts
+++ b/src/modules/inspections/service.ts
@@ -208,7 +208,7 @@ export async function createInspectionBooking(
propertyId: booking.propertyId,
inquiryId: booking.inquiryId,
} as Prisma.InputJsonValue,
- emailSubject: `New inspection request — ${property.title}`,
+ emailSubject: `New inspection request, ${property.title}`,
emailHtml: renderOperatorInspectionAlert({
buyerName: booking.fullName,
propertyTitle: property.title,
diff --git a/src/modules/invoices/service.ts b/src/modules/invoices/service.ts
index 572e296..74a73f7 100644
--- a/src/modules/invoices/service.ts
+++ b/src/modules/invoices/service.ts
@@ -40,7 +40,7 @@ function toNumber(value: unknown): number {
}
function shortDate(value: Date | null | undefined): string {
- if (!value) return "—";
+ if (!value) return "-";
return new Date(value).toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" });
}
@@ -51,7 +51,7 @@ function generateInvoiceNumber(): string {
return `INV-${stamp}-${suffix}`;
}
-/** Create an invoice. Never throws — returns a structured result. */
+/** Create an invoice. Never throws, returns a structured result. */
export async function createInvoice(
context: TenantContext,
input: CreateInvoiceInput,
diff --git a/src/modules/messaging/service.ts b/src/modules/messaging/service.ts
index 05afdaf..8cb3611 100644
--- a/src/modules/messaging/service.ts
+++ b/src/modules/messaging/service.ts
@@ -146,7 +146,7 @@ export async function getThread(
});
if (!thread) return null;
- // Mark read for the viewing side (best effort — never blocks the render).
+ // Mark read for the viewing side (best effort, never blocks the render).
try {
await prisma.messageThread.update({
where: { id: thread.id },
@@ -156,7 +156,7 @@ export async function getThread(
/* non-fatal */
}
- // Only the team view exposes the buyer's phone (for click-to-chat) — buyers
+ // Only the team view exposes the buyer's phone (for click-to-chat), buyers
// don't need their own number surfaced here.
let buyerPhone: string | null = null;
if (viewer === "team") {
@@ -303,7 +303,7 @@ export async function sendMessage(input: {
// 2) WhatsApp (via the shared Twilio + wallet layer). This no-ops safely
// when the buyer has no phone, Twilio is unconfigured, or the company's
- // messaging wallet is out of credit — so it's safe to always attempt.
+ // messaging wallet is out of credit, so it's safe to always attempt.
try {
const buyer = await prisma.user.findUnique({
where: { id: thread.buyerUserId },
diff --git a/src/modules/morning-briefing/aggregator.ts b/src/modules/morning-briefing/aggregator.ts
index b43a847..229056d 100644
--- a/src/modules/morning-briefing/aggregator.ts
+++ b/src/modules/morning-briefing/aggregator.ts
@@ -2,13 +2,13 @@
* Morning briefing data aggregator.
*
* Gathers the four sections of the daily digest for one company:
- * 1. Overdue payments — count + total amount at risk
- * 2. Today's inspections — confirmed/rescheduled between 00:00–23:59 UTC
- * 3. Stalled deals — in-progress transactions with no activity for 7+ days
- * 4. Urgent alerts — hidden properties and KYC submissions awaiting review
+ * 1. Overdue payments, count + total amount at risk
+ * 2. Today's inspections, confirmed/rescheduled between 00:00–23:59 UTC
+ * 3. Stalled deals, in-progress transactions with no activity for 7+ days
+ * 4. Urgent alerts, hidden properties and KYC submissions awaiting review
*
* All queries run in parallel (Promise.all). The function is pure with respect
- * to side-effects — callers decide what to do with the result.
+ * to side-effects, callers decide what to do with the result.
*/
import { endOfDay, startOfDay, subDays, differenceInDays } from "date-fns";
@@ -55,24 +55,24 @@ export interface MorningBriefingData {
companyName: string;
date: string; // e.g. "Tuesday, 15 April 2026"
- // Section 1 — overdue payments
+ // Section 1, overdue payments
overdueCount: number;
overdueTotalAtRisk: string;
overdueRows: OverduePaymentRow[];
- // Section 2 — today's inspections
+ // Section 2, today's inspections
inspectionCount: number;
inspectionRows: InspectionRow[];
- // Section 3 — stalled deals
+ // Section 3, stalled deals
stalledCount: number;
stalledRows: StalledDealRow[];
- // Section 4 — urgent alerts (scalar counts only — no detail rows needed)
+ // Section 4, urgent alerts (scalar counts only, no detail rows needed)
hiddenProperties: number;
pendingKyc: number;
- // Section 5 — at-risk deals (top 5 by score)
+ // Section 5, at-risk deals (top 5 by score)
atRiskCount: number;
atRiskRows: AtRiskDealRow[];
}
@@ -110,7 +110,7 @@ export async function getMorningBriefingData(
_sum: { outstandingBalance: true },
}),
- // Overdue rows — top 5, oldest due-date first
+ // Overdue rows, top 5, oldest due-date first
prisma.transaction.findMany({
where: { companyId, paymentStatus: "OVERDUE" },
orderBy: { nextPaymentDueAt: "asc" },
@@ -123,7 +123,7 @@ export async function getMorningBriefingData(
},
}),
- // Today's inspections — confirmed or rescheduled
+ // Today's inspections, confirmed or rescheduled
prisma.inspectionBooking.findMany({
where: {
companyId,
@@ -139,7 +139,7 @@ export async function getMorningBriefingData(
},
}),
- // Stalled deals — in-progress, no activity for 7+ days
+ // Stalled deals, in-progress, no activity for 7+ days
prisma.transaction.findMany({
where: {
companyId,
@@ -211,7 +211,7 @@ export async function getMorningBriefingData(
};
const formattedOverdueRows: OverduePaymentRow[] = (overdueRows as OverdueRaw[]).map((row) => ({
- reservationRef: row.reservation?.reference ?? "—",
+ reservationRef: row.reservation?.reference ?? "-",
buyerName: `${row.user.firstName ?? ""} ${row.user.lastName ?? ""}`.trim() || "Buyer",
outstandingBalance: formatCurrency(dec(row.outstandingBalance)),
daysOverdue: row.nextPaymentDueAt ? differenceInDays(now, row.nextPaymentDueAt) : 0,
@@ -224,7 +224,7 @@ export async function getMorningBriefingData(
}));
const formattedStalledRows: StalledDealRow[] = (stalledRows as StalledRaw[]).map((row) => ({
- reservationRef: row.reservation?.reference ?? "—",
+ reservationRef: row.reservation?.reference ?? "-",
buyerName: `${row.user.firstName ?? ""} ${row.user.lastName ?? ""}`.trim() || "Buyer",
currentStage: stageLabel(row.currentStage),
daysSinceActivity: differenceInDays(now, row.updatedAt),
@@ -237,7 +237,7 @@ export async function getMorningBriefingData(
};
const formattedAtRiskRows: AtRiskDealRow[] = (atRiskRows as AtRiskRaw[]).map((row) => ({
- reservationRef: row.reservation?.reference ?? "—",
+ reservationRef: row.reservation?.reference ?? "-",
buyerName: `${row.user.firstName ?? ""} ${row.user.lastName ?? ""}`.trim() || "Buyer",
riskScore: row.riskScore,
topSignal: row.riskScore >= 80 ? "High risk" : row.riskScore >= 65 ? "Elevated risk" : "At risk",
diff --git a/src/modules/onboarding/checklist.ts b/src/modules/onboarding/checklist.ts
index 0391b7c..c39b5d9 100644
--- a/src/modules/onboarding/checklist.ts
+++ b/src/modules/onboarding/checklist.ts
@@ -5,13 +5,13 @@
* so the UI can render progress without fetching anything itself.
*
* Steps (in display order):
- * 1. Workspace created — always complete
- * 2. Company profile — logoUrl set + supportEmail set
- * 3. Branding published — publishedBrandingConfig written at least once
- * 4. First property listed — Property row exists for this company
- * 5. Team member added — TeamMember row exists for this company
- * 6. Payment account linked — CompanyPaymentProviderAccount row exists
- * 7. First deal created — Transaction row exists for this company
+ * 1. Workspace created , always complete
+ * 2. Company profile , logoUrl set + supportEmail set
+ * 3. Branding published , publishedBrandingConfig written at least once
+ * 4. First property listed , Property row exists for this company
+ * 5. Team member added , TeamMember row exists for this company
+ * 6. Payment account linked , CompanyPaymentProviderAccount row exists
+ * 7. First deal created , Transaction row exists for this company
*/
import { prisma } from "@/lib/db/prisma";
diff --git a/src/modules/properties/verification.ts b/src/modules/properties/verification.ts
index 455b853..c7b32d7 100644
--- a/src/modules/properties/verification.ts
+++ b/src/modules/properties/verification.ts
@@ -219,11 +219,11 @@ export function buildPropertyVerificationPresentation(
* Public visibility filter.
*
* `isPubliclyVisible` / `verificationStatus` are STORED values, written only
- * when a property is mutated — there is no sweep that ages them. The presented
+ * when a property is mutated, there is no sweep that ages them. The presented
* status, by contrast, is recomputed from `lastVerifiedAt` on every render. A
* listing that crossed the hide threshold therefore kept its stored "visible"
* flag and stayed on the public site while rendering the label "Listing
- * hidden" — the policy was enforced in the UI copy but not in the query.
+ * hidden", the policy was enforced in the UI copy but not in the query.
*
* Passing `hideBefore` (now - hideDays) applies the same rule the presentation
* uses, in SQL, so an unverified-for-too-long listing leaves the public site
diff --git a/src/modules/provisioning/provision-user.ts b/src/modules/provisioning/provision-user.ts
index 492b4fd..0db3f8b 100644
--- a/src/modules/provisioning/provision-user.ts
+++ b/src/modules/provisioning/provision-user.ts
@@ -112,7 +112,7 @@ async function resolveActorName(actorUserId: string): Promise {
}
export async function provisionCompanyUser(input: ProvisionUserInput): Promise {
- // 1. Role gating — enforce on every call, not just in the UI
+ // 1. Role gating, enforce on every call, not just in the UI
const allowed = allowedRolesForActor(input.actorRoles);
if (!allowed.includes(input.role)) {
return { ok: false, error: `Your role does not permit creating a ${input.role} account.` };
@@ -134,7 +134,7 @@ export async function provisionCompanyUser(input: ProvisionUserInput): Promise
input.status === "CONVERTED"
- ? `Hi ${firstName}, congratulations — your reservation has converted to a sale. Your team will guide you through the next steps.`
- : `Hi ${firstName}, good news — your reservation is now confirmed. Open your portal for the details.`,
+ ? `Hi ${firstName}, congratulations, your reservation has converted to a sale. Your team will guide you through the next steps.`
+ : `Hi ${firstName}, good news, your reservation is now confirmed. Open your portal for the details.`,
metadata: { reservationId } as Prisma.InputJsonValue,
});
}
diff --git a/src/modules/transactions/risk-scoring.ts b/src/modules/transactions/risk-scoring.ts
index 9c6833e..fd03763 100644
--- a/src/modules/transactions/risk-scoring.ts
+++ b/src/modules/transactions/risk-scoring.ts
@@ -47,7 +47,7 @@ export function computeRiskScore(input: {
failedPaymentCount: number;
/** Timestamp of the last admin follow-up, or null if never followed up. */
lastFollowedUpAt: Date | null;
- /** updatedAt of the Transaction row — proxy for last stage change. */
+ /** updatedAt of the Transaction row, proxy for last stage change. */
transactionUpdatedAt: Date;
/** Override "now" for testing. */
now?: Date;
diff --git a/src/proxy.ts b/src/proxy.ts
index 70afa0b..2fc5ee1 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -27,7 +27,7 @@ const isAuthAccessRoute = createRouteMatcher(["/auth/access(.*)"]);
* Nonce-based CSP, enforced in production.
*
* The nonce is forwarded on the REQUEST via the `content-security-policy`
- * header — Next.js reads the nonce from there and stamps it onto its own
+ * header, Next.js reads the nonce from there and stamps it onto its own
* inline/bootstrap scripts and /_next chunk tags. The same policy is then set
* on the RESPONSE so the browser enforces it. See src/lib/security/csp.ts for
* the policy itself and the browser-generation fallback strategy.