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