Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions frontend/src/components/AssetSelector.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,21 @@
opacity: 0.5;
}

.asset-selector__selected {
display: flex;
flex-direction: column;
gap: 0.25rem;
}

.asset-selector__symbol {
font-weight: 700;
color: var(--text-primary);
font-size: 1rem;
}

.asset-selector__name {
font-size: 0.875rem;
.asset-selector__company-label {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.25rem;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.asset-selector__placeholder {
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/AssetSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,18 @@ export function AssetSelector({ assets, selectedSymbol, onSelect, disabled = fal
disabled={disabled}
>
{selectedAsset ? (
<div className={styles.assetSelectorSelected}>
<span className={styles.assetSelectorSymbol}>{selectedAsset.symbol}</span>
<span className={styles.assetSelectorName}>{selectedAsset.name}</span>
</div>
<span className={styles.assetSelectorSymbol}>{selectedAsset.symbol}</span>
) : (
<span className={styles.assetSelectorPlaceholder}>Choose an asset...</span>
)}
<span className={styles.assetSelectorArrow}>{isOpen ? '▲' : '▼'}</span>
</button>

{/* Company name below trigger — keeps trigger compact */}
{selectedAsset && (
<span className={styles.assetSelectorCompanyLabel}>{selectedAsset.name}</span>
)}

{isOpen && (
<div className={styles.assetSelectorDropdown}>
{/* Filter Controls */}
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/components/InvestmentBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface InvestmentBuilderProps {
export function InvestmentBuilder({ assets, onSimulate, isSimulating }: InvestmentBuilderProps) {
const { investments, setInvestments } = useSimulationContext();
const [showValidation, setShowValidation] = useState(false);
const [newInvestmentIds, setNewInvestmentIds] = useState<Set<string>>(new Set());

// Create a new empty investment
function createEmptyInvestment(): Investment {
Expand All @@ -36,15 +37,26 @@ export function InvestmentBuilder({ assets, onSimulate, isSimulating }: Investme
// Add new investment row
const handleAddInvestment = () => {
if (investments.length < 10) {
setInvestments([...investments, createEmptyInvestment()]);
const newInv = createEmptyInvestment();
setInvestments([...investments, newInv]);
setNewInvestmentIds((prev) => new Set(prev).add(newInv.id));
}
};

// Update an investment
// Update an investment (also marks it as no longer "new")
const handleUpdateInvestment = (index: number, updated: Investment) => {
const newInvestments = [...investments];
newInvestments[index] = updated;
setInvestments(newInvestments);

// Once a field is interacted with, stop suppressing validation
if (newInvestmentIds.has(updated.id)) {
setNewInvestmentIds((prev) => {
const next = new Set(prev);
next.delete(updated.id);
return next;
});
}
};

// Remove an investment
Expand All @@ -70,8 +82,9 @@ export function InvestmentBuilder({ assets, onSimulate, isSimulating }: Investme

// Handle simulation
const handleSimulate = () => {
// Always show validation when user clicks simulate
// Reveal all validation errors (including on "new" rows)
setShowValidation(true);
setNewInvestmentIds(new Set());

if (!canSimulate) return;

Expand Down Expand Up @@ -105,6 +118,7 @@ export function InvestmentBuilder({ assets, onSimulate, isSimulating }: Investme
onRemove={() => handleRemoveInvestment(index)}
canRemove={investments.length > 1}
showValidation={showValidation}
isNew={newInvestmentIds.has(investment.id)}
/>
))}
</div>
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/InvestmentForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
font-family: var(--font-family);
}

.investment-form__ipo-info {
margin-left: 0.375rem;
cursor: help;
font-size: 0.875rem;
vertical-align: middle;
}

.investment-form__input {
width: 100%;
padding: 0.75rem 1rem;
Expand Down
33 changes: 30 additions & 3 deletions frontend/src/components/InvestmentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ interface InvestmentFormProps {
onRemove: () => void;
canRemove: boolean;
showValidation?: boolean; // Only show errors when true (after simulate attempt)
isNew?: boolean; // Suppress validation on freshly-added rows
}

/**
* Format an ISO date string (YYYY-MM-DD) to a readable label.
* e.g. "1986-03-13" → "Mar 13, 1986"
*/
function formatIpoDate(dateStr: string): string {
const d = new Date(dateStr + 'T00:00:00');
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}

/**
Expand All @@ -26,7 +36,8 @@ export function InvestmentForm({
onUpdate,
onRemove,
canRemove,
showValidation = false
showValidation = false,
isNew = false
}: InvestmentFormProps) {
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
Expand All @@ -36,8 +47,9 @@ export function InvestmentForm({
setTouched(prev => ({ ...prev, [field]: true }));
};

// Show error only if touched or showValidation is true
// Show error only if (touched OR showValidation) AND not a fresh "new" row
const shouldShowError = (field: string) => {
if (isNew) return false;
return (touched[field] || showValidation) && errors[field];
};

Expand Down Expand Up @@ -99,6 +111,10 @@ export function InvestmentForm({
investment.amountUsd > 0 &&
investment.purchaseDate;

// Resolve selected asset for IPO date tooltip
const selectedAsset = assets.find(a => a.symbol === investment.symbol);
const ipoDate = selectedAsset?.ipoDate || undefined;

return (
<div className={`${styles.investmentForm} ${!isValid ? styles.investmentFormInvalid : ''}`}>
<div className={styles.investmentFormGrid}>
Expand Down Expand Up @@ -138,10 +154,21 @@ export function InvestmentForm({

{/* Date Picker */}
<div className={styles.investmentFormField}>
<label className={styles.investmentFormLabel}>Purchase Date</label>
<label className={styles.investmentFormLabel}>
Purchase Date
{ipoDate && (
<span
className={styles.investmentFormIpoInfo}
title={`${selectedAsset!.symbol} available from ${formatIpoDate(ipoDate)}`}
>
ℹ️
</span>
)}
</label>
<input
type="date"
className={styles.investmentFormInput}
min={ipoDate || undefined}
max={new Date().toISOString().split('T')[0]}
value={investment.purchaseDate}
onChange={handleDateChange}
Expand Down