Initial import: property management SaaS + security hardening + admin dashboard
Property Management Network — Next.js 16 (App Router), Better Auth, Drizzle ORM over PostgreSQL, Stripe, OpenAI, Resend. Includes: - Security hardening: access-control/IDOR fixes, TLS-by-default DB layer, constant-time cron auth, strict security headers, atomic AI quota gating, HTML/email output encoding, demo-backdoor disabled in production. - Superadmin dashboard at /admin (overview/MRR, server-paginated users with ban/impersonate/plan/delete, billing, platform activity + admin audit log, AI usage, system health) via the Better Auth admin plugin. - Seed/migration utility scripts under scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import Link from "next/link"
|
||||
import { ArrowRight, Code2, Lock, Zap, BookOpen } from "lucide-react"
|
||||
|
||||
export const metadata = {
|
||||
title: "API Docs — Property Management Network",
|
||||
description: "Property Management Network REST API documentation for developers.",
|
||||
}
|
||||
|
||||
const ENDPOINTS = [
|
||||
{ method: "GET", path: "/api/properties", desc: "List all properties for the authenticated landlord" },
|
||||
{ method: "POST", path: "/api/properties", desc: "Create a new property" },
|
||||
{ method: "GET", path: "/api/tenants", desc: "List all tenants with lease status" },
|
||||
{ method: "GET", path: "/api/payments", desc: "List rent payments with filters (status, date range)" },
|
||||
{ method: "POST", path: "/api/payments", desc: "Record a new payment manually" },
|
||||
{ method: "GET", path: "/api/maintenance", desc: "List all maintenance requests" },
|
||||
{ method: "POST", path: "/api/maintenance", desc: "Create a maintenance request" },
|
||||
{ method: "PATCH", path: "/api/maintenance/:id", desc: "Update request status or assign to contractor" },
|
||||
]
|
||||
|
||||
const METHOD_COLORS: Record<string, string> = {
|
||||
GET: "text-emerald-400 bg-emerald-500/10",
|
||||
POST: "text-blue-400 bg-blue-500/10",
|
||||
PATCH: "text-amber-400 bg-amber-500/10",
|
||||
DELETE: "text-red-400 bg-red-500/10",
|
||||
}
|
||||
|
||||
export default function ApiDocsPage() {
|
||||
return (
|
||||
<div className="bg-[#09090b] text-white min-h-screen">
|
||||
{/* Hero */}
|
||||
<div className="relative overflow-hidden pt-32 pb-16">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 h-[400px] w-[600px] rounded-full bg-violet-600/12 blur-[100px] -z-10" />
|
||||
<div className="mx-auto max-w-4xl px-6">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-violet-500/30 bg-violet-500/10 px-4 py-1.5 text-xs font-medium text-violet-300 mb-6">
|
||||
<Code2 className="h-3.5 w-3.5" />
|
||||
REST API · v1
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold sm:text-5xl mb-4">API Documentation</h1>
|
||||
<p className="text-lg text-white/50 max-w-2xl leading-relaxed">
|
||||
Build on top of Property Management Network. Automate your workflows, sync with external tools, or build custom dashboards using our REST API.
|
||||
</p>
|
||||
<div className="mt-6 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-4 py-2.5">
|
||||
<span className="text-xs font-mono text-white/40">Base URL:</span>
|
||||
<code className="text-xs font-mono text-indigo-300">https://api.propertymanagement.network/v1</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-4xl px-6 pb-24 space-y-12">
|
||||
{/* Auth */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Lock className="h-5 w-5 text-indigo-400" /> Authentication
|
||||
</h2>
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-6">
|
||||
<p className="text-sm text-white/60 mb-4 leading-relaxed">
|
||||
All API requests require a Bearer token in the Authorization header. Generate your API key from
|
||||
the <Link href="/login" className="text-indigo-400 hover:text-indigo-300 underline underline-offset-2">dashboard settings</Link>.
|
||||
</p>
|
||||
<div className="rounded-xl bg-[#0a0a12] border border-white/[0.06] p-4 font-mono text-xs text-emerald-300">
|
||||
<p className="text-white/30 mb-1"># Example request</p>
|
||||
<p>curl https://api.propertymanagement.network/v1/properties \</p>
|
||||
<p className="pl-4">-H "Authorization: Bearer YOUR_API_KEY"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Endpoints */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-amber-400" /> Endpoints
|
||||
</h2>
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] overflow-hidden">
|
||||
{ENDPOINTS.map((ep, i) => (
|
||||
<div key={i} className={`flex items-start gap-4 px-6 py-4 ${i !== ENDPOINTS.length - 1 ? "border-b border-white/[0.04]" : ""}`}>
|
||||
<span className={`shrink-0 rounded-md px-2.5 py-1 text-[11px] font-bold font-mono ${METHOD_COLORS[ep.method]}`}>
|
||||
{ep.method}
|
||||
</span>
|
||||
<div>
|
||||
<code className="text-xs font-mono text-white/80">{ep.path}</code>
|
||||
<p className="text-xs text-white/40 mt-0.5">{ep.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response format */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<BookOpen className="h-5 w-5 text-blue-400" /> Response Format
|
||||
</h2>
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-6">
|
||||
<p className="text-sm text-white/60 mb-4">All responses are JSON. Successful responses return a <code className="text-indigo-300 font-mono text-xs">data</code> field. Errors return an <code className="text-red-300 font-mono text-xs">error</code> field with a message and code.</p>
|
||||
<div className="rounded-xl bg-[#0a0a12] border border-white/[0.06] p-4 font-mono text-xs leading-relaxed">
|
||||
<p className="text-white/30">// Success</p>
|
||||
<p className="text-emerald-300">{"{"} "data": [...], "count": 12 {"}"}</p>
|
||||
<br />
|
||||
<p className="text-white/30">// Error</p>
|
||||
<p className="text-red-300">{"{"} "error": {"{"} "code": 401, "message": "Unauthorized" {"}"} {"}"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coming soon banner */}
|
||||
<div className="rounded-2xl border border-indigo-500/20 bg-indigo-500/5 p-6 text-center">
|
||||
<p className="text-sm font-semibold text-indigo-300 mb-1">Full SDK coming soon</p>
|
||||
<p className="text-xs text-white/40 mb-4">We're building official JavaScript and Python SDKs. Join the waitlist to be notified.</p>
|
||||
<Link
|
||||
href="/signup"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-2 text-xs font-semibold text-white hover:bg-indigo-500 transition"
|
||||
>
|
||||
Join waitlist <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
export const metadata = {
|
||||
title: "Cookie Policy — Property Management Network",
|
||||
description: "How Property Management Network uses cookies and similar tracking technologies.",
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{
|
||||
title: "What are cookies?",
|
||||
body: "Cookies are small text files stored on your device by your browser when you visit a website. They help websites remember your preferences, keep you logged in, and understand how the site is being used.",
|
||||
},
|
||||
{
|
||||
title: "Cookies we use",
|
||||
body: "We use the following types of cookies: (1) Essential cookies — required for the application to function, such as session authentication tokens. Without these, you cannot log in. (2) Analytics cookies — we use Vercel Analytics (privacy-preserving, no personal data stored) to understand page performance. (3) Preference cookies — we store your dashboard preferences (dark/light mode, column visibility) in browser localStorage.",
|
||||
},
|
||||
{
|
||||
title: "Third-party cookies",
|
||||
body: "Stripe may set cookies when you make a payment for fraud prevention and PCI compliance purposes. We do not use advertising, retargeting, or social media tracking cookies.",
|
||||
},
|
||||
{
|
||||
title: "How to control cookies",
|
||||
body: "You can manage cookies through your browser settings. Most browsers allow you to block or delete cookies. Note that blocking essential cookies will prevent you from logging in to Property Management Network. For analytics cookies, you can opt out by enabling the Do Not Track header in your browser.",
|
||||
},
|
||||
{
|
||||
title: "Cookie retention",
|
||||
body: "Session cookies expire when you close your browser. Authentication tokens are refreshed automatically and expire after 7 days of inactivity. Analytics data is retained for 90 days in aggregate, with no individual identifiers stored.",
|
||||
},
|
||||
{
|
||||
title: "Changes to this policy",
|
||||
body: "We may update this Cookie Policy as we add new features. Significant changes will be announced via the in-app notification banner. The date at the bottom of this page reflects the most recent update.",
|
||||
},
|
||||
{
|
||||
title: "Contact",
|
||||
body: "For questions about cookies or this policy, contact us at privacy@propertymanagement.network.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function CookiePolicyPage() {
|
||||
return (
|
||||
<div className="bg-[#09090b] text-white min-h-screen">
|
||||
<div className="mx-auto max-w-3xl px-6 pt-32 pb-24">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Cookie Policy</h1>
|
||||
<p className="text-xs text-white/30">Last updated: April 2026</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{SECTIONS.map((s) => (
|
||||
<div key={s.title}>
|
||||
<h2 className="text-base font-semibold text-white mb-2">{s.title}</h2>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{s.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Cookie types summary table */}
|
||||
<div className="mt-10 rounded-2xl border border-white/[0.06] bg-[#111118] overflow-hidden">
|
||||
<div className="px-5 py-3.5 border-b border-white/[0.04]">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-white/30">Cookie Summary</p>
|
||||
</div>
|
||||
{[
|
||||
{ name: "pf_session", type: "Essential", purpose: "Authentication session token", expires: "7 days" },
|
||||
{ name: "pf_prefs", type: "Preference", purpose: "Dashboard layout preferences", expires: "1 year" },
|
||||
{ name: "_vercel_*", type: "Analytics", purpose: "Anonymous page performance data", expires: "90 days" },
|
||||
{ name: "__stripe_*", type: "Third-party", purpose: "Stripe payment fraud prevention", expires: "Session" },
|
||||
].map((row, i, arr) => (
|
||||
<div key={row.name} className={`grid grid-cols-4 gap-4 px-5 py-3.5 text-xs ${i !== arr.length - 1 ? "border-b border-white/[0.04]" : ""}`}>
|
||||
<code className="font-mono text-indigo-300">{row.name}</code>
|
||||
<span className="text-white/60">{row.type}</span>
|
||||
<span className="text-white/40 col-span-1">{row.purpose}</span>
|
||||
<span className="text-white/40">{row.expires}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import Link from "next/link"
|
||||
|
||||
export const metadata = {
|
||||
title: "GDPR Compliance — Property Management Network",
|
||||
description: "Property Management Network's commitment to GDPR compliance and your data rights as a data subject.",
|
||||
}
|
||||
|
||||
const RIGHTS = [
|
||||
{ right: "Right of access", desc: "You can request a full export of all data we hold about you at any time from your account settings." },
|
||||
{ right: "Right to rectification", desc: "You can update your personal information directly in your account settings, or contact us to correct inaccurate data." },
|
||||
{ right: "Right to erasure", desc: "You can permanently delete your account and all associated data from the settings page. Deletion is irreversible and processed within 30 days." },
|
||||
{ right: "Right to data portability", desc: "You can export your data in machine-readable JSON or CSV format from the dashboard at any time." },
|
||||
{ right: "Right to restriction", desc: "You may request that we restrict processing of your data while a dispute is being resolved." },
|
||||
{ right: "Right to object", desc: "You may object to processing where we rely on legitimate interest. You can opt out of analytics tracking by enabling Do Not Track in your browser." },
|
||||
]
|
||||
|
||||
export default function GdprPage() {
|
||||
return (
|
||||
<div className="bg-[#09090b] text-white min-h-screen">
|
||||
<div className="mx-auto max-w-3xl px-6 pt-32 pb-24">
|
||||
<div className="mb-10">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-300 mb-4">
|
||||
EU GDPR Compliant
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">GDPR Compliance</h1>
|
||||
<p className="text-xs text-white/30">Last updated: April 2026</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 text-sm text-white/50 leading-relaxed">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Who we are</h2>
|
||||
<p>
|
||||
Property Management Network ("we", "us", "our") is the data controller for personal data collected through our platform.
|
||||
We are committed to complying with the General Data Protection Regulation (EU) 2016/679 (GDPR)
|
||||
and the UK GDPR where applicable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">What data we process</h2>
|
||||
<p className="mb-3">We process the following categories of personal data:</p>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
"Account data: name, email address, password hash",
|
||||
"Property data: addresses, rental amounts, lease terms you enter",
|
||||
"Tenant data: names, emails, phone numbers you provide as a landlord",
|
||||
"Payment data: payment amounts, dates, and status (card details handled by Stripe, not us)",
|
||||
"Usage data: pages visited, features used — anonymised via Vercel Analytics",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-start gap-2">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-400" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Legal basis for processing</h2>
|
||||
<p>
|
||||
We process personal data on the following legal bases: (1) Contract — data necessary to provide the service you signed up for.
|
||||
(2) Legitimate interest — anonymous analytics to improve the product. (3) Legal obligation — where required by applicable law.
|
||||
We do not process data on the basis of consent for core functionality.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Data storage and transfers</h2>
|
||||
<p>
|
||||
Your data is stored in Supabase (PostgreSQL), with servers located in the EU (Frankfurt, Germany) by default.
|
||||
Row-level security (RLS) policies ensure only you can access your data. We do not transfer personal data outside
|
||||
the EEA except where strictly necessary for integrated services (e.g. Stripe for payment processing,
|
||||
which is covered by Standard Contractual Clauses).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Your rights under GDPR</h2>
|
||||
<p className="mb-5">As a data subject, you have the following rights:</p>
|
||||
<div className="space-y-4">
|
||||
{RIGHTS.map((r) => (
|
||||
<div key={r.right} className="rounded-xl border border-white/[0.06] bg-[#111118] p-4">
|
||||
<p className="text-sm font-semibold text-white mb-1">{r.right}</p>
|
||||
<p className="text-xs text-white/50 leading-relaxed">{r.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Data retention</h2>
|
||||
<p>
|
||||
We retain account data for as long as your account is active. Upon deletion, all personal data is purged within 30 days,
|
||||
except where retention is required by law (e.g. financial records may be retained for up to 7 years for tax compliance).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Data breach notification</h2>
|
||||
<p>
|
||||
In the event of a data breach affecting your personal data, we will notify affected users within 72 hours of becoming aware,
|
||||
in accordance with GDPR Article 33 obligations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Sub-processors</h2>
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] overflow-hidden">
|
||||
{[
|
||||
{ name: "Supabase", purpose: "Database & file storage", location: "EU (Frankfurt)" },
|
||||
{ name: "Stripe", purpose: "Payment processing", location: "US (SCCs in place)" },
|
||||
{ name: "Resend", purpose: "Transactional email", location: "US (SCCs in place)" },
|
||||
{ name: "Vercel", purpose: "Hosting & edge network", location: "Global (anonymised data only)" },
|
||||
].map((sp, i, arr) => (
|
||||
<div key={sp.name} className={`grid grid-cols-3 gap-4 px-5 py-3.5 text-xs ${i !== arr.length - 1 ? "border-b border-white/[0.04]" : ""}`}>
|
||||
<span className="font-semibold text-white">{sp.name}</span>
|
||||
<span className="text-white/50">{sp.purpose}</span>
|
||||
<span className="text-white/40">{sp.location}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white mb-2">Contact & complaints</h2>
|
||||
<p>
|
||||
To exercise any of your rights or to raise a data protection concern, contact our Data Protection lead at{" "}
|
||||
<a href="mailto:privacy@propertymanagement.network" className="text-indigo-400 hover:text-indigo-300">
|
||||
privacy@propertymanagement.network
|
||||
</a>
|
||||
. You also have the right to lodge a complaint with your local supervisory authority (e.g. the ICO in the UK,
|
||||
or your national DPA in the EU).
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
See also our{" "}
|
||||
<Link href="/privacy" className="text-indigo-400 hover:text-indigo-300">Privacy Policy</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="/cookie-policy" className="text-indigo-400 hover:text-indigo-300">Cookie Policy</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Navbar } from "@/components/marketing/navbar"
|
||||
import { Footer } from "@/components/marketing/footer"
|
||||
|
||||
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#09090b] text-white">
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Hero } from "@/components/marketing/hero"
|
||||
import { Marquee } from "@/components/marketing/marquee"
|
||||
import { Problem } from "@/components/marketing/problem"
|
||||
import { Features } from "@/components/marketing/features"
|
||||
import { HowItWorks } from "@/components/marketing/how-it-works"
|
||||
import { Testimonials } from "@/components/marketing/testimonials"
|
||||
import { PricingSection } from "@/components/marketing/pricing-section"
|
||||
import { FAQ } from "@/components/marketing/faq"
|
||||
import { CtaBanner } from "@/components/marketing/cta-banner"
|
||||
|
||||
export const metadata = {
|
||||
title: "Property Management Network — Property management without the chaos",
|
||||
description: "Track rent, manage maintenance, monitor leases, and keep expenses organised. Built for independent landlords.",
|
||||
}
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Marquee />
|
||||
<Problem />
|
||||
<Features />
|
||||
<HowItWorks />
|
||||
<Testimonials />
|
||||
<PricingSection />
|
||||
<FAQ />
|
||||
<CtaBanner />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const metadata = { title: "Privacy Policy — Property Management Network" }
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-24">
|
||||
<h1 className="text-3xl font-bold text-white mb-8">Privacy Policy</h1>
|
||||
<p className="text-white/50 text-sm leading-relaxed">
|
||||
This Privacy Policy describes how Property Management Network collects, uses, and protects your information.
|
||||
We collect only the data necessary to provide the service (account info, property data you enter,
|
||||
and usage analytics). Your data is stored securely in Supabase with row-level security —
|
||||
no other user can access your records. We do not sell your data to third parties.
|
||||
Files you upload are stored in private buckets and accessible only to you.
|
||||
For questions, contact us at support@propertymanagement.network.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
export const metadata = {
|
||||
title: "System Status — Property Management Network",
|
||||
description: "Real-time status of all Property Management Network services.",
|
||||
}
|
||||
|
||||
const SERVICES = [
|
||||
{ name: "Web Application", status: "operational", uptime: "99.98%" },
|
||||
{ name: "API Gateway", status: "operational", uptime: "99.97%" },
|
||||
{ name: "Database (Supabase)", status: "operational", uptime: "99.99%" },
|
||||
{ name: "Payment Processing (Stripe)", status: "operational", uptime: "99.95%" },
|
||||
{ name: "Email Delivery (Resend)", status: "operational", uptime: "99.96%" },
|
||||
{ name: "File Storage", status: "operational", uptime: "99.99%" },
|
||||
{ name: "Tenant Portal", status: "operational", uptime: "99.97%" },
|
||||
{ name: "Webhook Delivery", status: "operational", uptime: "99.90%" },
|
||||
]
|
||||
|
||||
const INCIDENTS = [
|
||||
{
|
||||
date: "2026-03-28",
|
||||
title: "Resolved: Delayed email notifications",
|
||||
detail: "Email notifications were delayed by up to 12 minutes due to a Resend upstream issue. Fully resolved at 14:32 UTC.",
|
||||
severity: "minor",
|
||||
},
|
||||
{
|
||||
date: "2026-02-14",
|
||||
title: "Resolved: Slow dashboard load times",
|
||||
detail: "Database query optimisations were deployed to fix a slow index scan affecting dashboards with 50+ units. Resolved in 45 minutes.",
|
||||
severity: "minor",
|
||||
},
|
||||
]
|
||||
|
||||
export default function StatusPage() {
|
||||
const allOperational = SERVICES.every((s) => s.status === "operational")
|
||||
|
||||
return (
|
||||
<div className="bg-[#09090b] text-white min-h-screen">
|
||||
<div className="mx-auto max-w-3xl px-6 pt-32 pb-24">
|
||||
{/* Header */}
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">System Status</h1>
|
||||
<p className="text-white/40 text-sm">Real-time health of all Property Management Network services.</p>
|
||||
</div>
|
||||
|
||||
{/* Overall status */}
|
||||
<div className={`flex items-center gap-3 rounded-2xl border p-5 mb-8 ${allOperational ? "border-emerald-500/20 bg-emerald-500/5" : "border-red-500/20 bg-red-500/5"}`}>
|
||||
<div className={`relative flex h-3 w-3`}>
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${allOperational ? "bg-emerald-400" : "bg-red-400"}`} />
|
||||
<span className={`relative inline-flex rounded-full h-3 w-3 ${allOperational ? "bg-emerald-400" : "bg-red-400"}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`font-semibold text-sm ${allOperational ? "text-emerald-300" : "text-red-300"}`}>
|
||||
{allOperational ? "All systems operational" : "Partial outage detected"}
|
||||
</p>
|
||||
<p className="text-xs text-white/30 mt-0.5">Last checked: just now</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] overflow-hidden mb-10">
|
||||
<div className="px-5 py-3.5 border-b border-white/[0.04]">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-white/30">Services</p>
|
||||
</div>
|
||||
{SERVICES.map((svc, i) => (
|
||||
<div key={svc.name} className={`flex items-center justify-between px-4 sm:px-5 py-3.5 sm:py-4 gap-3 ${i !== SERVICES.length - 1 ? "border-b border-white/[0.04]" : ""}`}>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className={`shrink-0 h-2 w-2 rounded-full ${svc.status === "operational" ? "bg-emerald-400" : svc.status === "degraded" ? "bg-amber-400" : "bg-red-400"}`} />
|
||||
<span className="text-sm text-white/80 truncate">{svc.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 sm:gap-6 shrink-0">
|
||||
<span className="hidden sm:block text-xs text-white/30 tabular-nums">{svc.uptime} uptime</span>
|
||||
<span className={`text-xs font-medium capitalize ${svc.status === "operational" ? "text-emerald-400" : svc.status === "degraded" ? "text-amber-400" : "text-red-400"}`}>
|
||||
{svc.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Uptime graph placeholder */}
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-5 mb-10">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-white/30 mb-4">90-day uptime</p>
|
||||
<div className="flex gap-px sm:gap-0.5 h-6 sm:h-8 items-end">
|
||||
{Array.from({ length: 90 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 rounded-sm bg-emerald-500/70"
|
||||
style={{ height: `${Math.random() > 0.03 ? 100 : Math.floor(Math.random() * 60 + 20)}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 text-[10px] text-white/20">
|
||||
<span>90 days ago</span>
|
||||
<span>Today</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Incidents */}
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white mb-4">Past Incidents</h2>
|
||||
<div className="space-y-3">
|
||||
{INCIDENTS.map((inc) => (
|
||||
<div key={inc.date} className="rounded-2xl border border-white/[0.06] bg-[#111118] p-5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-amber-500/10 text-amber-400 uppercase">
|
||||
{inc.severity}
|
||||
</span>
|
||||
<span className="text-xs text-white/30">{inc.date}</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-white mb-1">{inc.title}</p>
|
||||
<p className="text-xs text-white/50 leading-relaxed">{inc.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import Link from "next/link"
|
||||
import { Building2, CheckCircle2, Smartphone, Bell, FileText, ArrowRight } from "lucide-react"
|
||||
|
||||
export const metadata = {
|
||||
title: "Tenant Portal — Property Management Network",
|
||||
description: "Give your tenants a dedicated portal to pay rent, submit maintenance requests, and view lease details.",
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{ icon: CheckCircle2, color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20", title: "Online Rent Payment", desc: "Tenants pay rent securely via Stripe — card or bank transfer. Auto-receipts sent by email." },
|
||||
{ icon: FileText, color: "text-indigo-400", bg: "bg-indigo-500/10 border-indigo-500/20", title: "Lease Documents", desc: "View and download lease agreements, addendums, and move-in checklists anytime." },
|
||||
{ icon: Building2, color: "text-violet-400", bg: "bg-violet-500/10 border-violet-500/20", title: "Maintenance Requests", desc: "Submit maintenance issues with photos. Track status from open → in progress → resolved." },
|
||||
{ icon: Bell, color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20", title: "Smart Notifications", desc: "Rent due reminders, request updates, and landlord messages via email or WhatsApp." },
|
||||
{ icon: Smartphone, color: "text-blue-400", bg: "bg-blue-500/10 border-blue-500/20", title: "Mobile Friendly", desc: "Works perfectly on any device — no app download needed, just a secure link." },
|
||||
{ icon: FileText, color: "text-rose-400", bg: "bg-rose-500/10 border-rose-500/20", title: "Payment History", desc: "Full history of all payments and receipts. Great for tenant records and disputes." },
|
||||
]
|
||||
|
||||
export default function TenantPortalInfoPage() {
|
||||
return (
|
||||
<div className="bg-[#09090b] text-white min-h-screen">
|
||||
{/* Hero */}
|
||||
<div className="relative overflow-hidden pt-32 pb-20">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 h-[400px] w-[700px] rounded-full bg-indigo-600/15 blur-[100px] -z-10" />
|
||||
<div className="mx-auto max-w-4xl px-6 text-center">
|
||||
<div className="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 mb-6">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-indigo-400" />
|
||||
For Tenants & Landlords
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold leading-tight mb-6">
|
||||
A dedicated portal<br />
|
||||
<span className="bg-gradient-to-r from-indigo-400 via-violet-400 to-indigo-400 bg-clip-text text-transparent">
|
||||
your tenants will actually use
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto leading-relaxed mb-10">
|
||||
Send tenants a secure link — no account required. They can pay rent, submit maintenance requests,
|
||||
and access their lease documents in seconds.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3 justify-center">
|
||||
<Link
|
||||
href="/signup"
|
||||
className="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/25"
|
||||
>
|
||||
Get started free <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-6 py-3 text-sm font-medium text-white/70 hover:text-white hover:bg-white/[0.08] transition-all"
|
||||
>
|
||||
Sign in to dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features grid */}
|
||||
<div className="mx-auto max-w-6xl px-6 pb-24">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl font-bold text-white sm:text-3xl">Everything tenants need, nothing they don't</h2>
|
||||
<p className="mt-3 text-white/40">Clean, fast, and works on any device.</p>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{FEATURES.map((f) => (
|
||||
<div key={f.title} className={`rounded-2xl border p-6 ${f.bg} transition-all hover:scale-[1.02]`}>
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-xl bg-black/20 mb-4`}>
|
||||
<f.icon className={`h-5 w-5 ${f.color}`} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-white mb-2">{f.title}</h3>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How it works */}
|
||||
<div className="border-t border-white/[0.06] py-24">
|
||||
<div className="mx-auto max-w-4xl px-6">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl font-bold text-white sm:text-3xl">How it works</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ step: "01", title: "Add your tenant", desc: "Enter your tenant's name and email in your Property Management Network dashboard." },
|
||||
{ step: "02", title: "Send the link", desc: "Property Management Network generates a secure, unique portal link and emails it automatically." },
|
||||
{ step: "03", title: "Tenant accesses portal", desc: "No sign-up needed. Tenant clicks the link and has instant access to their portal." },
|
||||
].map((s) => (
|
||||
<div key={s.step} className="relative">
|
||||
<div className="text-5xl font-black text-white/[0.04] mb-3">{s.step}</div>
|
||||
<h3 className="font-semibold text-white mb-2">{s.title}</h3>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{s.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="border-t border-white/[0.06] py-20">
|
||||
<div className="mx-auto max-w-2xl px-6 text-center">
|
||||
<h2 className="text-2xl font-bold text-white mb-4">Ready to give tenants a better experience?</h2>
|
||||
<p className="text-white/40 mb-8">Free plan includes up to 2 properties and unlimited tenant portal access.</p>
|
||||
<Link
|
||||
href="/signup"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-8 py-3.5 text-sm font-semibold text-white hover:bg-indigo-500 transition-all hover:shadow-lg hover:shadow-indigo-500/25"
|
||||
>
|
||||
Start for free <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const metadata = { title: "Terms of Service — Property Management Network" }
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-24">
|
||||
<h1 className="text-3xl font-bold text-white mb-8">Terms of Service</h1>
|
||||
<p className="text-white/50 text-sm leading-relaxed">
|
||||
By using Property Management Network you agree to these terms. Property Management Network is provided as-is for property
|
||||
management purposes. You are responsible for the accuracy of data you enter. Subscription fees
|
||||
are billed monthly or as a one-time charge through Stripe. You may cancel at any time —
|
||||
cancellation takes effect at the end of your billing period. Lifetime plans are non-refundable
|
||||
after 14 days. We reserve the right to suspend accounts that violate these terms.
|
||||
For questions, contact us at support@propertymanagement.network.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user