feat: Add basic styling

This commit is contained in:
bracesproul
2025-03-03 17:53:00 -08:00
parent 5857b634f9
commit 6b382879c5
8 changed files with 3035 additions and 40 deletions

View File

@@ -82,6 +82,7 @@ Before you can help them, you need to extract the following information from the
You are provided with the ENTIRE conversation history between you, and the user. Use these messages to extract the necessary information. You are provided with the ENTIRE conversation history between you, and the user. Use these messages to extract the necessary information.
Do NOT guess, or make up any information. If the user did NOT specify a location, please respond with a request for them to specify the location. Do NOT guess, or make up any information. If the user did NOT specify a location, please respond with a request for them to specify the location.
You should ONLY send a clarification message if the user did not provide the location. You do NOT need any of the other fields, so if they're missing, proceed without them.
It should be a single sentence, along the lines of "Please specify the location for the trip you want to go on". It should be a single sentence, along the lines of "Please specify the location for the trip you want to go on".
Extract only what is specified by the user. It is okay to leave fields blank if the user did not specify them. Extract only what is specified by the user. It is okay to leave fields blank if the user did not specify them.

View File

@@ -1,9 +1,959 @@
import "./index.css"; import { useState } from "react";
export default function PortfolioView() { export default function PortfolioView() {
// Placeholder portfolio data - ideally would come from props
const [portfolio] = useState({
totalValue: 156842.75,
cashBalance: 12467.32,
performance: {
daily: 1.24,
weekly: -0.52,
monthly: 3.87,
yearly: 14.28,
},
holdings: [
{
symbol: "AAPL",
name: "Apple Inc.",
shares: 45,
price: 187.32,
value: 8429.4,
change: 1.2,
allocation: 5.8,
avgCost: 162.5,
},
{
symbol: "MSFT",
name: "Microsoft Corporation",
shares: 30,
price: 403.78,
value: 12113.4,
change: 0.5,
allocation: 8.4,
avgCost: 340.25,
},
{
symbol: "AMZN",
name: "Amazon.com Inc.",
shares: 25,
price: 178.75,
value: 4468.75,
change: -0.8,
allocation: 3.1,
avgCost: 145.3,
},
{
symbol: "GOOGL",
name: "Alphabet Inc.",
shares: 20,
price: 164.85,
value: 3297.0,
change: 2.1,
allocation: 2.3,
avgCost: 125.75,
},
{
symbol: "NVDA",
name: "NVIDIA Corporation",
shares: 35,
price: 875.28,
value: 30634.8,
change: 3.4,
allocation: 21.3,
avgCost: 520.4,
},
{
symbol: "TSLA",
name: "Tesla, Inc.",
shares: 40,
price: 175.9,
value: 7036.0,
change: -1.2,
allocation: 4.9,
avgCost: 190.75,
},
],
});
const [activeTab, setActiveTab] = useState<"holdings" | "performance">(
"holdings",
);
const [sortConfig, setSortConfig] = useState<{
key: string;
direction: "asc" | "desc";
}>({
key: "allocation",
direction: "desc",
});
const [selectedHolding, setSelectedHolding] = useState<string | null>(null);
const sortedHoldings = [...portfolio.holdings].sort((a, b) => {
if (
a[sortConfig.key as keyof typeof a] < b[sortConfig.key as keyof typeof b]
) {
return sortConfig.direction === "asc" ? -1 : 1;
}
if (
a[sortConfig.key as keyof typeof a] > b[sortConfig.key as keyof typeof b]
) {
return sortConfig.direction === "asc" ? 1 : -1;
}
return 0;
});
const requestSort = (key: string) => {
let direction: "asc" | "desc" = "asc";
if (sortConfig.key === key && sortConfig.direction === "asc") {
direction = "desc";
}
setSortConfig({ key, direction });
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(value);
};
const formatPercent = (value: number) => {
return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
};
// Faux chart data for selected holding
const generateChartData = (symbol: string) => {
const data = [];
const basePrice =
portfolio.holdings.find((h) => h.symbol === symbol)?.price || 100;
for (let i = 0; i < 30; i++) {
const date = new Date();
date.setDate(date.getDate() - 30 + i);
const randomFactor = (Math.sin(i / 5) + Math.random() - 0.5) * 0.05;
const price = basePrice * (1 + randomFactor * (i / 3));
data.push({
date: date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
}),
price: parseFloat(price.toFixed(2)),
});
}
return data;
};
// Calculate total value and percent change for display
const totalValue = portfolio.totalValue;
const totalChange = portfolio.holdings.reduce(
(acc, curr) => acc + (curr.price - curr.avgCost) * curr.shares,
0,
);
const totalPercentChange =
(totalChange / (portfolio.totalValue - totalChange)) * 100;
const selectedStock = selectedHolding
? portfolio.holdings.find((h) => h.symbol === selectedHolding)
: null;
const chartData = selectedHolding ? generateChartData(selectedHolding) : [];
return ( return (
<div className="flex flex-col gap-2 border border-solid border-slate-500 p-4 rounded-md"> <div className="w-full max-w-4xl bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
Portfolio View <div className="bg-gradient-to-r from-indigo-700 to-indigo-500 px-6 py-4">
<div className="flex justify-between items-center">
<h2 className="text-white font-bold text-xl tracking-tight flex items-center">
<svg
className="w-6 h-6 mr-2"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path>
</svg>
Portfolio Summary
</h2>
<div className="bg-indigo-800/50 text-white px-3 py-1 rounded-md text-sm backdrop-blur-sm border border-indigo-400/30 flex items-center">
<svg
className="w-3 h-3 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
clipRule="evenodd"
></path>
</svg>
Updated: {new Date().toLocaleString()}
</div>
</div>
</div>
<div className="p-6 bg-gradient-to-b from-indigo-50 to-white">
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div className="flex justify-between">
<p className="text-gray-500 text-sm font-medium">Total Value</p>
<svg
className="w-5 h-5 text-indigo-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z"
clipRule="evenodd"
></path>
</svg>
</div>
<p className="text-2xl font-bold text-gray-900 mt-1">
{formatCurrency(portfolio.totalValue)}
</p>
<p
className={`text-xs mt-1 flex items-center ${totalPercentChange >= 0 ? "text-green-600" : "text-red-600"}`}
>
{totalPercentChange >= 0 ? (
<svg
className="w-3 h-3 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="w-3 h-3 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
clipRule="evenodd"
></path>
</svg>
)}
{formatPercent(totalPercentChange)} All Time
</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div className="flex justify-between">
<p className="text-gray-500 text-sm font-medium">Cash Balance</p>
<svg
className="w-5 h-5 text-indigo-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M8.433 7.418c.155-.103.346-.196.567-.267v1.698a2.305 2.305 0 01-.567-.267C8.07 8.34 8 8.114 8 8c0-.114.07-.34.433-.582zM11 12.849v-1.698c.22.071.412.164.567.267.364.243.433.468.433.582 0 .114-.07.34-.433.582a2.305 2.305 0 01-.567.267z"></path>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a1 1 0 10-2 0v.092a4.535 4.535 0 00-1.676.662C6.602 6.234 6 7.009 6 8c0 .99.602 1.765 1.324 2.246.48.32 1.054.545 1.676.662v1.941c-.391-.127-.68-.317-.843-.504a1 1 0 10-1.51 1.31c.562.649 1.413 1.076 2.353 1.253V15a1 1 0 102 0v-.092a4.535 4.535 0 001.676-.662C13.398 13.766 14 12.991 14 12c0-.99-.602-1.765-1.324-2.246A4.535 4.535 0 0011 9.092V7.151c.391.127.68.317.843.504a1 1 0 101.511-1.31c-.563-.649-1.413-1.076-2.354-1.253V5z"
clipRule="evenodd"
></path>
</svg>
</div>
<p className="text-2xl font-bold text-gray-900 mt-1">
{formatCurrency(portfolio.cashBalance)}
</p>
<p className="text-xs mt-1 text-gray-500">
{((portfolio.cashBalance / portfolio.totalValue) * 100).toFixed(
1,
)}
% of portfolio
</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div className="flex justify-between">
<p className="text-gray-500 text-sm font-medium">Daily Change</p>
<svg
className="w-5 h-5 text-indigo-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
clipRule="evenodd"
></path>
</svg>
</div>
<p
className={`text-2xl font-bold mt-1 ${portfolio.performance.daily >= 0 ? "text-green-600" : "text-red-600"}`}
>
{formatPercent(portfolio.performance.daily)}
</p>
<p
className={`text-xs mt-1 ${portfolio.performance.daily >= 0 ? "text-green-600" : "text-red-600"}`}
>
{formatCurrency(
(portfolio.totalValue * portfolio.performance.daily) / 100,
)}
</p>
</div>
</div>
<div className="border-b border-gray-200 mb-4">
<div className="flex space-x-4">
<button
onClick={() => {
setActiveTab("holdings");
setSelectedHolding(null);
}}
className={`px-4 py-2 font-medium text-sm focus:outline-none ${
activeTab === "holdings"
? "text-indigo-600 border-b-2 border-indigo-600 font-semibold"
: "text-gray-500 hover:text-gray-700"
}`}
>
Holdings
</button>
<button
onClick={() => {
setActiveTab("performance");
setSelectedHolding(null);
}}
className={`px-4 py-2 font-medium text-sm focus:outline-none ${
activeTab === "performance"
? "text-indigo-600 border-b-2 border-indigo-600 font-semibold"
: "text-gray-500 hover:text-gray-700"
}`}
>
Performance
</button>
</div>
</div>
{activeTab === "holdings" && !selectedHolding && (
<div className="overflow-x-auto rounded-lg border border-gray-200 shadow-sm">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
onClick={() => requestSort("symbol")}
className="group px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center">
<span>Symbol</span>
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
{sortConfig.key === "symbol"
? sortConfig.direction === "asc"
? "↑"
: "↓"
: "↕"}
</span>
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Company
</th>
<th
onClick={() => requestSort("shares")}
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center justify-end">
<span>Shares</span>
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
{sortConfig.key === "shares"
? sortConfig.direction === "asc"
? "↑"
: "↓"
: "↕"}
</span>
</div>
</th>
<th
onClick={() => requestSort("price")}
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center justify-end">
<span>Price</span>
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
{sortConfig.key === "price"
? sortConfig.direction === "asc"
? "↑"
: "↓"
: "↕"}
</span>
</div>
</th>
<th
onClick={() => requestSort("change")}
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center justify-end">
<span>Change</span>
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
{sortConfig.key === "change"
? sortConfig.direction === "asc"
? "↑"
: "↓"
: "↕"}
</span>
</div>
</th>
<th
onClick={() => requestSort("value")}
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center justify-end">
<span>Value</span>
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
{sortConfig.key === "value"
? sortConfig.direction === "asc"
? "↑"
: "↓"
: "↕"}
</span>
</div>
</th>
<th
onClick={() => requestSort("allocation")}
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center justify-end">
<span>Allocation</span>
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
{sortConfig.key === "allocation"
? sortConfig.direction === "asc"
? "↑"
: "↓"
: "↕"}
</span>
</div>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedHoldings.map((holding) => (
<tr
key={holding.symbol}
className="hover:bg-indigo-50 cursor-pointer transition-colors"
onClick={() => setSelectedHolding(holding.symbol)}
>
<td className="px-4 py-4 text-sm font-medium text-indigo-600">
{holding.symbol}
</td>
<td className="px-4 py-4 text-sm text-gray-900">
{holding.name}
</td>
<td className="px-4 py-4 text-sm text-gray-900 text-right">
{holding.shares.toLocaleString()}
</td>
<td className="px-4 py-4 text-sm text-gray-900 text-right">
{formatCurrency(holding.price)}
</td>
<td
className={`px-4 py-4 text-sm text-right font-medium flex items-center justify-end ${holding.change >= 0 ? "text-green-600" : "text-red-600"}`}
>
{holding.change >= 0 ? (
<svg
className="w-3 h-3 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="w-3 h-3 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
clipRule="evenodd"
></path>
</svg>
)}
{formatPercent(holding.change)}
</td>
<td className="px-4 py-4 text-sm text-gray-900 text-right font-medium">
{formatCurrency(holding.value)}
</td>
<td className="px-4 py-4 text-right">
<div className="flex items-center justify-end">
<div className="w-16 bg-gray-200 h-2 rounded-full overflow-hidden mr-2">
<div
className={`h-2 ${holding.change >= 0 ? "bg-green-500" : "bg-red-500"}`}
style={{
width: `${Math.min(100, holding.allocation * 3)}%`,
}}
></div>
</div>
<span className="text-sm text-gray-900">
{holding.allocation.toFixed(1)}%
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === "holdings" && selectedHolding && selectedStock && (
<div className="rounded-lg border border-gray-200 shadow-sm bg-white">
<div className="p-4 flex justify-between items-start">
<div>
<div className="flex items-center">
<h3 className="text-xl font-bold text-gray-900">
{selectedStock.symbol}
</h3>
<span className="ml-2 text-gray-600">
{selectedStock.name}
</span>
</div>
<div className="flex items-center mt-1">
<span className="text-2xl font-bold text-gray-900">
{formatCurrency(selectedStock.price)}
</span>
<span
className={`ml-2 text-sm font-medium ${selectedStock.change >= 0 ? "text-green-600" : "text-red-600"}`}
>
{selectedStock.change >= 0 ? "▲" : "▼"}{" "}
{formatPercent(selectedStock.change)}
</span>
</div>
</div>
<button
onClick={() => setSelectedHolding(null)}
className="bg-gray-100 hover:bg-gray-200 p-1 rounded-md"
>
<svg
className="w-5 h-5 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<div className="border-t border-gray-200 p-4">
<div className="h-40 bg-white">
<div className="flex items-end h-full space-x-1">
{chartData.map((point, index) => {
const maxPrice = Math.max(...chartData.map((d) => d.price));
const minPrice = Math.min(...chartData.map((d) => d.price));
const range = maxPrice - minPrice;
const heightPercent =
range === 0
? 50
: ((point.price - minPrice) / range) * 80 + 10;
return (
<div
key={index}
className="flex flex-col items-center flex-1"
>
<div
className={`w-full rounded-sm ${point.price >= chartData[Math.max(0, index - 1)].price ? "bg-green-500" : "bg-red-500"}`}
style={{ height: `${heightPercent}%` }}
></div>
{index % 5 === 0 && (
<span className="text-xs text-gray-500 mt-1">
{point.date}
</span>
)}
</div>
);
})}
</div>
</div>
</div>
<div className="border-t border-gray-200 p-4">
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-xs text-gray-500">Shares Owned</p>
<p className="text-sm font-medium">
{selectedStock.shares.toLocaleString()}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Market Value</p>
<p className="text-sm font-medium">
{formatCurrency(selectedStock.value)}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Avg. Cost</p>
<p className="text-sm font-medium">
{formatCurrency(selectedStock.avgCost)}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Cost Basis</p>
<p className="text-sm font-medium">
{formatCurrency(
selectedStock.avgCost * selectedStock.shares,
)}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Gain/Loss</p>
<p
className={`text-sm font-medium ${selectedStock.price - selectedStock.avgCost >= 0 ? "text-green-600" : "text-red-600"}`}
>
{formatCurrency(
(selectedStock.price - selectedStock.avgCost) *
selectedStock.shares,
)}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Allocation</p>
<p className="text-sm font-medium">
{selectedStock.allocation.toFixed(2)}%
</p>
</div>
</div>
</div>
<div className="border-t border-gray-200 p-4 flex space-x-2">
<button className="flex-1 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition-colors text-sm">
Buy More
</button>
<button className="flex-1 bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-md transition-colors text-sm">
Sell
</button>
<button className="flex items-center justify-center w-10 h-10 border border-gray-300 rounded-md hover:bg-gray-100 transition-colors">
<svg
className="w-5 h-5 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</button>
</div>
</div>
)}
{activeTab === "performance" && (
<div className="space-y-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
className="w-5 h-5 mr-2 text-indigo-500"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"></path>
</svg>
Performance Overview
</h3>
<div className="grid grid-cols-4 gap-4">
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-gray-500 text-sm font-medium">Daily</p>
<p
className={`text-lg font-bold flex items-center ${portfolio.performance.daily >= 0 ? "text-green-600" : "text-red-600"}`}
>
{portfolio.performance.daily >= 0 ? (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
clipRule="evenodd"
></path>
</svg>
)}
{formatPercent(portfolio.performance.daily)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-gray-500 text-sm font-medium">Weekly</p>
<p
className={`text-lg font-bold flex items-center ${portfolio.performance.weekly >= 0 ? "text-green-600" : "text-red-600"}`}
>
{portfolio.performance.weekly >= 0 ? (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
clipRule="evenodd"
></path>
</svg>
)}
{formatPercent(portfolio.performance.weekly)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-gray-500 text-sm font-medium">Monthly</p>
<p
className={`text-lg font-bold flex items-center ${portfolio.performance.monthly >= 0 ? "text-green-600" : "text-red-600"}`}
>
{portfolio.performance.monthly >= 0 ? (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
clipRule="evenodd"
></path>
</svg>
)}
{formatPercent(portfolio.performance.monthly)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-gray-500 text-sm font-medium">Yearly</p>
<p
className={`text-lg font-bold flex items-center ${portfolio.performance.yearly >= 0 ? "text-green-600" : "text-red-600"}`}
>
{portfolio.performance.yearly >= 0 ? (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
clipRule="evenodd"
></path>
</svg>
)}
{formatPercent(portfolio.performance.yearly)}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
className="w-5 h-5 mr-2 text-indigo-500"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path>
</svg>
Portfolio Allocation
</h3>
<div className="space-y-3">
{sortedHoldings.map((holding) => (
<div
key={holding.symbol}
className="flex items-center group hover:bg-indigo-50 p-2 rounded-lg transition-colors"
>
<div className="w-24 text-sm font-medium text-indigo-600 flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${holding.change >= 0 ? "bg-green-500" : "bg-red-500"}`}
></div>
{holding.symbol}
</div>
<div className="flex-grow">
<div className="bg-gray-200 h-4 rounded-full overflow-hidden shadow-inner">
<div
className="h-4 bg-gradient-to-r from-indigo-500 to-indigo-600"
style={{ width: `${holding.allocation}%` }}
></div>
</div>
</div>
<div className="w-16 text-sm font-medium text-gray-900 text-right ml-3">
{holding.allocation.toFixed(1)}%
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity ml-2">
<button className="p-1 text-gray-400 hover:text-indigo-600">
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
</div>
))}
</div>
<div className="mt-6 bg-gray-50 p-3 rounded-lg">
<h4 className="text-sm font-medium text-gray-700 mb-2">
Portfolio Diversification
</h4>
<div className="flex h-4 rounded-full overflow-hidden">
{[
"Technology",
"Consumer Cyclical",
"Communication Services",
"Financial",
"Other",
].map((sector, index) => {
const widths = [42, 23, 18, 10, 7]; // example percentages
const colors = [
"bg-indigo-600",
"bg-blue-500",
"bg-green-500",
"bg-yellow-500",
"bg-red-500",
];
return (
<div
key={sector}
className={`${colors[index]} h-full`}
style={{ width: `${widths[index]}%` }}
title={`${sector}: ${widths[index]}%`}
></div>
);
})}
</div>
<div className="flex flex-wrap mt-2 text-xs">
{[
"Technology",
"Consumer Cyclical",
"Communication Services",
"Financial",
"Other",
].map((sector, index) => {
const widths = [42, 23, 18, 10, 7]; // example percentages
const colors = [
"text-indigo-600",
"text-blue-500",
"text-green-500",
"text-yellow-500",
"text-red-500",
];
return (
<div key={sector} className="mr-3 flex items-center">
<div
className={`w-2 h-2 rounded-full ${colors[index].replace("text", "bg")} mr-1`}
></div>
<span className={`${colors[index]} font-medium`}>
{sector} {widths[index]}%
</span>
</div>
);
})}
</div>
</div>
</div>
<div className="flex justify-end space-x-2">
<button className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center">
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
Export Data
</button>
<button className="px-4 py-2 bg-indigo-600 border border-indigo-600 rounded-md text-sm font-medium text-white hover:bg-indigo-700 shadow-sm flex items-center">
<svg
className="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
></path>
</svg>
View Full Report
</button>
</div>
</div>
)}
</div>
</div> </div>
); );
} }

View File

@@ -1,13 +1,54 @@
import "./index.css";
import { useStream } from "@langchain/langgraph-sdk/react"; import { useStream } from "@langchain/langgraph-sdk/react";
import type { AIMessage, Message } from "@langchain/langgraph-sdk"; import type { AIMessage, Message } from "@langchain/langgraph-sdk";
import { useState } from "react"; import { useState, useEffect, useCallback } from "react";
export default function StockPrice(props: { export default function StockPrice(props: {
instruction: string; instruction: string;
logo: string; logo: string;
}) { }) {
const [counter, setCounter] = useState(0); const [quantity, setQuantity] = useState(1);
const [stockData, setStockData] = useState({
symbol: "AAPL",
name: "Apple Inc.",
price: 187.32,
change: 1.24,
changePercent: 0.67,
previousClose: 186.08,
open: 186.75,
dayHigh: 188.45,
dayLow: 186.2,
volume: 54320000,
marketCap: "2.92T",
peRatio: 29.13,
dividend: 0.96,
dividendYield: 0.51,
moving50Day: 182.46,
moving200Day: 175.8,
fiftyTwoWeekHigh: 201.48,
fiftyTwoWeekLow: 143.9,
analystRating: "Buy",
analystCount: 32,
priceTarget: 210.5,
});
const [priceHistory, setPriceHistory] = useState<
{ time: string; price: number }[]
>([]);
const [orderType, setOrderType] = useState<"buy" | "sell">("buy");
const [showOrder, setShowOrder] = useState(false);
const [activeTab, setActiveTab] = useState<"chart" | "details" | "analysis">(
"chart",
);
const [isLiveUpdating, setIsLiveUpdating] = useState(false);
const [orderTypeOptions] = useState([
{ value: "market", label: "Market" },
{ value: "limit", label: "Limit" },
{ value: "stop", label: "Stop" },
]);
const [selectedOrderTypeOption, setSelectedOrderTypeOption] =
useState("market");
const [limitPrice, setLimitPrice] = useState(stockData.price.toFixed(2));
const [showOrderSuccess, setShowOrderSuccess] = useState(false);
// useStream should be able to be infered from context // useStream should be able to be infered from context
const thread = useStream<{ messages: Message[] }>({ const thread = useStream<{ messages: Message[] }>({
@@ -27,33 +68,889 @@ export default function StockPrice(props: {
const toolCallId = aiTool?.tool_calls?.[0]?.id; const toolCallId = aiTool?.tool_calls?.[0]?.id;
return ( // Simulated price history generation on component mount
<div className="flex flex-col gap-2 border border-solid border-slate-500 p-4 rounded-md"> useEffect(() => {
Request: {props.instruction} generatePriceHistory();
<button className="text-left" onClick={() => setCounter(counter + 1)}> }, []);
Click me
</button> const generatePriceHistory = () => {
<p>Counter: {counter}</p> const now = new Date();
{toolCallId && ( const history = [];
<button
className="text-left" for (let i = 30; i >= 0; i--) {
onClick={() => { const time = new Date(now.getTime() - i * 15 * 60000); // 15-minute intervals
thread.submit({ const basePrice = 187.32;
messages: [ // Make the price movement more interesting with some trends
{ const trend = Math.sin(i / 5) * 1.5;
type: "tool", const randomFactor = (Math.random() - 0.5) * 1.5;
tool_call_id: toolCallId!, const price = basePrice + trend + randomFactor;
name: "stockbroker",
content: "hey", history.push({
}, time: time.toLocaleTimeString([], {
{ type: "human", content: `Buy ${counter}` }, hour: "2-digit",
], minute: "2-digit",
}),
price: parseFloat(price.toFixed(2)),
});
}
setPriceHistory(history);
};
// Simulate live price updates
useEffect(() => {
let interval: NodeJS.Timeout;
if (isLiveUpdating) {
interval = setInterval(() => {
setStockData((prev) => {
// Random small price movement
const priceChange = (Math.random() - 0.5) * 0.3;
const newPrice = parseFloat((prev.price + priceChange).toFixed(2));
// Update price history
setPriceHistory((history) => {
const now = new Date();
const newHistory = [...history];
if (newHistory.length > 30) {
newHistory.shift(); // Remove oldest entry to keep array length consistent
}
newHistory.push({
time: now.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
price: newPrice,
}); });
}} return newHistory;
> });
Buy
</button> return {
)} ...prev,
price: newPrice,
change: parseFloat((newPrice - prev.previousClose).toFixed(2)),
changePercent: parseFloat(
(
((newPrice - prev.previousClose) / prev.previousClose) *
100
).toFixed(2),
),
dayHigh: Math.max(prev.dayHigh, newPrice),
dayLow: Math.min(prev.dayLow, newPrice),
};
});
}, 3000);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isLiveUpdating]);
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value);
if (value > 0 && value <= 1000) {
setQuantity(value);
}
};
const handleLimitPriceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (/^\d*\.?\d*$/.test(value)) {
setLimitPrice(value);
}
};
const handleOrderTypeChange = (type: "buy" | "sell") => {
setOrderType(type);
};
const toggleLiveUpdates = () => {
setIsLiveUpdating((prev) => !prev);
};
const handleOrder = () => {
// Submit the order through the thread
if (toolCallId) {
const orderDetails = {
action: orderType,
quantity,
symbol: stockData.symbol,
orderType: selectedOrderTypeOption,
...(selectedOrderTypeOption !== "market" && {
limitPrice: parseFloat(limitPrice),
}),
};
thread.submit({
messages: [
{
type: "tool",
tool_call_id: toolCallId,
name: "stockbroker",
content: JSON.stringify(orderDetails),
},
{
type: "human",
content: `${orderType === "buy" ? "Bought" : "Sold"} ${quantity} shares of ${stockData.symbol} at ${
selectedOrderTypeOption === "market"
? formatCurrency(stockData.price)
: formatCurrency(parseFloat(limitPrice))
}`,
},
],
});
setShowOrderSuccess(true);
setTimeout(() => {
setShowOrderSuccess(false);
setShowOrder(false);
}, 2000);
}
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(value);
};
const formatPercent = (value: number) => {
return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
};
const getMinMax = () => {
if (priceHistory.length === 0) return { min: 0, max: 0 };
const prices = priceHistory.map((item) => item.price);
return {
min: Math.min(...prices),
max: Math.max(...prices),
};
};
// Generate a simple price chart with the last price as endpoint
const generateChartPath = useCallback(() => {
if (priceHistory.length === 0) return "";
const { min, max } = getMinMax();
const range = max - min || 1;
const width = 100; // % of container width
const height = 100; // % of container height
const points = priceHistory.map((point, i) => {
const x = (i / (priceHistory.length - 1)) * width;
const y = height - ((point.price - min) / range) * height;
return `${x},${y}`;
});
return `M ${points.join(" L ")}`;
}, [priceHistory]);
const { min, max } = getMinMax();
const range = max - min || 1;
const chartPath = generateChartPath();
return (
<div className="w-full max-w-md bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
<div
className={`px-4 py-3 ${stockData.change >= 0 ? "bg-gradient-to-r from-green-600 to-green-500" : "bg-gradient-to-r from-red-600 to-red-500"}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center">
{props.logo && (
<img
src={props.logo}
alt="Logo"
className="h-6 w-6 mr-2 rounded-full bg-white p-0.5"
/>
)}
<h3 className="text-white font-bold">
{stockData.symbol}{" "}
<span className="font-normal text-white/80 text-sm">
{stockData.name}
</span>
</h3>
</div>
<div className="flex items-center">
<span className="text-white font-bold text-xl mr-2">
{formatCurrency(stockData.price)}
</span>
<div
className={`flex items-center px-2 py-1 rounded text-xs font-medium text-white ${stockData.change >= 0 ? "bg-green-700/50" : "bg-red-700/50"} backdrop-blur-sm border ${stockData.change >= 0 ? "border-green-400/30" : "border-red-400/30"}`}
>
{stockData.change >= 0 ? "▲" : "▼"}{" "}
{formatCurrency(Math.abs(stockData.change))} (
{formatPercent(stockData.changePercent)})
</div>
</div>
</div>
<div className="flex justify-between items-center text-xs text-white/80 mt-1">
<span className="italic">{props.instruction}</span>
<button
onClick={toggleLiveUpdates}
className={`flex items-center text-xs px-2 py-0.5 rounded ${isLiveUpdating ? "bg-white/30 text-white" : "bg-white/10 text-white/70"} hover:bg-white/20 transition-colors`}
>
<span
className={`inline-block w-2 h-2 rounded-full mr-1 ${isLiveUpdating ? "bg-green-400 animate-pulse" : "bg-gray-400"}`}
></span>
{isLiveUpdating ? "Live" : "Static"}
</button>
</div>
</div>
<div className="border-b border-gray-200">
<div className="flex">
<button
onClick={() => setActiveTab("chart")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "chart"
? "text-indigo-600 border-b-2 border-indigo-600"
: "text-gray-500 hover:text-gray-700"
}`}
>
Chart
</button>
<button
onClick={() => setActiveTab("details")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "details"
? "text-indigo-600 border-b-2 border-indigo-600"
: "text-gray-500 hover:text-gray-700"
}`}
>
Details
</button>
<button
onClick={() => setActiveTab("analysis")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "analysis"
? "text-indigo-600 border-b-2 border-indigo-600"
: "text-gray-500 hover:text-gray-700"
}`}
>
Analysis
</button>
</div>
</div>
<div className="p-4">
{activeTab === "chart" && (
<>
<div className="mb-4">
<div className="border rounded-lg p-3 bg-white shadow-sm">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span className="font-medium">Price Chart</span>
<div className="flex space-x-1">
<button className="px-1.5 py-0.5 bg-indigo-100 text-indigo-700 rounded text-xs font-medium">
1D
</button>
<button className="px-1.5 py-0.5 hover:bg-gray-100 rounded text-xs text-gray-600">
1W
</button>
<button className="px-1.5 py-0.5 hover:bg-gray-100 rounded text-xs text-gray-600">
1M
</button>
<button className="px-1.5 py-0.5 hover:bg-gray-100 rounded text-xs text-gray-600">
1Y
</button>
</div>
</div>
<div className="relative h-40 mt-2">
<div className="absolute inset-0">
{/* Chart grid lines */}
<div className="h-full flex flex-col justify-between">
{[0, 1, 2, 3].map((line) => (
<div
key={line}
className="border-t border-gray-100 w-full h-0"
></div>
))}
</div>
{/* SVG Line Chart */}
<svg
className="absolute inset-0 w-full h-full"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
{/* Chart path */}
<path
d={chartPath}
stroke={stockData.change >= 0 ? "#10b981" : "#ef4444"}
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
/>
{/* Gradient area under the chart */}
<path
d={`${chartPath} L 100,100 L 0,100 Z`}
fill={
stockData.change >= 0
? "url(#greenGradient)"
: "url(#redGradient)"
}
fillOpacity="0.2"
/>
{/* Gradient definitions */}
<defs>
<linearGradient
id="greenGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop
offset="0%"
stopColor="#10b981"
stopOpacity="0.8"
/>
<stop
offset="100%"
stopColor="#10b981"
stopOpacity="0"
/>
</linearGradient>
<linearGradient
id="redGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop
offset="0%"
stopColor="#ef4444"
stopOpacity="0.8"
/>
<stop
offset="100%"
stopColor="#ef4444"
stopOpacity="0"
/>
</linearGradient>
</defs>
</svg>
{/* Price labels on Y-axis */}
<div className="absolute right-0 inset-y-0 flex flex-col justify-between text-right pr-1 text-xs text-gray-400 pointer-events-none">
<span>{formatCurrency(max)}</span>
<span>{formatCurrency(min + range * 0.66)}</span>
<span>{formatCurrency(min + range * 0.33)}</span>
<span>{formatCurrency(min)}</span>
</div>
</div>
</div>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<div className="flex items-center">
<span className="inline-block w-2 h-2 bg-gray-300 rounded-full mr-1"></span>
{priceHistory[0]?.time || "9:30 AM"}
</div>
<div className="flex items-center">
<span>Vol: {stockData.volume.toLocaleString()}</span>
</div>
<div className="flex items-center">
<span className="inline-block w-2 h-2 bg-gray-300 rounded-full mr-1"></span>
{priceHistory[priceHistory.length - 1]?.time || "4:00 PM"}
</div>
</div>
</div>
</div>
{/* Key Stock Information */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-gray-50 rounded-lg p-2.5 shadow-sm border border-gray-100">
<p className="text-xs text-gray-500 font-medium">Day Range</p>
<div className="relative pt-1">
<div className="overflow-hidden h-1.5 mb-1 text-xs flex rounded bg-gray-200 mt-1">
<div
className={`shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center ${stockData.change >= 0 ? "bg-green-500" : "bg-red-500"}`}
style={{
width: `${((stockData.price - stockData.dayLow) / (stockData.dayHigh - stockData.dayLow)) * 100}%`,
}}
></div>
</div>
<div className="flex justify-between text-xs text-gray-600">
<span>{formatCurrency(stockData.dayLow)}</span>
<span>{formatCurrency(stockData.dayHigh)}</span>
</div>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-2.5 shadow-sm border border-gray-100">
<p className="text-xs text-gray-500 font-medium">
52-Week Range
</p>
<div className="relative pt-1">
<div className="overflow-hidden h-1.5 mb-1 text-xs flex rounded bg-gray-200 mt-1">
<div
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-indigo-500"
style={{
width: `${((stockData.price - stockData.fiftyTwoWeekLow) / (stockData.fiftyTwoWeekHigh - stockData.fiftyTwoWeekLow)) * 100}%`,
}}
></div>
</div>
<div className="flex justify-between text-xs text-gray-600">
<span>{formatCurrency(stockData.fiftyTwoWeekLow)}</span>
<span>{formatCurrency(stockData.fiftyTwoWeekHigh)}</span>
</div>
</div>
</div>
</div>
</>
)}
{activeTab === "details" && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">Open</p>
<p className="font-medium">{formatCurrency(stockData.open)}</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">
Previous Close
</p>
<p className="font-medium">
{formatCurrency(stockData.previousClose)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">
Market Cap
</p>
<p className="font-medium">{stockData.marketCap}</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">Volume</p>
<p className="font-medium">
{stockData.volume.toLocaleString()}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">
P/E Ratio
</p>
<p className="font-medium">{stockData.peRatio}</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">
Dividend Yield
</p>
<p className="font-medium">{stockData.dividendYield}%</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">
50-Day Avg
</p>
<p className="font-medium">
{formatCurrency(stockData.moving50Day)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
<p className="text-xs text-gray-500 font-medium mb-1">
200-Day Avg
</p>
<p className="font-medium">
{formatCurrency(stockData.moving200Day)}
</p>
</div>
</div>
<div className="bg-indigo-50 rounded-lg p-4 border border-indigo-100">
<h4 className="font-medium text-indigo-900 text-sm mb-2">
Company Overview
</h4>
<p className="text-xs text-indigo-700">
{stockData.name} is a leading technology company that designs,
manufactures, and markets consumer electronics, computer
software, and online services. The company has a strong global
presence and is known for its innovation in the industry.
</p>
</div>
</div>
)}
{activeTab === "analysis" && (
<div className="space-y-4">
<div className="bg-white rounded-lg p-3 border border-gray-200 shadow-sm">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-900">Analyst Consensus</h4>
<div
className={`px-2 py-0.5 rounded text-xs font-medium ${
stockData.analystRating === "Buy"
? "bg-green-100 text-green-800"
: stockData.analystRating === "Hold"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{stockData.analystRating}
</div>
</div>
<div className="flex mb-1">
<div className="w-20 text-xs text-gray-500">Strong Buy</div>
<div className="flex-grow">
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
<div
className="h-4 bg-green-600"
style={{ width: "65%" }}
></div>
</div>
</div>
<div className="w-8 text-xs text-right text-gray-600">65%</div>
</div>
<div className="flex mb-1">
<div className="w-20 text-xs text-gray-500">Buy</div>
<div className="flex-grow">
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
<div
className="h-4 bg-green-500"
style={{ width: "20%" }}
></div>
</div>
</div>
<div className="w-8 text-xs text-right text-gray-600">20%</div>
</div>
<div className="flex mb-1">
<div className="w-20 text-xs text-gray-500">Hold</div>
<div className="flex-grow">
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
<div
className="h-4 bg-yellow-500"
style={{ width: "10%" }}
></div>
</div>
</div>
<div className="w-8 text-xs text-right text-gray-600">10%</div>
</div>
<div className="flex mb-1">
<div className="w-20 text-xs text-gray-500">Sell</div>
<div className="flex-grow">
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
<div
className="h-4 bg-red-500"
style={{ width: "5%" }}
></div>
</div>
</div>
<div className="w-8 text-xs text-right text-gray-600">5%</div>
</div>
<div className="mt-2 text-xs text-gray-500">
Based on {stockData.analystCount} analyst ratings
</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200 shadow-sm">
<div className="flex justify-between items-center mb-3">
<h4 className="font-medium text-gray-900">Price Target</h4>
<span className="text-sm font-bold text-indigo-600">
{formatCurrency(stockData.priceTarget)}
</span>
</div>
<div className="relative pt-1">
<div className="flex mb-2 items-center justify-between">
<div className="text-xs text-gray-500">
<span className="text-green-600 font-medium">
+
{(
(stockData.priceTarget / stockData.price - 1) *
100
).toFixed(2)}
%
</span>{" "}
Upside
</div>
</div>
<div className="overflow-hidden h-1.5 mb-1 text-xs flex rounded bg-gray-200">
<div
style={{
width: `${(stockData.price / stockData.priceTarget) * 100}%`,
}}
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-indigo-600"
></div>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>Current: {formatCurrency(stockData.price)}</span>
<span>Target: {formatCurrency(stockData.priceTarget)}</span>
</div>
</div>
</div>
<div className="bg-indigo-50 rounded-lg p-3 border border-indigo-100">
<h4 className="font-medium text-indigo-900 mb-2">Recent News</h4>
<div className="space-y-2">
<div className="text-xs text-indigo-800">
<p className="font-medium">
{stockData.name} Reports Strong Quarterly Earnings
</p>
<p className="text-indigo-600 text-xs">2 days ago</p>
</div>
<div className="text-xs text-indigo-800">
<p className="font-medium">
New Product Launch Expected Next Month
</p>
<p className="text-indigo-600 text-xs">5 days ago</p>
</div>
</div>
</div>
</div>
)}
{/* Order Interface */}
{!showOrder && !showOrderSuccess ? (
<div className="flex space-x-2 mt-4">
<button
onClick={() => {
setShowOrder(true);
setOrderType("buy");
}}
className="flex-1 bg-gradient-to-r from-green-600 to-green-500 hover:from-green-700 hover:to-green-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm transition-all flex items-center justify-center"
>
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
></path>
</svg>
Buy
</button>
<button
onClick={() => {
setShowOrder(true);
setOrderType("sell");
}}
className="flex-1 bg-gradient-to-r from-red-600 to-red-500 hover:from-red-700 hover:to-red-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm transition-all flex items-center justify-center"
>
<svg
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
clipRule="evenodd"
></path>
</svg>
Sell
</button>
</div>
) : showOrderSuccess ? (
<div className="mt-4 flex items-center justify-center bg-green-50 rounded-lg p-4 border border-green-200 text-green-800">
<svg
className="w-5 h-5 mr-2 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
></path>
</svg>
<span className="font-medium">Order submitted successfully!</span>
</div>
) : (
<div className="mt-4 bg-white rounded-lg border border-gray-200 shadow-md p-4">
<div className="flex justify-between items-center mb-3">
<h4
className={`font-bold text-lg ${orderType === "buy" ? "text-green-600" : "text-red-600"}`}
>
{orderType === "buy" ? "Buy" : "Sell"} {stockData.symbol}
</h4>
<button
onClick={() => setShowOrder(false)}
className="text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full p-1 transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<div className="mb-3">
<div className="flex space-x-1 mb-3">
{orderTypeOptions.map((option) => (
<button
key={option.value}
onClick={() => setSelectedOrderTypeOption(option.value)}
className={`flex-1 px-2 py-1.5 text-xs font-medium rounded-md transition-colors ${
selectedOrderTypeOption === option.value
? `${orderType === "buy" ? "bg-green-100 text-green-800 border-green-300" : "bg-red-100 text-red-800 border-red-300"} border`
: "bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200"
}`}
>
{option.label}
</button>
))}
</div>
<div className="flex space-x-2 mb-3">
<div className="flex-1">
<label
htmlFor="quantity"
className="block text-xs font-medium text-gray-700 mb-1"
>
Quantity
</label>
<div className="flex items-center">
<button
onClick={() => quantity > 1 && setQuantity(quantity - 1)}
className="border rounded-l px-2 py-1 text-gray-600 hover:bg-gray-100"
>
-
</button>
<input
type="number"
id="quantity"
min="1"
max="1000"
value={quantity}
onChange={handleQuantityChange}
className="border-t border-b w-full px-3 py-1 text-center focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
<button
onClick={() =>
quantity < 1000 && setQuantity(quantity + 1)
}
className="border rounded-r px-2 py-1 text-gray-600 hover:bg-gray-100"
>
+
</button>
</div>
</div>
{selectedOrderTypeOption !== "market" && (
<div className="flex-1">
<label
htmlFor="limitPrice"
className="block text-xs font-medium text-gray-700 mb-1"
>
{selectedOrderTypeOption === "limit"
? "Limit Price"
: "Stop Price"}
</label>
<div className="flex items-center border rounded overflow-hidden">
<span className="bg-gray-100 px-2 py-1 text-gray-500 text-sm border-r">
$
</span>
<input
type="text"
id="limitPrice"
value={limitPrice}
onChange={handleLimitPriceChange}
className="flex-1 px-3 py-1 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
</div>
)}
</div>
{selectedOrderTypeOption !== "market" && (
<div className="rounded-md bg-indigo-50 p-2 mb-3">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-indigo-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-2">
<p className="text-xs text-indigo-700">
{selectedOrderTypeOption === "limit"
? `Your ${orderType} order will execute only at ${formatCurrency(parseFloat(limitPrice))} or better.`
: `Your ${orderType} order will execute when the price reaches ${formatCurrency(parseFloat(limitPrice))}.`}
</p>
</div>
</div>
</div>
)}
<div className="bg-gray-50 rounded-md p-3 mb-4">
<div className="flex justify-between text-sm py-1">
<span className="text-gray-600">Market Price</span>
<span className="font-medium">
{formatCurrency(stockData.price)}
</span>
</div>
<div className="flex justify-between text-sm py-1">
<span className="text-gray-600">
{orderType === "buy" ? "Cost" : "Credit"} ({quantity}{" "}
{quantity === 1 ? "share" : "shares"})
</span>
<span>{formatCurrency(stockData.price * quantity)}</span>
</div>
<div className="flex justify-between text-sm py-1">
<span className="text-gray-600">Commission</span>
<span>$0.00</span>
</div>
<div className="flex justify-between text-sm py-1 border-t mt-1 pt-1">
<span className="font-medium">Estimated Total</span>
<span className="font-bold">
{formatCurrency(stockData.price * quantity)}
</span>
</div>
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => setShowOrder(false)}
className="flex-1 py-2 px-4 border border-gray-300 rounded-md text-gray-700 font-medium hover:bg-gray-50 text-sm transition-colors"
>
Cancel
</button>
<button
onClick={handleOrder}
disabled={!toolCallId}
className={`flex-1 py-2 px-4 rounded-md font-medium text-white text-sm ${
orderType === "buy"
? "bg-green-600 hover:bg-green-700"
: "bg-red-600 hover:bg-red-700"
} ${!toolCallId ? "opacity-50 cursor-not-allowed" : ""} transition-colors`}
>
Review {orderType === "buy" ? "Purchase" : "Sale"}
</button>
</div>
</div>
)}
</div>
</div> </div>
); );
} }

View File

@@ -1,9 +1,195 @@
import { TripDetails } from "../../../trip-planner/types"; import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function AccommodationsList({ export default function AccommodationsList({
tripDetails, tripDetails,
}: { }: {
tripDetails: TripDetails; tripDetails: TripDetails;
}) { }) {
return <div>Accommodations list for {JSON.stringify(tripDetails)}</div>; console.log("tripDetails", tripDetails);
// Placeholder data - ideally would come from props
const [accommodations] = useState([
{
id: "1",
name: "Grand Hotel",
type: "Hotel",
price: "$150/night",
rating: 4.8,
amenities: ["WiFi", "Pool", "Breakfast"],
image: "https://placehold.co/300x200?text=Hotel",
available: true,
},
{
id: "2",
name: "Cozy Apartment",
type: "Apartment",
price: "$120/night",
rating: 4.5,
amenities: ["WiFi", "Kitchen", "Washing Machine"],
image: "https://placehold.co/300x200?text=Apartment",
available: true,
},
{
id: "3",
name: "Beachside Villa",
type: "Villa",
price: "$300/night",
rating: 4.9,
amenities: ["WiFi", "Private Pool", "Ocean View"],
image: "https://placehold.co/300x200?text=Villa",
available: false,
},
]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedAccommodation = accommodations.find(
(acc) => acc.id === selectedId,
);
return (
<div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
<div className="bg-blue-600 px-4 py-3">
<div className="flex justify-between items-center">
<h3 className="text-white font-medium">
Accommodations in {tripDetails.location}
</h3>
{selectedId && (
<button
onClick={() => setSelectedId(null)}
className="text-white text-sm bg-blue-700 hover:bg-blue-800 px-2 py-1 rounded"
>
Back to list
</button>
)}
</div>
<p className="text-blue-100 text-xs">
{new Date(tripDetails.startDate).toLocaleDateString()} -{" "}
{new Date(tripDetails.endDate).toLocaleDateString()} ·{" "}
{tripDetails.numberOfGuests} guests
</p>
</div>
<div className="p-4">
{!selectedId ? (
<div className="space-y-3">
{accommodations.map((accommodation) => (
<div
key={accommodation.id}
onClick={() => setSelectedId(accommodation.id)}
className={`flex border rounded-lg p-3 cursor-pointer transition-all ${
accommodation.available
? "hover:border-blue-300 hover:shadow-md"
: "opacity-60"
}`}
>
<div className="w-20 h-20 bg-gray-200 rounded-md flex-shrink-0 overflow-hidden">
<img
src={accommodation.image}
alt={accommodation.name}
className="w-full h-full object-cover"
/>
</div>
<div className="ml-3 flex-1">
<div className="flex justify-between">
<h4 className="font-medium text-gray-900">
{accommodation.name}
</h4>
<span className="text-sm font-semibold text-blue-600">
{accommodation.price}
</span>
</div>
<p className="text-sm text-gray-500">{accommodation.type}</p>
<div className="flex items-center mt-1">
<svg
className="w-4 h-4 text-yellow-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
<span className="text-xs text-gray-500 ml-1">
{accommodation.rating}
</span>
</div>
{!accommodation.available && (
<span className="text-xs text-red-500 font-medium">
Unavailable for your dates
</span>
)}
</div>
</div>
))}
</div>
) : (
<div className="space-y-4">
{selectedAccommodation && (
<>
<div className="w-full h-40 bg-gray-200 rounded-lg overflow-hidden">
<img
src={selectedAccommodation.image}
alt={selectedAccommodation.name}
className="w-full h-full object-cover"
/>
</div>
<div>
<div className="flex justify-between items-start">
<h3 className="font-medium text-lg text-gray-900">
{selectedAccommodation.name}
</h3>
<span className="font-semibold text-blue-600">
{selectedAccommodation.price}
</span>
</div>
<div className="flex items-center mt-1">
<svg
className="w-4 h-4 text-yellow-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
<span className="text-sm text-gray-500 ml-1">
{selectedAccommodation.rating}
</span>
</div>
<p className="text-sm text-gray-600 mt-2">
Perfect accommodation in {tripDetails.location} for your{" "}
{tripDetails.numberOfGuests} guests.
</p>
<div className="mt-3">
<h4 className="text-sm font-medium text-gray-700">
Amenities:
</h4>
<div className="flex flex-wrap gap-1 mt-1">
{selectedAccommodation.amenities.map((amenity) => (
<span
key={amenity}
className="text-xs bg-gray-100 px-2 py-1 rounded"
>
{amenity}
</span>
))}
</div>
</div>
<button
className={`w-full mt-4 py-2 rounded-md text-white font-medium ${
selectedAccommodation.available
? "bg-blue-600 hover:bg-blue-700"
: "bg-gray-400 cursor-not-allowed"
}`}
disabled={!selectedAccommodation.available}
>
{selectedAccommodation.available
? "Book Now"
: "Unavailable"}
</button>
</div>
</>
)}
</div>
)}
</div>
</div>
);
} }

View File

@@ -1,4 +1,5 @@
import { TripDetails } from "../../../trip-planner/types"; import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function BookAccommodation({ export default function BookAccommodation({
tripDetails, tripDetails,
@@ -7,9 +8,395 @@ export default function BookAccommodation({
tripDetails: TripDetails; tripDetails: TripDetails;
accommodationName: string; accommodationName: string;
}) { }) {
// Placeholder data - ideally would come from props
const [accommodation] = useState({
name: accommodationName,
type: "Hotel",
price: "$150/night",
rating: 4.8,
totalPrice:
"$" +
150 *
Math.ceil(
(new Date(tripDetails.endDate).getTime() -
new Date(tripDetails.startDate).getTime()) /
(1000 * 60 * 60 * 24),
),
image: "https://placehold.co/300x200?text=Accommodation",
roomTypes: ["Standard", "Deluxe", "Suite"],
checkInTime: "3:00 PM",
checkOutTime: "11:00 AM",
});
const [selectedRoom, setSelectedRoom] = useState("Standard");
const [bookingStep, setBookingStep] = useState<
"details" | "payment" | "confirmed"
>("details");
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
specialRequests: "",
});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setBookingStep("payment");
};
const handlePayment = (e: React.FormEvent) => {
e.preventDefault();
setBookingStep("confirmed");
};
return ( return (
<div> <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
Book accommodation {accommodationName} for {JSON.stringify(tripDetails)} <div className="bg-blue-600 px-4 py-3">
<h3 className="text-white font-medium">Book {accommodation.name}</h3>
<p className="text-blue-100 text-xs">
{new Date(tripDetails.startDate).toLocaleDateString()} -{" "}
{new Date(tripDetails.endDate).toLocaleDateString()} ·{" "}
{tripDetails.numberOfGuests} guests
</p>
</div>
<div className="p-4">
{bookingStep === "details" && (
<>
<div className="flex items-center space-x-3 mb-4">
<div className="flex-shrink-0 w-16 h-16 bg-gray-200 rounded-md overflow-hidden">
<img
src={accommodation.image}
alt={accommodation.name}
className="w-full h-full object-cover"
/>
</div>
<div>
<h4 className="font-medium text-gray-900">
{accommodation.name}
</h4>
<div className="flex items-center mt-1">
<svg
className="w-4 h-4 text-yellow-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
<span className="text-xs text-gray-500 ml-1">
{accommodation.rating}
</span>
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-sm text-gray-500">
{accommodation.type}
</span>
<span className="text-sm font-semibold text-blue-600">
{accommodation.price}
</span>
</div>
</div>
</div>
<div className="border-t border-b py-3 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Check-in</span>
<span className="font-medium">
{new Date(tripDetails.startDate).toLocaleDateString()} (
{accommodation.checkInTime})
</span>
</div>
<div className="flex justify-between text-sm mt-2">
<span className="text-gray-600">Check-out</span>
<span className="font-medium">
{new Date(tripDetails.endDate).toLocaleDateString()} (
{accommodation.checkOutTime})
</span>
</div>
<div className="flex justify-between text-sm mt-2">
<span className="text-gray-600">Guests</span>
<span className="font-medium">
{tripDetails.numberOfGuests}
</span>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Room Type
</label>
<div className="grid grid-cols-3 gap-2">
{accommodation.roomTypes.map((room) => (
<button
key={room}
type="button"
onClick={() => setSelectedRoom(room)}
className={`text-sm py-2 px-3 rounded-md border transition-colors ${
selectedRoom === room
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 text-gray-700 hover:border-gray-400"
}`}
>
{room}
</button>
))}
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div>
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-700 mb-1"
>
Phone
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div>
<label
htmlFor="specialRequests"
className="block text-sm font-medium text-gray-700 mb-1"
>
Special Requests
</label>
<textarea
id="specialRequests"
name="specialRequests"
value={formData.specialRequests}
onChange={handleInputChange}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div className="border-t pt-3 mt-4">
<div className="flex justify-between items-center mb-3">
<span className="text-gray-600 text-sm">Total Price:</span>
<span className="font-semibold text-lg">
{accommodation.totalPrice}
</span>
</div>
<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Continue to Payment
</button>
</div>
</form>
</>
)}
{bookingStep === "payment" && (
<form onSubmit={handlePayment} className="space-y-3">
<h4 className="font-medium text-lg text-gray-900 mb-3">
Payment Details
</h4>
<div>
<label
htmlFor="cardName"
className="block text-sm font-medium text-gray-700 mb-1"
>
Name on Card
</label>
<input
type="text"
id="cardName"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
<div>
<label
htmlFor="cardNumber"
className="block text-sm font-medium text-gray-700 mb-1"
>
Card Number
</label>
<input
type="text"
id="cardNumber"
placeholder="XXXX XXXX XXXX XXXX"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label
htmlFor="expiry"
className="block text-sm font-medium text-gray-700 mb-1"
>
Expiry Date
</label>
<input
type="text"
id="expiry"
placeholder="MM/YY"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
<div>
<label
htmlFor="cvc"
className="block text-sm font-medium text-gray-700 mb-1"
>
CVC
</label>
<input
type="text"
id="cvc"
placeholder="XXX"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
</div>
<div className="border-t pt-3 mt-4">
<div className="flex justify-between items-center mb-3">
<span className="text-gray-600 text-sm">Total Amount:</span>
<span className="font-semibold text-lg">
{accommodation.totalPrice}
</span>
</div>
<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Complete Booking
</button>
<button
type="button"
onClick={() => setBookingStep("details")}
className="w-full mt-2 bg-white border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded-md hover:bg-gray-50 transition-colors"
>
Back
</button>
</div>
</form>
)}
{bookingStep === "confirmed" && (
<div className="text-center py-6">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-3">
<svg
className="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900">
Booking Confirmed!
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Your booking at {accommodation.name} has been confirmed. You'll
receive a confirmation email shortly at {formData.email}.
</p>
</div>
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-left">
<h4 className="font-medium text-sm text-gray-700">
Booking Summary
</h4>
<ul className="mt-2 space-y-1 text-xs text-gray-600">
<li className="flex justify-between">
<span>Check-in:</span>
<span className="font-medium">
{new Date(tripDetails.startDate).toLocaleDateString()}
</span>
</li>
<li className="flex justify-between">
<span>Check-out:</span>
<span className="font-medium">
{new Date(tripDetails.endDate).toLocaleDateString()}
</span>
</li>
<li className="flex justify-between">
<span>Room type:</span>
<span className="font-medium">{selectedRoom}</span>
</li>
<li className="flex justify-between">
<span>Guests:</span>
<span className="font-medium">
{tripDetails.numberOfGuests}
</span>
</li>
<li className="flex justify-between pt-1 mt-1 border-t">
<span>Total paid:</span>
<span className="font-medium">
{accommodation.totalPrice}
</span>
</li>
</ul>
</div>
</div>
)}
</div>
</div> </div>
); );
} }

View File

@@ -1,4 +1,5 @@
import { TripDetails } from "../../../trip-planner/types"; import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function BookRestaurant({ export default function BookRestaurant({
tripDetails, tripDetails,
@@ -7,9 +8,342 @@ export default function BookRestaurant({
tripDetails: TripDetails; tripDetails: TripDetails;
restaurantName: string; restaurantName: string;
}) { }) {
// Placeholder data - ideally would come from props
const [restaurant] = useState({
name: restaurantName,
cuisine: "Contemporary",
priceRange: "$$",
rating: 4.7,
image: "https://placehold.co/300x200?text=Restaurant",
openingHours: "5:00 PM - 10:00 PM",
address: "123 Main St, " + tripDetails.location,
availableTimes: ["6:00 PM", "7:00 PM", "8:00 PM", "9:00 PM"],
});
const [reservationStep, setReservationStep] = useState<
"selection" | "details" | "confirmed"
>("selection");
const [selectedDate, setSelectedDate] = useState<Date>(
new Date(tripDetails.startDate),
);
const [selectedTime, setSelectedTime] = useState<string | null>(null);
const [guests, setGuests] = useState(Math.min(tripDetails.numberOfGuests, 8));
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
specialRequests: "",
});
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const date = new Date(e.target.value);
setSelectedDate(date);
};
const handleGuestsChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setGuests(Number(e.target.value));
};
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleTimeSelection = (time: string) => {
setSelectedTime(time);
};
const handleContinue = () => {
if (selectedTime) {
setReservationStep("details");
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setReservationStep("confirmed");
};
const formatDate = (date: Date) => {
return date.toISOString().split("T")[0];
};
return ( return (
<div> <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
Book restaurant {restaurantName} for {JSON.stringify(tripDetails)} <div className="bg-orange-600 px-4 py-3">
<h3 className="text-white font-medium">Reserve at {restaurant.name}</h3>
<p className="text-orange-100 text-xs">
{restaurant.cuisine} {restaurant.priceRange} {restaurant.rating}
</p>
</div>
<div className="p-4">
{reservationStep === "selection" && (
<div className="space-y-4">
<div className="flex items-center space-x-3 mb-4">
<div className="flex-shrink-0 w-16 h-16 bg-gray-200 rounded-md overflow-hidden">
<img
src={restaurant.image}
alt={restaurant.name}
className="w-full h-full object-cover"
/>
</div>
<div>
<h4 className="font-medium text-gray-900">{restaurant.name}</h4>
<p className="text-sm text-gray-500">{restaurant.address}</p>
<p className="text-sm text-gray-500">
{restaurant.openingHours}
</p>
</div>
</div>
<div>
<label
htmlFor="date"
className="block text-sm font-medium text-gray-700 mb-1"
>
Date
</label>
<input
type="date"
id="date"
min={formatDate(new Date(tripDetails.startDate))}
max={formatDate(new Date(tripDetails.endDate))}
value={formatDate(selectedDate)}
onChange={handleDateChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="guests"
className="block text-sm font-medium text-gray-700 mb-1"
>
Guests
</label>
<select
id="guests"
value={guests}
onChange={handleGuestsChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
>
{Array.from({ length: 8 }, (_, i) => i + 1).map((num) => (
<option key={num} value={num}>
{num} {num === 1 ? "person" : "people"}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Available Times
</label>
<div className="grid grid-cols-3 gap-2">
{restaurant.availableTimes.map((time) => (
<button
key={time}
type="button"
onClick={() => handleTimeSelection(time)}
className={`text-sm py-2 px-3 rounded-md border transition-colors ${
selectedTime === time
? "border-orange-500 bg-orange-50 text-orange-700"
: "border-gray-300 text-gray-700 hover:border-gray-400"
}`}
>
{time}
</button>
))}
</div>
</div>
<button
onClick={handleContinue}
disabled={!selectedTime}
className={`w-full py-2 rounded-md text-white font-medium ${
selectedTime
? "bg-orange-600 hover:bg-orange-700"
: "bg-gray-400 cursor-not-allowed"
}`}
>
Continue
</button>
</div>
)}
{reservationStep === "details" && (
<form onSubmit={handleSubmit} className="space-y-3">
<div className="border-b pb-3 mb-1">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Date & Time</span>
<span className="font-medium">
{selectedDate.toLocaleDateString()} at {selectedTime}
</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span className="text-gray-600">Party Size</span>
<span className="font-medium">
{guests} {guests === 1 ? "person" : "people"}
</span>
</div>
<button
type="button"
onClick={() => setReservationStep("selection")}
className="text-orange-600 text-xs hover:underline mt-2"
>
Change
</button>
</div>
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-700 mb-1"
>
Phone
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="specialRequests"
className="block text-sm font-medium text-gray-700 mb-1"
>
Special Requests
</label>
<textarea
id="specialRequests"
name="specialRequests"
value={formData.specialRequests}
onChange={handleInputChange}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
placeholder="Allergies, special occasions, seating preferences..."
/>
</div>
<div className="pt-2">
<button
type="submit"
className="w-full bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Confirm Reservation
</button>
</div>
</form>
)}
{reservationStep === "confirmed" && (
<div className="text-center py-6">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-3">
<svg
className="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900">
Reservation Confirmed!
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Your table at {restaurant.name} has been reserved. You'll
receive a confirmation email shortly at {formData.email}.
</p>
</div>
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-left">
<h4 className="font-medium text-sm text-gray-700">
Reservation Details
</h4>
<ul className="mt-2 space-y-1 text-xs text-gray-600">
<li className="flex justify-between">
<span>Restaurant:</span>
<span className="font-medium">{restaurant.name}</span>
</li>
<li className="flex justify-between">
<span>Date:</span>
<span className="font-medium">
{selectedDate.toLocaleDateString()}
</span>
</li>
<li className="flex justify-between">
<span>Time:</span>
<span className="font-medium">{selectedTime}</span>
</li>
<li className="flex justify-between">
<span>Party Size:</span>
<span className="font-medium">
{guests} {guests === 1 ? "person" : "people"}
</span>
</li>
<li className="flex justify-between">
<span>Reservation Name:</span>
<span className="font-medium">{formData.name}</span>
</li>
</ul>
<p className="mt-3 text-xs text-gray-500">
Need to cancel or modify? Please call the restaurant directly at
(123) 456-7890.
</p>
</div>
</div>
)}
</div>
</div> </div>
); );
} }

View File

@@ -1,9 +1,249 @@
import { TripDetails } from "../../../trip-planner/types"; import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function RestaurantsList({ export default function RestaurantsList({
tripDetails, tripDetails,
}: { }: {
tripDetails: TripDetails; tripDetails: TripDetails;
}) { }) {
return <div>Restaurants list for {JSON.stringify(tripDetails)}</div>; // Placeholder data - ideally would come from props
const [restaurants] = useState([
{
id: "1",
name: "The Local Grill",
cuisine: "Steakhouse",
priceRange: "$$",
rating: 4.7,
distance: "0.5 miles from center",
image: "https://placehold.co/300x200?text=Restaurant1",
openingHours: "5:00 PM - 10:00 PM",
popular: true,
},
{
id: "2",
name: "Ocean Breeze",
cuisine: "Seafood",
priceRange: "$$$",
rating: 4.9,
distance: "0.8 miles from center",
image: "https://placehold.co/300x200?text=Restaurant2",
openingHours: "12:00 PM - 11:00 PM",
popular: true,
},
{
id: "3",
name: "Pasta Paradise",
cuisine: "Italian",
priceRange: "$$",
rating: 4.5,
distance: "1.2 miles from center",
image: "https://placehold.co/300x200?text=Restaurant3",
openingHours: "11:30 AM - 9:30 PM",
popular: false,
},
{
id: "4",
name: "Spice Garden",
cuisine: "Indian",
priceRange: "$$",
rating: 4.6,
distance: "0.7 miles from center",
image: "https://placehold.co/300x200?text=Restaurant4",
openingHours: "12:00 PM - 10:00 PM",
popular: false,
},
]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [filter, setFilter] = useState<string | null>(null);
const selectedRestaurant = restaurants.find((r) => r.id === selectedId);
const filteredRestaurants = filter
? restaurants.filter((r) => r.cuisine === filter)
: restaurants;
const cuisines = Array.from(new Set(restaurants.map((r) => r.cuisine)));
return (
<div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
<div className="bg-orange-600 px-4 py-3">
<div className="flex justify-between items-center">
<h3 className="text-white font-medium">
Restaurants in {tripDetails.location}
</h3>
{selectedId && (
<button
onClick={() => setSelectedId(null)}
className="text-white text-sm bg-orange-700 hover:bg-orange-800 px-2 py-1 rounded"
>
Back to list
</button>
)}
</div>
<p className="text-orange-100 text-xs">
For your trip {new Date(tripDetails.startDate).toLocaleDateString()} -{" "}
{new Date(tripDetails.endDate).toLocaleDateString()}
</p>
</div>
{!selectedId ? (
<div className="p-4">
<div className="mb-3">
<div className="flex flex-wrap gap-1 mb-1">
<button
onClick={() => setFilter(null)}
className={`px-2 py-1 text-xs rounded-full ${
filter === null
? "bg-orange-600 text-white"
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
}`}
>
All
</button>
{cuisines.map((cuisine) => (
<button
key={cuisine}
onClick={() => setFilter(cuisine)}
className={`px-2 py-1 text-xs rounded-full ${
filter === cuisine
? "bg-orange-600 text-white"
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
}`}
>
{cuisine}
</button>
))}
</div>
<p className="text-xs text-gray-500">
Showing {filteredRestaurants.length} restaurants{" "}
{filter ? `in ${filter}` : ""}
</p>
</div>
<div className="space-y-3">
{filteredRestaurants.map((restaurant) => (
<div
key={restaurant.id}
onClick={() => setSelectedId(restaurant.id)}
className="border rounded-lg p-3 cursor-pointer hover:border-orange-300 hover:shadow-md transition-all"
>
<div className="flex">
<div className="w-20 h-20 bg-gray-200 rounded-md flex-shrink-0 overflow-hidden">
<img
src={restaurant.image}
alt={restaurant.name}
className="w-full h-full object-cover"
/>
</div>
<div className="ml-3 flex-1">
<div className="flex justify-between items-start">
<div>
<h4 className="font-medium text-gray-900">
{restaurant.name}
</h4>
<p className="text-sm text-gray-500">
{restaurant.cuisine}
</p>
</div>
<span className="text-sm text-gray-700">
{restaurant.priceRange}
</span>
</div>
<div className="flex items-center mt-1">
<svg
className="w-4 h-4 text-yellow-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
<span className="text-xs text-gray-500 ml-1">
{restaurant.rating}
</span>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-gray-500">
{restaurant.distance}
</span>
{restaurant.popular && (
<span className="text-xs bg-orange-100 text-orange-800 px-1.5 py-0.5 rounded-sm">
Popular
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
) : (
<div className="p-4">
{selectedRestaurant && (
<div className="space-y-4">
<div className="w-full h-40 bg-gray-200 rounded-lg overflow-hidden">
<img
src={selectedRestaurant.image}
alt={selectedRestaurant.name}
className="w-full h-full object-cover"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-lg text-gray-900">
{selectedRestaurant.name}
</h3>
<p className="text-sm text-gray-600">
{selectedRestaurant.cuisine}
</p>
</div>
<span className="text-gray-700 font-medium">
{selectedRestaurant.priceRange}
</span>
</div>
<div className="flex items-center">
<svg
className="w-4 h-4 text-yellow-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
<span className="text-sm text-gray-600 ml-1">
{selectedRestaurant.rating} rating
</span>
</div>
<div className="flex items-center text-sm text-gray-600 space-x-4">
<span>{selectedRestaurant.distance}</span>
<span></span>
<span>{selectedRestaurant.openingHours}</span>
</div>
<p className="text-sm text-gray-600 pt-2 border-t">
{selectedRestaurant.name} offers a wonderful dining experience
in {tripDetails.location}. Perfect for a group of{" "}
{tripDetails.numberOfGuests} guests. Enjoy authentic{" "}
{selectedRestaurant.cuisine} cuisine in a relaxed atmosphere.
</p>
<div className="pt-3 flex flex-col space-y-2">
<button className="w-full bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-md transition-colors">
Reserve a Table
</button>
<button className="w-full bg-white border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded-md hover:bg-gray-50 transition-colors">
View Menu
</button>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
} }

View File

@@ -47,12 +47,12 @@ export function AssistantMessage({
message: Message; message: Message;
isLoading: boolean; isLoading: boolean;
}) { }) {
const contentString = getContentString(message.content);
const thread = useStreamContext(); const thread = useStreamContext();
const meta = thread.getMessagesMetadata(message); const meta = thread.getMessagesMetadata(message);
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint; const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
const contentString = getContentString(message.content);
const handleRegenerate = () => { const handleRegenerate = () => {
thread.submit(undefined, { checkpoint: parentCheckpoint }); thread.submit(undefined, { checkpoint: parentCheckpoint });
}; };