# Project export: SmileySpot

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: UC Berkeley AI Hackathon 2025
- Tagline: The Map that Smiles Back
- Devpost: https://devpost.com/software/smileyspot
- GitHub: https://github.com/Pranav-Pra/smiley-spotter.git
- Demo: https://smiley-spotter.vercel.app/
- Video: https://www.youtube.com/embed/VYauvK5r5Vc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Pranav Prasad (13 commits), sevenminusone (2 commits), kumarsathvik (1 commits)

## Devpost submission (written by the team)

### Inspiration

Pareidolia, the human tendency to perceive familiar patterns, particularly faces, in random or ambiguous stimuli. Research shows that moments of pareidolia can spark positive emotions like joy and curiosity. These small but meaningful emotional boosts can have a real impact on mental well-being. We wanted to create something that encourages people, especially those moving through moments of emotional heaviness or creative stillness, to explore the world around them through a more playful, mindful lens. By giving users a platform to capture, share, and track their pareidolia discoveries, we hope to promote outdoor engagement, creative thinking, and moments of instant positivity. The experience itself is therapeutic, helping users reconnect with their environment, imagination, and sense of curiosity.

### What it does

SmileySpot creates a fun, interactive platform where users can explore, capture, and celebrate pareidolia. After logging into their personalized dashboard, users are greeted with a dynamic map showing all the places they’ve previously spotted pareidolia, turning their experiences into a visual memory archive. Each “smiley” they upload is pinned to the location where it was discovered, creating a spatial diary of whimsical encounters with the world. To make the experience even more engaging, users earn points for every face they find. This subtle layer of gamification encourages curiosity and continued exploration, rewarding users for venturing outside and noticing the unnoticed. The point system fosters a sense of progress, turning each moment of perception into a small victory. Whether you’re walking through your neighborhood or traveling somewhere new, the app transforms the world into a playful canvas—one where the mundane smiles back.

### How we built it

Frontend: Next.js – React framework for building the UI and routing TypeScript – For type safety and better developer experience Tailwind CSS – Utility-first CSS framework for styling React Leaflet – For rendering interactive maps Google Places API – For address autocomplete and geocoding Backend: Next.js API Routes – handle authentications, database operations Gemini API (Google) – For AI-based smile/facial resemblance scoring Database: MongoDB Atlas – Cloud-hosted database for storing user accounts, pins, and scores MongoDB Node.js Driver – To interact with MongoDB in API routes Deployment: Vercel – Hosting platform for both frontend and backend

### Challenges we ran into

One of the most challenging parts of building this app was developing a reliable way to score how much an object resembled a face—a key part of gamifying the pareidolia experience. We started by experimenting with the “Faces in Things” library, which initially seemed promising but lacked the flexibility we needed for nuanced control and consistent results. The scoring felt binary or brittle, and it didn’t support our criteria for excluding real human faces, animals, or drawn illustrations. After running into these limitations, we shifted to Google’s Gemini API, which allowed us to iterate on prompts and fine-tune a system that could return a continuous score from 0 to 10 based on how strongly a given image evoked a face. This process involved a lot of iterative prompt engineering, testing edge cases, and tweaking constraints to ensure the model was scoring with both precision and the playful spirit of the app.

### Accomplishments we're proud of

We’re most proud of bringing such a unique and emotionally-driven idea to life within a short time span. The concept of mapping pareidolia—seeing faces in everyday objects—posed both creative and technical challenges, and we leaned into both. Along the way, we pushed ourselves to work with unfamiliar tools and technologies: from deploying a full-stack app on Vercel, to storing structured user data with MongoDB, to using Gemini for iterative prompt engineering and image scoring.

### What we learned

How to structure a full-stack app using Next.js, MongoDB, and Vercel for seamless deployment How to debug and integrate APIs like Google Maps and Places Autocomplete to create a more immersive user experience

### What's next

