From 42a406e3abbed9b1f4deb50feabbb8e0105015c8 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 23 Sep 2025 16:06:49 -0700 Subject: [PATCH] Update setup banner: navigate to settings#ai & Dyad Pro option (#1361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary by cubic Updated the setup banner to navigate to Settings and auto-scroll to the AI Providers section, and added a Dyad Pro setup option to streamline onboarding. - **New Features** - Added “Setup Dyad Pro” card with logo; opens Dyad Pro signup. - “Other providers” now jumps to Settings → Provider section with smooth scroll. - OpenRouter setup card updated with new teal styling. - **Refactors** - Introduced useScrollAndNavigateTo hook to navigate, scroll, and set active section. - Centralized active section state via activeSettingsSectionAtom (used by SettingsList). - SetupProviderCard supports a new “dyad” variant styling. --- src/atoms/viewAtoms.ts | 3 ++ src/components/SettingsList.tsx | 26 ++++++--------- src/components/SetupBanner.tsx | 41 ++++++++++++++++++++--- src/components/SetupProviderCard.tsx | 21 ++++++++---- src/hooks/useScrollAndNavigateTo.ts | 49 ++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 src/hooks/useScrollAndNavigateTo.ts diff --git a/src/atoms/viewAtoms.ts b/src/atoms/viewAtoms.ts index 9382dea..be09fdf 100644 --- a/src/atoms/viewAtoms.ts +++ b/src/atoms/viewAtoms.ts @@ -4,3 +4,6 @@ export const isPreviewOpenAtom = atom(true); export const selectedFileAtom = atom<{ path: string; } | null>(null); +export const activeSettingsSectionAtom = atom( + "general-settings", +); diff --git a/src/components/SettingsList.tsx b/src/components/SettingsList.tsx index a15a60c..b197ce7 100644 --- a/src/components/SettingsList.tsx +++ b/src/components/SettingsList.tsx @@ -1,7 +1,9 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; -import { useNavigate } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo"; +import { useAtom } from "jotai"; +import { activeSettingsSectionAtom } from "@/atoms/viewAtoms"; const SETTINGS_SECTIONS = [ { id: "general-settings", label: "General" }, @@ -16,10 +18,11 @@ const SETTINGS_SECTIONS = [ ]; export function SettingsList({ show }: { show: boolean }) { - const navigate = useNavigate(); - const [activeSection, setActiveSection] = useState( - "general-settings", - ); + const [activeSection, setActiveSection] = useAtom(activeSettingsSectionAtom); + const scrollAndNavigateTo = useScrollAndNavigateTo("/settings", { + behavior: "smooth", + block: "start", + }); useEffect(() => { const observer = new IntersectionObserver( @@ -50,16 +53,7 @@ export function SettingsList({ show }: { show: boolean }) { return null; } - const handleScrollAndNavigateTo = async (id: string) => { - await navigate({ - to: "/settings", - }); - const element = document.getElementById(id); - if (element) { - element.scrollIntoView({ behavior: "smooth", block: "start" }); - setActiveSection(id); - } - }; + const handleScrollAndNavigateTo = scrollAndNavigateTo; return (
diff --git a/src/components/SetupBanner.tsx b/src/components/SetupBanner.tsx index 12342fc..f1c3495 100644 --- a/src/components/SetupBanner.tsx +++ b/src/components/SetupBanner.tsx @@ -8,9 +8,10 @@ import { XCircle, Loader2, Settings, + GlobeIcon, } from "lucide-react"; import { providerSettingsRoute } from "@/routes/settings/providers/$provider"; -import { settingsRoute } from "@/routes/settings"; + import SetupProviderCard from "@/components/SetupProviderCard"; import { useState, useEffect, useCallback } from "react"; @@ -26,6 +27,10 @@ import { cn } from "@/lib/utils"; import { NodeSystemInfo } from "@/ipc/ipc_types"; import { usePostHog } from "posthog-js/react"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; +import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo"; +// @ts-ignore +import logo from "../../assets/logo.svg"; + type NodeInstallStep = | "install" | "waiting-for-continue" @@ -59,6 +64,11 @@ export function SetupBanner() { checkNode(); }, [checkNode]); + const settingsScrollAndNavigateTo = useScrollAndNavigateTo("/settings", { + behavior: "smooth", + block: "start", + }); + const handleGoogleSetupClick = () => { posthog.capture("setup-flow:ai-provider-setup:google:click"); navigate({ @@ -74,12 +84,16 @@ export function SetupBanner() { params: { provider: "openrouter" }, }); }; + const handleDyadProSetupClick = () => { + posthog.capture("setup-flow:ai-provider-setup:dyad:click"); + IpcClient.getInstance().openExternalUrl( + "https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=setup-banner", + ); + }; const handleOtherProvidersClick = () => { posthog.capture("setup-flow:ai-provider-setup:other:click"); - navigate({ - to: settingsRoute.id, - }); + settingsScrollAndNavigateTo("provider-settings"); }; const handleNodeInstallClick = useCallback(async () => { @@ -255,7 +269,7 @@ export function SetupBanner() { onClick={handleOpenRouterSetupClick} tabIndex={isNodeSetupComplete ? 0 : -1} leadingIcon={ - + } title="Setup OpenRouter API Key" subtitle={ @@ -266,6 +280,23 @@ export function SetupBanner() { } /> + + } + title="Setup Dyad Pro" + subtitle={ + <> + + Access all AI models with one plan + + } + /> +
void; +}; + +/** + * Returns an async function that navigates to the given route, then scrolls the element with the provided id into view. + */ +export function useScrollAndNavigateTo( + to: string = "/settings", + options?: ScrollOptions, +) { + const navigate = useNavigate(); + const setActiveSection = useSetAtom(activeSettingsSectionAtom); + + return useCallback( + async (id: string) => { + await navigate({ to }); + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ + behavior: options?.behavior ?? "smooth", + block: options?.block ?? "start", + inline: options?.inline, + }); + setActiveSection(id); + options?.onScrolled?.(id, element); + return true; + } + return false; + }, + [ + navigate, + to, + options?.behavior, + options?.block, + options?.inline, + options?.onScrolled, + setActiveSection, + ], + ); +}