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:
Leon Serfaty
2026-06-23 20:36:07 -04:00
co-authored by Claude Opus 4.8
commit 857b9a7811
291 changed files with 38996 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
"use client"
import Link from "next/link"
import { motion } from "framer-motion"
import { ArrowRight, CheckCircle2 } from "lucide-react"
export function CtaBanner() {
return (
<section className="mx-auto max-w-7xl px-4 sm:px-6 pb-16 sm:pb-24">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="relative overflow-hidden rounded-3xl border border-indigo-500/20 bg-gradient-to-br from-indigo-600/20 via-[#16161f] to-violet-600/20 p-8 sm:p-12 text-center"
>
{/* Glow blobs */}
<div className="absolute -top-20 left-1/2 -translate-x-1/2 h-60 w-60 rounded-full bg-indigo-600/30 blur-3xl pointer-events-none" />
<div className="absolute -bottom-20 left-1/3 h-40 w-40 rounded-full bg-violet-600/20 blur-3xl pointer-events-none" />
<div className="absolute -bottom-10 right-1/3 h-40 w-40 rounded-full bg-indigo-600/20 blur-3xl pointer-events-none" />
<div className="relative">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
>
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-4">Get started today</p>
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-white">
Stop managing rentals<br />the hard way
</h2>
<p className="mt-5 text-base sm:text-lg text-white/60 max-w-xl mx-auto">
Join 200+ landlords who replaced their spreadsheets with Property Management Network. Free plan, no credit card.
</p>
<div className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-3">
<Link
href="/signup"
className="flex w-full sm:w-auto items-center justify-center gap-2 rounded-xl bg-indigo-600 px-6 sm:px-8 py-3.5 sm:py-4 text-sm font-semibold text-white hover:bg-indigo-500 transition-all hover:shadow-xl hover:shadow-indigo-500/30 hover:-translate-y-0.5"
>
Create your free account <ArrowRight className="h-4 w-4" />
</Link>
<Link
href="/login"
className="w-full sm:w-auto rounded-xl border border-white/10 px-6 sm:px-8 py-3.5 sm:py-4 text-sm font-medium text-white/60 hover:text-white hover:bg-white/[0.05] transition text-center"
>
Already have an account?
</Link>
</div>
<div className="mt-6 flex flex-wrap items-center justify-center gap-x-4 sm:gap-x-6 gap-y-2 text-xs text-white/40">
{[
"Free plan forever",
"30-day money-back on paid plans",
"No credit card required",
"Cancel anytime",
].map((t) => (
<div key={t} className="flex items-center gap-1.5">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
{t}
</div>
))}
</div>
</motion.div>
</div>
</motion.div>
</section>
)
}
+41
View File
@@ -0,0 +1,41 @@
"use client"
import { useEffect, useState } from "react"
import { motion, useMotionValue, useSpring } from "framer-motion"
export function CursorGlow() {
const [visible, setVisible] = useState(false)
const mouseX = useMotionValue(-400)
const mouseY = useMotionValue(-400)
const springX = useSpring(mouseX, { stiffness: 80, damping: 20, mass: 0.5 })
const springY = useSpring(mouseY, { stiffness: 80, damping: 20, mass: 0.5 })
useEffect(() => {
const handler = (e: MouseEvent) => {
mouseX.set(e.clientX)
mouseY.set(e.clientY)
if (!visible) setVisible(true)
}
window.addEventListener("mousemove", handler, { passive: true })
return () => window.removeEventListener("mousemove", handler)
}, [visible, mouseX, mouseY])
return (
<div className="pointer-events-none fixed inset-0 z-[10] hidden lg:block overflow-hidden">
<motion.div
className="absolute rounded-full"
style={{
width: 500,
height: 500,
x: springX,
y: springY,
translateX: "-50%",
translateY: "-50%",
background: "radial-gradient(circle, rgba(99,102,241,0.07) 0%, rgba(139,92,246,0.04) 40%, transparent 70%)",
opacity: visible ? 1 : 0,
}}
/>
</div>
)
}
+101
View File
@@ -0,0 +1,101 @@
"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>
)
}
+144
View File
@@ -0,0 +1,144 @@
"use client"
import { motion } from "framer-motion"
import { CreditCard, Wrench, FileText, TrendingUp, Bell, Shield, Building2, Users } from "lucide-react"
const fadeUp = (delay = 0) => ({
initial: { opacity: 0, y: 24 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true },
transition: { duration: 0.5, delay },
})
// 8 cards — 2 large (col-span-2) + 4 small + 2 large on last row = clean 4-col grid
const BENTO = [
// Row 1: [Large 2] [Small 1] [Small 1]
{
icon: CreditCard,
title: "Rent Tracking",
body: "Log every payment, set due dates, and automatically mark overdue rent. Know exactly who has paid and who hasn't — at a glance.",
cols: "lg:col-span-2",
gradient: "from-indigo-600/30 via-indigo-600/10 to-transparent",
border: "border-indigo-500/30",
iconBg: "bg-indigo-500/20",
iconColor: "text-indigo-400",
glow: "group-hover:shadow-indigo-500/10",
},
{
icon: Wrench,
title: "Maintenance Portal",
body: "Tenants submit requests via their unique portal — no account needed. You track everything from open to resolved.",
cols: "lg:col-span-1",
gradient: "from-amber-600/25 via-amber-600/10 to-transparent",
border: "border-amber-500/30",
iconBg: "bg-amber-500/20",
iconColor: "text-amber-400",
glow: "group-hover:shadow-amber-500/10",
},
{
icon: Bell,
title: "Automated Reminders",
body: "Rent due emails go out automatically. Tenants pay on time without you chasing.",
cols: "lg:col-span-1",
gradient: "from-emerald-600/25 via-emerald-600/10 to-transparent",
border: "border-emerald-500/30",
iconBg: "bg-emerald-500/20",
iconColor: "text-emerald-400",
glow: "group-hover:shadow-emerald-500/10",
},
// Row 2: [Small 1] [Large 2] [Small 1]
{
icon: FileText,
title: "Lease Management",
body: "Track lease dates, get 60-day expiry alerts, and renew with one click — fully pre-filled from the previous lease.",
cols: "lg:col-span-1",
gradient: "from-blue-600/25 via-blue-600/10 to-transparent",
border: "border-blue-500/30",
iconBg: "bg-blue-500/20",
iconColor: "text-blue-400",
glow: "group-hover:shadow-blue-500/10",
},
{
icon: TrendingUp,
title: "Expense Reports & CSV Export",
body: "Categorise expenses per property, see your P&L, and export to CSV for your accountant in one click. All properties, all time.",
cols: "lg:col-span-2",
gradient: "from-violet-600/30 via-violet-600/10 to-transparent",
border: "border-violet-500/30",
iconBg: "bg-violet-500/20",
iconColor: "text-violet-400",
glow: "group-hover:shadow-violet-500/10",
},
{
icon: Shield,
title: "Secure by Default",
body: "Row-level security on every table. Tenants only see their own data — always.",
cols: "lg:col-span-1",
gradient: "from-rose-600/25 via-rose-600/10 to-transparent",
border: "border-rose-500/30",
iconBg: "bg-rose-500/20",
iconColor: "text-rose-400",
glow: "group-hover:shadow-rose-500/10",
},
// Row 3: [Large 2] [Large 2] — fills the row perfectly
{
icon: Building2,
title: "Multi-Property Management",
body: "One dashboard for all your properties and units. Switch between portfolios instantly — no per-property fees on the plans that matter.",
cols: "lg:col-span-2",
gradient: "from-cyan-600/25 via-cyan-600/10 to-transparent",
border: "border-cyan-500/30",
iconBg: "bg-cyan-500/20",
iconColor: "text-cyan-400",
glow: "group-hover:shadow-cyan-500/10",
},
{
icon: Users,
title: "Tenant Self-Service Portal",
body: "Every tenant gets a private link to view their rent history and submit maintenance requests — no login, no app, no friction.",
cols: "lg:col-span-2",
gradient: "from-fuchsia-600/25 via-fuchsia-600/10 to-transparent",
border: "border-fuchsia-500/30",
iconBg: "bg-fuchsia-500/20",
iconColor: "text-fuchsia-400",
glow: "group-hover:shadow-fuchsia-500/10",
},
]
export function Features() {
return (
<section id="features" className="mx-auto max-w-7xl px-4 sm:px-6 py-16 sm:py-24">
<motion.div {...fadeUp()} className="text-center mb-14">
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-3">Features</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">Everything you need nothing you don't</h2>
<p className="mt-4 text-white/50 max-w-xl mx-auto">
Built specifically for independent landlords. No enterprise bloat, no per-unit fees.
</p>
</motion.div>
{/* Bento grid — 4 columns, clean row fills */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{BENTO.map((card, i) => (
<motion.div
key={card.title}
{...fadeUp(i * 0.06)}
whileHover={{ scale: 1.02, y: -3 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className={`relative rounded-2xl border bg-[#16161f] bg-gradient-to-br p-6 overflow-hidden group cursor-default shadow-lg transition-shadow duration-300
${card.border} ${card.gradient} ${card.cols} ${card.glow}
`}
>
{/* Subtle inner glow on hover */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-br from-white/[0.04] to-transparent rounded-2xl" />
<div className={`mb-4 flex h-11 w-11 items-center justify-center rounded-xl ${card.iconBg} relative`}>
<card.icon className={`h-5 w-5 ${card.iconColor}`} />
</div>
<h3 className="font-semibold text-white mb-2 text-[15px]">{card.title}</h3>
<p className="text-sm text-white/55 leading-relaxed">{card.body}</p>
</motion.div>
))}
</div>
</section>
)
}
+52
View File
@@ -0,0 +1,52 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { motion, AnimatePresence } from "framer-motion"
import { ArrowRight, X } from "lucide-react"
export function FloatingCTA() {
const [show, setShow] = useState(false)
const [dismissed, setDismissed] = useState(false)
useEffect(() => {
const handler = () => {
if (!dismissed) setShow(window.scrollY > 700)
}
window.addEventListener("scroll", handler, { passive: true })
return () => window.removeEventListener("scroll", handler)
}, [dismissed])
return (
<div className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-[80]">
<AnimatePresence>
{show && !dismissed && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 16, scale: 0.9 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="relative flex items-center gap-2 sm:gap-3 rounded-2xl border border-indigo-500/30 bg-indigo-600 px-4 sm:px-5 py-3 shadow-2xl shadow-indigo-500/30"
>
<button
onClick={() => { setDismissed(true); setShow(false) }}
className="absolute -top-2 -right-2 flex h-5 w-5 items-center justify-center rounded-full border border-white/20 bg-[#16161f] text-white/50 hover:text-white transition"
>
<X className="h-3 w-3" />
</button>
<div>
<p className="text-xs font-bold text-white">Start free today</p>
<p className="text-[10px] text-indigo-200">No credit card required</p>
</div>
<Link
href="/signup"
className="flex items-center gap-1.5 rounded-xl bg-white px-4 py-2 text-xs font-bold text-indigo-600 hover:bg-indigo-50 transition-all hover:gap-2.5 whitespace-nowrap"
>
Get started <ArrowRight className="h-3.5 w-3.5" />
</Link>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
+79
View File
@@ -0,0 +1,79 @@
import Link from "next/link"
import { Building2, GitFork, Mail, ExternalLink } from "lucide-react"
const LINKS = {
Product: [
{ label: "Features", href: "#features" },
{ label: "Pricing", href: "#pricing" },
{ label: "How it works", href: "#how-it-works" },
{ label: "FAQ", href: "#faq" },
],
Platform: [
{ label: "Dashboard", href: "/login" },
{ label: "Tenant portal", href: "/tenant-portal-info" },
{ label: "API docs", href: "/api-docs" },
{ label: "Status", href: "/status" },
],
Legal: [
{ label: "Privacy policy", href: "/privacy" },
{ label: "Terms of service", href: "/terms" },
{ label: "Cookie policy", href: "/cookie-policy" },
{ label: "GDPR", href: "/gdpr" },
],
}
export function Footer() {
return (
<footer className="border-t border-white/[0.06] bg-[#09090b]">
<div className="mx-auto max-w-7xl px-4 sm:px-6 py-12 sm:py-14">
<div className="grid gap-10 sm:grid-cols-2 lg:grid-cols-5">
{/* Brand */}
<div className="lg:col-span-2">
<Link href="/" className="flex items-center gap-2 mb-4">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600">
<Building2 className="h-4 w-4 text-white" />
</div>
<span className="font-bold text-white">Property Management Network</span>
</Link>
<p className="text-sm text-white/40 max-w-xs leading-relaxed">
The property management platform built for independent landlords who want simplicity, not complexity.
</p>
<div className="mt-5 flex items-center gap-3">
{[
{ icon: ExternalLink, href: "https://twitter.com/propertymgmtnet", label: "Twitter" },
{ icon: GitFork, href: "https://github.com/propertymanagement-network", label: "GitHub" },
{ icon: Mail, href: "mailto:support@propertymanagement.network", label: "Email" },
].map(({ icon: Icon, href, label }) => (
<a key={label} href={href} aria-label={label}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-white/10 text-white/40 hover:text-white hover:border-white/20 transition">
<Icon className="h-3.5 w-3.5" />
</a>
))}
</div>
</div>
{/* Link columns */}
{Object.entries(LINKS).map(([section, links]) => (
<div key={section}>
<p className="text-xs font-semibold uppercase tracking-wider text-white/30 mb-4">{section}</p>
<ul className="space-y-2.5">
{links.map((l) => (
<li key={l.label}>
<Link href={l.href} className="text-sm text-white/50 hover:text-white transition">
{l.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-white/[0.06] pt-8 text-xs text-white/30">
<p>© {new Date().getFullYear()} Property Management Network. All rights reserved.</p>
<p>Built with Next.js · Supabase · Stripe · Resend</p>
</div>
</div>
</footer>
)
}
+306
View File
@@ -0,0 +1,306 @@
"use client"
import Link from "next/link"
import { motion, animate, useMotionValue, useInView } from "framer-motion"
import { ArrowRight, Building2, Wrench, CheckCircle2, Bell } from "lucide-react"
import { useRef, useEffect } from "react"
// ── Animated counter ─────────────────────────────────────────────
function Counter({ to, prefix = "", suffix = "" }: { to: number; prefix?: string; suffix?: string }) {
const ref = useRef<HTMLSpanElement>(null)
const inView = useInView(ref, { once: true, margin: "-80px" })
useEffect(() => {
if (!inView) return
const ctrl = animate(0, to, {
duration: 2,
ease: "easeOut",
onUpdate: (v) => {
if (ref.current) {
ref.current.textContent = prefix + Math.floor(v).toLocaleString() + suffix
}
},
})
return ctrl.stop
}, [inView, to, prefix, suffix])
return <span ref={ref}>{prefix}0{suffix}</span>
}
// ── Word-by-word text reveal ─────────────────────────────────────
function WordReveal({ text, className = "", delay = 0 }: { text: string; className?: string; delay?: number }) {
const words = text.split(" ")
return (
<span className={className}>
{words.map((word, i) => (
<motion.span
key={i}
initial={{ opacity: 0, y: 16, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ duration: 0.5, delay: delay + i * 0.08, ease: "easeOut" }}
className="inline-block mr-[0.25em]"
>
{word}
</motion.span>
))}
</span>
)
}
// ── Gradient border CTA button ───────────────────────────────────
function GradientBorderBtn({ href, children }: { href: string; children: React.ReactNode }) {
return (
<div className="gradient-border-wrapper">
<Link href={href} className="gradient-border-btn">
{children}
</Link>
</div>
)
}
// ── Mini dashboard mockup ────────────────────────────────────────
const DASHBOARD_CARDS = [
{ label: "Rent collected", value: "$12,400", sub: "This month", color: "text-emerald-400", dot: "bg-emerald-400" },
{ label: "Open requests", value: "3", sub: "Maintenance", color: "text-amber-400", dot: "bg-amber-400" },
{ label: "Occupancy", value: "94%", sub: "12/13 units", color: "text-indigo-400", dot: "bg-indigo-400" },
]
const STATS = [
{ prefix: "", to: 200, suffix: "+", label: "Active landlords" },
{ prefix: "", to: 5000, suffix: "+", label: "Units managed" },
{ prefix: "$", to: 2, suffix: "M+", label: "Rent tracked / mo" },
{ prefix: "", to: 98, suffix: "%", label: "Collection rate" },
]
function DashboardMockup() {
return (
<motion.div
initial={{ opacity: 0, y: 40, rotateX: 8 }}
animate={{ opacity: 1, y: 0, rotateX: 0 }}
transition={{ duration: 0.9, delay: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="relative w-full max-w-2xl mx-auto"
>
<div className="absolute -inset-4 bg-gradient-to-r from-indigo-600/20 via-violet-600/20 to-indigo-600/20 rounded-3xl blur-3xl" />
<div className="relative rounded-2xl border border-white/[0.08] bg-[#0f0f17] overflow-hidden shadow-2xl shadow-black/60">
{/* Browser chrome */}
<div className="flex items-center gap-2 border-b border-white/[0.06] bg-[#0a0a12] px-4 py-3">
<div className="flex gap-1.5">
<div className="h-3 w-3 rounded-full bg-red-500/60" />
<div className="h-3 w-3 rounded-full bg-amber-500/60" />
<div className="h-3 w-3 rounded-full bg-emerald-500/60" />
</div>
<div className="flex-1 mx-4 rounded-md bg-white/[0.04] px-3 py-1 text-xs text-white/30 font-mono">
app.propertymanagement.network/dashboard
</div>
</div>
<div className="p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-white/40">Good morning 👋</p>
<p className="text-sm font-semibold text-white">Dashboard Overview</p>
</div>
<div className="flex items-center gap-1.5 rounded-lg bg-indigo-600/20 border border-indigo-500/30 px-3 py-1.5">
<Bell className="h-3.5 w-3.5 text-indigo-400" />
<span className="text-xs text-indigo-300">2 alerts</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
{DASHBOARD_CARDS.map((c) => (
<div key={c.label} className="rounded-xl border border-white/[0.06] bg-white/[0.03] p-3">
<div className="flex items-center gap-1.5 mb-1.5">
<div className={`h-1.5 w-1.5 rounded-full ${c.dot}`} />
<span className="text-[10px] text-white/40">{c.label}</span>
</div>
<p className={`text-lg font-bold ${c.color}`}>{c.value}</p>
<p className="text-[10px] text-white/30">{c.sub}</p>
</div>
))}
</div>
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] overflow-hidden">
<div className="px-4 py-2.5 border-b border-white/[0.04]">
<p className="text-xs font-medium text-white/60">Recent Payments</p>
</div>
{[
{ name: "Sarah J.", amount: "$1,800", status: "paid", color: "text-emerald-400 bg-emerald-500/10" },
{ name: "Marcus L.", amount: "$1,350", status: "overdue", color: "text-red-400 bg-red-500/10" },
{ name: "Priya P.", amount: "$2,200", status: "pending", color: "text-amber-400 bg-amber-500/10" },
].map((row) => (
<div key={row.name} className="flex items-center justify-between px-4 py-2.5 border-b border-white/[0.03] last:border-0">
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-full bg-indigo-600/30 flex items-center justify-center text-[9px] font-bold text-indigo-300">
{row.name[0]}
</div>
<span className="text-xs text-white/70">{row.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-white">{row.amount}</span>
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${row.color}`}>{row.status}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Floating cards */}
<motion.div
animate={{ y: [0, -6, 0] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
className="absolute -right-8 top-16 hidden lg:block"
>
<div className="rounded-xl border border-emerald-500/20 bg-[#0f0f17]/90 backdrop-blur p-3 shadow-xl min-w-[140px]">
<div className="flex items-center gap-2 mb-1">
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
<span className="text-xs font-semibold text-white">Rent received</span>
</div>
<p className="text-lg font-bold text-emerald-400">$1,800</p>
<p className="text-[10px] text-white/40">Unit 1A · Sarah J.</p>
</div>
</motion.div>
<motion.div
animate={{ y: [0, 6, 0] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
className="absolute -left-8 bottom-24 hidden lg:block"
>
<div className="rounded-xl border border-amber-500/20 bg-[#0f0f17]/90 backdrop-blur p-3 shadow-xl min-w-[160px]">
<div className="flex items-center gap-2 mb-1">
<Wrench className="h-4 w-4 text-amber-400" />
<span className="text-xs font-semibold text-white">New request</span>
</div>
<p className="text-xs text-white/60">Leaking faucet</p>
<p className="text-[10px] text-white/40 mt-0.5">Unit 2B · 2 min ago</p>
</div>
</motion.div>
</motion.div>
)
}
// ── Hero ──────────────────────────────────────────────────────────
export function Hero() {
return (
<section className="relative overflow-hidden pt-24 sm:pt-32 pb-12 sm:pb-16 min-h-screen flex flex-col justify-center">
{/* Aurora background */}
<div className="absolute inset-0 -z-10">
<div className="absolute top-0 left-1/2 -translate-x-1/2 h-[600px] w-[900px] rounded-full bg-indigo-600/20 blur-[120px]" />
<div className="absolute top-20 left-1/4 h-[400px] w-[400px] rounded-full bg-violet-600/15 blur-[100px]" />
<div className="absolute top-40 right-1/4 h-[300px] w-[300px] rounded-full bg-blue-600/10 blur-[80px]" />
{/* Grid */}
<div className="absolute inset-0" style={{
backgroundImage: "linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px)",
backgroundSize: "60px 60px",
}} />
{/* Noise */}
<div className="absolute inset-0 opacity-[0.025]" style={{
backgroundImage: "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")",
}} />
</div>
<div className="mx-auto max-w-7xl px-4 sm:px-6 w-full">
<div className="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center">
{/* Left — copy */}
<div>
{/* Badge */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-6 inline-flex items-center gap-2 rounded-full border border-indigo-500/30 bg-indigo-500/10 px-4 py-1.5 text-xs font-medium text-indigo-300"
>
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-indigo-400" />
</span>
Trusted by 200+ landlords worldwide
</motion.div>
{/* Headline with word reveal */}
<h1 className="text-4xl font-bold leading-[1.1] tracking-tight sm:text-5xl lg:text-6xl">
<WordReveal text="Property management" delay={0.1} />
{" "}
<br className="hidden sm:block" />
<span className="bg-gradient-to-r from-indigo-400 via-violet-400 to-indigo-400 bg-clip-text text-transparent bg-[length:200%] animate-gradient">
<WordReveal text="without the chaos" delay={0.4} />
</span>
</h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.8 }}
className="mt-6 text-base sm:text-lg text-white/60 leading-relaxed"
>
Track rent, manage maintenance, monitor leases, and keep expenses organised all in one dashboard built for independent landlords.
</motion.p>
{/* CTAs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 1 }}
className="mt-8 flex flex-col sm:flex-row flex-wrap gap-3"
>
{/* Gradient border primary CTA */}
<div className="gradient-border-wrapper">
<Link
href="/signup"
className="gradient-border-btn flex items-center gap-2"
>
Start for free <ArrowRight className="h-4 w-4" />
</Link>
</div>
<Link
href="#features"
className="flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-6 py-3.5 text-sm font-medium text-white/70 hover:text-white hover:bg-white/[0.08] transition-all"
>
See how it works
</Link>
</motion.div>
{/* Trust signals */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 1.2 }}
className="mt-8 flex flex-wrap items-center gap-3 sm:gap-4 text-xs text-white/40"
>
{["Free plan forever", "No credit card required", "Setup in 5 minutes"].map((t) => (
<div key={t} className="flex items-center gap-1.5">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
{t}
</div>
))}
</motion.div>
{/* Animated stats */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 1.3 }}
className="mt-10 grid grid-cols-2 sm:grid-cols-4 gap-4 pt-8 border-t border-white/[0.06]"
>
{STATS.map(({ prefix, to, suffix, label }) => (
<div key={label}>
<p className="text-2xl font-bold text-white tabular-nums">
<Counter prefix={prefix} to={to} suffix={suffix} />
</p>
<p className="text-xs text-white/40 mt-0.5">{label}</p>
</div>
))}
</motion.div>
</div>
{/* Right — dashboard mockup */}
<div className="hidden lg:flex justify-center">
<DashboardMockup />
</div>
</div>
</div>
</section>
)
}
+97
View File
@@ -0,0 +1,97 @@
"use client"
import { motion } from "framer-motion"
import { Building2, Users, TrendingUp } from "lucide-react"
const STEPS = [
{
step: "01",
icon: Building2,
title: "Add your properties",
body: "Create properties, add units with rent amounts, and upload documents. Takes 3 minutes per property.",
color: "text-indigo-400",
bg: "bg-indigo-500/10 border-indigo-500/30",
shadow: "shadow-indigo-500/20",
},
{
step: "02",
icon: Users,
title: "Invite your tenants",
body: "Add tenant profiles, assign them to units, create leases. Each tenant automatically gets a private portal link.",
color: "text-violet-400",
bg: "bg-violet-500/10 border-violet-500/30",
shadow: "shadow-violet-500/20",
},
{
step: "03",
icon: TrendingUp,
title: "Run on autopilot",
body: "Rent reminders go out automatically. Maintenance gets logged. Lease expiries alert you 60 days early.",
color: "text-emerald-400",
bg: "bg-emerald-500/10 border-emerald-500/30",
shadow: "shadow-emerald-500/20",
},
]
export function HowItWorks() {
return (
<section id="how-it-works" className="relative 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-16"
>
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-3">How it works</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">Up and running in under 10 minutes</h2>
<p className="mt-4 text-white/50 max-w-xl mx-auto">
No training, no onboarding call. Sign up, add your properties, and you're managing.
</p>
</motion.div>
<div className="relative grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Connector line */}
<div className="absolute top-10 left-[calc(16.66%+2rem)] right-[calc(16.66%+2rem)] hidden md:block h-px bg-gradient-to-r from-indigo-500/40 via-violet-500/40 to-emerald-500/40" />
{STEPS.map((step, i) => (
<motion.div
key={step.step}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.15 }}
className="flex flex-col items-center text-center"
>
<motion.div
whileHover={{ scale: 1.08 }}
transition={{ type: "spring", stiffness: 300 }}
className={`relative z-10 mb-6 flex h-20 w-20 items-center justify-center rounded-2xl border shadow-2xl ${step.bg} ${step.shadow}`}
>
<step.icon className={`h-8 w-8 ${step.color}`} />
<div className="absolute -top-2.5 -right-2.5 flex h-6 w-6 items-center justify-center rounded-full bg-[#09090b] border border-white/10 text-[10px] font-bold text-white/50">
{step.step}
</div>
</motion.div>
<h3 className="text-lg font-semibold text-white mb-3">{step.title}</h3>
<p className="text-sm text-white/50 leading-relaxed max-w-xs">{step.body}</p>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.5 }}
className="mt-14 text-center"
>
<a
href="/signup"
className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-6 py-3 text-sm font-semibold text-white hover:bg-indigo-500 transition-all hover:shadow-lg hover:shadow-indigo-500/30 hover:-translate-y-0.5"
>
Start for free takes 3 minutes
</a>
</motion.div>
</section>
)
}
+105
View File
@@ -0,0 +1,105 @@
"use client"
import { motion } from "framer-motion"
import {
CreditCard, Mail, Cloud, Zap, BookOpen, MessageSquare,
BarChart3, Archive, FileText, Smartphone, Lock, RefreshCw,
} from "lucide-react"
const TOOLS_ROW1 = [
{ name: "Stripe", icon: CreditCard, color: "text-violet-400", bg: "bg-violet-500/10 border-violet-500/20", desc: "Payments" },
{ name: "Gmail", icon: Mail, color: "text-red-400", bg: "bg-red-500/10 border-red-500/20", desc: "Email" },
{ name: "Google Drive", icon: Cloud, color: "text-blue-400", bg: "bg-blue-500/10 border-blue-500/20", desc: "Storage" },
{ name: "Zapier", icon: Zap, color: "text-orange-400", bg: "bg-orange-500/10 border-orange-500/20", desc: "Automation" },
{ name: "Xero", icon: BarChart3, color: "text-cyan-400", bg: "bg-cyan-500/10 border-cyan-500/20", desc: "Accounting" },
{ name: "Dropbox", icon: Archive, color: "text-blue-300", bg: "bg-blue-400/10 border-blue-400/20", desc: "Documents" },
]
const TOOLS_ROW2 = [
{ name: "Slack", icon: MessageSquare, color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20", desc: "Notifications" },
{ name: "Notion", icon: FileText, color: "text-white/70", bg: "bg-white/5 border-white/10", desc: "Notes" },
{ name: "QuickBooks", icon: BookOpen, color: "text-green-400", bg: "bg-green-500/10 border-green-500/20", desc: "Accounting" },
{ name: "WhatsApp", icon: Smartphone, color: "text-emerald-300", bg: "bg-emerald-400/10 border-emerald-400/20", desc: "Reminders" },
{ name: "2FA / Auth", icon: Lock, color: "text-indigo-400", bg: "bg-indigo-500/10 border-indigo-500/20", desc: "Security" },
{ name: "Auto-sync", icon: RefreshCw, color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20", desc: "Sync" },
]
function ToolBadge({ name, icon: Icon, color, bg, desc }: typeof TOOLS_ROW1[0]) {
return (
<motion.div
whileHover={{ scale: 1.05, y: -2 }}
transition={{ type: "spring", stiffness: 300 }}
className={`flex shrink-0 items-center gap-3 rounded-xl border px-4 py-3 ${bg} cursor-default mx-3`}
>
<div className={`flex h-8 w-8 items-center justify-center rounded-lg bg-black/20`}>
<Icon className={`h-4 w-4 ${color}`} />
</div>
<div>
<p className="text-xs font-semibold text-white">{name}</p>
<p className="text-[10px] text-white/40">{desc}</p>
</div>
</motion.div>
)
}
export function Integrations() {
const doubled1 = [...TOOLS_ROW1, ...TOOLS_ROW1]
const doubled2 = [...TOOLS_ROW2, ...TOOLS_ROW2]
return (
<section className="py-14 sm:py-20 overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 mb-10 sm:mb-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center"
>
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-3">Integrations</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">Works with tools you already use</h2>
<p className="mt-4 text-white/50 max-w-xl mx-auto">
Property Management Network connects with your existing stack from payments to accounting to communication.
</p>
</motion.div>
</div>
{/* Marquee rows */}
<div className="space-y-4">
{/* Row 1 — left to right */}
<div className="relative flex overflow-hidden">
<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-32 bg-gradient-to-r from-[#09090b] to-transparent" />
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-32 bg-gradient-to-l from-[#09090b] to-transparent" />
<motion.div
animate={{ x: ["0%", "-50%"] }}
transition={{ duration: 22, ease: "linear", repeat: Infinity }}
className="flex"
>
{doubled1.map((t, i) => <ToolBadge key={i} {...t} />)}
</motion.div>
</div>
{/* Row 2 — right to left */}
<div className="relative flex overflow-hidden">
<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-32 bg-gradient-to-r from-[#09090b] to-transparent" />
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-32 bg-gradient-to-l from-[#09090b] to-transparent" />
<motion.div
animate={{ x: ["-50%", "0%"] }}
transition={{ duration: 25, ease: "linear", repeat: Infinity }}
className="flex"
>
{doubled2.map((t, i) => <ToolBadge key={i} {...t} />)}
</motion.div>
</div>
</div>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mt-8 text-center text-xs text-white/30"
>
+ more integrations via Zapier webhooks
</motion.p>
</section>
)
}
+39
View File
@@ -0,0 +1,39 @@
"use client"
import { motion } from "framer-motion"
const ITEMS = [
"🏠 Sarah J. paid $1,800 rent",
"📋 New lease signed — Unit 4B",
"🔧 Maintenance resolved — HVAC",
"✅ 94% occupancy this month",
"💰 $12,400 collected in April",
"📩 Reminder sent to 3 tenants",
"🏢 2 leases expiring this month",
"⭐ Tenant portal accessed 24× today",
]
export function Marquee() {
const doubled = [...ITEMS, ...ITEMS]
return (
<div className="relative overflow-hidden border-y border-white/[0.04] bg-white/[0.02] py-4">
{/* Fade edges */}
<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-24 bg-gradient-to-r from-[#09090b] to-transparent" />
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-24 bg-gradient-to-l from-[#09090b] to-transparent" />
<motion.div
animate={{ x: ["0%", "-50%"] }}
transition={{ duration: 28, ease: "linear", repeat: Infinity }}
className="flex gap-0 whitespace-nowrap"
>
{doubled.map((item, i) => (
<div key={i} className="flex items-center gap-8 px-8">
<span className="text-sm text-white/40">{item}</span>
<span className="text-white/10">·</span>
</div>
))}
</motion.div>
</div>
)
}
+104
View File
@@ -0,0 +1,104 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { motion, AnimatePresence } from "framer-motion"
import { Building2, Menu, X, ArrowRight } from "lucide-react"
const NAV_LINKS = [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how-it-works" },
{ label: "Pricing", href: "#pricing" },
{ label: "FAQ", href: "#faq" },
]
export function Navbar() {
const [scrolled, setScrolled] = useState(false)
const [open, setOpen] = useState(false)
useEffect(() => {
const handler = () => setScrolled(window.scrollY > 24)
window.addEventListener("scroll", handler, { passive: true })
return () => window.removeEventListener("scroll", handler)
}, [])
return (
<>
<motion.nav
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-[#09090b]/80 backdrop-blur-2xl border-b border-white/[0.06] shadow-2xl shadow-black/30"
: "bg-transparent"
}`}
>
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
<Link href="/" className="flex items-center gap-2 group">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 shadow-lg shadow-indigo-500/30 group-hover:shadow-indigo-500/50 transition-shadow">
<Building2 className="h-4 w-4 text-white" />
</div>
<span className="font-bold text-white tracking-tight">Property Management Network</span>
</Link>
<div className="hidden md:flex items-center gap-1">
{NAV_LINKS.map((l) => (
<Link
key={l.label}
href={l.href}
className="px-4 py-2 text-sm text-white/60 hover:text-white hover:bg-white/[0.06] rounded-lg transition-all"
>
{l.label}
</Link>
))}
</div>
<div className="hidden md:flex items-center gap-3">
<Link href="/login" className="text-sm text-white/60 hover:text-white transition px-3 py-2">
Sign in
</Link>
<Link
href="/signup"
className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 transition-all hover:shadow-lg hover:shadow-indigo-500/25"
>
Get started free <ArrowRight className="h-3.5 w-3.5" />
</Link>
</div>
<button onClick={() => setOpen(!open)} className="md:hidden p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/[0.06] transition">
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</motion.nav>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
className="fixed inset-x-0 top-16 z-40 bg-[#09090b]/95 backdrop-blur-2xl border-b border-white/[0.06] md:hidden"
>
<div className="flex flex-col px-6 py-5 gap-1">
{NAV_LINKS.map((l) => (
<Link key={l.label} href={l.href} onClick={() => setOpen(false)}
className="text-sm text-white/70 hover:text-white hover:bg-white/[0.04] rounded-lg px-3 py-2.5 transition">
{l.label}
</Link>
))}
<div className="flex flex-col gap-2 pt-3 mt-2 border-t border-white/[0.06]">
<Link href="/login" onClick={() => setOpen(false)} className="text-sm text-white/60 px-3 py-2">Sign in</Link>
<Link href="/signup" onClick={() => setOpen(false)}
className="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white text-center hover:bg-indigo-500 transition">
Get started free
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</>
)
}
+273
View File
@@ -0,0 +1,273 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { motion } from "framer-motion"
import { Check, X, Zap, ShieldCheck } from "lucide-react"
import { cn } from "@/lib/utils"
const PLANS = [
{
key: "starter",
name: "Starter",
monthlyPrice: 0,
description: "For landlords just getting started.",
highlight: false,
cta: "Get started free",
ctaHref: "/signup",
features: ["1 property", "Up to 3 tenants", "100 MB storage", "Rent tracking", "Maintenance portal", "Lease tracking", "Tenant portal"],
},
{
key: "pro",
name: "Pro",
monthlyPrice: 29,
description: "For active landlords growing their portfolio.",
highlight: true,
badge: "Most popular",
cta: "Start Pro",
ctaHref: "/signup",
features: ["10 properties", "Unlimited tenants", "5 GB storage", "50 AI calls/mo", "Email reminders", "Stripe payment links", "Lease expiry alerts", "CSV export"],
},
{
key: "landlord",
name: "Landlord",
monthlyPrice: 59,
description: "For serious portfolios at scale.",
highlight: false,
cta: "Start Landlord",
ctaHref: "/signup",
features: ["Unlimited properties", "Unlimited tenants", "25 GB storage", "200 AI calls/mo", "Team access", "White-label portal", "Priority support"],
},
{
key: "lifetime",
name: "Lifetime",
monthlyPrice: null,
fixedPrice: 199,
description: "Pay once, own it forever.",
highlight: false,
badge: "Best value",
cta: "Get lifetime access",
ctaHref: "/signup",
features: ["Everything in Landlord", "No recurring fees — ever", "All future updates", "White-label portal", "Team access", "25 GB storage"],
},
]
const COMPARISON = [
{ feature: "Properties", starter: "1", pro: "10", landlord: "Unlimited", lifetime: "Unlimited" },
{ feature: "Tenants", starter: "3", pro: "Unlimited", landlord: "Unlimited", lifetime: "Unlimited" },
{ feature: "Storage", starter: "100 MB", pro: "5 GB", landlord: "25 GB", lifetime: "25 GB" },
{ feature: "AI calls / month", starter: false, pro: "50", landlord: "200", lifetime: "200" },
{ feature: "Email reminders", starter: false, pro: true, landlord: true, lifetime: true },
{ feature: "Stripe payment links", starter: false, pro: true, landlord: true, lifetime: true },
{ feature: "Team access", starter: false, pro: false, landlord: true, lifetime: true },
{ feature: "White-label portal", starter: false, pro: false, landlord: true, lifetime: true },
{ feature: "CSV export", starter: true, pro: true, landlord: true, lifetime: true },
{ feature: "Lease expiry alerts", starter: true, pro: true, landlord: true, lifetime: true },
]
function Cell({ val }: { val: string | boolean }) {
if (val === true) return <Check className="mx-auto h-4 w-4 text-emerald-400" />
if (val === false) return <X className="mx-auto h-4 w-4 text-white/20" />
return <span className="text-sm font-medium text-white/80">{val}</span>
}
export function PricingSection() {
const [annual, setAnnual] = useState(false)
const DISCOUNT = 0.8
return (
<section id="pricing" className="relative py-24 overflow-hidden">
{/* Section background */}
<div className="absolute inset-0 -z-10 bg-gradient-to-b from-transparent via-indigo-950/20 to-transparent" />
<div className="absolute inset-0 -z-10" style={{
backgroundImage: "radial-gradient(ellipse 80% 50% at 50% 0%, rgba(99,102,241,0.08) 0%, transparent 70%)"
}} />
<div className="mx-auto max-w-6xl px-4 sm:px-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mb-12 text-center"
>
<p className="mb-3 text-xs font-semibold uppercase tracking-widest text-indigo-400">Pricing</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">Simple, honest pricing</h2>
<p className="mt-3 text-white/50">Start free, upgrade when you grow. No per-unit fees, ever.</p>
{/* Toggle */}
<div className="mt-8 inline-flex items-center rounded-full border border-white/10 bg-white/[0.04] p-1.5">
<button
onClick={() => setAnnual(false)}
className={cn(
"rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
!annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
)}
>
Monthly
</button>
<button
onClick={() => setAnnual(true)}
className={cn(
"flex items-center gap-1.5 sm:gap-2 rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
)}
>
Annual
<span className="rounded-full bg-emerald-500/20 px-2 py-0.5 text-[10px] font-bold text-emerald-400 border border-emerald-500/30">
20%
</span>
</button>
</div>
</motion.div>
{/* Plan cards */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4 items-start">
{PLANS.map((plan, i) => {
const isLifetime = plan.fixedPrice !== undefined
const displayPrice = isLifetime
? `$${plan.fixedPrice}`
: plan.monthlyPrice === 0
? "Free"
: annual
? `$${Math.round(plan.monthlyPrice! * DISCOUNT * 12)}`
: `$${plan.monthlyPrice}`
const interval = isLifetime
? " one-time"
: plan.monthlyPrice === 0
? ""
: annual ? "/yr" : "/mo"
return (
<motion.div
key={plan.key}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.08 }}
className={cn(
"relative flex flex-col rounded-2xl p-6 transition-all duration-300",
plan.highlight
? "border-2 border-indigo-500/60 bg-gradient-to-b from-indigo-600/15 to-[#16161f] shadow-2xl shadow-indigo-500/20 ring-1 ring-indigo-500/20"
: "border border-white/10 bg-[#16161f] hover:border-white/20"
)}
>
{plan.badge && (
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 z-10">
<span className={cn(
"flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold shadow-lg",
plan.highlight
? "bg-indigo-600 text-white shadow-indigo-500/40"
: "bg-[#1d1d28] border border-white/10 text-white/70"
)}>
<Zap className="h-3 w-3" />
{plan.badge}
</span>
</div>
)}
{/* Plan name + price */}
<div className="mb-6 pt-1">
<p className={cn("text-xs font-semibold uppercase tracking-wide mb-2", plan.highlight ? "text-indigo-400" : "text-white/40")}>
{plan.name}
</p>
<div className="flex items-baseline gap-1">
<span className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">{displayPrice}</span>
{interval && <span className="text-sm text-white/40 ml-0.5">{interval}</span>}
</div>
{annual && !isLifetime && plan.monthlyPrice! > 0 && (
<p className="mt-1 text-xs text-emerald-400 font-medium">
Saves ${Math.round(plan.monthlyPrice! * 12 * 0.2)}/year
</p>
)}
<p className="mt-2 text-xs text-white/40 leading-relaxed">{plan.description}</p>
</div>
{/* CTA */}
<Link
href={plan.ctaHref}
className={cn(
"mb-6 block rounded-xl px-4 py-2.5 text-center text-sm font-semibold transition-all",
plan.highlight
? "bg-indigo-600 text-white hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/30"
: "border border-white/10 bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white hover:border-white/20"
)}
>
{plan.cta}
</Link>
{/* Features */}
<ul className="flex-1 space-y-2.5">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<Check className={cn("mt-0.5 h-3.5 w-3.5 flex-shrink-0", plan.highlight ? "text-indigo-400" : "text-white/40")} />
<span className="text-xs text-white/60 leading-relaxed">{f}</span>
</li>
))}
</ul>
</motion.div>
)
})}
</div>
{/* Money-back guarantee */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mt-8 flex items-center justify-center gap-2 text-xs text-white/40"
>
<ShieldCheck className="h-4 w-4 text-emerald-400" />
<span>30-day money-back guarantee on all paid plans. No questions asked.</span>
</motion.div>
{/* Comparison table */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
className="mt-16"
>
<h3 className="mb-6 text-center text-lg font-semibold text-white">Full feature comparison</h3>
<div className="overflow-x-auto rounded-2xl border border-white/[0.08] bg-[#16161f]">
<table className="w-full min-w-[600px]">
<thead>
<tr className="border-b border-white/[0.08]">
<th className="px-5 py-4 text-left text-xs font-medium text-white/40 w-[40%]">Feature</th>
{[
{ name: "Starter", highlight: false },
{ name: "Pro", highlight: true },
{ name: "Landlord", highlight: false },
{ name: "Lifetime", highlight: false },
].map((h) => (
<th key={h.name} className={cn(
"px-4 py-4 text-center text-xs font-semibold",
h.highlight ? "text-indigo-400" : "text-white/60"
)}>
{h.name}
</th>
))}
</tr>
</thead>
<tbody>
{COMPARISON.map((row, i) => (
<tr key={row.feature} className={cn(
"border-b border-white/[0.04] last:border-0",
i % 2 === 0 ? "bg-transparent" : "bg-white/[0.015]"
)}>
<td className="px-5 py-3.5 text-sm text-white/60">{row.feature}</td>
<td className="px-4 py-3.5 text-center"><Cell val={row.starter} /></td>
<td className="px-4 py-3.5 text-center bg-indigo-500/[0.04]"><Cell val={row.pro} /></td>
<td className="px-4 py-3.5 text-center"><Cell val={row.landlord} /></td>
<td className="px-4 py-3.5 text-center"><Cell val={row.lifetime} /></td>
</tr>
))}
</tbody>
</table>
</div>
</motion.div>
</div>
</section>
)
}
+154
View File
@@ -0,0 +1,154 @@
import Link from "next/link"
import { Check, Zap } from "lucide-react"
import { cn } from "@/lib/utils"
const plans = [
{
name: "Starter",
price: "Free",
interval: "",
description: "For landlords just getting started.",
highlight: false,
cta: "Get started free",
ctaHref: "/signup",
features: [
"1 property",
"Up to 3 tenants",
"100 MB document storage",
"Rent tracking",
"Maintenance requests",
"Lease tracking",
"Tenant portal",
],
},
{
name: "Pro",
price: "$29",
interval: "/mo",
description: "For active landlords managing multiple properties.",
highlight: true,
badge: "Most popular",
cta: "Start Pro",
ctaHref: "/signup",
features: [
"Up to 10 properties",
"Unlimited tenants",
"5 GB document storage",
"AI rent receipts (50/mo)",
"AI maintenance reports",
"Automated rent reminders",
"Stripe payment links",
"Lease expiry alerts",
],
},
{
name: "Landlord",
price: "$59",
interval: "/mo",
description: "For serious portfolios that need full power.",
highlight: false,
cta: "Start Landlord",
ctaHref: "/signup",
features: [
"Unlimited properties",
"Unlimited tenants",
"25 GB document storage",
"AI calls (200/mo)",
"Team access",
"White-label tenant portal",
"Priority support",
],
},
{
name: "Lifetime",
price: "$199",
interval: " one-time",
description: "Pay once, own it forever.",
highlight: false,
badge: "Best value",
cta: "Get lifetime access",
ctaHref: "/signup",
features: [
"Everything in Landlord",
"No recurring fees — ever",
"All future updates included",
"White-label tenant portal",
"Team access",
"25 GB document storage",
],
},
]
export function Pricing() {
return (
<section id="pricing" className="py-24">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-14 text-center">
<p className="mb-3 text-sm font-semibold uppercase tracking-widest text-indigo-400">Pricing</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">Simple, honest pricing</h2>
<p className="mx-auto mt-4 max-w-lg text-white/50">
Start free, upgrade when you grow. No hidden fees, no per-unit charges.
</p>
</div>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{plans.map((plan) => (
<div
key={plan.name}
className={cn(
"relative flex flex-col rounded-2xl border p-6",
plan.highlight
? "border-indigo-500/50 bg-indigo-600/10 shadow-xl shadow-indigo-600/10"
: "border-white/[0.06] bg-[#16161f]"
)}
>
{plan.badge && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className={cn(
"flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold",
plan.highlight
? "bg-indigo-600 text-white"
: "bg-white/10 text-white/70"
)}>
<Zap className="h-3 w-3" />
{plan.badge}
</span>
</div>
)}
<div className="mb-5">
<p className="text-sm font-semibold text-white/60">{plan.name}</p>
<div className="mt-1 flex items-baseline gap-0.5">
<span className="text-3xl font-extrabold text-white">{plan.price}</span>
{plan.interval && <span className="text-sm text-white/40">{plan.interval}</span>}
</div>
<p className="mt-2 text-xs leading-relaxed text-white/40">{plan.description}</p>
</div>
<Link
href={plan.ctaHref}
className={cn(
"mb-6 block rounded-lg px-4 py-2.5 text-center text-sm font-semibold transition",
plan.highlight
? "bg-indigo-600 text-white hover:bg-indigo-500"
: "border border-white/10 text-white/70 hover:border-white/20 hover:text-white"
)}
>
{plan.cta}
</Link>
<ul className="flex-1 space-y-2.5">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2">
<Check className="mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-indigo-400" />
<span className="text-xs text-white/60">{f}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
)
}
+99
View File
@@ -0,0 +1,99 @@
"use client"
import { motion } from "framer-motion"
import { useInView } from "framer-motion"
import { useRef } from "react"
import { X, Check } from "lucide-react"
const BEFORE = [
"Chasing rent via text message every month",
"Spreadsheets breaking when you add a new property",
"Tenants calling you at 11pm about maintenance",
"Losing track of lease expiry dates",
"No idea which property is actually profitable",
"Receipts and invoices scattered in email",
]
const AFTER = [
"Automated reminders — tenants pay on time",
"One dashboard for all properties, all tenants",
"Tenants submit requests through their portal",
"60-day lease expiry alerts, auto-renewal ready",
"Per-property expense tracking and P&L view",
"Expenses categorised and exportable as CSV",
]
function fadeIn(delay = 0) {
return {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true },
transition: { duration: 0.5, delay },
}
}
export function Problem() {
return (
<section className="mx-auto max-w-7xl px-4 sm:px-6 py-16 sm:py-24">
<motion.div {...fadeIn()} className="text-center mb-14">
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-400 mb-3">Sound familiar?</p>
<h2 className="text-3xl font-bold text-white sm:text-4xl">The old way is exhausting</h2>
<p className="mt-4 text-white/50 max-w-xl mx-auto">
Most landlords are drowning in WhatsApp messages, broken spreadsheets, and missed follow-ups. There's a better way.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Before */}
<motion.div {...fadeIn(0.1)} className="rounded-2xl border border-red-500/20 bg-red-500/[0.04] p-7">
<div className="flex items-center gap-2 mb-6">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-red-500/20">
<X className="h-4 w-4 text-red-400" />
</div>
<h3 className="font-semibold text-red-400">Before Property Management Network</h3>
</div>
<ul className="space-y-3">
{BEFORE.map((item, i) => (
<motion.li
key={item}
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.15 + i * 0.07 }}
className="flex items-start gap-3 text-sm text-white/60"
>
<X className="h-4 w-4 text-red-500/60 mt-0.5 shrink-0" />
{item}
</motion.li>
))}
</ul>
</motion.div>
{/* After */}
<motion.div {...fadeIn(0.2)} className="rounded-2xl border border-emerald-500/20 bg-emerald-500/[0.04] p-7">
<div className="flex items-center gap-2 mb-6">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500/20">
<Check className="h-4 w-4 text-emerald-400" />
</div>
<h3 className="font-semibold text-emerald-400">With Property Management Network</h3>
</div>
<ul className="space-y-3">
{AFTER.map((item, i) => (
<motion.li
key={item}
initial={{ opacity: 0, x: 10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.15 + i * 0.07 }}
className="flex items-start gap-3 text-sm text-white/60"
>
<Check className="h-4 w-4 text-emerald-400 mt-0.5 shrink-0" />
{item}
</motion.li>
))}
</ul>
</motion.div>
</div>
</section>
)
}
+16
View File
@@ -0,0 +1,16 @@
"use client"
import { useScroll, motion } from "framer-motion"
export function ScrollProgress() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 z-[200] h-[2px] origin-left"
style={{
scaleX: scrollYProgress,
background: "linear-gradient(90deg, #6366f1, #8b5cf6, #06b6d4)",
}}
/>
)
}
@@ -0,0 +1,68 @@
"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Home } from "lucide-react"
const NOTIFICATIONS = [
{ name: "James T.", location: "Austin, TX", action: "just upgraded to Pro", time: "2 min ago" },
{ name: "Sarah M.", location: "London, UK", action: "just signed up", time: "4 min ago" },
{ name: "David K.", location: "Denver, CO", action: "got Lifetime access", time: "7 min ago" },
{ name: "Priya R.", location: "Toronto, CA", action: "just signed up", time: "12 min ago" },
{ name: "Marcus L.", location: "Miami, FL", action: "upgraded to Pro", time: "15 min ago" },
{ name: "Rachel W.", location: "Seattle, WA", action: "just signed up", time: "18 min ago" },
{ name: "Tom B.", location: "Chicago, IL", action: "got Lifetime access", time: "23 min ago" },
{ name: "Aisha N.", location: "New York, NY", action: "just signed up", time: "27 min ago" },
]
export function SocialProofToast() {
const [index, setIndex] = useState(0)
const [visible, setVisible] = useState(false)
useEffect(() => {
// First show after 6s
const initialTimer = setTimeout(() => tick(0), 6000)
return () => clearTimeout(initialTimer)
}, [])
function tick(i: number) {
setIndex(i)
setVisible(true)
setTimeout(() => {
setVisible(false)
const next = (i + 1) % NOTIFICATIONS.length
setTimeout(() => tick(next), 4000) // gap between toasts
}, 5000) // show duration
}
const notif = NOTIFICATIONS[index]
return (
<div className="fixed bottom-24 left-6 z-[80] md:bottom-8">
<AnimatePresence>
{visible && (
<motion.div
key={index}
initial={{ opacity: 0, x: -40, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: -20, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="flex items-center gap-3 rounded-2xl border border-white/10 bg-[#16161f]/95 backdrop-blur-xl px-4 py-3 shadow-2xl shadow-black/40 max-w-[260px]"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-indigo-600/20 border border-indigo-500/30">
<Home className="h-4 w-4 text-indigo-400" />
</div>
<div className="min-w-0">
<p className="text-xs font-semibold text-white truncate">
{notif.name} <span className="text-white/40 font-normal">from {notif.location}</span>
</p>
<p className="text-[11px] text-indigo-300">{notif.action}</p>
<p className="text-[10px] text-white/30 mt-0.5">{notif.time}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
+119
View File
@@ -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>
)
}