# Project export: PawPals

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: Don't 'trip' over the details, we've got you covered!
- Devpost: https://devpost.com/software/pawpals-bd8njs
- GitHub: https://github.com/Yatsz/PawPals
- Video: https://www.youtube.com/embed/WT39tK_BJ8E?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Daniel Kim (23 commits)

## Devpost submission (written by the team)

### Inspiration

As college students, we both agreed that fun trips definitely make college memorable. However, we are sometimes hesitant about initiating plans because of the low probability of plans making it out of the group chat. Our biggest problem was being able to find unique but local activities to do with a big group, and that inspired the creation of PawPals.

### What it does

Based on a list of criteria, we prompt OpenAI's API to generate the perfect itinerary for your trip, breaking down hour by hour a schedule of suggestions to have an amazing trip. This relieves some of the underlying stresses of planning trips such as research and decision-making.

### How we built it

On the front-end, we utilized Figma to develop an UI design that we then translated into React.js to develop a web app. We connect a Python back-end through the utilization of Fast API to integrate the usage of SQLite and OpenAI API into our web application.

### Challenges we ran into

Some challenges we ran into were the lack of experience in Full-Stack development. We did not have the strongest technical skill sets needed; thus, we took this time to explore and learn new technologies in order to accomplish the project. Furthermore, we recognized the long time it takes for OpenAI API to generate text, leading us to in the future output the generated text as a stream utilizing the SDK for it by Vercel.

### Accomplishments we're proud of

As a team of two, we were very proud of our final product and took pride in our work. We came up with a plan and vision that we truly dedicated ourselves to in order to accomplish it. Even though we have not had too much experience in creating a full-stack application and developing a Figma design, we decided to take the leap in exploring these domains, going out of our comfort zone which allowed us to truly learn a lot from this experience.

### What we learned

We learned the value of coming up with a plan and solving problems that arise creatively. There is always a solution to a goal, and we found that we simply need to find it. Furthermore, we both were able to develop ourselves technically as we explored new domains and technologies, pushing our knowledge to greater breadth and depth. We were also able to meet many people along the way that would provide pieces of advice and guidance on paths and technologies to look into. These people accelerated the learning process and truly added a lot to the hackathon experience. Overall, this hackathon was an amazing learning experience for us!

### What's next

