import { useRouter, useSearchParams } from "next/navigation"; import { Radio, RadioGroup } from "@headlessui/react"; import { t } from "@lingui/core/macro"; import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react"; import { HiUser } from "react-icons/hi2"; import { twMerge } from "tailwind-merge"; import { authClient } from "@kan/auth/client"; import Button from "~/components/Button"; import { api } from "~/utils/api"; type PlanId = "solo" | "team" | "pro"; type Billing = "monthly" | "annual"; // Each user orbits via a zero-size pivot div at center (144,144) that rotates. // The icon is offset from the pivot by `radius` pixels along the x-axis. // A counter-rotation on the icon keeps it upright. const ORBIT_USERS: Record< PlanId, { id: string; angle: number; radius: number; duration: number }[] > = { solo: [], team: [ { id: "t1", angle: 45, radius: 104, duration: 50 }, { id: "t2", angle: 170, radius: 104, duration: 50 }, { id: "t3", angle: 290, radius: 104, duration: 50 }, ], pro: [ { id: "t1", angle: 45, radius: 104, duration: 50 }, { id: "t2", angle: 170, radius: 104, duration: 50 }, { id: "t3", angle: 290, radius: 104, duration: 50 }, { id: "p1", angle: 10, radius: 144, duration: 70 }, { id: "p2", angle: 120, radius: 144, duration: 70 }, { id: "p4", angle: 240, radius: 144, duration: 70 }, { id: "p3", angle: 230, radius: 64, duration: 30 }, ], }; export default function SelectPlanView() { const router = useRouter(); const searchParams = useSearchParams(); const [selected, setSelected] = useState( (searchParams.get("plan") as PlanId | null) ?? "solo", ); const [billing, setBilling] = useState( (searchParams.get("billing") as Billing | null) ?? "annual", ); const returnUrl = searchParams.get("returnUrl") ?? "/boards"; const workspacePublicId = searchParams.get("workspacePublicId"); const { data: workspaces } = api.workspace.all.useQuery(); const { data: session } = authClient.useSession(); const { data: user } = api.user.getUser.useQuery(undefined, { enabled: !!session?.user, }); const userImage = user?.image ?? session?.user.image ?? null; const hasExistingWorkspace = !!workspaces?.length; const FREQUENCIES: { value: Billing; label: string }[] = [ { value: "monthly", label: t`Monthly` }, { value: "annual", label: t`Annual` }, ]; const PLANS: { id: PlanId; name: string; monthly: string; annual: string; description: string; trial?: boolean; }[] = [ { id: "solo", name: t`Solo`, monthly: t`Free`, annual: t`Free`, description: t`Good for individuals starting out who just need the essentials.`, }, { id: "team", name: t`Team`, monthly: "$10/user/mo", annual: "$8/user/mo", description: t`Best for small teams who want to collaborate and move faster together.`, trial: true, }, { id: "pro", name: t`Pro`, monthly: "$29/mo", annual: "$23/mo", description: t`Unlimited members and a custom workspace username for teams ready to scale.`, trial: true, }, ]; const buildSelectPlanUrl = (plan: PlanId, b: Billing) => { const base = `/onboarding/select-plan?plan=${plan}&billing=${b}&returnUrl=${encodeURIComponent(returnUrl)}`; return workspacePublicId ? `${base}&workspacePublicId=${workspacePublicId}` : base; }; const handleSelectPlan = (plan: PlanId) => { setSelected(plan); router.replace(buildSelectPlanUrl(plan, billing)); }; const handleSetBilling = (b: Billing) => { setBilling(b); router.replace(buildSelectPlanUrl(selected, b)); }; const handleContinue = async () => { if (workspacePublicId && selected !== "solo") { const response = await fetch("/api/stripe/create_checkout_session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ plan: selected, billing, workspacePublicId, successUrl: returnUrl, cancelUrl: returnUrl, }), }); const { url } = (await response.json()) as { url: string }; if (url) window.location.href = url; return; } router.push( `/onboarding/workspace?plan=${selected}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`, ); }; const handleCancel = () => router.push(returnUrl); return (
{/* Left panel */}

{t`Choose a plan`}

{t`Pick a plan to get started. All paid plans include a 14-day free trial.`}

{/* Billing toggle */}
{FREQUENCIES.map((f) => ( {f.label} ))}
{PLANS.map((plan) => { const badge = billing === "annual" ? plan.annual : plan.monthly; return ( ); })}
{hasExistingWorkspace && ( )}
{/* Right panel */}
{/* Orbit rings */}
{/* Centre person */}
{userImage ? ( {t`Your ) : ( )}
{/* Orbiting users — zero-size pivot at center rotates; icon offset by radius stays on the ring */} {ORBIT_USERS[selected].map((u) => ( {/* Counter-rotate icon so it stays upright */} ))}
{!hasExistingWorkspace && ( )}
); }