Parcourir la source

Add Recipe search

Tatiana Inama il y a 7 ans
Parent
commit
af7cc99826

+ 21 - 7
ktchn/src/recipes/controller.ts

@@ -58,9 +58,27 @@ const getById: Controller = (db) => ({ params }, res) => {
 }
 
 const getAll: Controller = (db) => ({ query }, res) => {
-  return db.find<Recipe>(query).then(recipes => res.json(recipes));
+  const _query = Object.keys(query).reduce((q, field) => {
+    return {
+      ...q,
+      ...buildSearchQuery(field, query[field])
+    }
+  }, {});
+  return db.find<Recipe>(_query).then(recipes => res.json(recipes));
 }
 
+type RegexQuery = {
+  '$regex': string,
+  '$options': 'i'
+};
+
+const buildSearchQuery = (fieldName: string, value: string): { [field: string] : RegexQuery} => ({
+  [fieldName]: {
+    '$regex': value,
+    '$options': 'i'
+  }
+});
+
 async function buildQuery(tryQuery:()=>FilterQuery<any>): Promise<FilterQuery<any>> {
   try {
     return Promise.resolve(tryQuery());
@@ -68,17 +86,13 @@ async function buildQuery(tryQuery:()=>FilterQuery<any>): Promise<FilterQuery<an
     return Promise.reject(error);
   }
 } 