We hope to deploy the site on Vercel and develop a mobile application of the app. We believe with the streaming technology, we can create faster response time. Furthermore, through some more scaling in interconnectivity with friends, budgeting, and schedule adjusting, we will be able to create an application that will definitely shift the travel planning industry!

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 16 recognized source files, 77 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
- FastAPI (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (24 of 24)

```
.DS_Store
backend/.gitignore
backend/database.py
backend/main.py
backend/models.py
backend/plan.db
frontend/.gitignore
frontend/package.json
frontend/public/index.html
frontend/public/manifest.json
frontend/public/robots.txt
frontend/README.md
frontend/src/api.js
frontend/src/App.css
frontend/src/index.css
frontend/src/index.js
frontend/src/pages/App.js
frontend/src/pages/Hawaii.js
frontend/src/pages/Home.js
frontend/src/pages/Palm.js
frontend/src/pages/Tahoe.js
frontend/src/reportWebVitals.js
frontend/src/Router.js
LICENSE
```

### Dependencies

- frontend/package.json: @emotion/react@^11.11.1, @emotion/styled@^11.11.0, @mui/material@^5.14.15, @mui/styled-engine-sc@^6.0.0-alpha.3, @testing-library/jest-dom@^5.17.0, @testing-library/react@^13.4.0, @testing-library/user-event@^13.5.0, axios@^1.6.0, react@^18.2.0, react-dom@^18.2.0, react-router-dom@^6.17.0, react-scripts@5.0.1, styled-components@^6.1.0, web-vitals@^2.1.4

### Recent commits (newest first)

- Merge pull request #9 from Yatsz/fixed-font
- Fixed Hawaii Image
- Merge pull request #8 from Yatsz/fixed-font
- Fixed formatting
- Merge pull request #7 from Yatsz/fixed-font
- Fixed Spacing on Tahoe Page
- Merge pull request #6 from Yatsz/fixed-font
- Added other pages and improved pages
- Merge pull request #5 from Yatsz/fixed-font
- Improved Formatting
- Merge pull request #4 from Yatsz/fixed-font
- Fixed parsing
- Merge pull request #3 from Yatsz/fixed-font
- Fixed Fonts
- Merge pull request #2 from Yatsz/new-front-end
- Improved Front-End
- Merge pull request #1 from Yatsz/new-front-end
- New Front-End
- Reordering Files
- Add frontend

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

### frontend/package.json

```
{
  "name": "trip-planner",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "@mui/material": "^5.14.15",
    "@mui/styled-engine-sc": "^6.0.0-alpha.3",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^1.6.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.17.0",
    "react-scripts": "5.0.1",
    "styled-components": "^6.1.0",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

```

### backend/main.py

```python
from fastapi import FastAPI, HTTPException, Depends, status
from typing import Annotated, List
from sqlalchemy.orm import Session
from pydantic import BaseModel
from database import SessionLocal, engine
import models 
from fastapi.middleware.cors import CORSMiddleware
import config
import openai
from fastapi.responses import StreamingResponse
import asyncio

openai.api_key = config.API_key

app = FastAPI()

origins = [
    'http://localhost:3000',
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials = True,
    allow_methods=['*'],
    allow_headers=['*']
)

class PlanBase(BaseModel):
    location: str
    duration: str
    interests: str
    age: str

class PlanModel(PlanBase):
    id: int

    class Config:
        orm_mode = True

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

db_dependency = Annotated[Session, Depends(get_db)]

models.Base.metadata.create_all(bind=engine)



@app.post("/plan/", response_model=PlanModel)
async def create_plan(plan: PlanBase, db: db_dependency):
    db_plan = models.Plan(**plan.dict())
    db.add(db_plan)
    db.commit()
    db.refresh(db_plan)
    
    # Get total number of plans
    total_plans = db.query(models.Plan).count()
    
    # If there are more than 4 plans, delete the excess
    if total_plans > 4:
        excess = total_plans - 4
        for _ in range(excess):
            oldest_plan = db.query(models.Plan).order_by(models.Plan.id).first()
            db.delete(oldest_plan)
            db.commit()

    return db_plan

@app.get("/plan/", response_model=List[PlanModel])
async def read_plan(db: db_dependency, skip: int = 0, limit: int = 100):
    plan = db.query(models.Plan).offset(skip).limit(limit).all()
    return plan

@app.post("/generate_itinerary/")
async def generate_itinerary(db: db_dependency):
    # Fetch the most recent Plan from the database
    latest_plan = db.query(models.Plan).order_by(models.Plan.id.desc()).first()

    # If no plan found, return an error or default response
    if not latest_plan:
        return {"error": "No plan found"}

    location = latest_plan.location
    duration = latest_plan.duration
    interests = latest_plan.interests
    age = latest_plan.age

    journey = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "You are a professional tour guide that has experience around the world at all places from food recommendations, to sight seeing. You like to provide various popular tourist spots depending on age range, interests, and amount of time in the city. You hit the most important spots when generating an itinerary and plan."},
            {"role": "user", "content": f"Provide a full itinerary for {duration} from 9am to 10 pm of what to do when visiting {location}. For each day provide specific food places to eat at, tourist spots, and fun activities to do that are unique to the area. Take into account that I am here to {interests} and I am {age}. Make sure to use the duration to make an itinerary for each day and not just one day alongside providing the hour time stamps."}
        ]
    )

    content = journey.choices[0].message['content']
    return {"itinerary": content}

```

### frontend/src/index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Router from './Router.js'
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Router />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

### frontend/src/pages/App.js

```javascript
import React, { useState, useEffect } from "react";
import api from "../api";
import { TextField, Button } from "@mui/material";

import imgOne from "../img1.png";
import imgTwentytwo from "../Images/dogPawOne.png";
import { createTheme } from '@mui/material/styles';
import { ThemeProvider } from '@mui/material/styles';

import { useNavigate } from 'react-router-dom';



const customTheme = createTheme({
  palette: {
    primary: {
      main: '#B88865',
    },
    secondary: {
      main: '#f6ece5',
    },
  },
});
const App = () => {
  const [plan, setPlan] = useState([]);
  const [itinerary, setItinerary] = useState("");
  const [loadingMessage, setLoadingMessage] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  const [formData, setFormData] = useState({
    location: "",
    duration: "",
    interests: "",
    age: "",
  });

  const fetchPlan = async () => {
    const response = await api.get("/plan/");
    setPlan(response.data);
  };

  useEffect(() => {
    fetchPlan();
  }, []);

  useEffect(() => {
    let count = 0;
    if (isLoading) {
      const interval = setInterval(() => {
        count += 1;
        setLoadingMessage(`Loading for ${count} seconds...`);
      }, 1000);

      return () => clearInterval(interval);
    } else {
      setLoadingMessage("");
    }
  }, [isLoading]);

  const handleInputChange = (event) => {
    const value =
      event.target.type === "checkbox"
        ? event.target.checked
        : event.target.value;
    setFormData({
      ...formData,
      [event.target.name]: value,
    });
  };

  const handleFormSubmit = async (event) => {
    event.preventDefault();
    setIsLoading(true);
    // Post the form data to /plan/
    const response = await api.post("/plan/", formData);

    // Check if the post to /plan/ was successful
    if (response.status === 200) {
      // Fetch the most recent plan
      fetchPlan();

      // Now, call the generate_itinerary endpoint to get the itinerary
      const itineraryResponse = await api.post("/generate_itinerary/");
      if (itineraryResponse.status === 200) {
        // Set the itinerary state with the received data
        setItinerary(itineraryResponse.data.itinerary);
      } else {
        console.error("Failed to generate itinerary");
      }
    } else {
      console.error("Failed to post plan");
    }

    // Reset the form
    setFormData({
      location: "",
      duration: "",
      interests: "",
      age: "",
    });
    setIsLoading(false);
  };

  function formatItinerary(itinerary) {
    const lines = itinerary.split("\n"); // Split by line breaks
    let result = [];
  
    for (let i = 0; i < lines.length; i++) {
      let currentLine = lines[i].trim();
      if (
        i !== lines.length - 1 &&                 // not the last line
        !currentLine.startsWith("Day") &&         // doesn't start with "Day"
        currentLine !== ""
      ) {
        result.push("• " + currentLine); // Add bullet point
      } else {
        result.push(currentLine);
      }
    }
  
    return result.join("\n"); // Join back into a string
  }
  

  const formattedItinerary = formatItinerary(itinerary);
  const navigate = useNavigate();
  const handleButtonClickTwo = () => {
    navigate('/home');
  }

  return (
    <ThemeProvider theme={customTheme}>
    <div
      style={{
        position: "relative",
        borderRadius: "25px",
        backgroundColor: "#f6ece5",
        width: "100%",
        height: "2724px",
        overflow: "hidden",
        textAlign: "left",
        fontSize: "24px",
        color: "rgba(184, 136, 101, 0.8)",
        fontFamily: "Montserrat",
      }}
    >
      <div
        style={{
          position: "absolute",
          top: "46px",
          left: "46px",
          borderRadius: "20px",
          backgroundColor: "#63857a",
          width: "63px",
          height: "63px",
        }}
      />
      
      <form onSubmit={handleFormSubmit}>
        <TextField
          style={{
            border: "none",
            backgroundColor: "transparent",
            position: "absolute",
            top: "358px",
            left: "147px",
          }}
          type="text"
          id="location"
          name="location"
          label="Location"
          color="primary"
          sx={{ width: 464 }}
          variant="outlined"
          onChange={handleInputChange}
          value={formData.location}
        />
        <TextField
          style={{
            border: "none",
            backgroundColor: "transparent",
            position: "absolute",
            top: "483px",
            left: "147px",
          }}
          type="text"
          id="duration"
          name="duration"
          label="Duration (Days)"
          color="primary"
          sx={{ width: 464 }}
          variant="outlined"
          onChange={handleInputChange}
          value={formData.duration}
        />
        <TextField
          style={{
            border: "none",
            backgroundColor: "transparent",
            position: "absolute",
            top: "608px",
            left: "147px",
          }}
          type="text"
          id="interests"
          name="interests"
          label="Intent of Trip (i.e. Relax, Party)"
          color="primary"
          sx={{ width: 464 }}
          variant="outlined"
          onChange={handleInputChange}
          value={formData.interests}
        />
        <TextField
          style={{
            border: "none",
            backgroundColor: "transparent",
            position: "absolute",
            top: "733px",
            left: "147px",
          }}
          type="text"
          id="age"
          name="age"
          label="Age"
          color="primary"
          sx={{ width: 464 }}
          variant="outlined"
          onChange={handleInputChange}
          value={formData.age}
        />
        <Button
          style={{ position: "absolute", top: "882px", left: "147px" }}
     
[truncated — 2409 more characters]
```

### backend/models.py

```python
from database import Base
from sqlalchemy import Column, Integer, String, Boolean, Float

class Plan(Base):
    __tablename__ = 'travel-plan'

    id = Column(Integer, primary_key=True, index=True)
    location = Column(String)
    duration = Column(String)
    interests = Column(String)
    age = Column(String)
    plan = Column(String)
```

### backend/database.py

```python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

URL_DATABASE = 'sqlite:///./plan.db'

engine = create_engine(URL_DATABASE, connect_args={"check_same_thread": False})

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
```

### frontend/src/api.js

```javascript
import axios from 'axios';

const api = axios.create({
    baseURL: 'http://localhost:8000'
});

export default api;
```

### frontend/src/reportWebVitals.js

```javascript
const reportWebVitals = onPerfEntry => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(onPerfEntry);
      getFID(onPerfEntry);
      getFCP(onPerfEntry);
      getLCP(onPerfEntry);
      getTTFB(onPerfEntry);
    });
  }
};

export default reportWebVitals;

```

### frontend/src/index.css

```css
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}


html, body {
  height: 100%;
  
}
```

### frontend/src/App.css

```css
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

html, body {
  height: 100%;
  overflow-y: auto; /* This allows for vertical scrolling */
}


@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

```

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