├── .babelrc ├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── api ├── index.js ├── initApollo.js └── withApolloClient.js ├── components ├── ErrorMessage │ ├── ErrorMessage.js │ ├── ErrorMessage.styles.js │ └── index.js ├── HandPicked │ ├── HandPicked.jsx │ ├── HandPicked.styles.js │ ├── HandPickedList.jsx │ ├── data.json │ └── index.js ├── Home │ ├── Home.jsx │ ├── Home.styles.js │ └── index.js ├── PetBio │ ├── PetBio.jsx │ ├── PetBio.styles.js │ └── index.js ├── PetCard │ ├── PetCard.jsx │ ├── PetCard.styles.js │ └── index.js ├── PetDetails │ ├── PetDetails.jsx │ ├── PetDetails.styles.js │ └── index.js ├── PetMediaSlider │ ├── PetMediaSlider.jsx │ ├── PetMediaSlider.styles.js │ └── index.js ├── PetOptions │ ├── PetOptions.jsx │ ├── PetOptions.styles.js │ └── index.js ├── Placeholder │ ├── Placeholder.jsx │ └── index.js ├── Search │ ├── Search.jsx │ ├── Search.styles.js │ └── index.js ├── SearchFilters │ ├── Filters │ │ ├── animal-filters.js │ │ ├── barnyard-filters.js │ │ ├── bird-filters.js │ │ ├── cat-filters.js │ │ ├── common-filters.js │ │ ├── dog-filters.js │ │ ├── horse-filters.js │ │ ├── index.js │ │ ├── other-filters.js │ │ ├── rabbit-filters.js │ │ └── small-furry-filters.js │ ├── SearchFilters.jsx │ ├── SearchFilters.styles.js │ └── index.js ├── SearchResults │ ├── SearchList.jsx │ ├── SearchResults.jsx │ ├── SearchResults.styles.js │ ├── data.json │ └── index.js ├── SelectBox │ ├── SelectBox.jsx │ ├── SelectBox.styles.js │ └── index.js ├── ShelterDetails │ ├── ShelterDetails.jsx │ ├── ShelterDetails.styles.js │ └── index.js ├── ShelterInfoBar │ ├── ShelterInfoBar.jsx │ ├── ShelterInfoBar.styles.js │ └── index.js ├── ShelterMap │ ├── ShelterMap.jsx │ ├── ShelterMap.styles.js │ └── index.js ├── ShelterPets │ ├── ShelterPets.jsx │ ├── ShelterPets.styles.js │ └── index.js ├── Sidebar │ ├── Sidebar.jsx │ ├── Sidebar.styles.js │ └── index.js ├── styles.js └── theme.js ├── helpers ├── hooks │ ├── index.js │ ├── useFilters.js │ ├── useGeoLocation.js │ └── useSearch.js └── index.js ├── jsconfig.json ├── layouts ├── MainLayout.js ├── MainLayout.styles.js ├── index.js └── logo-adoption.svg ├── next.config-dev.js ├── package.json ├── pages ├── _app.js ├── _document.js ├── _error.js ├── index.js ├── petDetails.js └── shelterDetails.js ├── queries ├── index.js ├── petDetails.query.js ├── petFind.query.js ├── shelterDetails.query.js └── shelterGetPets.query.js ├── screenshoots ├── 404.jpg ├── home.jpg └── search.jpg ├── server.js └── static └── images ├── bg.png ├── city.png ├── close.svg ├── dog-right.jpg ├── girl-and-dog.png ├── icons ├── 001-chicken-1.svg ├── 001-chicken-2.svg ├── 001-dog-1.svg ├── 001-dog-2.svg ├── 001-horse-1.svg ├── 001-horse-2.svg ├── 003-halloween-black-cat.svg ├── 003-track.svg ├── 004-pawprint.svg ├── 005-animal-paw-print.svg ├── 005-cat-1.svg ├── 005-cat-2.svg ├── 006-cat.svg ├── 006-horse-4.svg ├── 006-horse-5.svg ├── 007-cat-1.svg ├── 008-pet.svg ├── 009-bird-1.svg ├── 009-bird-2.svg ├── 009-pet-1.svg ├── 010-cat-2.svg ├── 011-cat-3.svg ├── 011-snake-1.svg ├── 011-snake-2.svg ├── 013-rabbit-1.svg ├── 013-rabbit-2.svg ├── 013-snake-2.svg ├── 014-small-fury.svg ├── 015-rabbit-2.svg ├── 016-rabbit-3.svg ├── 017-cat-house.svg ├── 018-pet-shelter.svg └── 019-animal-shelter.svg ├── image-18.jpg ├── logo-pati.svg ├── search.svg └── searching.gif /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["next/babel"], 5 | "plugins": [ 6 | "inline-react-svg", 7 | "react-element-info", 8 | ["module-resolver", { 9 | "root": ["./"], 10 | "alias": { 11 | "@helpers": "./helpers", 12 | "@api": "./api", 13 | "@components": "./components", 14 | "@styles": "./components/styles", 15 | "@theme": "./components/theme", 16 | "@static": "./static", 17 | "@queries": "./queries" 18 | } 19 | }] 20 | ] 21 | }, 22 | "production": { 23 | "presets": ["next/babel"], 24 | "plugins": [ 25 | "inline-react-svg", 26 | "transform-remove-console", 27 | ["module-resolver", { 28 | "root": ["./"], 29 | "alias": { 30 | "@helpers": "./helpers", 31 | "@queries": "./queries", 32 | "@api": "./api", 33 | "@components": "./components", 34 | "@styles": "./components/styles", 35 | "@theme": "./components/theme", 36 | "@static": "./static" 37 | } 38 | }] 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "es6": true 5 | }, 6 | "plugins": [ 7 | "react", 8 | "react-hooks" 9 | ], 10 | "parserOptions": { 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "react-hooks/rules-of-hooks": "error" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | .next 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | yarn.lock 26 | 27 | next.config.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Abdullah Musab Ceylan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ac-react-adoption 2 | Don't buy, adopt! 3 | 4 | Pet Adoption is a small application built with [React.js](https://github.com/facebook/react), [next.js](https://github.com/zeit/next.js), [React Hooks](https://reactjs.org/docs/hooks-intro.html), [Apollo](https://www.apollographql.com), [GraphQL](https://graphql.org/), [styled-components](https://github.com/styled-components/styled-components) and [petfinderQL](https://github.com/abdullahceylan/petfinderQL).

5 | 6 | ## Preview 7 | 8 | ### Homepage 9 | ![Preview](https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/master/screenshoots/home.jpg) 10 | 11 | ### Search 12 | ![Preview](https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/master/screenshoots/search.jpg) 13 | 14 | ### Pet and Shelter details 15 | ![Preview](https://i.gyazo.com/0ba5ba3175b87287cdb768e468d23474.gif) 16 | 17 | ### 404 18 | ![Preview](https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/master/screenshoots/404.jpg) 19 | 20 | ## Running Locally 21 | 22 | 1. Clone this repo 23 | 2. Type `cd ac-react-adoption` to enter the project folder 24 | 3. Run `npm install` or `yarn install` and install dependencies 25 | 4. Rename `next.config-dev.js` to `next.config.js` 26 | 5. To use the Google Maps for the shelter details (also in pet details page), you must get an API key which you can then add to your mobile app, website, or web server. Follow [this link](https://developers.google.com/maps/documentation/javascript/get-api-key) to get a Google Map API Key 27 | 6. Insert your API Key into the `next.config.js` 28 | 7. Run `npm run dev` or `yarn dev` and visit [localhost:3000](http://localhost:3000) 29 | 30 | ## Build 31 | 32 | 1. Run `npm run build` or `yarn build` 33 | 1. The compiled version will be in `/.next/dist/` 34 | 35 | ## Live Example 36 | 37 | You can test on https://ac-react-adoption.herokuapp.com 38 | 39 | ## Note 40 | You may experience some delays when you try to use search function due to Petfinder's very slow API responses. -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import withApolloClient from './withApolloClient'; 2 | 3 | export { withApolloClient }; 4 | -------------------------------------------------------------------------------- /api/initApollo.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, HttpLink } from 'apollo-boost'; 2 | import fetch from 'isomorphic-unfetch'; 3 | import getConfig from 'next/config'; 4 | 5 | // Error while running `getDataFromTree` { Error: Network error: Response not successful: Received status code 400 6 | 7 | const { publicRuntimeConfig } = getConfig(); 8 | 9 | let apolloClient = null; 10 | 11 | // Polyfill fetch() on the server (used by apollo-client) 12 | if (!process.browser) { 13 | global.fetch = fetch; 14 | } 15 | 16 | function create(initialState) { 17 | // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient 18 | return new ApolloClient({ 19 | connectToDevTools: process.browser, 20 | ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once) 21 | link: new HttpLink({ 22 | uri: 'https://ac-petfinderql.herokuapp.com/', // Server URL (must be absolute) 23 | // uri: 'http://localhost:4000/', // Server URL (must be absolute) 24 | credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` 25 | }), 26 | cache: new InMemoryCache().restore(initialState || {}), 27 | // onError: ({ networkError, graphQLErrors, ...rest }) => { 28 | // console.log('graphQLErrors', graphQLErrors); 29 | // console.log('networkError', networkError); 30 | // console.log('rest', rest); 31 | // }, 32 | }); 33 | } 34 | 35 | export default function initApollo(initialState) { 36 | // Make sure to create a new client for every server-side request so that data 37 | // isn't shared between connections (which would be bad) 38 | if (!process.browser) { 39 | return create(initialState); 40 | } 41 | 42 | // Reuse client on the client-side 43 | if (!apolloClient) { 44 | apolloClient = create(initialState); 45 | } 46 | 47 | return apolloClient; 48 | } 49 | -------------------------------------------------------------------------------- /api/withApolloClient.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import initApollo from './initApollo'; 3 | import Head from 'next/head'; 4 | import { getDataFromTree } from 'react-apollo'; 5 | 6 | export default App => { 7 | return class Apollo extends React.Component { 8 | static displayName = 'withApollo(App)'; 9 | static async getInitialProps(ctx) { 10 | const { Component, router } = ctx; 11 | 12 | let appProps = {}; 13 | if (App.getInitialProps) { 14 | appProps = await App.getInitialProps(ctx); 15 | } 16 | 17 | // Run all GraphQL queries in the component tree 18 | // and extract the resulting data 19 | const apollo = initApollo(); 20 | if (!process.browser) { 21 | try { 22 | // Run all GraphQL queries 23 | await getDataFromTree( 24 | , 30 | ); 31 | } catch (error) { 32 | // Prevent Apollo Client GraphQL errors from crashing SSR. 33 | // Handle them in components via the data.error prop: 34 | // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error 35 | console.error('Error while running `getDataFromTree`', error); 36 | } 37 | 38 | // getDataFromTree does not call componentWillUnmount 39 | // head side effect therefore need to be cleared manually 40 | Head.rewind(); 41 | } 42 | 43 | // Extract query data from the Apollo store 44 | const apolloState = apollo.cache.extract(); 45 | 46 | return { 47 | ...appProps, 48 | apolloState, 49 | }; 50 | } 51 | 52 | constructor(props) { 53 | super(props); 54 | this.apolloClient = initApollo(props.apolloState); 55 | } 56 | 57 | render() { 58 | return ; 59 | } 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /components/ErrorMessage/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | StatusCode, 5 | ErrorHeader, 6 | ErrorBody, 7 | ErrorFooter, 8 | ErrorWrapper, 9 | } from './ErrorMessage.styles'; 10 | 11 | const cityImage = '/static/images/city.png'; 12 | const kidImage = '/static/images/girl-and-dog.png'; 13 | 14 | const ErrorMessage = ({ statusCode }) => ( 15 | 16 | {statusCode} 17 | 18 | Sorry, we couldn't find the page that you've requested. 19 | 20 | But, we can try to find a pet for you! 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | ErrorMessage.propTypes = { 28 | statusCode: PropTypes.number.isRequired, 29 | }; 30 | 31 | export default ErrorMessage; 32 | -------------------------------------------------------------------------------- /components/ErrorMessage/ErrorMessage.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, Grid } from '@styles'; 2 | 3 | export const ErrorWrapper = styled(Grid)` 4 | width: 100vw; 5 | height: 100vh; 6 | position: relative; 7 | padding-top: 100px; 8 | `; 9 | 10 | export const ErrorFooter = styled.div` 11 | position: absolute; 12 | bottom: 0; 13 | width: 100vw; 14 | height: 330px; 15 | background-image: url(/static/images/city.png); 16 | background-size: contain; 17 | text-align: center; 18 | 19 | & > img { 20 | height: 330px; 21 | } 22 | `; 23 | 24 | export const StatusCode = styled.h1` 25 | color: #ccc; 26 | text-align: center; 27 | font-size: 137px; 28 | font-weight: bold; 29 | letter-spacing: 3.6px; 30 | line-height: 160px; 31 | margin: 0 0 7px; 32 | `; 33 | 34 | export const ErrorHeader = styled.h3` 35 | color: #ccc; 36 | text-align: center; 37 | font-size: 28px; 38 | line-height: 42px; 39 | font-weight: 600; 40 | margin-bottom: 21px; 41 | `; 42 | 43 | export const ErrorBody = styled.p` 44 | color: #ccc; 45 | text-align: center; 46 | font-size: 27px; 47 | line-height: 24px; 48 | `; 49 | -------------------------------------------------------------------------------- /components/ErrorMessage/index.js: -------------------------------------------------------------------------------- 1 | import ErrorMessage from './ErrorMessage'; 2 | 3 | export default ErrorMessage; 4 | -------------------------------------------------------------------------------- /components/HandPicked/HandPicked.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Query } from 'react-apollo'; 4 | import HandPickedList from './HandPickedList'; 5 | import { petFindQuery } from '@queries'; 6 | import { HandPickedWrapper, HandPickedListWrapper } from './HandPicked.styles'; 7 | 8 | const HandPicked = ({ count }) => { 9 | const petFindQueryVariables = { 10 | location: 'Canada', 11 | count, 12 | }; 13 | 14 | return ( 15 | 20 | {({ loading, error, data }) => { 21 | if (error) return
Error loading pets.
; 22 | 23 | // if (!data.petFind) { 24 | // return null; 25 | // } 26 | 27 | return ( 28 | 29 | 34 | 35 | ); 36 | }} 37 |
38 | ); 39 | }; 40 | 41 | HandPicked.propTypes = { 42 | count: PropTypes.number, 43 | }; 44 | 45 | HandPicked.defaultProps = { 46 | count: 6, 47 | }; 48 | 49 | export default React.memo(HandPicked); 50 | -------------------------------------------------------------------------------- /components/HandPicked/HandPicked.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, Grid, Row } from '@styles'; 2 | 3 | export const HandPickedWrapper = styled(Grid)` 4 | height: 570px; 5 | overflow: hidden; 6 | overflow-y: scroll; 7 | `; 8 | 9 | export const HandPickedListWrapper = styled(Row)``; 10 | -------------------------------------------------------------------------------- /components/HandPicked/HandPickedList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { map } from 'lodash/fp'; 4 | import PetCard from '@components/PetCard'; 5 | import Placeholder from '@components/Placeholder'; 6 | import { Col } from '@styles'; 7 | import { HandPickedListWrapper } from './HandPicked.styles'; 8 | 9 | const HandPickedList = ({ pets, placeholderCount, loading }) => ( 10 | <> 11 | 12 | {loading && 13 | Array(placeholderCount) 14 | .fill('') 15 | .map((p, i) => ( 16 | 17 | 18 | 19 | ))} 20 | {!loading && 21 | map( 22 | pet => ( 23 | 24 | 25 | 26 | ), 27 | pets, 28 | )} 29 | 30 | 31 | ); 32 | 33 | HandPickedList.propTypes = { 34 | pets: PropTypes.oneOfType([PropTypes.array]).isRequired, 35 | placeholderCount: PropTypes.number, 36 | loading: PropTypes.bool, 37 | }; 38 | 39 | HandPickedList.defaultProps = { 40 | placeholderCount: 12, 41 | loading: false, 42 | }; 43 | 44 | export default HandPickedList; 45 | -------------------------------------------------------------------------------- /components/HandPicked/index.js: -------------------------------------------------------------------------------- 1 | import HandPicked from './HandPicked'; 2 | 3 | export default HandPicked; 4 | -------------------------------------------------------------------------------- /components/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Slider from 'ac-react-simple-image-slider'; 4 | import HandPicked from '@components/HandPicked'; 5 | import { 6 | HomeWrapper, 7 | LeftSide, 8 | LeftContent, 9 | Description, 10 | ContentTitle, 11 | ContentSlogan, 12 | LeftFooter, 13 | RightSide, 14 | } from './Home.styles'; 15 | 16 | const slideList = [ 17 | { 18 | src: 19 | 'https://images.unsplash.com/photo-1537151608828-ea2b11777ee8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2744&q=80', 20 | title: 'Slide 1', 21 | }, 22 | { 23 | src: 24 | 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=960&q=80', 25 | title: 'Slide 2', 26 | }, 27 | { 28 | src: '/static/images/image-18.jpg', 29 | title: 'Slide 3', 30 | }, 31 | ]; 32 | 33 | const Home = props => { 34 | return ( 35 | 36 | 37 | 38 | 39 | Where Pets Find Their People 40 | 41 | Thousands of adoptable pets are looking for people. People Like 42 | You 43 | 44 | 45 | 46 | 47 | 48 |

Animals have no voice. We do!

49 | They need a friend as well! 50 |
51 |
52 | 53 | 62 | 63 |
64 | ); 65 | }; 66 | 67 | Home.propTypes = { 68 | // bla: PropTypes.string, 69 | }; 70 | 71 | Home.defaultProps = { 72 | // bla: 'test', 73 | }; 74 | 75 | export default Home; 76 | -------------------------------------------------------------------------------- /components/Home/Home.styles.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@styles'; 2 | const dogRight = '/static/images/dog.png'; 3 | 4 | export const HomeWrapper = styled.div` 5 | display: flex; 6 | flex-direction: row; 7 | width: 100%; 8 | height: 100%; 9 | `; 10 | 11 | export const LeftSide = styled.div` 12 | width: 50%; 13 | background-color: #f8f7f2; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: space-between; 17 | padding-top: 100px; 18 | `; 19 | 20 | export const LeftContent = styled.div` 21 | height: 100%; 22 | padding: 20px; 23 | overflow: hidden; 24 | overflow-y: scroll; 25 | `; 26 | 27 | export const Description = styled.div` 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-items: center; 32 | `; 33 | 34 | export const ContentTitle = styled.h1``; 35 | export const ContentSlogan = styled.p``; 36 | 37 | export const LeftFooter = styled.div` 38 | height: 100px; 39 | background-color: #f8f1e7; 40 | display: flex; 41 | flex-direction: column; 42 | align-items: flex-start; 43 | justify-content: center; 44 | padding: 0 20px; 45 | 46 | & > p { 47 | margin: 0; 48 | font-size: 18px; 49 | line-height: 2rem; 50 | font-weight: 700; 51 | } 52 | 53 | & > small { 54 | font-size: 12px; 55 | } 56 | `; 57 | 58 | export const RightSide = styled.div` 59 | width: 50%; 60 | background-color: #fce2b8; 61 | /* background-image: url(${dogRight}); 62 | background-size: contain; 63 | background-position: top center; 64 | background-repeat: no-repeat; */ 65 | `; 66 | -------------------------------------------------------------------------------- /components/Home/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | export default Home; 4 | -------------------------------------------------------------------------------- /components/PetBio/PetBio.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { capitalizeFirstLetter } from '@helpers'; 4 | import { 5 | Title, 6 | List, 7 | ListItem, 8 | Label, 9 | Value, 10 | } from '@components/PetDetails/PetDetails.styles'; 11 | 12 | const PetBio = ({ pet, filter }) => { 13 | if (Array.isArray(filter) && filter.length < 1) { 14 | return null; 15 | } 16 | 17 | return ( 18 | <> 19 | Bio 20 | 21 | {filter.map(attr => ( 22 | 23 | 24 | {pet[attr]} 25 | 26 | ))} 27 | 28 | 29 | ); 30 | }; 31 | 32 | PetBio.propTypes = { 33 | pet: PropTypes.oneOfType([ 34 | PropTypes.object 35 | ]).isRequired, 36 | filter: PropTypes.oneOfType([ 37 | PropTypes.array 38 | ]), 39 | }; 40 | 41 | PetBio.defaultProps = { 42 | filter: [], 43 | }; 44 | 45 | export default PetBio; 46 | -------------------------------------------------------------------------------- /components/PetBio/PetBio.styles.js: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Test = styled.div` 4 | // display: flex; 5 | // `; 6 | // -------------------------------------------------------------------------------- /components/PetBio/index.js: -------------------------------------------------------------------------------- 1 | import PetBio from './PetBio'; 2 | 3 | export default PetBio; 4 | -------------------------------------------------------------------------------- /components/PetCard/PetCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Link from 'next/link'; 4 | import { has, find } from 'lodash/fp'; 5 | import { PetCardContainer, PetName, PetInfo, PetMeta } from './PetCard.styles'; 6 | 7 | const getPetPhoto = (pet, size = 'pn') => { 8 | if (has('media.photos', pet)) { 9 | const getMatch = find(d => size === d.size, pet.media.photos); 10 | if (getMatch) { 11 | return getMatch.url; 12 | } 13 | return pet.media.photos[0].url; 14 | } 15 | return null; 16 | }; 17 | 18 | const PetCard = ({ pet }) => { 19 | if (!pet) { 20 | return null; 21 | } 22 | return ( 23 | 24 | 29 | 30 | {pet.name} 31 | 32 | {pet.animal} - {pet.sex} 33 | 34 | 35 | {pet.age} - {pet.size} 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | PetCard.propTypes = { 43 | pet: PropTypes.oneOfType([PropTypes.object]).isRequired, 44 | }; 45 | 46 | export default PetCard; 47 | -------------------------------------------------------------------------------- /components/PetCard/PetCard.styles.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@styles'; 2 | 3 | export const PetInfo = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | position: absolute; 9 | transform: translateY(calc(42px + 1em)); 10 | transition: transform 0.3s; 11 | bottom: 0; 12 | width: 100%; 13 | background-color: #fc5f90; 14 | color: #fff; 15 | padding: 15px 0; 16 | `; 17 | 18 | export const PetCardContainer = styled.div` 19 | position: relative; 20 | height: 310px; 21 | padding: 0; 22 | border-radius: 10px; 23 | overflow: hidden; 24 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); 25 | word-wrap: break-word; 26 | transition: all 0.2s ease-in-out; 27 | text-align: center; 28 | margin-bottom: 25px; 29 | padding-bottom: 10px; 30 | 31 | &:hover { 32 | box-shadow: 0 25px 55px rgba(0, 0, 0, 0.2); 33 | cursor: pointer; 34 | 35 | ${PetInfo} { 36 | height: 100%; 37 | transform: translateY(0); 38 | opacity: 0.9; 39 | bottom: 0; 40 | } 41 | } 42 | `; 43 | export const PetImage = styled.span` 44 | height: 150px; 45 | overflow: hidden; 46 | 47 | & > img { 48 | width: 100%; 49 | } 50 | `; 51 | export const PetName = styled.span` 52 | font-size: 18px; 53 | font-weight: 600; 54 | /* color: #9c27b0; */ 55 | height: 52px; 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | margin-bottom: 10px; 60 | `; 61 | 62 | export const PetMeta = styled.span` 63 | font-size: 14px; 64 | margin-bottom: 5px; 65 | `; 66 | -------------------------------------------------------------------------------- /components/PetCard/index.js: -------------------------------------------------------------------------------- 1 | import PetCard from './PetCard'; 2 | 3 | export default PetCard; 4 | -------------------------------------------------------------------------------- /components/PetDetails/PetDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { has } from 'lodash/fp'; 4 | import Link from 'next/link'; 5 | import ShelterMap from '@components/ShelterMap'; 6 | import PetMediaSlider from '@components/PetMediaSlider'; 7 | import PetBio from '@components/PetBio'; 8 | import PetOptions from '@components/PetOptions'; 9 | import ShelterInfoBar from '@components/ShelterInfoBar'; 10 | import { petDetailsQuery } from '@queries'; 11 | import { NoRecordAvailable, NoRecordText, ContentSidebar, SidebarSection } from '@styles'; 12 | import { 13 | PetDetailsWrapper, 14 | ContentWrapper, 15 | MainContent, 16 | ContentSection, 17 | Title, 18 | Details, 19 | } from './PetDetails.styles'; 20 | 21 | import NoRecordImage from '@static/images/icons/004-pawprint.svg'; 22 | 23 | const PetDetails = ({ getPetQuery, getShelterQuery, ...rest }) => { 24 | // console.log('rest', rest); 25 | if (getPetQuery.loading) { 26 | return
Loading
; 27 | } 28 | 29 | const isPetAvailable = has('pet', getPetQuery) && getPetQuery.pet; 30 | 31 | if (!isPetAvailable) { 32 | return ( 33 | 34 | 35 | 36 |

No pet details available!

37 | 38 | 39 | Home 40 | 41 | 42 |
43 |
44 | ); 45 | } 46 | 47 | const { pet } = getPetQuery; 48 | const { shelter } = getShelterQuery; 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | {pet.name} 57 |
58 |

