├── .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 | 
10 |
11 | ### Search
12 | 
13 |
14 | ### Pet and Shelter details
15 | 
16 |
17 | ### 404
18 | 
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 |
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 |
160 | );
161 | };
162 |
163 | export const XIcon = () => {
164 | return (
165 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/pages/_error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import MainLayout from '../layouts';
4 | import { Row, Col } from '@styles';
5 | import ErrorMessage from '@components/ErrorMessage';
6 |
7 | const Error = ({ params, statusCode }) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | Error.getInitialProps = ({ query, res, jsonPageRes }) => {
20 | const statusCode = res
21 | ? res.statusCode
22 | : jsonPageRes
23 | ? jsonPageRes.status
24 | : null;
25 | return { params: query, pageProps: { pageType: statusCode }, statusCode };
26 | };
27 |
28 | Error.propTypes = {
29 | params: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
30 | statusCode: PropTypes.number.isRequired,
31 | };
32 |
33 | export default Error;
34 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import theme, { ThemeProvider } from '@theme';
3 | import MainLayout from '../layouts';
4 | import Home from '@components/Home';
5 |
6 | class App extends Component {
7 | static async getInitialProps({ query }) {
8 | return { pageParams: query };
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/pages/petDetails.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import theme, { ThemeProvider } from '@theme';
3 | import MainLayout from '../layouts';
4 | import PetDetails from '@components/PetDetails';
5 |
6 | class Details extends Component {
7 | static async getInitialProps({ query }) {
8 | return { pageParams: query };
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default Details;
23 |
--------------------------------------------------------------------------------
/pages/shelterDetails.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import theme, { ThemeProvider } from '@theme';
3 | import MainLayout from '../layouts';
4 | import ShelterDetails from '@components/ShelterDetails';
5 |
6 | class ShelterDetailsPage extends Component {
7 | static async getInitialProps({ query }) {
8 | return { pageParams: query };
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default ShelterDetailsPage;
23 |
--------------------------------------------------------------------------------
/queries/index.js:
--------------------------------------------------------------------------------
1 | import petDetailsQuery from './petDetails.query';
2 | import petFindQuery from './petFind.query';
3 | import shelterQuery from './shelterDetails.query';
4 | import shelterGetPetsQuery from './shelterGetPets.query';
5 |
6 | export { petDetailsQuery, petFindQuery, shelterQuery, shelterGetPetsQuery };
7 |
--------------------------------------------------------------------------------
/queries/petDetails.query.js:
--------------------------------------------------------------------------------
1 | import { graphql, compose } from 'react-apollo';
2 | import gql from 'graphql-tag';
3 | import { has } from 'lodash/fp';
4 |
5 | export const getPetQuery = gql`
6 | query pet($id: String!) {
7 | pet(id: $id) {
8 | id
9 | name
10 | status
11 | age
12 | size
13 | id
14 | shelterPetId
15 | sex
16 | description
17 | mix
18 | shelterId
19 | lastUpdate
20 | animal
21 | options {
22 | option
23 | }
24 | media {
25 | photos {
26 | size
27 | url
28 | }
29 | }
30 | }
31 | }
32 | `;
33 |
34 | export const getPetQueryVariables = {
35 | id: '0',
36 | };
37 |
38 | const getShelterQuery = gql`
39 | query shelter($shelterId: String!) {
40 | shelter(shelterId: $shelterId) {
41 | id
42 | name
43 | phone
44 | email
45 | address1
46 | # address2
47 | city
48 | state
49 | zip
50 | country
51 | # fax
52 | latitude
53 | longitude
54 | }
55 | }
56 | `;
57 |
58 | const PetDetailsQuery = compose(
59 | graphql(getPetQuery, {
60 | name: 'getPetQuery',
61 | options: props => {
62 | // console.log('propsss', props);
63 | return {
64 | variables: {
65 | ...getPetQueryVariables,
66 | id: props.pageParams.id,
67 | },
68 | };
69 | },
70 | }),
71 | graphql(getShelterQuery, {
72 | name: 'getShelterQuery',
73 | options: ({ getPetQuery }) => {
74 | // console.log('getPetQuery', getPetQuery);
75 | return {
76 | variables: {
77 | shelterId:
78 | has('pet.shelterId', getPetQuery) && getPetQuery.pet.shelterId,
79 | },
80 | };
81 | },
82 | }),
83 | );
84 |
85 | export default PetDetailsQuery;
86 |
--------------------------------------------------------------------------------
/queries/petFind.query.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const petFindQuery = gql`
4 | query petFind(
5 | $location: String!
6 | $animal: String
7 | $breed: String
8 | $size: String
9 | $sex: String
10 | $age: String
11 | $offset: Int
12 | $count: Int
13 | ) {
14 | petFind(
15 | location: $location
16 | animal: $animal
17 | breed: $breed
18 | size: $size
19 | sex: $sex
20 | age: $age
21 | offset: $offset
22 | count: $count
23 | ) {
24 | lastOffset
25 | pets {
26 | name
27 | status
28 | age
29 | size
30 | media {
31 | photos {
32 | size
33 | url
34 | }
35 | }
36 | id
37 | shelterPetId
38 | breeds {
39 | breed
40 | }
41 | sex
42 | description
43 | mix
44 | shelterId
45 | lastUpdate
46 | animal
47 | }
48 | }
49 | }
50 | `;
51 | export default petFindQuery;
52 |
--------------------------------------------------------------------------------
/queries/shelterDetails.query.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const shelterFindQuery = gql`
4 | query shelterFindQuery($shelterId: String!) {
5 | shelter(shelterId: $shelterId) {
6 | id
7 | name
8 | phone
9 | email
10 | address1
11 | address2
12 | city
13 | state
14 | zip
15 | country
16 | fax
17 | latitude
18 | longitude
19 | }
20 | }
21 | `;
22 |
23 | export default shelterFindQuery;
24 |
--------------------------------------------------------------------------------
/queries/shelterGetPets.query.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const shelterPetsQuery = gql`
4 | query shelterGetPets(
5 | $shelterId: String!
6 | $count: Int
7 | $offset: Int
8 | $status: String
9 | ) {
10 | shelterGetPets(
11 | shelterId: $shelterId
12 | count: $count
13 | offset: $offset
14 | status: $status
15 | ) {
16 | lastOffset
17 | isLastRecord
18 | pets {
19 | id
20 | name
21 | breeds {
22 | breed
23 | }
24 | media {
25 | photos {
26 | size
27 | url
28 | }
29 | }
30 | animal
31 | age
32 | size
33 | sex
34 | }
35 | }
36 | }
37 | `;
38 |
39 | export default shelterPetsQuery;
40 |
--------------------------------------------------------------------------------
/screenshoots/404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/screenshoots/404.jpg
--------------------------------------------------------------------------------
/screenshoots/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/screenshoots/home.jpg
--------------------------------------------------------------------------------
/screenshoots/search.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/screenshoots/search.jpg
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const next = require('next');
3 |
4 | const dev = process.env.NODE_ENV !== 'production';
5 | const port = process.env.PORT || 3000;
6 | const app = next({ dev });
7 | const handle = app.getRequestHandler();
8 |
9 | const logger = console.log;
10 |
11 | if (dev) {
12 | logger('Starting the development server...\n');
13 | } else {
14 | logger('Starting the production server...\n');
15 | }
16 |
17 | app
18 | .prepare()
19 | .then(() => {
20 | const server = express();
21 |
22 | server.get('/pet-details/:id', (req, res) => {
23 | const page = '/petDetails';
24 | const queryParams = { id: req.params.id };
25 | app.render(req, res, page, queryParams);
26 | });
27 |
28 | server.get('/shelter-details/:id', (req, res) => {
29 | const page = '/shelterDetails';
30 | const queryParams = { id: req.params.id };
31 | app.render(req, res, page, queryParams);
32 | });
33 |
34 | server.get('*', (req, res) => {
35 | return handle(req, res);
36 | });
37 |
38 | server.listen(port, err => {
39 | if (err) {
40 | throw err.message;
41 | }
42 | logger(`> Ready on http://localhost:${port}`);
43 | });
44 | })
45 | .catch(ex => {
46 | logger(ex.stack);
47 | process.exit(1);
48 | });
49 |
--------------------------------------------------------------------------------
/static/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/bg.png
--------------------------------------------------------------------------------
/static/images/city.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/city.png
--------------------------------------------------------------------------------
/static/images/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/images/dog-right.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/dog-right.jpg
--------------------------------------------------------------------------------
/static/images/girl-and-dog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/girl-and-dog.png
--------------------------------------------------------------------------------
/static/images/icons/001-chicken-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/001-dog-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
84 |
--------------------------------------------------------------------------------
/static/images/icons/001-horse-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
84 |
--------------------------------------------------------------------------------
/static/images/icons/003-halloween-black-cat.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/static/images/icons/003-track.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
79 |
--------------------------------------------------------------------------------
/static/images/icons/004-pawprint.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/005-animal-paw-print.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/static/images/icons/005-cat-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
68 |
--------------------------------------------------------------------------------
/static/images/icons/005-cat-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
52 |
--------------------------------------------------------------------------------
/static/images/icons/006-horse-4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
85 |
--------------------------------------------------------------------------------
/static/images/icons/006-horse-5.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
94 |
--------------------------------------------------------------------------------
/static/images/icons/009-bird-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/009-bird-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/011-cat-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
114 |
--------------------------------------------------------------------------------
/static/images/icons/011-snake-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/011-snake-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/013-rabbit-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/014-small-fury.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/static/images/icons/015-rabbit-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
112 |
--------------------------------------------------------------------------------
/static/images/icons/018-pet-shelter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/static/images/icons/019-animal-shelter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
52 |
--------------------------------------------------------------------------------
/static/images/image-18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/image-18.jpg
--------------------------------------------------------------------------------
/static/images/logo-pati.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/static/images/searching.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahceylan/ac-react-adoption/772d836651427ee2ca45a03c4849b3606020fadd/static/images/searching.gif
--------------------------------------------------------------------------------