+
 const getByIngredients: Controller = (db) => ({ query }, res) => {
   const ingredientsQuery = (query: FilterQuery<any>) => () => {
     if(query.ingredients) {
       const ingredients: string[] = query.ingredients.split(',');
       return {
-        '$or': ingredients.map((ing) => ({
-          'ingredients.ingredients.name': {
-            '$regex': ing,
-            '$options': 'i'        
-          }
-        }))
+        '$or': ingredients.map((ing) => buildSearchQuery('ingredients.ingredients.name', ing))
       };  
     } else {
       throw new Error('Invalid query key');

+ 3 - 1
package.json

@@ -22,6 +22,7 @@
     "@types/react-redux": "^7.0.6",
     "@types/redux-thunk": "^2.1.0",
     "@types/sqlite3": "^3.1.3",
+    "@types/throttle-debounce": "^1.1.1",
     "dotenv": "^6.2.0",
     "express": "^4.16.4",
     "material-components-web": "^1.1.1",
@@ -29,7 +30,8 @@
     "nano": "^8.0.0",
     "ramda": "^0.26.1",
     "redux-thunk": "^2.3.0",
-    "sqlite3": "^4.0.6"
+    "sqlite3": "^4.0.6",
+    "throttle-debounce": "^2.1.0"
   },
   "devDependencies": {
     "@types/ramda": "github:types/npm-ramda#dist",

+ 5 - 1
recipes/src/components/Ingredient/List/index.tsx

@@ -26,7 +26,11 @@ export default function ShowIngredients(props: ShowIngredientsProps) {
               subRecipe.ingredients.map((ing, j) => (
                 <li className='cbk-ingredient-list__ingredient' key={j}>
                   <span>{ing.name}</span>
-                  <span>{ing.quantity + ing.unit}</span>
+                  {
+                    ing.quantity ? (
+                      <span><span>{ing.quantity}</span><span>{ing.unit}</span></span>
+                    ) : null
+                  }
                 </li>
               ))
             }

+ 4 - 1
recipes/src/components/Input/index.tsx

@@ -21,6 +21,7 @@ type InputProps = {
 		onClick: () => void,
 	},
 	field?: any,
+	className?: string,
 };
 
 const Input = ({
@@ -33,7 +34,8 @@ const Input = ({
 	style = 'regular',
 	icon,
 	button,
-	field = { name: '', value: '', onBlur: ()=>{}, onChange: ()=>{}}
+	field = { name: '', value: '', onBlur: ()=>{}, onChange: ()=>{}},
+	className = '',
 }: InputProps) => {
 	const fieldClasses = classNames(
 		'cbk-input',
@@ -45,6 +47,7 @@ const Input = ({
 	);
 	const containerClasses = classNames({
 		'cbk-input-container': !!icon || !!button,
+		[className]: className,
 	})
 
 	return (

+ 11 - 4
recipes/src/components/Navbar/index.tsx

@@ -8,7 +8,8 @@ type NavbarProps = {
     label: string,
     onClick: () => void,
   }[],
-  children?: ReactElement|never[],
+  children?: ReactElement[],
+  contentClassName?: string,
 }
 export default function Navbar(props: NavbarProps){
   return (
@@ -27,9 +28,15 @@ export default function Navbar(props: NavbarProps){
           </div>
         ) : null
       }
-      {
-        props.children
-      }
+      <div className={`cbk-navbar__content ${props.contentClassName||''}`}>
+        {
+          props.children && (
+            props.children.map(child => (
+              child
+            ))
+          )
+        }
+      </div>
     </div>
   );
 };

+ 8 - 0
recipes/src/components/Navbar/styles.scss

@@ -7,4 +7,12 @@
   display: flex;
   flex-direction: row;
   justify-content: space-between;
+  align-items: center;
+
+  &__content {
+    flex-basis: 75%;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+  }
 }

+ 0 - 4
recipes/src/components/RecipeCard/index.tsx

@@ -8,15 +8,11 @@ import { List as Ingredients } from 'components/Ingredient';
 import moment from 'moment';
 
 import sample_img from "components/Card/sample.png";
-import { ReactComponent as Preparation } from 'svgs/preparation.svg';
-import { ReactComponent as Cooking } from 'svgs/cooking.svg';
-import { ReactComponent as Servings } from 'svgs/servings.svg';
 
 import './styles.scss';
 
 type RecipeCardProps = {
   recipe: Recipe,
-  full?: boolean
 }
 
 function CBKRecipeCard(props: RecipeCardProps) {

+ 1 - 0
recipes/src/components/RecipeCard/styles.scss

@@ -39,6 +39,7 @@
           padding-right: $spacing-small;
           color: $grey-500;
           .cbk-icon {
+            margin-right: 8px;
             svg > g path {
               fill: $grey-500;
             }

+ 4 - 5
recipes/src/containers/Recipes/List/actions.ts

@@ -6,7 +6,7 @@ export const RECEIVE_RECIPES = 'RECEIVE_RECIPES';
 export const REQUEST_RECIPES = 'REQUEST_RECIPES';
 export const SELECT_RECIPE = 'SELECT_RECIPE';
 
-export const requestRecipes = (query: {}) => ({
+export const requestRecipes = (query: string) => ({
   type: REQUEST_RECIPES,
   isFetching: true,
 });
@@ -22,10 +22,10 @@ export const selectRecipe = (recipe?: Recipe) => ({
   payload: recipe,
 });
 
-export function fetchRecipes(query: any) {
+export function fetchRecipes(query: string) {
   return (dispatch: any) => {
     dispatch(requestRecipes(query))
-    return getRecipes({})
+    return getRecipes(query)
     .then(data => dispatch(receiveRecipes(data)))
     .catch(error => {
       console.log('error', error);
@@ -37,9 +37,8 @@ function shouldFetch(recipes: Recipe[]) {
   return recipes.length ? false : true;
 }
 
-export function fetchIfNeeded(query: any) {
+export function fetchIfNeeded(query: string) {
   return (dispatch: any, getState: any) => {
-    console.log("fetch");
     if(shouldFetch(getState())) {
       return dispatch(fetchRecipes(query))
     } else {

+ 35 - 7
recipes/src/containers/Recipes/List/index.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { Component, ReactEventHandler } from "react";
 import {
   fetchIfNeeded as fetch,
   receiveRecipes as receive,
@@ -12,27 +12,45 @@ import RecipeCard from 'components/RecipeCard';
 import Recipe, { DBRecipe } from 'types/recipes';
 import Navbar from 'components/Navbar';
 import { Link, RouteComponentProps } from 'react-router-dom';
+import Input from 'components/Input';
+import { throttle } from 'throttle-debounce';
+
+import './styles.scss';
 
 interface RecipeListProps extends RouteComponentProps {
   data: DBRecipe[],
   isFetching: boolean,
   selectedRecipe: any | undefined,
-  fetchRecipes: (query: any) => undefined,
+  fetchRecipes: (query: string) => undefined,
   receiveRecipes: (recipes: DBRecipe[]) => undefined,
   selectRecipe: (recipe?: DBRecipe) => undefined,
 };
 
-class RecipeList extends Component<RecipeListProps, {phoneDisplay: boolean}> {
+class RecipeList extends Component<RecipeListProps, {phoneDisplay: boolean, search: string}> {
   constructor(props: RecipeListProps) {
     super(props);
     this.state = {
-      phoneDisplay: window.innerWidth < 840
+      phoneDisplay: window.innerWidth < 840,
+      search: ''
     }
   }
 
   componentDidMount() {
     const { fetchRecipes } = this.props;
-    fetchRecipes({});
+    fetchRecipes('');
+  }
+
+  autocompleteSearch = throttle(500, (query: string) => {
+    this.props.fetchRecipes(query)
+  })
+
+
+  changeQuery = (query: string) => {
+    this.setState({
+      search: query
+    }, () => {
+      this.autocompleteSearch(this.state.search)
+    })
   }
 
   componentDidUpdate(prevProps: any) {
@@ -82,10 +100,20 @@ class RecipeList extends Component<RecipeListProps, {phoneDisplay: boolean}> {
     }];
 
     return(
-      <div>
+      <div className='cbk-recipes-list'>
         <Navbar
           title="Recipes"
         >
+          <Input
+            value={this.state.search}
+            label='search'
+            onChange={(e) => this.changeQuery(e.currentTarget.value)}
+            button={{
+              icon: 'clear',
+              onClick: () => this.changeQuery('')
+            }}
+            className='cbk-recipes-list__search'
+          />
           <Link to='/recipes/create'>
             <Button unelevated>
               Create Recipe
@@ -130,7 +158,7 @@ const mapStateToProps = ({ recipes }: any, ownProps: any) => {
 
 const mapDispatchToProps = (dispatch: any) => {
   return {
-    fetchRecipes: (query: any) => {
+    fetchRecipes: (query: string) => {
       dispatch(fetch(query))
     },
     receiveRecipes: (recipes: DBRecipe[]) => {

+ 8 - 0
recipes/src/containers/Recipes/List/styles.scss

@@ -0,0 +1,8 @@
+@import 'styles/_variables.scss';
+
+.cbk-recipes-list {
+  &__search {
+    flex-basis: 30%;
+    padding-right: $spacing-regular;
+  }
+}

+ 2 - 2
recipes/src/containers/Recipes/services.ts

@@ -4,8 +4,8 @@ import Recipe, { DBRecipe } from 'types/recipes';
 //@ts-ignore
 const API: string = process.env.REACT_APP_API_RECIPES;
 
-export const getRecipes = (query: any): Promise<DBRecipe[]> => 
-  axios.get(`${API}/all`)
+export const getRecipes = (query: string): Promise<DBRecipe[]> => 
+  axios.get(`${API}/all/?name=${query}`)
   .then(response => response.data);
 
 export const saveRecipe = (recipe: Recipe): Promise<any> =>