From 8ce8a20d356e7e7260555a8eb7add8667b4418f9 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 14:32:03 +0200 Subject: [PATCH] 51 - Trim onboarding to a single starting-value input --- app/Http/Controllers/TrackerController.php | 8 +- .../js/components/Display/LedDisplay.tsx | 16 +- .../components/Onboarding/OnboardingFlow.tsx | 227 ++++++------------ resources/js/pages/dashboard.tsx | 88 +++---- 4 files changed, 112 insertions(+), 227 deletions(-) diff --git a/app/Http/Controllers/TrackerController.php b/app/Http/Controllers/TrackerController.php index 512e6b4..4631686 100644 --- a/app/Http/Controllers/TrackerController.php +++ b/app/Http/Controllers/TrackerController.php @@ -26,8 +26,8 @@ public function show(): JsonResponse public function store(Request $request): JsonResponse { $validated = $request->validate([ - 'label' => 'required|string|max:255', - 'unit' => 'required|string|max:50', + 'label' => 'sometimes|string|max:255', + 'unit' => 'sometimes|string|max:50', 'price_tracking_enabled' => 'boolean', 'symbol' => 'nullable|string|max:10', 'full_name' => 'nullable|string|max:255', @@ -46,8 +46,8 @@ public function store(Request $request): JsonResponse } $tracker = $user->tracker()->create([ - 'label' => $validated['label'], - 'unit' => $validated['unit'], + 'label' => $validated['label'] ?? 'Counter', + 'unit' => $validated['unit'] ?? 'units', 'price_tracking_enabled' => $validated['price_tracking_enabled'] ?? false, 'asset_id' => $assetId, ]); diff --git a/resources/js/components/Display/LedDisplay.tsx b/resources/js/components/Display/LedDisplay.tsx index 4ca24a8..a535b35 100644 --- a/resources/js/components/Display/LedDisplay.tsx +++ b/resources/js/components/Display/LedDisplay.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'; interface LedDisplayProps { value: number; - unit?: string; className?: string; animate?: boolean; onClick?: () => void; @@ -11,7 +10,6 @@ interface LedDisplayProps { export default function LedDisplay({ value, - unit, className, onClick }: LedDisplayProps) { @@ -23,14 +21,7 @@ export default function LedDisplay({ return; }, [value]); - // Format number with zero-padding for consistent width - const formatValue = (value: number) => { - // Always pad to 5 digits for consistent display width - const integerPart = Math.floor(value); - return integerPart.toString().padStart(5, '0'); - }; - - const formattedValue = formatValue(displayValue); + const formattedValue = Math.floor(displayValue).toString(); return (
- {unit && ( -
- {unit} -
- )} ); } diff --git a/resources/js/components/Onboarding/OnboardingFlow.tsx b/resources/js/components/Onboarding/OnboardingFlow.tsx index 669ef72..a379c7f 100644 --- a/resources/js/components/Onboarding/OnboardingFlow.tsx +++ b/resources/js/components/Onboarding/OnboardingFlow.tsx @@ -1,175 +1,102 @@ -import { useState, useEffect, useCallback } from 'react'; -import AddEntryForm from '@/components/Transactions/AddEntryForm'; -import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; -import CreateTrackerStep from '@/components/Onboarding/CreateTrackerStep'; - -interface OnboardingStep { - id: string; - title: string; - description: string; - completed: boolean; - required: boolean; -} - -const STEPS: OnboardingStep[] = [ - { id: 'entries', title: 'STARTING AMOUNT', description: 'Enter your starting amount', completed: false, required: true }, - { id: 'milestones', title: 'SET MILESTONES', description: 'Define your goals', completed: false, required: true }, -]; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/InputError'; +import { todayISO } from '@/lib/utils'; +import { LoaderCircle } from 'lucide-react'; +import { FormEventHandler, useState } from 'react'; interface OnboardingFlowProps { onComplete?: () => void; } +function csrfToken(): string { + return (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? ''; +} + export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { - const [trackerCreated, setTrackerCreated] = useState(false); - const [currentStep, setCurrentStep] = useState(0); - const [steps, setSteps] = useState([]); + const [startingValue, setStartingValue] = useState('0'); + const [processing, setProcessing] = useState(false); + const [error, setError] = useState(); - // On mount: check if a tracker already exists and skip step 1 if so - useEffect(() => { - fetch('/tracker') - .then(r => r.ok ? r.json() : null) - .then(data => { - if (data?.tracker) { - setTrackerCreated(true); - } - }) - .catch(() => {}); - }, []); + const submit: FormEventHandler = async (e) => { + e.preventDefault(); + setProcessing(true); + setError(undefined); + + const headers = { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken(), + Accept: 'application/json', + }; - const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => { try { - const [entriesData, milestonesData] = await Promise.all([ - fetch('/entries/summary').then(r => r.json()), - fetch('/milestones').then(r => r.json()), - ]); + const trackerResponse = await fetch(route('tracker.store'), { + method: 'POST', + headers, + body: JSON.stringify({}), + }); - const hasEntries = entriesData.total_quantity > 0; - const hasMilestones = milestonesData.length > 0; - - const freshSteps = currentSteps.map(step => ({ - ...step, - completed: - (step.id === 'entries' && hasEntries) || - (step.id === 'milestones' && hasMilestones), - })); - - setSteps(freshSteps); - - const firstIncompleteRequired = freshSteps.findIndex(s => s.required && !s.completed); - - if (firstIncompleteRequired !== -1) { - setCurrentStep(firstIncompleteRequired); - } else if (onComplete) { - onComplete(); + if (!trackerResponse.ok && trackerResponse.status !== 409) { + setError('Could not create the counter. Please try again.'); + return; } - } catch (error) { - console.error('Failed to check onboarding status:', error); - } - }, [onComplete]); - useEffect(() => { - if (!trackerCreated) return; + const quantity = Number(startingValue); - setSteps(STEPS); - setCurrentStep(0); - checkOnboardingStatus(STEPS); - }, [trackerCreated, checkOnboardingStatus]); + if (quantity > 0) { + const entryResponse = await fetch(route('entries.store'), { + method: 'POST', + headers, + body: JSON.stringify({ date: todayISO(), quantity }), + }); - const handleTrackerCreated = () => { - setTrackerCreated(true); - }; + if (!entryResponse.ok) { + setError('Could not save the starting value. Please try again.'); + return; + } + } - const handleStepComplete = async () => { - const updatedSteps = steps.map((step, index) => - index === currentStep ? { ...step, completed: true } : step - ); - setSteps(updatedSteps); - await checkOnboardingStatus(updatedSteps); - }; - - const handleStepSelect = (stepIndex: number) => { - setCurrentStep(stepIndex); - }; - - const renderStepContent = () => { - const step = steps[currentStep]; - if (!step) return null; - - switch (step.id) { - case 'entries': - return ; - case 'milestones': - return ; - default: - return null; + onComplete?.(); + } catch { + setError('Something went wrong. Please try again.'); + } finally { + setProcessing(false); } }; return (
-
+
-
-

- [SYSTEM] ONBOARDING SEQUENCE -

-

- {!trackerCreated ? 'Set up your tracker' : 'Configure your tracker'} -

-
+
+ + setStartingValue(e.target.value)} + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red" + /> + - {!trackerCreated ? ( -
- -
- ) : ( - <> -
-
- {steps.map((step, index) => ( - - ))} -
- -
-

- {steps[currentStep]?.description} -

-

- STEP {currentStep + 1}/{steps.length} -

-
-
- -
- {renderStepContent()} -
- -
-
-

- [STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE -

-

- {steps.filter(s => !s.completed).length} REQUIRED REMAINING -

-
-
- - )} + +
diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index d79db8d..b380a5d 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -19,36 +19,37 @@ export default function Dashboard() { const [needsOnboarding, setNeedsOnboarding] = useState(false); const [tracker, setTracker] = useState(null); + const loadData = useCallback(async () => { + const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([ + fetch('/entries/summary'), + fetch('/milestones'), + fetch('/tracker'), + ]); + + if (entriesResponse.ok) { + const entries = await entriesResponse.json(); + setTotalShares(entries.total_quantity); + } + + if (milestonesResponse.ok) { + setMilestones(await milestonesResponse.json()); + } + + let trackerExists = false; + + if (trackerResponse.ok) { + const { exists, tracker: trackerData } = await trackerResponse.json(); + setTracker(trackerData ?? null); + trackerExists = Boolean(exists); + } + + setNeedsOnboarding(!trackerExists); + }, []); + useEffect(() => { const fetchData = async () => { try { - const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([ - fetch('/entries/summary'), - fetch('/milestones'), - fetch('/tracker'), - ]); - - let totalQuantity = 0; - let milestonesCount = 0; - - if (entriesResponse.ok) { - const entries = await entriesResponse.json(); - setTotalShares(entries.total_quantity); - totalQuantity = entries.total_quantity; - } - - if (milestonesResponse.ok) { - const milestonesData = await milestonesResponse.json(); - setMilestones(milestonesData); - milestonesCount = milestonesData.length; - } - - if (trackerResponse.ok) { - const { tracker: trackerData } = await trackerResponse.json(); - setTracker(trackerData ?? null); - } - - setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0); + await loadData(); } catch (error) { console.error('Failed to fetch data:', error); } finally { @@ -57,7 +58,7 @@ export default function Dashboard() { }; fetchData(); - }, []); + }, [loadData]); const handlePurchaseSuccess = async () => { try { @@ -88,35 +89,7 @@ export default function Dashboard() { setSelectedMilestoneIndex(index); }; - const handleOnboardingComplete = useCallback(async () => { - const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([ - fetch('/entries/summary'), - fetch('/milestones'), - fetch('/tracker'), - ]); - - let totalQuantity = 0; - let milestonesCount = 0; - - if (entriesResponse.ok) { - const entries = await entriesResponse.json(); - setTotalShares(entries.total_quantity); - totalQuantity = entries.total_quantity; - } - - if (milestonesResponse.ok) { - const milestonesData = await milestonesResponse.json(); - setMilestones(milestonesData); - milestonesCount = milestonesData.length; - } - - if (trackerResponse.ok) { - const { tracker: trackerData } = await trackerResponse.json(); - setTracker(trackerData ?? null); - } - - setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0); - }, []); + const handleOnboardingComplete = loadData; if (loading) { return ( @@ -158,7 +131,6 @@ export default function Dashboard() {