# Project export: Spritz

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 2025
- Tagline: Spritz is a web application built to help users find affordable alternatives—or "dupes"—for high-end perfumes and colognes.
- Devpost: https://devpost.com/software/spritz
- GitHub: https://github.com/paulleeisme06/CruzHacks2025
- Team: 3 GitHub contributor(s) — Bodie Feinberg (15 commits), Paul Lee (11 commits), judyladella (3 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# CruzHacks2025
Team Members: Bodie, Luis, Paul, Judy
Project Name: Spritz

# SpritzAI – Affordable Alternatives to Luxury Fragrances

**Spritz** is a web application built to help users find affordable alternatives—or "dupes"—for high-end perfumes and colognes. Aimed at students, budget-conscious shoppers, and everyday consumers who love great scents without the luxury markup, the app uses AI and web scraping to deliver personalized recommendations. Users simply paste the URL of a luxury fragrance product into the app, and it returns similar-smelling, budget-friendly options along with images, detailed information, and database-generated comparisons.

---

## How It Works

1. **User Input**: The user enters a URL of a high-end fragrance product.
2. **Web Scraping**: The backend uses **Puppeteer** to scrape the product page's HTML.
3. **Fragrance Analysis**: The HTML is sent to the **Gemini API**, which identifies the product name and classifies it into a predefined scent category (e.g., floral, woody, citrus).
4. **Database Lookup**: The backend queries a **PostgreSQL** database (running in a Docker container) to find exact or category-based dupes.
5. **AI-Powered Results**: If matches are found, they’re returned to the user along with product images, purchase links, and LLM-generated descriptions and comparisons.
   
---

## Tech Stack

### Frontend
- [React.js](https://reactjs.org/)
- [React Router](https://reactrouter.com/)
- [TailwindCSS](https://tailwindcss.com/) (for styling)

### Backend
- [Node.js](https://nodejs.org/)
- [Express.js](https://expressjs.com/)
- [Puppeteer](https://pptr.dev/)
- [Gemini API](https://deepmind.google/technologies/gemini/)
- [PostgreSQL](https://www.postgresql.org/)
- [Docker](https://www.docker.com/)
- [OpenAPI 3.0](https://swagger.io/specification/)
---
## Application Structure

- `client/`: React frontend
- `server/`: Node/Express backend
- `docker/`: Docker Compose & PostgreSQL setup
- `db/`: Database schema and dupe data

---

## 🚀 Running the App

To run the app locally:

```bash
# Stop and remove existing volumes
docker-compose -f ./docker/docker-compose.yml down --volumes

# Start PostgreSQL DB
npm run db

# Start backend server and frontend
npm run dev

#Start frontend (make sure all required components are installed, look in json file)
npm start


## Detected evidence (automated analysis)

Indexed codebase: 28 recognized source files, 549 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Firebase (technology) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- PostgreSQL (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (38 of 38)

```
.DS_Store
.gitignore
backend/app.js
backend/config/db.js
backend/docker/data.sql
backend/docker/docker-compose.yml
backend/docker/fragrance_dupes_insert_statements (1).txt
backend/docker/schema.sql
backend/package.json
backend/routes/scrapeRoutes.js
backend/server.js
backend/services/categories.js
backend/services/compound_categories_200.txt
backend/services/db.js
backend/services/format.js
backend/services/fragrance_dupes_insert_statements.txt
backend/services/gemini.js
backend/services/scraper.js
backend/swagger.yaml
README.md
spritz_test/.gitignore
spritz_test/package.json
spritz_test/public/index.html
spritz_test/public/manifest.json
spritz_test/public/robots.txt
spritz_test/README.md
spritz_test/src/App.css
spritz_test/src/App.jsx
spritz_test/src/App.test.js
spritz_test/src/components/api.jsx
spritz_test/src/components/SearchResultsPage.js
spritz_test/src/components/spritz_components.js
spritz_test/src/context/DupeContext.jsx
spritz_test/src/firebase-config.js
spritz_test/src/index.css
spritz_test/src/index.js
spritz_test/src/reportWebVitals.js
spritz_test/src/setupTests.js
```

### Dependencies

- backend/package.json: @google/generative-ai@^0.24.0, concurrently@^8.2.0, cors@^2.8.5, dotenv@^16.3.1, express@^4.19.2, nodemon@^3.1.0, pg@^8.11.5, puppeteer@^22.4.1, puppeteer-extra@^3.3.6, puppeteer-extra-plugin-stealth@^2.11.1, swagger-ui-express@^4.6.3, yamljs@^0.3.0
- spritz_test/package.json: @testing-library/dom@^10.4.0, @testing-library/jest-dom@^6.6.3, @testing-library/react@^16.3.0, @testing-library/user-event@^13.5.0, firebase@^11.6.0, react@^19.1.0, react-dom@^19.1.0, react-router-dom@^7.5.0, react-scripts@5.0.1, web-vitals@^2.1.4

### Recent commits (newest first)

- Update project name from Spritz to SpritzAI
- switched models
- got rid of testing UI
- Merge branch 'main' of https://github.com/paulleeisme06/CruzHacks2025
- tweaked LLM api prompts and parsing
- Update README.md
- MVP
- Merge branch 'main' of https://github.com/paulleeisme06/CruzHacks2025
- api merge attempt1
- Google Sign in
- fix
- draft_complete_frontend
- Merge branch 'main' of https://github.com/paulleeisme06/CruzHacks2025
- API working!
- Merge branch 'main' of github.com:paulleeisme06/CruzHacks2025
- Changed theme,and search page to 2 results
- Merge branch 'main' of https://github.com/paulleeisme06/CruzHacks2025
- Scraped more metadata, return more about dupe from API
- organize
- new changes

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

### backend/package.json

```
{
  "name": "fragrance-finder-backend",
  "version": "1.0.0",
  "description": "Express backend for fragrance dupe finder using Puppeteer, Gemini API, PostgreSQL, and Swagger UI",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "dev": "concurrently \"npm:db\" \"nodemon server.js\"",
    "db": "docker-compose -f ./docker/docker-compose.yml up --quiet-pull"
  },
  "dependencies": {
    "@google/generative-ai": "^0.24.0",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.19.2",
    "pg": "^8.11.5",
    "puppeteer": "^22.4.1",
    "puppeteer-extra": "^3.3.6",
    "puppeteer-extra-plugin-stealth": "^2.11.1",
    "swagger-ui-express": "^4.6.3",
    "yamljs": "^0.3.0"
  },
  "devDependencies": {
    "concurrently": "^8.2.0",
    "nodemon": "^3.1.0"
  },
  "keywords": [],
  "author": "Bodie Feinberg",
  "license": "ISC"
}

```

### spritz_test/package.json

```
{
  "name": "spritz_test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.3.0",
    "@testing-library/user-event": "^13.5.0",
    "firebase": "^11.6.0",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-router-dom": "^7.5.0",
    "react-scripts": "5.0.1",
    "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/docker/docker-compose.yml

```yaml
# backend/docker/docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:14
    container_name: fragrancefinder-db
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: fragrancefinder
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - ./schema.sql:/docker-entrypoint-initdb.d/1-schema.sql
      - ./data.sql:/docker-entrypoint-initdb.d/2-data.sql

```

### backend/server.js

```javascript
// server.js
import app from './app.js';

const PORT = 3000;

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
  console.log(`Swagger UI available at http://localhost:${PORT}/api-docs`);
});

```

### backend/app.js

```javascript
// app.js
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import * as scrapeRoutes from './routes/scrapeRoutes.js';

dotenv.config();

const app = express();

app.use(cors());
app.use(express.json());

// Load YAML Swagger document
// const swaggerDocument = YAML.load('./swagger.yaml');

// // Swagger UI setup
// app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

// API routes
app.post('/api/scrape', scrapeRoutes.findDupes);

// 404 handler
app.use((err, req, res, next) => {
  res.status(err.status).json({
    message: err.message,
    errors: err.errors,
    status: err.status,
  });
});

export default app;

```

### spritz_test/src/index.js

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </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();

```

### spritz_test/src/App.jsx

```javascript
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import SpritzComponent from './components/spritz_components';
import SearchResultsPage from './components/SearchResultsPage';
import {DupeProvider} from './context/DupeContext';

function App() {
  return (
    <DupeProvider>
      <Router>
        <Routes>
          <Route path="/" element={<SpritzComponent />} />
          <Route path="/searchResultsPage" element={<SearchResultsPage />} />
          <Route path="/*" element={<SearchResultsPage />} />
        </Routes>
      </Router>
    </DupeProvider>
  );
}

export default App;

```

### backend/swagger.yaml

```yaml
openapi: 3.0.0
info:
  title: Fragrance Finder API
  version: 1.0.0
  description: API documentation for the Fragrance Finder project

servers:
  - url: http://localhost:3000

paths:
  /api/scrape:
    post:
      summary: Scrape a fragrance URL and find dupes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  format: uri
                  example: "https://example.com/fragrance"
      responses:
        "200":
          description: A structured result of the scraped fragrance and its dupe
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FragranceDupeResponse"

        "400":
          description: Invalid input (e.g. malformed URL or empty body)

        "404":
          description: The URL could not be reached or scraped

        "422":
          description: Page was not determined to be a fragrance

        "500":
          description: LLM analysis failed or internal server error

components:
  schemas:
    FragranceDupeResponse:
      type: object
      properties:
        targetName:
          type: string
          example: "Ocean Storm"
        targetCategory:
          type: string
          example: "masculine"
        targetCopy:
          type: string
          example: "A bold aquatic scent inspired by the raw power of crashing waves."
        targetImage:
          type: string
          format: uri
          example: "https://example.com/images/ocean-storm.jpg"
        targetPrice:
          type: string
          example: "$89.99"
        dupeName:
          type: string
          example: "Blue Breeze"
        dupeCategory:
          type: string
          example: "masculine"
        dupeLink:
          type: string
          format: uri
          example: "https://cheapscents.com/blue-breeze"
        dupeBrand:
          type: string
          example: "BudgetScents"
        dupeCopy:
          type: string
          example: "Blue Breeze offers a crisp and clean scent at a fraction of the designer price."
        dupeImage:
          type: string
          format: uri
          example: "https://cheapscents.com/images/blue-breeze.jpg"
        dupePrice:
          type: string
          example: "$19.99"

```

### backend/docker/schema.sql

```sql
DROP TABLE IF EXISTS dupe CASCADE;

CREATE TABLE dupe (
  id UUID UNIQUE PRIMARY KEY DEFAULT gen_random_uuid(), 
  data jsonb
);
```

### backend/config/db.js

```javascript
// backend/config/db.js
import pg from 'pg';
import dotenv from 'dotenv';
dotenv.config();

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});

export default pool;

```

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