diff --git a/resources/js/components/Display/InlineForm.tsx b/resources/js/components/Display/InlineForm.tsx new file mode 100644 index 0000000..949de4d --- /dev/null +++ b/resources/js/components/Display/InlineForm.tsx @@ -0,0 +1,68 @@ +import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; +import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; +import { cn } from '@/lib/utils'; +import { X } from 'lucide-react'; + +interface InlineFormProps { + type: 'purchase' | 'milestone' | null; + onClose: () => void; + onPurchaseSuccess?: () => void; + onMilestoneSuccess?: () => void; + className?: string; +} + +export default function InlineForm({ + type, + onClose, + onPurchaseSuccess, + onMilestoneSuccess, + className +}: InlineFormProps) { + if (!type) return null; + + const title = type === 'purchase' ? 'ADD PURCHASE' : 'ADD MILESTONE'; + + return ( +
+ {/* Header */} +
+

+ {title} +

+ +
+ + {/* Form Content */} +
+ {type === 'purchase' ? ( + { + if (onPurchaseSuccess) onPurchaseSuccess(); + onClose(); + }} + /> + ) : ( + { + if (onMilestoneSuccess) onMilestoneSuccess(); + onClose(); + }} + /> + )} +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/LedDisplay.tsx b/resources/js/components/Display/LedDisplay.tsx new file mode 100644 index 0000000..c12f0c6 --- /dev/null +++ b/resources/js/components/Display/LedDisplay.tsx @@ -0,0 +1,104 @@ +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface LedDisplayProps { + value: number; + className?: string; + animate?: boolean; + onClick?: () => void; +} + +export default function LedDisplay({ + value, + className, + animate = true, + onClick +}: LedDisplayProps) { + const [displayValue, setDisplayValue] = useState(0); + + // Animate number changes + useEffect(() => { + if (!animate) { + setDisplayValue(value); + return; + } + + const duration = 1000; // 1 second animation + const steps = 60; // 60fps + const stepValue = (value - displayValue) / steps; + + if (Math.abs(stepValue) < 0.01) { + setDisplayValue(value); + return; + } + + const timer = setInterval(() => { + setDisplayValue(prev => { + const next = prev + stepValue; + if (Math.abs(next - value) < Math.abs(stepValue)) { + clearInterval(timer); + return value; + } + return next; + }); + }, duration / steps); + + return () => clearInterval(timer); + }, [value, displayValue, animate]); + + // Format number appropriately for shares + const formatValue = (value: number) => { + // If it's a whole number, show it as integer + if (value % 1 === 0) { + return value.toString(); + } + // Otherwise show up to 6 decimal places, removing trailing zeros + return value.toFixed(6).replace(/\.?0+$/, ''); + }; + + const formattedValue = formatValue(displayValue); + + return ( +
+
+ {/* Background glow effect */} +
+ {formattedValue} +
+ + {/* Main LED text */} +
+ {formattedValue} +
+ + {/* Subtle scan line effect */} +
+
+
+
+ + {/* Label */} +
+ total shares +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx new file mode 100644 index 0000000..a777d3a --- /dev/null +++ b/resources/js/components/Display/ProgressBar.tsx @@ -0,0 +1,60 @@ +import { cn } from '@/lib/utils'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { useState } from 'react'; + +interface ProgressBarProps { + value: number; + className?: string; + onClick?: () => void; +} + +export default function ProgressBar({ + value, + className, + onClick +}: ProgressBarProps) { + const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); + + // Milestone definitions + const milestones = [ + { target: 1500, label: '1.5K', color: 'bg-blue-500' }, + { target: 3000, label: '3K', color: 'bg-green-500' }, + { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, + { target: 6000, label: '6K', color: 'bg-red-500' }, + ]; + + const currentMilestone = milestones[currentMilestoneIndex]; + const progress = Math.min((value / currentMilestone.target) * 100, 100); + const isCompleted = value >= currentMilestone.target; + + // Milestone navigation + const nextMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev < milestones.length - 1 ? prev + 1 : 0 + ); + }; + + const prevMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev > 0 ? prev - 1 : milestones.length - 1 + ); + }; + + return ( +
+
+ PROGRESS BAR +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx new file mode 100644 index 0000000..c0e426e --- /dev/null +++ b/resources/js/components/Display/StatsBox.tsx @@ -0,0 +1,156 @@ +import { cn } from '@/lib/utils'; +import { Plus } from 'lucide-react'; + +interface StatsBoxProps { + stats: { + totalShares: number; + totalInvestment: number; + averageCostPerShare: number; + currentPrice?: number; + currentValue?: number; + profitLoss?: number; + profitLossPercentage?: number; + }; + className?: string; + onAddPurchase?: () => void; + onAddMilestone?: () => void; +} + +export default function StatsBox({ + stats, + className, + onAddPurchase, + onAddMilestone +}: StatsBoxProps) { + 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 ( +
+ {/* Current Price */} + {stats.currentPrice && ( +
+
+ current price +
+
+ {formatCurrencyDetailed(stats.currentPrice)} +
+
+ )} + + {/* Portfolio Stats Grid */} +
+ {/* Total Investment */} +
+
Total Investment
+
+ {formatCurrency(stats.totalInvestment)} +
+
+ + {/* Current Value */} + {stats.currentValue && ( +
+
Current Value
+
+ {formatCurrency(stats.currentValue)} +
+
+ )} + + {/* Average Cost */} +
+
Avg Cost/Share
+
+ {formatCurrencyDetailed(stats.averageCostPerShare)} +
+
+ + {/* Profit/Loss */} + {stats.profitLoss !== undefined && ( +
+
P&L
+
= 0 ? "text-green-400" : "text-red-400" + )}> + {stats.profitLoss >= 0 ? '+' : ''}{formatCurrency(stats.profitLoss)} +
+
+ )} +
+ + {/* Withdrawal Estimates */} + {stats.currentValue && ( +
+
Annual Withdrawal (Safe)
+
+
+
3% Rule
+
+ {formatCurrency(stats.currentValue * 0.03)} +
+
+
+
4% Rule
+
+ {formatCurrency(stats.currentValue * 0.04)} +
+
+
+
+ )} + + {/* Action Buttons */} +
+
+ {/* Add Purchase Button */} + {onAddPurchase && ( + + )} + + {/* Add Milestone Button */} + {onAddMilestone && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index e9a97e6..c9c3c1d 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,7 +1,7 @@ -import LedCounter from '@/components/Display/LedCounter'; -import MilestoneModal from '@/components/Display/MilestoneModal'; -import PurchaseModal from '@/components/Display/PurchaseModal'; -import StatsPanel from '@/components/Display/StatsPanel'; +import LedDisplay from '@/components/Display/LedDisplay'; +import InlineForm from '@/components/Display/InlineForm'; +import ProgressBar from '@/components/Display/ProgressBar'; +import StatsBox from '@/components/Display/StatsBox'; import { Head } from '@inertiajs/react'; import { useEffect, useState } from 'react'; @@ -26,9 +26,9 @@ export default function Dashboard() { current_price: null, }); - const [showStats, setShowStats] = useState(false); - const [showPurchaseModal, setShowPurchaseModal] = useState(false); - const [showMilestoneModal, setShowMilestoneModal] = useState(false); + const [showProgressBar, setShowProgressBar] = useState(false); + const [showStatsBox, setShowStatsBox] = useState(false); + const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null); const [loading, setLoading] = useState(true); // Fetch purchase summary and current price @@ -109,44 +109,62 @@ 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); + }; + return ( <> - {/* Main LED Display */} -
- setShowStats(!showStats)} - showStats={showStats} - onAddPurchase={() => setShowPurchaseModal(true)} - onAddMilestone={() => setShowMilestoneModal(true)} - /> - - {/* Stats Panel */} - - - {/* Purchase Modal */} - setShowPurchaseModal(false)} - onSuccess={handlePurchaseSuccess} - /> - - {/* Milestone Modal */} - setShowMilestoneModal(false)} - onSuccess={() => { - // Could refresh milestone data here if needed - console.log('Milestone added successfully'); - }} - /> + {/* Stacked Layout */} +
+
+ {/* Box 1: LED Number Display */} + + + {/* Box 2: Progress Bar (toggleable) */} +
+ +
+ + {/* Box 3: Stats Box (toggleable) */} +
+ setActiveForm('purchase')} + onAddMilestone={() => setActiveForm('milestone')} + /> +
+ + {/* Box 4: Forms (only when active form is set) */} +
+ setActiveForm(null)} + onPurchaseSuccess={handlePurchaseSuccess} + onMilestoneSuccess={() => { + console.log('Milestone added successfully'); + }} + /> +
+
);