Spaces:
Running
Running
Fix price filter to work in display currency (e.g. CAD)
Browse filesThe price filter compared the user's input directly against price_usd,
but displayed prices are converted to the user's chosen currency.
Typing C$187 would not filter out a C$233 flight because its USD
price (~$160) was below 187.
Now the filter converts max_price from display currency to USD via
toUsd() before comparing. Also shows the correct currency symbol
in the filter input instead of always "$".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
frontend/dist/assets/index-CNhMhsb3.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/index-zJXLSgGb.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Flight Search</title>
|
| 8 |
-
<script type="module" crossorigin src="/assets/index-
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-B_aBvo40.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
|
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Flight Search</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-zJXLSgGb.js"></script>
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-B_aBvo40.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
frontend/src/components/results/FilterPanel.tsx
CHANGED
|
@@ -5,9 +5,10 @@ interface Props {
|
|
| 5 |
flights: FlightOffer[];
|
| 6 |
filters: Filters;
|
| 7 |
onChange: (f: Filters) => void;
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
-
export default function FilterPanel({ flights, filters, onChange }: Props) {
|
| 11 |
// Compute available filter options from flights
|
| 12 |
const airlines = useMemo(() => {
|
| 13 |
const map = new Map<string, string>();
|
|
@@ -43,7 +44,7 @@ export default function FilterPanel({ flights, filters, onChange }: Props) {
|
|
| 43 |
<div>
|
| 44 |
<h3 className="mb-2 text-sm font-medium text-gray-900">Max price</h3>
|
| 45 |
<div className="flex items-center gap-2">
|
| 46 |
-
<span className="text-sm text-gray-500">
|
| 47 |
<input
|
| 48 |
type="number"
|
| 49 |
value={filters.max_price ?? ''}
|
|
|
|
| 5 |
flights: FlightOffer[];
|
| 6 |
filters: Filters;
|
| 7 |
onChange: (f: Filters) => void;
|
| 8 |
+
currencySymbol?: string;
|
| 9 |
}
|
| 10 |
|
| 11 |
+
export default function FilterPanel({ flights, filters, onChange, currencySymbol = '$' }: Props) {
|
| 12 |
// Compute available filter options from flights
|
| 13 |
const airlines = useMemo(() => {
|
| 14 |
const map = new Map<string, string>();
|
|
|
|
| 44 |
<div>
|
| 45 |
<h3 className="mb-2 text-sm font-medium text-gray-900">Max price</h3>
|
| 46 |
<div className="flex items-center gap-2">
|
| 47 |
+
<span className="text-sm text-gray-500">{currencySymbol}</span>
|
| 48 |
<input
|
| 49 |
type="number"
|
| 50 |
value={filters.max_price ?? ''}
|
frontend/src/contexts/CurrencyContext.tsx
CHANGED
|
@@ -5,6 +5,10 @@ interface CurrencyContextValue {
|
|
| 5 |
currency: string;
|
| 6 |
setCurrency: (c: string) => void;
|
| 7 |
formatPrice: (usd: number) => string;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
currencies: string[];
|
| 9 |
symbols: Record<string, string>;
|
| 10 |
}
|
|
@@ -13,6 +17,8 @@ const CurrencyContext = createContext<CurrencyContextValue>({
|
|
| 13 |
currency: 'USD',
|
| 14 |
setCurrency: () => {},
|
| 15 |
formatPrice: (usd) => `$${Math.round(usd).toLocaleString()}`,
|
|
|
|
|
|
|
| 16 |
currencies: ['USD'],
|
| 17 |
symbols: { USD: '$' },
|
| 18 |
});
|
|
@@ -49,21 +55,22 @@ export function CurrencyProvider({ children, defaultCurrency }: { children: Reac
|
|
| 49 |
localStorage.setItem('preferred_currency', c);
|
| 50 |
}
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
function formatPrice(usd: number): string {
|
| 53 |
-
const rate = rates[currency] ?? 1;
|
| 54 |
-
const symbol = symbols[currency] ?? currency;
|
| 55 |
const converted = Math.round(usd * rate);
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
return
|
| 61 |
}
|
| 62 |
|
| 63 |
const currencies = Object.keys(rates).sort();
|
| 64 |
|
| 65 |
return (
|
| 66 |
-
<CurrencyContext.Provider value={{ currency, setCurrency, formatPrice, currencies, symbols }}>
|
| 67 |
{children}
|
| 68 |
</CurrencyContext.Provider>
|
| 69 |
);
|
|
|
|
| 5 |
currency: string;
|
| 6 |
setCurrency: (c: string) => void;
|
| 7 |
formatPrice: (usd: number) => string;
|
| 8 |
+
/** Convert a display-currency amount back to USD. */
|
| 9 |
+
toUsd: (displayAmount: number) => number;
|
| 10 |
+
/** Current currency symbol (e.g. "C$", "€"). */
|
| 11 |
+
symbol: string;
|
| 12 |
currencies: string[];
|
| 13 |
symbols: Record<string, string>;
|
| 14 |
}
|
|
|
|
| 17 |
currency: 'USD',
|
| 18 |
setCurrency: () => {},
|
| 19 |
formatPrice: (usd) => `$${Math.round(usd).toLocaleString()}`,
|
| 20 |
+
toUsd: (n) => n,
|
| 21 |
+
symbol: '$',
|
| 22 |
currencies: ['USD'],
|
| 23 |
symbols: { USD: '$' },
|
| 24 |
});
|
|
|
|
| 55 |
localStorage.setItem('preferred_currency', c);
|
| 56 |
}
|
| 57 |
|
| 58 |
+
const rate = rates[currency] ?? 1;
|
| 59 |
+
const currentSymbol = symbols[currency] ?? currency;
|
| 60 |
+
|
| 61 |
function formatPrice(usd: number): string {
|
|
|
|
|
|
|
| 62 |
const converted = Math.round(usd * rate);
|
| 63 |
+
return `${currentSymbol}${converted.toLocaleString()}`;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function toUsd(displayAmount: number): number {
|
| 67 |
+
return displayAmount / rate;
|
| 68 |
}
|
| 69 |
|
| 70 |
const currencies = Object.keys(rates).sort();
|
| 71 |
|
| 72 |
return (
|
| 73 |
+
<CurrencyContext.Provider value={{ currency, setCurrency, formatPrice, toUsd, symbol: currentSymbol, currencies, symbols }}>
|
| 74 |
{children}
|
| 75 |
</CurrencyContext.Provider>
|
| 76 |
);
|
frontend/src/pages/ResultsPage.tsx
CHANGED
|
@@ -39,7 +39,7 @@ function parseMcLegs(searchParams: URLSearchParams): MultiCityLeg[] {
|
|
| 39 |
export default function ResultsPage() {
|
| 40 |
const [searchParams, setSearchParams] = useSearchParams();
|
| 41 |
const navigate = useNavigate();
|
| 42 |
-
const { formatPrice } = useCurrency();
|
| 43 |
const { outboundFlights, returnFlights, multiCityFlights, sameAirlineDiscount, loading, error, searched, search } = useFlightSearch();
|
| 44 |
const [sortBy, setSortBy] = useState<SortBy>('best');
|
| 45 |
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
|
|
@@ -160,15 +160,17 @@ export default function ResultsPage() {
|
|
| 160 |
flights = flights.filter(f => f.stops <= filters.max_stops!);
|
| 161 |
}
|
| 162 |
if (filters.max_price) {
|
|
|
|
|
|
|
| 163 |
flights = flights.filter(f => {
|
| 164 |
// For round trips, filter by the round-trip price shown to the user
|
| 165 |
if (showingReturn && selectedOutbound) {
|
| 166 |
-
return (returnRoundTripPrices.get(f.id) ?? f.price_usd) <=
|
| 167 |
}
|
| 168 |
if (isRoundTrip && !showingReturn) {
|
| 169 |
-
return (outboundMinPrices.get(f.id) ?? f.price_usd) <=
|
| 170 |
}
|
| 171 |
-
return f.price_usd <=
|
| 172 |
});
|
| 173 |
}
|
| 174 |
if (filters.airlines && filters.airlines.length > 0) {
|
|
@@ -211,7 +213,7 @@ export default function ResultsPage() {
|
|
| 211 |
}
|
| 212 |
|
| 213 |
return flights;
|
| 214 |
-
}, [outboundFlights, eligibleReturnFlights, currentLegFlights, showingReturn, isRoundTrip, isMultiCity, selectedOutbound, outboundMinPrices, returnRoundTripPrices, filters, sortBy]);
|
| 215 |
|
| 216 |
// Split into "best" and "other" flights
|
| 217 |
const bestFlights = useMemo(() => filteredFlights.filter(f => f.is_best), [filteredFlights]);
|
|
@@ -472,6 +474,7 @@ export default function ResultsPage() {
|
|
| 472 |
flights={filterSourceFlights}
|
| 473 |
filters={filters}
|
| 474 |
onChange={setFilters}
|
|
|
|
| 475 |
/>
|
| 476 |
</div>
|
| 477 |
</aside>
|
|
|
|
| 39 |
export default function ResultsPage() {
|
| 40 |
const [searchParams, setSearchParams] = useSearchParams();
|
| 41 |
const navigate = useNavigate();
|
| 42 |
+
const { formatPrice, toUsd, symbol: currencySymbol } = useCurrency();
|
| 43 |
const { outboundFlights, returnFlights, multiCityFlights, sameAirlineDiscount, loading, error, searched, search } = useFlightSearch();
|
| 44 |
const [sortBy, setSortBy] = useState<SortBy>('best');
|
| 45 |
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
|
|
|
|
| 160 |
flights = flights.filter(f => f.stops <= filters.max_stops!);
|
| 161 |
}
|
| 162 |
if (filters.max_price) {
|
| 163 |
+
// Convert display-currency amount to USD for comparison against price_usd
|
| 164 |
+
const maxUsd = toUsd(filters.max_price);
|
| 165 |
flights = flights.filter(f => {
|
| 166 |
// For round trips, filter by the round-trip price shown to the user
|
| 167 |
if (showingReturn && selectedOutbound) {
|
| 168 |
+
return (returnRoundTripPrices.get(f.id) ?? f.price_usd) <= maxUsd;
|
| 169 |
}
|
| 170 |
if (isRoundTrip && !showingReturn) {
|
| 171 |
+
return (outboundMinPrices.get(f.id) ?? f.price_usd) <= maxUsd;
|
| 172 |
}
|
| 173 |
+
return f.price_usd <= maxUsd;
|
| 174 |
});
|
| 175 |
}
|
| 176 |
if (filters.airlines && filters.airlines.length > 0) {
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
return flights;
|
| 216 |
+
}, [outboundFlights, eligibleReturnFlights, currentLegFlights, showingReturn, isRoundTrip, isMultiCity, selectedOutbound, outboundMinPrices, returnRoundTripPrices, filters, sortBy, toUsd]);
|
| 217 |
|
| 218 |
// Split into "best" and "other" flights
|
| 219 |
const bestFlights = useMemo(() => filteredFlights.filter(f => f.is_best), [filteredFlights]);
|
|
|
|
| 474 |
flights={filterSourceFlights}
|
| 475 |
filters={filters}
|
| 476 |
onChange={setFilters}
|
| 477 |
+
currencySymbol={currencySymbol}
|
| 478 |
/>
|
| 479 |
</div>
|
| 480 |
</aside>
|