# Project export: explr.ai

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 10.0
- Tagline: Your next vacation planned in just a few swipes.
- Devpost: https://devpost.com/software/trvl-ai
- GitHub: https://github.com/kieronong/travel-aoo
- Video: https://www.youtube.com/embed/msF_7emBiW0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Hack for Fun)
- Team: 3 GitHub contributor(s) — Kieron Ong (17 commits), yiqiingg (3 commits), Surbhi Jain (2 commits)

## Devpost submission (written by the team)

### Inspiration

Planning vacations can be hard. Traveling is a very fun experience but often comes with a lot of stress of curating the perfect itinerary with all the best sights to see, foods to eat, and shows to watch. You don't want to miss anything special, but you also want to make sure the trip is still up your alley in terms of your own interests - a balance that can be hard to find.

### What it does

explr.ai simplifies itinerary planning with just a few swipes. After selecting your destination, the duration of your visit, and a rough budget, explr.ai presents you with a curated list of up to 30 restaurants, attractions, and activities that could become part of your trip. With an easy-to-use swiping interface, you choose what sounds interesting or not to you, and after a minimum of 8 swipes, let explr.ai's power convert your opinions into a full itinerary of activities for your entire visit.

### How we built it

We built this app using React Typescript for the frontend and Convex for the backend. The app takes in user input from the homepage regarding the location, price point, and time frame. We pass the location and price range into the Google API to retrieve the highest-rated attractions and restaurants in the area. Those options are presented to the user on the frontend with React and CSS animations that allow you to swipe each card in a Tinder-style manner. Taking consideration of the user's swipes and initial preferences, we query the Google API once again to get additional similar locations that the user may like and pass this data into an LLM (using Together.ai's Llama2 model) to generate a custom itinerary for the user. For each location outputted, we string together images from the Google API to create a slideshow of what your trip would look like and an animated timeline with descriptions of the location.

### Challenges we ran into

Front-end and design require a LOT of skill. It took us quite a while to come up with our project, and we originally were planning on a mobile app, but it's also quite difficult to learn completely new languages such as swift along with new technologies all in a couple of days. Once we started on explr.ai's backend, we were also having trouble passing in the appropriate information to the LLM to get back proper data that we could inject back into our web app.

### Accomplishments we're proud of

We're proud at the overall functionality and our ability to get something working by the end of the hacking period :') More specifically, we're proud of some of our frontend, including the card swiping and timeline animations as well as the ability to parse data from various APIs and put it together with lots of user input.

### What we learned

We learned a ton about full-stack development overall, whether that be the importance of Figma and UX design work, or how to best split up a project when every part is moving at the same time. We also learned how to use Convex and Together.ai productively!

### What's next

We would love to see explr.ai become smarter and support more features. explr.ai, in the future, could get information from hotels, attractions, and restaurants to be able to check availability and book reservations straight from the web app. Once you're on your trip, you should also be able to check in to various locations and provide feedback on each component. explr.ai could have a social media component of sharing your itineraries, plans, and feedback with friends and help each other better plan trips.

## README (from the GitHub repository)

# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
   parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: ['./tsconfig.json', './tsconfig.node.json'],
    tsconfigRootDir: __dirname,
   },
