124 lines
5.2 KiB
TypeScript
124 lines
5.2 KiB
TypeScript
import { redirect } from "next/navigation"
|
|||
|
|
import Link from "next/link"
|
||
|
|
import { eq, sql } from "drizzle-orm"
|
||
|
|
import { db } from "@/lib/db"
|
||
|
|
import { profiles, properties, tenants, leases, rent_payments } from "@/lib/db/schema"
|
||
|
|
import { getSessionUser } from "@/lib/session"
|
||
|
|
import { completeOnboarding } from "@/app/actions/onboarding"
|
||
|
|
import { Building2, Users, FileText, CreditCard, CheckCircle2, ArrowRight, Sparkles } from "lucide-react"
|
||
|
|
|
||
|
|
export const dynamic = "force-dynamic"
|
||
|
|
|
||
|
|
async function count(table: typeof properties | typeof tenants | typeof leases | typeof rent_payments, userId: string) {
|
||
|
|
const [row] = await db.select({ c: sql<number>`count(*)::int` }).from(table).where(eq(table.user_id, userId))
|
||
|
|
return row?.c ?? 0
|
||
|
|
}
|
||
|
|
|
||
|
|
export default async function OnboardingPage() {
|
||
|
|
const user = await getSessionUser()
|
||
|
|
if (!user) redirect("/login")
|
||
|
|
|
||
|
|
const profile = await db.query.profiles.findFirst({
|
||
|
|
where: eq(profiles.id, user.id),
|
||
|
|
columns: { onboarding_completed: true, full_name: true },
|
||
|
|
})
|
||
|
|
|
||
|
|
// Users who already finished onboarding don't need this screen.
|
||
|
|
if (profile?.onboarding_completed) redirect("/dashboard")
|
||
|
|
|
||
|
|
const [propCount, tenantCount, leaseCount, rentCount] = await Promise.all([
|
||
|
|
count(properties, user.id),
|
||
|
|
count(tenants, user.id),
|
||
|
|
count(leases, user.id),
|
||
|
|
count(rent_payments, user.id),
|
||
|
|
])
|
||
|
|
|
||
|
|
const steps = [
|
||
|
|
{ label: "Add your first property", desc: "Create a building or unit to manage.", href: "/properties/new", icon: Building2, done: propCount > 0 },
|
||
|
|
{ label: "Add a tenant", desc: "Record who's renting from you.", href: "/tenants/new", icon: Users, done: tenantCount > 0 },
|
||
|
|
{ label: "Set up a lease", desc: "Track term, rent, and deposit.", href: "/leases/new", icon: FileText, done: leaseCount > 0 },
|
||
|
|
{ label: "Record a rent payment", desc: "Log or generate the first payment.", href: "/rent/new", icon: CreditCard, done: rentCount > 0 },
|
||
|
|
]
|
||
|
|
const doneCount = steps.filter((s) => s.done).length
|
||
|
|
const firstName = profile?.full_name?.split(" ")[0]
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="mx-auto max-w-2xl py-4">
|
||
|
|
<div className="mb-6 flex items-center gap-3">
|
||
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-600 shadow-lg shadow-indigo-500/25">
|
||
|
|
<Sparkles className="h-5 w-5 text-white" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl font-bold text-white">
|
||
|
|
Welcome{firstName ? `, ${firstName}` : ""} 👋
|
||
|
|
</h1>
|
||
|
|
<p className="text-sm text-white/50">Let's get your portfolio set up in four quick steps.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Progress */}
|
||
|
|
<div className="mb-6">
|
||
|
|
<div className="mb-1.5 flex items-center justify-between text-xs text-white/50">
|
||
|
|
<span>{doneCount} of {steps.length} complete</span>
|
||
|
|
<span>{Math.round((doneCount / steps.length) * 100)}%</span>
|
||
|
|
</div>
|
||
|
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-white/[0.06]">
|
||
|
|
<div
|
||
|
|
className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all"
|
||
|
|
style={{ width: `${(doneCount / steps.length) * 100}%` }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Steps */}
|
||
|
|
<ol className="space-y-3">
|
||
|
|
{steps.map((step, i) => (
|
||
|
|
<li
|
||
|
|
key={step.label}
|
||
|
|
className={`flex items-center gap-4 rounded-2xl border p-4 transition-colors ${
|
||
|
|
step.done
|
||
|
|
? "border-emerald-500/20 bg-emerald-500/[0.04]"
|
||
|
|
: "border-white/[0.06] bg-[#16161f]"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl ${
|
||
|
|
step.done ? "bg-emerald-500/15 text-emerald-400" : "bg-white/[0.04] text-white/50"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{step.done ? <CheckCircle2 className="h-5 w-5" /> : <step.icon className="h-5 w-5" />}
|
||
|
|
</div>
|
||
|
|
<div className="min-w-0 flex-1">
|
||
|
|
<p className={`text-sm font-semibold ${step.done ? "text-white/60 line-through" : "text-white"}`}>
|
||
|
|
{i + 1}. {step.label}
|
||
|
|
</p>
|
||
|
|
<p className="text-xs text-white/40">{step.desc}</p>
|
||
|
|
</div>
|
||
|
|
{step.done ? (
|
||
|
|
<span className="shrink-0 text-xs font-medium text-emerald-400">Done</span>
|
||
|
|
) : (
|
||
|
|
<Link
|
||
|
|
href={step.href}
|
||
|
|
className="flex shrink-0 items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-indigo-500"
|
||
|
|
>
|
||
|
|
Add <ArrowRight className="h-3.5 w-3.5" />
|
||
|
|
</Link>
|
||
|
|
)}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ol>
|
||
|
|
|
||
|
|
{/* Finish */}
|
||
|
|
<form action={completeOnboarding} className="mt-6 flex items-center justify-between gap-3">
|
||
|
|
<p className="text-xs text-white/40">You can always finish these later from the dashboard.</p>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
className="shrink-0 rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/10"
|
||
|
|
>
|
||
|
|
{doneCount === steps.length ? "Finish setup" : "Skip to dashboard"}
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|