# Project export: RecipeFinder

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: Build a Recipe Finder and Meal Planner app that recommends recipes based on user preferences (e.g., dietary restrictions, ingredients, cuisine type)
- Devpost: https://devpost.com/software/recipefinder-ls3itv
- GitHub: https://github.com/fridap251/Recipe-Finder-Meal-Planner-App
- Demo: https://ephemeral-concha-e3d16c.netlify.app/
- Video: https://www.youtube.com/embed/AM0XR_TYxB4?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

I'll provide a detailed implementation plan for building this AI-enabled application using the RecipeNLG dataset, Google Cloud integrations, and GitLab’s CI/CD capabilities

### What it does

The Recipe Finder application is a web-based tool designed to help users discover and manage recipes. I deployed on Google Cloud Run, it provides a scalable platform for accessing recipes anytime, anywhere How i built it I build frontend with GitLab OAuth, so i build with Google Cloud Vision API for image analysis for fastAPI Backend, and i make like dating app so its fun and engage Challenges i ran into These challenges highlight the complexity of coordinating frontend, backend, and CI/CD pipelines, especially with a new toolset like Google Cloud Build and Gitlab. Cloud Build and GitLab Integration: Setting up cloudbuild.yaml and ensuring environment variables were correctly passed from GitLab to Google Cloud Build was challenging, especially aligning the Vite VITE_ prefixes with the CI/CD process. Accomplishments that iam proud of Despite the hurdles, you’ve achieved significant milestones: Successful Deployment: The completed deployment revision on Cloud Run (with all steps—Updating service, Creating revision, Routing traffic—marked as Completed) is a major accomplishment, proving the end-to-end pipeline from code to production works. Learning Gitlab: Successfully pushing changes to a remote repository, even with initial difficulties, is a proud moment, enabling continuous deployment. What i learned Cloud Build Basics: I mastered creating and updating Cloud Build triggers, configuring cloudbuild.yaml, and monitoring builds with gcloud builds log, enhancing my CI/CD knowledge. Scalability Awareness: Deploying to Cloud Run introduced i to cloud scaling concepts, setting the stage for future optimizations.

### What's next

Enhance User Experience

## README (from the GitHub repository)

recipe-finder-meal-planner-application


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 20 KB.
- Express (technology) — 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
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- CSS (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (19 of 19)

```
.env
.env.example
.gcloudignore
.gitignore
cloudbuild.yaml
Dockerfile.api
Dockerfile.frontend
eslint.config.js
index.html
mock-server.js
nginx.conf
package.json
postcss.config.js
README.md
tailwind.config.js
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- package.json: @eslint/js@^9.9.1, @google-cloud/logging@^11.0.0, @google-cloud/logging-winston@^6.0.0, @supabase/supabase-js@^2.39.7, @types/react@^18.3.5, @types/react-dom@^18.3.0, @vitejs/plugin-react@^4.3.1, autoprefixer@^10.4.18, axios@^1.6.7, clsx@^2.1.0, cors@^2.8.5, eslint@^9.9.1, eslint-plugin-react-hooks@^5.1.0-rc.0, eslint-plugin-react-refresh@^0.4.11, express@^4.18.2, globals@^15.9.0, lucide-react@^0.344.0, postcss@^8.4.35, react@^18.3.1, react-dom@^18.3.1, react-router-dom@^6.22.3, tailwind-merge@^2.2.1, tailwindcss@^3.4.1, typescript@^5.5.3, typescript-eslint@^8.3.0, vite@^5.4.2, winston@^3.11.0

### Recent commits (newest first)

- Add files via upload

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

### package.json

```
{
  "name": "recipe-finder-meal-planner",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host 0.0.0.0 --port 5173",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview --host 0.0.0.0 --port 4173",
    "mock-server": "node mock-server.js",
    "start:api": "node mock-server.js",
    "start": "npm run dev",
    "test": "echo \"No tests specified\" && exit 0",
    "docker:build-api": "docker build -f Dockerfile.api -t recipe-finder-api .",
    "docker:build-frontend": "docker build -f Dockerfile.frontend -t recipe-finder-frontend .",
    "docker:run-api": "docker run -p 3000:3000 recipe-finder-api",
    "docker:run-frontend": "docker run -p 8080:80 recipe-finder-frontend"
  },
  "dependencies": {
    "@google-cloud/logging": "^11.0.0",
    "@google-cloud/logging-winston": "^6.0.0",
    "@supabase/supabase-js": "^2.39.7",
    "axios": "^1.6.7",
    "clsx": "^2.1.0",
    "lucide-react": "^0.344.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.22.3",
    "tailwind-merge": "^2.2.1",
    "winston": "^3.11.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.9.1",
    "@types/react": "^18.3.5",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "autoprefixer": "^10.4.18",
    "cors": "^2.8.5",
    "eslint": "^9.9.1",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.11",
    "express": "^4.18.2",
    "globals": "^15.9.0",
    "postcss": "^8.4.35",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.5.3",
    "typescript-eslint": "^8.3.0",
    "vite": "^5.4.2"
  }
}
```

### postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

```