```

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list


## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 88 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (40 of 40)

```
.eslintrc.cjs
.gitignore
convex/_generated/api.d.ts
convex/_generated/api.js
convex/_generated/dataModel.d.ts
convex/_generated/server.d.ts
convex/_generated/server.js
convex/backend_api.ts
convex/myFunctions.ts
convex/README.md
convex/schema.ts
convex/tsconfig.json
index.html
package.json
README.md
src/App.css
src/App.tsx
src/Carousel/CarouselLoading.tsx
src/Carousel/index.tsx
src/Carousel/Options.tsx
src/Carousel/TinderCard.tsx
src/data/data_scrapper.py
src/data/final_data.jsonl
src/data/kmn.jsonl
src/index.css
src/Itinerary/hooks/typing_hook.tsx
src/Itinerary/index.tsx
src/Itinerary/ItineraryLoading.tsx
src/Itinerary/Slideshow.tsx
src/main.tsx
src/Search/Counter/counter.tsx
src/Search/Counter/styles.css
src/Search/index.tsx
src/Search/MapContainer/index.tsx
src/Search/search.tsx
src/Search/style.css
src/vite-env.d.ts
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- package.json: @emotion/react@^11.11.1, @emotion/styled@^11.11.0, @mui/icons-material@^5.14.15, @mui/material@^5.14.15, @react-google-maps/api@^2.19.2, @types/google-map-react@^2.1.9, @types/react@^18.2.33, @types/react-dom@^18.2.14, @typescript-eslint/eslint-plugin@^6.0.0, @typescript-eslint/parser@^6.0.0, @vitejs/plugin-react@^4.0.3, convex@^1.5.1, eslint@^8.45.0, eslint-plugin-react-hooks@^4.6.0, eslint-plugin-react-refresh@^0.4.3, formik@^2.4.5, formik-stepper@^2.2.1, framer-motion@^10.16.4, google-map-react@^2.2.1, localforage@^1.10.0, match-sorter@^6.3.1, react@^18.2.0, react-autosuggest@^10.1.0, react-dom@^18.2.0, react-router-dom@^6.17.0, react-type-animation@^3.2.0, react-vertical-timeline-component@^3.6.0, sort-by@^1.2.0, styled-components@^6.1.0, typescript@^5.2.2, vite@^4.4.5, yup@^1.3.2

### Recent commits (newest first)

- asadfd
- yasss
- hello
- afd
- working 1
- what the fuck kill me
- merge merge
- hi
- adsf
- km
- cyrus health insurance
- kmn
- hello hello
- hello yiqing
- aagh
- merge merge 1
- hello
- more frontend work for homepage
- hello
- kmn

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "travel-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host",
    "prod": "vite build",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "@mui/icons-material": "^5.14.15",
    "@mui/material": "^5.14.15",
    "@react-google-maps/api": "^2.19.2",
    "convex": "^1.5.1",
    "formik": "^2.4.5",
    "formik-stepper": "^2.2.1",
    "framer-motion": "^10.16.4",
    "google-map-react": "^2.2.1",
    "localforage": "^1.10.0",
    "match-sorter": "^6.3.1",
    "react": "^18.2.0",
    "react-autosuggest": "^10.1.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.17.0",
    "react-type-animation": "^3.2.0",
    "react-vertical-timeline-component": "^3.6.0",
    "sort-by": "^1.2.0",
    "styled-components": "^6.1.0",
    "yup": "^1.3.2"
  },
  "devDependencies": {
    "@types/google-map-react": "^2.1.9",
    "@types/react": "^18.2.33",
    "@types/react-dom": "^18.2.14",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "typescript": "^5.2.2",
    "vite": "^4.4.5"
  }
}

```

### src/main.tsx

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { ConvexProvider, ConvexReactClient } from "convex/react";
import './index.css'

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ConvexProvider client={convex}>
      <App />
    </ConvexProvider>
  </React.StrictMode>,
)

```

### src/App.tsx

```typescript
import { useState } from 'react'
import Search from './Search';
import Carousel from './Carousel';
import styled from 'styled-components';
import Itinerary from './Itinerary'; 

interface Card {
  imageURL: string;
  name: string;
  description: string;
  rating: string;
}

function App() {
  const [step, setStep] = useState<number>(0);
  const [location, setLocation] = useState<string>('New York');
  const [days, setDays] = useState<number>(1);
  const [priceRange, setPriceRange] = useState<number>(2);
  const [acceptedCards, setAcceptedCards] = useState<Card[]>([]);
  const [rejectedCards, setRejectedCards] = useState<Card[]>([]);

  const onNextStep = async (location?: string, days?: number, priceRange?: number, acceptedCards?: Card[], rejectedCards?: Card[]) => {
    try {
      if (step === 0) {
        if (location) {
          setLocation(location);
        }
        if (days)  {
          setDays(days);
        }
        if (priceRange) {
          setPriceRange(priceRange);
        }
      } else if (step === 1) {
        if (acceptedCards) {
          setAcceptedCards(acceptedCards);
        }
        if (rejectedCards) {
          setRejectedCards(rejectedCards);
        }
      }
      setStep(prevStep => prevStep + 1);
    } catch (error) {
      console.error(error);
    }
  };

  const DebugButton = styled.button`
  width: 100px;
  height: 100px;
  position: fixed;
  bottom: 10%;
  }
