Files
Leon SerfatyandClaude Opus 4.8 857b9a7811 Initial import: property management SaaS + security hardening + admin dashboard
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>
2026-06-23 20:36:07 -04:00

120 lines
4.7 KiB
TypeScript

"use client"
import { motion } from "framer-motion"
import { Star } from "lucide-react"
const TESTIMONIALS = [
{
name: "James T.",
role: "Landlord · 6 properties · Austin, TX",
body: "Property Management Network replaced three separate spreadsheets. My rent collection rate went from 80% to 98% in the first month because tenants actually get reminders now.",
rating: 5,
initials: "JT",
color: "from-indigo-500 to-violet-600",
},
{
name: "Maria S.",
role: "Property Manager · 22 units · Miami, FL",
body: "The maintenance portal alone is worth it. Tenants submit requests, I get notified, everything is tracked. No more texts at midnight. Absolute game changer.",
rating: 5,
initials: "MS",
color: "from-emerald-500 to-teal-600",
},
{
name: "David K.",
role: "Real estate investor · 4 properties · Denver, CO",
body: "I do my own books. The expense tracker and CSV export save me 3 hours every time I prepare for my accountant. Clean, fast, actually useful — rare.",
rating: 5,
initials: "DK",
color: "from-amber-500 to-orange-600",
},
{
name: "Rachel M.",
role: "Landlord · 2 properties · Seattle, WA",
body: "The tenant portal is brilliant. My tenants love having their own link. And lease expiry alerts saved me from an accidental month-to-month situation.",
rating: 5,
initials: "RM",
color: "from-rose-500 to-pink-600",
},
{
name: "Tom B.",
role: "Portfolio investor · 11 properties · Chicago, IL",
body: "Switched from a £150/month enterprise tool. Property Management Network does everything I actually use — at a fraction of the cost. The Lifetime deal was a no-brainer.",
rating: 5,
initials: "TB",
color: "from-blue-500 to-cyan-600",
},
{
name: "Aisha R.",
role: "Property manager · 8 units · London, UK",
body: "Gorgeous UI, fast, and actually works. I've tried 4 property management tools — this is the first one I've stuck with past 2 weeks.",
rating: 5,
initials: "AR",
color: "from-violet-500 to-purple-600",
},
]
function Stars({ count }: { count: number }) {
return (
<div className="flex gap-0.5">
{Array.from({ length: count }).map((_, i) => (
<Star key={i} className="h-4 w-4 fill-amber-400 text-amber-400" />
))}
</div>
)
}
export function Testimonials() {
return (
<section className="mx-auto max-w-7xl 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-14"
>
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-3">Social proof</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">Landlords love Property Management Network</h2>
<p className="mt-4 text-white/50 max-w-xl mx-auto">
Real feedback from property owners who ditched their spreadsheets.
</p>
{/* Aggregate rating */}
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-amber-500/20 bg-amber-500/10 px-4 py-1.5">
<Stars count={5} />
<span className="text-sm font-semibold text-white">5.0</span>
<span className="text-xs text-white/40">from 200+ reviews</span>
</div>
</motion.div>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{TESTIMONIALS.map((t, i) => (
<motion.div
key={t.name}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.08 }}
whileHover={{ y: -4, scale: 1.01 }}
className="relative rounded-2xl border border-white/[0.06] bg-[#16161f] p-6 flex flex-col gap-4 group overflow-hidden cursor-default"
>
{/* Hover glow */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-br from-white/[0.02] to-transparent" />
<Stars count={t.rating} />
<p className="text-sm text-white/70 leading-relaxed flex-1 relative">"{t.body}"</p>
<div className="flex items-center gap-3 relative">
<div className={`flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br ${t.color} text-xs font-bold text-white shadow-lg`}>
{t.initials}
</div>
<div>
<p className="text-sm font-semibold text-white">{t.name}</p>
<p className="text-xs text-white/40">{t.role}</p>
</div>
</div>
</motion.div>
))}
</div>
</section>
)
}