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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions e2e/health.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { expect, test } from "@playwright/test";

/**
* These checks call the API from Node, not from the browser, so Chromium's
* `--host-resolver-rules` mapping for the `<tenant>.localhost` host does not
* apply and Node's resolver would fail on platforms that do not resolve
* *.localhost (Windows). The health endpoints are not tenant-specific, so the
* local runs talk to loopback directly; a deployed target is used unchanged.
*/
function healthUrl(path: string) {
const base = process.env.E2E_BASE_URL ?? "http://acme-realty.localhost:3000";
const resolved = base.includes(".localhost") ? "http://127.0.0.1:3000" : base;
return `${resolved.replace(/\/$/, "")}${path}`;
}

/**
* Deployment health gates.
*
Expand All @@ -13,7 +26,7 @@ import { expect, test } from "@playwright/test";
*/

test("database has every migration the deployed code expects", async ({ request }) => {
const response = await request.get("/api/readyz");
const response = await request.get(healthUrl("/api/readyz"));

expect(
response.status(),
Expand All @@ -34,12 +47,12 @@ test("database has every migration the deployed code expects", async ({ request
});

test("liveness endpoint responds", async ({ request }) => {
const response = await request.get("/api/health");
const response = await request.get(healthUrl("/api/health"));
expect(response.status()).toBe(200);
});

test("readyz never leaks credentials", async ({ request }) => {
const response = await request.get("/api/readyz");
const response = await request.get(healthUrl("/api/readyz"));
const raw = await response.text();

// Host names are fine; secrets are not.
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/contracts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function UploadContractForm({ transactions }: { transactions: TransactionWithout
name="file"
accept=".pdf,application/pdf"
required
className="w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-800)] file:mr-3 file:rounded file:border-0 file:bg-[var(--sand-100)] file:px-2 file:py-0.5 file:text-xs file:font-medium"
className="w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-800)] file:mr-3 file:rounded file:border-0 file:bg-[var(--sand-100)] file:px-2 file:py-0.5 file:text-xs file:font-medium"
/>
</div>
<div className="flex items-end">
Expand Down
40 changes: 27 additions & 13 deletions src/app/(marketing)/properties/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,23 @@ export default async function PropertiesPage({
</Reveal>
<form className="space-y-4" method="GET">
<Card className="space-y-4 p-5">
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--ink-700)]">
{/* On a phone these six fields pushed every listing below the fold,
so they collapse behind a toggle and buyers see inventory first.
Checkbox + peer is used rather than <details> or JS so the panel
works server-rendered, and it is force-open from `md` up. The
checkbox has no name, so it is never submitted with the form. */}
<input type="checkbox" id="refine-listings" className="peer sr-only" />
<label
htmlFor="refine-listings"
className="admin-focus flex min-h-11 cursor-pointer items-center gap-2 text-sm font-semibold text-[var(--ink-700)] md:cursor-default"
>
<SlidersHorizontal className="h-4 w-4 text-[var(--brand-700)]" aria-hidden />
Refine listings
</div>
<div className="grid gap-4 lg:grid-cols-4">
<span className="ml-auto text-xs font-medium text-[var(--ink-500)] md:hidden">
Tap to {activeFilters.length > 0 ? "edit" : "filter"}
</span>
</label>
<div className="hidden grid-cols-1 gap-4 peer-checked:grid md:grid lg:grid-cols-4">
<PropertyLocationSearch
defaultLocation={filters.location}
defaultLatitude={filters.latitude}
Expand Down Expand Up @@ -98,22 +110,24 @@ export default async function PropertiesPage({
<option value="RESERVED">Reserved</option>
<option value="SOLD">Sold</option>
</Select>
<label className="flex items-center gap-3 rounded-2xl border border-[var(--line)] px-4 text-sm text-[var(--ink-700)]">
<input type="checkbox" name="hasPaymentPlan" value="true" defaultChecked={Boolean(filters.hasPaymentPlan)} />
<label className="flex min-h-11 items-center gap-3 rounded-2xl border border-[var(--line)] px-4 py-3 text-sm text-[var(--ink-700)]">
<input type="checkbox" name="hasPaymentPlan" value="true" className="h-5 w-5" defaultChecked={Boolean(filters.hasPaymentPlan)} />
Payment plan available
</label>
<label className="flex items-center gap-3 rounded-2xl border border-[var(--line)] px-4 text-sm text-[var(--ink-700)]">
<input type="checkbox" name="featured" value="true" defaultChecked={Boolean(filters.featured)} />
<label className="flex min-h-11 items-center gap-3 rounded-2xl border border-[var(--line)] px-4 py-3 text-sm text-[var(--ink-700)]">
<input type="checkbox" name="featured" value="true" className="h-5 w-5" defaultChecked={Boolean(filters.featured)} />
Featured only
</label>
</div>
{/* Sibling of the peer checkbox so the actions collapse with the
fields they apply to, instead of floating above the listings. */}
<div className="hidden flex-wrap gap-3 peer-checked:flex md:flex">
<Button type="submit">Apply filters</Button>
<Link href="/properties">
<Button type="button" variant="outline">Reset</Button>
</Link>
</div>
</Card>
<div className="flex flex-wrap gap-3">
<Button type="submit">Apply filters</Button>
<Link href="/properties">
<Button type="button" variant="outline">Reset</Button>
</Link>
</div>
</form>
<div className="flex flex-wrap items-center gap-3">
<span className="text-sm font-semibold text-[var(--ink-950)]">
Expand Down
4 changes: 2 additions & 2 deletions src/app/(marketing)/testimonials/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export default async function TestimonialsPage({
</option>
))}
</Select>
<input name="year" defaultValue={filters.year} placeholder="Year" className="admin-focus rounded-[var(--radius-md)] border border-[var(--line)] px-3 py-2 text-sm" />
<input name="month" defaultValue={filters.month} placeholder="Month" className="admin-focus rounded-[var(--radius-md)] border border-[var(--line)] px-3 py-2 text-sm" />
<input name="year" defaultValue={filters.year} placeholder="Year" inputMode="numeric" className="admin-focus min-h-11 rounded-[var(--radius-md)] border border-[var(--line)] px-3 py-2 text-base sm:text-sm" />
<input name="month" defaultValue={filters.month} placeholder="Month" inputMode="numeric" className="admin-focus min-h-11 rounded-[var(--radius-md)] border border-[var(--line)] px-3 py-2 text-base sm:text-sm" />
<button className="admin-interactive admin-focus rounded-full bg-[var(--brand-700)] px-5 py-2 text-sm font-semibold text-white" type="submit">
Filter
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/add-buyer-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { ProvisionUserResult } from "@/modules/provisioning/provision-user"
const INITIAL: ProvisionUserResult | null = null;

const inputCls =
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";

const labelCls = "block text-xs font-medium uppercase tracking-[0.12em] text-[var(--ink-500)]";

Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/announcement-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Select } from "@/components/ui/select";
const INITIAL: AnnouncementFormState = { ok: false, error: null };

const inputClass =
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";

function PostButton() {
const { pending } = useFormStatus();
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/branding-management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ function ColorField({
<Field label={label}>
<div className="flex min-w-0 items-center gap-3 rounded-[var(--radius-md)] border border-[var(--border-subtle,var(--line))] bg-white px-3 py-2 focus-within:border-[var(--brand-500)] focus-within:ring-2 focus-within:ring-[var(--brand-500)]">
<input type="color" className="h-8 w-10 rounded-lg border-0 bg-transparent p-0" value={pickerValue} onChange={(event) => onChange(event.target.value.toUpperCase())} />
<input className="numeric h-9 min-w-0 flex-1 bg-transparent text-sm outline-none" value={value} onChange={(event) => onChange(event.target.value.toUpperCase())} />
<input className="numeric h-9 min-w-0 flex-1 bg-transparent text-base sm:text-sm outline-none" value={value} onChange={(event) => onChange(event.target.value.toUpperCase())} />
</div>
</Field>
);
Expand Down
7 changes: 5 additions & 2 deletions src/components/admin/deal-board-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,11 @@ export function DealBoardView({
) : null}

<div className="grid gap-6 2xl:grid-cols-[minmax(0,1fr)_340px]">
<div className="overflow-x-auto pb-2">
<div className="grid min-w-[960px] gap-4 xl:grid-cols-2 3xl:grid-cols-3">
{/* The 960px floor is what keeps the stage columns side by side, but on
a phone it forced the whole board to scroll sideways inside a 335px
viewport. Below `md` the stages stack vertically instead. */}
<div className="pb-2 md:overflow-x-auto">
<div className="grid gap-4 md:min-w-[960px] xl:grid-cols-2 3xl:grid-cols-3">
{visibleColumns.map((column) => (
<Card key={column.key} className={cn("min-w-0 rounded-[32px] p-5", stageTone[column.key].columnClass)}>
{inspect && column.key === "OVERDUE" ? (
Expand Down
6 changes: 3 additions & 3 deletions src/components/admin/front-desk-logbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { WhatsAppButton } from "@/components/shared/whatsapp-button";
import { Select } from "@/components/ui/select";

const inputClass =
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
const buttonClass =
"admin-focus inline-flex items-center justify-center gap-1.5 rounded-[var(--radius-md)] bg-[var(--brand-700)] px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-[var(--brand-800,#15803d)]";

Expand Down Expand Up @@ -45,7 +45,7 @@ export function FrontDeskLogbookView({ logbook }: { logbook: FrontDeskLogbook })
</h2>
<form action={logVisitorAction} className="mt-4 grid gap-3 sm:grid-cols-2">
<input name="fullName" required placeholder="Full name *" className={`${inputClass} sm:col-span-2`} aria-label="Visitor full name" />
<input name="phone" placeholder="Phone" className={inputClass} aria-label="Visitor phone" />
<input name="phone" type="tel" autoComplete="tel" placeholder="Phone" className={inputClass} aria-label="Visitor phone" />
<input name="hostName" placeholder="Here to see" className={inputClass} aria-label="Host name" />
<input name="purpose" placeholder="Purpose of visit" className={`${inputClass} sm:col-span-2`} aria-label="Purpose of visit" />
<div className="sm:col-span-2">
Expand Down Expand Up @@ -105,7 +105,7 @@ export function FrontDeskLogbookView({ logbook }: { logbook: FrontDeskLogbook })
</h2>
<form action={logCallAction} className="mt-4 grid gap-3 sm:grid-cols-2">
<input name="callerName" required placeholder="Caller name *" className={inputClass} aria-label="Caller name" />
<input name="phone" placeholder="Phone" className={inputClass} aria-label="Caller phone" />
<input name="phone" type="tel" autoComplete="tel" placeholder="Phone" className={inputClass} aria-label="Caller phone" />
<Select name="direction" defaultValue="INBOUND" className="w-full" aria-label="Call direction">
<option value="INBOUND">Inbound</option>
<option value="OUTBOUND">Outbound</option>
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/invoice-create-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createInvoiceAction, type InvoiceFormState } from "@/modules/invoices/a
const INITIAL: InvoiceFormState = { ok: false, error: null };

const inputClass =
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";

function SubmitButton() {
const { pending } = useFormStatus();
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/users-management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { AppRole } from "@prisma/client";
const PROVISION_INITIAL: ProvisionUserResult | null = null;

const inputCls =
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
const labelCls = "block text-xs font-medium uppercase tracking-[0.12em] text-[var(--ink-500)]";

const PROVISIONABLE_ROLES: Array<{ value: AppRole; label: string }> = [
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/quick-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const ICONS: Record<QuickActionIcon, LucideIcon> = {
};

const triggerClass =
"group relative grid h-10 w-10 cursor-pointer place-items-center rounded-[var(--radius-md)] border border-[var(--line)] bg-[var(--surface-1,#fff)] text-[var(--ink-600)] shadow-[var(--shadow-sm)] transition-[colors,transform] duration-200 hover:-translate-y-0.5 hover:border-[var(--brand-300)] hover:text-[var(--brand-700)] hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-500)] focus-visible:ring-offset-2 motion-reduce:transition-none motion-reduce:hover:translate-y-0";
"group relative grid h-11 w-11 cursor-pointer place-items-center sm:h-10 sm:w-10 rounded-[var(--radius-md)] border border-[var(--line)] bg-[var(--surface-1,#fff)] text-[var(--ink-600)] shadow-[var(--shadow-sm)] transition-[colors,transform] duration-200 hover:-translate-y-0.5 hover:border-[var(--brand-300)] hover:text-[var(--brand-700)] hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-500)] focus-visible:ring-offset-2 motion-reduce:transition-none motion-reduce:hover:translate-y-0";

const tooltipClass =
"pointer-events-none absolute -bottom-9 left-1/2 z-20 -translate-x-1/2 whitespace-nowrap rounded-[var(--radius-sm,6px)] bg-slate-900 px-2 py-1 text-xs font-medium text-white opacity-0 shadow-md transition-opacity duration-150 group-hover:opacity-100 group-focus-visible:opacity-100 motion-reduce:transition-none";
Expand Down
6 changes: 3 additions & 3 deletions src/components/dashboard/quick-create-forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
const QUICK_FORM_INITIAL: QuickFormState = { ok: false, error: null };

const inputClass =
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";
"admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]";

function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus();
Expand Down Expand Up @@ -46,7 +46,7 @@ export function VisitorQuickForm({ onSuccess }: { onSuccess: () => void }) {
return (
<form action={formAction} className="grid gap-3">
<input name="fullName" required placeholder="Full name *" className={inputClass} aria-label="Visitor full name" />
<input name="phone" placeholder="Phone" className={inputClass} aria-label="Visitor phone" />
<input name="phone" type="tel" autoComplete="tel" placeholder="Phone" className={inputClass} aria-label="Visitor phone" />
<input name="hostName" placeholder="Here to see" className={inputClass} aria-label="Host name" />
<input name="purpose" placeholder="Purpose of visit" className={inputClass} aria-label="Purpose of visit" />
<ErrorNote message={state.error} />
Expand All @@ -66,7 +66,7 @@ export function LeadQuickForm({ onSuccess }: { onSuccess: () => void }) {
<form action={formAction} className="grid gap-3">
<input name="fullName" required placeholder="Full name *" className={inputClass} aria-label="Lead full name" />
<input name="email" type="email" required placeholder="Email *" className={inputClass} aria-label="Lead email" />
<input name="phone" placeholder="Phone" className={inputClass} aria-label="Lead phone" />
<input name="phone" type="tel" autoComplete="tel" placeholder="Phone" className={inputClass} aria-label="Lead phone" />
<textarea
name="message"
required
Expand Down
Loading
Loading