`;

  return (
    <>
      {/* <DebugButton onClick={() => setStep(step + 1)}>Next Step</DebugButton> */}
      {step === 0 && <Search onNextStep={onNextStep} />}
      {step === 1 && <Carousel location={location} days={days} priceRange={priceRange} onNextStep={onNextStep}/>}
      {step === 2 && <Itinerary location={location} days={days} priceRange={priceRange} acceptedCards={acceptedCards} rejectedCards={rejectedCards} onNextStep={onNextStep}/>}
    </>
  );
}

export default App;


```

### src/Search/index.tsx

```typescript
import React from 'react';
import SearchBar from './search';

// Define the prop types
interface SearchProps {
  onNextStep: (location: string) => Promise<void>;
}

const Search: React.FC<SearchProps> = ({ onNextStep }) => {    
    return (
        <SearchBar onNextStep={onNextStep}/>
    );
};

export default Search;

```

### convex/_generated/server.js

```javascript
/* eslint-disable */
/**
 * Generated utilities for implementing server-side Convex query and mutation functions.
 *
 * THIS CODE IS AUTOMATICALLY GENERATED.
 *
 * Generated by convex@1.5.1.
 * To regenerate, run `npx convex dev`.
 * @module
 */

import {
  actionGeneric,
  httpActionGeneric,
  queryGeneric,
  mutationGeneric,
  internalActionGeneric,
  internalMutationGeneric,
  internalQueryGeneric,
} from "convex/server";

/**
 * Define a query in this Convex app's public API.
 *
 * This function will be allowed to read your Convex database and will be accessible from the client.
 *
 * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
 * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
 */
export const query = queryGeneric;

/**
 * Define a query that is only accessible from other Convex functions (but not from the client).
 *
 * This function will be allowed to read from your Convex database. It will not be accessible from the client.
 *
 * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
 * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
 */
export const internalQuery = internalQueryGeneric;

/**
 * Define a mutation in this Convex app's public API.
 *
 * This function will be allowed to modify your Convex database and will be accessible from the client.
 *
 * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
 * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
 */
export const mutation = mutationGeneric;

/**
 * Define a mutation that is only accessible from other Convex functions (but not from the client).
 *
 * This function will be allowed to modify your Convex database. It will not be accessible from the client.
 *
 * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
 * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
 */
export const internalMutation = internalMutationGeneric;

/**
 * Define an action in this Convex app's public API.
 *
 * An action is a function which can execute any JavaScript code, including non-deterministic
 * code and code with side-effects, like calling third-party services.
 * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
 * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
 *
 * @param func - The action. It receives an {@link ActionCtx} as its first argument.
 * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
 */
export const action = actionGeneric;

/**
 * Define an action that is only accessible from other Convex functions (but not from the client).
 *
 * @param func - The function. It receives an {@link ActionCtx} as its first argument.
 * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
 */
export const internalAction = internalActionGeneric;

/**
 * Define a Convex HTTP action.
 *
 * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
 * as its second.
 * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
 */
export const httpAction = httpActionGeneric;

```

### src/Carousel/index.tsx

```typescript
import React, { useEffect } from 'react';
import styled from 'styled-components';
import {useQuery, useConvex } from "convex/react"

import TinderCard from './TinderCard'
import CarouselLoading from './CarouselLoading'
import accept from './accept.png'
import reject from './reject.png'
import next from './next.png'
import { api } from "../../convex/_generated/api"

// Define the prop types
interface CarouselProps {
    location: string,
    days: number,
    priceRange: number,
    onNextStep: (apiPayload: string) => Promise<void>;
}

interface Card {
    imageURL: string;
    name: string;
    description: string;
    rating: string;
}

const defaultCards: Card[] = [

]

const CenterContainer = styled.div`
    width: 100%;
    left: 50%;
    top: 20%;
    transform: translate(-28%, 0%);
    position: absolute
