Преглед на файлове

Create RecipeSearch component: Autocompletes recipe searches. Use it in planner to add new recipes to the backlog

Tatiana Inama преди 7 години
родител
ревизия
7849afb867

+ 1 - 0
recipes/package.json

@@ -23,6 +23,7 @@
     "@types/node": "^11.13.17",
     "@types/ramda": "^0.26.22",
     "@types/react": "^16.8.23",
+    "@types/react-autosuggest": "^9.3.11",
     "@types/react-beautiful-dnd": "^11.0.3",
     "@types/react-dom": "^16.8.3",
     "@types/react-router-dom": "^4.3.4",

+ 3 - 2
recipes/src/components/Card/index.tsx

@@ -28,12 +28,13 @@ type CBKCardProps = {
     handler: (event: React.MouseEvent) => void
   }[],
   onClick: (event: React.MouseEvent) => void,
+  className?: string,
 }
 
-function CBKCard({onClick, img, title, summary, actions, icons, noMedia = false}: CBKCardProps){
+function CBKCard({onClick, img, title, summary, actions, icons, noMedia = false, className = ''}: CBKCardProps){
   
   return(
-    <Card outlined className='cbk-card'>
+    <Card outlined className={`cbk-card ${className}`}>
       <CardPrimaryContent onClick={onClick}>
         { noMedia ? null : <CardMedia square imageUrl={img || sample_img}></CardMedia>}
         <div className='cbk-card__main'>

+ 2 - 0
recipes/src/components/Input/index.tsx

@@ -36,6 +36,7 @@ const Input = ({
 	button,
 	field = { name: '', value: '', onBlur: ()=>{}, onChange: ()=>{}},
 	className = '',
+
 }: InputProps) => {
 	const fieldClasses = classNames(
 		'cbk-input',
@@ -71,6 +72,7 @@ const Input = ({
 						value={value}
 						//@ts-ignore
 						onChange={onChange}
+						onKeyDown={onKeyDown}
 						type={type}
 						min={0}
 						rows={1}

+ 4 - 3
recipes/src/components/List/index.tsx

@@ -6,8 +6,9 @@ import './styles.scss';
 type ListProps<T> = {
   nonInteractive?: boolean,
   dense?: boolean,
+  focus?: number,
   items: T[],
-  render: (item: T) => React.ComponentElement<T, any>
+  render: (item: T, index: number) => React.ComponentElement<T, any>
 }
 
 const CBKList = <T extends {}>(props: ListProps<T>) => (
@@ -16,9 +17,9 @@ const CBKList = <T extends {}>(props: ListProps<T>) => (
       props.items.map((item, index) => (
         <li
           key={index}
-          className='cbk-list__item'
+          className={`cbk-list__item ${index === props.focus ? 'cbk-list__item--focus' : ''}`}
         >
-          {props.render(item)}
+          {props.render(item, index)}
         </li>
       ))
     }

+ 2 - 1
recipes/src/components/List/styles.scss

@@ -13,7 +13,8 @@
     overflow: hidden;
     transition: opacity 15ms linear,
                 background-color 15ms linear;
-    &:hover {
+    &:hover,
+    &--focus {
       background-color: rgba(0,0,0,0.04)
     }
     .primary-text,

+ 130 - 0
recipes/src/components/RecipeSearch/index.tsx

@@ -0,0 +1,130 @@
+import React from 'react';
+import { DBRecipe } from 'types/recipes';
+
+import Input from 'components/Input';
+import List from 'components/List';
+
+import classnames from 'classnames';
+
+import { getRecipes } from 'containers/Recipes/services';
+import { throttle } from 'throttle-debounce';
+
+import './styles.scss';
+
+enum Key {
+  Down = 40,
+  Up = 38,
+  Enter = 13,
+  Esc = 27
+}
+
+type Props = {
+  onSelect: (selected: DBRecipe) => void
+}
+
+type State = {
+  results: DBRecipe[],
+  search: string,
+  cursor: number,
+  selected?: DBRecipe,
+  openResult: boolean,
+}
+
+class RecipeSearch extends React.Component<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      results: [],
+      search: '',
+      cursor: 0,
+      openResult: false 
+    }
+  }
+
+  autocompleteSearch = throttle(500, (query: string) => {
+    getRecipes(query).then(results => this.setState({results}))
+  })
+
+  incrementCursor = () => this.state.results.length && this.state.cursor < (this.state.results.length - 1) ? this.state.cursor + 1 : 0;
+
+  decrementCursor = () => this.state.results.length && this.state.cursor > 0 ? this.state.cursor - 1 : (this.state.results.length - 1);
+
+  onKeyDown = (event: React.KeyboardEvent) => {
+    
+    if (event.keyCode === Key.Down) {
+      const cursor = this.incrementCursor();
+      this.setState({
+        selected: this.state.results[cursor],
+        cursor: cursor,
+      });
+    }
+    if (event.keyCode === Key.Up) {
+      const cursor = this.decrementCursor();
+      this.setState({
+        selected: this.state.results[cursor],
+        cursor: cursor,
+      });
+    }
+
+    if (event.keyCode === Key.Enter) {
+      this.selectRecipe(this.state.results[this.state.cursor])
+    }
+
+    if (event.keyCode === Key.Esc) {
+      this.setState({
+        openResult: false
+      })
+    }
+  }
+
+  selectRecipe = (recipe: DBRecipe) => {
+    this.props.onSelect(recipe);
+    this.setState({
+      openResult: false,
+      search: ''
+    })
+  }
+
+  changeQuery = (event: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({
+      search: event.target.value,
+      openResult: true,
+    }, () => {
+      this.autocompleteSearch(this.state.search)
+    })
+  }
+
+  render() {
+    const { results, search, cursor, openResult } = this.state;
+    return (
+    <div className='cbk-recipe-search'>
+      <Input 
+        label=''
+        value={search}
+        onChange={this.changeQuery}
+        onKeyDown={this.onKeyDown}
+      />
+      <div className='cbk-recipe-search__results'>
+        {
+          results.length && openResult ? (
+            <List
+              dense
+              items={results}
+              focus={cursor}
+              render={recipe => (
+                <div
+                  className='cbk-recipe-search__results__item'
+                  onClick={() => this.selectRecipe(recipe)}
+                >
+                  {recipe.name}
+                </div>
+              )}
+            />
+          ) : null
+        }
+      </div>
+    </div>)
+  }    
+}
+
+export default RecipeSearch

+ 15 - 0
recipes/src/components/RecipeSearch/styles.scss

@@ -0,0 +1,15 @@
+.cbk-recipe-search {
+  width: 100%;
+  position: relative;
+  &__results {
+    display: block;
+    width: 100%;
+    position: absolute;
+    z-index: 10000;
+    background-color: white;
+    .cbk-list {
+      max-height: 20rem;
+      overflow-y: scroll;
+    }
+  }
+}

+ 6 - 2
recipes/src/containers/Planner/index.tsx

@@ -12,7 +12,8 @@ import './styles.scss';
 import Button from 'components/Button';
 import { ThunkDispatch } from 'redux-thunk';
 import { bindActionCreators } from 'redux';
-
+import RecipeSearch from 'components/RecipeSearch';
+import { DBRecipe } from 'src/types/recipes';
 type Actions = typeof PlannerActions
 
 interface PlannerContainerProps extends RouteComponentProps, PlannerState, Actions {
@@ -104,6 +105,7 @@ class PlannerContainer extends Component<PlannerContainerProps, PlannerContainer
           backlog={this.props.backlog}
           planner={this.props.planner}
           removeMeal={this.removeMeal}
+          addToBacklog={this.props.addToBacklog}
         />
       </div>
     );
@@ -117,13 +119,15 @@ const DisplayPlanner: React.SFC<{
   backlog: RecipePlan[],
   planner: WeekPlan,
   removeMeal: typeof PlannerActions.removeMeal,
+  addToBacklog: (recipe: DBRecipe) => {},
   mode?: PlannerMode,
-}> = ({ mode, onDragEnd, backlog, weekNumber, week, planner, removeMeal}) => (
+}> = ({ mode, onDragEnd, backlog, weekNumber, week, planner, removeMeal, addToBacklog }) => (
   <section className='cbk-planner__body'>
     <DragDropContext onDragEnd={onDragEnd}>
       {
         mode === PlannerMode.Edit ? (
           <div className='cbk-planner__body__backlog'>
+            <RecipeSearch onSelect={(selected)=>{ addToBacklog(selected) }}/>
             <Droppable droppableId='recipeList'>
               {(provided) => (
                 <div ref={provided.innerRef}>

+ 6 - 16
recipes/src/containers/Planner/reducers.ts

@@ -2,7 +2,7 @@ import { PlannerActions } from './actions';
 import { PlannerState, DBPlanner, WeekPlan, Weekday, PlannerMode } from 'types/planner';
 import { getWeekNumber, mkWeekDay } from 'services/time';
 import { Reducer } from 'redux';
-import { merge } from 'ramda';
+import { merge, uniqBy } from 'ramda';
 
 const initialState: PlannerState = {
   mode: PlannerMode.View,
@@ -21,20 +21,7 @@ const initialState: PlannerState = {
     saturday:   { date: mkWeekDay(6)},
     sunday:     { date: mkWeekDay(7)},
   },
-  backlog: [
-    {
-      _id: '5d638a66eed9f450ff3b32dc',
-      name: 'Summer Corn Chowder'
-    },
-    {
-      _id: '5d638aedeed9f450ff3b32dd',
-      name: 'Chicken Parm'
-    },
-    {
-      _id: '5d6906f806662f07a2825ab7',
-      name: 'Creamy Chicken And Wild Rice Soup'
-    }
-  ]
+  backlog: []
 }
 
 const joinPlanner = (old: WeekPlan, updated: DBPlanner): WeekPlan => Object.keys(old).reduce((_oldPlanner, day) => ({
@@ -50,7 +37,10 @@ const PlannerReducer: Reducer<PlannerState, PlannerActions> = (
     case 'ADD_TO_BACKLOG': 
       return {
         ...state,
-        backlog: state.backlog.concat([action.recipe])
+        backlog: uniqBy(item => item._id, [
+          action.recipe,
+          ...state.backlog
+        ])
       };
     case 'ASSIGN_TO_DAY':
       return {