{pet.description}

59 |
60 | 61 |
62 |
63 | 64 | 65 | 66 | 70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | PetDetails.propTypes = { 81 | // bla: PropTypes.string, 82 | }; 83 | 84 | PetDetails.defaultProps = { 85 | // bla: 'test', 86 | }; 87 | 88 | export default petDetailsQuery(PetDetails); 89 | -------------------------------------------------------------------------------- /components/PetDetails/PetDetails.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, ifProp, prop, css, Grid, Row, Col, media } from '@styles'; 2 | 3 | export const PetDetailsWrapper = styled(Grid)` 4 | height: 100%; 5 | margin-top: 100px; 6 | `; 7 | export const ContentWrapper = styled(Row)` 8 | height: 'auto'; 9 | `; 10 | export const MainContent = styled(Col)` 11 | height: auto; 12 | overflow: visible; 13 | padding: 23px 30px; 14 | margin-bottom: 82px; 15 | position: relative; 16 | border: none; 17 | border-radius: 10px; 18 | box-shadow: 0 3px 3px rgba(77, 71, 81, 0.2); 19 | background-color: #fff; 20 | `; 21 | 22 | export const ContentSection = styled.div``; 23 | 24 | export const Details = styled.div` 25 | & > p { 26 | line-height: 29px; 27 | } 28 | `; 29 | 30 | export const Title = styled.h3` 31 | font-size: 27px; 32 | min-height: 40px; 33 | margin-top: 24px; 34 | margin-bottom: 20px; 35 | border-bottom: 1px solid #8bc34a; 36 | display: block; 37 | color: ${prop('color', '#000')}; 38 | 39 | ${ifProp( 40 | 'noBorderBottom', 41 | css` 42 | border-bottom: 0; 43 | `, 44 | )} 45 | ${ifProp( 46 | 'centered', 47 | css` 48 | text-align: center; 49 | `, 50 | )} 51 | `; 52 | 53 | export const List = styled.ul``; 54 | 55 | export const ListItem = styled.li` 56 | display: flex; 57 | flex-direction: row; 58 | min-height: 39px; 59 | justify-content: space-between; 60 | align-items: center; 61 | `; 62 | 63 | export const Label = styled.span` 64 | font-weight: 600; 65 | `; 66 | 67 | export const Value = styled.span` 68 | display: flex; 69 | flex-direction: column; 70 | text-align: right; 71 | font-size: ${prop('fontSize', 16)}px; 72 | & > span { 73 | line-height: 23px; 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /components/PetDetails/index.js: -------------------------------------------------------------------------------- 1 | import PetDetails from './PetDetails'; 2 | 3 | export default PetDetails; 4 | -------------------------------------------------------------------------------- /components/PetMediaSlider/PetMediaSlider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Slider from 'ac-react-simple-image-slider'; 4 | import { has, filter, map } from 'lodash/fp'; 5 | 6 | const PetMediaSlider = ({ media }) => { 7 | if (!media || !has('photos', media)) { 8 | return null; 9 | } 10 | 11 | // Filter pet's images to pick only one image for the same ID 12 | // pn: 300px 13 | const photos = filter(photo => photo.size === 'pn', media.photos); 14 | if (!photos) return null; 15 | 16 | // Build the slide list 17 | const slideList = map.convert({ cap: false })((photo, i) => ({ 18 | src: photo.url, 19 | title: `Slide ${i + 1}`, 20 | }), photos); 21 | 22 | return ( 23 | 1} 29 | duration={5} 30 | showDots={false} 31 | elementWrapperStyles={{ 32 | borderRadius: '5px', 33 | overflow: 'hidden', 34 | }} 35 | itemStyles={{ 36 | objectPosition: 'center', 37 | }} 38 | /> 39 | ); 40 | }; 41 | 42 | PetMediaSlider.propTypes = { 43 | media: PropTypes.oneOfType([PropTypes.object]), 44 | }; 45 | 46 | PetMediaSlider.defaultProps = { 47 | media: {}, 48 | }; 49 | 50 | export default PetMediaSlider; 51 | -------------------------------------------------------------------------------- /components/PetMediaSlider/PetMediaSlider.styles.js: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Test = styled.div` 4 | // display: flex; 5 | // `; 6 | // -------------------------------------------------------------------------------- /components/PetMediaSlider/index.js: -------------------------------------------------------------------------------- 1 | import PetMediaSlider from './PetMediaSlider'; 2 | 3 | export default PetMediaSlider; 4 | -------------------------------------------------------------------------------- /components/PetOptions/PetOptions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { has, find } from 'lodash/fp'; 4 | import { Title, Details } from '@components/PetDetails/PetDetails.styles'; 5 | import { 6 | OptionList, 7 | OptionItem, 8 | OptionTitle, 9 | OptionValue, 10 | } from './PetOptions.styles'; 11 | 12 | const PetOptions = ({ pet }) => { 13 | if (!pet || !has('options', pet)) { 14 | return null; 15 | } 16 | 17 | const options = { 18 | altered: find(b => b.option === 'altered', pet.options), 19 | houseTrained: find(b => b.option === 'housetrained', pet.options), 20 | hasShots: find(b => b.option === 'hasShots', pet.options), 21 | }; 22 | 23 | return ( 24 | <> 25 | About 26 |
27 | 28 | 29 | Characteristics 30 | loving, cuddly, friendly 31 | 32 | 33 | House Trained 34 | {options.houseTrained ? 'Yes' : 'No'} 35 | 36 | 37 |
38 | 39 | ); 40 | }; 41 | 42 | PetOptions.propTypes = { 43 | pet: PropTypes.oneOfType([ 44 | PropTypes.object 45 | ]).isRequired, 46 | }; 47 | 48 | export default PetOptions; 49 | -------------------------------------------------------------------------------- /components/PetOptions/PetOptions.styles.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@styles'; 2 | 3 | export const OptionList = styled.ul``; 4 | 5 | export const OptionItem = styled.li` 6 | display: flex; 7 | flex-direction: column; 8 | margin-bottom: 40px; 9 | `; 10 | 11 | export const OptionTitle = styled.span` 12 | font-size: 18px; 13 | font-weight: 600; 14 | margin-bottom: 10px; 15 | `; 16 | 17 | export const OptionValue = styled.span``; 18 | -------------------------------------------------------------------------------- /components/PetOptions/index.js: -------------------------------------------------------------------------------- 1 | import PetOptions from './PetOptions'; 2 | 3 | export default PetOptions; 4 | -------------------------------------------------------------------------------- /components/Placeholder/Placeholder.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ContentLoader from 'react-content-loader'; 4 | 5 | const Placeholder = ({ width, height, speed, ...rest }) => ( 6 | 15 | 16 | 17 | ); 18 | 19 | Placeholder.propTypes = { 20 | width: PropTypes.number, 21 | height: PropTypes.number, 22 | speed: PropTypes.number, 23 | }; 24 | 25 | Placeholder.defaultProps = { 26 | width: 276, 27 | height: 310, 28 | speed: 2, 29 | }; 30 | 31 | export default Placeholder; 32 | -------------------------------------------------------------------------------- /components/Placeholder/index.js: -------------------------------------------------------------------------------- 1 | import Placeholder from './Placeholder'; 2 | 3 | export default Placeholder; 4 | -------------------------------------------------------------------------------- /components/Search/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { map } from 'lodash/fp'; 4 | import SelectBox from '@components/SelectBox'; 5 | import SearchFilters, { AnimalFilters } from '@components/SearchFilters'; 6 | import SearchResults from '@components/SearchResults'; 7 | import { 8 | useGeoLocation, 9 | useAddress, 10 | coor2address, 11 | useCurrentAnimal, 12 | useUserFilters, 13 | } from '@helpers/hooks'; 14 | import { 15 | SearchWrapper, 16 | SearchHeader, 17 | FormWrapper, 18 | SearchForm, 19 | SearchInput, 20 | SearchIcon, 21 | CloseIcon, 22 | SearchInfoText, 23 | } from './Search.styles'; 24 | 25 | import SearchImage from '@static/images/search.svg'; 26 | import CloseImage from '@static/images/close.svg'; 27 | 28 | const Search = ({ isSearchActive, onClickHandler }) => { 29 | const { currentAnimal, setAnimal } = useCurrentAnimal(); 30 | const { currentAddress, setAddress } = useAddress(); 31 | const { userFilters, setUserFilter } = useUserFilters(); 32 | 33 | // const location = useGeoLocation(); 34 | 35 | // // console.log(location.latitude); 36 | 37 | // useEffect( 38 | // () => { 39 | // // console.log('currentAddress', currentAddress); 40 | // setTimeout(() => { 41 | // coor2address(location.latitude, location.longitude, setAddress); 42 | // // console.log('address', address); 43 | // // setAddress(address); 44 | // }, 2000); 45 | // }, 46 | // [location], 47 | // ); 48 | 49 | const onChangeLocation = e => { 50 | // setAddress(e.target.value); 51 | setUserFilter({ ...userFilters, location: e.target.value }); 52 | }; 53 | 54 | // const onChangeSelectBox = (e, type) => { 55 | // setUserFilter({ ...userFilters, [type]: e.target.value }); 56 | // } 57 | 58 | const onChangeSelectBox = (selectedItem, downshiftState, returnThis) => { 59 | if (returnThis) { 60 | if (returnThis.type === 'animal') { 61 | setAnimal(selectedItem); 62 | } 63 | selectedItem && 64 | setUserFilter({ 65 | ...userFilters, 66 | [returnThis.type]: selectedItem.value 67 | ? selectedItem.value 68 | : selectedItem.id, 69 | }); 70 | } 71 | }; 72 | return ( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {map( 81 | filter => 82 | filter.display && 83 | filter.component === 'searchbar' && ( 84 | 93 | ), 94 | AnimalFilters.common, 95 | )} 96 | 103 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 121 | 122 | {userFilters.animal && } 123 | {!userFilters.animal && ( 124 | 125 | Enter a location in the search input above, and results will be displayed as you type. You can customize the search results, size, age, breed etc. after select an animal type. 126 | 127 | )} 128 | 129 | ); 130 | }; 131 | 132 | Search.propTypes = { 133 | isSearchActive: PropTypes.bool, 134 | onClickHandler: PropTypes.func, 135 | }; 136 | 137 | Search.defaultProps = { 138 | isSearchActive: false, 139 | onClickHandler: () => {}, 140 | }; 141 | 142 | export default Search; 143 | -------------------------------------------------------------------------------- /components/Search/Search.styles.js: -------------------------------------------------------------------------------- 1 | import { 2 | styled, 3 | css, 4 | ifProp, 5 | prop, 6 | media, 7 | FadeIn, 8 | FadeOut, 9 | Row, 10 | } from '@styles'; 11 | 12 | export const SearchWrapper = styled.div` 13 | display: none; 14 | flex-direction: column; 15 | position: fixed; 16 | width: 100vw; 17 | z-index: 999; 18 | background: #f9fafb; 19 | height: 100vh; 20 | padding: 28px 32px; 21 | overflow: hidden; 22 | will-change: transform; 23 | opacity: 0; 24 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.25); 25 | animation: ${FadeOut} 0.3s; 26 | 27 | ${ifProp( 28 | { isSearchActive: true }, 29 | css` 30 | display: flex; 31 | opacity: 1; 32 | animation: ${FadeIn} 0.3s; 33 | `, 34 | )} 35 | 36 | ${media.sm(` 37 | top:0; 38 | left:0; 39 | width:100vw; 40 | height:100vh; 41 | box-shadow:0 0 0 42 | `)} 43 | `; 44 | 45 | export const SearchHeader = styled.div` 46 | width: 100%; 47 | box-shadow: 0 5px 50px rgba(0, 0, 0, 0.025); 48 | `; 49 | 50 | export const FormWrapper = styled.div` 51 | height: 78px; 52 | position: relative; 53 | display: flex; 54 | flex-direction: row; 55 | align-items: center; 56 | justify-content: flex-start; 57 | padding: 0; 58 | `; 59 | 60 | export const SearchForm = styled.div` 61 | width: calc(100% - 2rem); 62 | display: flex; 63 | flex-direction: row; 64 | justify-content: flex-start; 65 | align-items: center; 66 | `; 67 | 68 | export const SearchInput = styled.input` 69 | display: block; 70 | width: ${prop('width', '100%')}; 71 | height: 70px; 72 | font-size: 1.05rem; 73 | font-weight: 300; 74 | padding: 0 1rem 0 1.5rem; 75 | appearance: none; 76 | outline: 0; 77 | box-shadow: none; 78 | margin: 0; 79 | color: #6c7987; 80 | border-radius: 3px; 81 | transition: box-shadow 0.2s ease; 82 | border: 1px solid rgba(34, 36, 38, 0.15); 83 | border-radius: 0.3rem; 84 | margin: 0 5px; 85 | `; 86 | 87 | export const SearchIcon = styled.span` 88 | & > svg { 89 | width: 25px; 90 | fill: #ccc; 91 | } 92 | `; 93 | 94 | export const CloseIcon = styled.span` 95 | cursor: pointer; 96 | & > svg { 97 | width: 16px; 98 | } 99 | `; 100 | 101 | export const SearchInfoText = styled.p` 102 | font-size: 21px; 103 | line-height: 2rem; 104 | `; 105 | -------------------------------------------------------------------------------- /components/Search/index.js: -------------------------------------------------------------------------------- 1 | import Search from './Search'; 2 | 3 | export default Search; 4 | -------------------------------------------------------------------------------- /components/SearchFilters/Filters/animal-filters.js: -------------------------------------------------------------------------------- 1 | const iconBase = '/static/images/icons'; 2 | 3 | const animalFilters = [ 4 | { 5 | id: 'all-animals', 6 | label: 'All animals', 7 | // iconURL: `${iconBase}/005-cat-1.svg`, 8 | // iconURLSelected: `${iconBase}/005-cat-2.svg`, 9 | }, 10 | { 11 | id: 'cat', 12 | label: 'Cat', 13 | iconURL: `${iconBase}/005-cat-1.svg`, 14 | iconURLSelected: `${iconBase}/005-cat-2.svg`, 15 | }, 16 | { 17 | id: 'dog', 18 | label: 'Dog', 19 | iconURL: `${iconBase}/001-dog-1.svg`, 20 | iconURLSelected: `${iconBase}/001-dog-2.svg`, 21 | }, 22 | { 23 | id: 'horse', 24 | label: 'Horse', 25 | iconURL: `${iconBase}/001-horse-1.svg`, 26 | iconURLSelected: `${iconBase}/001-horse-2.svg`, 27 | }, 28 | { 29 | id: 'bird', 30 | label: 'Bird', 31 | iconURL: `${iconBase}/009-bird-1.svg`, 32 | iconURLSelected: `${iconBase}/009-bird-2.svg`, 33 | }, 34 | { 35 | id: 'reptile', 36 | label: 'Reptile', 37 | iconURL: `${iconBase}/011-snake-1.svg`, 38 | iconURLSelected: `${iconBase}/011-snake-2.svg`, 39 | }, 40 | { 41 | id: 'smallfurry', 42 | label: 'Smallfurry', 43 | iconURL: `${iconBase}/013-rabbit-1.svg`, 44 | iconURLSelected: `${iconBase}/013-rabbit-2.svg`, 45 | }, 46 | { 47 | id: 'barnyard', 48 | label: 'Barnyard', 49 | iconURL: `${iconBase}/001-chicken-1.svg`, 50 | iconURLSelected: `${iconBase}/001-chicken-2.svg`, 51 | }, 52 | ]; 53 | 54 | export default animalFilters; 55 | -------------------------------------------------------------------------------- /components/SearchFilters/Filters/barnyard-filters.js: -------------------------------------------------------------------------------- 1 | const barnyardFilters = { 2 | breed: { 3 | name: 'breed', 4 | label: 'Breed', 5 | display: true, 6 | options: [ 7 | { 8 | value: 'Alpaca', 9 | label: 'Alpaca', 10 | long_label: 'Alpaca', 11 | facet_value: '557', 12 | default: false, 13 | }, 14 | { 15 | value: 'Alpine', 16 | label: 'Alpine', 17 | long_label: 'Alpine', 18 | facet_value: '567', 19 | default: false, 20 | }, 21 | { 22 | value: 'Angora', 23 | label: 'Angora', 24 | long_label: 'Angora', 25 | facet_value: '680', 26 | default: false, 27 | }, 28 | { 29 | value: 'Angus', 30 | label: 'Angus', 31 | long_label: 'Angus', 32 | facet_value: '558', 33 | default: false, 34 | }, 35 | { 36 | value: 'Barbados', 37 | label: 'Barbados', 38 | long_label: 'Barbados', 39 | facet_value: '562', 40 | default: false, 41 | }, 42 | { 43 | value: 'Boer', 44 | label: 'Boer', 45 | long_label: 'Boer', 46 | facet_value: '681', 47 | default: false, 48 | }, 49 | { 50 | value: 'Cow', 51 | label: 'Cow', 52 | long_label: 'Cow', 53 | facet_value: '561', 54 | default: false, 55 | }, 56 | { 57 | value: 'Duroc', 58 | label: 'Duroc', 59 | long_label: 'Duroc', 60 | facet_value: '688', 61 | default: false, 62 | }, 63 | { 64 | value: 'Goat', 65 | label: 'Goat', 66 | long_label: 'Goat', 67 | facet_value: '570', 68 | default: false, 69 | }, 70 | { 71 | value: 'Hampshire', 72 | label: 'Hampshire', 73 | long_label: 'Hampshire', 74 | facet_value: '689', 75 | default: false, 76 | }, 77 | { 78 | value: 'Holstein', 79 | label: 'Holstein', 80 | long_label: 'Holstein', 81 | facet_value: '559', 82 | default: false, 83 | }, 84 | { 85 | value: 'Jersey', 86 | label: 'Jersey', 87 | long_label: 'Jersey', 88 | facet_value: '560', 89 | default: false, 90 | }, 91 | { 92 | value: 'LaMancha', 93 | label: 'LaMancha', 94 | long_label: 'LaMancha', 95 | facet_value: '682', 96 | default: false, 97 | }, 98 | { 99 | value: 'Landrace', 100 | label: 'Landrace', 101 | long_label: 'Landrace', 102 | facet_value: '690', 103 | default: false, 104 | }, 105 | { 106 | value: 'Llama', 107 | label: 'Llama', 108 | long_label: 'Llama', 109 | facet_value: '556', 110 | default: false, 111 | }, 112 | { 113 | value: 'Merino', 114 | label: 'Merino', 115 | long_label: 'Merino', 116 | facet_value: '564', 117 | default: false, 118 | }, 119 | { 120 | value: 'Mouflon', 121 | label: 'Mouflon', 122 | long_label: 'Mouflon', 123 | facet_value: '565', 124 | default: false, 125 | }, 126 | { 127 | value: 'Myotonic / Fainting', 128 | label: 'Myotonic / Fainting', 129 | long_label: 'Myotonic / Fainting', 130 | facet_value: '683', 131 | default: false, 132 | }, 133 | { 134 | value: 'Nigerian Dwarf', 135 | label: 'Nigerian Dwarf', 136 | long_label: 'Nigerian Dwarf', 137 | facet_value: '569', 138 | default: false, 139 | }, 140 | { 141 | value: 'Nubian', 142 | label: 'Nubian', 143 | long_label: 'Nubian', 144 | facet_value: '684', 145 | default: false, 146 | }, 147 | { 148 | value: 'Oberhasli', 149 | label: 'Oberhasli', 150 | long_label: 'Oberhasli', 151 | facet_value: '685', 152 | default: false, 153 | }, 154 | { 155 | value: 'Pig', 156 | label: 'Pig', 157 | long_label: 'Pig', 158 | facet_value: '571', 159 | default: false, 160 | }, 161 | { 162 | value: 'Pot Bellied', 163 | label: 'Pot Bellied', 164 | long_label: 'Pot Bellied', 165 | facet_value: '572', 166 | default: false, 167 | }, 168 | { 169 | value: 'Pygmy', 170 | label: 'Pygmy', 171 | long_label: 'Pygmy', 172 | facet_value: '568', 173 | default: false, 174 | }, 175 | { 176 | value: 'Saanen', 177 | label: 'Saanen', 178 | long_label: 'Saanen', 179 | facet_value: '686', 180 | default: false, 181 | }, 182 | { 183 | value: 'Sheep', 184 | label: 'Sheep', 185 | long_label: 'Sheep', 186 | facet_value: '566', 187 | default: false, 188 | }, 189 | { 190 | value: 'Shetland', 191 | label: 'Shetland', 192 | long_label: 'Shetland', 193 | facet_value: '563', 194 | default: false, 195 | }, 196 | { 197 | value: 'Toggenburg', 198 | label: 'Toggenburg', 199 | long_label: 'Toggenburg', 200 | facet_value: '687', 201 | default: false, 202 | }, 203 | { 204 | value: 'Vietnamese Pot Bellied', 205 | label: 'Vietnamese Pot Bellied', 206 | long_label: 'Vietnamese Pot Bellied', 207 | facet_value: '573', 208 | default: false, 209 | }, 210 | { 211 | value: 'Yorkshire', 212 | label: 'Yorkshire', 213 | long_label: 'Yorkshire', 214 | facet_value: '691', 215 | default: false, 216 | }, 217 | ], 218 | }, 219 | }; 220 | 221 | export default barnyardFilters; 222 | -------------------------------------------------------------------------------- /components/SearchFilters/Filters/common-filters.js: -------------------------------------------------------------------------------- 1 | const commonFilters = { 2 | size: { 3 | name: 'size', 4 | label: 'Size', 5 | display: true, 6 | component: 'filter', 7 | options: [ 8 | { 9 | value: 'S', 10 | label: 'Small', 11 | long_label: 'Small', 12 | default: false, 13 | }, 14 | { 15 | value: 'M', 16 | label: 'Medium', 17 | long_label: 'Medium', 18 | default: false, 19 | }, 20 | { 21 | value: 'L', 22 | label: 'Large', 23 | long_label: 'Large', 24 | default: false, 25 | }, 26 | { 27 | value: 'XL', 28 | label: 'Extra Large', 29 | long_label: 'Extra Large', 30 | default: false, 31 | }, 32 | ], 33 | }, 34 | gender: { 35 | name: 'sex', 36 | label: 'Gender', 37 | display: true, 38 | component: 'filter', 39 | options: [ 40 | { 41 | value: 'M', 42 | label: 'Male', 43 | long_label: 'Male', 44 | default: false, 45 | }, 46 | { 47 | value: 'F', 48 | label: 'Female', 49 | long_label: 'Female', 50 | default: false, 51 | }, 52 | ], 53 | }, 54 | distance: { 55 | name: 'distance', 56 | label: 'Distance', 57 | display: false, 58 | component: 'searchbar', 59 | options: [ 60 | { 61 | value: '10', 62 | label: '10 miles', 63 | long_label: 'Within 10 mi', 64 | default: false, 65 | }, 66 | { 67 | value: '25', 68 | label: '25 miles', 69 | long_label: 'Within 25 mi', 70 | default: false, 71 | }, 72 | { 73 | value: '50', 74 | label: '50 miles', 75 | long_label: 'Within 50 mi', 76 | default: false, 77 | }, 78 | { 79 | value: '100', 80 | label: '100 miles', 81 | long_label: 'Within 100 mi', 82 | default: true, 83 | }, 84 | { 85 | value: 'Anywhere', 86 | label: 'Anywhere', 87 | long_label: 'Anywhere', 88 | default: false, 89 | }, 90 | ], 91 | }, 92 | sort: { 93 | name: 'sort', 94 | label: 'Sort By', 95 | display: true, 96 | component: 'searchbar', 97 | options: [ 98 | { 99 | value: 'recently_added', 100 | label: 'Recently added', 101 | long_label: 'Recently added', 102 | default: false, 103 | }, 104 | { 105 | value: 'available_longest', 106 | label: 'Available longest', 107 | long_label: 'Available longest', 108 | default: false, 109 | }, 110 | { 111 | value: 'nearest', 112 | label: 'Nearest', 113 | long_label: 'Nearest', 114 | default: true, 115 | }, 116 | ], 117 | }, 118 | }; 119 | 120 | export default commonFilters; 121 | -------------------------------------------------------------------------------- /components/SearchFilters/Filters/index.js: -------------------------------------------------------------------------------- 1 | import barnyardFilters from './barnyard-filters'; 2 | import birdFilters from './bird-filters'; 3 | import catFilters from './cat-filters'; 4 | import dogFilters from './dog-filters'; 5 | import horseFilters from './horse-filters'; 6 | import otherFilters from './other-filters'; 7 | import rabbitFilters from './rabbit-filters'; 8 | import smallFurryFilters from './small-furry-filters'; 9 | import animalFilters from './animal-filters'; 10 | import commonFilters from './common-filters'; 11 | 12 | const Filters = { 13 | animals: animalFilters, 14 | cat: { 15 | ...commonFilters, 16 | ...catFilters, 17 | }, 18 | dog: { 19 | ...commonFilters, 20 | ...dogFilters, 21 | }, 22 | horse: { 23 | ...commonFilters, 24 | ...horseFilters, 25 | }, 26 | bird: { 27 | ...commonFilters, 28 | ...birdFilters, 29 | }, 30 | smallFurry: { 31 | ...commonFilters, 32 | ...smallFurryFilters, 33 | }, 34 | barnyard: { 35 | ...commonFilters, 36 | ...barnyardFilters, 37 | }, 38 | common: commonFilters, 39 | }; 40 | 41 | export default Filters; 42 | -------------------------------------------------------------------------------- /components/SearchFilters/Filters/small-furry-filters.js: -------------------------------------------------------------------------------- 1 | const smallFurryFilters = { 2 | breed: { 3 | name: 'breed', 4 | label: 'Breed', 5 | display: true, 6 | options: [ 7 | { 8 | value: 'Abyssinian', 9 | label: 'Abyssinian', 10 | long_label: 'Abyssinian', 11 | facet_value: '628', 12 | default: false, 13 | }, 14 | { 15 | value: 'Chinchilla', 16 | label: 'Chinchilla', 17 | long_label: 'Chinchilla', 18 | facet_value: '618', 19 | default: false, 20 | }, 21 | { 22 | value: 'Degu', 23 | label: 'Degu', 24 | long_label: 'Degu', 25 | facet_value: '619', 26 | default: false, 27 | }, 28 | { 29 | value: 'Dwarf Hamster', 30 | label: 'Dwarf Hamster', 31 | long_label: 'Dwarf Hamster', 32 | facet_value: '707', 33 | default: false, 34 | }, 35 | { 36 | value: 'Ferret', 37 | label: 'Ferret', 38 | long_label: 'Ferret', 39 | facet_value: '620', 40 | default: false, 41 | }, 42 | { 43 | value: 'Gerbil', 44 | label: 'Gerbil', 45 | long_label: 'Gerbil', 46 | facet_value: '621', 47 | default: false, 48 | }, 49 | { 50 | value: 'Guinea Pig', 51 | label: 'Guinea Pig', 52 | long_label: 'Guinea Pig', 53 | facet_value: '634', 54 | default: false, 55 | }, 56 | { 57 | value: 'Hamster', 58 | label: 'Hamster', 59 | long_label: 'Hamster', 60 | facet_value: '622', 61 | default: false, 62 | }, 63 | { 64 | value: 'Hedgehog', 65 | label: 'Hedgehog', 66 | long_label: 'Hedgehog', 67 | facet_value: '623', 68 | default: false, 69 | }, 70 | { 71 | value: 'Mouse', 72 | label: 'Mouse', 73 | long_label: 'Mouse', 74 | facet_value: '624', 75 | default: false, 76 | }, 77 | { 78 | value: 'Peruvian', 79 | label: 'Peruvian', 80 | long_label: 'Peruvian', 81 | facet_value: '629', 82 | default: false, 83 | }, 84 | { 85 | value: 'Prairie Dog', 86 | label: 'Prairie Dog', 87 | long_label: 'Prairie Dog', 88 | facet_value: '677', 89 | default: false, 90 | }, 91 | { 92 | value: 'Rat', 93 | label: 'Rat', 94 | long_label: 'Rat', 95 | facet_value: '625', 96 | default: false, 97 | }, 98 | { 99 | value: 'Rex', 100 | label: 'Rex', 101 | long_label: 'Rex', 102 | facet_value: '630', 103 | default: false, 104 | }, 105 | { 106 | value: 'Short-Haired', 107 | label: 'Short-Haired', 108 | long_label: 'Short-Haired', 109 | facet_value: '632', 110 | default: false, 111 | }, 112 | { 113 | value: 'Silkie / Sheltie', 114 | label: 'Silkie / Sheltie', 115 | long_label: 'Silkie / Sheltie', 116 | facet_value: '633', 117 | default: false, 118 | }, 119 | { 120 | value: 'Skunk', 121 | label: 'Skunk', 122 | long_label: 'Skunk', 123 | facet_value: '626', 124 | default: false, 125 | }, 126 | { 127 | value: 'Sugar Glider', 128 | label: 'Sugar Glider', 129 | long_label: 'Sugar Glider', 130 | facet_value: '627', 131 | default: false, 132 | }, 133 | { 134 | value: 'Teddy', 135 | label: 'Teddy', 136 | long_label: 'Teddy', 137 | facet_value: '631', 138 | default: false, 139 | }, 140 | ], 141 | }, 142 | }; 143 | 144 | export default smallFurryFilters; 145 | -------------------------------------------------------------------------------- /components/SearchFilters/SearchFilters.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { isEmpty, map } from 'lodash/fp'; 4 | import Filters from './Filters'; 5 | import SelectBox from '@components/SelectBox'; 6 | import { SearchFiltersWrapper, SingleFilter } from './SearchFilters.styles'; 7 | 8 | const SearchFilters = ({ currentAnimal, onChange }) => { 9 | if (isEmpty(currentAnimal) || !Filters[currentAnimal.id]) { 10 | return null; 11 | } 12 | 13 | const currentFilters = Filters[currentAnimal.id]; 14 | // console.log('currentFilters', currentFilters); 15 | return ( 16 | 17 | {map( 18 | filter => 19 | filter.display && filter.component !== 'searchbar' && ( 20 | 21 | 29 | 30 | ), 31 | currentFilters, 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | SearchFilters.propTypes = { 38 | currentAnimal: PropTypes.oneOfType([ 39 | PropTypes.object, 40 | ]).isRequired, 41 | onChange: PropTypes.func, 42 | }; 43 | 44 | SearchFilters.defaultProps = { 45 | onChange: () => {} 46 | }; 47 | 48 | export default SearchFilters; 49 | -------------------------------------------------------------------------------- /components/SearchFilters/SearchFilters.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, Row, Col } from '@styles'; 2 | 3 | export const SearchFiltersWrapper = styled(Row)` 4 | padding: 15px 0 5px 0; 5 | margin-bottom: 20px; 6 | `; 7 | export const SingleFilter = styled(Col)` 8 | margin-bottom: 10px; 9 | `; 10 | -------------------------------------------------------------------------------- /components/SearchFilters/index.js: -------------------------------------------------------------------------------- 1 | import SearchFilters from './SearchFilters'; 2 | import AnimalFilters from './Filters'; 3 | 4 | export { AnimalFilters }; 5 | 6 | export default SearchFilters; 7 | -------------------------------------------------------------------------------- /components/SearchResults/SearchList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { map } from 'lodash/fp'; 4 | import PetCard from '@components/PetCard'; 5 | import Placeholder from '@components/Placeholder'; 6 | import { Col } from '@styles'; 7 | import { SearchListWrapper } from './SearchResults.styles'; 8 | 9 | const SearchList = ({ pets, placeholderCount, loading }) => ( 10 | <> 11 | 12 | {loading && 13 | Array(placeholderCount) 14 | .fill('') 15 | .map((p, i) => ( 16 | 17 | 18 | 19 | ))} 20 | {!loading && 21 | map( 22 | pet => ( 23 | 24 | 25 | 26 | ), 27 | pets, 28 | )} 29 | 30 | 31 | ); 32 | 33 | SearchList.propTypes = { 34 | pets: PropTypes.oneOfType([PropTypes.array]).isRequired, 35 | placeholderCount: PropTypes.number, 36 | loading: PropTypes.bool, 37 | }; 38 | 39 | SearchList.defaultProps = { 40 | placeholderCount: 12, 41 | loading: false, 42 | }; 43 | 44 | export default SearchList; 45 | -------------------------------------------------------------------------------- /components/SearchResults/SearchResults.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Query } from 'react-apollo'; 4 | import SearchList from './SearchList'; 5 | import { petFindQuery } from '@queries'; 6 | import { loadMoreContent } from '@helpers'; 7 | import { Button } from '@styles'; 8 | import { 9 | SearchResultsWrapper, 10 | GeneralWrapper, 11 | InformText, 12 | } from './SearchResults.styles'; 13 | 14 | const SearchResults = ({ userFilters }) => { 15 | const petFindQueryVariables = { 16 | location: 'Canada', 17 | animal: null, 18 | breed: null, 19 | size: null, 20 | sex: null, 21 | age: null, 22 | offset: null, 23 | count: 12, 24 | ...userFilters, 25 | }; 26 | if (petFindQueryVariables.animal === 'all-animals') { 27 | petFindQueryVariables.animal = null; 28 | } 29 | 30 | return ( 31 | 36 | {({ loading, error, data, fetchMore, refetch, networkStatus }) => { 37 | const fetching = loading || networkStatus === 4; 38 | if (!fetching && error) { 39 | return ( 40 | 41 | Error loading content. 42 | 43 | 44 | ); 45 | } 46 | 47 | return ( 48 | 49 | 54 | {!fetching && ( 55 | 66 | )} 67 | 68 | ); 69 | }} 70 | 71 | ); 72 | }; 73 | SearchResults.propTypes = { 74 | userFilters: PropTypes.oneOfType([ 75 | PropTypes.object, 76 | ]), 77 | }; 78 | 79 | SearchResults.defaultProps = { 80 | userFilters: {}, 81 | }; 82 | 83 | export default SearchResults; 84 | -------------------------------------------------------------------------------- /components/SearchResults/SearchResults.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, Grid, Row } from '@styles'; 2 | 3 | export const SearchResultsWrapper = styled(Grid)` 4 | height: calc(100vh - 60px); 5 | overflow: auto; 6 | padding-top: 2rem; 7 | `; 8 | 9 | export const SearchListWrapper = styled(Row)` 10 | margin: 0 5px; 11 | `; 12 | 13 | export const GeneralWrapper = styled.div` 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | align-content: center; 18 | min-height: 500px; 19 | flex-direction: column; 20 | `; 21 | 22 | export const LoadingImage = styled.img``; 23 | 24 | export const InformText = styled.p` 25 | font-size: 30px; 26 | font-weight: 300; 27 | `; 28 | -------------------------------------------------------------------------------- /components/SearchResults/index.js: -------------------------------------------------------------------------------- 1 | import SearchResults from './SearchResults'; 2 | 3 | export default SearchResults; 4 | -------------------------------------------------------------------------------- /components/SelectBox/SelectBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import matchSorter from 'match-sorter'; 4 | import Downshift from 'downshift'; 5 | import { 6 | Menu, 7 | ControllerButton, 8 | SelectBoxContainer, 9 | InputWrapper, 10 | Input, 11 | Item, 12 | ItemIcon, 13 | SelectedItemIcon, 14 | ArrowIcon, 15 | XIcon, 16 | } from './SelectBox.styles'; 17 | 18 | const SelectInput = ({ itemToString, items, width, height, margin, ...rest }) => { 19 | return ( 20 | 21 | {({ 22 | getRootProps, 23 | getInputProps, 24 | getToggleButtonProps, 25 | getItemProps, 26 | isOpen, 27 | toggleMenu, 28 | clearSelection, 29 | selectedItem, 30 | inputValue, 31 | highlightedIndex, 32 | }) => ( 33 | 41 | 42 | {selectedItem && selectedItem.iconURL && ( 43 | 51 | )} 52 | 63 | {selectedItem ? ( 64 | 69 | 70 | 71 | ) : ( 72 | 73 | 74 | 75 | )} 76 | 77 | {!isOpen ? null : ( 78 | 79 | {items.map((item, index) => ( 80 | 89 | {item.iconURL && ( 90 | 91 | )} 92 | {itemToString(item)} 93 | 94 | ))} 95 | 96 | )} 97 | 98 | )} 99 | 100 | ); 101 | }; 102 | 103 | const SelectBox = ({ items, width, height, placeholder, onChangeCallback, returnThis, ...rest }) => { 104 | // console.log('items', items); 105 | // items = animals.map(s => ({ name: s, id: s.toLowerCase() })); 106 | if (!Array.isArray(items) || items.length <= 0) { 107 | return null; 108 | } 109 | 110 | const finalItems = items.map(s => ({ ...s, id: s.id ? s.id.toLowerCase() : s.value.toLowerCase() })); 111 | 112 | const [isOpen, setToggle] = useState(false); 113 | const [itemsToShow, setItems] = useState([]); 114 | 115 | const handleStateChange = (changes, downshiftState) => { 116 | if (changes.hasOwnProperty('isOpen')) { 117 | // downshift is saying that isOpen should change, so let's change it... 118 | const isStatesEqual = changes.type === Downshift.stateChangeTypes.mouseUp; 119 | 120 | setToggle( 121 | isStatesEqual 122 | ? isOpen 123 | : changes.isOpen, 124 | ); 125 | 126 | if (!isStatesEqual && changes.isOpen) { 127 | // if the menu is going to be open, then we should limit the results 128 | // by what the user has typed in, otherwise, we'll leave them as they 129 | // were last... 130 | setItems(getItemsToShow(downshiftState.inputValue)); 131 | } 132 | } else if (changes.hasOwnProperty('inputValue')) { 133 | // downshift is saying that the inputValue is changing. Since we don't 134 | // control that, we'll just use that information to update the items 135 | // that we should show. 136 | setItems(getItemsToShow(downshiftState.inputValue)); 137 | } 138 | }; 139 | 140 | const handleChange = (selectedItem, downshiftState) => { 141 | // handle the new selectedItem here 142 | return onChangeCallback(selectedItem, downshiftState, returnThis); 143 | }; 144 | 145 | const handleToggleButtonClick = () => { 146 | setTogle(!isOpen); 147 | setItems(finalItems); 148 | }; 149 | 150 | const getItemsToShow = value => { 151 | return value 152 | ? matchSorter(finalItems, value, { 153 | keys: ['label'], 154 | }) 155 | : finalItems; 156 | }; 157 | 158 | const itemToString = i => (i ? i.label : ''); 159 | 160 | return ( 161 | 172 | ); 173 | }; 174 | 175 | SelectBox.propTypes = { 176 | items: PropTypes.oneOfType([ 177 | PropTypes.array 178 | ]).isRequired, 179 | width: PropTypes.number, 180 | height: PropTypes.number, 181 | placeholder: PropTypes.string, 182 | onChangeCallback: PropTypes.func, 183 | returnThis: PropTypes.any, 184 | }; 185 | 186 | SelectBox.defaultProps = { 187 | width: 250, 188 | height: 44, 189 | placeholder: 'Please select', 190 | onChangeCallback: () => { }, 191 | returnThis: null, 192 | } 193 | 194 | export default SelectBox; 195 | -------------------------------------------------------------------------------- /components/SelectBox/SelectBox.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, ifProp, prop, css } from '@styles'; 2 | 3 | const DEFAULT_WIDTH = 250; 4 | const DEFAULT_HEIGHT = 32; 5 | 6 | export const InputWrapper = styled.div` 7 | position: relative; 8 | display: flex; 9 | justify-content: center; 10 | flex-direction: row; 11 | align-items: center; 12 | `; 13 | 14 | export const Item = styled.div` 15 | position: relative; 16 | cursor: pointer; 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: flex-start; 21 | border: none; 22 | height: auto; 23 | text-align: left; 24 | border-top: none; 25 | line-height: 1.4em; 26 | color: rgba(0, 0, 0, 0.87); 27 | font-size: 0.8rem; 28 | text-transform: none; 29 | font-weight: 400; 30 | box-shadow: none; 31 | padding: 0.8rem 1.1rem; 32 | 33 | ${ifProp( 34 | { isActive: true }, 35 | css` 36 | color: rgba(0, 0, 0, 0.95); 37 | background: rgba(0, 0, 0, 0.03); 38 | `, 39 | )} 40 | ${ifProp( 41 | { isSelected: true }, 42 | css` 43 | color: rgba(0, 0, 0, 0.95); 44 | font-weight: 700; 45 | `, 46 | )} 47 | `; 48 | 49 | export const ItemIcon = styled.img` 50 | height: 16px; 51 | margin-right: 10px; 52 | `; 53 | 54 | export const SelectedItemIcon = styled.img` 55 | height: 24px; 56 | z-index: 2; 57 | position: absolute; 58 | left: 12px; 59 | `; 60 | 61 | export const Input = styled.input` 62 | width: ${prop( 63 | 'width', 64 | DEFAULT_WIDTH, 65 | )}px; /* full width - icon width/2 - border*/ 66 | height: ${prop('height', DEFAULT_HEIGHT)}px; 67 | min-height: 2em; 68 | font-size: 14px; 69 | word-wrap: break-word; 70 | line-height: 1em; 71 | outline: 0; 72 | background: #fff; 73 | padding: 0.5em 2em 0.5em 1em; 74 | color: rgba(0, 0, 0, 0.87); 75 | box-shadow: none; 76 | border: 1px solid rgba(34, 36, 38, 0.15); 77 | border-radius: 0.3rem; 78 | transition: box-shadow 0.1s ease, width 0.1s ease; 79 | &:hover { 80 | border-color: rgba(34, 36, 38, 0.35); 81 | box-shadow: none; 82 | } 83 | &:focus { 84 | border-color: #96c8da; 85 | box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); 86 | } 87 | 88 | ${ifProp( 89 | { hasIcon: true }, 90 | css` 91 | padding-left: 41px; 92 | `, 93 | )} 94 | 95 | ${ifProp( 96 | { isOpen: true }, 97 | css` 98 | border-bottom-left-radius: 0; 99 | border-bottom-right-radius: 0; 100 | `, 101 | )} 102 | `; 103 | 104 | export const Menu = styled.div` 105 | position: absolute; 106 | width: ${prop('width', DEFAULT_WIDTH)}px; 107 | max-height: 20rem; 108 | overflow-y: auto; 109 | overflow-x: hidden; 110 | outline: 0; 111 | border-radius: 0 0 0.28571429rem 0.28571429rem; 112 | transition: opacity 0.1s ease; 113 | box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); 114 | border-color: #f9fafb; 115 | border: 1px solid #f9fafb; 116 | border-top-width: 0; 117 | background-color: #f9fafb; 118 | z-index: 2; 119 | `; 120 | 121 | export const SelectBoxContainer = styled.div` 122 | width: ${prop('width', DEFAULT_WIDTH)}px; 123 | height: ${prop('height', DEFAULT_HEIGHT)}px; 124 | position: relative; 125 | ${ifProp( 126 | 'margin', 127 | css` 128 | margin: ${prop('margin', 0)}; 129 | `, 130 | )} 131 | 132 | ${Menu} { 133 | top: ${prop('height', DEFAULT_HEIGHT)}px; 134 | } 135 | `; 136 | 137 | export const ControllerButton = styled.button` 138 | background-color: transparent; 139 | border: none; 140 | position: absolute; 141 | right: 8px; 142 | top: 50%; 143 | transform: translate(0, -50%); 144 | cursor: pointer; 145 | `; 146 | 147 | export const ArrowIcon = ({ isOpen }) => { 148 | return ( 149 | 158 | 159 | 160 | ); 161 | }; 162 | 163 | export const XIcon = () => { 164 | return ( 165 | 173 | 174 | 175 | 176 | ); 177 | }; 178 | -------------------------------------------------------------------------------- /components/SelectBox/index.js: -------------------------------------------------------------------------------- 1 | import SelectBox from './SelectBox'; 2 | 3 | export default SelectBox; 4 | -------------------------------------------------------------------------------- /components/ShelterDetails/ShelterDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { has } from 'lodash/fp'; 4 | import Link from 'next/link'; 5 | import { Query } from 'react-apollo'; 6 | import ShelterMap from '@components/ShelterMap'; 7 | import ShelterInfoBar from '@components/ShelterInfoBar'; 8 | import ShelterPets from '@components/ShelterPets'; 9 | import { shelterQuery } from '@queries'; 10 | import { 11 | NoRecordAvailable, 12 | NoRecordText, 13 | GeneralContainer, 14 | } from '@styles'; 15 | 16 | import { 17 | ShelterDetailsWrapper, 18 | ContentWrapper, 19 | MainContent, 20 | ContentSection, 21 | Title, 22 | Details, 23 | } from './ShelterDetails.styles'; 24 | 25 | import NoRecordImage from '@static/images/icons/004-pawprint.svg'; 26 | 27 | const ShelterDetails = ({ pageParams, ...rest }) => { 28 | const { id = 0 } = pageParams; 29 | const shelterQueryVariables = { 30 | shelterId: id, 31 | }; 32 | 33 | return ( 34 | 35 | {({ loading, error, data }) => { 36 | if (error) return
error
; 37 | if (loading) return
loading
; 38 | 39 | const isShelterAvailable = has('shelter.id', data) && data.shelter.id; 40 | 41 | if (!isShelterAvailable) { 42 | return ( 43 | 44 | 45 | 46 |

No shelter details available!

47 | 48 | 49 | Home 50 | 51 | 52 |
53 |
54 | ); 55 | } 56 | 57 | const { shelter } = data; 58 | 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | {shelter.name} 66 | 67 | 68 | 69 |
70 | Our Pets 71 | 72 |
73 |
74 |
75 |
76 |
77 | ); 78 | }} 79 |
80 | ); 81 | }; 82 | 83 | ShelterDetails.propTypes = { 84 | pageParams: PropTypes.oneOfType([ 85 | PropTypes.object, 86 | ]), 87 | }; 88 | 89 | ShelterDetails.defaultProps = { 90 | pageParams: {}, 91 | }; 92 | 93 | export default ShelterDetails; 94 | -------------------------------------------------------------------------------- /components/ShelterDetails/ShelterDetails.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, ifProp, prop, css, Grid, Row, Col, media } from '@styles'; 2 | 3 | export const ShelterDetailsWrapper = styled(Grid)` 4 | height: 100%; 5 | margin-top: 100px; 6 | `; 7 | 8 | export const ContentWrapper = styled(Row)` 9 | height: 'auto'; 10 | `; 11 | 12 | export const MainContent = styled(Col)` 13 | height: auto; 14 | overflow: visible; 15 | padding: 23px 30px; 16 | margin-bottom: 82px; 17 | position: relative; 18 | border: none; 19 | border-radius: 10px; 20 | box-shadow: 0 3px 3px rgba(77, 71, 81, 0.2); 21 | background-color: #fff; 22 | `; 23 | 24 | export const ContentSection = styled.div``; 25 | 26 | export const Details = styled.div` 27 | & > p { 28 | line-height: 29px; 29 | } 30 | `; 31 | 32 | export const Title = styled.h3` 33 | font-size: 27px; 34 | min-height: 40px; 35 | margin-bottom: 20px; 36 | border-bottom: 1px solid #8bc34a; 37 | display: block; 38 | color: ${prop('color', '#000')}; 39 | 40 | ${ifProp( 41 | 'noBorderBottom', 42 | css` 43 | border-bottom: 0; 44 | `, 45 | )} 46 | ${ifProp( 47 | 'centered', 48 | css` 49 | text-align: center; 50 | `, 51 | )} 52 | `; 53 | -------------------------------------------------------------------------------- /components/ShelterDetails/index.js: -------------------------------------------------------------------------------- 1 | import ShelterDetails from './ShelterDetails'; 2 | 3 | export default ShelterDetails; 4 | -------------------------------------------------------------------------------- /components/ShelterInfoBar/ShelterInfoBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Link from 'next/link'; 4 | import { SidebarSectionIcon, Button } from '@styles'; 5 | import { 6 | Title, 7 | List, 8 | ListItem, 9 | Label, 10 | Value, 11 | } from '@components/PetDetails/PetDetails.styles'; 12 | 13 | import PetShelterIcon from '@static/images/icons/018-pet-shelter.svg'; 14 | 15 | const ShelterInfoBar = ({ shelter, withIcon, withTitle, withButton }) => { 16 | if (!shelter) { 17 | return null; 18 | } 19 | 20 | return ( 21 | <> 22 | {withIcon && ( 23 | 24 | 25 | 26 | )} 27 | 28 | {withTitle && ( 29 | 30 | {shelter.name} 31 | 32 | )} 33 | 34 | 35 | 36 | {shelter.address1} 37 | {shelter.zip} 38 | 39 | {shelter.city}, {shelter.state} 40 | 41 | 42 | 43 | 44 | 45 | {shelter.phone} 46 | 47 | 48 | 49 | 25 ? 13 : 16}> 50 | {shelter.email} 51 | 52 | 53 | 54 | {withButton && ( 55 | 56 | 57 | 58 | )} 59 | 60 | ); 61 | }; 62 | 63 | ShelterInfoBar.propTypes = { 64 | shelter: PropTypes.oneOfType([PropTypes.object]).isRequired, 65 | withIcon: PropTypes.bool, 66 | withTitle: PropTypes.bool, 67 | withButton: PropTypes.bool, 68 | }; 69 | 70 | ShelterInfoBar.defaultProps = { 71 | withIcon: false, 72 | withTitle: false, 73 | withButton: false, 74 | }; 75 | 76 | export default ShelterInfoBar; 77 | -------------------------------------------------------------------------------- /components/ShelterInfoBar/ShelterInfoBar.styles.js: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Test = styled.div` 4 | // display: flex; 5 | // `; 6 | // -------------------------------------------------------------------------------- /components/ShelterInfoBar/index.js: -------------------------------------------------------------------------------- 1 | import ShelterInfoBar from './ShelterInfoBar'; 2 | 3 | export default ShelterInfoBar; 4 | -------------------------------------------------------------------------------- /components/ShelterMap/ShelterMap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import GoogleMapReact from 'google-map-react'; 4 | import getConfig from 'next/config'; 5 | import { ShelterMapContainer, MapMarker } from './ShelterMap.styles'; 6 | 7 | const { publicRuntimeConfig } = getConfig(); 8 | 9 | const ShelterMap = ({ shelter }) => { 10 | if (!shelter) { 11 | return null; 12 | } 13 | 14 | const coordinates = { 15 | lat: Number(parseFloat(shelter.latitude).toFixed(2)), 16 | lng: Number(parseFloat(shelter.longitude).toFixed(2)), 17 | }; 18 | 19 | const Marker = ({ text }) => {text}; 20 | 21 | return ( 22 | 23 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | ShelterMap.propTypes = { 35 | shelter: PropTypes.oneOfType([PropTypes.object]).isRequired, 36 | }; 37 | 38 | export default ShelterMap; 39 | -------------------------------------------------------------------------------- /components/ShelterMap/ShelterMap.styles.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@styles'; 2 | 3 | export const ShelterMapContainer = styled.div` 4 | width: 100%; 5 | height: 300px; 6 | margin-bottom: 20px; 7 | `; 8 | 9 | export const MapMarker = styled.div` 10 | background-color: #aaa; 11 | background-image: linear-gradient( 12 | left, 13 | hsla(0, 0%, 100%, 0.2), 14 | hsla(0, 0%, 0%, 0.2) 15 | ); 16 | display: block; 17 | height: 1em; 18 | left: 50%; 19 | margin: -1em -0.05em; 20 | position: absolute; 21 | top: 50%; 22 | width: 0.15em; 23 | 24 | &:after { 25 | background-color: red; 26 | background-image: radial-gradient( 27 | circle, 28 | 25% 25%, 29 | hsla(0, 0%, 100%, 0.2), 30 | hsla(0, 0%, 0%, 0.2) 31 | ); 32 | border-radius: 50%; 33 | content: ''; 34 | height: 1.1em; 35 | left: -0.45em; 36 | position: absolute; 37 | top: -0.65em; 38 | width: 1.1em; 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /components/ShelterMap/index.js: -------------------------------------------------------------------------------- 1 | import ShelterMap from './ShelterMap'; 2 | 3 | export default ShelterMap; 4 | -------------------------------------------------------------------------------- /components/ShelterPets/ShelterPets.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { map } from 'lodash/fp'; 4 | import { Query } from 'react-apollo'; 5 | import PetCard from '@components/PetCard'; 6 | import Placeholder from '@components/Placeholder'; 7 | import { shelterGetPetsQuery } from '@queries'; 8 | import { loadMoreContent } from '@helpers'; 9 | import { ShelterPetsWrapper, ShelterPetsList } from './ShelterPets.styles'; 10 | import { Col, Button } from '@styles'; 11 | // import ShelterPetsList from './data.json'; 12 | 13 | const ShelterPets = ({ shelterId }) => { 14 | if (!shelterId) { 15 | return null; 16 | } 17 | 18 | const shelterGetPetsQueryVariables = { 19 | shelterId: shelterId, 20 | status: 'A', 21 | count: 8, 22 | offset: 0, 23 | }; 24 | 25 | return ( 26 | 31 | {({ loading, error, data, fetchMore }) => { 32 | if (error) return
Error loading pets.
; 33 | if (loading) { 34 | return ( 35 | 36 | 37 | {Array(shelterGetPetsQueryVariables.count) 38 | .fill('') 39 | .map((p, i) => ( 40 | 41 | 42 | 43 | ))} 44 | 45 | 46 | ); 47 | } 48 | 49 | if (!data.shelterGetPets) { 50 | return null; 51 | } 52 | 53 | return ( 54 | 55 | 56 | {map( 57 | pet => ( 58 | 59 | 60 | 61 | ), 62 | data.shelterGetPets.pets, 63 | )} 64 | 65 | 76 | 77 | ); 78 | }} 79 |
80 | ); 81 | }; 82 | 83 | ShelterPets.propTypes = { 84 | shelterId: PropTypes.string.isRequired, 85 | }; 86 | 87 | export default React.memo(ShelterPets); 88 | -------------------------------------------------------------------------------- /components/ShelterPets/ShelterPets.styles.js: -------------------------------------------------------------------------------- 1 | import { styled, Grid, Row } from '@styles'; 2 | 3 | export const ShelterPetsWrapper = styled(Grid)``; 4 | 5 | export const ShelterPetsList = styled(Row)``; 6 | -------------------------------------------------------------------------------- /components/ShelterPets/index.js: -------------------------------------------------------------------------------- 1 | import ShelterPets from './ShelterPets'; 2 | 3 | export default ShelterPets; 4 | -------------------------------------------------------------------------------- /components/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { SidebarWrapper, Navigation, MenuList, MenuItem, MenuLink } from './Sidebar.styles'; 4 | 5 | const Sidebar = (props) => ( 6 | 7 | 8 | 9 | 10 | Home 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | Sidebar.propTypes = { 18 | // bla: PropTypes.string, 19 | }; 20 | 21 | Sidebar.defaultProps = { 22 | // bla: 'test', 23 | }; 24 | 25 | export default Sidebar; 26 | -------------------------------------------------------------------------------- /components/Sidebar/Sidebar.styles.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@styles'; 2 | import Link from 'next/link'; 3 | 4 | export const SidebarWrapper = styled.div` 5 | display: flex; 6 | width: 100px; 7 | background-color: #000; 8 | color: #fff; 9 | `; 10 | 11 | export const Navigation = styled.nav``; 12 | 13 | export const MenuList = styled.ul``; 14 | 15 | export const MenuItem = styled.li``; 16 | 17 | export const MenuLink = styled(Link)``; 18 | -------------------------------------------------------------------------------- /components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import Sidebar from './Sidebar'; 2 | 3 | export default Sidebar; 4 | -------------------------------------------------------------------------------- /components/styles.js: -------------------------------------------------------------------------------- 1 | // May be we can use emotion later. 2 | // So, with this, we don't need to change all of the components styles. 3 | // Just change 'styled-components' to '@emotion/styled' 4 | import styled, { css, keyframes } from 'styled-components'; 5 | import { ifProp, prop } from 'styled-tools'; 6 | import resetCSS from 'styled-reset'; 7 | import theme from '@theme'; 8 | 9 | export { styled, ifProp, prop, css }; 10 | 11 | // Rest of styles 12 | export const globalStyles = ` 13 | ${resetCSS} 14 | html { 15 | box-sizing: border-box; 16 | } 17 | 18 | *, *:before, *:after { 19 | box-sizing: inherit; 20 | } 21 | 22 | body { 23 | width: 100vw; 24 | height: 100vh; 25 | font-family: 'Quicksand', Georgia, "Times New Roman", sans-serif; 26 | margin: 0; 27 | overflow-x: hidden; 28 | font-weight: 400; 29 | background-color: #F8F7F2; 30 | background-image: url(/static/images/bg.png); 31 | background-blend-mode: screen; 32 | } 33 | 34 | body > div { 35 | height: 100%; 36 | width: 100%; 37 | } 38 | 39 | strong, b, h2, h1, h3, h4, h5, h6 { 40 | font-weight: 600; 41 | } 42 | 43 | h1 { 44 | display: block; 45 | font-size: 2em; 46 | margin: 15px 0; 47 | } 48 | 49 | p { 50 | margin: 30px 0; 51 | } 52 | `; 53 | 54 | export const media = { 55 | xs: (...args) => css` 56 | @media (max-width: ${theme.grid.breakpoints.sm}px) { 57 | ${css(...args)}; 58 | } 59 | `, 60 | sm: (...args) => css` 61 | @media (min-width: ${theme.grid.breakpoints.sm}px) { 62 | ${css(...args)}; 63 | } 64 | `, 65 | md: (...args) => css` 66 | @media (min-width: ${theme.grid.breakpoints.md}px) { 67 | ${css(...args)}; 68 | } 69 | `, 70 | lg: (...args) => css` 71 | @media (min-width: ${theme.grid.breakpoints.lg}px) { 72 | ${css(...args)}; 73 | } 74 | `, 75 | mdOnly: (...args) => css` 76 | @media (min-width: ${theme.grid.breakpoints.md}px) and (max-width: ${theme 77 | .grid.breakpoints.lg - 1}px) { 78 | ${css(...args)}; 79 | } 80 | `, 81 | smOnly: (...args) => css` 82 | @media (min-width: ${theme.grid.breakpoints.sm}px) and (max-width: ${theme 83 | .grid.breakpoints.md - 1}px) { 84 | ${css(...args)}; 85 | } 86 | `, 87 | smLess: (...args) => css` 88 | @media (max-width: ${theme.grid.breakpoints.sm}px) { 89 | ${css(...args)}; 90 | } 91 | `, 92 | mdLess: (...args) => css` 93 | @media (max-width: ${theme.grid.breakpoints.md}px) { 94 | ${css(...args)}; 95 | } 96 | `, 97 | lgLess: (...args) => css` 98 | @media (max-width: ${theme.grid.breakpoints.lg}px) { 99 | ${css(...args)}; 100 | } 101 | `, 102 | }; 103 | 104 | export const FadeIn = keyframes` 105 | 0% { 106 | opacity: 0; 107 | } 108 | 100% { 109 | opacity: 1; 110 | } 111 | `; 112 | 113 | export const FadeOut = keyframes` 114 | 0% { 115 | opacity: 1; 116 | } 117 | 100% { 118 | opacity: 0; 119 | } 120 | `; 121 | 122 | export const GeneralContainer = styled.div` 123 | display: flex; 124 | flex-direction: column; 125 | width: ${prop('width', 300)}px; 126 | margin-bottom: 20px; 127 | `; 128 | 129 | export const LayoutContainer = styled.div` 130 | width: 100%; 131 | height: 100%; 132 | display: flex; 133 | flex-direction: column; 134 | `; 135 | 136 | export const Content = styled.div` 137 | display: flex; 138 | width: 100%; 139 | height: 100%; 140 | `; 141 | 142 | export const Grid = styled.div` 143 | width: ${prop('width', '100%')}; 144 | margin-left: -5px; 145 | margin-right: -5px; 146 | ${ifProp( 147 | 'width', 148 | css` 149 | margin: 0 auto; 150 | `, 151 | )} 152 | `; 153 | 154 | export const Row = styled.div` 155 | width: 100%; 156 | display: flex; 157 | padding: 5px 0 5px 0px; 158 | flex-wrap: wrap; 159 | `; 160 | 161 | export const Col = styled.div` 162 | width: auto; 163 | ${ifProp('extend', 'width: 100%;')} 164 | ${ifProp( 165 | 'xs', 166 | media.xs` 167 | width: ${({ gridSize, xs }) => 168 | (100 / (gridSize || theme.grid.size)) * xs}%; 169 | `, 170 | )} 171 | ${ifProp( 172 | 'sm', 173 | media.sm` 174 | width: ${({ gridSize, sm }) => 175 | (100 / (gridSize || theme.grid.size)) * sm}%; 176 | `, 177 | )} 178 | ${ifProp( 179 | 'md', 180 | media.md` 181 | width: ${({ gridSize, md }) => 182 | (100 / (gridSize || theme.grid.size)) * md}%; 183 | `, 184 | )} 185 | ${ifProp( 186 | 'lg', 187 | media.lg` 188 | width: ${({ gridSize, lg }) => (100 / (gridSize || theme.grid.size)) * lg}%; 189 | `, 190 | )} 191 | padding-left: 5px; 192 | padding-right: 5px; 193 | display: inline-block; 194 | vertical-align: top; 195 | `; 196 | 197 | export const NoRecordAvailable = styled.div` 198 | width: 100vw; 199 | height: 100vh; 200 | display: flex; 201 | justify-content: center; 202 | align-items: center; 203 | `; 204 | 205 | export const NoRecordText = styled.div` 206 | display: flex; 207 | flex-direction: column; 208 | align-items: center; 209 | 210 | & > svg { 211 | height: 200px; 212 | 213 | ${media.smLess` 214 | height: 150px; 215 | `} 216 | } 217 | 218 | & > p { 219 | font-size: 52px; 220 | 221 | ${media.smLess` 222 | font-size: 32px; 223 | `} 224 | } 225 | `; 226 | 227 | export const ContentSidebar = styled(Col)` 228 | display: flex; 229 | flex-direction: column; 230 | `; 231 | 232 | export const SidebarSection = styled.div` 233 | overflow: visible; 234 | padding: 19px 30px; 235 | margin-bottom: 82px; 236 | position: relative; 237 | border: none; 238 | border-radius: 10px; 239 | box-shadow: 0 3px 3px rgba(77, 71, 81, 0.2); 240 | background-color: #fff; 241 | 242 | ${ifProp( 243 | { withIcon: true }, 244 | css` 245 | padding-top: 85px; 246 | `, 247 | )} 248 | `; 249 | 250 | export const SidebarSectionIcon = styled.div` 251 | display: flex; 252 | justify-content: center; 253 | align-items: center; 254 | align-content: center; 255 | width: 115px; 256 | height: 115px; 257 | border: 7px solid #fff; 258 | border-radius: 50%; 259 | position: absolute; 260 | top: -56px; 261 | left: calc(50% - 58.5px); 262 | background-color: #a3d256; 263 | 264 | & > svg { 265 | width: 60px; 266 | fill: #fff; 267 | } 268 | `; 269 | 270 | export const Button = styled.span` 271 | width: ${prop('width', '100%')}; 272 | display: flex; 273 | justify-content: center; 274 | align-items: center; 275 | border-color: #4d4751; 276 | background-color: #4d4751; 277 | color: #fff; 278 | height: 50px; 279 | border-radius: 27px; 280 | margin: 17px 0 0; 281 | cursor: pointer; 282 | 283 | &:hover { 284 | background-color: #868686; 285 | } 286 | `; 287 | -------------------------------------------------------------------------------- /components/theme.js: -------------------------------------------------------------------------------- 1 | // May be we can use emotion later. 2 | // So, with this, we don't need to change into all of the components. 3 | // Just change 'styled-components' to 'emotion-theming' 4 | import { ThemeProvider } from 'styled-components'; 5 | 6 | export { ThemeProvider }; 7 | 8 | // Rest of the theme 9 | const theme = { 10 | grid: { 11 | size: 12, 12 | gutter: 10, // 10px 13 | outerMargin: 1, 14 | breakpoints: { 15 | xs: 0, // px 16 | sm: 768, // px 17 | md: 960, // px 18 | lg: 1200, // px 19 | }, 20 | }, 21 | }; 22 | 23 | export default theme; 24 | -------------------------------------------------------------------------------- /helpers/hooks/index.js: -------------------------------------------------------------------------------- 1 | import useSearch from './useSearch'; 2 | import { useGeoLocation, useAddress, coor2address } from './useGeoLocation'; 3 | import { useCurrentAnimal, useUserFilters } from './useFilters'; 4 | 5 | export { 6 | useSearch, 7 | useGeoLocation, 8 | useAddress, 9 | coor2address, 10 | useCurrentAnimal, 11 | useUserFilters, 12 | }; 13 | -------------------------------------------------------------------------------- /helpers/hooks/useFilters.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useCurrentAnimal = (initialAnimal = {}) => { 4 | const [currentAnimal, setAnimal] = useState(initialAnimal); 5 | 6 | return { currentAnimal, setAnimal }; 7 | }; 8 | 9 | export const useUserFilters = () => { 10 | const [userFilters, setUserFilter] = useState({ 11 | location: 'Canada', 12 | animal: null, 13 | breed: null, 14 | size: null, 15 | sex: null, 16 | age: null, 17 | offset: null, 18 | count: 12, 19 | }); 20 | 21 | return { userFilters, setUserFilter }; 22 | }; 23 | -------------------------------------------------------------------------------- /helpers/hooks/useGeoLocation.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { has } from 'lodash/fp'; 3 | 4 | export const useGeoLocation = () => { 5 | const [location, setLocation] = useState({ 6 | accuracy: null, 7 | altitude: null, 8 | altitudeAccuracy: null, 9 | heading: null, 10 | latitude: null, 11 | longitude: null, 12 | address: null, 13 | speed: null, 14 | timestamp: Date.now(), 15 | }); 16 | let isMounted = true; 17 | let watchId; 18 | 19 | const onEvent = event => { 20 | if (isMounted) { 21 | // const currentAddress = event.coords.latitude 22 | // ? coor2address(event.coords.latitude, event.coords.longitude) 23 | // : ''; 24 | 25 | setLocation({ 26 | accuracy: event.coords.accuracy, 27 | altitude: event.coords.altitude, 28 | altitudeAccuracy: event.coords.altitudeAccuracy, 29 | heading: event.coords.heading, 30 | latitude: event.coords.latitude, 31 | longitude: event.coords.longitude, 32 | address: '', 33 | speed: event.coords.speed, 34 | timestamp: event.timestamp, 35 | }); 36 | } 37 | }; 38 | 39 | useEffect( 40 | () => { 41 | navigator.geolocation.getCurrentPosition(onEvent); 42 | watchId = navigator.geolocation.watchPosition(onEvent); 43 | 44 | return () => { 45 | isMounted = false; 46 | navigator.geolocation.clearWatch(watchId); 47 | }; 48 | }, 49 | [0], 50 | ); 51 | 52 | return location; 53 | }; 54 | 55 | export const useAddress = () => { 56 | const [currentAddress, setAddress] = useState({ 57 | result: '', 58 | error: false, 59 | }); 60 | 61 | return { currentAddress, setAddress }; 62 | }; 63 | 64 | export const coor2address = (lat, long, cb = () => {}) => { 65 | const output = (result = '', error = false) => ({ 66 | result, 67 | error, 68 | }); 69 | 70 | try { 71 | const geocoder = new google.maps.Geocoder(); 72 | const latLng = new google.maps.LatLng(lat, long); 73 | let address; 74 | 75 | geocoder.geocode( 76 | { 77 | latLng: latLng, 78 | }, 79 | (result, message) => { 80 | console.log('result', result); 81 | message === google.maps.GeocoderStatus.OK 82 | ? result[7] 83 | ? cb(output(result[7].formatted_address)) 84 | : cb(output(null, 'No results found')) 85 | : cb(output(null, `Geocoder failed due to: ${message}`)); 86 | }, 87 | ); 88 | } catch (err) { 89 | cb(output(null, `Geocoder failed due to: ${err.message}`)); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /helpers/hooks/useSearch.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useSearch = () => { 4 | const [isSearchActive, setSearchStatus] = useState(false); 5 | 6 | return { isSearchActive, setSearchStatus }; 7 | }; 8 | 9 | export default useSearch; 10 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | import { has, uniqWith, isEqual, uniqBy } from 'lodash/fp'; 2 | 3 | /** 4 | * @param {string} The word to convert the first letter to uppercase. 5 | * @param {string} locale 6 | * 7 | * capitalizeFirstLetter('italia', 'en'); // "Italya" 8 | * capitalizeFirstLetter('italya', 'tr'); // "İtalya" 9 | */ 10 | export const capitalizeFirstLetter = ([first, ...rest], locale = 'en') => { 11 | return [first.toLocaleUpperCase(locale), ...rest].join(''); 12 | }; 13 | 14 | const deepMerge = (target, source) => { 15 | if (typeof target !== 'object' || typeof source !== 'object') return false; 16 | for (var prop in source) { 17 | if (!source.hasOwnProperty(prop)) continue; 18 | if (prop in target) { 19 | if (typeof target[prop] !== 'object') { 20 | target[prop] = source[prop]; 21 | } else { 22 | if (typeof source[prop] !== 'object') { 23 | target[prop] = source[prop]; 24 | } else { 25 | if (target[prop].concat && source[prop].concat) { 26 | target[prop] = target[prop].concat(source[prop]); 27 | } else { 28 | target[prop] = deepMerge(target[prop], source[prop]); 29 | } 30 | } 31 | } 32 | } else { 33 | target[prop] = source[prop]; 34 | } 35 | } 36 | return target; 37 | }; 38 | 39 | const getUniqPets = (data, queryType) => { 40 | const uniqPets = uniqBy('id', data[queryType].pets); 41 | const result = { 42 | [queryType]: { 43 | ...data[queryType], 44 | pets: uniqPets, 45 | }, 46 | }; 47 | 48 | return result; 49 | }; 50 | 51 | export const loadMoreContent = ({ 52 | queryType, 53 | data, 54 | fetchMore, 55 | params = {}, 56 | }) => { 57 | fetchMore({ 58 | variables: { 59 | offset: data.length, 60 | // skip: data.length, 61 | ...params, 62 | }, 63 | updateQuery: (previousResult, { fetchMoreResult }) => { 64 | const prev = getUniqPets(previousResult, queryType); 65 | const next = getUniqPets(fetchMoreResult, queryType); 66 | 67 | if (!next) { 68 | return prev; 69 | } 70 | 71 | const merged = deepMerge(prev, next); 72 | const uniqPets = getUniqPets(merged, queryType); 73 | 74 | return uniqPets; 75 | }, 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "root": ["src/*"], 6 | "@component/*": ["components/*"], 7 | "@styles": ["components/styles"], 8 | "@theme": ["components/theme"], 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /layouts/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { LayoutContainer, Content } from '@styles'; 4 | import Search from '@components/Search'; 5 | import { useSearch } from '@helpers/hooks'; 6 | import { Header, Title, LogoWrapper, SearchIcon } from './MainLayout.styles'; 7 | 8 | import LogoImage from './logo-adoption.svg'; 9 | const SearchImage = '/static/images/search.svg'; 10 | 11 | const MainLayout = ({ children }) => { 12 | const { isSearchActive, setSearchStatus } = useSearch(); 13 | 14 | const onClickSearch = () => { 15 | setSearchStatus(!isSearchActive); 16 | 17 | console.log('isSearchActive', isSearchActive); 18 | }; 19 | 20 | return ( 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 33 |
34 | 35 | {children} 36 |
37 | ); 38 | }; 39 | 40 | export default MainLayout; 41 | -------------------------------------------------------------------------------- /layouts/MainLayout.styles.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@styles'; 2 | 3 | export const Header = styled.div` 4 | height: 100px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 20px; 9 | position: absolute; 10 | width: 100%; 11 | z-index: 3; 12 | `; 13 | 14 | export const Title = styled.span` 15 | font-weight: 700; 16 | cursor: pointer; 17 | `; 18 | 19 | export const LogoWrapper = styled.span` 20 | width: 230px; 21 | height: 60px; 22 | cursor: pointer; 23 | `; 24 | 25 | export const SearchIcon = styled.img` 26 | &:hover { 27 | cursor: pointer; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /layouts/index.js: -------------------------------------------------------------------------------- 1 | import MainLayout from './MainLayout'; 2 | 3 | export default MainLayout; 4 | -------------------------------------------------------------------------------- /layouts/logo-adoption.svg: -------------------------------------------------------------------------------- 1 | Asset 2pet adoption -------------------------------------------------------------------------------- /next.config-dev.js: -------------------------------------------------------------------------------- 1 | const PETFINDER_API_REMOTE = 'https://ac-petfinderql.herokuapp.com/'; 2 | const PETFINDER_API_LOCAL = 'http://localhost:4000/'; 3 | const GOOGLE_MAP_API_KEY = ''; 4 | 5 | module.exports = { 6 | serverRuntimeConfig: { 7 | PETFINDER_API_REMOTE, 8 | PETFINDER_API_LOCAL, 9 | GOOGLE_MAP_API_KEY, 10 | }, 11 | publicRuntimeConfig: { 12 | PETFINDER_API_REMOTE, 13 | PETFINDER_API_LOCAL, 14 | GOOGLE_MAP_API_KEY, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ac-react-adoption", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/abdullahceylan/ac-react-adoption.git" 8 | }, 9 | "author": "Abdullah Ceylan ", 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "node server.js", 13 | "dev:next": "next", 14 | "build": "export NODE_ENV=production; next build", 15 | "start": "export NODE_ENV=production; node server.js", 16 | "start:next": "next start -p $PORT", 17 | "heroku-postbuild": "npm run build" 18 | }, 19 | "dependencies": { 20 | "ac-react-simple-image-slider": "0.0.7", 21 | "apollo-boost": "^0.1.23", 22 | "downshift": "^3.1.8", 23 | "express": "^4.16.4", 24 | "google-map-react": "^1.1.2", 25 | "graphql": "^14.0.2", 26 | "isomorphic-unfetch": "^3.0.0", 27 | "lodash": "^4.17.11", 28 | "match-sorter": "^2.3.0", 29 | "next": "^7.0.2", 30 | "prop-types": "^15.6.2", 31 | "react": "16.7.0-alpha.2", 32 | "react-apollo": "^2.3.3", 33 | "react-content-loader": "^3.4.2", 34 | "react-dom": "16.7.0-alpha.2", 35 | "react-lazyload": "^2.3.0", 36 | "styled-components": "^4.1.3", 37 | "styled-icons": "^6.2.0", 38 | "styled-reset": "^1.6.4", 39 | "styled-tools": "^1.6.0" 40 | }, 41 | "devDependencies": { 42 | "babel-plugin-inline-react-svg": "^1.0.1", 43 | "babel-plugin-module-resolver": "^3.1.1", 44 | "babel-plugin-react-element-info": "^1.0.1", 45 | "babel-plugin-transform-remove-console": "^6.9.4", 46 | "eslint-import-resolver-babel-module": "^4.0.0", 47 | "eslint-plugin-import": "^2.14.0", 48 | "eslint-plugin-react-hooks": "^0.0.0" 49 | }, 50 | "browserslist": [ 51 | ">0.2%", 52 | "not dead", 53 | "not ie <= 11", 54 | "not op_mini all" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, { Container } from 'next/app'; 2 | import Head from 'next/head'; 3 | import React from 'react'; 4 | import { ApolloProvider } from 'react-apollo'; 5 | import { withApolloClient } from '@api'; 6 | class Adoption extends App { 7 | static async getInitialProps({ Component, router, ctx }) { 8 | let pageProps = {}; 9 | 10 | if (Component.getInitialProps) { 11 | pageProps = await Component.getInitialProps(ctx); 12 | } 13 | 14 | return { pageProps }; 15 | } 16 | 17 | render() { 18 | const { Component, pageProps, apolloClient } = this.props; 19 | return ( 20 | 21 | 22 | 23 | AC Pet Adoption 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default withApolloClient(Adoption); 33 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Head, Main, NextScript } from 'next/document'; 3 | import { ServerStyleSheet } from 'styled-components'; 4 | import { globalStyles } from '../components/styles'; 5 | 6 | export default class MyDocument extends Document { 7 | static getInitialProps({ renderPage }) { 8 | const sheet = new ServerStyleSheet(); 9 | const page = renderPage(App => props => 10 | sheet.collectStyles(), 11 | ); 12 | const styleTags = sheet.getStyleElement(); 13 | return { ...page, styleTags }; 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 25 | {this.props.styleTags} 26 | Asset 1 -------------------------------------------------------------------------------- /static/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/images/searching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/searching.gif --------------------------------------------------------------------------------