44 - Hide price tracking UI and functionality for v0.3.0

This commit is contained in:
myrmidex 2026-05-09 10:51:40 +02:00
parent 195e316da5
commit bace06d993
6 changed files with 58 additions and 421 deletions

View file

@ -1,14 +1,12 @@
import AddEntryForm from '@/components/Transactions/AddEntryForm';
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
import { cn } from '@/lib/utils';
type FormType = 'purchase' | 'milestone' | 'price';
type FormType = 'purchase' | 'milestone';
interface InlineFormProps {
type: FormType | null;
unit?: string;
priceTrackingEnabled?: boolean;
onClose: () => void;
onSuccess?: (type: FormType) => void;
className?: string;
@ -17,7 +15,6 @@ interface InlineFormProps {
export default function InlineForm({
type,
unit = 'units',
priceTrackingEnabled = false,
onClose,
onSuccess,
className,
@ -42,17 +39,11 @@ export default function InlineForm({
{type === 'purchase' ? (
<AddEntryForm
unit={unit}
priceTrackingEnabled={priceTrackingEnabled}
onSuccess={handleSuccess}
onCancel={onClose}
/>
) : type === 'milestone' ? (
<AddMilestoneForm
onSuccess={handleSuccess}
onCancel={onClose}
/>
) : (
<UpdatePriceForm
<AddMilestoneForm
onSuccess={handleSuccess}
onCancel={onClose}
/>

View file

@ -7,12 +7,6 @@ import type { Milestone } from '@/types/domain';
interface StatsBoxProps {
stats: {
totalShares: number;
totalInvestment: number;
averageCostPerShare: number;
currentPrice?: number;
currentValue?: number;
profitLoss?: number;
profitLossPercentage?: number;
};
unit?: string;
milestones?: Milestone[];
@ -21,9 +15,6 @@ interface StatsBoxProps {
className?: string;
onAddPurchase?: () => void;
onAddMilestone?: () => void;
onUpdatePrice?: () => void;
assetSymbol?: string;
priceTrackingEnabled?: boolean;
}
export default function StatsBox({
@ -35,9 +26,6 @@ export default function StatsBox({
className,
onAddPurchase,
onAddMilestone,
onUpdatePrice,
assetSymbol,
priceTrackingEnabled = false,
}: StatsBoxProps) {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@ -46,22 +34,6 @@ export default function StatsBox({
const nextIndex = (selectedMilestoneIndex + 1) % milestones.length;
onMilestoneSelect(nextIndex);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
};
const formatCurrencyDetailed = (amount: number) => {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 4,
}).format(amount);
};
return (
<div
@ -72,17 +44,10 @@ export default function StatsBox({
)}
>
<div className="w-full border-4 border-red-500 p-2 bg-black space-y-4 glow-red">
{/* STATS Title and Current Price */}
<div className="flex justify-between items-center mb-6 relative">
<ComponentTitle>Stats</ComponentTitle>
<div className="flex items-center space-x-2 relative">
{priceTrackingEnabled && stats.currentPrice && (
<div className="text-red-500 text-sm font-mono tracking-wider">
{assetSymbol ?? 'PRICE'}: {formatCurrencyDetailed(stats.currentPrice)}
</div>
)}
{/* Action Dropdown */}
<div className="relative">
<button
@ -93,7 +58,6 @@ export default function StatsBox({
<Plus className="w-4 h-4" />
</button>
{/* Dropdown Menu */}
{isDropdownOpen && (
<div className="absolute top-full right-0 mt-2 bg-black border-2 border-red-500/50 rounded shadow-lg min-w-40 z-10">
{onAddPurchase && (
@ -113,22 +77,11 @@ export default function StatsBox({
onAddMilestone();
setIsDropdownOpen(false);
}}
className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
>
ADD MILESTONE
</button>
)}
{priceTrackingEnabled && onUpdatePrice && (
<button
onClick={() => {
onUpdatePrice();
setIsDropdownOpen(false);
}}
className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0"
>
UPDATE PRICE
</button>
)}
</div>
)}
</div>
@ -155,64 +108,31 @@ export default function StatsBox({
<tr>
<th className="text-left text-red-500 text-xs py-2">DESCRIPTION</th>
<th className="text-right text-red-500 text-xs py-2">{unit.toUpperCase()}</th>
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2 pr-4">SWR 3%</th>}
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2">SWR 4%</th>}
</tr>
</thead>
<tbody>
{/* Current position row */}
<tr className="text-red-500 font-bold">
<td className="py-1 pr-4">CURRENT</td>
<td className="text-right py-1 pr-4">
<td className="text-right py-1">
{Math.floor(stats.totalShares).toLocaleString()}
</td>
{priceTrackingEnabled && (
<td className="text-right py-1 pr-4">
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.03) : 'N/A'}
</td>
)}
{priceTrackingEnabled && (
<td className="text-right py-1">
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.04) : 'N/A'}
</td>
)}
</tr>
{/* Render milestones after current */}
{milestones.map((milestone, index) => {
const swr3 = stats.currentPrice ? milestone.target * stats.currentPrice * 0.03 : 0;
const swr4 = stats.currentPrice ? milestone.target * stats.currentPrice * 0.04 : 0;
const isSelectedMilestone = index === selectedMilestoneIndex;
return (
{milestones.map((milestone, index) => (
<tr
key={index}
className={cn(
isSelectedMilestone
index === selectedMilestoneIndex
? "bg-red-500 text-black"
: "text-red-500 font-bold"
)}
>
<td className="py-1 pr-4">
{milestone.description}
</td>
<td className="text-right py-1 pr-4">
<td className="py-1 pr-4">{milestone.description}</td>
<td className="text-right py-1">
{Math.floor(milestone.target).toLocaleString()}
</td>
{priceTrackingEnabled && (
<td className="text-right py-1 pr-4">
{stats.currentPrice ? formatCurrency(swr3) : 'N/A'}
</td>
)}
{priceTrackingEnabled && (
<td className="text-right py-1">
{stats.currentPrice ? formatCurrency(swr4) : 'N/A'}
</td>
)}
</tr>
);
})}
))}
</tbody>
</table>
</div>

View file

@ -7,26 +7,15 @@ import { FormEventHandler, useState } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface CreateTrackerStepProps {
onSuccess: (priceTrackingEnabled: boolean) => void;
onSuccess: () => void;
}
export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps) {
const [label, setLabel] = useState('');
const [unit, setUnit] = useState('');
const [priceTracking, setPriceTracking] = useState(false);
const [symbol, setSymbol] = useState('');
const [fullName, setFullName] = useState('');
const [processing, setProcessing] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const togglePriceTracking = (enabled: boolean) => {
setPriceTracking(enabled);
if (!enabled) {
setSymbol('');
setFullName('');
}
};
const submit: FormEventHandler = async (e) => {
e.preventDefault();
setProcessing(true);
@ -43,14 +32,12 @@ export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps)
body: JSON.stringify({
label,
unit,
price_tracking_enabled: priceTracking ? 1 : 0,
symbol: priceTracking ? symbol : null,
full_name: priceTracking ? fullName : null,
price_tracking_enabled: 0,
}),
});
if (response.ok || response.status === 201 || response.status === 409) {
onSuccess(priceTracking);
onSuccess();
} else {
const data = await response.json();
if (data.errors) {
@ -111,59 +98,9 @@ export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps)
<InputError message={errors.unit} />
</div>
<div className="border border-red-500/30 p-4 space-y-3">
<label className="flex items-center gap-3 cursor-pointer group">
<input
type="checkbox"
checked={priceTracking}
onChange={(e) => togglePriceTracking(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
</span>
</label>
<p className="text-red-400/60 font-mono text-xs">
Track market price, portfolio value, and P&amp;L. Requires an asset symbol.
</p>
{priceTracking && (
<div className="space-y-3 pt-2">
<div>
<Label htmlFor="symbol" className="text-red-400 font-mono text-xs uppercase tracking-wider">
&gt; Asset Symbol
</Label>
<Input
id="symbol"
type="text"
placeholder="VWCE"
value={symbol}
onChange={(e) => setSymbol(e.target.value.toUpperCase())}
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 focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all"
/>
<InputError message={errors.symbol} />
</div>
<div>
<Label htmlFor="full_name" className="text-red-400 font-mono text-xs uppercase tracking-wider">
&gt; Full Name (Optional)
</Label>
<Input
id="full_name"
type="text"
placeholder="Vanguard FTSE All-World UCITS ETF"
value={fullName}
onChange={(e) => setFullName(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 focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all"
/>
<InputError message={errors.full_name} />
</div>
</div>
)}
</div>
<Button
type="submit"
disabled={processing || !label || !unit || (priceTracking && !symbol)}
disabled={processing || !label || !unit}
className="w-full bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}

View file

@ -1,48 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
import AssetSetupForm from '@/components/Assets/AssetSetupForm';
import AddEntryForm from '@/components/Transactions/AddEntryForm';
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>
);
}
interface OnboardingStep {
id: string;
title: string;
@ -51,13 +11,7 @@ interface OnboardingStep {
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[] = [
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 },
];
@ -68,7 +22,6 @@ interface OnboardingFlowProps {
export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
const [trackerCreated, setTrackerCreated] = useState(false);
const [priceTracking, setPriceTracking] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [steps, setSteps] = useState<OnboardingStep[]>([]);
@ -78,7 +31,6 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data?.tracker) {
setPriceTracking(data.tracker.price_tracking_enabled ?? false);
setTrackerCreated(true);
}
})
@ -87,22 +39,19 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
const checkOnboardingStatus = useCallback(async (currentSteps: OnboardingStep[]) => {
try {
const [entriesData, milestonesData, priceData] = await Promise.all([
const [entriesData, milestonesData] = await Promise.all([
fetch('/entries/summary').then(r => r.json()),
fetch('/milestones').then(r => r.json()),
fetch('/pricing/current').then(r => r.json()),
]);
const hasEntries = entriesData.total_quantity > 0;
const hasMilestones = milestonesData.length > 0;
const hasPrice = !!priceData.current_price;
const freshSteps = currentSteps.map(step => ({
...step,
completed:
(step.id === 'entries' && hasEntries) ||
(step.id === 'milestones' && hasMilestones) ||
(step.id === 'price' && hasPrice),
(step.id === 'milestones' && hasMilestones),
}));
setSteps(freshSteps);
@ -122,14 +71,12 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
useEffect(() => {
if (!trackerCreated) return;
const initialSteps = priceTracking ? ASSET_STEPS : SIMPLE_STEPS;
setSteps(initialSteps);
setSteps(STEPS);
setCurrentStep(0);
checkOnboardingStatus(initialSteps);
}, [trackerCreated, priceTracking, checkOnboardingStatus]);
checkOnboardingStatus(STEPS);
}, [trackerCreated, checkOnboardingStatus]);
const handleTrackerCreated = (withPriceTracking: boolean) => {
setPriceTracking(withPriceTracking);
const handleTrackerCreated = () => {
setTrackerCreated(true);
};
@ -151,11 +98,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
switch (step.id) {
case 'entries':
return <AddEntryForm onSuccess={handleStepComplete} priceTrackingEnabled={priceTracking} />;
return <AddEntryForm onSuccess={handleStepComplete} />;
case 'milestones':
return <AddMilestoneForm onSuccess={handleStepComplete} />;
case 'price':
return <PriceTrackingStep onEnable={handleStepComplete} onSkip={onComplete ?? (() => {})} />;
default:
return null;
}
@ -194,7 +139,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
: '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}
{step.completed ? '[✓]' : '[REQ]'} {step.title}
</button>
))}
</div>
@ -219,7 +164,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
[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
{steps.filter(s => !s.completed).length} REQUIRED REMAINING
</p>
</div>
</div>

View file

@ -11,30 +11,23 @@ import ComponentTitle from '@/components/ui/ComponentTitle';
interface EntryFormData {
date: string;
quantity: string;
unit_price: string;
total_cost: string;
[key: string]: string;
}
interface AddEntryFormProps {
unit?: string;
priceTrackingEnabled?: boolean;
onSuccess?: () => void;
onCancel?: () => void;
}
interface EntrySummary {
total_quantity: number;
total_cost: number;
average_cost_per_unit: number;
}
export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = false, onSuccess, onCancel }: AddEntryFormProps) {
export default function AddEntryForm({ unit = 'units', onSuccess, onCancel }: AddEntryFormProps) {
const { data, setData, post, processing, errors, reset } = useForm<EntryFormData>({
date: todayISO(),
quantity: '',
unit_price: '',
total_cost: '',
});
const [currentHoldings, setCurrentHoldings] = useState<EntrySummary | null>(null);
@ -55,18 +48,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
fetchSummary();
}, []);
// Auto-calculate total cost when quantity or unit_price changes
useEffect(() => {
if (data.quantity && data.unit_price) {
const quantity = parseFloat(data.quantity);
const unitPrice = parseFloat(data.unit_price);
if (!isNaN(quantity) && !isNaN(unitPrice)) {
setData('total_cost', (quantity * unitPrice).toFixed(2));
}
}
}, [data.quantity, data.unit_price, setData]);
const submit: FormEventHandler = (e) => {
e.preventDefault();
@ -86,7 +67,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
{currentHoldings && currentHoldings.total_quantity > 0 && (
<p className="text-sm text-red-400/60 font-mono">
[CURRENT] {currentHoldings.total_quantity.toFixed(6)} {unit}
{priceTrackingEnabled && ` • €${currentHoldings.total_cost.toFixed(2)} spent`}
</p>
)}
<form onSubmit={submit} className="space-y-4">
@ -120,41 +100,6 @@ export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = fa
<InputError message={errors.quantity} />
</div>
{priceTrackingEnabled && (
<>
<div>
<Label htmlFor="unit_price" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Price per {unit} ()</Label>
<Input
id="unit_price"
type="number"
step="0.01"
min="0"
placeholder="123.45"
value={data.unit_price}
onChange={(e) => setData('unit_price', 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"
/>
<InputError message={errors.unit_price} />
</div>
<div>
<Label htmlFor="total_cost" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Total Cost ()</Label>
<Input
id="total_cost"
type="number"
step="0.01"
min="0"
placeholder="1234.56"
value={data.total_cost}
onChange={(e) => setData('total_cost', 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"
/>
<p className="text-xs text-red-400/60 mt-1 font-mono">[AUTO-CALC] quantity × price</p>
<InputError message={errors.total_cost} />
</div>
</>
)}
<div className="flex gap-3 pt-2">
<Button
type="submit"

View file

@ -6,47 +6,28 @@ import OnboardingFlow from '@/components/Onboarding/OnboardingFlow';
import TerminalSpinner from '@/components/ui/TerminalSpinner';
import { Head } from '@inertiajs/react';
import { useCallback, useEffect, useState } from 'react';
import type { Milestone, Tracker, TrackerAsset } from '@/types/domain';
import type { Milestone, Tracker } from '@/types/domain';
interface PurchaseSummary {
interface EntrySummary {
total_shares: number;
total_investment: number;
average_cost_per_share: number;
}
interface CurrentPrice {
current_price: number | null;
}
export default function Dashboard() {
const [purchaseData, setPurchaseData] = useState<PurchaseSummary>({
total_shares: 0,
total_investment: 0,
average_cost_per_share: 0,
});
const [priceData, setPriceData] = useState<CurrentPrice>({
current_price: null,
});
const [totalShares, setTotalShares] = useState(0);
const [milestones, setMilestones] = useState<Milestone[]>([]);
const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0);
const [showProgressBar, setShowProgressBar] = useState(false);
const [showStatsBox, setShowStatsBox] = useState(false);
const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null);
const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null);
const [loading, setLoading] = useState(true);
const [needsOnboarding, setNeedsOnboarding] = useState(false);
const [tracker, setTracker] = useState<Tracker | null>(null);
const [currentAsset, setCurrentAsset] = useState<TrackerAsset | null>(null);
const [priceTrackingEnabled, setPriceTrackingEnabled] = useState(false);
// Fetch entry summary, current price, milestones, and check onboarding
useEffect(() => {
const fetchData = async () => {
try {
const [entriesResponse, priceResponse, milestonesResponse, trackerResponse] = await Promise.all([
const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([
fetch('/entries/summary'),
fetch('/pricing/current'),
fetch('/milestones'),
fetch('/tracker'),
]);
@ -56,19 +37,10 @@ export default function Dashboard() {
if (entriesResponse.ok) {
const entries = await entriesResponse.json();
setPurchaseData({
total_shares: entries.total_quantity,
total_investment: entries.total_cost,
average_cost_per_share: entries.average_cost_per_unit,
});
setTotalShares(entries.total_quantity);
totalQuantity = entries.total_quantity;
}
if (priceResponse.ok) {
const price = await priceResponse.json();
setPriceData(price);
}
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
@ -78,8 +50,6 @@ export default function Dashboard() {
if (trackerResponse.ok) {
const { tracker: trackerData } = await trackerResponse.json();
setTracker(trackerData ?? null);
setCurrentAsset(trackerData?.asset ?? null);
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
}
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
@ -93,31 +63,24 @@ export default function Dashboard() {
fetchData();
}, []);
// Refresh data after successful entry
const handlePurchaseSuccess = async () => {
try {
const entriesResponse = await fetch('/entries/summary');
if (entriesResponse.ok) {
const entries = await entriesResponse.json();
setPurchaseData({
total_shares: entries.total_quantity,
total_investment: entries.total_cost,
average_cost_per_share: entries.average_cost_per_unit,
});
setTotalShares(entries.total_quantity);
}
} catch (error) {
console.error('Failed to refresh entry data:', error);
}
};
// Refresh milestones after successful creation
const handleMilestoneSuccess = async () => {
try {
const milestonesResponse = await fetch('/milestones');
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
// Reset to first milestone when milestones change
setSelectedMilestoneIndex(0);
}
} catch (error) {
@ -125,53 +88,13 @@ export default function Dashboard() {
}
};
// Handle milestone selection
const handleMilestoneSelect = (index: number) => {
setSelectedMilestoneIndex(index);
};
// Refresh price data after successful update
const handlePriceSuccess = async () => {
try {
const priceResponse = await fetch('/pricing/current');
if (priceResponse.ok) {
const price = await priceResponse.json();
setPriceData(price);
}
} catch (error) {
console.error('Failed to refresh price data:', error);
}
};
// Calculate portfolio stats
const currentValue = priceData.current_price
? purchaseData.total_shares * priceData.current_price
: undefined;
const profitLoss = currentValue
? currentValue - purchaseData.total_investment
: undefined;
const profitLossPercentage = profitLoss && purchaseData.total_investment > 0
? (profitLoss / purchaseData.total_investment) * 100
: undefined;
const statsData = {
totalShares: purchaseData.total_shares,
totalInvestment: purchaseData.total_investment,
averageCostPerShare: purchaseData.average_cost_per_share,
currentPrice: priceData.current_price || undefined,
currentValue,
profitLoss,
profitLossPercentage,
};
// Handle onboarding completion — must be before any early returns (Rules of Hooks)
const handleOnboardingComplete = useCallback(async () => {
const [entriesResponse, priceResponse, milestonesResponse, trackerResponse] = await Promise.all([
const [entriesResponse, milestonesResponse, trackerResponse] = await Promise.all([
fetch('/entries/summary'),
fetch('/pricing/current'),
fetch('/milestones'),
fetch('/tracker'),
]);
@ -181,19 +104,10 @@ export default function Dashboard() {
if (entriesResponse.ok) {
const entries = await entriesResponse.json();
setPurchaseData({
total_shares: entries.total_quantity,
total_investment: entries.total_cost,
average_cost_per_share: entries.average_cost_per_unit,
});
setTotalShares(entries.total_quantity);
totalQuantity = entries.total_quantity;
}
if (priceResponse.ok) {
const price = await priceResponse.json();
setPriceData(price);
}
if (milestonesResponse.ok) {
const milestonesData = await milestonesResponse.json();
setMilestones(milestonesData);
@ -201,10 +115,8 @@ export default function Dashboard() {
}
if (trackerResponse.ok) {
const trackerData = await trackerResponse.json();
setTracker(trackerData);
setCurrentAsset(trackerData?.asset ?? null);
setPriceTrackingEnabled(trackerData?.price_tracking_enabled ?? false);
const { tracker: trackerData } = await trackerResponse.json();
setTracker(trackerData ?? null);
}
setNeedsOnboarding(totalQuantity === 0 || milestonesCount === 0);
@ -219,22 +131,19 @@ export default function Dashboard() {
);
}
// Toggle handlers with cascading behavior
const handleLedClick = () => {
const newShowProgressBar = !showProgressBar;
setShowProgressBar(newShowProgressBar);
if (!newShowProgressBar) {
// If hiding progress bar, also hide stats box
setShowStatsBox(false);
}
};
const handleProgressClick = () => {
setShowStatsBox(!showStatsBox);
setActiveForm(null)
setActiveForm(null);
};
// Show onboarding if needed
if (needsOnboarding) {
return (
<>
@ -248,55 +157,45 @@ export default function Dashboard() {
<>
<Head title="incr" />
{/* Stacked Layout */}
<div className="min-h-screen bg-black">
<div className="w-full max-w-4xl mx-auto px-4">
{/* Box 1: LED Number Display - Fixed position from top */}
<div className="pt-32">
<LedDisplay
value={purchaseData.total_shares}
value={totalShares}
unit={tracker?.unit}
onClick={handleLedClick}
/>
</div>
{/* Box 2: Progress Bar (toggleable) */}
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
<ProgressBar
currentQuantity={purchaseData.total_shares}
currentQuantity={totalShares}
milestones={milestones}
selectedMilestoneIndex={selectedMilestoneIndex}
onClick={handleProgressClick}
/>
</div>
{/* Box 3: Stats Box (toggleable) */}
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
<StatsBox
stats={statsData}
stats={{ totalShares }}
unit={tracker?.unit}
milestones={milestones}
selectedMilestoneIndex={selectedMilestoneIndex}
onMilestoneSelect={handleMilestoneSelect}
onAddPurchase={() => setActiveForm('purchase')}
onAddMilestone={() => setActiveForm('milestone')}
onUpdatePrice={() => setActiveForm('price')}
assetSymbol={currentAsset?.symbol}
priceTrackingEnabled={priceTrackingEnabled}
/>
</div>
{/* Box 4: Forms (only when active form is set) */}
<div style={{ display: activeForm && showProgressBar && showStatsBox ? 'block' : 'none' }}>
<InlineForm
type={activeForm}
unit={tracker?.unit}
priceTrackingEnabled={priceTrackingEnabled}
onClose={() => setActiveForm(null)}
onSuccess={(type) => {
if (type === 'purchase') handlePurchaseSuccess();
else if (type === 'milestone') handleMilestoneSuccess();
else if (type === 'price') handlePriceSuccess();
}}
/>
</div>