From 3abbb60ec694f7a995d904ad377f8003ed860495 Mon Sep 17 00:00:00 2001 From: ignamiranda <34501347+ignamiranda@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:11:46 -0400 Subject: [PATCH] Beginner UX: soft first-run readiness card on Home (#11282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on the combined 8-PR board: home-page-static 4/4, check:dashboard-typecheck clean (220 pre-existing, all within frozen baseline — zero syntax errors), 88/88 across the board's focused suites, static gates within baseline. The soft first-run readiness card replaces the hard Home→onboarding redirect with a dismissable 4-step path. Thank you @ignamiranda! --- .../11282-first-run-readiness-card.md | 1 + .../dashboard/FirstRunReadinessCard.tsx | 97 +++++++++++++++++++ src/app/(dashboard)/home/page.tsx | 6 +- src/i18n/messages/en.json | 11 ++- tests/unit/home-page-static.test.ts | 77 +++++++++++++++ 5 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 changelog.d/features/11282-first-run-readiness-card.md create mode 100644 src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx create mode 100644 tests/unit/home-page-static.test.ts diff --git a/changelog.d/features/11282-first-run-readiness-card.md b/changelog.d/features/11282-first-run-readiness-card.md new file mode 100644 index 0000000000..e2899a54e5 --- /dev/null +++ b/changelog.d/features/11282-first-run-readiness-card.md @@ -0,0 +1 @@ +- **feat(dashboard):** replace the hard Home → onboarding redirect with a dismissable first-run readiness card so returning users can stay on Home while new users still get a clear 4-step path ([#11282](https://github.com/diegosouzapw/OmniRoute/pull/11282)) diff --git a/src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx b/src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx new file mode 100644 index 0000000000..43eb5b6bc9 --- /dev/null +++ b/src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; + +const DISMISS_STORAGE_KEY = "omniroute-first-run-readiness-dismissed"; + +type FirstRunReadinessCardProps = { + setupComplete: boolean; +}; + +/** + * Soft entry path for first-run users. Replaces the hard redirect to + * /dashboard/onboarding so returning users can dismiss and stay on Home. + */ +export default function FirstRunReadinessCard({ setupComplete }: FirstRunReadinessCardProps) { + const t = useTranslations("home"); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (setupComplete) { + setVisible(false); + return; + } + try { + setVisible(!localStorage.getItem(DISMISS_STORAGE_KEY)); + } catch { + setVisible(true); + } + }, [setupComplete]); + + if (!visible || setupComplete) return null; + + const dismiss = () => { + try { + localStorage.setItem(DISMISS_STORAGE_KEY, "true"); + } catch { + // ignore storage failures; still hide for this session + } + setVisible(false); + }; + + const steps = [ + t("readinessStep1"), + t("readinessStep2"), + t("readinessStep3"), + t("readinessStep4"), + ]; + + return ( +
+
+
+

+ {t("readinessEyebrow")} +

+

+ {t("readinessTitle")} +

+

+ {t("readinessSubtitle")} +

+
    + {steps.map((label, index) => ( +
  1. + + {index + 1} + + {label} +
  2. + ))} +
+
+ + {t("readinessContinue")} + + +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/home/page.tsx b/src/app/(dashboard)/home/page.tsx index bc10df88f4..2c2fa62405 100644 --- a/src/app/(dashboard)/home/page.tsx +++ b/src/app/(dashboard)/home/page.tsx @@ -1,4 +1,3 @@ -import { redirect } from "next/navigation"; import { getMachineId } from "@/shared/utils/machine"; import { getSettings } from "@/lib/localDb"; import HomePageClient from "../dashboard/HomePageClient"; @@ -7,19 +6,18 @@ import KimiSponsorBanner from "../dashboard/KimiSponsorBanner"; import CheaperInferenceSponsorBanner from "../dashboard/CheaperInferenceSponsorBanner"; import VscodeCopilotBanner from "../dashboard/VscodeCopilotBanner"; import NewsBanner from "../dashboard/NewsBanner"; +import FirstRunReadinessCard from "../dashboard/FirstRunReadinessCard"; export const dynamic = "force-dynamic"; export default async function HomePage() { const settings = await getSettings(); - if (!settings.setupComplete) { - redirect("/dashboard/onboarding"); - } const machineId = await getMachineId(); const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true"; return ( <> {isBootstrapped && } + diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 12e1f03e47..4aaaf250cc 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1874,7 +1874,16 @@ "directDownloadHint": "Or download the respective installer format directly:", "releaseNotes": "Release Notes", "readMore": "Read More", - "noAuthLabel": "No Auth" + "noAuthLabel": "No Auth", + "readinessEyebrow": "Get ready to route", + "readinessTitle": "Send your first request", + "readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.", + "readinessStep1": "Connect a provider", + "readinessStep2": "Configure endpoint authentication", + "readinessStep3": "Copy your endpoint", + "readinessStep4": "Send a test request", + "readinessContinue": "Continue setup", + "readinessDismiss": "Dismiss for now" }, "analytics": { "title": "Analytics", diff --git a/tests/unit/home-page-static.test.ts b/tests/unit/home-page-static.test.ts new file mode 100644 index 0000000000..3e4ffe62e4 --- /dev/null +++ b/tests/unit/home-page-static.test.ts @@ -0,0 +1,77 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../.."); + +function readHomePage(): string { + return readFileSync( + join(repoRoot, "src/app/(dashboard)/home/page.tsx"), + "utf8", + ); +} + +function readReadinessCard(): string { + return readFileSync( + join(repoRoot, "src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx"), + "utf8", + ); +} + +function readEnKeys(): string[] { + const en = JSON.parse( + readFileSync(join(repoRoot, "src/i18n/messages/en.json"), "utf8"), + ) as { home: Record }; + return Object.keys(en.home); +} + +describe("home page first-run readiness card", () => { + it("does not hard-redirect incomplete setup to onboarding", () => { + const source = readHomePage(); + assert.doesNotMatch(source, /redirect\(["']\/dashboard\/onboarding["']\)/); + assert.match(source, /FirstRunReadinessCard/); + assert.match(source, /setupComplete=\{Boolean\(settings\.setupComplete\)\}/); + }); + + it("keeps the readiness card dismissable via localStorage", () => { + const source = readReadinessCard(); + assert.match(source, /omniroute-first-run-readiness-dismissed/); + assert.match(source, /localStorage/); + assert.match(source, /readinessContinue/); + assert.match(source, /readinessDismiss/); + }); + + it("uses t() keys for readiness copy", () => { + const source = readReadinessCard(); + for (const key of [ + "readinessEyebrow", + "readinessTitle", + "readinessSubtitle", + "readinessStep1", + "readinessStep2", + "readinessStep3", + "readinessStep4", + ]) { + assert.match(source, new RegExp(key)); + } + }); + + it("new i18n keys exist in en.json home namespace", () => { + const keys = readEnKeys(); + for (const key of [ + "readinessEyebrow", + "readinessTitle", + "readinessSubtitle", + "readinessStep1", + "readinessStep2", + "readinessStep3", + "readinessStep4", + "readinessContinue", + "readinessDismiss", + ]) { + assert.ok(keys.includes(key), `Missing home.${key}`); + } + }); +});