├── frontend
└── src
│ ├── component
│ ├── PetfinderButtons.js
│ ├── FavPage.js
│ ├── SignupForm.js
│ └── PreferenceForm.js
│ ├── index.js
│ ├── index.html
│ ├── App.js
│ ├── pages
│ ├── Home.js
│ └── Petfinder.js
│ ├── container
│ └── MainContainer.js
│ └── styles.scss
├── .babelrc
├── .prettierignore
├── .prettierrc
├── .gitignore
├── .eslintrc.js
├── backend
├── middleware
│ └── dbController.js
├── server.js
├── models
│ └── userModel.js
└── controllers
│ └── PetController.js
├── LICENSE
├── README.md
├── webpack.config.js
└── package.json
/frontend/src/component/PetfinderButtons.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | build/
4 | *.min.json
5 | *.node_modules
6 | *.json
7 | *.svg
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "printWidth": 120,
5 | "singleQuote": true,
6 | "trailingComma": "none",
7 | "jsxBracketSameLine": true
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | .env
4 | dist/
5 | build/
6 | *.log
7 | .vscode/
8 | coverage/
9 | idea/
10 | *.swp
11 | .DS_Store
12 | *.cache
13 | secrets/
14 | *.key
15 | *.class
16 | *.jar
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App.js';
4 | import './styles.scss';
5 |
6 | createRoot(document.getElementById('app')).render( );
7 |
--------------------------------------------------------------------------------
/frontend/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pet Friendr
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
3 | // import Home from './pages/Home';
4 | import Petfinder from './pages/Petfinder';
5 | import MainContainer from './container/MainContainer';
6 | import Home from './pages/Home';
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | // {/*
16 | //
17 | // {/* }> */}
18 | // } />
19 | //
20 | // ; */}
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:prettier/recommended'],
9 | overrides: [
10 | {
11 | files: ['*.js'],
12 | rules: {
13 | 'react/jsx-filename-extension': 'off',
14 | },
15 | },
16 | ],
17 | parserOptions: {
18 | ecmaVersion: 'latest',
19 | ecmaFeatures: {
20 | jsx: true,
21 | },
22 | sourceType: 'module',
23 | },
24 | plugins: ['react'],
25 | rules: {
26 | 'no-console': 'off',
27 | 'react/react-in-jsx-scope': 'off',
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/backend/middleware/dbController.js:
--------------------------------------------------------------------------------
1 | const pool = require('../models/userModel');
2 |
3 | const dbController = {};
4 |
5 | // queries the database for user data based on id and stores in res.locals.userData
6 | dbController.getUserData = async (req, res, next) => {
7 | try {
8 | // TODO (Chris/Alex): Modify SELECT in db query as needed by frontend team, currently returns location and preference based on user id from res.locals.body (which is set in petController.getAuthToken)
9 | await pool.query(`SELECT location, preference FROM users WHERE id = ${res.locals.body.id};`, (err, result) => {
10 | if (err) {
11 | return next('Error getting user data in dbController.getUserData', err);
12 | } else {
13 | res.locals.userData = result.rows[0];
14 | return next();
15 | }
16 | });
17 | } catch (error) {
18 | return next('Error getting user data in dbController.getUserData', error);
19 | }
20 | };
21 |
22 | module.exports = dbController;
23 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { BrowserRouter, useNavigate, Route } from 'react-router-dom';
3 | import MainContainer from '../container/MainContainer';
4 | import FavPage from '../component/FavPage';
5 |
6 | const Home = () => {
7 | console.log('hello');
8 |
9 | const [onSwiperPage, setOnSwiperPage] = useState(true);
10 | const [savedCards, setSavedCards] = useState([]);
11 |
12 |
13 | let pageToDisplay;
14 | if (!onSwiperPage) {
15 | pageToDisplay = ;
16 | } else {
17 | pageToDisplay = ;
18 | }
19 |
20 |
21 |
22 | return (
23 |
24 |
{pageToDisplay}
25 |
26 | );
27 | };
28 |
29 | export default Home;
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Star-Nosed-Molers
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 | # Pet-Friendr
2 |
3 | An app to find and connect with our furry friends!
4 |
5 | ## Table of Contents
6 |
7 | - [Description](#description)
8 | - [Installation](#installation)
9 | - [Usage](#usage)
10 | - [License](#license)
11 | - [For Iteration Team](#for-iteration-team)
12 |
13 | ## Description
14 |
15 | Pet Friendr is an app that allows users to connect with local animal shelters to find their next furry friend. Users can search for pets by location, breed, and and other preferences. Users can swipe right to like a pet and swipe left to pass on a pet. If a user like a pet, they can view more information about the pet and contact the shelter to set up a meet and greet.
16 |
17 | ## Installation
18 |
19 | To install necessary dependencies, run the following command:
20 |
21 | ```
22 | npm i
23 | ```
24 |
25 | ## Usage
26 |
27 | To use Pet Friendr, run the following command:
28 |
29 | ```
30 | npm start
31 | ```
32 |
33 | ## License
34 |
35 | This project is licensed under the MIT license.
36 |
37 | ## For Iteration Team
38 |
39 | - [PetFinder API Reference](https://www.petfinder.com/developers/)
40 |
41 | ## Scratch Team Members
42 |
43 | - [Chris Suzukida](https://github.com/csuzukida)
44 | - [Alex Klein](https://github.com/a-t-klein)
45 | - [Tricia Corwin](https://github.com/triciacorwin)
46 | - [Jason Huang](https://github.com/jjhuang3)
47 | - [Jamie Lee](https://github.com/jamieslee97)
48 |
--------------------------------------------------------------------------------
/frontend/src/pages/Petfinder.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | // components to be passed down
4 | import MainContainer from '../container/MainContainer';
5 | import SignupForm from '../component/SignupForm';
6 | import PreferenceForm from '../component/PreferenceForm';
7 |
8 | const Petfinder = () => {
9 | // will have 3 things name&description, image , buttons
10 |
11 | const [userData, setUserData] = useState({});
12 | const [signUpClicked, setSignUpClicked] = useState(false);
13 | const [preferenceClicked, setPreferenceClicked] = useState(false);
14 |
15 | // Determine the table to display between search result and my recipe
16 | let tableToDisplay;
17 | if (!signUpClicked) {
18 | tableToDisplay = ;
19 | } else {
20 | tableToDisplay = (
21 |
27 | );
28 | }
29 |
30 | if (preferenceClicked) {
31 | tableToDisplay = ;
32 | }
33 |
34 | return (
35 |
36 |
{tableToDisplay}
37 |
38 | );
39 | };
40 |
41 | export default Petfinder;
42 |
--------------------------------------------------------------------------------
/frontend/src/component/FavPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const db = [
4 | {
5 | name: 'Dog',
6 | url: 'https://media.cnn.com/api/v1/images/stellar/prod/220818142713-dogs-tears-emotions-wellness-stock.jpg?c=16x9&q=h_720,w_1280,c_fill'
7 | },
8 | {
9 | name: 'cat',
10 | url: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT4Lm66tNBerBY_Yi2AxL8gbMoDoj46cktsAg&usqp=CAU'
11 | }
12 | ];
13 |
14 | const FavPage = ({ onSwiperPage, setOnSwiperPage, savedCards, setSavedCards }) => {
15 | const onHandleClick = (e) => {
16 | e.preventDefault();
17 | setOnSwiperPage(true);
18 | };
19 |
20 | console.log('saved card name', savedCards[0].name);
21 | console.log('saved card url', savedCards[0].url);
22 | //saved card name {dogName: 'HELP!! BEAR NEEDS FOSTER/ADOPTER '}
23 | //saved card url {imgUrl: 'https://dl5zpyw5k3jeb.cloudfront.net/photos/pets/60541288/4/?bust=1678131695&width=450'
24 |
25 | return (
26 |
27 |
Favorite Pets
28 |
29 | {savedCards.map((pet, index) => (
30 |
31 |
32 |
{pet.name.dogName}
33 |
34 | ))}
35 |
36 |
Find a new fur friend!
37 |
38 | );
39 | };
40 |
41 | export default FavPage;
42 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const petController = require('./controllers/PetController');
3 | const dbController = require('./middleware/dbController');
4 |
5 | const port = 3000;
6 | const app = express();
7 |
8 | app.use(express.json());
9 | app.use(express.urlencoded({ extended: true }));
10 |
11 | /*
12 | First middleware makes a request to PetFinder API to get an access token
13 | Second middleware gets user data from postgres database
14 | Third middleware gets a list of pets that match the user's preferences from the API
15 | */
16 | app.post(
17 | '/api/preferences/',
18 | petController.getAuthToken,
19 | dbController.getUserData,
20 | petController.getPreferences,
21 | (req, res) => {
22 | res.status(200).send(res.locals.preferences);
23 | }
24 | );
25 |
26 | // gets all pets from PetFinder API
27 | app.get('/api/all', petController.getAuthToken, petController.getAllPets, (req, res) => {
28 | res.status(200).send(res.locals.pets);
29 | });
30 |
31 | // global error handler
32 | // eslint-disable-next-line no-unused-vars
33 | app.use((err, req, res, next) => {
34 | const defaultErr = {
35 | log: 'Express error handler caught unknown middleware error',
36 | status: 500,
37 | message: { err: 'An error occurred' }
38 | };
39 | const errorObj = Object.assign({}, defaultErr, err);
40 | console.log(errorObj.log);
41 | return res.status(errorObj.status).json(errorObj.message);
42 | });
43 |
44 | app.listen(port, () => console.log(`listening on port: ${port}`));
45 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './frontend/src/index.js',
7 | output: {
8 | path: path.resolve(__dirname, 'dist'),
9 | filename: 'bundle.js'
10 | },
11 | plugins: [
12 | new HtmlWebpackPlugin({
13 | template: path.resolve(__dirname, 'frontend', 'src', 'index.html')
14 | })
15 | ],
16 | module: {
17 | rules: [
18 | {
19 | test: /\.(js|jsx)$/,
20 | exclude: /node_modules/,
21 | use: {
22 | loader: 'babel-loader',
23 | options: {
24 | presets: ['@babel/preset-env', '@babel/preset-react']
25 | }
26 | }
27 | },
28 | {
29 | test: /\.(sa|sc|c)ss$/,
30 | exclude: /node_modules/,
31 | use: ['style-loader', 'css-loader', 'sass-loader']
32 | },
33 | {
34 | test: /\.(png|woff|woff2|eot|ttf|svg|jpg|jpeg)$/i,
35 | exclude: /node_modules/,
36 | loader: 'url-loader',
37 | options: { limit: false }
38 | }
39 | ]
40 | },
41 | devServer: {
42 | port: 8080,
43 | proxy: {
44 | '/': {
45 | target: 'http://localhost:3000',
46 | changeOrigin: true
47 | }
48 | }
49 | },
50 | resolve: {
51 | fallback: {
52 | fs: false,
53 | path: require.resolve('path-browserify'),
54 | os: require.resolve('os-browserify/browser')
55 | },
56 | extensions: ['.js', '.jsx', '.json']
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | const { Pool } = require('pg');
3 |
4 | const pool = new Pool({
5 | connectionString: `postgres://fizxmgog:OGEYeJGBLpmp90rgSiJfh825on0nXDLc@mahmud.db.elephantsql.com/fizxmgog`
6 | });
7 |
8 | // testing the connection
9 | pool.connect((err, client, release) => {
10 | if (err) {
11 | return console.error('Error acquiring client', err.stack);
12 | }
13 | client.query('SELECT NOW()', (err, results) => {
14 | release();
15 | if (err) {
16 | return console.error('Error executing query', err.stack);
17 | }
18 | return;
19 | });
20 | });
21 |
22 | // create user table
23 | const createUserTable = `CREATE TABLE IF NOT EXISTS users (
24 | id SERIAL PRIMARY KEY,
25 | name VARCHAR( 50 ) NOT NULL,
26 | email VARCHAR( 255 ) NOT NULL,
27 | password VARCHAR (50),
28 | location VARCHAR( 255 ),
29 | preference json NOT NULL)`;
30 |
31 | pool.query(createUserTable, (err, results) => {
32 | if (err) {
33 | return console.error(err);
34 | }
35 | return;
36 | });
37 |
38 | // insert test data
39 | const jsonData = JSON.stringify({ species: 'Dog', age: 'Young', gender: 'Male', size: 'Medium' });
40 |
41 | const insertTest = `
42 | INSERT INTO users (name, email, location, preference)
43 | VALUES ('testUser', 'test@123.com', '90032', '${jsonData}')
44 | `;
45 |
46 | // pool.query(insertTest, async (err, results) => {
47 | // if (err) {
48 | // return console.error(err);
49 | // }
50 | // return;
51 | // });
52 |
53 | pool.query('SELECT * FROM users', (err, results) => {
54 | if (err) {
55 | return console.error('ERROR IN SELECT ALL FROM USERS', err);
56 | }
57 | // console.log('RESULT ROWS', results.rows);
58 | return;
59 | });
60 |
61 | module.exports = pool;
62 |
--------------------------------------------------------------------------------
/frontend/src/component/SignupForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | function SignupForm({ setUserData, userData, setSignUpClicked }) {
4 | const [name, setName] = useState('');
5 | const [email, setEmail] = useState('');
6 | const [password, setPassword] = useState('');
7 | const [location, setLocation] = useState('');
8 |
9 | const onInput = (e, setState) => {
10 | setState(e.target.value);
11 | };
12 |
13 | const handleSubmit = (e) => {
14 | e.preventDefault();
15 | const newUserData = {
16 | name,
17 | email,
18 | password,
19 | location,
20 | preferences: {
21 | type: '',
22 | age: '',
23 | gender: '',
24 | size: ''
25 | }
26 | };
27 | setUserData(newUserData);
28 | setSignUpClicked(true);
29 | };
30 |
31 | // Set up user info
32 | return (
33 |
55 | );
56 | }
57 |
58 | export default SignupForm;
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pet-friendr",
3 | "version": "1.0.0",
4 | "description": "An app to find and connect with our furry friends!",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "webpack serve --mode development --open",
8 | "build": "webpack --mode production",
9 | "start": "node backend/server.js",
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "lint": "eslint .",
12 | "lint:fix": "eslint --fix",
13 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/Star-Nosed-Molers/Pet-Friendr.git"
18 | },
19 | "author": "",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/Star-Nosed-Molers/Pet-Friendr/issues"
23 | },
24 | "husky": {
25 | "hooks": {
26 | "pre-commit": "pretty-quick --staged"
27 | }
28 | },
29 | "homepage": "https://github.com/Star-Nosed-Molers/Pet-Friendr#readme",
30 | "dependencies": {
31 | "axios": "^1.3.4",
32 | "browserify": "^17.0.0",
33 | "dotenv": "^16.0.3",
34 | "express": "^4.18.2",
35 | "fs": "^0.0.1-security",
36 | "jsonwebtoken": "^9.0.0",
37 | "os": "^0.1.2",
38 | "path-browserify": "^1.0.1",
39 | "pg": "^8.9.0",
40 | "pg-pool": "^3.5.2",
41 | "react": "^18.2.0",
42 | "react-dom": "^18.2.0",
43 | "react-router-dom": "^6.8.2",
44 | "react-tinder-card": "^1.6.2",
45 | "sass": "^1.58.3"
46 | },
47 | "devDependencies": {
48 | "@babel/cli": "^7.21.0",
49 | "@babel/core": "^7.21.0",
50 | "@babel/preset-env": "^7.20.2",
51 | "@babel/preset-react": "^7.18.6",
52 | "@react-spring/web": "^9.7.1",
53 | "babel-loader": "^9.1.2",
54 | "concurrently": "^7.6.0",
55 | "css-loader": "^6.7.3",
56 | "eslint": "^8.35.0",
57 | "eslint-config-airbnb": "^19.0.4",
58 | "eslint-config-prettier": "^8.6.0",
59 | "eslint-plugin-import": "^2.27.5",
60 | "eslint-plugin-jsx-a11y": "^6.7.1",
61 | "eslint-plugin-prettier": "^4.2.1",
62 | "eslint-plugin-react": "^7.32.2",
63 | "eslint-plugin-react-hooks": "^4.6.0",
64 | "html-webpack-plugin": "^5.5.0",
65 | "husky": "^8.0.3",
66 | "nodemon": "^2.0.21",
67 | "prettier": "^2.8.4",
68 | "pretty-quick": "^3.1.3",
69 | "sass-loader": "^13.2.0",
70 | "style-loader": "^3.3.1",
71 | "url-loader": "^4.1.1",
72 | "webpack": "^5.75.0",
73 | "webpack-cli": "^5.0.1",
74 | "webpack-dev-server": "^4.11.1"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/frontend/src/component/PreferenceForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function PreferenceForm({ userData, setUserData, preferenceClicked, setPreferenceClicked }) {
4 | const onHandleClick = (e) => {
5 | e.preventDefault();
6 | const formData = new FormData(e.currentTarget);
7 | const userPreferences = {
8 | ...userData,
9 | preferences: {
10 | type: formData.get('species'),
11 | age: formData.get('age'),
12 | gender: formData.get('gender'),
13 | size: formData.get('size')
14 | }
15 | };
16 | setUserData(userPreferences);
17 | setPreferenceClicked(true);
18 | };
19 |
20 | console.log(userData);
21 |
22 | return (
23 |
24 |
Preferences
25 |
67 |
68 | );
69 | }
70 |
71 | export default PreferenceForm;
72 |
--------------------------------------------------------------------------------
/frontend/src/container/MainContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import TinderCard from 'react-tinder-card';
3 |
4 | const DogSwiper = ({ onSwiperPage, setOnSwiperPage, savedCards, setSavedCards }) => {
5 | const [dogData, setDogData] = useState([]);
6 | const [cards, setCards] = useState([]);
7 | const [lastDirection, setLastDirection] = useState();
8 | const [isLoading, setIsLoading] = useState(true);
9 | // const [savedCards, setSavedCards] = useState([]);
10 |
11 | // utility function for swiping
12 | const swiped = (direction, nameToDelete, dogImage, savedCards, setSavedCards) => {
13 | console.log('removing: ' + nameToDelete);
14 | // console.log('dogImage', dogImage);
15 | // create new obj with name and url
16 | const dogObj = {
17 | name: nameToDelete,
18 | url: dogImage
19 | };
20 | // pass that into new array with the existing array elements using ...
21 | // setState, passing in new array
22 | setLastDirection(direction);
23 | if (direction === 'right') {
24 | setSavedCards(savedCards => [...savedCards, dogObj]);
25 | }
26 | };
27 |
28 | // utility function for out of frame
29 | const outOfFrame = (name) => {
30 | console.log(name + ' left the screen!');
31 | };
32 |
33 | // change OnSwiperPage to false when main conainter button is clicked
34 | const onHandleClick = (e) => {
35 | e.preventDefault();
36 | setOnSwiperPage(false);
37 | }
38 |
39 | // processes dog data and creates cards
40 | const dogCards = () => {
41 |
42 | // console.log(dogData[0])
43 | const processedCards =
44 | dogData &&
45 | dogData.map((dog) => {
46 | const url = 'https://dl5zpyw5k3jeb.cloudfront.net/photos/pets/60481973/1/?bust=1678048544&width=450';
47 | const imgUrl = dog.primary_photo_cropped !== null ? dog.primary_photo_cropped.medium : url;
48 | const dogName = dog.name !== 'Dog' ? dog.name : 'I still need a name! :-(';
49 | return (
50 | swiped(dir, {dogName} ,{imgUrl}, savedCards, setSavedCards)}
54 | onCardLeftScreen={() => outOfFrame({dogName})}>
55 |
62 |
{dogName}
63 |
64 |
65 | );
66 | });
67 | setCards(processedCards);
68 | setIsLoading(false);
69 | };
70 |
71 | // fetches dog data from backend
72 | useEffect(() => {
73 | const fetchDogs = async () => {
74 | try {
75 | setIsLoading(true);
76 | const response = await fetch('/api/preferences', {
77 | method: 'POST',
78 | headers: {
79 | 'Content-Type': 'application/json'
80 | },
81 | body: JSON.stringify({ id: 22 }) // THIS IS SAMPLE ID, CHANGE ME
82 | });
83 | const data = await response.json();
84 | // console.log('RESPONSE DATA', await data);
85 | setDogData(data);
86 | } catch (error) {
87 | console.log('Error in MainContainer useEffect to fetch dogs', error);
88 | }
89 | };
90 | fetchDogs();
91 | }, []);
92 |
93 | // runs dogCards when dogData is updated
94 | useEffect(() => {
95 | dogCards();
96 | }, [dogData]);
97 |
98 |
99 | // if lastDirection is right, we will invoke setSavedCards by passing in the dog id to the existing saveCards
100 | // useEffect(() => {
101 | // if (lastDirection === 'right') {
102 |
103 | // }
104 | // });
105 |
106 |
107 | return (
108 |
109 |
Pet swiper here
110 | {dogData.length === 0 ? (
111 |
117 | ) : (
118 |
{cards}
119 | )}
120 | {lastDirection ?
You swiped {lastDirection} :
}
121 |
View Favorite Pets
122 |
123 | );
124 | };
125 |
126 | export default DogSwiper;
--------------------------------------------------------------------------------
/backend/controllers/PetController.js:
--------------------------------------------------------------------------------
1 | // need to import schema
2 | const petController = {};
3 |
4 | // TODO (Chris/Alex): Modify to use environment variables to store idKey and secret
5 | //alex's key/secret
6 | // const idKey = 'Td80x9tGqOQnNnlwX3oKu9hjvYBqbYZnuzGwijbPd4iEmsb7EH';
7 | // const secret = 'SdDvmwwjpY4zjKYCpmGtwqGznXQu5JxY4ro8jOfK';
8 |
9 | //tricia's keys
10 | // const idKey = 'ETP9nJrnccMT9nAYIQpa8zxOKgrIJUyIMFsJNjXWIfg9jpAwd9'
11 | // const secret = '0a8locc2QKcZd8gj9gfCfHz4ZzZoGIsJlyqZd45p'
12 |
13 | // chris' key/secret
14 | // const idKey = 'NkAKvJ91IkpakFYHnV8HYcgyqeOdFcYQvnWOlKrhptNrn4kFz8';
15 | // const secret = 'pmNPLwJqpIR6rxDpl49qY1VjVg5zvRug8Kza60WW';
16 |
17 | // jamie's key/secret
18 | // const idKey = 'EGPxXXOai1WgDyLsFOWSQXpVHrX7JQ3SDxvBkAwdfl1NzTC60l'
19 | // const secret = 'M1rtNVQmo9G5SrmqR73hxXXbclcpjgT8odPyskCB'
20 |
21 | //jason's key/secret
22 | const idKey = 'Fl7qnYb3k3vMNCW08WCqEDKRo4LKG8mfJh3SnjVg8tLoL2gVEE'
23 | const secret = 'hrVXJ4xUJobzvFnC9VqmbQaZksc9HYPCHv9zWWTz'
24 |
25 | // gets auth token from petFinder API
26 | petController.getAuthToken = async (req, res, next) => {
27 | try {
28 | const params = new URLSearchParams();
29 |
30 | params.append('grant_type', 'client_credentials');
31 | params.append('client_id', idKey);
32 | params.append('client_secret', secret);
33 |
34 | const petFinderRes = await fetch('https://api.petfinder.com/v2/oauth2/token', {
35 | method: 'POST',
36 | body: params
37 | });
38 | res.locals.body = req.body;
39 | res.locals.authToken = await petFinderRes.json();
40 | return next();
41 | } catch (e) {
42 | return next({
43 | log: 'Error in petController.getAuthToken',
44 | status: 400,
45 | message: { err: `in petController.getAuthToken: ${e}` }
46 | });
47 | }
48 | };
49 |
50 | // gets pet data from PetFinder API and stores in res.locals.pets
51 | petController.getAllPets = async (req, res, next) => {
52 | try {
53 | const url = `https://api.petfinder.com/v2/animals&limit=100`;
54 | const accessTokenObject = res.locals.authToken;
55 | const petResults = await fetch(url, {
56 | headers: {
57 | Authorization: `Bearer ${accessTokenObject.access_token}`
58 | }
59 | });
60 | const json = await petResults.json();
61 | res.locals.pets = json.animals;
62 | return next();
63 | } catch (e) {
64 | return next({
65 | log: 'Error in petController.getPets',
66 | status: 400,
67 | message: { err: `in petController.getAllPets: ${e}` }
68 | });
69 | }
70 | };
71 |
72 | // gets dog data from PetFinder API and stores in res.locals.dogs
73 | petController.getAllDogs = async (req, res, next) => {
74 | try {
75 | const url = `https://api.petfinder.com/v2/animals?type=Dog&limit=100`;
76 | const accessTokenObject = res.locals.authToken;
77 | const petResults = await fetch(url, {
78 | headers: {
79 | Authorization: `Bearer ${accessTokenObject.access_token}`
80 | }
81 | });
82 | const json = await petResults.json();
83 | res.locals.dogs = json.animals;
84 | return next();
85 | } catch (e) {
86 | return next({
87 | log: 'Error in petController.getPets',
88 | status: 400,
89 | message: { err: `in petController.getPets: ${e}` }
90 | });
91 | }
92 | };
93 |
94 | // gets pet data from PetFinder API based on user preferences and stores in res.locals.preferences (based on species and zip code proximity)
95 | petController.getPreferences = async (req, res, next) => {
96 | /*
97 | Incoming res.locals.userData from dbController.getUserData looks like this:
98 | {
99 | location: '90032',
100 | preference: {
101 | species: 'Dog',
102 | age: 'Young',
103 | gender: 'Male',
104 | size: 'Medium'
105 | }
106 | }
107 | */
108 | try {
109 | const { location, preference } = res.locals.userData;
110 | const type = preference.species;
111 | // type filter by cat/dog and location filters by zip code
112 | const url = `https://api.petfinder.com/v2/animals?type=${type}&location=${location}&limit=100`;
113 | const accessTokenObject = res.locals.authToken;
114 | const petResults = await fetch(url, {
115 | headers: {
116 | Authorization: `Bearer ${accessTokenObject.access_token}`
117 | }
118 | });
119 | const json = await petResults.json();
120 | res.locals.preferences = json.animals;
121 | return next();
122 | } catch (e) {
123 | return next({
124 | log: 'Error in petController.getPreferences',
125 | status: 400,
126 | message: { err: `in petController.getPreferences: ${e}` }
127 | });
128 | }
129 | };
130 |
131 | module.exports = petController;
132 |
--------------------------------------------------------------------------------
/frontend/src/styles.scss:
--------------------------------------------------------------------------------
1 | body {
2 | display:flex;
3 | justify-content: center;
4 | // background: #78D1D1;
5 | background: linear-gradient(90deg, rgba(34,193,195,1) 0%, rgba(139,201,123,1) 80%, rgba(253,210,45,1) 100%);
6 | text-align: center;
7 | font-family: 'Roboto', 'Open Sans', sans-serif;
8 | }
9 | // #root {
10 | // text-align: center;
11 | // display: flex;
12 | // justify-content: center;
13 | // width: 100vw;
14 | // min-height: 100vh;
15 | // overflow: hidden;
16 | // background: linear-gradient(#fff, #999);
17 | // background-color: linear-gradient(#E66465, #9198E5);
18 | // }
19 | * {
20 | user-select: none;
21 | }
22 | #root>div {
23 | display: flex;
24 | flex-direction: column;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 | .app {
29 | overflow: hidden;
30 | }
31 | .app>div {
32 | display: flex;
33 | flex-direction: column;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 | .row {
38 | flex-direction: row !important;
39 | }
40 | .row>* {
41 | margin: 5px;
42 | }
43 | h1 {
44 | // font-family: 'Damion', cursive;
45 | color: hsl(0, 0%, 0%);
46 | text-shadow: 0px 0px 60px 0px rgba(0,0,0,0.30);
47 | }
48 | h2 {
49 | color: #fff;
50 | }
51 | .swipe {
52 | position: absolute;
53 | }
54 | .pets {
55 | display: flex;
56 | justify-content: center;
57 | width: auto;
58 | }
59 | .signupForm, .preferenceForm {
60 | background: #FF90E8;
61 | }
62 | .signupForm, .preferenceForm, .cardWrapper {
63 | display: flex;
64 | flex-direction: column;
65 | justify-content: flex-start;
66 | text-align: left;
67 | padding: 2em 2.5em 4em 2em;
68 | margin-top: 2em;
69 | border-radius: 2em;
70 | background: rgb(233, 233, 233);
71 |
72 | div {
73 | margin-top: 0.5em;
74 | }
75 | input, select, text {
76 | width: 100%;
77 | padding: 6px 2px;
78 | margin: 5px 3px;
79 | color: #959B93;
80 | border: 2px solid rgb(120, 209, 209);
81 | border-radius: 10px;
82 | letter-spacing: 1px;
83 | height: 2rem;
84 | background: #FFF0F0;
85 | box-shadow: 4px 6px 0px #000000;
86 | }
87 | label {
88 | color:hsl(167, 14%, 12%);
89 | font-size: 0.5rem;
90 | font-family: 'Open Sans', sans-serif;
91 | }
92 | button {
93 | width: 100%;
94 | color: rgb(41, 111, 111);
95 | border: 2px solid rgb(93, 168, 168);
96 | font-size: 1em;
97 | font-weight: bold;
98 | border-radius: 10px;
99 | padding: 12px;
100 | letter-spacing: 1px;
101 | cursor: pointer;
102 | left: 22px;
103 | top: 209px;
104 | box-shadow: 4px 6px 0px #000000;
105 | z-index: 1;
106 | transition: all 0.5s;
107 | }
108 | button:hover {
109 | color: white;
110 | background: rgb(34,193,195);
111 | // background: linear-gradient(to right, rgba(34,193,195,1) 0%, rgba(253,187,45,1) 100%);
112 | }
113 | }
114 | .cardContainer {
115 | width: 90vw;
116 | max-width: 260px;
117 | height: 300px;
118 | background: rgb(233, 233, 233);
119 | }
120 | .card {
121 | position: relative;
122 | background-color: rgb(233, 233, 233);
123 | width: 80vw;
124 | max-width: 260px;
125 | height: 300px;
126 | // box-shadow: 0px 0px 60px 0px rgba(0,0,0,0.30);
127 | border-radius: 20px;
128 | background-size: cover;
129 | background-position: center;
130 | }
131 | .cardContent {
132 | width: 100%;
133 | height: 100%;
134 | }
135 | // .swipe:last-of-type {
136 | // }
137 | .card h3 {
138 | position: absolute;
139 | bottom: 0;
140 | margin: 10px;
141 | color: white;
142 | text-shadow: rgb(66, 65, 62) 1px 0 10px;
143 | }
144 | .infoText {
145 | width: 100%;
146 | justify-content: center;
147 | display: flex;
148 | color: #61A57B;
149 | animation-name: popup;
150 | animation-duration: 800ms;
151 | }
152 | .buttons {
153 | margin: 20px;
154 | display: flex;
155 | flex-wrap: wrap;
156 | }
157 | @media (max-width: 625px) {
158 | .buttons {
159 | flex-direction: column;
160 | }
161 | }
162 | .buttons button {
163 | flex-shrink: 0;
164 | padding: 10px;
165 | border-radius: 5px;
166 | border: none;
167 | color: #fff;
168 | font-size: 18px;
169 | background-color: #9198E5;
170 | transition: 200ms;
171 | margin: 10px;
172 | font-weight: bolder;
173 | width: 160px;
174 | box-shadow: 0px 0px 30px 0px rgba(0,0,0,0.10);
175 | }
176 | .buttons button:hover {
177 | transform: scale(1.05);
178 | }
179 | @keyframes popup {
180 | 0% { transform: scale(1,1) }
181 | 10% { transform: scale(1.1,1.1) }
182 | 30% { transform: scale(.9,.9) }
183 | 50% { transform: scale(1,1) }
184 | 57% { transform: scale(1,1) }
185 | 64% { transform: scale(1,1) }
186 | 100% { transform: scale(1,1) }
187 | }
188 |
189 | // .homeToMain-Btn,
190 | // .viewPets-Btn {
191 | // background-color: #222;
192 | // border-radius: 4px;
193 | // border-style: none;
194 | // box-sizing: border-box;
195 | // color: #fff;
196 | // cursor: pointer;
197 | // display: inline-block;
198 | // font-family: "Farfetch Basis","Helvetica Neue",Arial,sans-serif;
199 | // font-size: 16px;
200 | // font-weight: 700;
201 | // line-height: 1.5;
202 | // margin: 0;
203 | // max-width: none;
204 | // min-height: 44px;
205 | // min-width: 10px;
206 | // outline: none;
207 | // overflow: hidden;
208 | // padding: 9px 20px 8px;
209 | // position: relative;
210 | // text-align: center;
211 | // text-transform: none;
212 | // user-select: none;
213 | // -webkit-user-select: none;
214 | // touch-action: manipulation;
215 | // width: 100%;
216 | // }
217 |
218 | // .homeToMain-Btn:hover,
219 | // .viewPets-Btn:hover,
220 | // .homeToMain-Btn:focus
221 | // .viewPets-Btn:focus {
222 | // opacity: .75;
223 | // }
224 |
225 | .favoritePets {
226 | display: flex;
227 | flex-wrap: wrap;
228 | justify-content: center;
229 | align-items: center;
230 | }
231 |
232 | .favoritePets img {
233 | width: 200px;
234 | height: 200px;
235 | object-fit: cover;
236 | margin: 10px;
237 | border-radius: 50%;
238 | }
239 |
240 | .favoritePets img:hover {
241 | opacity: 0.8;
242 | cursor: pointer;
243 | }
244 |
245 | .favoritePets h3 {
246 | text-align: center;
247 | }
--------------------------------------------------------------------------------