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
90 changes: 54 additions & 36 deletions src/app/(marketing)/careers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,82 @@
import { HeartHandshake, Sparkles, TrendingUp } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";

import { Container } from "@/components/shared/container";
import { Reveal } from "@/components/shared/reveal";
import { SectionHeading } from "@/components/shared/section-heading";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { buildAuthRedirect, buildServerDomainConfig } from "@/lib/domains";
import { env } from "@/lib/env";
import { requirePublicTenantContext } from "@/lib/tenancy/context";
import { getPublicTenantPresentation } from "@/modules/branding/service";
import { resolveTenantSiteContent } from "@/modules/cms/site-content";
import { getPublishedSiteContent } from "@/modules/cms/site-content-service";

const VALUES = [
{
icon: TrendingUp,
title: "Real ownership",
body: "Small, senior team where your work ships to real buyers and sellers — not a backlog that never sees daylight.",
},
{
icon: HeartHandshake,
title: "Trust by default",
body: "We win by making property transactions feel safe and transparent. That standard shapes how we work together too.",
},
{
icon: Sparkles,
title: "Craft that shows",
body: "We care about the details buyers feel — clarity, speed, and follow-through — across every part of the experience.",
},
];
export const dynamic = "force-dynamic";

/** Fixed icon set for the three value cards; copy comes from the tenant CMS. */
const VALUE_ICONS = [TrendingUp, HeartHandshake, Sparkles] as const;