`;

const AcceptButton = styled.button`
    width: 100px;
    height: 100px;
    position: fixed;
    bottom: 10%;
    right: 43vw;
    transform: translate(50%, 0);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #7BBE8E;
    border: none;
    cursor: pointer;
    box-shadow: 0 10px 10px 0 rgba(0,0,0,0.2);
    transition: background-color 0.3s, width 0.3s, height 0.3s;
    &:hover {
        background-color: green;
        width: 120px;
        height: 120px;
    }
`;

const RejectButton = styled.button`
    width: 100px;
    height: 100px;
    position: fixed;
    bottom: 10%;
    left: 43vw;
    transform: translate(-50%, 0);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #FF758A;
    border: none;
    cursor: pointer;
    box-shadow: 0 10px 10px 0 rgba(0,0,0,0.2);
    transition: background-color 0.3s, width 0.3s, height 0.3s;
    &:hover {
        background-color: #FF2446;
        width: 120px;
        height: 120px;
    }
`;

const ContinueButton = styled.button`
    width: 100px;
    height: 100px;
    position: fixed;
    bottom: 50%;
    transform: translate(0, 50%);
    right: 2%;
    border-radius: 50%;
    font-size: 1em;
    font-family: 'Rubik';
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--background-blue);
    border: none;
    cursor: pointer;
    box-shadow: 0 10px 10px 0 rgba(0,0,0,0.2);
    transition: background-color 0.3s, width 0.3s, height 0.3s;
    &:hover {
        background-color: blue;
        width: 120px;
        height: 120px;
    }
`;

const IconImage = styled.img`
    width: 30px;
    height: 30px;
    margin: 10px;
`;

const StackedCardsContainer = styled.div`
    position: relative;
`;


const Carousel: React.FC<CarouselProps> = ({ location, days, priceRange, onNextStep }) => {

    const [cardIndex, setCardIndex] = React.useState(0);
    const [cardList, setCardList] = React.useState<Card[]>(defaultCards);
    const [rejectedCards, setRejectedCards] = React.useState<Card[]>([]);
    const [acceptedCards, setAcceptedCards] = React.useState<Card[]>([]);
    const [isLoading, setIsLoading] = React.useState(true);
    const [swipeDirection, setSwipeDirection] = React.useState<string | null>(null);
    const convex = useConvex();

    const handleAccept = () => {
        if (cardIndex === cardList.length - 2) {
            handleSubmit();
        }
        setSwipeDirection('right')
        setAcceptedCards([...acceptedCards, cardList[cardIndex]]);
        setCardIndex(cardIndex + 1);

    };

    const handleReject = () => {
        if (cardIndex === cardList.length - 2) {
            handleSubmit();
        }
        setSwipeDirection('left')
        setRejectedCards([...rejectedCards, cardList[cardIndex]]);
        setCardIndex(cardIndex + 1);
    };

    const handleSubmit = () => {
        const apiPayload = JSON.stringify(
            {
                location: location,
                days: days,
                priceRange: priceRange,
                rejectedCards: rejectedCards,
                acceptedCards: acceptedCards,
            }
        )
        onNextStep(apiPayload);
    };

    const renderCards = () => {
        return cardList.slice(cardIndex, cardIndex + 5).map((card, index) => (
            <TinderCard
                index={index}
                swipeDirection={(index === 0 && swipeDirection) ? swipeDirection : ''} // Only animate the top card
                key={card.name}
                imageURL={card.imageURL}
                name={card.name}
                description={card.description}
                rating={card.rating}
            />
        ));
    };

    // Fill with actual API call
    useEffect(() => {
        const fetchCards = async () => {
            try {
                let price_high = 0
                let price_low = -1
                if (priceRange === 0) {
                price_high = 1
                price_low = -1
                } else if (priceRange === 1){
                price_high = 2
                price_low = -1
                } else if (priceRange === 2){
                price_high = 3
                price_low = -1
                } 
                const data = await convex.query(api.backend_api.get_attractions,{city: location, price_high: price_high, price_low: price_low })
                const newCardList = data.map((attraction) => {
                    let rating = String(attraction.rating)
                    if (rating.length < 3) {
                        rating = rating + ".0"
                    }
                    return {
                        imageURL: attraction.photo_url,
                        name: attraction.name,
                        description: attraction.description,
                        rating: rating,
                    }
                })
                setCardList(newCardList)
                setIsLoading(false);
            } catch (error) {
                console.error(error);
            }
        };
        fetchCards();
    }, [])

    ret
[truncated — 744 more characters]
```

