# Project export: Munch Match - Stateful-AI-Based Tinder for Restaurants

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 12.0
- Tagline: Tired of not knowing where to eat? Look no further! With MunchMatch, you can swipe-right swipe-left to your hearts content, and develop your own unique taste profile crafted by a stateful AI agent.
- Devpost: https://devpost.com/software/munch-match-stateful-ai-based-tinder-for-restaurants
- GitHub: https://github.com/ninichyu/calhacks12
- Video: https://www.youtube.com/embed/4nbq-_8JxTY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Angie Wang (19 commits), connie !! (1 commits)

## Devpost submission (written by the team)

### Inspiration

We wanted to create a web app for people who have trouble finding new food and cuisines to try, and for those who are too busy to cook their own food but want to expand their palettes.

### What it does

Munch Match allows users to swipe left or right on food locations near them, swiping right if they like it and left if they’re uninterested. Based on their swipes, our web app will recommend more locations that are similar to the liked restaurants and adapt accordingly. It also has a map marking all the restaurants you’ve liked, showing you which location it is and where it is relative to you.

### How we built it

We used a React.js framework to build the full-stack web app connecting to a PostgreSQL database through the Supabase API. We began by constructing a basic .html and .js site that filtered through a sample set of restaurant data with a basic interface, restaurant information, semi-working images, and two buttons. We then built on this foundation for the remaining duration, implementing Letta AI to adapt and push out restaurant recommendations dependent on the user's feedback. After, we made a map section for the liked restaurants to see each of the locations, as well as a page to see which restaurants the user as liked.

### Challenges we ran into

One of the primary challenges was the wifi issue that impacted our ability to collaborate. Instead, we utilized the time where the wifi was down to focus our efforts on ideation, brainstorming, and outlining our project plan. We also struggled with database management as it was most of our first times working with databases. After hours of debugging and reading documentation, we were able to have reliable loading image files drawn from the Yelp public database.

### Accomplishments we're proud of

We are proud to finish our first real hackathon project. Our team has spent a lot of time on building our product and knowing that we built something that was functional and useful is amazing.

### What we learned

We learned a lot about how to collaborate as a team. It was difficult at first to decide on an idea that everyone was passionate about and find ways to split up tasks, but it ended up being very fun! We also learned to quickly adapt and learn things we previously haven't even heard of or interacted with (i.e. Supabase and AI implementation).

### What's next

We’d like to add a section where users can connect with their contacts on their phone, allowing the contacts to see which restaurants they’ve liked, creating a gateway for a social aspect on the web app. From there, the users can make plans with their contacts to go and try those new restaurants together if they choose to.

## README (from the GitHub repository)

# calhacks12

## Detected evidence (automated analysis)

Indexed codebase: 16 recognized source files, 89 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (19 of 19)

```
.env
index.html
package.json
README.md
src/App.jsx
src/components/CardStack.jsx
src/components/LikedRestaurants.jsx
src/components/Login.jsx
src/components/RestaurantMapView.jsx
src/components/SwipeCard.jsx
src/data/restaurants.json
src/main.jsx
src/pages/Home.jsx
src/services/lettaAI.js
src/services/lettaService.js
src/services/supabase.js
src/styles/globals.css
src/styles/Login.css
vite.config.js
```

### Dependencies

- package.json: @letta-ai/letta-client@^0.0.68665, @supabase/supabase-js@^2.76.1, @vitejs/plugin-react@^5.1.0, react@^19.2.0, react-dom@^19.2.0, vite@^7.1.12

### Recent commits (newest first)

- yay
- implemented letta ai and made nicer UI for cardstacks
- map view
- UI design for app
- app interface multi-page
- fixing merge
- AI IMPLEMENTED
- made carousel loop infinitely
- connected swipes table to restaurant_ids and created multiple image carousel
- commit message
- sign in ui background
- picture
- PICTURE
- Login Page Styling
- Updated Login UI
- fixed categories parsing error
- attaching restaurants to database
- database works??
- trying to fix swipe database
- Fixed duplicate users with the same email

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

### package.json

