Property Management Network — Next.js 16 (App Router), Better Auth, Drizzle ORM over PostgreSQL, Stripe, OpenAI, Resend. Includes: - Security hardening: access-control/IDOR fixes, TLS-by-default DB layer, constant-time cron auth, strict security headers, atomic AI quota gating, HTML/email output encoding, demo-backdoor disabled in production. - Superadmin dashboard at /admin (overview/MRR, server-paginated users with ban/impersonate/plan/delete, billing, platform activity + admin audit log, AI usage, system health) via the Better Auth admin plugin. - Seed/migration utility scripts under scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.4 KiB
TypeScript
102 lines
4.4 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { motion, AnimatePresence } from "framer-motion"
|
|
import { Plus, Minus } from "lucide-react"
|
|
|
|
const FAQS = [
|
|
{
|
|
q: "Is there really a free plan?",
|
|
a: "Yes — the Starter plan is free forever. You get 1 property, up to 3 tenants, rent tracking, maintenance requests, and lease management. No credit card needed to sign up.",
|
|
},
|
|
{
|
|
q: "What happens when my trial ends?",
|
|
a: "There's no trial — paid plans start immediately when you upgrade. If you're on the free Starter plan, it never expires. You upgrade when you outgrow it.",
|
|
},
|
|
{
|
|
q: "Can I cancel anytime?",
|
|
a: "Yes. Cancel any time from your billing portal — no questions, no fees. Your data stays accessible for 30 days after cancellation so you can export everything.",
|
|
},
|
|
{
|
|
q: "Do tenants need to create an account?",
|
|
a: "No. Each tenant gets a unique private link to their portal — no account creation, no password. They can view rent history and submit maintenance requests instantly.",
|
|
},
|
|
{
|
|
q: "Does Property Management Network handle actual rent collection?",
|
|
a: "Yes — the Pro and Landlord plans include Stripe payment link generation. You can create a payment link for each tenant and they pay directly via card or bank transfer.",
|
|
},
|
|
{
|
|
q: "Is my data secure?",
|
|
a: "Yes. All data is stored in Supabase with row-level security (RLS) — meaning landlords only ever see their own data, and tenants only see their own records. All data is encrypted at rest and in transit.",
|
|
},
|
|
{
|
|
q: "Can I manage multiple properties?",
|
|
a: "The Starter plan supports 1 property. Pro supports up to 10. Landlord and Lifetime plans support unlimited properties and units.",
|
|
},
|
|
{
|
|
q: "What's included in the Lifetime deal?",
|
|
a: "The Lifetime plan is a one-time payment of $199. You get everything in the Landlord plan — unlimited properties, tenants, 25GB storage, AI calls, team access — with no recurring fees, ever. All future updates included.",
|
|
},
|
|
]
|
|
|
|
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) => (
|
|
<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"
|
|
>
|
|
<button
|
|
onClick={() => setOpen(open === i ? null : 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 ${
|
|
open === i ? "border-indigo-500/40 bg-indigo-500/10" : "border-white/10"
|
|
}`}>
|
|
{open === i
|
|
? <Minus className="h-3 w-3 text-indigo-400" />
|
|
: <Plus className="h-3 w-3 text-white/50" />
|
|
}
|
|
</div>
|
|
</button>
|
|
<AnimatePresence initial={false}>
|
|
{open === i && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: "auto", opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.25, ease: "easeInOut" }}
|
|
>
|
|
<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>
|
|
)}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|