merge main and remove book resturant and accommodation tools/uis

This commit is contained in:
bracesproul
2025-03-10 10:41:27 -07:00
42 changed files with 1370 additions and 1263 deletions

View File

@@ -5,6 +5,8 @@ import BookAccommodation from "./trip-planner/book-accommodation";
import RestaurantsList from "./trip-planner/restaurants-list";
import BookRestaurant from "./trip-planner/book-restaurant";
import BuyStock from "./stockbroker/buy-stock";
import Plan from "./open-code/plan";
import ProposedChange from "./open-code/proposed-change";
const ComponentMap = {
"stock-price": StockPrice,
@@ -14,5 +16,7 @@ const ComponentMap = {
"restaurants-list": RestaurantsList,
"book-restaurant": BookRestaurant,
"buy-stock": BuyStock,
"code-plan": Plan,
"proposed-change": ProposedChange,
} as const;
export default ComponentMap;

View File

@@ -0,0 +1,76 @@
import "./index.css";
import { motion } from "framer-motion";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
interface PlanProps {
toolCallId: string;
executedPlans: string[];
rejectedPlans: string[];
remainingPlans: string[];
}
export default function Plan(props: PlanProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="flex flex-col w-full max-w-4xl border-[1px] rounded-xl border-slate-200 overflow-hidden">
<div className="p-6">
<h2 className="text-2xl font-semibold text-left">Code Plan</h2>
</div>
<motion.div
className="relative overflow-hidden"
animate={{
height: isExpanded ? "auto" : "200px",
opacity: isExpanded ? 1 : 0.7,
}}
transition={{
height: { duration: 0.3, ease: [0.4, 0, 0.2, 1] },
opacity: { duration: 0.2 },
}}
initial={false}
>
<div className="grid grid-cols-3 divide-x divide-slate-300 w-full border-t border-slate-200 px-6 pt-4 pb-4">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Remaining Plans
</h3>
{props.remainingPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{index + 1}. {step}
</p>
))}
</div>
<div className="flex flex-col gap-2 px-6">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Executed Plans
</h3>
{props.executedPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{step}
</p>
))}
</div>
<div className="flex flex-col gap-2 px-6">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Rejected Plans
</h3>
{props.rejectedPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{step}
</p>
))}
</div>
</div>
</motion.div>
<motion.button
className="w-full py-2 border-t border-slate-200 flex items-center justify-center hover:bg-slate-50 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
>
<ChevronDown className="w-5 h-5 text-slate-600" />
</motion.button>
</div>
);
}

View File

@@ -0,0 +1,122 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,189 @@
import "./index.css";
import { v4 as uuidv4 } from "uuid";
import { Button } from "@/components/ui/button";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui";
import { Message } from "@langchain/langgraph-sdk";
import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses";
import { useEffect, useState } from "react";
import { getToolResponse } from "../../utils/get-tool-response";
import { cn } from "@/lib/utils";
interface ProposedChangeProps {
toolCallId: string;
change: string;
planItem: string;
/**
* Whether or not to show the "Accept"/"Reject" buttons
* If true, this means the user selected the "Accept, don't ask again"
* button for this session.
*/
fullWriteAccess: boolean;
}
const ACCEPTED_CHANGE_CONTENT =
"User accepted the proposed change. Please continue.";
const REJECTED_CHANGE_CONTENT =
"User rejected the proposed change. Please continue.";
export default function ProposedChange(props: ProposedChangeProps) {
const [isAccepted, setIsAccepted] = useState(false);
const [isRejected, setIsRejected] = useState(false);
const thread = useStreamContext<
{ messages: Message[]; ui: UIMessage[] },
{ MetaType: { ui: UIMessage | undefined } }
>();
const handleReject = () => {
thread.submit({
messages: [
{
type: "tool",
tool_call_id: props.toolCallId,
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
name: "update_file",
content: REJECTED_CHANGE_CONTENT,
},
{
type: "human",
content: `Rejected change.`,
},
],
});
setIsRejected(true);
};
const handleAccept = (shouldGrantFullWriteAccess = false) => {
const humanMessageContent = `Accepted change. ${shouldGrantFullWriteAccess ? "Granted full write access." : ""}`;
thread.submit(
{
messages: [
{
type: "tool",
tool_call_id: props.toolCallId,
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
name: "update_file",
content: ACCEPTED_CHANGE_CONTENT,
},
{
type: "human",
content: humanMessageContent,
},
],
},
{
config: {
configurable: {
permissions: {
full_write_access: shouldGrantFullWriteAccess,
},
},
},
},
);
setIsAccepted(true);
};
useEffect(() => {
if (typeof window === "undefined" || isAccepted) return;
const toolResponse = getToolResponse(props.toolCallId, thread);
if (toolResponse) {
if (toolResponse.content === ACCEPTED_CHANGE_CONTENT) {
setIsAccepted(true);
} else if (toolResponse.content === REJECTED_CHANGE_CONTENT) {
setIsRejected(true);
}
}
}, []);
if (isAccepted || isRejected) {
return (
<div
className={cn(
"flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl",
isAccepted ? "border-green-300" : "border-red-300",
)}
>
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">
{isAccepted ? "Accepted" : "Rejected"} Change
</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown
children={props.change}
components={{
code(props) {
const { children, className, node: _node } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={coldarkDark}
/>
) : (
<code className={className}>{children}</code>
);
},
}}
/>
</div>
);
}
return (
<div className="flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl border-slate-200">
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">Proposed Change</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown
children={props.change}
components={{
code(props) {
const { children, className, node: _node } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={coldarkDark}
/>
) : (
<code className={className}>{children}</code>
);
},
}}
/>
{!props.fullWriteAccess && (
<div className="flex gap-2 items-center w-full">
<Button
className="cursor-pointer w-full"
variant="destructive"
onClick={handleReject}
>
Reject
</Button>
<Button
className="cursor-pointer w-full"
onClick={() => handleAccept()}
>
Accept
</Button>
<Button
className="cursor-pointer w-full bg-blue-500 hover:bg-blue-500/90"
onClick={() => handleAccept(true)}
>
Accept, don&apos;t ask again
</Button>
</div>
)}
</div>
);
}

