From c1360eada0d455e6081098a89c6a7aa663e8921c Mon Sep 17 00:00:00 2001 From: "modeinspect-eu[bot]" <204106916+modeinspect-eu[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 13:26:40 +0000 Subject: [PATCH] Added interactive flight price history component with dual-view interface for enhanced booking experience and personalized recommendations. --- PriceHistoryWidget.tsx | 295 +++++++++++++++++++++++++++++++++++++++++ README.md | 60 +++++++-- index.tsx | 37 ++++++ mockPriceData.ts | 81 +++++++++++ 4 files changed, 459 insertions(+), 14 deletions(-) create mode 100644 PriceHistoryWidget.tsx create mode 100644 index.tsx create mode 100644 mockPriceData.ts diff --git a/PriceHistoryWidget.tsx b/PriceHistoryWidget.tsx new file mode 100644 index 0000000..442b413 --- /dev/null +++ b/PriceHistoryWidget.tsx @@ -0,0 +1,295 @@ +import React, { useState } from 'react'; +import { mockPriceData, PriceHistoryData, PriceDataPoint } from './mockPriceData'; + +const PriceHistoryWidget: React.FC = () => { + const [isExpanded, setIsExpanded] = useState(false); + const data = mockPriceData; + + return ( +
+ {isExpanded ? ( + setIsExpanded(false)} + /> + ) : ( + setIsExpanded(true)} + /> + )} +
+ ); +}; + +interface ViewProps { + data: PriceHistoryData; + onExpand?: () => void; + onCollapse?: () => void; +} + +const CollapsedView: React.FC = ({ data, onExpand }) => { + const { + currentPrice, + currency, + recommendation, + priceChangePercentage + } = data; + + return ( +
+
+

Price History

+ +
+ +
+
{currency} {currentPrice}
+
+ {priceChangePercentage >= 0 ? '+' : ''}{priceChangePercentage}% +
+
+ +
+ {recommendation === 'buy' && ( +
+ + + + Good time to buy! Prices are below average. +
+ )} + {recommendation === 'wait' && ( +
+ + + + Consider waiting. Prices are higher than usual. +
+ )} + {recommendation === 'neutral' && ( +
+ + + + Prices are around average for this route. +
+ )} +
+
+ ); +}; + +const ExpandedView: React.FC = ({ data, onCollapse }) => { + const { + currentPrice, + currency, + averagePrice, + lowestPrice, + highestPrice, + recommendation, + history + } = data; + + // Find the highest price in the dataset for graph scaling + const maxPrice = Math.max(...history.map(item => item.price)); + const minPrice = Math.min(...history.map(item => item.price)); + const priceRange = maxPrice - minPrice; + const buffer = priceRange * 0.1; // 10% buffer + + // Graph height and scaling + const graphHeight = 150; + const getY = (price: number) => { + return graphHeight - ((price - (minPrice - buffer)) / ((maxPrice + buffer) - (minPrice - buffer))) * graphHeight; + }; + + // Generate path for the graph + const graphWidth = 300; + const pointSpacing = graphWidth / (history.length - 1); + + let pathData = `M 0,${getY(history[0].price)}`; + history.forEach((point, index) => { + if (index > 0) { + pathData += ` L ${index * pointSpacing},${getY(point.price)}`; + } + }); + + return ( +
+
+

Price History

+ +
+ +
+
Last 30 days
+
Current: {currency} {currentPrice}
+
+ + {/* Graph */} +
+ + {/* Price line */} + + + {/* Average price line */} + + + {/* Data points */} + {history.map((point, index) => ( + + + + {/* Label for lowest price */} + {point.isLowestPrice && ( + <> + + Lowest + + + )} + + ))} + + + {/* Y-axis labels */} +
+
{Math.round(maxPrice + buffer)}
+
{Math.round(minPrice - buffer)}
+
+
+ + {/* Price stats */} +
+
+
Lowest
+
{currency} {lowestPrice}
+
+
+
Average
+
{currency} {averagePrice}
+
+
+
Highest
+
{currency} {highestPrice}
+
+
+ + {/* Recommendation */} +
+
+ {recommendation === 'buy' && ( +
+ + + +
+

Good time to buy!

+

Current prices are {Math.round((averagePrice - currentPrice) / averagePrice * 100)}% below the average for this route.

+
+
+ )} + {recommendation === 'wait' && ( +
+ + + +
+

Consider waiting for prices to drop

+

Current prices are {Math.round((currentPrice - averagePrice) / averagePrice * 100)}% above the average for this route.

+
+
+ )} + {recommendation === 'neutral' && ( +
+ + + +
+

Prices are stable

+

Current prices are close to the average for this route.

+
+
+ )} +
+
+
+ ); +}; + +export default PriceHistoryWidget; \ No newline at end of file diff --git a/README.md b/README.md index 0b8e1c8..f0f8280 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,54 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Flight Price History Component -## Getting Started +This is a prototype of a price history feature for a flight search website. The component displays price trends and recommendations at a glance, with the ability to expand into a full interactive graph view. -First, run the development server: +## Features -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +- Compact view showing current price, price trend, and quick recommendation +- Expandable detailed view with price history graph +- Visual indicators for price changes (up/down) +- Recommendations based on price analysis +- Responsive design using Tailwind CSS + +## How to Use + +Import the component in your Next.js/React application: + +```jsx +import PriceHistoryWidget from './components/PriceHistoryWidget'; + +// Then use it in your component +function FlightDetails() { + return ( +
+