### src/Itinerary/index.tsx

```typescript
import React from 'react';
import styled from 'styled-components';
import ItineraryLoading from './ItineraryLoading';
import {useAction, useConvex } from "convex/react";
import { api } from "../../convex/_generated/api";
import Slideshow from './Slideshow';
import { useTypingEffect } from "./hooks/typing_hook";
import { VerticalTimeline, VerticalTimelineElement }  from 'react-vertical-timeline-component';
import 'react-vertical-timeline-component/style.min.css';
import AttractionsIcon from '@mui/icons-material/Attractions';
import LocalDiningIcon from '@mui/icons-material/LocalDining';

// Define the prop types
interface ItineraryProps {
    onNextStep: () => Promise<void>;
    location: string;
    priceRange: number;
    days: number;
    acceptedCards: Card[];
    rejectedCards: Card[];
}

interface Card {
    imageURL: string;
    name: string;
    description: string;
    rating: string;
}

interface Event {
    imageURL: string;
    name: string;
    description: string;
    category: string;
    day: string;
    time: string;
}

// Styling components
const TimelineContainer = styled.div`
    max-width: 600px;
    margin: auto;
    padding: 20px;
`;

const ScreenContainer = styled.div`
    width: 100%;
    display: flex;
    flex-direction: column;
`;

const Title = styled.h1`
    text-align: center;
    font-size: 2em;
    width: 100%;
`;

const CoverImage = styled.img`
    width: 100%;
    height: auto;
`;

const Event = styled.div`
    margin: 30px 0;
`;

const Time = styled.h2`
    margin: 0;
`;

const EventTitle = styled.h3`
    margin: 5px 0;
