Full-bleed photo hero with highlights and a property search bar (from Marbrook)
"use client";
import { MapPin, Search } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import type React from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsList, TabsTab } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import type { HeroContent } from "../_types/hero";
import { defaultHero9Content } from "./data";
const eyebrowClass =
"inline-flex items-center gap-2 font-medium text-[0.6875rem] uppercase tracking-[0.22em]";
const bareSelectTrigger =
"min-h-0 w-full border-none bg-transparent px-0 text-base text-primary shadow-none before:shadow-none focus-visible:ring-0 sm:min-h-0 sm:text-base dark:bg-transparent";
function SearchField({
children,
className,
label,
}: {
children: React.ReactNode;
className?: string;
label: string;
}) {
return (
<div
className={cn(
"flex flex-col justify-center gap-1.5 border-border px-5 py-4 lg:px-6",
className,
)}
>
<span className={cn(eyebrowClass, "text-muted-foreground/70")}>
{label}
</span>
{children}
</div>
);
}
export default function Hero9({
content = defaultHero9Content,
onSearch,
}: {
content?: HeroContent;
/** Receives the active tab and every field's chosen option, by label. */
onSearch?: (query: { tab?: string; values: Record<string, string> }) => void;
}) {
const { search } = content;
const [tab, setTab] = useState(search?.tabs?.[0]?.value);
const [values, setValues] = useState<Record<string, string>>(() =>
Object.fromEntries(
(search?.fields ?? []).map((field) => [field.label, field.options[0]]),
),
);
const [primaryAction, secondaryAction] = content.actions;
return (
<section
className={cn(
"relative isolate flex min-h-[94vh] flex-col justify-end pt-28",
search ? "mb-12 lg:mb-14" : "pb-10",
)}
>
<div className="absolute inset-0 -z-10 overflow-hidden">
<Image
alt={content.image.alt}
className="object-cover object-center"
fill
priority
sizes="100vw"
src={content.image.src}
/>
<div className="absolute inset-0 bg-linear-to-t from-primary/90 via-primary/45 to-primary/25" />
{/* Second pass from the left keeps the headline legible over bright façades. */}
<div className="absolute inset-0 bg-linear-to-r from-primary/70 via-primary/20 to-transparent" />
</div>
<div className="mx-auto w-full max-w-7xl px-5 pb-10 sm:px-8 lg:px-12 lg:pb-14">
<div className="flex flex-col gap-10 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-2xl">
<p className={cn(eyebrowClass, "text-white/70")}>
<span className="size-1 bg-secondary" />
{content.eyebrow}
</p>
<h1 className="mt-6 font-display text-5xl text-white leading-[1.02] sm:text-6xl lg:text-7xl">
{content.title}
{content.titleAccent && (
<span className="block italic">{content.titleAccent}</span>
)}
</h1>
<p className="mt-6 max-w-lg text-base text-white/75 leading-relaxed">
{content.description}
</p>
<div className="mt-9 flex flex-wrap items-center gap-3">
{primaryAction && (
<Button
className="border-none bg-white px-7 text-primary shadow-none hover:bg-white/90"
render={<Link href={primaryAction.href} />}
size="xl"
>
{primaryAction.label}
</Button>
)}
{secondaryAction && (
<Button
className="border-white/40 bg-transparent px-7 text-white shadow-none before:shadow-none hover:bg-white/10 dark:bg-transparent"
render={<Link href={secondaryAction.href} />}
size="xl"
variant="outline"
>
{secondaryAction.label}
</Button>
)}
</div>
</div>
{content.highlights && content.highlights.length > 0 && (
<dl className="grid grid-cols-3 gap-8 border-white/20 border-t pt-6 lg:shrink-0 lg:gap-x-10 lg:border-t-0 lg:border-l lg:pt-0 lg:pl-8">
{content.highlights.map((highlight) => (
<div key={highlight.label}>
<dd className="font-display text-3xl text-white lg:text-4xl">
{highlight.value}
</dd>
<dt className="mt-1 text-white/60 text-xs leading-snug">
{highlight.label}
</dt>
</div>
))}
</dl>
)}
</div>
</div>
{search && (
<div className="relative z-10 mx-auto -mb-12 w-full max-w-7xl px-5 sm:px-8 lg:-mb-14 lg:px-12">
{search.tabs && search.tabs.length > 0 && (
<Tabs onValueChange={(value) => setTab(String(value))} value={tab}>
<TabsList
className="w-fit gap-0 rounded-b-none bg-white/10 p-0 backdrop-blur-sm"
variant="default"
>
{search.tabs.map((item) => (
<TabsTab
className="h-11 rounded-b-none px-5 text-sm text-white/70 data-active:text-primary sm:h-11 sm:text-sm"
key={item.value}
value={item.value}
>
{item.label}
</TabsTab>
))}
</TabsList>
</Tabs>
)}
<form
className="grid grid-cols-1 overflow-hidden rounded-lg rounded-tl-none bg-card shadow-2xl shadow-primary/30 sm:grid-cols-2 sm:divide-x sm:divide-border lg:grid-cols-[1.2fr_1fr_1fr_auto]"
onSubmit={(event) => {
event.preventDefault();
onSearch?.({ tab, values });
}}
>
{search.fields.map((field, index) => (
<SearchField
className={cn(
"border-b sm:border-b-0",
index >= 2 && "sm:border-t lg:border-t-0",
)}
key={field.label}
label={field.label}
>
<div className="flex items-center gap-2">
{field.icon === "pin" && (
<MapPin className="size-4 shrink-0 text-muted-foreground" />
)}
<Select
onValueChange={(value) =>
setValues((current) => ({
...current,
[field.label]: String(value ?? field.options[0]),
}))
}
value={values[field.label]}
>
<SelectTrigger
aria-label={field.label}
className={bareSelectTrigger}
>
<SelectValue />
</SelectTrigger>
<SelectPopup>
{field.options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectPopup>
</Select>
</div>
</SearchField>
))}
<div className="flex items-stretch p-3 sm:col-span-2 lg:col-span-1 lg:p-4">
<Button
className="h-full w-full gap-2 border-none bg-primary px-8 text-primary-foreground shadow-none hover:bg-primary/90 max-lg:h-12"
size="xl"
type="submit"
>
<Search className="size-4" />
{search.submitLabel}
</Button>
</div>
</form>
</div>
)}
</section>
);
}