### vite.config.ts

```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  optimizeDeps: {
    exclude: ['lucide-react'],
  },
  server: {
    host: '0.0.0.0',
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        secure: false,
      },
    },
  },
  preview: {
    host: '0.0.0.0',
    port: 4173,
  },
});
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Recipe Finder & Meal Planner</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

### eslint.config.js

```javascript
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  }
);

```

### tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#f0f9ff',
          100: '#e0f2fe',
          200: '#bae6fd',
          300: '#7dd3fc',
          400: '#38bdf8',
          500: '#0ea5e9',
          600: '#0284c7',
          700: '#0369a1',
          800: '#075985',
          900: '#0c4a6e',
          950: '#082f49',
        },
        secondary: {
          50: '#f5f3ff',
          100: '#ede9fe',
          200: '#ddd6fe',
          300: '#c4b5fd',
          400: '#a78bfa',
          500: '#8b5cf6',
          600: '#7c3aed',
          700: '#6d28d9',
          800: '#5b21b6',
          900: '#4c1d95',
          950: '#2e1065',
        },
        accent: {
          50: '#fff7ed',
          100: '#ffedd5',
          200: '#fed7aa',
          300: '#fdba74',
          400: '#fb923c',
          500: '#f97316',
          600: '#ea580c',
          700: '#c2410c',
          800: '#9a3412',
          900: '#7c2d12',
          950: '#431407',
        },
        success: {
          50: '#f0fdf4',
          100: '#dcfce7',
          200: '#bbf7d0',
          300: '#86efac',
          400: '#4ade80',
          500: '#22c55e',
          600: '#16a34a',
          700: '#15803d',
          800: '#166534',
          900: '#14532d',
          950: '#052e16',
        },
        warning: {
          50: '#fffbeb',
          100: '#fef3c7',
          200: '#fde68a',
          300: '#fcd34d',
          400: '#fbbf24',
          500: '#f59e0b',
          600: '#d97706',
          700: '#b45309',
          800: '#92400e',
          900: '#78350f',
          950: '#451a03',
        },
        error: {
          50: '#fef2f2',
          100: '#fee2e2',
          200: '#fecaca',
          300: '#fca5a5',
          400: '#f87171',
          500: '#ef4444',
          600: '#dc2626',
          700: '#b91c1c',
          800: '#991b1b',
          900: '#7f1d1d',
          950: '#450a0a',
        },
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
      animation: {
        'fade-in': 'fadeIn 0.5s ease-in-out',
        'slide-up': 'slideUp 0.5s ease-in-out',
        'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(10px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
      },
    },
  },
  plugins: [],
};
```

### cloudbuild.yaml

