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>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user