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,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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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