```
{
  "name": "calhacks12",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ninichyu/calhacks12.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ninichyu/calhacks12/issues"
  },
  "homepage": "https://github.com/ninichyu/calhacks12#readme",
  "dependencies": {
    "@letta-ai/letta-client": "^0.0.68665",
    "@supabase/supabase-js": "^2.76.1",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^5.1.0",
    "vite": "^7.1.12"
  }
}

```

### src/main.jsx

```javascript
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### src/App.jsx

```javascript
import React, { useState, useEffect } from "react";
import Login from "./components/Login";
import CardStack from "./components/CardStack";
import LikedRestaurants from "./components/LikedRestaurants";
import RestaurantMapView from "./components/RestaurantMapView";
import { supabase } from "./services/supabase";

function App() {
  const [restaurants, setRestaurants] = useState([]);
  const [userId, setUserId] = useState(null);
  const [loading, setLoading] = useState(true);
  const [currentPage, setCurrentPage] = useState("swipe"); // "swipe", "liked", or "map"

  useEffect(() => {
    const fetchRestaurants = async () => {
      // Fetch all restaurants with photo_ids
      const { data, error } = await supabase
        .from("restaurant")
        .select("*")
        .not("photo_ids", "is", null)
        .neq("photo_ids", "");

      if (error) {
        console.error("Error fetching restaurants:", error);
        setLoading(false);
        return;
      }

      console.log(`Loaded ${data.length} total restaurants`);

      // Get all restaurants this user has already swiped on (seen)
      const { data: seenSwipes } = await supabase
        .from("swipes")
        .select("restaurant_id")
        .eq("user_id", userId);

      const seenRestaurantIds = new Set(
        seenSwipes ? seenSwipes.map(s => s.restaurant_id) : []
      );

      console.log(`User has already seen ${seenRestaurantIds.size} restaurants`);

      // Filter out restaurants the user has already seen
      const unseenRestaurants = data.filter(
        r => !seenRestaurantIds.has(r.business_id)
      );

      console.log(`Showing ${unseenRestaurants.length} new restaurants`);

      setRestaurants(unseenRestaurants);
      setLoading(false);
    };

    if (userId) {
      fetchRestaurants();
    }
  }, [userId]);

  if (!userId) return <Login onLogin={setUserId} />;
  
  return (
    <div style={{ 
      minHeight: "100vh",
      background: "linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)",
      padding: "0",
      fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif"
    }}>
      {/* Header */}
      <div style={{
        background: "linear-gradient(90deg, #FF6B6B 0%, #C44569 50%, #8B5CF6 100%)",
        padding: "20px 40px",
        boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
        position: "sticky",
        top: 0,
        zIndex: 1000
      }}>
        <div style={{
          maxWidth: "1200px",
          margin: "0 auto",
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          flexWrap: "wrap",
          gap: "20px"
        }}>
          {/* Logo */}
          <h1 style={{ 
            margin: 0,
            fontSize: "32px",
            fontWeight: "800",
            color: "white",
            textShadow: "2px 2px 4px rgba(0,0,0,0.3)",
            letterSpacing: "1px"
          }}>
            🍴 Munch Match
          </h1>
          
          {/* Navigation */}
          <div style={{ 
            display: "flex", 
            gap: "12px",
            backgroundColor: "rgba(255,255,255,0.15)",
            padding: "6px",
            borderRadius: "50px",
            backdropFilter: "blur(10px)"
          }}>
            <button
              onClick={() => setCurrentPage("swipe")}
              style={{
                padding: "12px 28px",
                backgroundColor: currentPage === "swipe" 
                  ? "rgba(255,255,255,0.95)" 
                  : "transparent",
                color: currentPage === "swipe" ? "#C44569" : "white",
                border: "none",
                borderRadius: "50px",
                cursor: "pointer",
                fontSize: "16px",
                fontWeight: "600",
                transition: "all 0.3s ease",
                boxShadow: currentPage === "swipe" 
                  ? "0 4px 15px rgba(0,0,0,0.2)" 
                  : "none"
              }}
            >
              🔥 Swipe
            </button>
            <button
              onClick={() => setCurrentPage("liked")}
              style={{
                padding: "12px 28px",
                backgroundColor: currentPage === "liked" 
                  ? "rgba(255,255,255,0.95)" 
                  : "transparent",
                color: currentPage === "liked" ? "#C44569" : "white",
                border: "none",
                borderRadius: "50px",
                cursor: "pointer",
                fontSize: "16px",
                fontWeight: "600",
                transition: "all 0.3s ease",
                boxShadow: currentPage === "liked" 
                  ? "0 4px 15px rgba(0,0,0,0.2)" 
                  : "none"
              }}
            >
              💚 Liked
            </button>
            <button
              onClick={() => setCurrentPage("map")}
              style={{
                padding: "12px 28px",
                backgroundColor: currentPage === "map" 
                  ? "rgba(255,255,255,0.95)" 
                  : "transparent",
                color: currentPage === "map" ? "#C44569" : "white",
                border: "none",
                borderRadius: "50px",
                cursor: "pointer",
                fontSize: "16px",
                fontWeight: "600",
                transition: "all 0.3s ease",
                boxShadow: currentPage === "map" 
                  ? "0 4px 15px rgba(0,0,0,0.2)" 
                  : "none"
              }}
            >
              🗺️ Map
            </button>
          </div>
        </div>
      </div>

      {/* Content */}
      <div style={{
        maxWidth: currentPage === "map" ? "100%" : "1200px",
        margin: "0 auto",
        padding: currentPage === "map" ? "0" : "30px 20px"
      }}>
        {currentPage === "swipe" && (
          <div style={{
            backgroundColor: "rgba(255,255,255,0.95)",
           
[truncated — 2770 more characters]
```

### vite.config.js

```javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});

```

### index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Munch Match</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### src/components/SwipeCard.jsx

```javascript
import React from "react";

export default function SwipeCard({ restaurant }) {
  return (
    <div className="swipe-card">
      <img src={restaurant.image_url} alt={restaurant.name} />
      <div className="card-info">
        <h2>{restaurant.name}</h2>
        <p>{restaurant.rating} ⭐ • {restaurant.price || "$$"} • {restaurant.location.city}</p>
      </div>
    </div>
  );
}

```

### src/styles/globals.css

```css
body {
  font-family: system-ui, sans-serif;
  background-color: #fefefe;
  margin: 0;
  text-align: center;
}

.swipe-card {
  width: 300px;
  margin: 20px auto;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

.swipe-card img {
  width: 100%;
  height: 250px;
  object-fit: cover;
}

.card-info {
  padding: 12px;
}

.buttons button {
  margin: 10px;
  font-size: 20px;
  cursor: pointer;
}

```

### src/services/supabase.js

```javascript
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = "https://ssodzocvvyhzhuisaqtg.supabase.co";
const supabaseKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNzb2R6b2N2dnloemh1aXNhcXRnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjEzNTEwMDAsImV4cCI6MjA3NjkyNzAwMH0.WXRufKrFgt4kLqPzpKPx4w44cO0IsxFu6xtdLDQELHw";
export const supabase = createClient(supabaseUrl, supabaseKey);

// Sign up
export const signUp = async (email, password) =>
  await supabase.auth.signUp({ email, password });

// Sign in
export const signIn = async (email, password) =>
  await supabase.auth.signInWithPassword({ email, password });

// Get current user
export const getUser = () => supabase.auth.getUser();

```

### src/pages/Home.jsx

```javascript
import React, { useEffect, useState } from "react";
import { fetchRestaurants } from "../services/yelpService";
import CardStack from "../components/CardStack";
import MapView from "../components/MapView";

export default function Home() {
  const [restaurants, setRestaurants] = useState([]);
  const [current, setCurrent] = useState(0);

  useEffect(() => {
    fetchRestaurants("San Francisco").then(setRestaurants);
  }, []);

  const currentRestaurant = restaurants[current];

  return (
    <div className="home">
      <h1>🍴 Munch Match</h1>
      <CardStack restaurants={restaurants} onSwipe={() => setCurrent(i => i + 1)} />
      {currentRestaurant && (
        <MapView
          lat={currentRestaurant.coordinates.latitude}
          lon={currentRestaurant.coordinates.longitude}
        />
      )}
    </div>
  );
}

```

### src/components/Login.jsx

```javascript
import React, { useState } from "react";
import { supabase } from "../services/supabase";
import "../styles/Login.css";

export default function Login({ onLogin }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [isSignUp, setIsSignUp] = useState(false);

  // Helper: ensure user exists in custom table
  const ensureUserInTable = async (user) => {
    if (!user) return;
    const { error } = await supabase
      .from("users")
      .upsert([{ id: user.id, email: user.email }], { onConflict: ["id"] });
    if (error) console.error("Failed to insert/update user:", error.message);
  };

  // Sign up new user
  const handleSignUp = async () => {
    setLoading(true);
    try {
      // First, check if user already exists in custom users table
      const { data: existingUsers, error: checkError } = await supabase
        .from("users")
        .select("*")
        .eq("email", email);

      if (checkError) throw checkError;

      if (existingUsers && existingUsers.length > 0) {
        // User already exists — try signing them in instead
        alert("User already exists, signing you in...");
        setLoading(false);
        return await handleSignIn(); // Call sign-in instead of creating duplicate
      }

      // Proceed with Supabase Auth signup
      const { data, error } = await supabase.auth.signUp({ email, password });
      if (error) throw error;

      const user = data.user;
      if (!user) throw new Error("User creation failed");

      // Insert into users table for foreign key use
      await ensureUserInTable(user);

      onLogin(user.id);
    } catch (err) {
      alert("Sign-up failed: " + err.message);
    } finally {
      setLoading(false);
    }
  };

  // Sign in existing user
  const handleSignIn = async () => {
    setLoading(true);
    try {
      const { data, error } = await supabase.auth.signInWithPassword({ email, password });
      if (error) throw error;

      const user = data.user;
      if (!user) throw new Error("Sign-in failed");

      await ensureUserInTable(user);
      onLogin(user.id);
    } catch (err) {
      alert("Sign-in failed: " + err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleAuth = async () => {
    if (isSignUp) {
      await handleSignUp();
    } else {
      await handleSignIn();
    }
  };

  return (
    <div className="login-container">
      {/* Floating kitchen utensil icons */}
      <div className="floating-utensil">🍴</div>
      <div className="floating-utensil">🥄</div>
      <div className="floating-utensil">🍴</div>
      <div className="floating-utensil">🥄</div>
      <div className="floating-utensil">🍴</div>
      <div className="floating-utensil">🥄</div>
      <div className="floating-utensil">🍴</div>
      <div className="floating-utensil">🥄</div>
      
      <div className="login-card">
        <div className="login-header">
          <h2>Munch Match</h2>
          <p>{isSignUp ? "Create your account" : "Sign in to start swiping"}</p>
        </div>

        <div className="login-form">
          <div className="input-group">
            <label htmlFor="email">Email Address</label>
            <input
              id="email"
              type="email"
              placeholder="Enter your email"
              value={email}
              onChange={e => setEmail(e.target.value)}
              className="login-input"
              disabled={loading}
            />
          </div>

          <div className="input-group">
            <label htmlFor="password">Password</label>
            <input
              id="password"
              type="password"
              placeholder="Enter your password"
              value={password}
              onChange={e => setPassword(e.target.value)}
              className="login-input"
              disabled={loading}
            />
          </div>

          <button
            onClick={handleAuth}
            className="login-button"
            disabled={loading || !email || !password}
          >
            {loading ? (
              <div className="loading-spinner">
                <div className="spinner"></div>
                Processing...
              </div>
            ) : (
              isSignUp ? "Create Account" : "Sign In"
            )}
          </button>

          <div className="login-divider">
            <span>or</span>
          </div>

          <div className="toggle-section">
            <p>
              {isSignUp ? "Already have an account?" : "Don't have an account?"}
              <button
                type="button"
                onClick={() => setIsSignUp(!isSignUp)}
                className="toggle-button"
                disabled={loading}
              >
                {isSignUp ? "Sign In" : "Sign Up"}
              </button>
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}
```

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