From af64f563991897723c69749ac69bf724b162f44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Wed, 1 Jul 2026 08:34:51 +0200 Subject: [PATCH] feat(onboarding): clarify the "categorize at least 5" goal in the categorizer (#616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Feedback from onboarding users: in the categorizer step people often **don't realize they need to categorize at least 5 transactions** before *Continue* unlocks, and can't tell why the button is disabled. Worse, the old counter switched to **"N remaining"** after 5 (showing *every* uncategorized transaction, e.g. "195 remaining"), which made several users think they had to categorize **all** of them. ## What changed ### Categorizer clarity (`step-categorize-transactions.tsx`) - **New full-width progress banner** above the transaction card, replacing the cramped corner counter that was getting truncated on mobile: - explicit instruction: *"To continue, you need to categorize at least 5 transactions."* (reuses the existing intro string) - a **5-segment progress bar** that fills as you go - the live **X/5** count - a reassurance line: *"You do not need to categorize all of them."* - When the minimum is reached (or the list is exhausted), the banner turns **green** — *"Done! You can continue now, or keep categorizing if you want."* — and the **Continue** button gets a ring so it's obvious the step is unlocked. - **Removed the misleading "N remaining"** display. ### Onboarding layout & toasts - **Mobile top spacing** (`onboarding-layout.tsx`): reduced the large top padding on the step content on mobile so it sits closer to the progress dots (unchanged on desktop). - **Toasts during onboarding** (`app.tsx`): onboarding has no bottom navigation bar, so toasts now render **bottom-center, flush to the bottom** instead of being lifted 110px to clear the (absent) mobile tab bar. Behavior elsewhere in the app is unchanged. Two new strings added to `lang/es.json`. ## Notes - The `canContinue` / `minimumRequired` gating logic is unchanged. - Verified: `lint` clean, `es.json` valid, `LocalizationTest` passes, full CI green. - Recorded mobile + desktop walkthroughs locally. --- lang/es.json | 2 + resources/js/app.tsx | 49 ++++++++----- .../step-categorize-transactions.tsx | 71 +++++++++++++------ resources/js/layouts/onboarding-layout.tsx | 2 +- 4 files changed, 84 insertions(+), 40 deletions(-) diff --git a/lang/es.json b/lang/es.json index cc2d168d..84cbb937 100644 --- a/lang/es.json +++ b/lang/es.json @@ -52,6 +52,8 @@ "Not doable": "No viable", "In progress": "En proceso", "Done": "Hecho", + "Done! You can continue now, or keep categorizing if you want.": "¡Listo! Ya puedes continuar, o seguir categorizando si quieres.", + "You do not need to categorize all of them.": "No necesitas categorizarlas todas.", "Something went wrong.": "Algo salió mal.", "Integration requests": "Solicitudes de integración", "Request integration": "Solicitar integración", diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 46c2daa1..adb1fe31 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -11,7 +11,7 @@ import { OctagonXIcon, TriangleAlertIcon, } from 'lucide-react'; -import { StrictMode, useEffect } from 'react'; +import { StrictMode, useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { toast, Toaster } from 'sonner'; import { EncryptionKeyProvider } from './contexts/encryption-key-context'; @@ -134,6 +134,35 @@ const getProgressBarColor = () => { return isDark ? '#EEE' : '#4B5563'; // gray-400 for dark mode, gray-600 for light mode }; +const isOnboardingPath = () => + typeof window !== 'undefined' && + window.location.pathname.startsWith('/onboarding'); + +// Onboarding has no bottom navigation bar, so toasts sit flush at the bottom +// center instead of being lifted to clear the (absent) mobile tab bar. +function AppToaster() { + const [isOnboarding, setIsOnboarding] = useState(isOnboardingPath); + + useEffect(() => { + return router.on('navigate', () => setIsOnboarding(isOnboardingPath())); + }, []); + + return ( + , + info: , + warning: , + error: , + loading: , + }} + /> + ); +} + createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), resolve: (name) => @@ -228,23 +257,7 @@ createInertiaApp({ initialExpiredConnections } /> - - ), - info: , - warning: ( - - ), - error: , - loading: ( - - ), - }} - /> + diff --git a/resources/js/components/onboarding/step-categorize-transactions.tsx b/resources/js/components/onboarding/step-categorize-transactions.tsx index 631616d1..8d492712 100644 --- a/resources/js/components/onboarding/step-categorize-transactions.tsx +++ b/resources/js/components/onboarding/step-categorize-transactions.tsx @@ -52,7 +52,6 @@ export function StepCategorizeTransactions({ isComplete, uncategorizedTransactions, currentTransaction, - remainingCount, animationState, lastSelectedCategory, sortedCategories, @@ -79,8 +78,6 @@ export function StepCategorizeTransactions({ categorizedCount >= minimumRequired || totalAvailable === 0; - const hasReachedMinimum = categorizedCount >= minimumRequired; - // Show rules hint after first categorization, only once useEffect(() => { if (categorizedCount === 1 && !hasSeenHint) { @@ -262,6 +259,51 @@ export function StepCategorizeTransactions({ return (
+ {/* Progress banner: makes the "categorize at least N" goal obvious */} +
+ {canContinue ? ( +
+ +

+ {__( + 'Done! You can continue now, or keep categorizing if you want.', + )} +

+
+ ) : ( +
+
+

+ {__( + 'To continue, you need to categorize at least :count transactions.', + { count: minimumRequired }, + )} +

+ + {categorizedCount}/{minimumRequired} + +
+
+ {Array.from({ length: minimumRequired }).map( + (_, i) => ( +
+ ), + )} +
+

+ {__('You do not need to categorize all of them.')} +

+
+ )} +
+ {/* Header row */}
{}}> @@ -322,27 +364,14 @@ export function StepCategorizeTransactions({ size="sm" onClick={onComplete} disabled={!canContinue} + className={ + canContinue + ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' + : undefined + } > {__('Continue')} - - - {hasReachedMinimum ? ( - <> - - {remainingCount} - {' '} - {__('remaining')} - - ) : ( - <> - - {categorizedCount} - - /{minimumRequired} - - )} -
diff --git a/resources/js/layouts/onboarding-layout.tsx b/resources/js/layouts/onboarding-layout.tsx index 999abe94..a60c82bd 100644 --- a/resources/js/layouts/onboarding-layout.tsx +++ b/resources/js/layouts/onboarding-layout.tsx @@ -55,7 +55,7 @@ export default function OnboardingLayout({