```yaml
# Google Cloud Build configuration for Recipe Finder application
steps:
  # Install dependencies
  - name: 'node:18'
    entrypoint: 'npm'
    args: ['ci']
    
  # Run tests
  - name: 'node:18'
    entrypoint: 'npm'
    args: ['run', 'test']
    env:
      - 'NODE_ENV=test'
    
  # Build the application
  - name: 'node:18'
    entrypoint: 'npm'
    args: ['run', 'build']
    env:
      - 'NODE_ENV=production'
      - 'VITE_API_BASE_URL=${_API_BASE_URL}'
      - 'VITE_SUPABASE_URL=${_SUPABASE_URL}'
      - 'VITE_SUPABASE_ANON_KEY=${_SUPABASE_ANON_KEY}'
    
  # Build Docker image for the API server
  - name: 'gcr.io/cloud-builders/docker'
    args: [
      'build',
      '-t', 'gcr.io/$PROJECT_ID/recipe-finder-api:$BUILD_ID',
      '-t', 'gcr.io/$PROJECT_ID/recipe-finder-api:latest',
      '-f', 'Dockerfile.api',
      '.'
    ]
    
  # Build Docker image for the frontend
  - name: 'gcr.io/cloud-builders/docker'
    args: [
      'build',
      '-t', 'gcr.io/$PROJECT_ID/recipe-finder-frontend:$BUILD_ID',
      '-t', 'gcr.io/$PROJECT_ID/recipe-finder-frontend:latest',
      '-f', 'Dockerfile.frontend',
      '.'
    ]
    
  # Push images to Container Registry
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/recipe-finder-api:$BUILD_ID']
    
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/recipe-finder-api:latest']
    
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/recipe-finder-frontend:$BUILD_ID']
    
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/recipe-finder-frontend:latest']
    
  # Deploy API to Cloud Run
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: 'gcloud'
    args: [
      'run', 'deploy', 'recipe-finder-api',
      '--image', 'gcr.io/$PROJECT_ID/recipe-finder-api:$BUILD_ID',
      '--region', '${_REGION}',
      '--platform', 'managed',
      '--allow-unauthenticated',
      '--set-env-vars', 'NODE_ENV=production,GOOGLE_CLOUD_PROJECT_ID=$PROJECT_ID,ENABLE_CLOUD_LOGGING=true',
      '--set-env-vars', 'SUPABASE_URL=${_SUPABASE_URL},SUPABASE_ANON_KEY=${_SUPABASE_ANON_KEY}',
      '--memory', '1Gi',
      '--cpu', '1',
      '--max-instances', '10'
    ]
    
  # Deploy Frontend to Cloud Run
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: 'gcloud'
    args: [
      'run', 'deploy', 'recipe-finder-frontend',
      '--image', 'gcr.io/$PROJECT_ID/recipe-finder-frontend:$BUILD_ID',
      '--region', '${_REGION}',
      '--platform', 'managed',
      '--allow-unauthenticated',
      '--port', '80',
      '--memory', '512Mi',
      '--cpu', '1',
      '--max-instances', '5'
    ]

# Substitution variables
substitutions:
  _REGION: 'us-central1'
  _API_BASE_URL: 'https://recipe-finder-api-${_REGION}-${PROJECT_ID}.a.run.app'
  _SUPABASE_URL: 'your-supabase-url'
  _SUPABASE_ANON_KEY: 'your-supabase-anon-key'

# Build options
options:
  logging: CLOUD_LOGGING_ONLY
  machineType: 'E2_HIGHCPU_8'
  
# Service account for build
serviceAccount: 'projects/$PROJECT_ID/serviceAccounts/cloudbuild-recipe-finder@$PROJECT_ID.iam.gserviceaccount.com'

# Build timeout
timeout: '1200s'
```

### mock-server.js

