deploy: polish public page formatting
This commit is contained in:
parent
7144aa4943
commit
656b78bf8e
8 changed files with 500 additions and 307 deletions
|
|
@ -10,10 +10,18 @@ import { FAQSection } from "@/components/faq-section"
|
|||
import { ContactPage } from "@/components/contact-page"
|
||||
import { AboutPage } from "@/components/about-page"
|
||||
import { WhoWeServePage } from "@/components/who-we-serve-page"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicProse,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import {
|
||||
generateLocationPageMetadata,
|
||||
LocationLandingPage,
|
||||
} from "@/components/location-landing-page"
|
||||
import Link from "next/link"
|
||||
|
||||
// Required for static export - ensures this route is statically generated
|
||||
export const dynamic = "force-static"
|
||||
|
|
@ -48,6 +56,7 @@ const routeMapping: Record<string, string> = {
|
|||
|
||||
// Food & Beverage
|
||||
"food-and-beverage/healthy-options": "healthy-vending",
|
||||
"food-and-beverage/snack-and-drink-delivery": "snack-and-drink-delivery",
|
||||
"food-and-beverage/traditional-options": "traditional-vending",
|
||||
"food-and-beverage/suppliers":
|
||||
"diverse-vending-options-with-rocky-mountain-vendings-exclusive-wholesale-accounts",
|
||||
|
|
@ -348,6 +357,7 @@ export default async function WordPressPage({ params }: PageProps) {
|
|||
"vending-machines-for-your-car-wash",
|
||||
]
|
||||
const isWhoWeServePage = whoWeServeSlugs.includes(pageSlug)
|
||||
const routePath = `/${slugArray.join("/")}`
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -377,13 +387,58 @@ export default async function WordPressPage({ params }: PageProps) {
|
|||
pageSlug !== "contact-us" &&
|
||||
pageSlug !== "about-us" &&
|
||||
!isWhoWeServePage && (
|
||||
<article className="container mx-auto px-4 py-8 md:py-12 max-w-4xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6">
|
||||
{page.title || "Page"}
|
||||
</h1>
|
||||
</header>
|
||||
{content}
|
||||
<article className="container mx-auto max-w-5xl px-4 py-10 md:py-14">
|
||||
<Breadcrumbs
|
||||
className="mb-6"
|
||||
items={[
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: page.title || "Page", href: routePath },
|
||||
]}
|
||||
/>
|
||||
<PublicPageHeader
|
||||
eyebrow={pageSlug.startsWith("blog") ? "Article" : "Information"}
|
||||
title={page.title || "Page"}
|
||||
description={
|
||||
page.seoDescription ||
|
||||
page.excerpt ||
|
||||
"Explore the details, service guidance, and next steps from Rocky Mountain Vending."
|
||||
}
|
||||
className="mx-auto mb-10 max-w-3xl text-center"
|
||||
align="center"
|
||||
/>
|
||||
<PublicSurface className="p-5 md:p-7 lg:p-9">
|
||||
<PublicProse className="mx-auto max-w-3xl">{content}</PublicProse>
|
||||
</PublicSurface>
|
||||
<PublicInset className="mx-auto mt-8 max-w-4xl border-primary/12 bg-[linear-gradient(180deg,rgba(41,160,71,0.06),rgba(255,255,255,0.84))] p-5 md:p-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Need Help Choosing The Right Next Step?
|
||||
</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
|
||||
Talk with Rocky Mountain Vending
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
|
||||
Reach out about placement, machine sales, repairs, moving help,
|
||||
manuals, or parts and we'll point you in the right direction.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
className="inline-flex min-h-11 items-center justify-center rounded-full bg-primary px-5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
|
||||
>
|
||||
Talk to Our Team
|
||||
</Link>
|
||||
<Link
|
||||
href="/#request-machine"
|
||||
className="inline-flex min-h-11 items-center justify-center rounded-full border border-border bg-white px-5 text-sm font-medium text-foreground transition hover:border-primary/35 hover:text-primary"
|
||||
>
|
||||
See If You Qualify
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</PublicInset>
|
||||
</article>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@ import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
|||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicProse,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
const WORDPRESS_SLUG = "abandoned-vending-machines"
|
||||
|
||||
|
|
@ -81,17 +88,61 @@ export default async function AbandonedVendingMachinesPage() {
|
|||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||
/>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{
|
||||
label: page.title || "Abandoned Vending Machines",
|
||||
href: "/blog/abandoned-vending-machines",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<article className="container mx-auto px-4 py-8 md:py-12 max-w-4xl">
|
||||
{content}
|
||||
<article className="container mx-auto max-w-5xl px-4 py-10 md:py-14">
|
||||
<Breadcrumbs
|
||||
className="mb-6"
|
||||
items={[
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{
|
||||
label: page.title || "Abandoned Vending Machines",
|
||||
href: "/blog/abandoned-vending-machines",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<PublicPageHeader
|
||||
eyebrow="Article"
|
||||
title={page.title || "Abandoned Vending Machines"}
|
||||
description={
|
||||
page.seoDescription ||
|
||||
page.excerpt ||
|
||||
"Guidance, next steps, and practical considerations from Rocky Mountain Vending."
|
||||
}
|
||||
align="center"
|
||||
className="mx-auto mb-10 max-w-3xl"
|
||||
/>
|
||||
<PublicSurface className="p-5 md:p-7 lg:p-9">
|
||||
<PublicProse className="mx-auto max-w-3xl">{content}</PublicProse>
|
||||
</PublicSurface>
|
||||
<PublicInset className="mx-auto mt-8 max-w-4xl border-primary/12 bg-[linear-gradient(180deg,rgba(41,160,71,0.06),rgba(255,255,255,0.84))] p-5 md:p-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Need Help With A Machine Situation?
|
||||
</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
|
||||
Get the right kind of support quickly
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
|
||||
Reach out if you need help with abandoned machines, service questions,
|
||||
moving help, or figuring out the right next step for your location.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
className="inline-flex min-h-11 items-center justify-center rounded-full bg-primary px-5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
|
||||
>
|
||||
Talk to Our Team
|
||||
</Link>
|
||||
<Link
|
||||
href="/services/repairs"
|
||||
className="inline-flex min-h-11 items-center justify-center rounded-full border border-border bg-white px-5 text-sm font-medium text-foreground transition hover:border-primary/35 hover:text-primary"
|
||||
>
|
||||
Explore Repair Help
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</PublicInset>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Breadcrumbs, type BreadcrumbItem } from "@/components/breadcrumbs"
|
|||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicProse,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
|
@ -64,19 +65,19 @@ export function DropdownPageShell({
|
|||
</PublicPageHeader>
|
||||
|
||||
{contentIntro ? (
|
||||
<section className="mt-10 grid gap-4 lg:grid-cols-2">{contentIntro}</section>
|
||||
<section className="mt-10 grid gap-5 lg:grid-cols-2">{contentIntro}</section>
|
||||
) : null}
|
||||
|
||||
<section className={cn(contentIntro ? "mt-6" : "mt-10")}>
|
||||
<section className={cn(contentIntro ? "mt-8" : "mt-10")}>
|
||||
<PublicSurface
|
||||
className={cn(
|
||||
"relative overflow-hidden p-0 md:p-0",
|
||||
contentSurfaceClassName
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 h-18 bg-[radial-gradient(circle_at_top,rgba(41,160,71,0.11),transparent_72%)]" />
|
||||
<div className="relative p-5 md:p-7 lg:p-9">
|
||||
<div className="mb-6 flex items-center justify-between gap-4 border-b border-border/55 pb-4">
|
||||
<div className="absolute inset-x-0 top-0 h-24 bg-[radial-gradient(circle_at_top,rgba(41,160,71,0.09),transparent_74%)]" />
|
||||
<div className="relative p-5 md:p-7 lg:p-10">
|
||||
<div className="mb-8 flex items-center justify-between gap-4 border-b border-border/55 pb-5">
|
||||
<div>
|
||||
<p className="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-primary/80">
|
||||
Location Guide
|
||||
|
|
@ -87,15 +88,17 @@ export function DropdownPageShell({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("mx-auto max-w-3xl", contentClassName)}>{content}</div>
|
||||
<PublicProse className={cn("mx-auto max-w-3xl", contentClassName)}>
|
||||
{content}
|
||||
</PublicProse>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</section>
|
||||
|
||||
{sections ? <div className="mt-12 space-y-12">{sections}</div> : null}
|
||||
{sections ? <div className="mt-14 space-y-14">{sections}</div> : null}
|
||||
|
||||
{cta ? (
|
||||
<section className="mt-12">
|
||||
<section className="mt-14">
|
||||
<PublicSurface className="overflow-hidden text-center">
|
||||
<div className="absolute inset-x-0 top-0 h-20 bg-[radial-gradient(circle_at_top,rgba(41,160,71,0.10),transparent_70%)]" />
|
||||
{cta.eyebrow ? (
|
||||
|
|
|
|||
|
|
@ -114,11 +114,52 @@ export function LocationLandingPage({
|
|||
}: {
|
||||
locationData: LocationData
|
||||
}) {
|
||||
const isSaltLakeCity = locationData.slug === "salt-lake-city-utah"
|
||||
const countyName = getCountyName(locationData.slug)
|
||||
const industries = getIndustryFocus(locationData)
|
||||
const canonicalUrl = buildAbsoluteUrl(buildLocationRoute(locationData.slug))
|
||||
const title = `Vending Machines in ${locationData.city}, ${locationData.stateAbbr}`
|
||||
const description = `Rocky Mountain Vending provides free placement for qualifying locations, machine sales, repairs, and vending service for businesses in ${locationData.city}, ${locationData.stateAbbr}.`
|
||||
const comparisonRows = [
|
||||
["Credit card readers", "Yes", "Maybe", "Yes"],
|
||||
["Locally owned", "Yes", "Yes", "Maybe"],
|
||||
["Fast service", "Yes", "Maybe", "Maybe"],
|
||||
["Large selection of products", "Yes", "Maybe", "Yes"],
|
||||
["Quality of equipment used", "Excellent", "Varies", "Excellent"],
|
||||
["Locked into Coke or Pepsi equipment", "No", "Maybe", "Probably"],
|
||||
]
|
||||
const saltLakeServiceLinks = [
|
||||
{
|
||||
title: "Traditional snacks and drinks",
|
||||
body: "Stock the machine with the classic snacks, sodas, and convenience items most locations still want every day.",
|
||||
href: "/food-and-beverage/traditional-options",
|
||||
},
|
||||
{
|
||||
title: "Healthy snacks and drinks",
|
||||
body: "Offer protein bars, better-for-you snacks, and drink choices that fit health-conscious teams and customers.",
|
||||
href: "/food-and-beverage/healthy-options",
|
||||
},
|
||||
{
|
||||
title: "Snack and drink delivery",
|
||||
body: "Need product delivery or a broader refreshment setup beyond standard machine placement? We can help there too.",
|
||||
href: "/food-and-beverage/snack-and-drink-delivery",
|
||||
},
|
||||
{
|
||||
title: "Vending machine sales",
|
||||
body: "Compare purchase options if you want equipment ownership instead of a free-placement arrangement.",
|
||||
href: "/vending-machines/machines-for-sale",
|
||||
},
|
||||
{
|
||||
title: "Parts, repairs, and moving",
|
||||
body: "Get support for repair work, machine moving, replacement parts, and operational issues that need direct service help.",
|
||||
href: "/services/parts",
|
||||
},
|
||||
{
|
||||
title: "Training and support",
|
||||
body: "Browse the support pages and machine guides if you need help with specific models, manuals, or machine operation questions.",
|
||||
href: "/blog",
|
||||
},
|
||||
]
|
||||
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
|
|
@ -188,15 +229,15 @@ export function LocationLandingPage({
|
|||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Local Service Area"
|
||||
title={`${locationData.city}, ${locationData.stateAbbr} vending machine service`}
|
||||
description={`Rocky Mountain Vending serves businesses in ${locationData.city}, ${locationData.stateAbbr} with free placement for qualifying locations, machine sales, repairs, parts, and ongoing restocking and service across ${countyName} and nearby communities.`}
|
||||
title={`Vending machine service for businesses in ${locationData.city}, ${locationData.stateAbbr}`}
|
||||
description={`Rocky Mountain Vending helps businesses in ${locationData.city} with free placement for qualifying locations, machine sales, repairs, parts, and ongoing service across ${countyName} and nearby communities.`}
|
||||
className="mb-12 md:mb-16"
|
||||
/>
|
||||
|
||||
<div className="mx-auto mb-12 grid max-w-5xl gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div className="mx-auto mb-14 grid max-w-5xl gap-6 lg:grid-cols-[1.08fr_0.92fr]">
|
||||
<PublicSurface className="p-6 md:p-8">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-balance">
|
||||
Vending service for businesses across {locationData.city}
|
||||
A local vending partner for businesses across {locationData.city}
|
||||
</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
||||
If your business is in {locationData.neighborhoods.join(", ")}, or
|
||||
|
|
@ -219,11 +260,11 @@ export function LocationLandingPage({
|
|||
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
|
||||
Common business types we serve in {locationData.city}
|
||||
</h2>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<div className="mt-5 flex flex-wrap gap-2.5">
|
||||
{industries.map((industry) => (
|
||||
<span
|
||||
key={industry}
|
||||
className="rounded-full border border-border/60 bg-background px-3 py-1 text-sm text-muted-foreground"
|
||||
className="rounded-full border border-border/60 bg-background px-3 py-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
{industry}
|
||||
</span>
|
||||
|
|
@ -236,6 +277,15 @@ export function LocationLandingPage({
|
|||
Utah service area.
|
||||
</p>
|
||||
</PublicInset>
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<GetFreeMachineCta buttonLabel="Check Placement Fit" />
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
className="inline-flex min-h-11 items-center justify-center rounded-full border border-border bg-white px-4 text-sm font-medium text-foreground transition hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
Talk to Our Team
|
||||
</Link>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</div>
|
||||
|
||||
|
|
@ -243,7 +293,7 @@ export function LocationLandingPage({
|
|||
<h2 className="text-3xl font-bold tracking-tight text-balance">
|
||||
Vending services available in {locationData.city}
|
||||
</h2>
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-2">
|
||||
<div className="mt-8 grid gap-5 md:grid-cols-2">
|
||||
{[
|
||||
{
|
||||
title: "Free vending placement",
|
||||
|
|
@ -270,7 +320,7 @@ export function LocationLandingPage({
|
|||
cta: "View manuals and parts",
|
||||
},
|
||||
].map((service) => (
|
||||
<PublicSurface key={service.title} className="h-full p-6">
|
||||
<PublicSurface key={service.title} className="h-full p-6 md:p-7">
|
||||
<h3 className="text-xl font-semibold">{service.title}</h3>
|
||||
<p className="mt-3 leading-7 text-muted-foreground">
|
||||
{service.body}
|
||||
|
|
@ -287,6 +337,95 @@ export function LocationLandingPage({
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{isSaltLakeCity ? (
|
||||
<section className="mx-auto mb-16 max-w-5xl space-y-6">
|
||||
<PublicSurface className="p-6 md:p-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Why Rocky Mountain Vending
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-balance">
|
||||
What Salt Lake City businesses usually want to verify before they choose a vendor.
|
||||
</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
||||
Most businesses care about the same things: service speed,
|
||||
product flexibility, local ownership, and whether the machines
|
||||
feel modern and dependable after install.
|
||||
</p>
|
||||
<div className="mt-6 overflow-hidden rounded-[1.5rem] border border-border/60">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[680px] border-collapse text-sm">
|
||||
<thead className="bg-muted/55">
|
||||
<tr className="border-b border-border/60">
|
||||
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
||||
Comparison point
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
||||
Rocky Mountain Vending
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
||||
Small Vendor
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
||||
Large Vendor
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparisonRows.map((row, index) => (
|
||||
<tr
|
||||
key={row[0]}
|
||||
className={`border-b border-border/50 ${index % 2 === 0 ? "bg-background" : "bg-muted/20"}`}
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-foreground">
|
||||
{row[0]}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{row[1]}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{row[2]}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{row[3]}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
|
||||
<PublicSurface className="p-6 md:p-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Salt Lake City Services
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-balance">
|
||||
The service paths Salt Lake City businesses usually ask about first.
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
{saltLakeServiceLinks.map((item) => (
|
||||
<PublicInset key={item.title} className="flex h-full flex-col">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted-foreground">
|
||||
{item.body}
|
||||
</p>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mx-auto mb-16 grid max-w-5xl gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<PublicSurface className="p-6 md:p-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-balance">
|
||||
|
|
@ -405,7 +544,7 @@ export function LocationLandingPage({
|
|||
vending help you need. We'll follow up with the next best
|
||||
option for your location.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col items-center gap-3">
|
||||
<div className="mt-6 flex flex-col items-center gap-3 sm:flex-row sm:justify-center">
|
||||
<GetFreeMachineCta buttonLabel="See If Your Location Qualifies" />
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ import {
|
|||
ShoppingCart,
|
||||
LayoutGrid,
|
||||
List,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from "lucide-react"
|
||||
import type { Manual, ManualGroup } from "@/lib/manuals-types"
|
||||
import { getManualUrl, getThumbnailUrl } from "@/lib/manuals-types"
|
||||
|
|
@ -40,210 +37,6 @@ import {
|
|||
} from "@/lib/manuals-config"
|
||||
import { ManualViewer } from "@/components/manual-viewer"
|
||||
import { getManualsWithParts } from "@/lib/parts-lookup"
|
||||
import type { CachedEbayListing, EbayCacheState } from "@/lib/ebay-parts-match"
|
||||
|
||||
interface ProductSuggestionsResponse {
|
||||
query: string
|
||||
results: CachedEbayListing[]
|
||||
cache: EbayCacheState
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ProductSuggestionsProps {
|
||||
manual: Manual
|
||||
className?: string
|
||||
}
|
||||
|
||||
function ProductSuggestions({
|
||||
manual,
|
||||
className = "",
|
||||
}: ProductSuggestionsProps) {
|
||||
const [suggestions, setSuggestions] = useState<CachedEbayListing[]>([])
|
||||
const [cache, setCache] = useState<EbayCacheState | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSuggestions() {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const query = [
|
||||
manual.manufacturer,
|
||||
manual.category,
|
||||
manual.commonNames?.[0],
|
||||
manual.searchTerms?.[0],
|
||||
"vending machine",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
const params = new URLSearchParams({
|
||||
keywords: query,
|
||||
maxResults: "6",
|
||||
sortOrder: "BestMatch",
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/ebay/search?${params.toString()}`)
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| ProductSuggestionsResponse
|
||||
| null
|
||||
|
||||
if (!response.ok || !body) {
|
||||
throw new Error(
|
||||
body && typeof body.error === "string"
|
||||
? body.error
|
||||
: `Failed to load cached listings (${response.status})`
|
||||
)
|
||||
}
|
||||
|
||||
setSuggestions(Array.isArray(body.results) ? body.results : [])
|
||||
setCache(body.cache || null)
|
||||
setError(typeof body.error === "string" ? body.error : null)
|
||||
} catch (err) {
|
||||
console.error("Error loading product suggestions:", err)
|
||||
setSuggestions([])
|
||||
setCache(null)
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Could not load product suggestions"
|
||||
)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (manual) {
|
||||
loadSuggestions()
|
||||
}
|
||||
}, [manual])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white/60 dark:bg-yellow-900/20 rounded border border-yellow-300/30 dark:border-yellow-700/30 p-4 ${className}`}
|
||||
>
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<Loader2 className="h-5 w-5 text-yellow-700 dark:text-yellow-300 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white/60 dark:bg-yellow-900/20 rounded border border-yellow-300/30 dark:border-yellow-700/30 p-4 ${className}`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-2 h-32 text-center">
|
||||
<AlertCircle className="h-6 w-6 text-yellow-600" />
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-200">
|
||||
{error}
|
||||
</span>
|
||||
{cache?.lastSuccessfulAt ? (
|
||||
<span className="text-[11px] text-yellow-600/80 dark:text-yellow-200/70">
|
||||
Last refreshed {new Date(cache.lastSuccessfulAt).toLocaleString()}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white/60 dark:bg-yellow-900/20 rounded border border-yellow-300/30 dark:border-yellow-700/30 p-4 ${className}`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-2 h-32 text-center">
|
||||
<AlertCircle className="h-6 w-6 text-yellow-500" />
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-200">
|
||||
No cached eBay matches yet
|
||||
</span>
|
||||
<span className="text-[11px] text-yellow-600/80 dark:text-yellow-200/70">
|
||||
{cache?.isStale
|
||||
? "The background poll is behind, so this manual is showing the last known cache."
|
||||
: "Try again after the next periodic cache refresh."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-white/60 dark:bg-yellow-900/20 rounded border border-yellow-300/30 dark:border-yellow-700/30 p-4 ${className}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ShoppingCart className="h-4 w-4 text-yellow-700 dark:text-yellow-300" />
|
||||
<h3 className="text-sm font-semibold text-yellow-900 dark:text-yellow-100">
|
||||
Related Products
|
||||
</h3>
|
||||
</div>
|
||||
{cache && (
|
||||
<div className="mb-3 text-[11px] text-yellow-700/80 dark:text-yellow-200/70">
|
||||
{cache.lastSuccessfulAt
|
||||
? `Cache refreshed ${new Date(cache.lastSuccessfulAt).toLocaleString()}`
|
||||
: "Cache is warming up"}
|
||||
{cache.isStale ? " • stale cache" : ""}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{suggestions.map((product) => (
|
||||
<a
|
||||
key={product.itemId}
|
||||
href={product.affiliateLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block group"
|
||||
>
|
||||
<div className="bg-white dark:bg-yellow-900/30 rounded border border-yellow-300/40 dark:border-yellow-700/40 p-2 hover:bg-yellow-50 dark:hover:bg-yellow-900/40 transition-colors">
|
||||
{/* Image */}
|
||||
{product.imageUrl && (
|
||||
<div className="mb-2 rounded overflow-hidden bg-yellow-100 dark:bg-yellow-900/50">
|
||||
<img
|
||||
src={product.imageUrl}
|
||||
alt={product.title}
|
||||
className="w-full h-16 object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = `https://via.placeholder.com/120x80/fbbf24/1f2937?text=${encodeURIComponent(product.title)}`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!product.imageUrl && (
|
||||
<div className="mb-2 rounded overflow-hidden bg-yellow-100 dark:bg-yellow-900/50 h-16 flex items-center justify-center">
|
||||
<span className="text-[10px] text-yellow-700 dark:text-yellow-300">
|
||||
No Image
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Details */}
|
||||
<div className="space-y-1">
|
||||
<div className="text-[11px] text-yellow-900 dark:text-yellow-100 line-clamp-2 min-h-[1.5rem]">
|
||||
{product.title}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-yellow-900 dark:text-yellow-100">
|
||||
{product.price}
|
||||
</span>
|
||||
<ExternalLink className="h-3 w-3 text-yellow-700 dark:text-yellow-300 group-hover:text-yellow-900 dark:group-hover:text-yellow-100 transition-colors flex-shrink-0" />
|
||||
</div>
|
||||
{product.condition && (
|
||||
<div className="text-[9px] text-yellow-700/80 dark:text-yellow-300/80">
|
||||
{product.condition}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ManualsPageClientProps {
|
||||
manuals: Manual[]
|
||||
|
|
@ -272,7 +65,7 @@ function ManualCard({
|
|||
const thumbnailUrl = getThumbnailUrl(manual)
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_24px_60px_rgba(15,23,42,0.12)]">
|
||||
<Card className="overflow-hidden rounded-[1.75rem] border border-border/70 bg-background shadow-[0_16px_40px_rgba(15,23,42,0.075)] transition-all hover:-translate-y-0.5 hover:shadow-[0_22px_54px_rgba(15,23,42,0.11)]">
|
||||
{thumbnailUrl && (
|
||||
<div className="relative h-48 min-h-[192px] w-full overflow-hidden bg-[radial-gradient(circle_at_top_left,rgba(196,154,52,0.12),transparent_52%),linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,255,255,0.98))]">
|
||||
<Image
|
||||
|
|
@ -284,13 +77,13 @@ function ManualCard({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className="px-5 pt-5">
|
||||
<CardHeader className="space-y-3 px-5 pt-5">
|
||||
<CardTitle className="line-clamp-2 text-base leading-snug">
|
||||
{manual.filename.replace(/\.pdf$/i, "")}
|
||||
</CardTitle>
|
||||
{manual.commonNames && manual.commonNames.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{manual.commonNames.map((name, index) => (
|
||||
{manual.commonNames.slice(0, 3).map((name, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
|
|
@ -299,13 +92,21 @@ function ManualCard({
|
|||
{name}
|
||||
</Badge>
|
||||
))}
|
||||
{manual.commonNames.length > 3 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-full border border-border/60 bg-muted/35 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground"
|
||||
>
|
||||
+{manual.commonNames.length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{manual.searchTerms &&
|
||||
manual.searchTerms.length > 0 &&
|
||||
!manual.commonNames && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{manual.searchTerms.map((term, index) => (
|
||||
{manual.searchTerms.slice(0, 4).map((term, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
|
|
@ -314,6 +115,14 @@ function ManualCard({
|
|||
{term}
|
||||
</Badge>
|
||||
))}
|
||||
{manual.searchTerms.length > 4 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-full border border-border/60 bg-muted/35 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground"
|
||||
>
|
||||
+{manual.searchTerms.length - 4} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
|
@ -337,7 +146,7 @@ function ManualCard({
|
|||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div className="space-y-1.5 text-sm text-muted-foreground">
|
||||
{showManufacturer && (
|
||||
<p>
|
||||
<strong>Manufacturer:</strong> {manual.manufacturer}
|
||||
|
|
@ -681,9 +490,28 @@ export function ManualsPageClient({
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Search and Filter Controls */}
|
||||
<PublicSurface>
|
||||
<CardContent className="p-0">
|
||||
<div className="space-y-6">
|
||||
<PublicSurface className="p-4 md:p-6">
|
||||
<CardContent className="p-0">
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col gap-4 rounded-[1.5rem] border border-border/60 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,249,240,0.94))] p-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
|
||||
Start With Search
|
||||
</p>
|
||||
<h2 className="mt-2 text-xl font-semibold tracking-tight text-foreground">
|
||||
Find the manual first, then narrow it down
|
||||
</h2>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
|
||||
Search by model, manufacturer, or category. Use filters if
|
||||
you already know the brand or want manuals with parts.
|
||||
</p>
|
||||
</div>
|
||||
<PublicInset className="w-full rounded-[1.25rem] px-4 py-3 text-sm text-muted-foreground shadow-none md:max-w-xs">
|
||||
Showing <strong>{filteredManuals.length}</strong> of{" "}
|
||||
<strong>{manuals.length}</strong> manuals
|
||||
</PublicInset>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
|
|
@ -697,12 +525,14 @@ export function ManualsPageClient({
|
|||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div className="flex flex-col sm:flex-row flex-wrap gap-3 sm:gap-4 items-start sm:items-center">
|
||||
<div className="rounded-[1.5rem] border border-border/60 bg-white/80 p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<Filter className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Filters:</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center">
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 w-full sm:w-auto">
|
||||
<div className="relative">
|
||||
<Select
|
||||
|
|
@ -800,10 +630,12 @@ export function ManualsPageClient({
|
|||
Clear Filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle and Results Count */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4 border-t border-border/55 pt-1">
|
||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||
<span className="text-sm text-muted-foreground flex-shrink-0">
|
||||
View:
|
||||
|
|
@ -841,8 +673,8 @@ export function ManualsPageClient({
|
|||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground w-full sm:w-auto text-center sm:text-left">
|
||||
Showing <strong>{filteredManuals.length}</strong> of{" "}
|
||||
<strong>{manuals.length}</strong> manuals
|
||||
View the full library grouped by manufacturer or switch to list
|
||||
view for a faster scan.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -870,11 +702,15 @@ export function ManualsPageClient({
|
|||
{filteredGroupedManuals.map((group) => {
|
||||
const organized = organizeCategories(group.categories)
|
||||
return (
|
||||
<PublicSurface key={group.manufacturer} className="space-y-6">
|
||||
<div className="border-b border-border/60 pb-3">
|
||||
<PublicSurface key={group.manufacturer} className="space-y-7 p-5 md:p-7">
|
||||
<div className="border-b border-border/60 pb-4">
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
{group.manufacturer}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{Object.values(group.categories).flat().length} manuals
|
||||
available from this manufacturer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Machine Type Categories First */}
|
||||
|
|
@ -912,7 +748,7 @@ export function ManualsPageClient({
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{categoryManuals.map((manual) => (
|
||||
<ManualCard
|
||||
key={manual.path}
|
||||
|
|
@ -955,7 +791,7 @@ export function ManualsPageClient({
|
|||
)
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{categoryManuals.map((manual) => (
|
||||
<ManualCard
|
||||
key={manual.path}
|
||||
|
|
@ -972,28 +808,13 @@ export function ManualsPageClient({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Suggestions Section */}
|
||||
{filteredManuals.length > 0 && (
|
||||
<div className="space-y-6 mt-8">
|
||||
<div className="border-t border-border/60 pt-6">
|
||||
<h3 className="text-lg font-semibold text-muted-foreground mb-4">
|
||||
Related Products
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredManuals.slice(0, 3).map((manual) => (
|
||||
<ProductSuggestions key={manual.path} manual={manual} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PublicSurface>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
/* List View */
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredManuals.map((manual) => (
|
||||
<ManualCard
|
||||
key={manual.path}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ type PublicPageHeaderProps = {
|
|||
children?: ReactNode
|
||||
}
|
||||
|
||||
type PublicProseProps = {
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function PublicSection({
|
||||
id,
|
||||
tone = "default",
|
||||
|
|
@ -157,3 +162,16 @@ export function PublicSectionHeader({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PublicProse({ className, children }: PublicProseProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"prose prose-slate max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-foreground prose-p:text-muted-foreground prose-p:leading-7 prose-li:text-muted-foreground prose-li:leading-7 prose-strong:text-foreground prose-a:text-foreground prose-a:decoration-primary/35 prose-a:underline-offset-4 hover:prose-a:decoration-primary prose-img:rounded-[1.5rem] prose-img:border prose-img:border-border/55 prose-img:shadow-[0_18px_45px_rgba(15,23,42,0.08)] prose-hr:border-border/50 prose-blockquote:border-primary/25 prose-blockquote:text-foreground md:prose-lg",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,47 @@ import {
|
|||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
|
||||
const reviewThemes = [
|
||||
{
|
||||
title: "Always cold and stocked",
|
||||
body: "Customers consistently mention that the machines stay full, drinks stay cold, and the day-to-day experience feels dependable instead of neglected.",
|
||||
},
|
||||
{
|
||||
title: "Fast, friendly service",
|
||||
body: "When something needs attention, businesses talk about quick follow-through, easy communication, and issues getting handled without a long delay.",
|
||||
},
|
||||
{
|
||||
title: "Wide product variety",
|
||||
body: "Reviews regularly call out strong drink selection, snack variety, and the ability to request items that fit the people using the location.",
|
||||
},
|
||||
{
|
||||
title: "Fair pricing and reliability",
|
||||
body: "People mention competitive pricing, clean machines, and a setup that feels professional instead of frustrating or outdated.",
|
||||
},
|
||||
]
|
||||
|
||||
const featuredQuotes = [
|
||||
{
|
||||
quote:
|
||||
"He is arguably one of the best vendors in the industry. There probably isn't too many people I would trust more than him.",
|
||||
author: "Martin Harrison",
|
||||
},
|
||||
{
|
||||
quote: "He always has my favorite energy drink at a great price!",
|
||||
author: "TOPX Kingsford",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"This vending machine is my favorite at my job! It always works and has the best stuff!!",
|
||||
author: "DJ Montoya",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"Great to work with, looking forward to having him in more locations.",
|
||||
author: "Jennifer Spencer",
|
||||
},
|
||||
]
|
||||
|
||||
export function ReviewsPage() {
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector(
|
||||
|
|
@ -44,8 +85,65 @@ export function ReviewsPage() {
|
|||
align="center"
|
||||
eyebrow="Customer Reviews"
|
||||
title="What Utah businesses say about working with Rocky Mountain Vending."
|
||||
description="Browse the live Google review feed and see what Utah businesses say about placement, restocking, repairs, and service."
|
||||
/>
|
||||
description="See the real themes customers mention most often, browse featured comments, and then dig into the live Google review feed."
|
||||
>
|
||||
<PublicInset className="mx-auto inline-flex w-fit rounded-full px-4 py-2 text-sm text-muted-foreground shadow-none">
|
||||
Average rating: 4.9 out of 5, based on 50+ customer reviews.
|
||||
</PublicInset>
|
||||
</PublicPageHeader>
|
||||
|
||||
<section className="mt-12 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<PublicSurface>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Why Businesses Trust Rocky
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
||||
The same strengths keep showing up in the reviews.
|
||||
</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
||||
We're honored to have earned strong feedback from Utah
|
||||
businesses that rely on us for placement, restocking, repairs, and
|
||||
day-to-day service. The reviews tend to point back to the same
|
||||
things: stocked machines, responsive help, a better product mix,
|
||||
and follow-through after install.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
{reviewThemes.map((theme) => (
|
||||
<PublicInset key={theme.title} className="h-full">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{theme.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{theme.body}
|
||||
</p>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
</PublicSurface>
|
||||
|
||||
<PublicSurface className="flex flex-col justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Featured Comments
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
||||
A few of the comments that capture the pattern.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-6 grid gap-4">
|
||||
{featuredQuotes.map((item) => (
|
||||
<PublicInset key={item.author} className="p-5">
|
||||
<p className="text-sm leading-relaxed text-foreground">
|
||||
“{item.quote}”
|
||||
</p>
|
||||
<p className="mt-3 text-sm font-semibold text-primary">
|
||||
{item.author}
|
||||
</p>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<PublicSurface className="overflow-hidden p-5 md:p-7">
|
||||
|
|
@ -69,9 +167,9 @@ export function ReviewsPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="mt-6 rounded-[1.5rem] bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,249,240,0.92))] p-2 sm:p-3">
|
||||
<iframe
|
||||
className="lc_reviews_widget min-h-[780px] w-full rounded-[1.5rem] border border-border/60 bg-background"
|
||||
className="lc_reviews_widget min-h-[620px] w-full rounded-[1.35rem] border border-border/60 bg-background md:min-h-[780px]"
|
||||
src="https://reputationhub.site/reputation/widgets/review_widget/YAoWLgNSid8oG44j9BjG"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
|
|
@ -143,30 +241,32 @@ export function ReviewsPage() {
|
|||
<li>Should we ask about placement, machine sales, or direct service help?</li>
|
||||
</ul>
|
||||
</PublicInset>
|
||||
<Link
|
||||
href="/#request-machine"
|
||||
className="rounded-[1.5rem] border border-border/55 bg-background/70 p-5 text-left transition hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Free Placement
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
See whether your business qualifies for vending machine
|
||||
placement and ongoing service.
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
className="rounded-[1.5rem] border border-border/55 bg-background/70 p-5 text-left transition hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Service or Sales
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
Reach out about repairs, moving, manuals, parts, or machine
|
||||
sales.
|
||||
</p>
|
||||
</Link>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<Link
|
||||
href="/#request-machine"
|
||||
className="rounded-[1.5rem] border border-border/55 bg-background/70 p-5 text-left transition hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Free Placement
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
See whether your business qualifies for vending machine
|
||||
placement and ongoing service.
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
className="rounded-[1.5rem] border border-border/55 bg-background/70 p-5 text-left transition hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Service or Sales
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
Reach out about repairs, moving, manuals, parts, or machine
|
||||
sales.
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 rounded-[1.5rem] border border-primary/12 bg-[linear-gradient(180deg,rgba(41,160,71,0.06),rgba(255,255,255,0.7))] p-5">
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export function WhoWeServePage({
|
|||
}
|
||||
contentIntro={
|
||||
<>
|
||||
<PublicInset className="h-full p-5 md:p-6">
|
||||
<PublicInset className="h-full border-primary/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,251,245,0.94))] p-5 md:p-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
How We Tailor Service
|
||||
</p>
|
||||
|
|
@ -87,7 +87,7 @@ export function WhoWeServePage({
|
|||
<li>Service cadence adjusted so stocking and support stay consistent without adding staff work.</li>
|
||||
</ul>
|
||||
</PublicInset>
|
||||
<PublicInset className="h-full p-5 md:p-6">
|
||||
<PublicInset className="h-full border-border/50 bg-white/80 p-5 md:p-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Good Fit Signals
|
||||
</p>
|
||||
|
|
@ -103,12 +103,12 @@ export function WhoWeServePage({
|
|||
</>
|
||||
}
|
||||
content={
|
||||
<div className="text-foreground">{content}</div>
|
||||
<div className="space-y-6 text-foreground">{content}</div>
|
||||
}
|
||||
contentClassName="prose prose-lg max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-a:text-foreground prose-a:underline prose-a:decoration-primary/35 prose-a:underline-offset-4 hover:prose-a:decoration-primary prose-strong:text-foreground"
|
||||
contentClassName="prose-headings:mb-4 prose-headings:mt-10 prose-p:max-w-[68ch] prose-p:text-[1.02rem] prose-p:leading-8 prose-li:max-w-[68ch] prose-ul:space-y-2"
|
||||
sections={
|
||||
<section>
|
||||
<div className="mx-auto mb-6 max-w-3xl text-center">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Why Rocky
|
||||
</p>
|
||||
|
|
@ -154,6 +154,12 @@ export function WhoWeServePage({
|
|||
variant: "outline",
|
||||
},
|
||||
],
|
||||
note: (
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
We can help you sort out whether this should start as a placement request,
|
||||
a machine-sales conversation, or direct service support.
|
||||
</p>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue