37 - Add tracker type choice as first onboarding step

This commit is contained in:
myrmidex 2026-05-01 23:31:01 +02:00
parent c6a1681876
commit dd5f1c514e

View file

@ -1,9 +1,47 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import AssetSetupForm from '@/components/Assets/AssetSetupForm'; import AssetSetupForm from '@/components/Assets/AssetSetupForm';
import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm';
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
type TrackerType = 'simple' | 'asset';
function TrackerTypeSelector({ onSelect }: { onSelect: (type: TrackerType) => void }) {
return (
<div className="space-y-6">
<p className="text-red-400 font-mono text-sm uppercase tracking-wider">
[SELECT] What do you want to track?
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
onClick={() => onSelect('simple')}
className="text-left border-2 border-red-500/50 bg-black p-6 hover:bg-red-950/30 hover:border-red-400 hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] transition-all"
>
<span className="block text-red-400 font-mono text-lg font-bold uppercase tracking-wider mb-3">
[01] Simple counter
</span>
<p className="text-red-400/60 font-mono text-xs">
Track anything you accumulate no price tracking, no asset setup.
</p>
</button>
<button
onClick={() => onSelect('asset')}
className="text-left border-2 border-red-500/50 bg-black p-6 hover:bg-red-950/30 hover:border-red-400 hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] transition-all"
>
<span className="block text-red-400 font-mono text-lg font-bold uppercase tracking-wider mb-3">
[02] Asset tracker
</span>
<p className="text-red-400/60 font-mono text-xs">
Track holdings with price tracking and P&amp;L.
</p>
</button>
</div>
</div>
);
}
function PriceTrackingStep({ onEnable, onSkip }: { onEnable: () => void; onSkip?: () => void }) { function PriceTrackingStep({ onEnable, onSkip }: { onEnable: () => void; onSkip?: () => void }) {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
@ -50,77 +88,48 @@ interface OnboardingStep {
required: boolean; required: boolean;
} }
const ASSET_STEPS: OnboardingStep[] = [
{ id: 'asset', title: 'SET ASSET', description: 'Choose the asset you want to track', completed: false, required: true },
{ id: 'purchases', title: 'ADD PURCHASES', description: 'Enter your current holdings', completed: false, required: true },
{ id: 'milestones', title: 'SET MILESTONES', description: 'Define your goals', completed: false, required: true },
{ id: 'price', title: 'CURRENT PRICE', description: 'Set current asset price (optional)', completed: false, required: false },
];
const SIMPLE_STEPS: OnboardingStep[] = [
{ id: 'purchases', 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 },
];
interface OnboardingFlowProps { interface OnboardingFlowProps {
onComplete?: () => void; onComplete?: () => void;
} }
export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
const [trackerType, setTrackerType] = useState<TrackerType | null>(null);
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
const [steps, setSteps] = useState<OnboardingStep[]>([ const [steps, setSteps] = useState<OnboardingStep[]>([]);
{
id: 'asset',
title: 'SET ASSET',
description: 'Choose the asset you want to track',
completed: false,
required: true,
},
{
id: 'purchases',
title: 'ADD PURCHASES',
description: 'Enter your current holdings',
completed: false,
required: true,
},
{
id: 'milestones',
title: 'SET MILESTONES',
description: 'Define your goals',
completed: false,
required: true,
},
{
id: 'price',
title: 'CURRENT PRICE',
description: 'Set current asset price (optional)',
completed: false,
required: false,
},
]);
// Check onboarding status on mount const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => {
useEffect(() => {
checkOnboardingStatus();
}, []);
const checkOnboardingStatus = async () => {
try { try {
// Check asset const [purchaseData, milestonesData, assetData, priceData] = await Promise.all([
const assetResponse = await fetch('/assets/current'); fetch('/purchases/summary').then(r => r.json()),
const assetData = await assetResponse.json(); fetch('/milestones').then(r => r.json()),
const hasAsset = !!assetData.asset; fetch('/assets/current').then(r => r.json()),
fetch('/pricing/current').then(r => r.json()),
]);
// Check purchases
const purchaseResponse = await fetch('/purchases/summary');
const purchaseData = await purchaseResponse.json();
const hasPurchases = purchaseData.total_shares > 0; const hasPurchases = purchaseData.total_shares > 0;
// Check milestones
const milestonesResponse = await fetch('/milestones');
const milestonesData = await milestonesResponse.json();
const hasMilestones = milestonesData.length > 0; const hasMilestones = milestonesData.length > 0;
const hasAsset = !!assetData.asset;
// Check current price
const priceResponse = await fetch('/pricing/current');
const priceData = await priceResponse.json();
const hasPrice = !!priceData.current_price; const hasPrice = !!priceData.current_price;
const freshSteps = steps.map(step => ({ const freshSteps = currentSteps.map(step => ({
...step, ...step,
completed: completed:
(step.id === 'asset' && hasAsset) || (step.id === 'asset' && hasAsset) ||
(step.id === 'purchases' && hasPurchases) || (step.id === 'purchases' && hasPurchases) ||
(step.id === 'milestones' && hasMilestones) || (step.id === 'milestones' && hasMilestones) ||
(step.id === 'price' && hasPrice) (step.id === 'price' && hasPrice),
})); }));
setSteps(freshSteps); setSteps(freshSteps);
@ -129,24 +138,31 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
if (firstIncompleteRequired !== -1) { if (firstIncompleteRequired !== -1) {
setCurrentStep(firstIncompleteRequired); setCurrentStep(firstIncompleteRequired);
} else { } else if (onComplete) {
if (onComplete) { onComplete();
onComplete();
}
} }
} catch (error) { } catch (error) {
console.error('Failed to check onboarding status:', error); console.error('Failed to check onboarding status:', error);
} }
}; }, [onComplete]);
useEffect(() => {
if (trackerType === null) {
return;
}
const initialSteps = trackerType === 'simple' ? SIMPLE_STEPS : ASSET_STEPS;
setSteps(initialSteps);
setCurrentStep(0);
checkOnboardingStatus(initialSteps);
}, [trackerType, checkOnboardingStatus]);
const handleStepComplete = async () => { const handleStepComplete = async () => {
// Mark current step as completed const updatedSteps = steps.map((step, index) =>
setSteps(prev => prev.map((step, index) =>
index === currentStep ? { ...step, completed: true } : step index === currentStep ? { ...step, completed: true } : step
)); );
setSteps(updatedSteps);
// Refresh onboarding status — handles setCurrentStep and onComplete await checkOnboardingStatus(updatedSteps);
await checkOnboardingStatus();
}; };
const handleStepSelect = (stepIndex: number) => { const handleStepSelect = (stepIndex: number) => {
@ -155,33 +171,19 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
const renderStepContent = () => { const renderStepContent = () => {
const step = steps[currentStep]; const step = steps[currentStep];
if (!step) {
return null;
}
switch (step.id) { switch (step.id) {
case 'asset': case 'asset':
return ( return <AssetSetupForm onSuccess={handleStepComplete} />;
<AssetSetupForm
onSuccess={handleStepComplete}
/>
);
case 'purchases': case 'purchases':
return ( return <AddPurchaseForm onSuccess={handleStepComplete} />;
<AddPurchaseForm
onSuccess={handleStepComplete}
/>
);
case 'milestones': case 'milestones':
return ( return <AddMilestoneForm onSuccess={handleStepComplete} />;
<AddMilestoneForm
onSuccess={handleStepComplete}
/>
);
case 'price': case 'price':
return ( return <PriceTrackingStep onEnable={handleStepComplete} onSkip={onComplete} />;
<PriceTrackingStep
onEnable={handleStepComplete}
onSkip={onComplete}
/>
);
default: default:
return null; return null;
} }
@ -190,66 +192,69 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
return ( return (
<div className="min-h-screen bg-black flex items-center justify-center p-4"> <div className="min-h-screen bg-black flex items-center justify-center p-4">
<div className="w-full max-w-4xl"> <div className="w-full max-w-4xl">
{/* Terminal-style border with red glow */}
<div className="border-2 border-red-500 bg-black shadow-[0_0_20px_rgba(239,68,68,0.3)] p-8"> <div className="border-2 border-red-500 bg-black shadow-[0_0_20px_rgba(239,68,68,0.3)] p-8">
{/* Header */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-red-400 font-mono text-2xl font-bold uppercase tracking-wider mb-2"> <h1 className="text-red-400 font-mono text-2xl font-bold uppercase tracking-wider mb-2">
[SYSTEM] ONBOARDING SEQUENCE [SYSTEM] ONBOARDING SEQUENCE
</h1> </h1>
<p className="text-red-400/60 font-mono text-sm"> <p className="text-red-400/60 font-mono text-sm">
Set up your tracker {trackerType === null ? 'Choose how you want to track' : 'Set up your tracker'}
</p> </p>
</div> </div>
{/* Progress indicator */} {trackerType === null ? (
<div className="mb-8"> <div className="border border-red-500/30 bg-black/50 p-6">
<div className="flex items-center justify-between mb-4"> <TrackerTypeSelector onSelect={setTrackerType} />
{steps.map((step, index) => (
<button
key={step.id}
onClick={() => handleStepSelect(index)}
className={`flex-1 px-4 py-2 font-mono text-xs uppercase tracking-wider border border-red-500/50 transition-all ${
index === currentStep
? 'bg-red-500 text-black border-red-500'
: step.completed
? 'bg-red-950/50 text-red-300 border-red-400'
: 'bg-black text-red-400/60 hover:text-red-400 hover:border-red-400'
} ${index > 0 ? 'ml-2' : ''}`}
>
{step.completed ? '[✓]' : step.required ? '[REQ]' : '[OPT]'} {step.title}
</button>
))}
</div> </div>
) : (
<div className="text-center"> <>
<p className="text-red-400 font-mono text-sm"> <div className="mb-8">
{steps[currentStep].description} <div className="flex items-center justify-between mb-4">
</p> {steps.map((step, index) => (
<p className="text-red-400/60 font-mono text-xs mt-1"> <button
STEP {currentStep + 1}/{steps.length} key={step.id}
</p> onClick={() => handleStepSelect(index)}
</div> className={`flex-1 px-4 py-2 font-mono text-xs uppercase tracking-wider border border-red-500/50 transition-all ${
</div> index === currentStep
? 'bg-red-500 text-black border-red-500'
: step.completed
? 'bg-red-950/50 text-red-300 border-red-400'
: 'bg-black text-red-400/60 hover:text-red-400 hover:border-red-400'
} ${index > 0 ? 'ml-2' : ''}`}
>
{step.completed ? '[✓]' : step.required ? '[REQ]' : '[OPT]'} {step.title}
</button>
))}
</div>
{/* Step content */} <div className="text-center">
<div className="border border-red-500/30 bg-black/50 p-6"> <p className="text-red-400 font-mono text-sm">
{renderStepContent()} {steps[currentStep]?.description}
</div> </p>
<p className="text-red-400/60 font-mono text-xs mt-1">
STEP {currentStep + 1}/{steps.length}
</p>
</div>
</div>
{/* Status footer */} <div className="border border-red-500/30 bg-black/50 p-6">
<div className="mt-6 pt-4 border-t border-red-500/30"> {renderStepContent()}
<div className="flex justify-between items-center"> </div>
<p className="text-red-400/60 font-mono text-xs">
[STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE <div className="mt-6 pt-4 border-t border-red-500/30">
</p> <div className="flex justify-between items-center">
<p className="text-red-400/60 font-mono text-xs"> <p className="text-red-400/60 font-mono text-xs">
{steps.filter(s => s.required && !s.completed).length} REQUIRED REMAINING [STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE
</p> </p>
</div> <p className="text-red-400/60 font-mono text-xs">
</div> {steps.filter(s => s.required && !s.completed).length} REQUIRED REMAINING
</p>
</div>
</div>
</>
)}
</div> </div>
</div> </div>
</div> </div>
); );
} }