Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/web/app/(app)/onboarding/welcome/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { useAuth } from "@lib/auth-context"
import { analytics } from "@/lib/analytics"

export const WELCOME_STEPS = [
Expand Down Expand Up @@ -51,13 +52,17 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
const searchParams = useSearchParams()
const { name, setName, memoryFormData, setMemoryFormData } =
useOnboardingContext()
const { organizations } = useAuth()
const hasOrgs = Array.isArray(organizations) && organizations.length > 0

const stepParam = searchParams.get("step")
const currentStep: WelcomeStep = WELCOME_STEPS.includes(
const resolvedStep: WelcomeStep = WELCOME_STEPS.includes(
stepParam as WelcomeStep,
)
? (stepParam as WelcomeStep)
Comment thread
MaheshtheDev marked this conversation as resolved.
: "input"
const currentStep: WelcomeStep =
resolvedStep === "input" && hasOrgs ? "greeting" : resolvedStep

const [isSubmitting, setIsSubmitting] = useState(false)
const [showWelcomeContent, setShowWelcomeContent] = useState(false)
Expand Down
104 changes: 55 additions & 49 deletions apps/web/app/(app)/onboarding/welcome/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client"

import { useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
import { cn } from "@lib/utils"

Expand Down Expand Up @@ -102,66 +103,70 @@ export default function WelcomePage() {
} = useWelcomeContext()

const { refetchOrganizations, setActiveOrg } = useAuth()
const submitLockRef = useRef(false)

const handleSubmit = async () => {
localStorage.setItem("username", name)
if (name.trim()) {
setIsSubmitting(true)
const trimmed = name.trim()
if (!trimmed) return
if (submitLockRef.current) return
submitLockRef.current = true
localStorage.setItem("username", trimmed)
setIsSubmitting(true)

try {
await authClient.updateUser({
displayUsername: name.trim(),
username: generateUsername(name.trim()),
})
try {
await authClient.updateUser({
displayUsername: trimmed,
username: generateUsername(trimmed),
})

const refetchResult = await refetchOrganizations()
const refetchData = (
refetchResult as { data?: unknown[] | null | undefined }
)?.data
const existingOrgs = Array.isArray(refetchData) ? refetchData : []
const refetchResult = await refetchOrganizations()
const refetchData = (
refetchResult as { data?: unknown[] | null | undefined }
)?.data
const existingOrgs = Array.isArray(refetchData) ? refetchData : []

if (existingOrgs.length > 0) {
analytics.onboardingNameSubmitted({
name_length: name.trim().length,
})
goToStep("greeting")
return
}
if (existingOrgs.length > 0) {
analytics.onboardingNameSubmitted({
name_length: trimmed.length,
})
goToStep("greeting")
return
}

const uniqueSlug = generateOrgSlug(name.trim())
const completedAt = new Date().toISOString()
const newOrg = await authClient.organization.create({
name: name.trim(),
slug: uniqueSlug,
metadata: {
signupSource: "consumer",
webOnboarding: {
completedAt: null,
steps: {
welcomeInput: {
startedAt: completedAt,
completedAt,
data: {},
},
const uniqueSlug = generateOrgSlug(trimmed)
const completedAt = new Date().toISOString()
const newOrg = await authClient.organization.create({
name: trimmed,
slug: uniqueSlug,
metadata: {
signupSource: "consumer",
webOnboarding: {
completedAt: null,
steps: {
welcomeInput: {
startedAt: completedAt,
completedAt,
data: {},
},
},
},
})
},
})

await setActiveOrg(newOrg.slug)
await setActiveOrg(newOrg.slug)

analytics.onboardingNameSubmitted({ name_length: name.trim().length })
goToStep("greeting")
} catch (error) {
console.error("Onboarding submit failed:", error)
toast.error(
error instanceof Error
? error.message
: "Could not set up your workspace. Please try again.",
)
} finally {
setIsSubmitting(false)
}
analytics.onboardingNameSubmitted({ name_length: trimmed.length })
goToStep("greeting")
} catch (error) {
console.error("Onboarding submit failed:", error)
toast.error(
error instanceof Error
? error.message
: "Could not set up your workspace. Please try again.",
)
} finally {
submitLockRef.current = false
setIsSubmitting(false)
}
}

Expand Down Expand Up @@ -212,6 +217,7 @@ export default function WelcomePage() {
showUserSupermemory={
currentStep === "features" || currentStep === "memories"
}
showSkipOnboarding={currentStep !== "input"}
name={name}
/>

Expand Down
41 changes: 34 additions & 7 deletions apps/web/components/initial-header.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
"use client"

import { Logo } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
import { useRouter } from "next/navigation"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"

export function InitialHeader({
showUserSupermemory,
showSkipOnboarding,
name,
}: {
showUserSupermemory?: boolean
showSkipOnboarding?: boolean
name?: string
}) {
const router = useRouter()
const { markOrgOnboarded, isLoading } = useOrgOnboarding()
const userName = name ? `${name.split(" ")[0]}'s` : "My"

const handleSkip = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
router.push("/")
}
Comment thread
MaheshtheDev marked this conversation as resolved.

return (
<div className="flex p-6 justify-between items-center">
<div className="flex items-center z-10!">
Expand All @@ -24,13 +40,24 @@ export function InitialHeader({
</div>
)}
</div>
<Button
variant="newDefault"
className="rounded-2xl text-base gap-1 h-11! z-10!"
size={"lg"}
>
Memory API <span className="text-xs mt-[4px]">↗</span>
</Button>
<div className="flex items-center gap-3 z-10!">
{showSkipOnboarding && !isLoading && (
<button
type="button"
onClick={handleSkip}
className="text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer"
>
Skip Onboarding
</button>
)}
<Button
variant="newDefault"
className="rounded-2xl text-base gap-1 h-11!"
size="lg"
>
Memory API <span className="text-xs mt-[4px]">↗</span>
</Button>
</div>
</div>
)
}
27 changes: 24 additions & 3 deletions apps/web/components/onboarding/setup/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@ import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"

export function SetupHeader() {
const { user } = useAuth()
const router = useRouter()
const localStorageUsername = useLocalStorageUsername()
const { markOrgOnboarded, isLoading: isOrgLoading } = useOrgOnboarding()

const handleSkip = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
router.push("/")
}

const displayName =
user?.displayUsername || localStorageUsername || user?.name || ""
Expand Down Expand Up @@ -59,9 +68,21 @@ export function SetupHeader() {
</span>
<span className="text-white/50 font-medium shrink-0">Setup</span>
</nav>
{user && (
<UserProfileMenu className="z-10" avatarClassName="border-border" />
)}
<div className="flex items-center gap-3 z-10">
{!isOrgLoading && (
<button
type="button"
onClick={handleSkip}
className={cn(
"text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer",
dmSansClassName(),
)}
>
Skip Onboarding
</button>
)}
{user && <UserProfileMenu avatarClassName="border-border" />}
</div>
</motion.div>
)
}
21 changes: 16 additions & 5 deletions apps/web/components/onboarding/welcome/input-step.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { motion } from "motion/react"
import { cn } from "@lib/utils"
import { LabeledInput } from "@ui/input/labeled-input"
import { Button } from "@ui/components/button"

Expand All @@ -17,7 +18,10 @@ export function InputStep({
}: InputStepProps) {
return (
<motion.div
className="text-center min-w-[250px] flex flex-col"
className={cn(
"text-center min-w-[250px] flex flex-col",
isSubmitting && "pointer-events-none",
)}
style={{ gap: "24px" }}
initial={{
opacity: 0,
Expand Down Expand Up @@ -53,20 +57,27 @@ export function InputStep({
className="w-full flex-1"
inputProps={{
defaultValue: name,
disabled: isSubmitting,
onKeyDown: (e) => {
if (e.key === "Enter") {
handleSubmit()
}
if (e.key !== "Enter") return
e.preventDefault()
if (isSubmitting) return
handleSubmit()
},
className: "!text-white placeholder:!text-[#525966] !h-[40px] pl-4",
}}
onChange={(e) => setName((e.target as HTMLInputElement).value)}
onChange={(e) => {
if (isSubmitting) return
setName((e.target as HTMLInputElement).value)
}}
style={{
background:
"linear-gradient(0deg, rgba(91, 126, 245, 0.04) 0%, rgba(91, 126, 245, 0.04) 100%)",
}}
/>
<Button
type="button"
disabled={isSubmitting}
className={`rounded-[8px] w-8 h-8 p-2 absolute right-1 border-[0.5px] border-[#161F2C] hover:cursor-pointer hover:scale-[0.95] active:scale-[0.95] transition-transform ${
isSubmitting ? "scale-[0.90]" : ""
}`}
Expand Down
Loading