Flight Details

+ +
+ ); +} ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +## Data Structure + +The component expects price history data in the following format: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```typescript +type PriceHistoryData = { + currency: string; + currentPrice: number; + averagePrice: number; + lowestPrice: number; + highestPrice: number; + recommendation: "buy" | "wait" | "neutral"; + priceChangePercentage: number; + history: PriceDataPoint[]; +}; -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +type PriceDataPoint = { + date: string; // ISO date string + price: number; + isLowestPrice?: boolean; +}; +``` +For this prototype, mock data is generated internally. In a real application, you would connect this to your actual price history API. \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..cb7bcb0 --- /dev/null +++ b/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PriceHistoryWidget from './PriceHistoryWidget'; + +const Home: React.FC = () => { + return ( +
+
+

+ Flight Price History Feature +

+ +
+
+
+

+ New York to London +

+

+ Dec 15 - Dec 22 • Economy • 1 Adult +

+
+ + + +
+ +
+
+
+
+
+ ); +}; + +export default Home; \ No newline at end of file diff --git a/mockPriceData.ts b/mockPriceData.ts new file mode 100644 index 0000000..20ce8c6 --- /dev/null +++ b/mockPriceData.ts @@ -0,0 +1,81 @@ +// Mock data for the price history component +export type PriceDataPoint = { + date: string; // ISO date string + price: number; + isLowestPrice?: boolean; +}; + +export type PriceHistoryData = { + currency: string; + currentPrice: number; + averagePrice: number; + lowestPrice: number; + highestPrice: number; + recommendation: "buy" | "wait" | "neutral"; + priceChangePercentage: number; // negative means price dropped + history: PriceDataPoint[]; +}; + +// Generate mock data for the last 30 days +const generateMockData = (): PriceHistoryData => { + const today = new Date(); + const history: PriceDataPoint[] = []; + let lowestPrice = Infinity; + let highestPrice = 0; + let lowestPriceIndex = 0; + + // Generate price points for the last 30 days + for (let i = 29; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + + // Base price with some randomness + const basePrice = 350; + const randomFactor = Math.sin(i / 5) * 50 + Math.random() * 30 - 15; + const price = Math.round(basePrice + randomFactor); + + history.push({ + date: date.toISOString().split('T')[0], + price + }); + + if (price < lowestPrice) { + lowestPrice = price; + lowestPriceIndex = history.length - 1; + } + + if (price > highestPrice) { + highestPrice = price; + } + } + + // Mark the lowest price + history[lowestPriceIndex].isLowestPrice = true; + + const currentPrice = history[history.length - 1].price; + const sum = history.reduce((acc, item) => acc + item.price, 0); + const averagePrice = Math.round(sum / history.length); + + // Determine recommendation based on current price vs average + let recommendation: "buy" | "wait" | "neutral" = "neutral"; + if (currentPrice < averagePrice * 0.95) { + recommendation = "buy"; + } else if (currentPrice > averagePrice * 1.05) { + recommendation = "wait"; + } + + const priceChangePercentage = Math.round(((currentPrice - history[0].price) / history[0].price) * 100); + + return { + currency: "USD", + currentPrice, + averagePrice, + lowestPrice, + highestPrice, + recommendation, + priceChangePercentage, + history + }; +}; + +export const mockPriceData = generateMockData(); \ No newline at end of file