Cycle milestones
This commit is contained in:
parent
6c1f4a14c3
commit
0f720d7c91
3 changed files with 69 additions and 26 deletions
|
|
@ -9,6 +9,7 @@ interface Milestone {
|
|||
interface ProgressBarProps {
|
||||
currentShares: number;
|
||||
milestones: Milestone[];
|
||||
selectedMilestoneIndex?: number;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
|
@ -16,15 +17,18 @@ interface ProgressBarProps {
|
|||
export default function ProgressBar({
|
||||
currentShares,
|
||||
milestones,
|
||||
selectedMilestoneIndex = 0,
|
||||
className,
|
||||
onClick
|
||||
}: ProgressBarProps) {
|
||||
// Get the first milestone (lowest target) for progress calculation
|
||||
const firstMilestone = milestones.length > 0 ? milestones[0] : null;
|
||||
// Get the selected milestone for progress calculation
|
||||
const selectedMilestone = milestones.length > 0 && selectedMilestoneIndex < milestones.length
|
||||
? milestones[selectedMilestoneIndex]
|
||||
: null;
|
||||
|
||||
// Calculate progress percentage
|
||||
const progressPercentage = firstMilestone
|
||||
? Math.min((currentShares / firstMilestone.target) * 100, 100)
|
||||
const progressPercentage = selectedMilestone
|
||||
? Math.min((currentShares / selectedMilestone.target) * 100, 100)
|
||||
: 0;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -49,7 +53,7 @@ export default function ProgressBar({
|
|||
/>
|
||||
|
||||
{/* Text overlay */}
|
||||
{firstMilestone && (
|
||||
{selectedMilestone && (
|
||||
<div className="relative h-full flex items-center justify-center">
|
||||
{/* Base text (red on black background) */}
|
||||
<div className="text-red-500 font-mono text-sm font-bold mix-blend-difference relative z-10">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Plus, ChevronRight } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Milestone {
|
||||
|
|
@ -19,21 +19,31 @@ interface StatsBoxProps {
|
|||
profitLossPercentage?: number;
|
||||
};
|
||||
milestones?: Milestone[];
|
||||
selectedMilestoneIndex?: number;
|
||||
onMilestoneSelect?: (index: number) => void;
|
||||
className?: string;
|
||||
onAddPurchase?: () => void;
|
||||
onAddMilestone?: () => void;
|
||||
onUpdatePrice?: () => void;
|
||||
}
|
||||
|
||||
export default function StatsBox({
|
||||
stats,
|
||||
export default function StatsBox({
|
||||
stats,
|
||||
milestones = [],
|
||||
selectedMilestoneIndex = 0,
|
||||
onMilestoneSelect,
|
||||
className,
|
||||
onAddPurchase,
|
||||
onAddMilestone,
|
||||
onUpdatePrice
|
||||
}: StatsBoxProps) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const handleCycleMilestone = () => {
|
||||
if (milestones.length === 0 || !onMilestoneSelect) return;
|
||||
const nextIndex = (selectedMilestoneIndex + 1) % milestones.length;
|
||||
onMilestoneSelect(nextIndex);
|
||||
};
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
|
|
@ -52,7 +62,7 @@ export default function StatsBox({
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={cn(
|
||||
"bg-black p-8",
|
||||
"transition-all duration-300",
|
||||
|
|
@ -65,13 +75,13 @@ export default function StatsBox({
|
|||
<h2 className="text-red-500 text-lg font-mono font-bold tracking-wider">
|
||||
STATS
|
||||
</h2>
|
||||
<div className="flex items-center space-x-4 relative">
|
||||
<div className="flex items-center space-x-2 relative">
|
||||
{stats.currentPrice && (
|
||||
<div className="text-red-500 text-sm font-mono tracking-wider">
|
||||
VWCE: {formatCurrencyDetailed(stats.currentPrice)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Action Dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
|
|
@ -81,7 +91,7 @@ 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">
|
||||
|
|
@ -121,6 +131,17 @@ export default function StatsBox({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Milestone Cycle Button */}
|
||||
{milestones.length > 1 && (
|
||||
<button
|
||||
onClick={handleCycleMilestone}
|
||||
className="flex items-center justify-center px-2 py-1 rounded bg-blue-600/20 border border-blue-500/50 text-blue-400 hover:bg-blue-600/40 hover:text-blue-300 transition-colors text-sm"
|
||||
aria-label="Cycle milestone"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -141,28 +162,35 @@ export default function StatsBox({
|
|||
{/* Create combined array with current position and milestones, sorted by target */}
|
||||
{[
|
||||
...milestones.map(m => ({ ...m, isCurrent: false })),
|
||||
{
|
||||
target: stats.totalShares,
|
||||
description: 'CURRENT',
|
||||
created_at: '',
|
||||
isCurrent: true
|
||||
{
|
||||
target: stats.totalShares,
|
||||
description: 'CURRENT',
|
||||
created_at: '',
|
||||
isCurrent: true
|
||||
}
|
||||
]
|
||||
.sort((a, b) => a.target - b.target)
|
||||
.map((item, index) => {
|
||||
const swr3 = stats.currentPrice ? item.target * stats.currentPrice * 0.03 : 0;
|
||||
const swr4 = stats.currentPrice ? item.target * stats.currentPrice * 0.04 : 0;
|
||||
|
||||
|
||||
// Check if this milestone is the selected one for progress bar
|
||||
const isSelectedMilestone = !item.isCurrent && milestones.findIndex(m =>
|
||||
m.target === item.target && m.description === item.description
|
||||
) === selectedMilestoneIndex;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
<tr
|
||||
key={index}
|
||||
className={cn(
|
||||
"border-b border-red-500/10",
|
||||
item.isCurrent
|
||||
? "bg-red-500/10 text-red-300"
|
||||
: stats.totalShares >= item.target
|
||||
? "text-green-400/80"
|
||||
: "text-red-400/70"
|
||||
item.isCurrent
|
||||
? "bg-red-500/10 text-red-300"
|
||||
: isSelectedMilestone
|
||||
? "bg-blue-500/10 text-blue-300 border-blue-500/30"
|
||||
: stats.totalShares >= item.target
|
||||
? "text-green-400/80"
|
||||
: "text-red-400/70"
|
||||
)}
|
||||
>
|
||||
<td className="py-2 pr-4">
|
||||
|
|
@ -191,4 +219,4 @@ export default function StatsBox({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export default function Dashboard() {
|
|||
});
|
||||
|
||||
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);
|
||||
|
|
@ -92,12 +93,19 @@ export default function Dashboard() {
|
|||
if (milestonesResponse.ok) {
|
||||
const milestonesData = await milestonesResponse.json();
|
||||
setMilestones(milestonesData);
|
||||
// Reset to first milestone when milestones change
|
||||
setSelectedMilestoneIndex(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh milestone data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle milestone selection
|
||||
const handleMilestoneSelect = (index: number) => {
|
||||
setSelectedMilestoneIndex(index);
|
||||
};
|
||||
|
||||
// Refresh price data after successful update
|
||||
const handlePriceSuccess = async () => {
|
||||
try {
|
||||
|
|
@ -182,6 +190,7 @@ export default function Dashboard() {
|
|||
<ProgressBar
|
||||
currentShares={purchaseData.total_shares}
|
||||
milestones={milestones}
|
||||
selectedMilestoneIndex={selectedMilestoneIndex}
|
||||
onClick={handleProgressClick}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -191,6 +200,8 @@ export default function Dashboard() {
|
|||
<StatsBox
|
||||
stats={statsData}
|
||||
milestones={milestones}
|
||||
selectedMilestoneIndex={selectedMilestoneIndex}
|
||||
onMilestoneSelect={handleMilestoneSelect}
|
||||
onAddPurchase={() => setActiveForm('purchase')}
|
||||
onAddMilestone={() => setActiveForm('milestone')}
|
||||
onUpdatePrice={() => setActiveForm('price')}
|
||||
|
|
|
|||
Loading…
Reference in a new issue