<file_sep>// @flow
import { initializeFilterState } from './utils';

const initialState = {
  filters: {}
};

initialState.filters.greenspaces = initializeFilterState({
  farmerDesired: ['yes', 'no'],
  plotSize: ['initial', 'largePlot', 'microPlot', 'backyard', 'frontyard', 'fullyard']
});

initialState.filters.farmers = initializeFilterState({
  experience: ['initial', 'expert', 'intermediate', 'novice'],
  skills: [
    'initial',
    'fruits',
    'vegetables',
    'herbs',
    'farming education',
    'home gardening',
    'sustainability',
    'organic',
    'CSA',
    'farmers market'
  ]
});

export default initialState;
<file_sep>// @flow

import React from 'react';

const Landing = () => (
  <div>
    <section>
      <h1>Landing Component</h1>
    </section>
  </div>
);

export default Landing;
<file_sep>// @flow

import React from 'react';
import { Link } from 'react-router-dom';

const FarmerCard = (props: FarmerBrief) => {
  let skillCol1 = [];
  let skillCol2 = [];
  if (props.skills) {
    if (props.skills.length > 1) {
      // skill distribution to columns, check if length is even
      if (props.skills.length % 2 === 0) {
        skillCol1 = skillCol1.concat(props.skills.slice(0, props.skills.length / 2));
        // $FlowFixMe
        skillCol2 = skillCol2.concat(props.skills.slice(props.skills.length / 2));
      } else {
        // if odd we want the column with the highest number of items to be skillCol1
        // $FlowFixMe
        skillCol1 = skillCol1.concat(props.skills.slice(0, Math.floor(props.skills.length / 2) + 1));
        // $FlowFixMe
        skillCol2 = skillCol2.concat(props.skills.slice(Math.floor(props.skills.length / 2) + 1));
      }
    } else {
      skillCol1 = skillCol1.concat(props.skills);
    }
  }

  let shortBio;
  if (!props.skills) {
    // If we don't have skills we'll provide the bio instead but we just want an abstract,
    // so we take the first 101 chars, cut off the last - to get rid of any cut off words,
    shortBio = props.bio
      .substr(0, 101)
      .split(' ')
      .slice(0, -1)
      .join(' ');
    // And if a period is the last character we want to delete it, because we're going to have an ellipsis
    // at the end of our string variable.
    if (shortBio.lastIndexOf('.') === shortBio.length - 1) {
      shortBio = shortBio.substr(0, shortBio.length - 1);
    }
  }

  const skillList = (
    <div>
      <div className="flex justify-between ph2">
        <ul className="mt3 pl0 list">
          {skillCol1.map(skill => (
            <li key={skill} className="tl f7 lh-copy ttc helvetica b dark-green">
              {skill}
            </li>
          ))}
        </ul>
        <ul className="mt3 pl0 list">
          {skillCol2.map(skill => (
            <li key={skill} className="tl f7 lh-copy ttc helvetica b dark-green">
              {skill}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
  const bio = (
    <div className="ph2 pt3">
      <p className="mt0 tl lh-copy f6 avenir">{shortBio}...</p>
    </div>
  );

  return (
    <div
      className="mt3 pb3 ba b--black-05 br3 grow shadow-5 tc"
      style={{ width: '23%', marginRight: '2%', marginBottom: '48px' }}
    >
      <header className="w-100 mb3 relative">
        <div className="w-100 absolute top-0 br3 br--top bg-near-white z-0" style={{ height: '90px' }} />
        <div
          className="dib auto mt4 relative br-100 bg-center cover"
          style={{
            backgroundImage: `url(/public/images/profile_images/${props.profileImage})`,
            height: '100px',
            width: '100px'
          }}
        />
      </header>
      <h3 className="mt1 mb2 f3 lh-title ttc avenir ">{props.userName}</h3>
      <h5 className="mt2 mb3 f5 lh-title ttc avenir black-70">{`${props.experience} farmer`}</h5>
      <Link
        className="dib auto ph3 pv2 ba br-pill dark-green f7 avenir b no-underline link dim"
        to={`/user/${props.id}/#farming`}
      >
        View Farmer
      </Link>

      <div className="w-100 mt3 ph3 bt b--black-10 ">
        <h4 className="mb0 mt3 pl2 tl f4 ttc avenir black-70">{props.skills ? 'skills' : 'about'}</h4>
        <hr className="mw4 ml2 mt1 mb0 bb bw1 b--black-05 tl" />
        {props.skills ? skillList : bio}
      </div>
    </div>
  );
};

export default FarmerCard;
<file_sep>// @flow

import React from 'react';
import FilterCatProps from '../const';
import FilterButtonRow from './FilterButtonRow';
import { optionalComputedPropVal } from '../utils';

const Filters = (props: { filterCat: string, filters: {}, updateFilters: Function }) => {
  const passFilterState = (filter: string, filterType: string) =>
    props.updateFilters({ filter, filterType, filterState: props.filters });
  const filterIsBinary = (filter: string, filterRowProps: FilterProp) => {
    if (
      optionalComputedPropVal(filterRowProps, ['binaryFilters']) &&
      // $FlowFixMe - first check negates possibility of reaching this line if optionalComputedPropVal(filterRowProps, ['binaryFilters']) returns null
      optionalComputedPropVal(filterRowProps, ['binaryFilters']).includes(filter)
    ) {
      return true;
    }
    return undefined;
  };

  const filterRowProps = FilterCatProps[props.filterCat];
  const filterGroup = (filterRow: any, filterTitle: string) => (
    <div key={filterTitle} className="mr4 flex justify-start">
      <button className="mr3 pl1 pr3 pv1 bg-transparent bt-0 br-0 bb bl-0 bw2 b--green fw6 f5 avenir tl pointer">
        {filterTitle}
      </button>
      {filterRow}
    </div>
  );
  const filterRow = (filter: string) => (
    <FilterButtonRow
      key={filter}
      filter={filter}
      filterState={props.filters[filter]}
      filterOptions={filterRowProps.options[filter]}
      changeFilter={passFilterState}
      // $FlowFixMe - flow doesn't like optionalComputedVal because it can return type any
      btnTextArr={optionalComputedPropVal(filterRowProps, ['optionsText', filter], true)}
      includesBinaryBoth={filterIsBinary(filter, filterRowProps)}
      // $FlowFixMe - flow doesn't like optionalComputedVal because it can return type any
      binaryBothBtnText={optionalComputedPropVal(filterRowProps, ['binaryFilterProps', filter, 'btnText'], true)}
    />
  );
  const filters = filterRowProps.filters.map((filter: string, index) =>
    filterGroup(filterRow(filter), filterRowProps.titles[index])
  );

  return (
    <div className="pt4 pb0">
      <button
        className="bt-0 br-0 bb-0 bl-0 bg-transparent pointer flex justify-start items-center"
        style={{ paddingLeft: '0' }}
      >
        <h3 className="mr2 f3 fw6 bw0 avenir near-black">Filters</h3>
        <svg className="w1 near-black" data-icon="chevronDown" viewBox="0 0 32 32" style={{ fill: 'currentcolor' }}>
          <title>chevronDown icon</title>
          <path d="M1 18 L5 14 L16 24 L27 14 L31 18 L16 32 Z" />
        </svg>
      </button>
      <div className="flex items-center">{filters}</div>
    </div>
  );
};

export default Filters;
<file_sep>// @flow

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import type { MapStateToProps } from 'react-redux';
import preload from '../data.json';

const GlobalHeader = (props: { selectedUser: User }) => (
  <div>
    <header className="fixed z-999 w-100 flex items-center h3 pa4 ba bw4 bg-green green">
      <nav className="w-50 flex items-center h3">
        <Link className="mr3 f2 white avenir no-underline b" to="/">
          Greenspace
        </Link>
        <Link className="mr3 pt2 f4 white avenir no-underline b dim" to="/farmers">
          Farmers
        </Link>
        <Link className="pt2 f4 white avenir no-underline b dim" to="/greenspaces">
          Spaces
        </Link>
      </nav>
      <nav className="w-50 flex flex-row-reverse items-center h3">
        <Link
          className="pt2 f4 white avenir ttc no-underline b dim"
          to={props.selectedUser ? `/user/${props.selectedUser.id}` : '/'}
        >
          {props.selectedUser ? props.selectedUser.userName : 'Login'}
        </Link>
      </nav>
    </header>
    <div className="spacer w-100" style={{ height: '96px' }} />
  </div>
);

const mapStateToProps: MapStateToProps<*, *, *> = state => {
  const selectedUser = preload.users.find((user: User) => user.id === state.user.id);
  return { selectedUser };
};

export default connect(mapStateToProps)(GlobalHeader);
<file_sep>// @flow

import React from 'react';
import FarmerCard from './FarmerCard';
import { paginationSlice } from './utils';

const FarmerCardList = (props: {
  farmerCardList: Array<FarmerBrief>,
  currentPageNumber: number | null,
  cardsPerPage: number | null
}) => (
  <div className="flex flex-wrap justify-start pv4">
    {props.currentPageNumber && props.cardsPerPage
      ? paginationSlice(props.farmerCardList, props.currentPageNumber, props.cardsPerPage).map(
          (farmer: FarmerBrief) => <FarmerCard key={farmer.id} {...farmer} />
        )
      : props.farmerCardList.map((farmer: FarmerBrief) => <FarmerCard key={farmer.id} {...farmer} />)}
  </div>
);

export default FarmerCardList;
<file_sep>// @flow

import React from 'react';
import preload from '../data.json';
import GreenspaceCardList from './GreenspaceCardList';

const UserFarmingView = (props: { farmer: FarmerBrief }) => {
  const farmingGreenspaces = preload.greenspaces.filter(
    (greenspace: Greenspace) =>
      typeof props.farmer.properties !== 'undefined' && props.farmer.properties.includes(greenspace.id)
  );
  const farmingGreenspacesTitle = (
    <h3 className="mt5 mb0 f3 avenir near-black">{`Greenspaces ${props.farmer.userName} is Farming`}</h3>
  );
  const farmingGreenspacesList = (
    <div className="ph5">
      <GreenspaceCardList greenspaceCardList={farmingGreenspaces} />
    </div>
  );
  let farmingSkillList;
  let experienceOutlineColor: string;
  if (props.farmer.skills) {
    farmingSkillList = (
      <div className="avenir">
        <h5 className="mb2 f5 i">Farming Skills:</h5>
        <ul className="pb3 pl0">
          {props.farmer.skills.map((skill: farmingSkills) => (
            <li key={skill} className="dib pv2 ph3 bg-green white mr3">
              {skill}
            </li>
          ))}
        </ul>
      </div>
    );
  }
  if (props.farmer.experience === 'novice') {
    experienceOutlineColor = 'near-white';
  } else if (props.farmer.experience === 'intermediate') {
    experienceOutlineColor = 'near-black';
  } else {
    experienceOutlineColor = 'green';
  }
  return (
    <section>
      <div className="ph5">
        <h4 className={`mb2 pv2 ph3 dib ba bw2 b--${experienceOutlineColor} avenir f4 ttc`}>{`${
          props.farmer.experience
        } farmer`}</h4>
        {farmingSkillList}
        <p className="mw8 lh-copy avenir">{props.farmer.bio}</p>
        {farmingGreenspaces.length ? farmingGreenspacesTitle : null}
      </div>
      {farmingGreenspaces.length ? farmingGreenspacesList : null}
    </section>
  );
};

export default UserFarmingView;
<file_sep>// @flow

import React from 'react';
import GreenspaceCard from './GreenspaceCard';
import { paginationSlice } from './utils';

const GreenspaceCardList = (props: {
  greenspaceCardList: Array<Greenspace>,
  currentPageNumber?: number | null,
  cardsPerPage?: number | null
}) => (
  <section className="flex flex-wrap justify-start pv4">
    {/* 
            if pagination info was passed in props, then list the greenspace cards sliced for current page and cars per pages
            else just map cards from all of the greenspaces passed - this way we don't have to always specify pagination for greenspaces 
          */}
    {props.currentPageNumber && props.cardsPerPage
      ? paginationSlice(props.greenspaceCardList, props.currentPageNumber, props.cardsPerPage).map(
          (greenspace: Greenspace) => <GreenspaceCard {...greenspace} key={greenspace.id} />
        )
      : props.greenspaceCardList.map((greenspace: Greenspace) => (
          <GreenspaceCard {...greenspace} key={greenspace.id} />
        ))}
  </section>
);

GreenspaceCardList.defaultProps = {
  currentPageNumber: null,
  cardsPerPage: null
};

export default GreenspaceCardList;
<file_sep>// @flow

import React, { Component } from 'react';
import { connect } from 'react-redux';
import FarmerCardList from './FarmerCardList';
import NoFilteredEntitiesMsg from './utilComponents/NoFilteredEntitiesMsg';
import Filters from './utilComponents/Filters';
import { NextPreviousBtns } from './utilComponents/PageControls';
import { passFilterUpdateToSetter, setFilter, getSelectedFiltersMap, filterStateIsInitial } from './utils';
import { setFarmersFilters } from './actionCreators';

type Props = { farmers: Array<FarmerBrief>, filters: genFilters, filtersSetter: Function };

type State = {
  pageNumber: number,
  filteredFarmers: Array<FarmerBrief>
};

class Farmers extends Component<Props, State> {
  state = {
    pageNumber: 1,
    filteredFarmers: this.props.farmers
  };

  componentWillReceiveProps(nextProps) {
    // if filters will be changed, then we need to reset the current page to page 1
    if (this.props.filters !== nextProps.filters) {
      this.updatePage(1);
      this.updateFilteredFarmers(nextProps.filters);
    }
  }

  updatePage = (val: number) => this.setState({ pageNumber: parseInt(val, 10) });
  updateFilteredFarmers = updatedFilters => this.setState({ filteredFarmers: this.filterFarmers(updatedFilters) });

  passesExperienceFilter = (farmerExperience: farmingExperienceLevel, selectedFilters: Array<string>) => {
    if (filterStateIsInitial(selectedFilters)) {
      return true;
    }
    if (selectedFilters.includes(farmerExperience)) {
      return true;
    }
    return false;
  };
  passesSkillsFilter = (farmerSkills: Array<farmingSkills> | typeof undefined, selectedFilters: Array<string>) => {
    if (filterStateIsInitial(selectedFilters)) {
      return true;
    }
    // User/FarmerBrief.skills is an optional property, so we need to account for it not being available
    if (!farmerSkills) {
      return false;
    }
    // if any of the selectedFilters are in farmerSkills return true - the farmer is filtered in
    for (let i = 0; i < selectedFilters.length; i += 1) {
      if (farmerSkills.includes(selectedFilters[i])) {
        return true;
      }
    }
    return false;
  };

  filterFarmers = updatedFilters => {
    const selectedFiltersMap: { experience: Array<string>, skills: Array<string> } = getSelectedFiltersMap(
      updatedFilters
    );
    const filteredFarmers = this.props.farmers.filter(
      farmer =>
        this.passesExperienceFilter(farmer.experience, selectedFiltersMap.experience) &&
        this.passesSkillsFilter(farmer.skills, selectedFiltersMap.skills)
    );
    return filteredFarmers;
  };

  render() {
    const cardsPerPage = 8;

    return (
      <section className="ph5">
        <Filters
          filterCat="farmers"
          filters={this.props.filters}
          updateFilters={resolveFiltersParams =>
            passFilterUpdateToSetter(resolveFiltersParams, this.props.filtersSetter)
          }
        />
        <NoFilteredEntitiesMsg entities={this.state.filteredFarmers} entitiesType="farmers" />
        <FarmerCardList
          farmerCardList={this.state.filteredFarmers}
          currentPageNumber={this.state.pageNumber}
          cardsPerPage={cardsPerPage}
        />
        <NextPreviousBtns
          pageNumber={this.state.pageNumber}
          dataLength={this.state.filteredFarmers.length}
          cardsPerPage={cardsPerPage}
          clickHandler={this.updatePage}
        />
      </section>
    );
  }
}

const mapStateToProps = state => ({ filters: state.farmersFilters });
const mapDispatchToProps = (dispatch: Function) => ({
  filtersSetter(resolveFiltersParams: resolveFiltersParams) {
    setFilter(resolveFiltersParams, setFarmersFilters, dispatch);
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(Farmers);
<file_sep>// @flow

import React from 'react';
import { Link } from 'react-router-dom';

const GreenspaceCard = (props: Greenspace) => {
  let seekingFarmer;
  // let gsBackgroundImg;
  if (props.farmerDesired) {
    seekingFarmer = (
      <Link
        to={`/greenspace/${props.id}#become-a-farmer`}
        className="dib ph2 pv2 ba bw1 br-pill dim link no-underline light-red"
      >
        Seeking Farmer
      </Link>
    );
  } else {
    seekingFarmer = (
      <Link to={`/greenspace/${props.id}#farmers`} className="dib ph2 pv2 ba bw1 br-pill dim link no-underline green">
        See The Farmers
      </Link>
    );
  }

  // if
  return (
    <div className="mt3" style={{ width: '23%', marginRight: '2%', marginBottom: '48px' }}>
      <section className="h-100 bg-near-white br3 shadow-5">
        <div
          className="h4 w-100 bg-center cover bg-dark-green br3 br--top"
          style={props.mainImage ? { backgroundImage: `url(/public/images/${props.mainImage})` } : null}
        />
        <Link
          to={`/greenspace/${props.id}`}
          className="db w-75 center mt4 mb3 pv2 br-pill bg-green link dim tc f4 avenir b white no-underline"
        >
          See Greenspace
        </Link>
        <div className="pt0 pb2 tc">
          <h3 className="mv0 ph3  f4 lh-copy ttc avenir">{props.name}</h3>
          <h5 className="mt2 mb3 ph3 f5 lh-title normal avenir">{props.address}</h5>
          <div className="bt b--black-10">
            <div className="flex mt4 mb2 ph4 avenir">
              <ul className="w-50 mt0 pl3 tl">
                {props.plotSize.map(tag => (
                  <li key={tag} className="mv1 ttc f6 dark-green">
                    {tag}
                  </li>
                ))}
              </ul>
              <div className="w-50 f7 tc">{seekingFarmer}</div>
            </div>
          </div>
        </div>
      </section>
    </div>
  );
};

export default GreenspaceCard;
<file_sep>// @flow

import React from 'react';
import { Route, Switch } from 'react-router-dom';
import type { Match, RouterHistory } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './store';
import preload from '../data.json';
import Landing from './Landing';
import GlobalHeader from './GlobalHeader';
import Greenspaces from './Greenspaces';
import GreenspaceDetail from './GreenspaceDetail';
import UserDetail from './UserDetail';
import Farmers from './Farmers';

const FourOhFour = () => <h1>404 - Uh Oh Nothing at this address - 404</h1>;

const App = () => (
  <Provider store={store}>
    <div className="app">
      <GlobalHeader />

      <Switch>
        <Route exact path="/" component={Landing} />
        <Route path="/greenspaces" component={props => <Greenspaces greenspaces={preload.greenspaces} {...props} />} />
        <Route
          path="/greenspace/:id"
          component={(props: { match: Match, history: RouterHistory }) => {
            const selectedGreenspace = preload.greenspaces.find(
              (greenspace: Greenspace) => greenspace.id === props.match.params.id
            );
            return <GreenspaceDetail greenspace={selectedGreenspace} history={props.history} {...props} />;
          }}
        />
        <Route
          path="/user/:id"
          component={(props: { match: Match }) => {
            const selectedUser = preload.users.find((user: User) => user.id === props.match.params.id);
            return <UserDetail user={selectedUser} {...props} />;
          }}
        />
        <Route
          path="/farmers"
          component={props => {
            const farmers = preload.users.filter((user: User) => user.farmer).map((farmer: User) =>
              Object.assign(
                {},
                {
                  userName: farmer.userName,
                  id: farmer.id,
                  bio: farmer.bio,
                  profileImage: farmer.profileImage,
                  community: farmer.community,
                  experience: farmer.farmingExperienceLevel,
                  skills: farmer.farmingSkills
                }
              )
            );
            return <Farmers farmers={farmers} {...props} />;
          }}
        />
        <Route component={FourOhFour} />
      </Switch>
    </div>
  </Provider>
);

export default App;
<file_sep>// @flow

import { updateFilter, composeFilters, resolveFiltersState } from '../utils';

const farmerDesiredFilter = {
  initial: {
    yes: true,
    no: true
  },
  yes: {
    yes: true,
    no: false
  },
  no: {
    yes: false,
    no: true
  }
};

// tests binary filter - user selecting btn with 'any' value
test('updateFilter with binaryFilters - test for value = any', () => {
  // initial state
  expect(updateFilter('any', farmerDesiredFilter.initial, true)).toEqual(farmerDesiredFilter.initial);
  expect(updateFilter('any', farmerDesiredFilter.yes, true)).toEqual(farmerDesiredFilter.initial);
  expect(updateFilter('any', farmerDesiredFilter.no, true)).toEqual(farmerDesiredFilter.initial);
});

// tests binary filter - user selecting btn with 'yes' value
test('updateFilter with binaryFilters - test for value = yes', () => {
  // initial state
  expect(updateFilter('yes', farmerDesiredFilter.initial, true)).toEqual(farmerDesiredFilter.yes);
  expect(updateFilter('yes', farmerDesiredFilter.yes, true)).toEqual(farmerDesiredFilter.yes);
  expect(updateFilter('yes', farmerDesiredFilter.no, true)).toEqual(farmerDesiredFilter.yes);
});

// tests binary filter - user selecting btn with 'yes' value
test('updateFilter with binaryFilters - test for value = no', () => {
  // initial state
  expect(updateFilter('no', farmerDesiredFilter.initial, true)).toEqual(farmerDesiredFilter.no);
  expect(updateFilter('no', farmerDesiredFilter.yes, true)).toEqual(farmerDesiredFilter.no);
  expect(updateFilter('no', farmerDesiredFilter.no, true)).toEqual(farmerDesiredFilter.no);
});

// test general/non-binary filter
const exampGeneralFilters = {
  initial: {
    initial: true,
    filter1: true,
    filter2: true,
    filter3: true
  },
  fromInitialSel1: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: false
  },
  from1Sel2: {
    initial: false,
    filter1: true,
    filter2: true,
    filter3: false
  },
  from2And1Sel2: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: false
  }
};

test('updateFilter with non-binary: filter1 selected from initial state', () => {
  expect(updateFilter('filter1', exampGeneralFilters.initial)).toEqual(exampGeneralFilters.fromInitialSel1);
});

test('updateFilter with non-binary: filter2 selected from filter1 already selected', () => {
  expect(updateFilter('filter2', exampGeneralFilters.fromInitialSel1)).toEqual(exampGeneralFilters.from1Sel2);
});

test('updateFilter with non-binary: filter2 selected from filter1 and filter2 already selected', () => {
  expect(updateFilter('filter2', exampGeneralFilters.from1Sel2)).toEqual(exampGeneralFilters.from2And1Sel2);
});

test('updateFilter with non-binary: any selected from filter1 and filter2 already selected', () => {
  expect(updateFilter('any', exampGeneralFilters.from1Sel2)).toEqual(exampGeneralFilters.initial);
});

// testing composeFilters

const preComposeFilters = {
  nonBinary: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: false
  },
  binary: {
    yes: false,
    no: true
  }
};

const postComposeFilters = {
  nonBinary: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: true
  },
  binary: {
    yes: true,
    no: false
  }
};

test('composeFilters with one binary filter, one non-binary', () => {
  expect(
    composeFilters(
      ['nonBinary', 'binary'],
      [updateFilter('filter3', exampGeneralFilters.fromInitialSel1), updateFilter('yes', farmerDesiredFilter.no, true)],
      preComposeFilters
    )
  ).toEqual(postComposeFilters);
});

// testing resolveFiltersState

const preResolveFilters = {
  nonBinary: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: false
  },
  binary: {
    yes: true,
    no: false
  }
};

const postResolveFilters1 = {
  nonBinary: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: true
  },
  binary: {
    yes: true,
    no: false
  }
};

const postResolveFilters2 = {
  nonBinary: {
    initial: false,
    filter1: true,
    filter2: false,
    filter3: true
  },
  binary: {
    yes: false,
    no: true
  }
};

test('test resovleFiltersState() with option change to 1 non-binary filter, followed by 1 binary filter in 2 filter state', () => {
  expect(resolveFiltersState('filter3', preResolveFilters)).toEqual(postResolveFilters1);
  expect(resolveFiltersState('no', postResolveFilters1)).toEqual(postResolveFilters2);
});
<file_sep>// @flow

import React from 'react';

export const PageBtns = (props: { dataLength: number, cardsPerPage: number, clickHandler: Function }) => {
  // $FlowFixMe
  const passValueToHandler = (event: SyntheticEvent<*>) => props.clickHandler(event.target.innerHTML);
  return (
    <div>
      {Array.from({ length: Math.ceil(props.dataLength / props.cardsPerPage) }, (v, i) => i + 1).map(val => (
        <button key={val} onClick={passValueToHandler}>
          {val}
        </button>
      ))}
    </div>
  );
};

export const NextPreviousBtns = (props: {
  pageNumber: number,
  dataLength: number,
  cardsPerPage: number,
  clickHandler: Function
}) => {
  const totalPages = Math.ceil(props.dataLength / props.cardsPerPage);
  // $FlowFixMe
  const passValueToHandler = (event: SyntheticEvent<*>) => props.clickHandler(event.target.name);
  const previousBtn = (
    <button
      name={props.pageNumber - 1}
      onClick={passValueToHandler}
      className="w4 pa3 mr4 tr pointer bg-white ba b--black bg-animate hover-bg-black hover-white v-top"
    >
      <svg className="w1 mt1 fl" data-icon="chevronLeft" viewBox="0 0 32 32" style={{ fill: 'currentcolor' }}>
        <title>chevronLeft icon</title>
        <path d="M20 1 L24 5 L14 16 L24 27 L20 31 L6 16 z" />
      </svg>
      <span className="dib mt1">Previous</span>
    </button>
  );
  const nextBtn = (
    <button
      name={props.pageNumber + 1}
      onClick={passValueToHandler}
      className="w4 pa3 mr4 tl pointer bg-white ba b--black bg-animate hover-bg-black hover-white v-top"
    >
      <span className="dib mt1">Next</span>
      <svg className="w1 mt1 fr" data-icon="chevronRight" viewBox="0 0 32 32" style={{ fill: 'currentcolor' }}>
        <title>chevronRight icon</title>
        <path d="M12 1 L26 16 L12 31 L8 27 L18 16 L8 5 z" />
      </svg>
    </button>
  );
  const pageOfPages = (
    <h5 className="dib black avenir f5" style={{ lineHeight: '2.4' }}>{`${props.pageNumber} of ${totalPages}`}</h5>
  );
  return (
    <div className="avenir v-btm">
      {props.pageNumber > 1 ? previousBtn : null}
      {props.pageNumber < totalPages ? nextBtn : null}
      {totalPages > 1 ? pageOfPages : null}
    </div>
  );
};
<file_sep>// @flow

import React from 'react';

const FilterButton = (props: { filterSelected: boolean, val: string, text: string, updateFilterOption: Function }) => {
  const selectedFilterArray = ['b--near-black', 'bb', 'bw2', 'bt-0', 'br-0', 'bl-0'];
  return (
    <button
      className={`${
        props.filterSelected === true ? selectedFilterArray.join(' ') : 'deSelectedFilter'
      } mr2 bw0 bg-transparent avenir f6 pointer`}
      value={props.val}
      onClick={props.updateFilterOption}
    >
      {props.text}
    </button>
  );
};

export default FilterButton;
<file_sep>// @flow

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { setGreenspacesFilters } from './actionCreators';
import GreenspaceCardList from './GreenspaceCardList';
import Filters from './utilComponents/Filters';
import NoFilteredEntitiesMsg from './utilComponents/NoFilteredEntitiesMsg';
import { NextPreviousBtns } from './utilComponents/PageControls';
import { passFilterUpdateToSetter, setFilter, getSelectedFiltersMap } from './utils';

type Props = { greenspaces: Array<Greenspace>, filters: genFilters, filtersSetter: Function };

type State = { pageNumber: number, filteredGreenspaces: Array<Greenspace> };

class Greenspaces extends Component<Props, State> {
  state = {
    pageNumber: 1,
    filteredGreenspaces: this.props.greenspaces
  };

  componentWillReceiveProps(nextProps) {
    // if filters will be changed, then we need to reset the current page to page 1
    if (this.props.filters !== nextProps.filters) {
      this.updatePage(1);
      this.updateFilteredGreenspaces(nextProps.filters);
    }
  }

  updatePage = (val: number) => this.setState({ pageNumber: parseInt(val, 10) });
  updateFilteredGreenspaces = updatedFilters => {
    this.setState({ filteredGreenspaces: this.filterGreenspaces(updatedFilters) });
  };

  // try refactor using class polymorphism
  passesFarmerDesiredFilter = (farmerDesired: boolean, selectedFilters: Array<string>) => {
    if (selectedFilters.includes('yes')) {
      // If no is also a selected filter than filters.farmerDesired is effectively 'either' and we don't filter any greenspaces
      if (selectedFilters.includes('no')) {
        return true;
      }
      // greenspace.farmerDesired(true) === selectedFilters only element('yes')
      if (farmerDesired) {
        return true;
      }
      // greenspace.farmerDesired(false) !== selectedFilters only element('yes')
      return false;
    }
    // greenspace.farmerDesired(false) === selectedFilters only element('no')
    if (!farmerDesired) {
      return true;
    }
    return false;
  };

  passesPlotSizeFilter = (plotSize: Array<plotSize>, selectedFilters: Array<string>) => {
    // if selectedFilters includes initial, then we return any filter - state is initial and nothing is being filtered out
    if (selectedFilters.includes('initial')) {
      return true;
    }
    const selectedFiltersIncludesPlotSize = plotSize.some(plotSizeTag => selectedFilters.includes(plotSizeTag));
    if (selectedFiltersIncludesPlotSize) {
      return true;
    }
    return false;
  };

  filterGreenspaces = updatedFilters => {
    const selectedFiltersMap: { plotSize: Array<string>, farmerDesired: Array<string> } = getSelectedFiltersMap(
      updatedFilters
    );
    const filteredGreenspaces = this.props.greenspaces.filter(
      (greenspace: Greenspace) =>
        this.passesFarmerDesiredFilter(greenspace.farmerDesired, selectedFiltersMap.farmerDesired) &&
        this.passesPlotSizeFilter(greenspace.plotSize, selectedFiltersMap.plotSize)
    );
    return filteredGreenspaces;
  };

  render() {
    const cardsPerPage = 8;
    return (
      <section className="ph5">
        <Filters
          filterCat="greenspaces"
          filters={this.props.filters}
          updateFilters={resolveFiltersParams =>
            passFilterUpdateToSetter(resolveFiltersParams, this.props.filtersSetter)
          }
        />
        <NoFilteredEntitiesMsg entities={this.state.filteredGreenspaces} entitiesType="greenspaces" />
        <GreenspaceCardList
          greenspaceCardList={this.state.filteredGreenspaces}
          currentPageNumber={this.state.pageNumber}
          cardsPerPage={cardsPerPage}
        />
        <div className="pl5">
          <NextPreviousBtns
            pageNumber={this.state.pageNumber}
            dataLength={this.state.filteredGreenspaces.length}
            cardsPerPage={cardsPerPage}
            clickHandler={this.updatePage}
          />
        </div>
      </section>
    );
  }
}

const mapStateToProps = state => ({ filters: state.greenspacesFilters });
const mapDispatchToProps = (dispatch: Function) => ({
  filtersSetter(resolveFiltersParams: resolveFiltersParams) {
    setFilter(resolveFiltersParams, setGreenspacesFilters, dispatch);
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(Greenspaces);
<file_sep>// @flow

import React from 'react';
import preload from '../data.json';
import GreenspaceCardList from './GreenspaceCardList';

const UserGreenspaceView = (props: { greenspaceOwner: GreenspaceOwnerBrief }) => {
  const ownedGreenspaces = preload.greenspaces.filter(
    (greenspace: Greenspace) =>
      typeof props.greenspaceOwner.ownedPropertyIDs !== 'undefined' &&
      props.greenspaceOwner.ownedPropertyIDs.includes(greenspace.id)
  );
  const lendingGreenspacesTitle = (
    <h3 className="mt5 mb0 f3 avenir near-black">{`Greenspaces ${props.greenspaceOwner.userName} is Lending`}</h3>
  );
  const lendingGreenspacesList = (
    <div className="ph5">
      <GreenspaceCardList greenspaceCardList={ownedGreenspaces} />
    </div>
  );
  let landownerParticipationInfo;
  let participationColor = 'green';
  if (props.greenspaceOwner.desiredLandOwnerParticipation) {
    if (props.greenspaceOwner.desiredLandOwnerParticipation === 'hands-off') {
      participationColor = 'near-white';
    } else if (props.greenspaceOwner.desiredLandOwnerParticipation === 'helping hand') {
      participationColor = 'near-black';
    }
  }
  if (props.greenspaceOwner.desiredLandOwnerParticipation) {
    landownerParticipationInfo = (
      <div>
        <h5 className="mb3 f5 avenir i">Farming Participation:</h5>
        <h5 className={`mt0 dib pv2 ph3 ba b--${participationColor} bw2 f5 avenir`}>
          {props.greenspaceOwner.desiredLandOwnerParticipation}
        </h5>
      </div>
    );
  }
  return (
    <section>
      <div className="ph5">
        {landownerParticipationInfo}
        <p className="mw8 lh-copy avenir">{props.greenspaceOwner.bio}</p>
        {ownedGreenspaces.length ? lendingGreenspacesTitle : null}
      </div>
      {ownedGreenspaces.length ? lendingGreenspacesList : null}
    </section>
  );
};

export default UserGreenspaceView;
<file_sep>// @flow

// easings, more here as needed
export const cubicOut = (time: number) => {
  let t = time;
  t -= 1;
  return t * t * t + 1;
};

// easings grouping
export const easings = { cubicOut };

// to dom element scrolling
export const scrollToElem = (
  targetElem: HTMLElement | null = null,
  duration: number = 300,
  easingFunction: Function = (time: number) => time,
  verticalCompensation: number = 0,
  event: SyntheticEvent<*> | null = null
) => {
  if (targetElem !== null) {
    if (event !== null) {
      event.preventDefault();
    }
    const startPosition = window.pageYOffset;
    const startTime = new Date().getTime();
    // $FlowFixMe
    const documentHeight = document.body.scrollHeight;
    const windowHeight = window.innerHeight;
    const destinationOffset = targetElem.offsetTop + verticalCompensation;
    let destinationScroll;
    if (documentHeight - destinationOffset < windowHeight) {
      destinationScroll = documentHeight - windowHeight;
    } else {
      destinationScroll = destinationOffset;
    }

    // if we don't have access to requestAnimationFrame then we just scroll
    if ('requestAnimationFrame' in window === false) {
      return window.scrollTo(0, targetElem.offsetTop);
    }
    const scroll = () => {
      const now = new Date().getTime();
      const time = Math.min(1, (now - startTime) / duration);
      window.scroll(0, Math.ceil(easingFunction(time) * (destinationScroll - startPosition) + startPosition));

      if (window.pageYOffset === destinationScroll) {
        return;
      }

      requestAnimationFrame(scroll);
    };

    return scroll();
  }
  return null;
};

// takes a per-page slice of paginated content
export const paginationSlice = (items: Array<Object>, pageNumber: number, cardsPerPage: number) =>
  items.slice(pageNumber * cardsPerPage - (cardsPerPage - 1) - 1, pageNumber * cardsPerPage);

/**
 * Takes state of a single filter object and return array of the names of the selected filter options
 * -- options are props of the filter object; if their value is true they are selected
 * @param { {[filter: string]: {[filterOption: string]: boolean}} } filter - the filter object whose options are evaluated for selection
 */
const getSelectedFilterOptions = (filter: { [filterOption: string]: boolean }): Array<string> => {
  const selectedFilterOptionKeys = Object.keys(filter).filter(filterOptionKey => filter[filterOptionKey]);
  return selectedFilterOptionKeys;
};

/**
 * takes a general 3d filters object and simplifies it to a 2d object
 * - each prop key is the filter it represents. Each prop value is an array of the selected filter options for that filter
 * @param {genFilters or { [filterType: string]: {[filter: string]: boolean} }} filters to map to the simpler filtersMapObj
 */
export const getSelectedFiltersMap = (filters: genFilters): { [filter: string]: Array<string> } => {
  const filtersMapObj = {};
  Object.keys(filters).forEach((filter: string) => {
    const selectedFilterOptions = getSelectedFilterOptions(filters[filter]);
    filtersMapObj[filter] = selectedFilterOptions;
  });
  return filtersMapObj;
};

export const filterStateIsInitial = (selectedFilterOptions: Array<string>) => {
  if (selectedFilterOptions.includes('initial')) {
    return true;
  }
  return false;
};

/**
 * => initialState obj for filters
 * @param {Object with any num props of any name of type Array<string> } filters - filter objects, keys are the filter name,
 *  vals are array of strings that translate to filter options
 * Each prop of filters becomes a prop of returned initialState obj
 * Each val in filters.<prop> becomes a prop of initialState.<prop>,
 *  all initialState.<prop> child props are given val true
 */
export const initializeFilterState = (filters: {}): {} => {
  const initialState = {};
  Object.keys(filters).forEach(key => {
    const filterOptions = {};
    filters[key].forEach(innerKey => {
      filterOptions[innerKey] = true;
    });
    initialState[key] = filterOptions;
  });
  return initialState;
};

const isFilterBinary = (filterState: {}) => Object.keys(filterState).length === 2;

/*
function filterFromOption (option: string, filterState: {}): string | null {
  let filter: string | null = null;
  Object.keys(filterState).forEach(key => {
    if (Object.keys(filterState[key]).includes(option)) {
      filter = key;
    }
  });
  return filter;
};
*/

const turnFiltersOff = (filters: { [filter: string]: string } | { yes: boolean, no: boolean }) => {
  const updatedFilters = {};
  Object.keys(filters).forEach(key => {
    // only add/set prop if prop is not already true
    if (!filters[key]) {
      updatedFilters[key] = true;
    }
  });
  return Object.assign({}, filters, updatedFilters);
};

// for updating a filter with two options, one of which, if true, sets the other to false:
const updateBinaryFilter = (
  filter: 'yes' | 'no' | 'binaryBoth',
  filtersState: { yes: boolean, no: boolean }
): { yes: boolean, no: boolean } => {
  if (filter === 'binaryBoth') {
    return turnFiltersOff(filtersState);
  }
  if (filter === 'yes') {
    return Object.assign({}, filtersState, { yes: true, no: false });
  }
  // return for 'no' selected is default
  return Object.assign({}, filtersState, { yes: false, no: true });
};

export const updateFilter = (
  filter: 'initial' | string,
  filtersState: { [filter: string]: boolean },
  isBinaryFilter: boolean = false
): {} => {
  if (isBinaryFilter) {
    // $FlowFixMe
    return updateBinaryFilter(filter, filtersState);
  }
  // if filter is any, we set all filters to true
  if (filter === 'initial') {
    // setting all filters to true
    return turnFiltersOff(filtersState);
  }
  // if filtersState.initial is true, then a filter btn filters result to only that filter
  if (filtersState.initial) {
    const changedFiltersState = {};
    Object.keys(filtersState).forEach(key => {
      if (key !== filter) {
        changedFiltersState[key] = false;
      }
    });
    return Object.assign({}, filtersState, changedFiltersState);
  }
  // if the current filter - which isn't initial - is the only true/on filter, if we toggle it off then let's return to initial state
  // - this prevents filtering down to no results:
  if (Object.values(filtersState).filter(filterVal => filterVal).length === 1 && filtersState[filter]) {
    return turnFiltersOff(filtersState);
  }
  // if filtersState.initial is not true, filter btns toggle that filter
  // creating single prop object - props with key of filter and opposite value of filter in current filter state
  const newFilter = {};
  // $FlowFixMe
  newFilter[filter] = !filtersState[filter];
  return Object.assign({}, filtersState, newFilter);
};

export const resolveFiltersState = (
  option: string,
  targetFilterName: string,
  filterState: { [filterType: string]: { [filter: string]: boolean } }
): {} => {
  const targetFilter: { [filter: string]: boolean } = filterState[targetFilterName];
  const updatedFiltersState = {};
  updatedFiltersState[targetFilterName] = updateFilter(option, targetFilter, isFilterBinary(targetFilter));
  return Object.assign({}, filterState, updatedFiltersState);
};

/**
 * return nested object representing state of all filters for view/component
 * @param {Array<string>} filterNames - the name of each filter type to be composed, will be prop key in returned obj
 * @param {Array<{any}>} filterStates - the states of each filter in return filters obj, should be in same ordered as filterNames
 * @param {Object<any>} currentFilters - the current state of view/component filters - optional
 */
export const composeFilters = (
  filterNames: Array<string>,
  filterStates: Array<{ [filterType: string]: { [filter: string]: string } }>,
  currentFilters: {} = {}
): {} => {
  const newFilters = {};
  filterNames.forEach((name: string, index: number) => {
    newFilters[name] = filterStates[index];
  });
  return Object.assign({}, currentFilters, newFilters);
};

/**
 * side-effect only function, passes params needed to setFilter state to the filterSetter supplied
 * @param {string} filter - the name of the filter being changed
 * @param {*} filterType - nae of the filter-type or filter-parent of the updated filter
 * @param {*} filterState - state of the all filters for the component
 * @param {*} filtersSetter - setter function which sends the evaluated filter state to the dispatcher
 */
export const passFilterUpdateToSetter = (resolveFiltersParams: resolveFiltersParams, filtersSetter: Function) => {
  filtersSetter(resolveFiltersParams);
};

/**
 * -- generalized prop-to-store dispatch handler, for use in body of mapDispatchToProps
 * side-effect only function, passes result of resolveFilterState() to provided actionCreator on through provided dispatch()
 * @param {*} resolveFilterParams - params for resolveFilterState() - a util function
 * @param {*} dispatch - dispatch function to pass: should be 'dispatch' param of mapDispatchToProps
 * @param {*} actionCreator - actionCreator to pass result of resolveFilterParam: should be an exported member of ./actionCreators.js
 */
export const setFilter = (resolveFiltersParams: resolveFiltersParams, actionCreator: Function, dispatch: Function) => {
  dispatch(
    actionCreator(
      resolveFiltersState(
        resolveFiltersParams.filter,
        resolveFiltersParams.filterType,
        resolveFiltersParams.filterState
      )
    )
  );
};

/**
 * return value of optional computed object property
 * @param {Object<any>} baseObject - the base object to test props against
 * @param {Array<string>} propNames - array of nested props that could exist on base object, if 1 prop provided, will return val of that prop, if 2 props, will return value of second prop within first prop, etc.
 * @param {boolean} undefinedIfNoProp - if true - if prop does not exist return undefined - otherwise return null; this is for use when returning directly in JSX component prop assignment
 */

export const optionalComputedPropVal = (
  baseObj: any,
  propNames: Array<string>,
  undefinedIfNoProp: boolean = false
): any | null | typeof undefined => {
  const noProp = undefinedIfNoProp ? undefined : null;
  // we're always going to test the base object against the 0'th index of propNames, so here's a shorthand
  const hasPropShorthand = () => {
    if (Object.prototype.hasOwnProperty.call(baseObj, propNames[0])) {
      return true;
    }
    return false;
  };

  if (propNames.length === 1) {
    // if we only have one prop to test - we return value of baseObj[deepestProperty] if test passes; on fail we return noProp val
    if (hasPropShorthand()) {
      return baseObj[propNames[0]];
    }
    return noProp;
  }

  if (hasPropShorthand()) {
    // If we have more than one propName and the baseObj has propNames[0], we re-run the function with baseObj[propNames[0]] as the new base object,
    // we slice propNames by just the first index to return a property list without the first property we've made the new baseObj with
    // and we pass the undefinedIfNoProp val along as is.
    return optionalComputedPropVal(baseObj[propNames[0]], propNames.slice(1), undefinedIfNoProp);
  }

  return noProp;
};
<file_sep>// @flow

export const SET_GREENSPACES_FILTERS = 'SET_GREENSPACES_FILTERS';

export const SET_FARMERS_FILTERS = 'SET_FARMERS_FILTERS';
<file_sep>// @flow

import React from 'react';

const NoFilteredEntitiesMsg = (props: {
  entities: UserFilteredEntities,
  entitiesType?: 'greenspaces' | 'farmers' | 'default'
}) => {
  const messages = {
    default: "Sorry, it doesn't look like we have what you're looking for.",
    greenspaces: "Sorry, we don't have any greenspaces like that yet.",
    farmers: "Sorry, we don't have any farmers like that yet."
  };

  const msgToRender = <h2>{props.entitiesType ? messages[props.entitiesType] : messages.default}</h2>;
  return <section>{!props.entities.length ? msgToRender : null}</section>;
};

NoFilteredEntitiesMsg.defaultProps = {
  entitiesType: 'default'
};

export default NoFilteredEntitiesMsg;
<file_sep>// @flow

import React from 'react';
import FilterButton from './FilterButton';

const FilterButtonRow = (props: {
  filter: string,
  filterState: {},
  filterOptions: Array<string>,
  changeFilter: Function,
  btnTextArr?: Array<string>,
  includesBinaryBoth?: boolean,
  binaryBothBtnText?: string
}) => {
  // pass filterValue and target filter up to parent
  const passFilterValue = event => props.changeFilter(event.target.value, props.filter);
  const filterStateVals = Object.keys(props.filterState).map((filterKey: string) => props.filterState[filterKey]);
  const genFilterSelectedTest = filter => {
    // $FlowFixMe
    if (filter === 'initial') return props.filterState[filter];
    return props.filterState[filter] && !props.filterState[props.filterOptions[0]];
  };
  const binaryFilterSelectedTest = filter => props.filterState[filter] && filterStateVals.includes(false);
  const binaryBothBtn = (
    <FilterButton
      filterSelected={!filterStateVals.includes(false)}
      key="binaryBoth"
      val="binaryBoth"
      text={props.binaryBothBtnText ? props.binaryBothBtnText : 'Either'}
      updateFilterOption={passFilterValue}
    />
  );
  return (
    <nav className="flex justify-start">
      {props.includesBinaryBoth ? binaryBothBtn : null}
      {props.filterOptions.map((filter: string, index) => (
        <FilterButton
          filterSelected={props.includesBinaryBoth ? binaryFilterSelectedTest(filter) : genFilterSelectedTest(filter)}
          key={filter}
          val={filter}
          // $FlowFixMe - possibly undefined value sufficiently covered by defaultProps
          text={props.btnTextArr.length >= index ? props.btnTextArr[index] : filter}
          updateFilterOption={passFilterValue}
        />
      ))}
    </nav>
  );
};

FilterButtonRow.defaultProps = { btnTextArr: [], includesBinaryBoth: false, binaryBothBtnText: 'Either' };

export default FilterButtonRow;
<file_sep>// @flow

import React from 'react';
import { configure, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import Filters from '../utilComponents/Filters';
import initialState from '../initialState';

configure({ adapter: new Adapter() });

const gsFiltersPropsHandOff = (option: string, filter: string, filters: {}) => console.log(option, filter, filters);
const shallowGsFilters = render(
  <Filters filterCat="greenspaces" filters={initialState.filters.greenspaces} updateOptions={gsFiltersPropsHandOff} />
);

describe('gsFilters-suite', () => {
  it('Should contain 11 buttons', () => {
    expect(shallowGsFilters.find('button').length).toBe(11);
  });
});
<file_sep>// @flow

import { combineReducers } from 'redux';
import initialState from './initialState';
import { SET_GREENSPACES_FILTERS, SET_FARMERS_FILTERS } from './actions';

const user = (state = { id: '<KEY>' }, action: Action) => {
  if (action) {
    return state;
  }
  return state;
};

const greenspacesFilters = (state = initialState.filters.greenspaces, action: Action) => {
  if (action.type === SET_GREENSPACES_FILTERS) {
    return Object.assign({}, state, action.payload);
  }
  return state;
};
const farmersFilters = (state = initialState.filters.farmers, action: Action) => {
  if (action.type === SET_FARMERS_FILTERS) {
    return Object.assign({}, state, action.payload);
  }
  return state;
};

const rootReducer = combineReducers({ user, greenspacesFilters, farmersFilters });

export default rootReducer;
