# Project export: PawScout

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: CruzHacks 2026
- Tagline: Smarter matching for faster reunions.
- Devpost: https://devpost.com/software/paw-scout
- GitHub: https://github.com/SpeedRelativity/cruzhacks-pet-finder
- Video: https://www.youtube.com/embed/C3IEU7MDVBM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Ethan Liu (7 commits), SpeedRelativity (6 commits), Sadia (6 commits), Danceshari (3 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/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 `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/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) - 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/app/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

Indexed codebase: 37 recognized source files, 210 KB.
- CSS (language) — detected in the code
- FastAPI (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
- AWS (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- MongoDB (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (45 of 45)

```
.gitignore
.gitignore_backup
backend/ai_service.py
backend/cleanup_matches.py
backend/email_service.py
backend/image_scraper.py
backend/main.py
backend/models.py
backend/populate_gallery.py
backend/requirements.txt
backend/s3_config.py
eslint.config.mjs
next.config.ts
package.json
postcss.config.mjs
README.md
src/app/api/auth/[auth0]/route.ts
src/app/api/health/route.ts
src/app/auth0-demo/page.tsx
src/app/gallery/page.tsx
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/app/swipe/page.tsx
src/app/upload/page.tsx
src/components/Alerts.tsx
src/components/Badge.tsx
src/components/Dashboard.tsx
src/components/DiscoveryMap.tsx
src/components/Gallery.tsx
src/components/HeroSection.tsx
src/components/LoginButton.tsx
src/components/LogoutButton.tsx
src/components/Navigation.tsx
src/components/Profile.tsx
src/components/SwipeCard.tsx
src/components/SwipeDeck.tsx
src/components/SwipeMatch.tsx
src/components/WhiskerLoader.tsx
src/lib/auth0.ts
src/lib/mockData.ts
src/proxy.ts
tailwind.config.mjs
test_upload.py
tsconfig.json
```

### Dependencies

- backend/requirements.txt: beanie@>=1.23.6, boto3@>=1.29.7, fastapi@>=0.104.1, google-generativeai@>=0.3.2, motor@>=3.3.2, pydantic@>=2.10.0,<3.0.0, python-dotenv@>=1.0.0, uvicorn[standard]@>=0.24.0
- package.json: @auth0/nextjs-auth0@^4.14.0, @tailwindcss/postcss@^4.1.18, @types/node@^20, @types/react@^19, @types/react-dom@^19, clsx@^2.1.1, eslint@^9, eslint-config-next@16.1.3, framer-motion@^12.26.2, lucide-react@^0.562.0, next@16.1.3, react@19.2.3, react-dom@19.2.3, tailwind-merge@^3.4.0, tailwindcss@^4.1.18, typescript@^5

### Recent commits (newest first)

- Frontend: Redesign dashboard with 3-step process, articles section, gallery page, and local images
- Name of App
- Pawnder
- navbar
- Fixed Home Page, Navigation Bar
- Merge branch 'frontend' of https://github.com/SpeedRelativity/cruzhacks-pet-finder into frontend
- added new gallery images and made major changes to matchup
- added email notification
- matching logic works
- Update UI theme and fix bugs: match homepage warm theme across all pages, fix navigation, improve UX
- fixed login
- linked some items
- feat: add pet gallery, navigation, and swipe match components
- Delete venv directory
- feat: Complete pet upload form with details, S3 integration, and MongoDB storage
- Update models.py
- Update main.py
- Update ai_service.py
- Add backend folder with AI and MongoDB
- Add backend folder

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

### package.json

```
{
  "name": "petfind",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@auth0/nextjs-auth0": "^4.14.0",
    "clsx": "^2.1.1",
    "framer-motion": "^12.26.2",
    "lucide-react": "^0.562.0",
    "next": "16.1.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "tailwind-merge": "^3.4.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.18",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.3",
    "tailwindcss": "^4.1.18",
    "typescript": "^5"
  }
}

```

### backend/requirements.txt

```
fastapi>=0.104.1
uvicorn[standard]>=0.24.0
motor>=3.3.2
beanie>=1.23.6
python-dotenv>=1.0.0
google-generativeai>=0.3.2
boto3>=1.29.7
pydantic>=2.10.0,<3.0.0

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Auth0Provider } from "@auth0/nextjs-auth0/client";
import "./globals.css";

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

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

export const metadata: Metadata = {
  title: "Pet Finder - Auth0",
  description: "AI-powered pet finder with Auth0 authentication",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <Auth0Provider>
          {children}
        </Auth0Provider>
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
'use client';

import { useState, useEffect, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { Navigation } from '../components/Navigation';
import { Dashboard } from '../components/Dashboard';
import { SwipeMatch } from '../components/SwipeMatch';
import { Alerts } from '../components/Alerts';
import { Gallery } from '../components/Gallery';

function AppContent() {
  const searchParams = useSearchParams();
  const [currentPage, setCurrentPage] = useState<'dashboard' | 'swipe' | 'alerts' | 'gallery'>('dashboard');

  // Handle URL query parameter for navigation
  useEffect(() => {
    const pageParam = searchParams.get('page');
    if (pageParam && ['dashboard', 'swipe', 'alerts', 'gallery'].includes(pageParam)) {
      setCurrentPage(pageParam as 'dashboard' | 'swipe' | 'alerts' | 'gallery');
    }
  }, [searchParams]);

  // Also handle custom events for backward compatibility
  useEffect(() => {
    const handleNavigate = (e: CustomEvent) => {
      if (e.detail === 'matches' || e.detail === 'swipe') {
        setCurrentPage('swipe');
      }
    };
    
    window.addEventListener('navigateToMatches' as any, handleNavigate as EventListener);
    return () => {
      window.removeEventListener('navigateToMatches' as any, handleNavigate as EventListener);
    };
  }, []);

  return (
    <div className="min-h-screen w-full overflow-x-hidden bg-background">
      <Navigation currentPage ={currentPage} onNavigate={setCurrentPage} />
      
      {currentPage === 'dashboard' && <Dashboard />}
      {currentPage === 'swipe' && <SwipeMatch />}
      {currentPage === 'gallery' && <Gallery />}
      {currentPage === 'alerts' && <Alerts />}
    </div>
  );
}

export default function App() {
  return (
    <Suspense fallback={
      <div className="min-h-screen w-full flex items-center justify-center bg-background">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
          <p className="text-muted-foreground">Loading...</p>
        </div>
      </div>
    }>
      <AppContent />
    </Suspense>
  );
}

```

### backend/main.py

```python
import os
import time
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
from beanie import init_beanie
from dotenv import load_dotenv
from uuid import uuid4
import json

from models import PetReport, PetTags, UserInfo, PetMatch
from ai_service import analyze_pet_image
from s3_config import upload_to_s3
from email_service import send_match_notification
from typing import Optional, List
from datetime import datetime

load_dotenv()
app = FastAPI(title="Pet Finder API", version="1.0.0")

# #region agent log
_DEBUG_LOG_PATH = "/home/necharkc/cruzhack/.cursor/debug.log"
def _log_debug(payload):
    try:
        os.makedirs(os.path.dirname(_DEBUG_LOG_PATH), exist_ok=True)
        with open(_DEBUG_LOG_PATH, "a", encoding="utf-8") as f:
            f.write(json.dumps(payload) + "\n")
    except Exception as e:
        # Fallback visibility if file write fails
        print(f"DEBUG_LOG_WRITE_FAILED path={_DEBUG_LOG_PATH} error={type(e).__name__}:{str(e)[:120]}")
# #endregion agent log

@app.on_event("startup")
async def _log_startup():
    # #region agent log
    _log_debug({
        "sessionId": "debug-session",
        "runId": "pre-fix",
        "hypothesisId": "H5",
        "location": "main.py:startup",
        "message": "FastAPI startup",
        "data": {"cwd": os.getcwd()},
        "timestamp": int(time.time() * 1000)
    })
    # #endregion agent log

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3001"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Mock AI response for testing (when Gemini API is unavailable)
def get_mock_ai_response(filename: str) -> dict:
    """Return mock AI analysis for testing S3 upload flow"""
    return {
        "species": "Dog",
        "breed": "Golden Retriever",
        "primary_color": "Golden",
        "age_group": "Adult",
        "marks": ["Fluffy coat", "Long ears"],
        "size": "Large"
    }

async def get_ai_tags(image_bytes: bytes, mime_type: str = "image/jpeg"):
    """Analyze image with Gemini AI. Only uses mock if API key is completely missing."""
    # Check if API key exists before trying
    api_key = os.getenv("GEMINI_API_KEY")
    # #region agent log
    _log_debug({
        "sessionId": "debug-session",
        "runId": "pre-fix",
        "hypothesisId": "H1",
        "location": "main.py:get_ai_tags:entry",
        "message": "get_ai_tags called",
        "data": {"image_bytes_len": len(image_bytes) if image_bytes else 0, "mime_type": mime_type, "api_key_present": bool(api_key), "api_key_len": len(api_key) if api_key else 0},
        "timestamp": int(time.time() * 1000)
    })
    # #endregion agent log
    if not api_key or api_key == "your_gemini_api_key_here" or len(api_key) < 10:
        print("⚠️ GEMINI_API_KEY not configured properly. Using mock response.")
        # #region agent log
        _log_debug({
            "sessionId": "debug-session",
            "runId": "pre-fix",
            "hypothesisId": "H1",
            "location": "main.py:get_ai_tags:mock_branch",
            "message": "Using mock response due to API key check",
            "data": {"api_key_present": bool(api_key), "api_key_len": len(api_key) if api_key else 0},
            "timestamp": int(time.time() * 1000)
        })
        # #endregion agent log
        return get_mock_ai_response("")
    
    # Try real AI analysis - don't catch errors, let them propagate
    print(f"🤖 Attempting Gemini AI analysis with {len(image_bytes)} bytes...")
    # #region agent log
    _log_debug({
        "sessionId": "debug-session",
        "runId": "pre-fix",
        "hypothesisId": "H2",
        "location": "main.py:get_ai_tags:before_analyze",
        "message": "Calling analyze_pet_image",
        "data": {"image_bytes_len": len(image_bytes) if image_bytes else 0, "mime_type": mime_type},
        "timestamp": int(time.time() * 1000)
    })
    # #endregion agent log
    result = await analyze_pet_image(image_bytes, mime_type)
    print(f"✅ AI analysis successful: {result.get('species')} - {result.get('breed')}")
    # #region agent log
    _log_debug({
        "sessionId": "debug-session",
        "runId": "pre-fix",
        "hypothesisId": "H3",
        "location": "main.py:get_ai_tags:after_analyze",
        "message": "analyze_pet_image returned",
        "data": {"species": result.get("species"), "breed": result.get("breed")},
        "timestamp": int(time.time() * 1000)
    })
    # #endregion agent log
    return result


def calculate_match_score(tags1: PetTags, tags2: PetTags) -> tuple[int, List[str]]:
    """
    Calculate match score between two pet tags.
    Returns (score, matched_tags) where score is 0-3 and matched_tags are the matching tag names.
    """
    matched_tags = []
    score = 0
    
    # Compare species
    if tags1.species.lower() == tags2.species.lower():
        score += 1
        matched_tags.append("species")
    
    # Compare breed
    if tags1.breed.lower() == tags2.breed.lower():
        score += 1
        matched_tags.append("breed")
    
    # Compare primary color
    if tags1.primary_color.lower() == tags2.primary_color.lower():
        score += 1
        matched_tags.append("primary_color")
    
    return score, matched_tags


async def find_matches(new_report: PetReport):
    """
    Find matching reports when a new report is created.
    If new report is 'Found', match with 'Lost' reports.
    If new report is 'Lost', match with 'Found' reports.
    Create PetMatch records for matches with score >= 3 (perfect matches).
    """
    try:
        # Determine which type of reports to search
        search_type = "Lost" if new_report.report_type == "Found" else "Found"
        
        # Find reports of opposite type
        candidate_reports = await PetReport.find({
      
[truncated — 21652 more characters]
```

### src/app/gallery/page.tsx

```typescript
'use client';

import { useState } from 'react';
import { Navigation } from '@/components/Navigation';
import { Gallery } from '@/components/Gallery';

export default function GalleryPage() {
  const [currentPage, setCurrentPage] = useState<'dashboard' | 'swipe' | 'alerts' | 'gallery'>('gallery');

  return (
    <div className="min-h-screen w-full overflow-x-hidden bg-background">
      <Navigation currentPage={currentPage} onNavigate={setCurrentPage} />
      <Gallery />
    </div>
  );
}

```

### src/app/swipe/page.tsx

```typescript
import { SwipeDeck } from '@/components/SwipeDeck';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';

export default function SwipePage() {
    return (
        <main className="flex min-h-screen flex-col items-center bg-black text-white p-4">
            {/* Header */}
            <header className="w-full max-w-lg flex items-center justify-between py-6">
                <Link href="/" className="p-2 hover:bg-gray-800 rounded-full transition">
                    <ArrowLeft size={24} />
                </Link>
                <h1 className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500">
                    Find Your Buddy
                </h1>
                <div className="w-10" /> {/* Spacer for centering */}
            </header>

            {/* Deck */}
            <div className="flex-1 flex items-center justify-center w-full max-w-lg">
                <SwipeDeck />
            </div>

            {/* Footer Instructions */}
            <div className="mb-8 text-center text-gray-500 text-sm">
                <p>Swipe <span className="text-green-500 font-bold">RIGHT</span> if this is your pet</p>
                <p>Swipe <span className="text-red-500 font-bold">LEFT</span> to skip</p>
            </div>
        </main>
    );
}

```

### src/app/auth0-demo/page.tsx

```typescript
import { auth0 } from "@/lib/auth0";
import LoginButton from "@/components/LoginButton";
import LogoutButton from "@/components/LogoutButton";
import Profile from "@/components/Profile";

export default async function Auth0DemoPage() {
    const session = await auth0.getSession();
    const user = session?.user;

    return (
        <div className="min-h-screen flex flex-col items-center justify-center p-4">
            <div className="w-full max-w-md bg-card rounded-2xl shadow-primary p-8 flex flex-col items-center animate-fade-in relative overflow-hidden">

                {/* Decorative background blur */}
                <div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-primary via-secondary to-accent" />

                <img
                    src="https://cdn.auth0.com/quantum-assets/dist/latest/logos/auth0/auth0-lockup-en-ondark.png"
                    alt="Auth0 Logo"
                    className="w-40 mb-8 opacity-90"
                />

                <h1 className="text-3xl font-bold mb-2 text-center font-heading text-foreground">
                    Welcome Back
                </h1>
                <p className="text-muted-foreground text-center mb-8">
                    Sign in to manage your lost & found reports
                </p>

                <div className="w-full flex flex-col gap-4">
                    {user ? (
                        <div className="flex flex-col items-center w-full gap-6">
                            <div className="bg-primary/10 text-primary-foreground px-4 py-2 rounded-full font-semibold flex items-center gap-2">
                                ✅ Successfully logged in
                            </div>
                            <Profile />
                            <div className="w-full">
                                <LogoutButton />
                            </div>
                        </div>
                    ) : (
                        <div className="w-full flex flex-col gap-4">
                            <LoginButton />
                            <p className="text-xs text-center text-muted-foreground mt-4">
                                By continuing, you agree to our Terms of Service and Privacy Policy.
                            </p>
                        </div>
                    )}
                </div>
            </div>
        </div>
    );
}

```

### src/app/api/health/route.ts

```typescript
import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({
    status: 'ok',
    message: 'Backend logic is working!',
    timestamp: new Date().toISOString(),
  });
}

```

### src/app/upload/page.tsx

```typescript
'use client';

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { ArrowLeft, UploadCloud, Loader, X, CheckCircle2, ChevronLeft, ChevronRight } from 'lucide-react';
import { useState, useRef } from 'react';

type UploadStatus = 'idle' | 'uploading' | 'analyzing' | 'saving' | 'complete';

export default function UploadPage() {
    const router = useRouter();
    const [loading, setLoading] = useState(false);
    const [uploadStatus, setUploadStatus] = useState<UploadStatus>('idle');
    const [progressMessage, setProgressMessage] = useState('');
    const [result, setResult] = useState<any>(null);
    const [error, setError] = useState('');
    const [reportType, setReportType] = useState<'Lost' | 'Found'>('Lost');
    const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
    const [currentImageIndex, setCurrentImageIndex] = useState(0);
    
    // Form fields
    const [petName, setPetName] = useState('');
    const [petType, setPetType] = useState('Dog');
    const [userName, setUserName] = useState('');
    const [userEmail, setUserEmail] = useState('');
    const [userPhone, setUserPhone] = useState('');
    const [userLocation, setUserLocation] = useState('');
    const [description, setDescription] = useState('');
    
    const fileInputRef = useRef<HTMLInputElement>(null);

    const handleFileSelect = (files: FileList) => {
        const newFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
        if (newFiles.length === 0) {
            setError('Please select image files');
            return;
        }
        setSelectedFiles([...selectedFiles, ...newFiles]);
        setError('');
    };

    const removeFile = (index: number) => {
        setSelectedFiles(selectedFiles.filter((_, i) => i !== index));
    };

    const handleDragDrop = (e: React.DragEvent) => {
        e.preventDefault();
        handleFileSelect(e.dataTransfer.files);
    };

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.currentTarget.files) {
            handleFileSelect(e.currentTarget.files);
        }
    };

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        
        // Validation
        if (selectedFiles.length === 0) {
            setError('Please upload at least one image');
            return;
        }
        if (!petName || !userName || !userEmail || !userPhone || !userLocation) {
            setError('Please fill in all required fields');
            return;
        }

        setLoading(true);
        setError('');
        setResult(null);
        setUploadStatus('uploading');
        setProgressMessage('Preparing images...');

        try {
            // Step 1: Prepare form data
            setProgressMessage('Preparing upload...');
            const formData = new FormData();
            
            // Add all files
            selectedFiles.forEach(file => {
                formData.append('files', file);
            });
            
            // Add form data
            formData.append('report_type', reportType);
            formData.append('pet_name', petName);
            formData.append('pet_type', petType);
            formData.append('user_name', userName);
            formData.append('user_email', userEmail);
            formData.append('user_phone', userPhone);
            formData.append('user_location', userLocation);
            formData.append('description', description);

            // Step 2: Upload to S3
            setUploadStatus('uploading');
            setProgressMessage(`Uploading ${selectedFiles.length} image(s) to cloud storage...`);

            const response = await fetch('http://localhost:8000/api/reports', {
                method: 'POST',
                body: formData,
            });

            if (!response.ok) {
                const errorData = await response.json().catch(() => ({ detail: response.statusText }));
                throw new Error(errorData.detail || `Upload failed: ${response.statusText}`);
            }

            // Step 3: AI Analysis
            setUploadStatus('analyzing');
            setProgressMessage('Analyzing pet with AI...');

            // Step 4: Saving to database
            setUploadStatus('saving');
            setProgressMessage('Saving report to database...');

            const data = await response.json();
            
            // Check if matches were created - wait a bit for backend to process
            let hasMatches = false;
            if (data.report_id) {
                try {
                    // Small delay to ensure backend has processed the match
                    await new Promise(resolve => setTimeout(resolve, 500));
                    const matchResponse = await fetch(`http://localhost:8000/api/matches?status=pending&limit=50`);
                    if (matchResponse.ok) {
                        const matchData = await matchResponse.json();
                        if (matchData.status === 'success' && matchData.matches && matchData.matches.length > 0) {
                            // Check if any match involves this report
                            hasMatches = matchData.matches.some((m: any) => 
                                m.lost_report?.report_id === data.report_id || 
                                m.found_report?.report_id === data.report_id
                            );
                        }
                    }
                } catch (err) {
                    console.error('Error checking for matches:', err);
                    // Don't fail the upload if match check fails
                }
            }
            
            setUploadStatus('complete');
            setProgressMessage('Report created successfully!');
            setResult({ ...data, hasMatches });
            console.log('Upload successful:', data);
            
            // Store hasMatches for popup display
            setResult({ ...data, hasMa
[truncated — 28230 more characters]
```

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