Files
property-management-network/components/marketing/faq.tsx
T
Leon SerfatyandClaude Opus 5 1d02598786 chore: sync in-progress work across marketing, admin, API and tests
Snapshot of uncommitted work that had accumulated in the tree alongside
the Turnstile changes:

- marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and
  structured data
- admin billing actions and a per-user portfolio view, plus an admin
  error boundary
- rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface
- CSP and proxy adjustments, accounting/webhook lib updates
- Playwright config and an e2e/unit test suite
- next bumped to ^16.3.4 with the lockfile regenerated
- generated AGENTS.md / CLAUDE.md

Authored by other sessions working in this tree; committed here so the
Turnstile work could be pushed without leaving the tree dirty.
Typecheck passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 16:27:16 -04:00

87 lines
3.4 KiB
TypeScript

"use client"
import { useState } from "react"
import { motion } from "framer-motion"
import { Plus, Minus } from "lucide-react"
import { FAQS } from "@/lib/marketing/faqs"
export function FAQ() {
const [open, setOpen] = useState<number | null>(null)
return (
<section id="faq" className="mx-auto max-w-3xl px-4 sm:px-6 py-16 sm:py-24">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-3">FAQ</p>
<h2 className="text-3xl font-bold text-white">Common questions</h2>
<p className="mt-3 text-white/50">Everything you need to know before signing up.</p>
</motion.div>
<div className="space-y-3">
{FAQS.map((faq, i) => {
const isOpen = open === i
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.05 }}
className="rounded-xl border border-white/[0.06] bg-[#16161f] overflow-hidden"
>
<h3>
<button
type="button"
onClick={() => setOpen(isOpen ? null : i)}
aria-expanded={isOpen}
aria-controls={`faq-answer-${i}`}
id={`faq-question-${i}`}
className="flex w-full items-center justify-between px-5 py-4 text-left"
>
<span className="text-sm font-medium text-white pr-4">{faq.q}</span>
<div
className={`shrink-0 flex h-6 w-6 items-center justify-center rounded-full border transition-colors ${
isOpen ? "border-indigo-500/40 bg-indigo-500/10" : "border-white/10"
}`}
>
{isOpen ? (
<Minus className="h-3 w-3 text-indigo-400" />
) : (
<Plus className="h-3 w-3 text-white/50" />
)}
</div>
</button>
</h3>
{/*
The answer stays mounted and is collapsed by animating its height
rather than being conditionally rendered. Google requires the
answer text behind an FAQ accordion to be present in the served
HTML — unmounting it when closed would leave the FAQPage JSON-LD
in components/marketing/structured-data.tsx describing content no
crawler can see.
*/}
<motion.div
id={`faq-answer-${i}`}
role="region"
aria-labelledby={`faq-question-${i}`}
initial={false}
animate={{ height: isOpen ? "auto" : 0, opacity: isOpen ? 1 : 0 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
className="overflow-hidden"
>
<p className="px-5 pb-5 text-sm text-white/50 leading-relaxed border-t border-white/[0.04] pt-3">
{faq.a}
</p>
</motion.div>
</motion.div>
)
})}
</div>
</section>
)
}