incr/resources/js/components/Onboarding/OnboardingFlow.tsx

220 lines
9.7 KiB
TypeScript
Raw Normal View History

import { useState, useEffect, useCallback } from 'react';
2025-08-01 00:56:26 +02:00
import AssetSetupForm from '@/components/Assets/AssetSetupForm';
import AddEntryForm from '@/components/Transactions/AddEntryForm';
2025-08-01 00:56:26 +02:00
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
import CreateTrackerStep from '@/components/Onboarding/CreateTrackerStep';
function PriceTrackingStep({ onEnable, onSkip }: { onEnable: () => void; onSkip?: () => void }) {
const [enabled, setEnabled] = useState(false);
return (
<div className="space-y-6">
<label className="flex items-center gap-3 cursor-pointer group">
<input
type="checkbox"
checked={enabled}
onChange={e => setEnabled(e.target.checked)}
className="w-4 h-4 accent-red-500"
/>
<span className="text-red-400 font-mono text-sm uppercase tracking-wider group-hover:text-red-300">
Enable price tracking (optional)
</span>
</label>
<p className="text-red-400/60 font-mono text-xs">
Track the current market price of your asset to see portfolio value and P&amp;L. You can enable this later in settings.
</p>
{enabled && (
<div className="border border-red-500/30 p-4">
<UpdatePriceForm onSuccess={onEnable} />
</div>
)}
{!enabled && (
<button
onClick={onSkip ?? (() => {})}
className="w-full py-2 font-mono text-xs uppercase tracking-wider border border-red-500/50 text-red-400 hover:bg-red-950/30 hover:text-red-300 transition-colors"
>
Skip and finish
</button>
)}
</div>
);
}
2025-08-01 00:56:26 +02:00
interface OnboardingStep {
id: string;
title: string;
description: string;
completed: boolean;
required: boolean;
}
const ASSET_STEPS: OnboardingStep[] = [
{ id: 'entries', title: 'ADD ENTRIES', 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: '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 },
];
2025-08-01 00:56:26 +02:00
interface OnboardingFlowProps {
onComplete?: () => void;
}
export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
const [trackerCreated, setTrackerCreated] = useState(false);
const [priceTracking, setPriceTracking] = useState(false);
2025-08-01 00:56:26 +02:00
const [currentStep, setCurrentStep] = useState(0);
const [steps, setSteps] = useState<OnboardingStep[]>([]);
2025-08-01 00:56:26 +02:00
const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => {
2025-08-01 00:56:26 +02:00
try {
const [entriesData, milestonesData, priceData] = await Promise.all([
fetch('/entries/summary').then(r => r.json()),
fetch('/milestones').then(r => r.json()),
fetch('/pricing/current').then(r => r.json()),
]);
2025-08-01 00:56:26 +02:00
const hasEntries = entriesData.total_quantity > 0;
2025-08-01 00:56:26 +02:00
const hasMilestones = milestonesData.length > 0;
const hasPrice = !!priceData.current_price;
const freshSteps = currentSteps.map(step => ({
2025-08-01 00:56:26 +02:00
...step,
completed:
(step.id === 'entries' && hasEntries) ||
2025-08-01 00:56:26 +02:00
(step.id === 'milestones' && hasMilestones) ||
(step.id === 'price' && hasPrice),
}));
setSteps(freshSteps);
const firstIncompleteRequired = freshSteps.findIndex(s => s.required && !s.completed);
if (firstIncompleteRequired !== -1) {
setCurrentStep(firstIncompleteRequired);
} else if (onComplete) {
onComplete();
2025-08-01 00:56:26 +02:00
}
} catch (error) {
console.error('Failed to check onboarding status:', error);
}
}, [onComplete]);
useEffect(() => {
if (!trackerCreated) return;
const initialSteps = priceTracking ? ASSET_STEPS : SIMPLE_STEPS;
setSteps(initialSteps);
setCurrentStep(0);
checkOnboardingStatus(initialSteps);
}, [trackerCreated, priceTracking, checkOnboardingStatus]);
const handleTrackerCreated = (withPriceTracking: boolean) => {
setPriceTracking(withPriceTracking);
setTrackerCreated(true);
};
2025-08-01 00:56:26 +02:00
const handleStepComplete = async () => {
const updatedSteps = steps.map((step, index) =>
2025-08-01 00:56:26 +02:00
index === currentStep ? { ...step, completed: true } : step
);
setSteps(updatedSteps);
await checkOnboardingStatus(updatedSteps);
2025-08-01 00:56:26 +02:00
};
const handleStepSelect = (stepIndex: number) => {
setCurrentStep(stepIndex);
};
const renderStepContent = () => {
const step = steps[currentStep];
if (!step) return null;
2025-08-01 00:56:26 +02:00
switch (step.id) {
case 'entries':
return <AddEntryForm onSuccess={handleStepComplete} priceTrackingEnabled={priceTracking} />;
2025-08-01 00:56:26 +02:00
case 'milestones':
return <AddMilestoneForm onSuccess={handleStepComplete} />;
2025-08-01 00:56:26 +02:00
case 'price':
return <PriceTrackingStep onEnable={handleStepComplete} onSkip={onComplete ?? (() => {})} />;
2025-08-01 00:56:26 +02:00
default:
return null;
}
};
return (
<div className="min-h-screen bg-black flex items-center justify-center p-4">
<div className="w-full max-w-4xl">
<div className="border-2 border-red-500 bg-black shadow-[0_0_20px_rgba(239,68,68,0.3)] p-8">
<div className="mb-8">
<h1 className="text-red-400 font-mono text-2xl font-bold uppercase tracking-wider mb-2">
[SYSTEM] ONBOARDING SEQUENCE
</h1>
<p className="text-red-400/60 font-mono text-sm">
{!trackerCreated ? 'Set up your tracker' : 'Configure your tracker'}
2025-08-01 00:56:26 +02:00
</p>
</div>
{!trackerCreated ? (
<div className="border border-red-500/30 bg-black/50 p-6">
<CreateTrackerStep onSuccess={handleTrackerCreated} />
2025-08-01 00:56:26 +02:00
</div>
) : (
<>
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
{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>
2025-08-01 00:56:26 +02:00
<div className="text-center">
<p className="text-red-400 font-mono text-sm">
{steps[currentStep]?.description}
</p>
<p className="text-red-400/60 font-mono text-xs mt-1">
STEP {currentStep + 1}/{steps.length}
</p>
</div>
</div>
2025-08-01 00:56:26 +02:00
<div className="border border-red-500/30 bg-black/50 p-6">
{renderStepContent()}
</div>
<div className="mt-6 pt-4 border-t border-red-500/30">
<div className="flex justify-between items-center">
<p className="text-red-400/60 font-mono text-xs">
[STATUS] {steps.filter(s => s.completed).length}/{steps.length} STEPS COMPLETE
</p>
<p className="text-red-400/60 font-mono text-xs">
{steps.filter(s => s.required && !s.completed).length} REQUIRED REMAINING
</p>
</div>
</div>
</>
)}
2025-08-01 00:56:26 +02:00
</div>
</div>
</div>
);
}