deploy: polish public marketing pages

This commit is contained in:
DMleadgen 2026-04-16 13:03:12 -06:00
parent 9dfee33e49
commit 14cb8ce1fc
Signed by: matt
GPG key ID: C2720CF8CD701894
14 changed files with 714 additions and 153 deletions

View file

@ -183,7 +183,8 @@
transition:
color 0.2s ease,
text-decoration-color 0.2s ease,
opacity 0.2s ease;
opacity 0.2s ease,
transform 0.2s ease;
}
a:hover,
@ -202,7 +203,8 @@
transition:
color 0.2s ease,
text-decoration-color 0.2s ease,
opacity 0.2s ease;
opacity 0.2s ease,
transform 0.2s ease;
}
a[href]:hover,
@ -211,6 +213,29 @@
background-color: transparent;
}
button a,
[role="button"] a,
.bg-primary a,
.text-primary-foreground a {
color: inherit;
}
button a:hover,
button a:focus,
[role="button"] a:hover,
[role="button"] a:focus,
.bg-primary a:hover,
.bg-primary a:focus,
.text-primary-foreground a:hover,
.text-primary-foreground a:focus {
color: inherit;
}
p,
li {
text-wrap: pretty;
}
a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible,

View file

@ -37,10 +37,10 @@ function LocationCard({
}) {
return (
<Link href={href} className="group block h-full">
<PublicSurface className="h-full p-5 transition-all hover:-translate-y-0.5 hover:shadow-[0_26px_65px_rgba(0,0,0,0.12)] md:p-6">
<PublicInset className="h-full p-4 transition-all hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-[0_20px_48px_rgba(0,0,0,0.09)] md:p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<h3 className="text-xl font-semibold text-foreground transition-colors group-hover:text-primary">
<h3 className="text-lg font-semibold text-foreground transition-colors group-hover:text-primary">
{city}
</h3>
<p className="text-sm text-muted-foreground">{zipCode}</p>
@ -49,15 +49,15 @@ function LocationCard({
<ArrowRight className="h-4 w-4" />
</div>
</div>
<PublicInset className="mt-5">
<div className="mt-4 rounded-[1.1rem] border border-border/45 bg-background/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/75">
Popular Areas
</p>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{neighborhoods.slice(0, 2).join(", ")}
</p>
</PublicInset>
</PublicSurface>
</div>
</PublicInset>
</Link>
)
}
@ -222,7 +222,7 @@ export default function ServiceAreasPage() {
</PublicSurface>
</section>
<section className="mt-12 space-y-12">
<section className="mt-12 space-y-8">
{[
{
title: "Salt Lake County",
@ -243,19 +243,24 @@ export default function ServiceAreasPage() {
items: utahCounty,
},
].map((section) => (
<div key={section.title} className="space-y-5">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Coverage Area
</p>
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-balance">
{section.title}
</h2>
<p className="mt-2 max-w-3xl text-base leading-relaxed text-muted-foreground">
{section.description}
</p>
<PublicSurface key={section.title} className="p-5 md:p-6">
<div className="flex flex-col gap-4 border-b border-border/55 pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Coverage Area
</p>
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-balance">
{section.title}
</h2>
<p className="mt-2 max-w-3xl text-base leading-relaxed text-muted-foreground">
{section.description}
</p>
</div>
<div className="w-fit rounded-full border border-border/55 bg-background/70 px-3 py-1 text-sm text-muted-foreground">
{section.items.length} cities
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{section.items.map((location) => (
<LocationCard
key={location.slug}
@ -266,11 +271,11 @@ export default function ServiceAreasPage() {
/>
))}
</div>
</div>
</PublicSurface>
))}
</section>
<section className="mt-12 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<section className="mt-12 grid gap-6 lg:grid-cols-2">
<PublicSurface>
<h2 className="text-3xl font-semibold tracking-tight text-balance">
Why businesses choose Rocky Mountain Vending

View file

@ -1,11 +1,9 @@
import { notFound } from "next/navigation"
import { loadImageMapping } from "@/lib/wordpress-content"
import {
generateRegistryMetadata,
generateRegistryStructuredData,
} from "@/lib/seo"
import { getPageBySlug } from "@/lib/wordpress-data-loader"
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
import { FAQSection } from "@/components/faq-section"
import { ServiceAreasSection } from "@/components/service-areas-section"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
@ -58,21 +56,13 @@ export default async function RepairsPage() {
notFound()
}
let imageMapping: any = {}
try {
imageMapping = loadImageMapping()
} catch (e) {
imageMapping = {}
}
// Extract FAQs from content
const faqs: Array<{ question: string; answer: string }> = []
let contentWithoutFAQs = page.content || ""
let contentWithoutVirtualServices = ""
let virtualServicesContent = ""
if (page.content) {
const contentStr = String(page.content)
let strippedContent = contentStr
// Extract FAQ items from accordion structure
const questionMatches = contentStr.matchAll(
@ -112,40 +102,28 @@ export default async function RepairsPage() {
if (faqs.length > 0) {
const faqSectionRegex =
/<h2[^>]*>.*?Answers\s+To\s+Common\s+Questions.*?<\/h2>[\s\S]*?(?=<h2[^>]*>.*?Virtual\s+Services|<h2[^>]*>.*?Service\s+Area|$)/i
contentWithoutFAQs = contentStr.replace(faqSectionRegex, "").trim()
strippedContent = contentStr.replace(faqSectionRegex, "").trim()
}
// Extract Virtual Services section
const virtualServicesRegex =
/<h2[^>]*>.*?Virtual\s+Services.*?<\/h2>([\s\S]*?)(?=<h2[^>]*>.*?Service\s+Area|$)/i
const virtualMatch = contentStr.match(virtualServicesRegex)
const virtualMatch = strippedContent.match(virtualServicesRegex)
if (virtualMatch) {
virtualServicesContent = virtualMatch[1]
// Remove Virtual Services from main content
contentWithoutVirtualServices = contentWithoutFAQs
.replace(virtualServicesRegex, "")
.trim()
} else {
contentWithoutVirtualServices = contentWithoutFAQs
}
}
const content = contentWithoutVirtualServices ? (
<div className="max-w-none">
{cleanWordPressContent(String(contentWithoutVirtualServices), {
imageMapping,
pageTitle: page.title,
})}
</div>
) : (
<p className="text-muted-foreground">No content available.</p>
)
const structuredData = generateRegistryStructuredData("repairs", {
datePublished: page.date,
dateModified: page.modified || page.date,
})
const excerpt = String(page.excerpt || "")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim()
const surfaceCardClass =
"rounded-[var(--public-surface-radius)] border border-border/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,251,243,0.96))] shadow-[var(--public-surface-shadow)]"
const insetCardClass =
@ -171,37 +149,14 @@ export default async function RepairsPage() {
align="center"
className="mb-8"
eyebrow="Repair Services"
title={page.title || "Vending Machine Repairs and Service"}
title="Vending machine repairs and service for Utah businesses"
description={
"Rocky Mountain Vending delivers expert vending machine repair and maintenance services to keep your business thriving."
"Get help with payment issues, refrigeration problems, machine errors, and ongoing maintenance from a local vending service team."
}
>
<p className="mx-auto max-w-3xl text-base leading-relaxed text-muted-foreground md:text-lg">
Rocky Mountain Vending delivers expert{" "}
<Link
href="/services/repairs"
className="text-primary hover:underline font-semibold"
>
vending machine repair
</Link>{" "}
and maintenance services to keep your business thriving. From
resolving jammed coin slots and refrigeration issues to fixing
non-dispensing machines, our skilled technicians ensure reliable
performance. For all your{" "}
<Link
href="/services/parts"
className="text-primary hover:underline"
>
vending machine parts
</Link>{" "}
needs and professional{" "}
<Link
href="/services/moving"
className="text-primary hover:underline"
>
vending machine moving
</Link>{" "}
services, contact us today for fast, professional solutions!
{excerpt ||
"Rocky Mountain Vending helps businesses across Davis, Salt Lake, and Utah counties keep machines running with practical repair, maintenance, and support guidance."}
</p>
</PublicPageHeader>
{/* Images Carousel */}
@ -211,15 +166,71 @@ export default async function RepairsPage() {
</div>
</section>
{contentWithoutVirtualServices ? (
<section className="py-16 md:py-20 bg-background">
<div className="container mx-auto px-4 max-w-4xl">
<PublicSurface>
<div className="max-w-none">{content}</div>
</PublicSurface>
</div>
</section>
) : null}
<section className="py-16 md:py-20 bg-background">
<div className="container mx-auto grid max-w-6xl gap-6 px-4 lg:grid-cols-[1.08fr_0.92fr]">
<PublicSurface className="h-full">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Repair Overview
</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
Clear next steps when a machine is down, rejecting payments, or
needs service.
</h2>
<p className="mt-4 text-base leading-7 text-muted-foreground md:text-lg md:leading-8">
We help with common vending issues like bill acceptor problems,
refrigeration failures, card reader troubleshooting, controller
errors, and recurring maintenance needs. If the issue can be
handled virtually, we can talk through that too.
</p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Link
href="#request-service"
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:opacity-95"
>
Request Service
</Link>
<Link
href="/services/parts"
className="inline-flex min-h-11 items-center justify-center rounded-full border border-border bg-background px-5 text-sm font-medium text-foreground transition hover:border-primary/35 hover:text-primary"
>
Need Parts Instead?
</Link>
</div>
</PublicSurface>
<PublicInset className="h-full p-5 md:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Before You Reach Out
</p>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
The more detail you share, the faster we can point you in the
right direction.
</h2>
<ul className="mt-5 space-y-3">
<li className="flex items-start gap-3">
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-primary" />
<span className="text-sm leading-6 text-muted-foreground">
Include the machine model, brand, and whether the issue is
intermittent or constant.
</span>
</li>
<li className="flex items-start gap-3">
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-primary" />
<span className="text-sm leading-6 text-muted-foreground">
Tell us if the problem is payment-related, refrigeration,
dispensing, display errors, or a recent setup change.
</span>
</li>
<li className="flex items-start gap-3">
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-primary" />
<span className="text-sm leading-6 text-muted-foreground">
Photos or short videos can make remote triage much easier
before an on-site visit is scheduled.
</span>
</li>
</ul>
</PublicInset>
</div>
</section>
{/* Services Section */}
<section className="py-20 md:py-28 bg-muted/30">

View file

@ -1,16 +1,74 @@
import { notFound } from "next/navigation"
import { loadImageMapping } from "@/lib/wordpress-content"
import type { ImageMapping } from "@/lib/wordpress-content"
import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import {
CheckCircle2,
CreditCard,
Refrigerator,
ShoppingCart,
} from "lucide-react"
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
import { getPageBySlug } from "@/lib/wordpress-data-loader"
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
import type { Metadata } from "next"
import { PublicPageHeader, PublicSurface } from "@/components/public-surface"
import {
PublicInset,
PublicPageHeader,
PublicSectionHeader,
PublicSurface,
} from "@/components/public-surface"
import { GetFreeMachineCta } from "@/components/get-free-machine-cta"
import { Breadcrumbs } from "@/components/breadcrumbs"
import { Button } from "@/components/ui/button"
const WORDPRESS_SLUG = "vending-machines-for-sale-in-utah"
const machineHighlights = [
"Snack, drink, combo, and card-reader-ready equipment options",
"New and used machine guidance based on traffic, budget, and product mix",
"Local help with payment hardware, installation planning, and next-step questions",
]
const machineOptions = [
{
title: "Snack and drink machines",
description:
"Traditional snack, beverage, and combo machines for breakrooms, customer spaces, and mixed-traffic locations.",
icon: ShoppingCart,
},
{
title: "Cashless payment hardware",
description:
"Card reader and mobile-payment options that help modernize older machines or support new installs.",
icon: CreditCard,
},
{
title: "Refrigerated equipment",
description:
"Cold drink and refrigerated machine options for workplaces that need dependable temperature-controlled service.",
icon: Refrigerator,
},
]
const buyingSteps = [
{
title: "Tell us about the location",
body: "We learn about traffic, available space, product needs, and whether you are comparing free placement with a direct purchase.",
},
{
title: "Compare the right machine options",
body: "We help narrow down machine styles, payment hardware, and new-versus-used tradeoffs so you are looking at realistic fits.",
},
{
title: "Plan install and support",
body: "Once you know what you want, we can talk through delivery, setup, payment configuration, and any follow-up service needs.",
},
]
function normalizeWpImageUrl(url?: string) {
if (!url) return null
return url.replace("https:///", "https://rockymountainvending.com/")
}
export async function generateMetadata(): Promise<Metadata> {
const page = getPageBySlug(WORDPRESS_SLUG)
@ -39,13 +97,6 @@ export default async function MachinesForSalePage() {
notFound()
}
let imageMapping: ImageMapping = {}
try {
imageMapping = loadImageMapping()
} catch {
imageMapping = {}
}
const structuredData = (() => {
try {
return generateStructuredData({
@ -70,6 +121,16 @@ export default async function MachinesForSalePage() {
}
})()
const heroImage =
normalizeWpImageUrl(page.images?.[0]?.url) ??
"https://rockymountainvending.com/wp-content/uploads/2024/01/EH0A1551-HDR.webp"
const comboImage =
normalizeWpImageUrl(page.images?.[1]?.url) ??
"https://rockymountainvending.com/wp-content/uploads/2022/06/Seage-HY900-Combo.webp"
const paymentImage =
normalizeWpImageUrl(page.images?.[2]?.url) ??
"https://rockymountainvending.com/wp-content/uploads/2024/01/Parlevel-Pay-Plus.jpg"
return (
<>
<script
@ -88,19 +149,168 @@ export default async function MachinesForSalePage() {
]}
/>
<PublicPageHeader
align="center"
eyebrow="Machine Sales"
title={page.title || "Vending Machines for Sale in Utah"}
description="Compare machine options, payment hardware, and support with help from the Rocky Mountain Vending team."
/>
title="Compare vending machines, payment hardware, and purchase options with a local Utah team."
description="If you are looking at buying equipment instead of free placement, we can help you compare machine styles, payment systems, and next-step support without sending you through a generic catalog dump."
>
<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
<Button asChild size="lg" className="min-h-11 rounded-full px-6">
<Link href="/contact-us#contact-form">Ask About Sales</Link>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="min-h-11 rounded-full px-6"
>
<Link href="/products">Browse Product Listings</Link>
</Button>
</div>
</PublicPageHeader>
<PublicSurface className="mt-10">
<div className="max-w-none">
{cleanWordPressContent(String(page.content || ""), {
imageMapping,
pageTitle: page.title,
<section className="mt-10 grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<PublicSurface className="flex h-full flex-col justify-center">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Sales Overview
</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
Buying a machine should feel clear before you spend money.
</h2>
<p className="mt-4 text-base leading-7 text-muted-foreground md:text-lg md:leading-8">
We help Utah businesses sort through machine type, payment
hardware, and install considerations so you can decide whether a
direct purchase is the right move for your location.
</p>
<ul className="mt-6 space-y-3">
{machineHighlights.map((highlight) => (
<li key={highlight} className="flex items-start gap-3">
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-primary" />
<span className="text-sm leading-6 text-foreground md:text-base">
{highlight}
</span>
</li>
))}
</ul>
</PublicSurface>
<div className="relative min-h-[320px] overflow-hidden rounded-[var(--public-surface-radius)] border border-border/65 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,249,240,0.96))] p-3 shadow-[0_20px_52px_rgba(15,23,42,0.075)]">
<Image
src={heroImage}
alt="Vending machine option available for sale in Utah"
fill
className="rounded-[calc(var(--public-surface-radius)-0.45rem)] object-cover"
sizes="(max-width: 1280px) 100vw, 560px"
priority
/>
</div>
</section>
<section className="mt-12">
<PublicSectionHeader
eyebrow="Machine Options"
title="What businesses usually want help comparing"
description="Most sales conversations come down to the machine type, payment setup, and whether a direct purchase makes more sense than placement."
className="mx-auto mb-8 max-w-3xl text-center"
/>
<div className="grid gap-4 lg:grid-cols-3">
{machineOptions.map((option) => {
const Icon = option.icon
return (
<PublicInset key={option.title} className="h-full p-5 md:p-6">
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-primary/10 text-primary">
<Icon className="h-5 w-5" />
</div>
<h3 className="mt-4 text-xl font-semibold tracking-tight text-foreground">
{option.title}
</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground md:text-base">
{option.description}
</p>
</PublicInset>
)
})}
</div>
</PublicSurface>
</section>
<section className="mt-12 grid gap-6 lg:grid-cols-[0.98fr_1.02fr]">
<PublicSurface className="overflow-hidden p-0">
<div className="grid gap-0 md:grid-cols-2">
<div className="relative min-h-[260px]">
<Image
src={comboImage}
alt="Combo vending machine for sale"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 360px"
/>
</div>
<div className="flex flex-col justify-center p-5 md:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
New vs. Used
</p>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
We can help you sort through budget, features, and condition.
</h2>
<p className="mt-3 text-sm leading-6 text-muted-foreground md:text-base">
Some buyers need dependable starter equipment. Others need a
cleaner, more modern machine with stronger payment support.
We can talk through both without pushing you into the wrong
setup.
</p>
</div>
</div>
</PublicSurface>
<PublicSurface className="overflow-hidden p-0">
<div className="grid gap-0 md:grid-cols-2">
<div className="order-2 flex flex-col justify-center p-5 md:order-1 md:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Payment Hardware
</p>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
Card readers and cashless upgrades are often part of the decision.
</h2>
<p className="mt-3 text-sm leading-6 text-muted-foreground md:text-base">
If you are trying to modernize how people pay, we can help
you think through card readers, mobile payments, and
compatibility before you commit to a machine.
</p>
</div>
<div className="relative order-1 min-h-[260px] md:order-2">
<Image
src={paymentImage}
alt="Cashless payment hardware for vending machines"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 360px"
/>
</div>
</div>
</PublicSurface>
</section>
<section className="mt-12">
<PublicSectionHeader
eyebrow="Buying Process"
title="A simpler way to move from questions to a real option"
description="You do not need to have the exact model picked out before you reach out. Most of the work is narrowing to the right fit."
className="mx-auto mb-8 max-w-3xl text-center"
/>
<div className="grid gap-4 lg:grid-cols-3">
{buyingSteps.map((step, index) => (
<PublicInset key={step.title} className="h-full p-5 md:p-6">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary text-lg font-semibold text-primary-foreground">
{index + 1}
</div>
<h3 className="mt-4 text-xl font-semibold tracking-tight text-foreground">
{step.title}
</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground md:text-base">
{step.body}
</p>
</PublicInset>
))}
</div>
</section>
<section className="mt-12 grid gap-6 lg:grid-cols-[0.95fr_1.05fr]">
<PublicSurface>
@ -111,9 +321,9 @@ export default async function MachinesForSalePage() {
Need a free machine instead of buying one?
</h2>
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
If you&apos;re a business looking for placement instead of a
purchase, we can help you find the right setup for your
location.
If you are a business looking for placement instead of a
purchase, we can help you figure out whether your location is a
fit before you spend money on equipment.
</p>
<div className="mt-6">
<GetFreeMachineCta buttonLabel="Get Free Placement" />
@ -125,13 +335,23 @@ export default async function MachinesForSalePage() {
Need Sales Help?
</p>
<h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
Talk through machine sales, placement, or feature questions.
Talk through machine sales, placement, or payment questions.
</h3>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
We can help with new vs. used options, payment hardware, and
whether free placement or a direct purchase makes more sense
for your location.
</p>
<div className="mt-6">
<Button
asChild
size="lg"
variant="outline"
className="min-h-11 rounded-full px-6"
>
<Link href="/contact-us#contact-form">Talk to Sales</Link>
</Button>
</div>
</div>
</PublicSurface>
</section>

View file

@ -11,7 +11,7 @@ import {
export function AboutPage() {
return (
<div className="public-page max-w-6xl">
<div className="public-page">
<Breadcrumbs
className="mb-6"
items={[{ label: "About Us", href: "/about-us" }]}

View file

@ -29,7 +29,37 @@ export function ContactPage() {
description="Use the form for repairs, moving, manuals, machine sales, or general questions. If you'd rather talk now, call us during business hours."
/>
<div className="mt-10 grid gap-8 lg:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)] lg:items-start">
<section className="mt-10 grid gap-4 lg:grid-cols-3">
<PublicInset className="h-full p-5">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
Repairs
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
Include the machine model, what the machine is doing, and any photos
or videos that can help us triage the issue faster.
</p>
</PublicInset>
<PublicInset className="h-full p-5">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
Sales or Placement
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
Tell us about your location, traffic, and whether you are asking
about free placement, machine sales, or both.
</p>
</PublicInset>
<PublicInset className="h-full p-5">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
Manuals or Parts
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
Share the machine brand and model so we can point you toward the
right part, manual, or support path.
</p>
</PublicInset>
</section>
<div className="mt-6 grid gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(320px,0.9fr)] lg:items-start">
<PublicSurface id="contact-form" as="section" className="p-5 md:p-7">
<div className="mb-6 flex flex-wrap items-center gap-3">
<div className="rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-primary">
@ -45,7 +75,21 @@ export function ContactPage() {
/>
</PublicSurface>
<aside className="space-y-5">
<aside className="space-y-4 lg:sticky lg:top-28">
<PublicSurface className="p-6">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
Quick Guidance
</p>
<h2 className="mt-2 text-2xl font-semibold text-foreground">
We&apos;ll route you to the right next step
</h2>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
If you are not sure whether to ask about placement, repairs,
moving, manuals, or sales, that&apos;s fine. Send the details you
have and we&apos;ll help sort it out.
</p>
</PublicSurface>
<PublicSurface className="overflow-hidden p-6">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
Direct Options
@ -59,10 +103,10 @@ export function ContactPage() {
below.
</p>
<div className="mt-6 space-y-4">
<div className="mt-5 space-y-3">
<a
href={businessConfig.publicCallUrl}
className="flex items-start gap-4 rounded-2xl border border-border/60 bg-white px-4 py-4 transition hover:border-primary/35"
className="flex items-start gap-4 rounded-2xl border border-border/55 bg-background/65 px-4 py-4 transition hover:border-primary/35"
>
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-primary/10 text-primary">
<Phone className="h-5 w-5" />
@ -80,7 +124,7 @@ export function ContactPage() {
<a
href={`mailto:${businessConfig.email}?Subject=Rocky%20Mountain%20Vending%20Inquiry`}
className="flex items-start gap-4 rounded-2xl border border-border/60 bg-white px-4 py-4 transition hover:border-primary/35"
className="flex items-start gap-4 rounded-2xl border border-border/55 bg-background/65 px-4 py-4 transition hover:border-primary/35"
>
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-primary/10 text-primary">
<Mail className="h-5 w-5" />
@ -113,7 +157,7 @@ export function ContactPage() {
</div>
</div>
<div className="mt-5 space-y-2">
<div className="mt-4 space-y-2">
{businessHours.map((schedule) => (
<PublicInset
key={schedule.day}

View file

@ -21,6 +21,7 @@ interface DropdownPageShellProps {
title: string
description?: string
headerContent?: ReactNode
contentIntro?: ReactNode
content: ReactNode
contentClassName?: string
contentSurfaceClassName?: string
@ -41,6 +42,7 @@ export function DropdownPageShell({
title,
description,
headerContent,
contentIntro,
content,
contentClassName,
contentSurfaceClassName,
@ -61,11 +63,32 @@ export function DropdownPageShell({
{headerContent}
</PublicPageHeader>
<section className="mt-10">
{contentIntro ? (
<section className="mt-10 grid gap-4 lg:grid-cols-2">{contentIntro}</section>
) : null}
<section className={cn(contentIntro ? "mt-6" : "mt-10")}>
<PublicSurface
className={cn("overflow-hidden", contentSurfaceClassName)}
className={cn(
"relative overflow-hidden p-0 md:p-0",
contentSurfaceClassName
)}
>
<div className={cn("max-w-none", contentClassName)}>{content}</div>
<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>
<p className="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-primary/80">
Location Guide
</p>
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
How Rocky Mountain Vending typically approaches this type of
location, from placement fit to service expectations.
</p>
</div>
</div>
<div className={cn("mx-auto max-w-3xl", contentClassName)}>{content}</div>
</div>
</PublicSurface>
</section>
@ -73,7 +96,8 @@ export function DropdownPageShell({
{cta ? (
<section className="mt-12">
<PublicSurface className="text-center">
<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 ? (
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
{cta.eyebrow}
@ -99,7 +123,7 @@ export function DropdownPageShell({
))}
</div>
{cta.note ? (
<PublicInset className="mx-auto mt-6 max-w-2xl text-left sm:text-center">
<PublicInset className="mx-auto mt-6 max-w-2xl border-primary/10 text-left sm:text-center">
{cta.note}
</PublicInset>
) : null}

View file

@ -8,15 +8,15 @@ const features = [
title: "Free Placement",
description:
"For qualifying locations, we can place the machines, stock them, and stay responsible for day-to-day service after install.",
link: "/about-us",
link: "/#how-it-works",
linkText: "How placement works",
},
{
icon: Wrench,
title: "Repairs and Service",
title: "Repairs and Services",
description:
"We handle repairs, restocking, and routine service so your team does not have to manage the machines.",
link: "/services/repairs",
link: "/services/repairs#request-service",
linkText: "Repair services",
},
{

View file

@ -19,7 +19,7 @@ export function Footer() {
<div className="mx-auto w-full max-w-[var(--public-shell-max)] px-4 py-14 sm:px-5 md:py-20 lg:px-6">
<div className="grid gap-6 lg:grid-cols-[1.2fr_0.9fr_0.9fr_1fr]">
{/* Company Info */}
<div className="rounded-[2rem] border border-border/60 bg-white/92 p-6 shadow-[0_18px_48px_rgba(15,23,42,0.07)] lg:col-span-1">
<div className="px-6 py-5 lg:col-span-1">
<Link href="/" className="inline-flex">
<Image
src="/rmv-logo.png"
@ -104,7 +104,7 @@ export function Footer() {
</div>
{/* Services */}
<div className="footer-section rounded-[2rem] border border-border/60 bg-white/88 px-5 py-5 shadow-[0_14px_38px_rgba(15,23,42,0.06)]">
<div className="px-5 py-5">
<h3 className="font-semibold mb-5 text-base">Services</h3>
<ul className="space-y-3 text-sm text-muted-foreground">
<li>
@ -159,7 +159,7 @@ export function Footer() {
</div>
{/* Company */}
<div className="footer-section rounded-[2rem] border border-border/60 bg-white/88 px-5 py-5 shadow-[0_14px_38px_rgba(15,23,42,0.06)]">
<div className="px-5 py-5">
<h3 className="font-semibold mb-5 text-base">Company</h3>
<ul className="space-y-3 text-sm text-muted-foreground">
<li>
@ -198,7 +198,7 @@ export function Footer() {
</div>
{/* Service Areas */}
<div className="footer-section rounded-[2rem] border border-border/60 bg-white/88 px-5 py-5 shadow-[0_14px_38px_rgba(15,23,42,0.06)]">
<div className="px-5 py-5">
<h3 className="font-semibold mb-5 text-base">Service Areas</h3>
<ul className="space-y-3 text-sm text-muted-foreground">
<li>

View file

@ -162,9 +162,16 @@ export function Header() {
{ label: "Reviews", href: "/reviews" },
{ label: "FAQs", href: "/about/faqs" },
]
const moreItems = [
{ label: "Food & Beverage", href: "/food-and-beverage/healthy-options" },
{ label: "Blog Posts", href: "/blog" },
{ label: "About Us", href: "/about-us" },
{ label: "Products", href: "/products" },
{ label: "Service Areas", href: "/service-areas" },
]
const desktopLinkClassName =
"rounded-full px-3 py-2 text-sm font-medium text-foreground transition hover:text-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15"
"inline-flex items-center whitespace-nowrap rounded-full px-2.5 py-2 text-[0.95rem] font-medium text-foreground transition hover:text-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15 lg:px-3 lg:text-sm"
const mobileLinkClassName =
"flex min-h-11 items-center rounded-[1rem] px-4 text-sm font-medium text-foreground transition hover:bg-primary/6 hover:text-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15"
const mobileGroupButtonClassName =
@ -175,7 +182,7 @@ export function Header() {
return (
<header className="sticky top-0 z-40 w-full border-b border-border/50 bg-white/92 shadow-[0_10px_35px_rgba(15,23,42,0.06)] backdrop-blur supports-[backdrop-filter]:bg-white/80">
<div className="mx-auto w-full max-w-[var(--public-shell-max)] px-4 sm:px-5 lg:px-6">
<div className="flex h-[var(--header-height)] items-center justify-between gap-3 lg:gap-6">
<div className="flex h-[var(--header-height)] items-center justify-between gap-3 md:hidden lg:gap-6">
{/* Logo */}
<Link
href="/"
@ -192,7 +199,7 @@ export function Header() {
</Link>
{/* Desktop Navigation */}
<nav className="hidden flex-1 items-center justify-center gap-1 md:flex lg:gap-2">
<nav className="hidden flex-1 items-center justify-center gap-1 2xl:flex 2xl:gap-2">
<Link href="/" className={desktopLinkClassName}>
Home
</Link>
@ -371,7 +378,7 @@ export function Header() {
</nav>
{/* Desktop CTA */}
<div className="hidden flex-shrink-0 items-center gap-2 md:flex lg:gap-3">
<div className="hidden flex-shrink-0 items-center gap-2 2xl:flex 2xl:gap-3">
<CartButton
onClick={() => dispatch({ type: "SET_CART", value: true })}
/>
@ -393,7 +400,7 @@ export function Header() {
{/* Mobile Menu Button */}
<button
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-border/60 bg-white text-foreground transition hover:border-primary/35 hover:text-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15 md:hidden"
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-border/60 bg-white text-foreground transition hover:border-primary/35 hover:text-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15 2xl:hidden"
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
aria-label="Toggle menu"
aria-expanded={state.isMenuOpen}
@ -406,6 +413,161 @@ export function Header() {
</button>
</div>
<div className="hidden md:block">
<div className="flex min-h-[4.75rem] items-center justify-between gap-4 py-3">
<Link
href="/"
className="flex min-w-0 flex-shrink-0 items-center gap-2 rounded-full"
>
<Image
src="/rmv-logo.png"
alt="Rocky Mountain Vending"
width={220}
height={55}
className="h-12 w-auto object-contain lg:h-14"
priority
/>
</Link>
<div className="flex min-w-0 flex-shrink-0 items-center gap-2 lg:gap-3">
<CartButton
onClick={() => dispatch({ type: "SET_CART", value: true })}
/>
<a
href="tel:+14352339668"
className="inline-flex min-h-10 items-center gap-2 rounded-full border border-border/60 bg-white px-3 text-sm font-medium text-foreground transition hover:border-primary/35 hover:text-primary lg:px-4"
>
<Phone className="h-4 w-4 flex-shrink-0" />
<span className="hidden xl:inline">(435) 233-9668</span>
<span className="xl:hidden">Call</span>
</a>
<Button
onClick={() => dispatch({ type: "SET_MODAL", value: true })}
className="h-10 whitespace-nowrap rounded-full bg-primary px-4 text-sm hover:bg-primary/90 lg:px-5"
size="lg"
>
Get Free Machine
</Button>
</div>
</div>
<div className="flex min-h-[3.5rem] items-center justify-center border-t border-border/45 py-2">
<nav className="flex flex-wrap items-center justify-center gap-x-1 gap-y-2 lg:gap-x-2">
<Link href="/" className={desktopLinkClassName}>
Home
</Link>
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
desktopLinkClassName,
"gap-1.5 data-[state=open]:text-primary"
)}
>
Who We Serve
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("w-56", desktopDropdownClassName)}
>
{whoWeServeItems.map((item) => (
<DropdownMenuItem
key={item.href}
asChild
className="rounded-xl px-3 py-2.5"
>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
desktopLinkClassName,
"gap-1.5 data-[state=open]:text-primary"
)}
>
Vending Machines
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("w-64", desktopDropdownClassName)}
>
{vendingMachinesItems.map((item) => (
<DropdownMenuItem
key={item.href}
asChild
className="rounded-xl px-3 py-2.5"
>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
desktopLinkClassName,
"gap-1.5 data-[state=open]:text-primary"
)}
>
Services
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("w-72", desktopDropdownClassName)}
>
{servicesItems.map((item) => (
<DropdownMenuItem
key={item.href}
asChild
className="rounded-xl px-3 py-2.5"
>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Link href="/contact-us" className={desktopLinkClassName}>
Contact Us
</Link>
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
desktopLinkClassName,
"gap-1.5 data-[state=open]:text-primary"
)}
>
More
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={cn("w-56", desktopDropdownClassName)}
>
{moreItems.map((item) => (
<DropdownMenuItem
key={item.href}
asChild
className="rounded-xl px-3 py-2.5"
>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</nav>
</div>
</div>
{/* Mobile Navigation */}
{state.isMenuOpen && (
<nav className="border-t border-border/40 py-5 md:hidden">

View file

@ -102,7 +102,7 @@ export function PublicSurface({
return (
<Component
className={cn(
"rounded-[var(--public-surface-radius)] border border-border/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,251,243,0.96))] p-5 shadow-[var(--public-surface-shadow)] md:p-7",
"rounded-[var(--public-surface-radius)] border border-border/65 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(255,249,240,0.96))] p-5 shadow-[0_20px_52px_rgba(15,23,42,0.075)] md:p-7",
className
)}
{...props}
@ -120,7 +120,7 @@ export function PublicInset({
return (
<div
className={cn(
"rounded-[var(--public-inset-radius)] border border-border/60 bg-white/95 p-4 shadow-[0_10px_28px_rgba(15,23,42,0.06)]",
"rounded-[var(--public-inset-radius)] border border-border/55 bg-[linear-gradient(180deg,rgba(255,255,255,0.94),rgba(255,250,244,0.92))] p-4 shadow-[0_12px_30px_rgba(15,23,42,0.055)]",
className
)}
{...props}
@ -144,14 +144,16 @@ export function PublicSectionHeader({
className,
}: PublicSectionHeaderProps) {
return (
<div className={cn("space-y-2", className)}>
<div className={cn("space-y-2.5", className)}>
<p className="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-primary/80">
{eyebrow}
</p>
<h2 className="text-xl font-semibold tracking-tight text-foreground md:text-[1.375rem]">
{title}
</h2>
<p className="text-sm leading-6 text-muted-foreground">{description}</p>
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
{description}
</p>
</div>
)
}

View file

@ -11,7 +11,7 @@ export function RequestMachineSection() {
<section id="request-machine" className="bg-background py-16 md:py-24">
<div className="container mx-auto px-4">
<div className="grid gap-8 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-start">
<PublicSurface className="bg-white p-6 md:p-8 lg:sticky lg:top-28">
<PublicSurface className="p-6 md:p-8 lg:sticky lg:top-28">
<div className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-primary">
<Package className="h-4 w-4" />
Free Placement
@ -61,7 +61,7 @@ export function RequestMachineSection() {
</div>
</PublicSurface>
<PublicSurface className="bg-white p-5 md:p-7">
<PublicSurface className="p-5 md:p-7">
<RequestMachineForm
onSubmit={(data) =>
console.log("Machine request form submitted:", data)

View file

@ -71,7 +71,7 @@ export function ReviewsPage() {
<div className="mt-6">
<iframe
className="lc_reviews_widget min-h-[900px] w-full rounded-[1.5rem] border border-border/60 bg-background"
className="lc_reviews_widget min-h-[780px] w-full rounded-[1.5rem] border border-border/60 bg-background"
src="https://reputationhub.site/reputation/widgets/review_widget/YAoWLgNSid8oG44j9BjG"
frameBorder="0"
scrolling="no"
@ -81,7 +81,7 @@ export function ReviewsPage() {
</PublicSurface>
</section>
<section className="mt-12 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<section className="mt-12 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
<PublicSurface>
<h2 className="text-3xl font-semibold tracking-tight text-balance">
What businesses usually want to verify before they choose a vendor
@ -120,21 +120,32 @@ export function ReviewsPage() {
<PublicSurface className="flex flex-col justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Next Step
Why It Matters
</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
Want to see whether your location qualifies?
Reviews are usually the last confidence check before someone reaches out.
</h2>
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
Tell us about your traffic, breakroom, or customer area and
we&apos;ll help you decide between free placement, machine sales,
or service help.
Most businesses are trying to verify the same things: follow-through,
communication, and whether the machines stay stocked and working
after install. If that sounds like your checklist too, we can help
you sort through next steps quickly.
</p>
</div>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
<div className="mt-6 grid gap-4">
<PublicInset className="p-5">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
Common Questions
</p>
<ul className="mt-3 space-y-3 text-sm leading-relaxed text-muted-foreground">
<li>Does this location qualify for free placement?</li>
<li>Can Rocky handle repairs and restocking without extra staff work on our side?</li>
<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/60 bg-white p-5 text-left transition hover:border-primary/30 hover:text-primary"
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
@ -146,7 +157,7 @@ export function ReviewsPage() {
</Link>
<Link
href="/contact-us#contact-form"
className="rounded-[1.5rem] border border-border/60 bg-white p-5 text-left transition hover:border-primary/30 hover:text-primary"
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
@ -157,6 +168,16 @@ export function ReviewsPage() {
</p>
</Link>
</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">
Looking for a direct answer?
</p>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
Tell us about your location, traffic, and what kind of help you
need. We&apos;ll point you toward the right option instead of
making you guess between service pages.
</p>
</div>
</PublicSurface>
</section>
</div>

View file

@ -4,6 +4,8 @@ import { ReactNode } from "react"
import { CheckCircle2 } from "lucide-react"
import { DropdownPageShell } from "@/components/dropdown-page-shell"
import { PublicInset } from "@/components/public-surface"
import { Button } from "@/components/ui/button"
import Link from "next/link"
interface WhoWeServePageProps {
title: string
@ -55,6 +57,51 @@ export function WhoWeServePage({
description ||
"See how Rocky Mountain Vending adapts machine placement, product mix, and ongoing service to the way this kind of location actually runs."
}
headerContent={
<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
<Button asChild size="lg" className="min-h-11 rounded-full px-6">
<Link href="/contact-us#contact-form">Talk to Our Team</Link>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="min-h-11 rounded-full px-6"
>
<Link href="/#request-machine">See If You Qualify</Link>
</Button>
</div>
}
contentIntro={
<>
<PublicInset className="h-full p-5 md:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
How We Tailor Service
</p>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance text-foreground">
We shape the setup around the pace of the location.
</h2>
<ul className="mt-4 space-y-3 text-sm leading-relaxed text-muted-foreground">
<li>Machine type and product mix matched to how people actually use the space.</li>
<li>Placement recommendations based on traffic flow, break patterns, and visibility.</li>
<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">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
Good Fit Signals
</p>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance text-foreground">
These are usually the reasons businesses reach out first.
</h2>
<ul className="mt-4 space-y-3 text-sm leading-relaxed text-muted-foreground">
<li>Your team or visitors need easier access to drinks, snacks, or convenience items on site.</li>
<li>You want a cleaner vending setup without daily oversight falling back on your staff.</li>
<li>You need local follow-through when a machine needs restocking, repair, or payment support.</li>
</ul>
</PublicInset>
</>
}
content={
<div className="text-foreground">{content}</div>
}