export default async function CareersPage() {
const tenant = await requirePublicTenantContext();
const [presentation, storedContent] = await Promise.all([
getPublicTenantPresentation(tenant),
getPublishedSiteContent(tenant),
]);
const runtimeConfig = buildServerDomainConfig(env);
const siteContent = resolveTenantSiteContent({
companyName: presentation.companyName,
startPurchaseHref: buildAuthRedirect(runtimeConfig, {
returnTo: "/portal",
tenantSlug: tenant.companySlug,
tenantHost: tenant.host,
entry: "buyer",
}),
stored: storedContent,
});

// Companies that aren't hiring can hide the page entirely from the CMS.
if (!siteContent.careers.visible) {
notFound();
}

const careers = siteContent.careers;

export default function CareersPage() {
return (
<Container className="space-y-12 py-16">
<Reveal>
<SectionHeading
eyebrow="Careers & Partnerships"
title="Help us make property transactions something people trust."
description="We're building the operating system for how homes are marketed, sold, and paid for. If that sounds like work worth doing, we'd love to hear from you."
eyebrow={careers.eyebrow}
title={careers.title}
description={careers.intro}
/>
</Reveal>

<Reveal className="grid gap-4 sm:grid-cols-3">
{VALUES.map((value) => (
<Card key={value.title} className="p-6">
<span className="flex h-11 w-11 items-center justify-center rounded-[var(--radius-md)] bg-[var(--brand-100,#dcfce7)] text-[var(--brand-700)]">
<value.icon className="h-5 w-5" aria-hidden />
</span>
<h3 className="mt-4 text-lg font-semibold text-[var(--ink-950)]">{value.title}</h3>
<p className="mt-2 text-sm leading-7 text-[var(--ink-600)]">{value.body}</p>
</Card>
))}
{careers.values.map((value, index) => {
const Icon = VALUE_ICONS[index] ?? Sparkles;
return (
<Card key={value.title} className="p-6">
<span className="flex h-11 w-11 items-center justify-center rounded-[var(--radius-md)] bg-[var(--brand-100,#dcfce7)] text-[var(--brand-700)]">
<Icon className="h-5 w-5" aria-hidden />
</span>
<h3 className="mt-4 text-lg font-semibold text-[var(--ink-950)]">{value.title}</h3>
<p className="mt-2 text-sm leading-7 text-[var(--ink-600)]">{value.description}</p>
</Card>
);
})}
</Reveal>

<Reveal>
<Card className="flex flex-col items-start gap-4 p-8 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 className="text-xl font-semibold text-[var(--ink-950)]">No open roles right now</h3>
<p className="mt-1 text-sm leading-7 text-[var(--ink-600)]">
We&apos;re not actively hiring at the moment, but we always like meeting great people. Tell us
how you&apos;d like to contribute and we&apos;ll keep you in mind.
</p>
<h3 className="text-xl font-semibold text-[var(--ink-950)]">{careers.ctaHeading}</h3>
<p className="mt-1 text-sm leading-7 text-[var(--ink-600)]">{careers.ctaBody}</p>
</div>
<Link href="/contact" className="shrink-0">
<Button>Introduce yourself</Button>
<Button>{careers.ctaLabel}</Button>
</Link>
</Card>
</Reveal>
Expand Down
41 changes: 36 additions & 5 deletions src/app/(marketing)/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import { Mail, MapPin, Phone, type LucideIcon } from "lucide-react";
import { Clock, Mail, MapPin, MessageCircle, Phone, type LucideIcon } from "lucide-react";

import { InquiryForm } from "@/components/marketing/inquiry-form";
import { Container } from "@/components/shared/container";
import { Reveal } from "@/components/shared/reveal";
import { SectionHeading } from "@/components/shared/section-heading";
import { Card } from "@/components/ui/card";
import { buildAuthRedirect, buildServerDomainConfig } from "@/lib/domains";
import { env } from "@/lib/env";
import { requirePublicTenantContext } from "@/lib/tenancy/context";
import { getPublicTenantContact } from "@/modules/cms/site-content-service";
import { resolveTenantSiteContent } from "@/modules/cms/site-content";
import {
getPublicTenantContact,
getPublishedSiteContent,
} from "@/modules/cms/site-content-service";
import { getPublicTenantPresentation } from "@/modules/branding/service";

export const dynamic = "force-dynamic";

export default async function ContactPage() {
const tenant = await requirePublicTenantContext();
const contact = await getPublicTenantContact(tenant);
const [contact, presentation, storedContent] = await Promise.all([
getPublicTenantContact(tenant),
getPublicTenantPresentation(tenant),
getPublishedSiteContent(tenant),
]);
const runtimeConfig = buildServerDomainConfig(env);
const siteContent = resolveTenantSiteContent({
companyName: presentation.companyName,
startPurchaseHref: buildAuthRedirect(runtimeConfig, {
returnTo: "/portal",
tenantSlug: tenant.companySlug,
tenantHost: tenant.host,
entry: "buyer",
}),
stored: storedContent,
});

const methods: { icon: LucideIcon; label: string; value: string; href?: string }[] = [];
if (contact.address) {
Expand All @@ -29,14 +51,23 @@ export default async function ContactPage() {
href: `tel:${contact.phone.replace(/[^+\d]/g, "")}`,
});
}
if (siteContent.social.whatsapp) {
methods.push({
icon: MessageCircle,
label: "WhatsApp",
value: siteContent.social.whatsapp,
href: `https://wa.me/${siteContent.social.whatsapp.replace(/[^\d]/g, "")}`,
});
}
methods.push({ icon: Clock, label: "Office hours", value: siteContent.contact.hours });

return (
<Container className="grid items-start gap-8 py-16 lg:grid-cols-[1fr_0.9fr]">
<Reveal className="space-y-6">
<SectionHeading
eyebrow="Contact"
title="Talk to a team that treats the transaction like a product."
description="Use the inquiry form for purchase intent, partnership conversations, or serious buyer support."
title={`Talk to the ${presentation.companyName} team.`}
description={siteContent.contact.note}
/>
{methods.length > 0 ? (
<Card className="divide-y divide-[var(--line)] p-2">
Expand Down
22 changes: 19 additions & 3 deletions src/app/(marketing)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,38 @@ import { notFound } from "next/navigation";

import { TenantPublicShell } from "@/components/marketing/tenant-public-shell";
import { getPublishedTenantBranding, getTenantPresentation } from "@/modules/branding/service";
import { resolveTenantSiteContent } from "@/modules/cms/site-content";
import { getPublishedSiteContent } from "@/modules/cms/site-content-service";
import { requirePublicTenantContext } from "@/lib/tenancy/context";

export const dynamic = "force-dynamic";

export async function generateMetadata(): Promise<Metadata> {
try {
const tenant = await requirePublicTenantContext();
const [branding, presentation] = await Promise.all([
const [branding, presentation, storedContent] = await Promise.all([
getPublishedTenantBranding(tenant),
getTenantPresentation(tenant),
getPublishedSiteContent(tenant),
]);
const name = presentation.companyName;
// Tenant-authored SEO title/description with company-derived fallbacks.
const siteContent = resolveTenantSiteContent({
companyName: name,
startPurchaseHref: "/portal",
stored: storedContent,
});

return {
title: { default: name, template: `%s \u00b7 ${name}` },
description: `${name} — property listings, viewings, and secure transactions.`,
title: { default: siteContent.seo.title, template:`%s \u00b7 ${name}` },
description: siteContent.seo.description,
openGraph: {
title: siteContent.seo.title,
description: siteContent.seo.description,
siteName: name,
type: "website",
...(branding.heroImageUrl ? { images: [{ url: branding.heroImageUrl }] } : {}),
},
icons: branding.faviconUrl ? { icon: branding.faviconUrl } : undefined,
};
} catch {
Expand Down
Loading
Loading