```javascript
const express = require('express');
const cors = require('cors');
const logger = require('./server/logger');

const app = express();
const PORT = 3000;

// Enable CORS for your frontend
app.use(cors({
  origin: 'http://localhost:5173',
  credentials: true
}));

app.use(express.json());

// Logging middleware
app.use((req, res, next) => {
  const startTime = Date.now();
  
  // Log incoming request
  logger.logRecipeRequest(req.method, req.path, req.headers['user-id'] || 'anonymous', {
    query: req.query,
    userAgent: req.headers['user-agent'],
    ip: req.ip,
  });

  // Override res.json to log response
  const originalJson = res.json;
  res.json = function(data) {
    const responseTime = Date.now() - startTime;
    logger.logRecipeResponse(req.method, req.path, res.statusCode, responseTime, {
      dataSize: JSON.stringify(data).length,
    });
    return originalJson.call(this, data);
  };

  next();
});

// Client logs endpoint
app.post('/api/logs', (req, res) => {
  try {
    const { logs } = req.body;
    
    if (!Array.isArray(logs)) {
      return res.status(400).json({ error: 'Logs must be an array' });
    }

    logs.forEach(log => {
      const logData = {
        event: 'client_log',
        level: log.level,
        message: log.message,
        userId: log.userId,
        sessionId: log.sessionId,
        timestamp: log.timestamp,
        metadata: log.metadata,
      };

      switch (log.level) {
        case 'error':
          logger.error(`[CLIENT] ${log.message}`, logData);
          break;
        case 'warn':
          logger.warn(`[CLIENT] ${log.message}`, logData);
          break;
        case 'debug':
          logger.debug(`[CLIENT] ${log.message}`, logData);
          break;
        default:
          logger.info(`[CLIENT] ${log.message}`, logData);
      }
    });

    res.json({ success: true, processed: logs.length });
  } catch (error) {
    logger.logError(error, { endpoint: '/api/logs' });
    res.status(500).json({ error: 'Failed to process logs' });
  }
});

// Mock recipes data
const mockRecipes = [
  {
    id: '1',
    title: 'Classic Margherita Pizza',
    image: 'https://images.pexels.com/photos/1146760/pexels-photo-1146760.jpeg',
    ingredients: ['1 pizza dough', '1/2 cup pizza sauce', '8 oz fresh mozzarella', '1/4 cup fresh basil leaves', '2 tbsp olive oil', 'Salt and pepper', '1 tsp dried oregano'],
    instructions: 'Preheat oven to 475°F. Roll out pizza dough on a floured surface. Transfer dough to a pizza stone or baking sheet. Spread pizza sauce evenly over dough, leaving a 1-inch border. Add sliced mozzarella and drizzle with olive oil. Bake for 12-15 minutes until crust is golden and cheese is bubbly. Remove from oven and top with fresh basil leaves. Season with salt, pepper, and oregano.',
    cookTime: 15,
    prepTime: 10,
    servings: 4,
    calories: 285,
    cuisine: 'Italian',
    dietaryRestrictions: ['Vegetarian'],
    nutritionalInfo: { protein: 12, carbs: 35, fat: 15, fiber: 2 },
    difficulty: 'Medium',
    mealType: 'Dinner'
  },
  {
    id: '2',
    title: 'Chicken Teriyaki Bowl',
    image: 'https://images.pexels.com/photos/1640777/pexels-photo-1640777.jpeg',
    ingredients: ['2 chicken breasts, sliced', '2 cups cooked rice', '1/4 cup teriyaki sauce', '1 cup broccoli florets', '1 carrot, julienned', '2 tbsp sesame oil', '1 tbsp sesame seeds', '2 green onions, chopped'],
    instructions: 'Heat sesame oil in a large pan over medium-high heat. Add chicken and cook until golden brown. Add vegetables and stir-fry for 3-4 minutes. Pour teriyaki sauce over chicken and vegetables. Serve over rice and garnish with sesame seeds and green onions.',
    cookTime: 20,
    prepTime: 15,
    servings: 4,
    calories: 420,
    cuisine: 'Asian',
    dietaryRestrictions: ['Gluten-Free'],
    nutritionalInfo: { protein: 28, carbs: 45, fat: 12, fiber: 4 },
    difficulty: 'Easy',
    mealType: 'Dinner'
  },
  {
    id: '3',
    title: 'Mediterranean Quinoa Salad',
    image: 'https://images.pexels.com/photos/1059905/pexels-photo-1059905.jpeg',
    ingredients: ['1 cup quinoa, cooked', '1 cucumber, diced', '1 cup cherry tomatoes, halved', '1/2 red onion, diced', '1/2 cup kalamata olives', '1/2 cup feta cheese, crumbled', '1/4 cup olive oil', '2 tbsp lemon juice', '2 tbsp fresh herbs (parsley, mint)'],
    instructions: 'Cook quinoa according to package directions and let cool. In a large bowl, combine quinoa, cucumber, tomatoes, red onion, and olives. Whisk together olive oil, lemon juice, salt, and pepper. Pour dressing over salad and toss. Top with feta cheese and fresh herbs.',
    cookTime: 0,
    prepTime: 20,
    servings: 6,
    calories: 320,
    cuisine: 'Mediterranean',
    dietaryRestrictions: ['Vegetarian', 'Gluten-Free'],
    nutritionalInfo: { protein: 12, carbs: 35, fat: 18, fiber: 6 },
    difficulty: 'Easy',
    mealType: 'Lunch'
  },
  {
    id: '4',
    title: 'Avocado Toast with Poached Egg',
    image: 'https://images.pexels.com/photos/566566/pexels-photo-566566.jpeg',
    ingredients: ['4 slices whole grain bread', '2 ripe avocados', '4 eggs', '2 tbsp lemon juice', '1 tbsp white vinegar', 'Red pepper flakes', 'Everything bagel seasoning', 'Salt and pepper to taste', 'Microgreens for garnish'],
    instructions: 'Toast bread until golden. Mash avocados with lemon juice, salt, and pepper. Bring water to a gentle simmer, add vinegar. Crack eggs into small bowls and gently slide into water. Poach for 3-4 minutes. Spread avocado on toast, top with poached egg. Season with red pepper flakes, everything seasoning, and microgreens.',
    cookTime: 10,
    prepTime: 10,
    servings: 4,
    calories: 320,
    cuisine: 'American',
    dietaryRestrictions: ['Vegetarian'],
    nutritionalInfo: { protein: 16, carbs: 28, fat: 18, fiber: 12 },
    difficulty: 'Medium',
    mealType: 'Breakfast'
  },
  {
    id: '5',
    title: 'Thai Green Curry',
    image: 'https://images.pexels.com/photos/2347311/pexels-photo-2347311.jpeg',

[truncated — 6239 more characters]
```