We envision evolving this app from a personal archive into a shared, community-driven experience. In future versions, we’d love to introduce: A public pareidolia map — a collective archive where everyone’s smiley discoveries appear, allowing users to explore the whimsical faces others have spotted around the world A public pareidolia map — a collective archive where everyone’s smiley discoveries appear, allowing users to explore the whimsical faces others have spotted around the world Location-based exploration — imagine walking through a city and seeing nearby pareidolia pins others have left behind, turning everyday walks into playful scavenger hunts Location-based exploration — imagine walking through a city and seeing nearby pareidolia pins others have left behind, turning everyday walks into playful scavenger hunts Leaderboards and point systems — to gamify the experience further, we plan to add global and local rankings based on smile scores, encouraging friendly competition and engagement Leaderboards and point systems — to gamify the experience further, we plan to add global and local rankings based on smile scores, encouraging friendly competition and engagement Ultimately, we want this project to become a breathing map of wonder—part game, part gallery.

## README (from the GitHub repository)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.

This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

Indexed codebase: 19 recognized source files, 29 KB.
- CSS (language) — detected in the code
- Google Gemini (technology) — detected in the code
- MongoDB (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (23 of 23)

```
.gitignore
next.config.ts
package.json
postcss.config.mjs
README.md
src/components/map.tsx
src/lib/mongodb.ts
src/pages/_app.tsx
src/pages/_document.tsx
src/pages/api/add-smiley.ts
src/pages/api/auth/login.ts
src/pages/api/auth/signup.ts
src/pages/api/smile-detection-gemini.ts
src/pages/api/smile-detection.py
src/pages/api/users/pins.ts
src/pages/api/users/points.ts
src/pages/camera.tsx
src/pages/dashboard.tsx
src/pages/index.tsx
src/pages/photo.tsx
src/pages/signup.tsx
src/styles/globals.css
tsconfig.json
```

### Dependencies

- package.json: @auth/mongodb-adapter@^3.9.1, @google/generative-ai@^0.24.1, @tailwindcss/postcss@^4, @types/jsonwebtoken@^9.0.10, @types/leaflet@^1.9.18, @types/node@^20, @types/react@^19, @types/react-dom@^19, @types/react-places-autocomplete@^7.2.14, bcryptjs@^3.0.2, jsonwebtoken@^9.0.2, leaflet@^1.9.4, mongodb@^6.17.0, next@15.3.4, next-auth@^4.24.11, react@^19.0.0, react-dom@^19.0.0, react-leaflet@^5.0.0, react-places-autocomplete@^7.3.0, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- added some comments, changed some ui
- pins display images
- Pins display on the map
- Points screen updates, fixed auth bugs
- gemini ai used to judge pareidolia from scale of 1 to 10
- added photo upload feature
- added map to dashboard
- fixed merge conflict with logo
- Merge branch 'main' of https://github.com/Pranav-Pra/smiley-spotter
- Added authentication / fixed bug
- Merge branch 'main' of https://github.com/Pranav-Pra/smiley-spotter
- add image
- Created a smile detection
- added some backend connectivity
- front-end for authentication
- created project

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

### package.json

```
{
  "name": "smiley-spotter",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@auth/mongodb-adapter": "^3.9.1",
    "@google/generative-ai": "^0.24.1",
    "bcryptjs": "^3.0.2",
    "jsonwebtoken": "^9.0.2",
    "leaflet": "^1.9.4",
    "mongodb": "^6.17.0",
    "next": "15.3.4",
    "next-auth": "^4.24.11",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-leaflet": "^5.0.0",
    "react-places-autocomplete": "^7.3.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/jsonwebtoken": "^9.0.10",
    "@types/leaflet": "^1.9.18",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@types/react-places-autocomplete": "^7.2.14",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### src/pages/index.tsx

```typescript
import Link from "next/link";
import Image from "next/image";
import { Geist, Geist_Mono } from "next/font/google";
import { useState } from "react";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});