View File

@@ -160,7 +160,7 @@ export default function PortfolioView() {
const chartData = selectedHolding ? generateChartData(selectedHolding) : [];
return (
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
<div className="w-full max-w-3xl bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
<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">

View File

@@ -138,8 +138,18 @@ export default function StockPrice(props: {
};
}, [oneDayPrices, thirtyDayPrices, displayRange]);
const formatDateByDisplayRange = (value: string, isTooltip?: boolean) => {
if (displayRange === "1d") {
return format(value, "h:mm a");
}
if (isTooltip) {
return format(value, "LLL do h:mm a");
}
return format(value, "LLL do");
};
return (
<div className="w-full max-w-4xl rounded-xl shadow-md overflow-hidden border border-gray-200 flex flex-col gap-4 p-3">
<div className="w-full max-w-3xl rounded-xl shadow-md overflow-hidden border border-gray-200 flex flex-col gap-4 p-3">
<div className="flex items-center justify-start gap-4 mb-2 text-lg font-medium text-gray-700">
<p>{ticker}</p>
<p>${currentPrice}</p>
@@ -180,7 +190,7 @@ export default function StockPrice(props: {
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => format(value, "h:mm a")}
tickFormatter={(v) => formatDateByDisplayRange(v)}
/>
<YAxis
domain={[lowPrice - 2, highPrice + 2]}
@@ -191,10 +201,11 @@ export default function StockPrice(props: {
/>
<ChartTooltip
cursor={false}
wrapperStyle={{ backgroundColor: "white" }}
content={
<ChartTooltipContent
hideLabel={false}
labelFormatter={(value) => format(value, "h:mm a")}
labelFormatter={(v) => formatDateByDisplayRange(v, true)}
/>
}
/>

View File

@@ -320,7 +320,7 @@ export default function AccommodationsList({
align: "start",
loop: true,
}}
className="w-full sm:max-w-sm md:max-w-2xl lg:max-w-3xl"
className="w-full sm:max-w-sm md:max-w-3xl lg:max-w-3xl"
>
<CarouselContent>
{accommodations.map((accommodation) => (

View File

@@ -1,403 +0,0 @@
import "./index.css";
import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function BookAccommodation({
tripDetails,
accommodationName,
}: {
tripDetails: TripDetails;
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 (
<div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
<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>
);
}

View File

@@ -1 +0,0 @@
@import "tailwindcss";

View File

@@ -1,350 +0,0 @@
import "./index.css";
import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function BookRestaurant({
tripDetails,
restaurantName,
}: {
tripDetails: TripDetails;
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 (
<div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
<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>
);
}