diff --git a/src/app/onboarding/_components/finish-step.tsx b/src/app/onboarding/_components/finish-step.tsx new file mode 100644 index 00000000..750a7295 --- /dev/null +++ b/src/app/onboarding/_components/finish-step.tsx @@ -0,0 +1,127 @@ +"use client"; + +import Link from "next/link"; + +import { motion } from "framer-motion"; +import { ArrowRight, Check } from "lucide-react"; + +interface FinishStepProps { + onBack: () => void; +} + +export function FinishStep({ onBack }: FinishStepProps) { + // Mock team ID - will be replaced with actual ID when hooked up + const mockTeamId = "demo-team"; + + return ( + + {/* Success icon */} + + + + +

+ you're all set +

+

+ your organization is ready. let's build your first role canvas. +

+ + {/* Summary of what was created */} + +
+
+ + ORGANIZATION + + + created + +
+
+
+ + TEAM + + + created + +
+
+
+ + FIRST ROLE + + + defined + +
+ + +
+ + + open role canvas + + +
+ +

+ you can always add more roles, teams, and metrics later from your + dashboard. +

+ + ); +} diff --git a/src/app/onboarding/_components/import-members-step.tsx b/src/app/onboarding/_components/import-members-step.tsx new file mode 100644 index 00000000..dd5e073a --- /dev/null +++ b/src/app/onboarding/_components/import-members-step.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState } from "react"; + +import { motion } from "framer-motion"; + +interface ImportMembersStepProps { + onNext: () => void; + onBack: () => void; +} + +export function ImportMembersStep({ onNext, onBack }: ImportMembersStepProps) { + const [selectedIntegration, setSelectedIntegration] = useState< + "slack" | "google" | null + >(null); + + return ( + +

+ bring in your team +

+

+ connect your workspace to automatically import team members and keep + everything in sync. +

+ +
+ {/* Google Workspace option */} + + + {/* Slack option */} + +
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/src/app/onboarding/_components/kpi-step.tsx b/src/app/onboarding/_components/kpi-step.tsx new file mode 100644 index 00000000..9b47993f --- /dev/null +++ b/src/app/onboarding/_components/kpi-step.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useState } from "react"; + +import { motion } from "framer-motion"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; + +interface KpiStepProps { + onNext: () => void; + onBack: () => void; +} + +type GoalType = "absolute" | "relative"; + +export function KpiStep({ onNext, onBack }: KpiStepProps) { + const [kpiName, setKpiName] = useState(""); + const [integrationSource, setIntegrationSource] = useState(""); + const [goalType, setGoalType] = useState("absolute"); + const [absoluteGoal, setAbsoluteGoal] = useState(""); + const [relativeGoal, setRelativeGoal] = useState(""); + const [relativePeriod, setRelativePeriod] = useState("month"); + + const handleAbsoluteGoalChange = (e: React.ChangeEvent) => { + const value = e.target.value; + // Allow empty string, numbers, and decimal point + if (value === "" || /^\d*\.?\d*$/.test(value)) { + setAbsoluteGoal(value); + } + }; + + const handleRelativeGoalChange = (e: React.ChangeEvent) => { + const value = e.target.value; + // Allow empty string, numbers, and decimal point + if (value === "" || /^\d*\.?\d*$/.test(value)) { + setRelativeGoal(value); + } + }; + + const integrationOptions = [ + { value: "manual", label: "manual entry" }, + { value: "google-sheets", label: "google sheets" }, + { value: "github", label: "github" }, + { value: "posthog", label: "posthog" }, + { value: "youtube", label: "youtube" }, + { value: "slack", label: "slack" }, + ]; + + return ( + +

+ track what matters +

+

+ connect a key metric to this role. what number should they move? +

+ + {/* Example metrics */} + + {["MRR", "NPS score", "conversion rate", "churn %", "active users"].map( + (example) => ( + + ), + )} + + +
+ {/* Metric Name */} +
+ + setKpiName(e.target.value)} + placeholder="e.g., monthly recurring revenue" + className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none" + style={{ letterSpacing: "-0.02em" }} + autoFocus + /> +
+ + {/* Integration Source */} +
+ + +
+ + {/* Goal Type Toggle */} +
+ + { + if (value) setGoalType(value as GoalType); + }} + variant="outline" + className="w-full" + > + + + absolute + + + + + relative + + + +
+ + {/* Goal Inputs */} + {goalType === "absolute" ? ( +
+ + +

+ enter a numeric target value. +

+
+ ) : ( +
+
+ +
+ + + % + +
+
+
+ + +
+

+ track growth as a percentage increase over time. +

+
+ )} +
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/src/app/onboarding/_components/org-name-step.tsx b/src/app/onboarding/_components/org-name-step.tsx new file mode 100644 index 00000000..50dd85a2 --- /dev/null +++ b/src/app/onboarding/_components/org-name-step.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useState } from "react"; + +import { motion } from "framer-motion"; + +interface OrgNameStepProps { + onNext: () => void; +} + +export function OrgNameStep({ onNext }: OrgNameStepProps) { + const [orgName, setOrgName] = useState(""); + const [orgUrl, setOrgUrl] = useState(""); + const canContinue = orgName.trim().length > 0; + + return ( + +

+ welcome to ryō +

+

+ let's begin by setting up your organization. this will only take a + few minutes. +

+ +
+
+ + setOrgName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && canContinue) { + onNext(); + } + }} + placeholder="acme inc." + className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none" + style={{ letterSpacing: "-0.02em" }} + autoFocus + /> +
+ +
+ + setOrgUrl(e.target.value)} + placeholder="https://acme.com" + className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none" + style={{ letterSpacing: "-0.02em" }} + /> +

+ we'll use this to learn more about your company and personalize + your experience. +

+
+
+ +
+ +
+
+ ); +} diff --git a/src/app/onboarding/_components/progress-indicator.tsx b/src/app/onboarding/_components/progress-indicator.tsx new file mode 100644 index 00000000..06631906 --- /dev/null +++ b/src/app/onboarding/_components/progress-indicator.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { motion } from "framer-motion"; + +interface ProgressIndicatorProps { + currentStep: number; + totalSteps: number; +} + +export function ProgressIndicator({ + currentStep, + totalSteps, +}: ProgressIndicatorProps) { + return ( +
+ {Array.from({ length: totalSteps }, (_, i) => { + const stepNumber = i + 1; + const isActive = stepNumber === currentStep; + const isCompleted = stepNumber < currentStep; + + return ( + +
+ {isActive && ( + + )} + + ); + })} +
+ ); +} diff --git a/src/app/onboarding/_components/role-creation-step.tsx b/src/app/onboarding/_components/role-creation-step.tsx new file mode 100644 index 00000000..137f2474 --- /dev/null +++ b/src/app/onboarding/_components/role-creation-step.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useState } from "react"; + +import { motion } from "framer-motion"; + +interface RoleCreationStepProps { + onNext: () => void; + onBack: () => void; +} + +export function RoleCreationStep({ onNext, onBack }: RoleCreationStepProps) { + const [title, setTitle] = useState(""); + const [purpose, setPurpose] = useState(""); + const [accountabilities, setAccountabilities] = useState(""); + + const canContinue = title.trim().length > 0 && purpose.trim().length > 0; + + return ( + +

+ define your first role +

+

+ forget job titles. think about what your company needs to succeed, then + work backwards. +

+ +
+
+ + setTitle(e.target.value)} + placeholder="what does your company need?" + className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none" + style={{ letterSpacing: "-0.02em" }} + autoFocus + /> +

+ think outcomes, not job titles. "revenue driver" beats + "sales rep". +

+
+ +
+ +