All files / src/components RecipeSuggester.tsx

100% Statements 28/28
100% Branches 14/14
100% Functions 4/4
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104          1x 60x 60x 60x 60x   60x   8x 8x 8x 8x   8x   9x     8x 2x 2x 2x     6x 6x 4x   4x 2x     2x   4x 4x 4x   6x           60x                                       38x                                                                    
// src/components/RecipeSuggester.tsx
import React, { useState, useCallback } from 'react';
import { suggestRecipe } from '../services/apiClient';
import { logger } from '../services/logger.client';
 
export const RecipeSuggester: React.FC = () => {
  const [ingredients, setIngredients] = useState<string>('');
  const [suggestion, setSuggestion] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
 
  const handleSubmit = useCallback(
    async (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      setIsLoading(true);
      setError(null);
      setSuggestion(null);
 
      const ingredientList = ingredients
        .split(',')
        .map((item) => item.trim())
        .filter(Boolean);
 
      if (ingredientList.length === 0) {
        setError('Please enter at least one ingredient.');
        setIsLoading(false);
        return;
      }
 
      try {
        const response = await suggestRecipe(ingredientList);
        const data = await response.json();
 
        if (!response.ok) {
          throw new Error(data.message || 'Failed to get suggestion.');
        }
 
        setSuggestion(data.suggestion);
      } catch (err) {
        const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred.';
        logger.error({ error: err }, 'Failed to fetch recipe suggestion.');
        setError(errorMessage);
      } finally {
        setIsLoading(false);
      }
    },
    [ingredients],
  );
 
  return (
    <div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
      <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
        Get a Recipe Suggestion
      </h2>
      <p className="text-gray-600 dark:text-gray-400 mb-4">
        Enter some ingredients you have, separated by commas.
      </p>
      <form onSubmit={handleSubmit}>
        <div className="mb-4">
          <label
            htmlFor="ingredients-input"
            className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
          >
            Ingredients:
          </label>
          <input
            id="ingredients-input"
            type="text"
            value={ingredients}
            onChange={(e) => setIngredients(e.target.value)}
            placeholder="e.g., chicken, rice, broccoli"
            disabled={isLoading}
            className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm p-2 border"
          />
        </div>
        <button
          type="submit"
          disabled={isLoading}
          className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 transition-colors"
        >
          {isLoading ? 'Getting suggestion...' : 'Suggest a Recipe'}
        </button>
      </form>
 
      {error && (
        <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/50 text-red-700 dark:text-red-200 rounded-md text-sm">
          {error}
        </div>
      )}
 
      {suggestion && (
        <div className="mt-6 bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 border border-gray-200 dark:border-gray-600">
          <div className="prose dark:prose-invert max-w-none">
            <h5 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
              Recipe Suggestion
            </h5>
            <p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{suggestion}</p>
          </div>
        </div>
      )}
    </div>
  );
};