export default function LoginPage() {

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = async (e: React.FormEvent) => {
  e.preventDefault();

  const res = await fetch("/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });

  const data = await res.json();

  if (res.ok) {
    alert("Login successful!");
    window.location.href = "/dashboard";
  } else {
    alert(`Login failed: ${data.message}`);
  }
};



  return (
    <main className="min-h-screen flex items-center justify-center bg-white px-6">
      <div className="w-full max-w-xs space-y-6">
        <div className="flex items-center justify-center space-x-3">
          <Image
            src="/yellowsmileyface.png" 
            alt="Smile Icon"
            width={64}
            height={64}
          />
          <h1 className="text-3xl font-quicksand text-center text-black">SmileySpot</h1>
        </div>

        <form onSubmit={handleLogin} className="space-y-4">
          <div>
            <label className="block text-sm text-gray-700 mb-1">Username:</label>
            <input
              type="text"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
              placeholder="Username"
              className="w-full px-3 py-2 border-b border-gray-300 focus:outline-none focus:border-black placeholder-gray-400 text-black"
            />
          </div>

          <div>
            <label className="block text-sm text-gray-700 mb-1">Password:</label>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder="Password"
              className="w-full px-3 py-2 border-b border-gray-300 focus:outline-none focus:border-black placeholder-gray-400 text-black"
            />
          </div>

          <button
            type="submit"
            className="w-full py-2 mt-2 bg-yellow-400 text-black rounded-full text-sm font-semibold"
          >
            Log In
          </button>

          <p className="text-sm text-center text-gray-600 mt-2">
            Don’t have an account?{" "}
            <Link href="/signup" className="text-blue-600 underline">
              Sign Up
            </Link>
          </p>
        </form>
      </div>
    </main>
  );
}

```

### next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  reactStrictMode: true,
};

export default nextConfig;

```

### src/pages/_app.tsx

```typescript
import "@/styles/globals.css";
import 'leaflet/dist/leaflet.css';

import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

```

### src/lib/mongodb.ts

```typescript
import { MongoClient } from 'mongodb';

const uri = process.env.MONGODB_URI!;
const options = {};

let client: MongoClient;
let clientPromise: Promise<MongoClient>;

if (!process.env.MONGODB_URI) {
  throw new Error("Please add your Mongo URI to .env.local");
}

client = new MongoClient(uri, options);
clientPromise = client.connect();

export default clientPromise;
```

### src/pages/_document.tsx

```typescript
import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html>
      <Head>
        <script
          src={`https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places`}
          async
          defer
        ></script>
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

```

### src/styles/globals.css

```css
@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

```

### src/components/map.tsx

```typescript
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import type { LatLngExpression } from "leaflet";

interface Pin {
  lat: number;
  lng: number;
  image?: string; // ✅ Add image property
}

interface MapProps {
  center: LatLngExpression;
  pins?: Pin[];
}

const smileyIcon = L.icon({
  iconUrl: "/circle button.PNG", // ✅ Make sure this is in public/
  iconSize: [40, 40],
  iconAnchor: [20, 40],
  popupAnchor: [0, -40],
});

export default function Map({ center, pins = [] }: MapProps) {
  return (
    <MapContainer
      center={center}
      zoom={13}
      scrollWheelZoom={true}
      style={{ height: "100%", width: "100%" }}
    >
      <TileLayer
        attribution='&copy; OpenStreetMap contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {pins.map((pin, idx) => (
        <Marker key={idx} position={[pin.lat, pin.lng]} icon={smileyIcon}>
          <Popup>
            {pin.image ? (
              <img
                src={pin.image}
                alt="Smiley"
                className="w-32 h-auto rounded"
              />
            ) : (
              "No image"
            )}
          </Popup>
        </Marker>
      ))}
    </MapContainer>
  );
}
```

### src/pages/dashboard.tsx

```typescript
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import type { LatLngExpression } from 'leaflet';

const Map = dynamic(() => import("@/components/map"), {
  ssr: false,
});

export default function DashboardPage() {
  const router = useRouter();
  const [loading, setLoading] = useState(true);
  const [center, setCenter] = useState<LatLngExpression>([37.7749, -122.4194]); //SF is defaultt
  const [totalPoints, setTotalPoints] = useState<number | null>(null);
  const [pins, setPins] = useState<{ lat: number; lng: number }[]>([]);

  useEffect(() => {
  const fetchPointsandPins = async () => {
    try {
      const pointsRes = await fetch("/api/users/points");
      //const pinsRes = await fetch("/api/users/points");
      const pinsRes = await fetch("/api/users/pins");
      if (!pointsRes.ok || !pinsRes.ok) throw new Error("Failed to fetch data");

      const pointsData = await pointsRes.json();
      const pinsData = await pinsRes.json();

      setTotalPoints(pointsData.totalPoints);
      setPins(pinsData.pins || []);
    } catch (err) {
      console.error("Error fetching points:", err);
    } finally {
      setLoading(false);
    }
  };

  fetchPointsandPins();
}, []);

  if (loading) return <p className="text-center p-6">Loading...</p>;

  return (
   <main className="min-h-screen bg-white relative mb-12">
  <div className="flex items-center justify-between px-6 mt-6">
    <div className="flex items-center space-x-4 text-2xl font-semibold text-black">
      <h1 className="text-2xl font-semibold text-black">Points</h1>
      <p>{totalPoints ?? "Loading..."}</p>
    </div>
    <button
      onClick={() => {
        localStorage.removeItem("token");
        router.push("/");
      }}
      className="bg-yellow-400 text-white px-2 py-1 rounded shadow text-sm hover:bg-red-600 transition"
    >
      Sign Out
    </button>
  </div>

  <div className="h-[80vh] w-full px-4 mt-4">
    <Map center={center} pins={pins}/>
  </div>

  <div className="absolute bottom-6 right-6 z-[1000]">
    <button onClick={() => router.push("/camera")}>
      <img
        src="/circle button.PNG"
        alt="Add Pin"
        className="w-20 h-20 object-contain drop-shadow-md hover:scale-105 transition-transform hover:brightness-95"
      />
    </button>
  </div>
</main>
  );
}
```

### src/pages/camera.tsx

```typescript
import { useState } from "react";
import { useRouter } from "next/router";

export default function CameraPage() {
  const router = useRouter();
  const [image, setImage] = useState<string | null>(null);

  const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];

  if (!file) return;

  // for preview
  const imageUrl = URL.createObjectURL(file);
  setImage(imageUrl);
  localStorage.setItem("previewImage", imageUrl); // 👈 new key

  // for gemini
  const reader = new FileReader();
  reader.onloadend = () => {
    const base64 = reader.result?.toString();
    if (base64) {
      localStorage.setItem("base64Image", base64); // 👈 new key
    }
  };
  reader.readAsDataURL(file);
};

// const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
//   const file = e.target.files?.[0];
//   if (file) {
//     const reader = new FileReader();
//     reader.onloadend = () => {
//       const base64String = reader.result as string;
//       localStorage.setItem("uploadedImageBase64", base64String);
//       setImage(base64String); // or image URL if needed
//     };
//     reader.readAsDataURL(file);
//   }
// };

  const handleUpload = () => {
    if (image) {
        localStorage.setItem("uploadedImage", image);
        router.push("/photo");
    }
  };

  

  return (
    <main className="min-h-screen bg-white flex flex-col items-center justify-center p-4 space-y-6">

    <button
        onClick={() => router.push("/dashboard")}
        className="absolute top-4 right-4 bg-gray-200 text-black px-2 py-1 rounded shadow hover:bg-gray-300 transition"
    >
        Back
    </button>
  
    <div className="flex flex-col items-center space-y-4">
        <h1 className="text-2xl font-semibold text-black text-center">Select an Image</h1>

        <input
        type="file"
        accept="image/*"
        capture="environment"
        onChange={handleImageChange}
        className="block text-sm text-gray-500
            file:mr-4 file:py-2 file:px-4
            file:rounded-full file:border-0
            file:text-sm file:font-semibold
            file:bg-yellow-400 file:text-black
            hover:file:bg-yellow-500"
        />
    </div>`

      {image && (
        <img
          src={image}
          alt="Preview"
          className="w-64 h-64 object-cover rounded shadow"
        />
      )}

      <button
        onClick={handleUpload}
        disabled={!image}
        className="bg-yellow-400 text-black px-6 py-2 rounded font-semibold shadow hover:bg-yellow-500 disabled:opacity-50"
      >
        Upload Image
      </button>
    </main>
  );
}
```

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