`;

const Description = styled.p``;

// Sample data structure, you may replace this with your actual data fetching

const testData = [
        {
            "category": "tourist_attraction",
            "day": "Day 1",
            "description": "Famous comics are often in the lineup at this brick-walled comedy club with several shows nightly.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuGs4zfCPdcpxXktouA4MaXD_pZ8gmz8i2uCr06RbbKWr3Wak727JdEjGWA0Z1hA5L95qglNokbnAP7pj2HRvOxakZHT8uRlZgv3ISlGIgVDKFAklYP2t1CXAtEinsjCtfMoIBRkfi19tZJH_DXHNfB_oujh1ZiYi1ESbkScZ_2GJFBL&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "Comedy Cellar",
            "time": "9:00 AM"
        },
        {
            "category": "tourist_attraction",
            "day": "Day 1",
            "description": "Indie headliners & up-and-comers have kept this small music venue crowded since the '90s.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuElITGpWnxC-xkNLSgwWavSihZ0_8oxgGIKJnlemXFUDxsmmX2Q6fpy8PCUOV6DKVBDPsHaqyLug_YBoPPwCtyG1IeFm3qykfGZsR4coKQX3tRTe44G2wjmrs67KdINS8ISrWDEIMRVBvZSTc3Z-gWJrbZcaKI24TtJuGEvteU4WRUD&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "The Mercury Lounge",
            "time": "11:00 AM"
        },
        {
            "category": "restaurant",
            "day": "Day 1",
            "description": "Shop with spiffy space-age decor selling plastic bowlfuls of rice pudding in unusual flavors.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuElkCBqtOm-AQugEbqMo98jppkLCEbBki_hiloS2puzXgGoQ3C8v0vHAh0KW73g8-58IoX3_sKtNM-vJTP2MvlB4ly-DuAUBuPWehKG9D1QOuxrzmM1EAQmAkA5xaOz6nLTv7zlfQtpGLjat2b56EVHkILqxehC_ddhxXea0FoKFRdk&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "Rice To Riches",
            "time": "4:00 PM"
        },
        {
            "category": "restaurant",
            "day": "Day 2",
            "description": "Landmark NoLita restaurant serving coal-fired, thin-crust Neapolitan pizza since 1905.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuE9mgy8E1AuvB5z3VdWhi6ekZDarTPxW0QMD31YutyFg-9CSQf29JuUvPZJahdpaugSk5ixuGswLYeyg6NUgx-QuAqVJPSax5fjkftfpJ3srG89Kug1z_zp-YYfETkMFnQHRN7Yd1c4Rh1wTKs-T1BvE1x7hTWCXiEdEECqG6pPpU7j&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "Lombardi's Pizza",
            "time": "8:00 AM"
        },
        {
            "category": "restaurant",
            "day": "Day 2",
            "description": "Luxe rooms & suites in a hip lodging with a swanky restaurant, a glam cocktail lounge & a 24/7 gym.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuFD_TQyuiHj1s7-GBf1ssYokNGtjS_4XXDPzEBS202NXK3MLlMlKNpfIT_-VVqCkTnByeXl8pHwNoT4w7tNQZmfqCJm5MTbIAWtBYKpMZhm5T444sUmrUCAe0nQIX2MHWAQzfrvEImAEZz3gIMTHNO8h_vN574VuDr_tiQfD_p4ZGmm&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "NOMO SOHO",
            "time": "11:00 AM"
        },
        {
            "category": "restaurant",
            "day": "Day 2",
            "description": "TriBeCa hot spot showcasing rustic Italian cuisine in a lively atmosphere.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuG1hvZVpdNVSfVXEXClnDJcHZSLU_klp_FYnb6O4usvGF2hcxXLr2TaFemM9huE9URPxnUfP3e97a2WwLK4dMdm72c3gjN_iiRJw9xvLVYtK1vtUmnbwxenQuyBWLhGnrdrF5g0oIjnI-K_E4uPJobqMj-oUyzuuxRHgW0wmtkt9vxd&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "Locanda Verde",
            "time": "3:00 PM"
        },
        {
            "category": "tourist_attraction",
            "day": "Day 3",
            "description": "Historic park with Ellis Island & Statue of Liberty views, plus ferry service to both islands.",
            "imageURL": "https://maps.googleapis.com/maps/api/place/photo?maxwidth=1600&photo_reference=AcJnMuGu-yFSLGOgRGblFVC3F_5ahd8HB7ctqJsvjk4SXOFUSeyfD2csAsMYZGFkHGjzWdAgE9Rn0SjLjduMErHhTY_O_OB4YITxM-J2iXvMXVDmzxISlwDzrpL1FTQI7VVCo4Sz9lgjdJUxD4sx9LlwV6RSAh95Z6b5qArTOVkYkJiQM2Hq&key=AIzaSyBG8-ZzBHlTA6e7WfcvUm1OZxXCNk7ErMU",
            "name": "The Battery",
            "time": "8:00 AM"
        },
        {

[truncated — 5198 more characters]
```

### src/Search/MapContainer/index.tsx

```typescript
import { GoogleMap, LoadScript } from '@react-google-maps/api';

const mapStyles = {
  height: "100vh",
  width: "100%"
};

const defaultCenter = {
  lat: 40.712776,
  lng: -74.005974
};

function MapContainer() {
  return (
    <LoadScript
      googleMapsApiKey='AIzaSyAd4qE0Arj9FNGITFuHVJpYx-FRJn31Hfc'
    >
      <GoogleMap
        mapContainerStyle={mapStyles}
        zoom={13}
        center={defaultCenter}
      >
      </GoogleMap>
    </LoadScript>
  )
}

export default MapContainer;

```

### vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Bodoni+Moda:opsz@6..96&display=swap" rel="stylesheet">
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

[21 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]