First commit
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 John Pham
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
70
README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `yarn start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `yarn test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `yarn build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `yarn eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `yarn build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
30773
package-lock.json
generated
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "cute-pets-website",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^14.4.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.14.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"msw": "^0.28.2",
|
||||||
|
"prettier": "^2.2.1"
|
||||||
|
},
|
||||||
|
"msw": {
|
||||||
|
"workerDirectory": "public"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
203
public/index.css
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
.nav-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
padding: 12px 180px;
|
||||||
|
background: #56009b;
|
||||||
|
color: white;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 6px 24px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
background: #320059;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-type-label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
grid-gap: 40px 8px;
|
||||||
|
justify-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-image-container {
|
||||||
|
height: 200px;
|
||||||
|
width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-image {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet {
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h3 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #56009b;
|
||||||
|
margin: 40px 0 35px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page img {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-detail {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-detail h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-detail h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-detail p {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 370px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container,
|
||||||
|
h2 {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo {
|
||||||
|
padding: 18px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet h3 {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid #a9a9a9;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.1s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:hover,
|
||||||
|
.search-button:focus {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: #56009b;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
49
public/index.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<link rel="stylesheet" href="/index.css" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
BIN
public/missing-animal.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
322
public/mockServiceWorker.js
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
/**
|
||||||
|
* Mock Service Worker.
|
||||||
|
* @see https://github.com/mswjs/msw
|
||||||
|
* - Please do NOT modify this file.
|
||||||
|
* - Please do NOT serve this file on production.
|
||||||
|
*/
|
||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
|
||||||
|
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
|
||||||
|
const bypassHeaderName = 'x-msw-bypass'
|
||||||
|
const activeClientIds = new Set()
|
||||||
|
|
||||||
|
self.addEventListener('install', function () {
|
||||||
|
return self.skipWaiting()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('activate', async function (event) {
|
||||||
|
return self.clients.claim()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('message', async function (event) {
|
||||||
|
const clientId = event.source.id
|
||||||
|
|
||||||
|
if (!clientId || !self.clients) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await self.clients.get(clientId)
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allClients = await self.clients.matchAll()
|
||||||
|
|
||||||
|
switch (event.data) {
|
||||||
|
case 'KEEPALIVE_REQUEST': {
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'KEEPALIVE_RESPONSE',
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'INTEGRITY_CHECK_REQUEST': {
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||||
|
payload: INTEGRITY_CHECKSUM,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_ACTIVATE': {
|
||||||
|
activeClientIds.add(clientId)
|
||||||
|
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'MOCKING_ENABLED',
|
||||||
|
payload: true,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_DEACTIVATE': {
|
||||||
|
activeClientIds.delete(clientId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CLIENT_CLOSED': {
|
||||||
|
activeClientIds.delete(clientId)
|
||||||
|
|
||||||
|
const remainingClients = allClients.filter((client) => {
|
||||||
|
return client.id !== clientId
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unregister itself when there are no more clients
|
||||||
|
if (remainingClients.length === 0) {
|
||||||
|
self.registration.unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resolve the "master" client for the given event.
|
||||||
|
// Client that issues a request doesn't necessarily equal the client
|
||||||
|
// that registered the worker. It's with the latter the worker should
|
||||||
|
// communicate with during the response resolving phase.
|
||||||
|
async function resolveMasterClient(event) {
|
||||||
|
const client = await self.clients.get(event.clientId)
|
||||||
|
|
||||||
|
if (client.frameType === 'top-level') {
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
const allClients = await self.clients.matchAll()
|
||||||
|
|
||||||
|
return allClients
|
||||||
|
.filter((client) => {
|
||||||
|
// Get only those clients that are currently visible.
|
||||||
|
return client.visibilityState === 'visible'
|
||||||
|
})
|
||||||
|
.find((client) => {
|
||||||
|
// Find the client ID that's recorded in the
|
||||||
|
// set of clients that have registered the worker.
|
||||||
|
return activeClientIds.has(client.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequest(event, requestId) {
|
||||||
|
const client = await resolveMasterClient(event)
|
||||||
|
const response = await getResponse(event, client, requestId)
|
||||||
|
|
||||||
|
// Send back the response clone for the "response:*" life-cycle events.
|
||||||
|
// Ensure MSW is active and ready to handle the message, otherwise
|
||||||
|
// this message will pend indefinitely.
|
||||||
|
if (client && activeClientIds.has(client.id)) {
|
||||||
|
;(async function () {
|
||||||
|
const clonedResponse = response.clone()
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'RESPONSE',
|
||||||
|
payload: {
|
||||||
|
requestId,
|
||||||
|
type: clonedResponse.type,
|
||||||
|
ok: clonedResponse.ok,
|
||||||
|
status: clonedResponse.status,
|
||||||
|
statusText: clonedResponse.statusText,
|
||||||
|
body:
|
||||||
|
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||||
|
headers: serializeHeaders(clonedResponse.headers),
|
||||||
|
redirected: clonedResponse.redirected,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResponse(event, client, requestId) {
|
||||||
|
const { request } = event
|
||||||
|
const requestClone = request.clone()
|
||||||
|
const getOriginalResponse = () => fetch(requestClone)
|
||||||
|
|
||||||
|
// Bypass mocking when the request client is not active.
|
||||||
|
if (!client) {
|
||||||
|
return getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass initial page load requests (i.e. static assets).
|
||||||
|
// The absence of the immediate/parent client in the map of the active clients
|
||||||
|
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||||
|
// and is not ready to handle requests.
|
||||||
|
if (!activeClientIds.has(client.id)) {
|
||||||
|
return await getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass requests with the explicit bypass header
|
||||||
|
if (requestClone.headers.get(bypassHeaderName) === 'true') {
|
||||||
|
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
|
||||||
|
|
||||||
|
// Remove the bypass header to comply with the CORS preflight check.
|
||||||
|
delete cleanRequestHeaders[bypassHeaderName]
|
||||||
|
|
||||||
|
const originalRequest = new Request(requestClone, {
|
||||||
|
headers: new Headers(cleanRequestHeaders),
|
||||||
|
})
|
||||||
|
|
||||||
|
return fetch(originalRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request to the client-side MSW.
|
||||||
|
const reqHeaders = serializeHeaders(request.headers)
|
||||||
|
const body = await request.text()
|
||||||
|
|
||||||
|
const clientMessage = await sendToClient(client, {
|
||||||
|
type: 'REQUEST',
|
||||||
|
payload: {
|
||||||
|
id: requestId,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
|
headers: reqHeaders,
|
||||||
|
cache: request.cache,
|
||||||
|
mode: request.mode,
|
||||||
|
credentials: request.credentials,
|
||||||
|
destination: request.destination,
|
||||||
|
integrity: request.integrity,
|
||||||
|
redirect: request.redirect,
|
||||||
|
referrer: request.referrer,
|
||||||
|
referrerPolicy: request.referrerPolicy,
|
||||||
|
body,
|
||||||
|
bodyUsed: request.bodyUsed,
|
||||||
|
keepalive: request.keepalive,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (clientMessage.type) {
|
||||||
|
case 'MOCK_SUCCESS': {
|
||||||
|
return delayPromise(
|
||||||
|
() => respondWithMock(clientMessage),
|
||||||
|
clientMessage.payload.delay,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_NOT_FOUND': {
|
||||||
|
return getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'NETWORK_ERROR': {
|
||||||
|
const { name, message } = clientMessage.payload
|
||||||
|
const networkError = new Error(message)
|
||||||
|
networkError.name = name
|
||||||
|
|
||||||
|
// Rejecting a request Promise emulates a network error.
|
||||||
|
throw networkError
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'INTERNAL_ERROR': {
|
||||||
|
const parsedBody = JSON.parse(clientMessage.payload.body)
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`\
|
||||||
|
[MSW] Request handler function for "%s %s" has thrown the following exception:
|
||||||
|
|
||||||
|
${parsedBody.errorType}: ${parsedBody.message}
|
||||||
|
(see more detailed error stack trace in the mocked response body)
|
||||||
|
|
||||||
|
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
|
||||||
|
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
|
||||||
|
`,
|
||||||
|
request.method,
|
||||||
|
request.url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return respondWithMock(clientMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('fetch', function (event) {
|
||||||
|
const { request } = event
|
||||||
|
|
||||||
|
// Bypass navigation requests.
|
||||||
|
if (request.mode === 'navigate') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opening the DevTools triggers the "only-if-cached" request
|
||||||
|
// that cannot be handled by the worker. Bypass such requests.
|
||||||
|
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass all requests when there are no active clients.
|
||||||
|
// Prevents the self-unregistered worked from handling requests
|
||||||
|
// after it's been deleted (still remains active until the next reload).
|
||||||
|
if (activeClientIds.size === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = uuidv4()
|
||||||
|
|
||||||
|
return event.respondWith(
|
||||||
|
handleRequest(event, requestId).catch((error) => {
|
||||||
|
console.error(
|
||||||
|
'[MSW] Failed to mock a "%s" request to "%s": %s',
|
||||||
|
request.method,
|
||||||
|
request.url,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function serializeHeaders(headers) {
|
||||||
|
const reqHeaders = {}
|
||||||
|
headers.forEach((value, name) => {
|
||||||
|
reqHeaders[name] = reqHeaders[name]
|
||||||
|
? [].concat(reqHeaders[name]).concat(value)
|
||||||
|
: value
|
||||||
|
})
|
||||||
|
return reqHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToClient(client, message) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel()
|
||||||
|
|
||||||
|
channel.port1.onmessage = (event) => {
|
||||||
|
if (event.data && event.data.error) {
|
||||||
|
return reject(event.data.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.postMessage(JSON.stringify(message), [channel.port2])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function delayPromise(cb, duration) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(cb()), duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondWithMock(clientMessage) {
|
||||||
|
return new Response(clientMessage.payload.body, {
|
||||||
|
...clientMessage.payload,
|
||||||
|
headers: clientMessage.payload.headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function uuidv4() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = (Math.random() * 16) | 0
|
||||||
|
const v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
BIN
public/pets-hero.png
Normal file
After Width: | Height: | Size: 676 KiB |
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
27
src/App.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import HomePage from "./pages/home";
|
||||||
|
import SearchPage from "./pages/search";
|
||||||
|
import PetDetailsPage from "./pages/detail";
|
||||||
|
import PetDetailsNotFound from "./pages/petDetailsNotFound";
|
||||||
|
import Root from "./components/root";
|
||||||
|
import {
|
||||||
|
Route,
|
||||||
|
RouterProvider,
|
||||||
|
createBrowserRouter,
|
||||||
|
createRoutesFromElements,
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
|
// Add react-router-dom imports
|
||||||
|
|
||||||
|
// create router with JSX Route elements
|
||||||
|
const appRouter = createBrowserRouter(
|
||||||
|
createRoutesFromElements(<Route path="/" element={<Root />}></Route>)
|
||||||
|
);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
// replace below with a Router Provider
|
||||||
|
<RouterProvider router={appRouter} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
8
src/App.test.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
34
src/api/petfinder/index.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export const getPets = async (type = '', query = '') => {
|
||||||
|
const searchParams = new URLSearchParams({ type, query });
|
||||||
|
const requestUrl = `/animals?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPetDetails = async (id) => {
|
||||||
|
const requestUrl = `/animals/${id}`;
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPetTypes = async () => {
|
||||||
|
const requestUrl = `/types`;
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
};
|
3
src/assets/logo.svg
Normal file
After Width: | Height: | Size: 6.1 KiB |
39
src/components/hero/index.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Hero = ({ image, displayText }) => {
|
||||||
|
const type = ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="hero-container"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(black, black), url("${
|
||||||
|
image || 'pets-hero.png'
|
||||||
|
}")
|
||||||
|
`,
|
||||||
|
backgroundBlendMode: 'saturation',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundColor: '#0000008f'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>{displayText || getHeroTitle(type)}</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hero;
|
||||||
|
|
||||||
|
const getHeroTitle = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'dog':
|
||||||
|
return 'Dogs';
|
||||||
|
case 'cat':
|
||||||
|
return 'Cats';
|
||||||
|
case 'rabbit':
|
||||||
|
return 'Rabbits';
|
||||||
|
case 'bird':
|
||||||
|
return 'Birds';
|
||||||
|
default:
|
||||||
|
return 'Find your perfect pet';
|
||||||
|
}
|
||||||
|
};
|
52
src/components/navigation/index.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getPetTypes } from '../../api/petfinder';
|
||||||
|
import Logo from '../../assets/logo.svg';
|
||||||
|
import Search from '../search';
|
||||||
|
|
||||||
|
// Import NavLink
|
||||||
|
|
||||||
|
const Navigation = () => {
|
||||||
|
const [petTypes, setPetTypes] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getPetTypesData() {
|
||||||
|
const { types } = await getPetTypes();
|
||||||
|
setPetTypes(types);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPetTypesData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav>
|
||||||
|
<div className="nav-logo">
|
||||||
|
<img src={Logo} alt="Petlover" />
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
<ul className="nav-links">
|
||||||
|
<li key={'all'}>
|
||||||
|
{/* These links should be NavLink component and add a special active class name if its an active link */}
|
||||||
|
<a href="/"
|
||||||
|
className='nav-link'
|
||||||
|
>
|
||||||
|
All Pets
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{petTypes
|
||||||
|
? petTypes.map((type) => (
|
||||||
|
<li key={type.name}>
|
||||||
|
{/* These links should be NavLink component and add a special active class name if its an active link */}
|
||||||
|
<a href={`/${type._links.self.href.split('/').pop()}`}
|
||||||
|
key={type.name}
|
||||||
|
className='nav-link' >
|
||||||
|
{type.name}s
|
||||||
|
</a>{' '}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
: 'Loading...'}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navigation;
|
31
src/components/pet/index.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Pet = ({ animal }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={animal.id}
|
||||||
|
href={`/${animal.type.toLowerCase()}/${animal.id}`}
|
||||||
|
className="pet"
|
||||||
|
>
|
||||||
|
<article>
|
||||||
|
<div className="pet-image-container">
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
className="pet-image"
|
||||||
|
src={
|
||||||
|
animal.photos[0]?.medium || 'https://i.imgur.com/aEcJUFK.png'
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<h3>{animal.name}</h3>
|
||||||
|
<p>Breed: {animal.breeds.primary}</p>
|
||||||
|
<p>Color: {animal.colors.primary}</p>
|
||||||
|
<p>Gender: {animal.gender}</p>
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pet;
|
14
src/components/root/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Navigation from '../navigation';
|
||||||
|
// import Outlet
|
||||||
|
|
||||||
|
const Root = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navigation/>
|
||||||
|
{/* Add an Outlet*/}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Root;
|
35
src/components/search/index.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
// Import createSearchParams
|
||||||
|
// Import useNavigate
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
|
||||||
|
// get navigate function
|
||||||
|
const navigate = "REPLACE ME";
|
||||||
|
|
||||||
|
const searchInputRef = useRef();
|
||||||
|
|
||||||
|
const onSearchHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const searchQuery = {
|
||||||
|
name: searchInputRef.current.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// use createSearchParams
|
||||||
|
const query = "REPLACE ME";
|
||||||
|
|
||||||
|
// imperatively redirect with useNavigate() returned function
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSearchHandler} className="search-form">
|
||||||
|
<input type="text" className="search" ref={searchInputRef} />
|
||||||
|
<button type="submit" className="search-button">
|
||||||
|
🔎
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
19
src/index.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
const { worker } = require('./mocks/browser');
|
||||||
|
worker.start();
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
const root = ReactDOM.createRoot(container);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
1
src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
4
src/mocks/browser.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { setupWorker } from 'msw';
|
||||||
|
import { handlers } from './handlers';
|
||||||
|
|
||||||
|
export const worker = setupWorker(...handlers);
|
3240
src/mocks/data/animals.json
Normal file
1379
src/mocks/data/details.json
Normal file
120
src/mocks/data/types.json
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Dog",
|
||||||
|
"coats": ["Hairless", "Short", "Medium", "Long", "Wire", "Curly"],
|
||||||
|
"colors": [
|
||||||
|
"Apricot / Beige",
|
||||||
|
"Bicolor",
|
||||||
|
"Black",
|
||||||
|
"Brindle",
|
||||||
|
"Brown / Chocolate",
|
||||||
|
"Golden",
|
||||||
|
"Gray / Blue / Silver",
|
||||||
|
"Harlequin",
|
||||||
|
"Merle (Blue)",
|
||||||
|
"Merle (Red)",
|
||||||
|
"Red / Chestnut / Orange",
|
||||||
|
"Sable",
|
||||||
|
"Tricolor (Brown, Black, \u0026 White)",
|
||||||
|
"White / Cream",
|
||||||
|
"Yellow / Tan / Blond / Fawn"
|
||||||
|
],
|
||||||
|
"genders": ["Male", "Female"],
|
||||||
|
"_links": {
|
||||||
|
"self": { "href": "/v2/types/dog" },
|
||||||
|
"breeds": { "href": "/v2/types/dog/breeds" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cat",
|
||||||
|
"coats": ["Hairless", "Short", "Medium", "Long"],
|
||||||
|
"colors": [
|
||||||
|
"Black",
|
||||||
|
"Black \u0026 White / Tuxedo",
|
||||||
|
"Blue Cream",
|
||||||
|
"Blue Point",
|
||||||
|
"Brown / Chocolate",
|
||||||
|
"Buff \u0026 White",
|
||||||
|
"Buff / Tan / Fawn",
|
||||||
|
"Calico",
|
||||||
|
"Chocolate Point",
|
||||||
|
"Cream / Ivory",
|
||||||
|
"Cream Point",
|
||||||
|
"Dilute Calico",
|
||||||
|
"Dilute Tortoiseshell",
|
||||||
|
"Flame Point",
|
||||||
|
"Gray \u0026 White",
|
||||||
|
"Gray / Blue / Silver",
|
||||||
|
"Lilac Point",
|
||||||
|
"Orange \u0026 White",
|
||||||
|
"Orange / Red",
|
||||||
|
"Seal Point",
|
||||||
|
"Smoke",
|
||||||
|
"Tabby (Brown / Chocolate)",
|
||||||
|
"Tabby (Buff / Tan / Fawn)",
|
||||||
|
"Tabby (Gray / Blue / Silver)",
|
||||||
|
"Tabby (Leopard / Spotted)",
|
||||||
|
"Tabby (Orange / Red)",
|
||||||
|
"Tabby (Tiger Striped)",
|
||||||
|
"Torbie",
|
||||||
|
"Tortoiseshell",
|
||||||
|
"White"
|
||||||
|
],
|
||||||
|
"genders": ["Male", "Female"],
|
||||||
|
"_links": {
|
||||||
|
"self": { "href": "/v2/types/cat" },
|
||||||
|
"breeds": { "href": "/v2/types/cat/breeds" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rabbit",
|
||||||
|
"coats": ["Short", "Long"],
|
||||||
|
"colors": [
|
||||||
|
"Agouti",
|
||||||
|
"Black",
|
||||||
|
"Blue / Gray",
|
||||||
|
"Brown / Chocolate",
|
||||||
|
"Cream",
|
||||||
|
"Lilac",
|
||||||
|
"Orange / Red",
|
||||||
|
"Sable",
|
||||||
|
"Silver Marten",
|
||||||
|
"Tan",
|
||||||
|
"Tortoiseshell",
|
||||||
|
"White"
|
||||||
|
],
|
||||||
|
"genders": ["Male", "Female"],
|
||||||
|
"_links": {
|
||||||
|
"self": { "href": "/v2/types/rabbit" },
|
||||||
|
"breeds": { "href": "/v2/types/rabbit/breeds" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bird",
|
||||||
|
"coats": [],
|
||||||
|
"colors": [
|
||||||
|
"Black",
|
||||||
|
"Blue",
|
||||||
|
"Brown",
|
||||||
|
"Buff",
|
||||||
|
"Gray",
|
||||||
|
"Green",
|
||||||
|
"Olive",
|
||||||
|
"Orange",
|
||||||
|
"Pink",
|
||||||
|
"Purple / Violet",
|
||||||
|
"Red",
|
||||||
|
"Rust / Rufous",
|
||||||
|
"Tan",
|
||||||
|
"White",
|
||||||
|
"Yellow"
|
||||||
|
],
|
||||||
|
"genders": ["Male", "Female", "Unknown"],
|
||||||
|
"_links": {
|
||||||
|
"self": { "href": "/v2/types/bird" },
|
||||||
|
"breeds": { "href": "/v2/types/bird/breeds" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
41
src/mocks/handlers.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { rest } from 'msw';
|
||||||
|
import types from './data/types.json';
|
||||||
|
import animals from './data/animals.json';
|
||||||
|
import details from './data/details.json';
|
||||||
|
|
||||||
|
export const handlers = [
|
||||||
|
rest.get('/types', (_req, res, ctx) => {
|
||||||
|
return res(ctx.status(200), ctx.json(types));
|
||||||
|
}),
|
||||||
|
rest.get('/animals', (req, res, ctx) => {
|
||||||
|
const type = req.url.searchParams.get('type');
|
||||||
|
const query = req.url.searchParams.get('query');
|
||||||
|
let response = animals.animals;
|
||||||
|
|
||||||
|
if (type !== '') {
|
||||||
|
response = response.filter(
|
||||||
|
(animal) => animal.type.toLowerCase() === type.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (query !== '') {
|
||||||
|
response = response.filter(
|
||||||
|
(animal) =>
|
||||||
|
animal.contact.address.state
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase()) ||
|
||||||
|
animal.name.toLowerCase().includes(query.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res(ctx.status(200), ctx.json(response));
|
||||||
|
}),
|
||||||
|
rest.get('/animals/:id', (req, res, ctx) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
let response = details[id];
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return res(ctx.status(404));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res(ctx.status(200), ctx.json(response));
|
||||||
|
})
|
||||||
|
];
|
68
src/pages/detail/index.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getPetDetails } from '../../api/petfinder';
|
||||||
|
import Hero from '../../components/hero';
|
||||||
|
|
||||||
|
// Import useParams
|
||||||
|
// Import Navigate
|
||||||
|
|
||||||
|
const PetDetailsPage = () => {
|
||||||
|
const [data, setData] = useState();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const id = '51322435'; // <--- Update me!
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getPetsData() {
|
||||||
|
try {
|
||||||
|
const petsData = await getPetDetails(id);
|
||||||
|
setData(petsData);
|
||||||
|
setError(false);
|
||||||
|
} catch (e) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPetsData();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{loading ? (
|
||||||
|
<h3>Loading...</h3>
|
||||||
|
) : error ? (
|
||||||
|
<div>
|
||||||
|
{/* Redirect to /pet-details-not-found if there was an error! */}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<main>
|
||||||
|
<Hero
|
||||||
|
image={data.photos[1]?.full || 'https://i.imgur.com/aEcJUFK.png'}
|
||||||
|
displayText={`Meet ${data.name}`}
|
||||||
|
/>
|
||||||
|
<div className="pet-detail">
|
||||||
|
<div className="pet-image-container">
|
||||||
|
<img
|
||||||
|
className="pet-image"
|
||||||
|
src={
|
||||||
|
data.photos[0]?.medium || 'https://i.imgur.com/aEcJUFK.png'
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>{data.name}</h1>
|
||||||
|
<h3>Breed: {data.breeds.primary}</h3>
|
||||||
|
<p>Color: {data.colors.primary || 'Unknown'}</p>
|
||||||
|
<p>Gender: {data.gender}</p>
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>{data.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PetDetailsPage;
|
69
src/pages/home/index.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getPets } from '../../api/petfinder';
|
||||||
|
import Hero from '../../components/hero';
|
||||||
|
|
||||||
|
// import useParams
|
||||||
|
// import Link
|
||||||
|
|
||||||
|
const HomePage = () => {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const type = ''; // Fix me!
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getPetsData() {
|
||||||
|
const petsData = await getPets(type);
|
||||||
|
setData(petsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPetsData();
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <h2>Loading...</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<Hero />
|
||||||
|
<h3>
|
||||||
|
<span className="pet-type-label">{type ? `${type}s` : 'Pets'}</span>{' '}
|
||||||
|
available for adoption near you
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{data.length ? (
|
||||||
|
<div className="grid">
|
||||||
|
{data.map((animal) => (
|
||||||
|
<a // Change me to a Link!
|
||||||
|
key={animal.id}
|
||||||
|
href={`/${animal.type.toLowerCase()}/${animal.id}`}
|
||||||
|
className="pet"
|
||||||
|
>
|
||||||
|
<article>
|
||||||
|
<div className="pet-image-container">
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
className="pet-image"
|
||||||
|
src={
|
||||||
|
animal.photos[0]?.medium ||
|
||||||
|
'/missing-animal.png'
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<h3>{animal.name}</h3>
|
||||||
|
<p>Breed: {animal.breeds.primary}</p>
|
||||||
|
<p>Color: {animal.colors.primary}</p>
|
||||||
|
<p>Gender: {animal.gender}</p>
|
||||||
|
</article>
|
||||||
|
</a> // Don't forget to change me!
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="prompt">No {type}s available for adoption now.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
30
src/pages/petDetailsNotFound/index.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// import useNavigate here.
|
||||||
|
|
||||||
|
const PetDetailsNotFound = () => {
|
||||||
|
|
||||||
|
// get the navigate function from useNavigate
|
||||||
|
const navigate = "REPLACE ME";
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
// Go home!
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<h3>404: Who let the dogs out?</h3>
|
||||||
|
<p>Sorry, but the details for this pet have not been uploaded by the shelter yet. Check back later!</p>
|
||||||
|
<img
|
||||||
|
src="https://i.chzbgr.com/full/8362031616/h9EB970C5/weve-lost-our-corgination"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div className="actions-container">
|
||||||
|
<button className="button" onClick={goHome}>
|
||||||
|
Go Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PetDetailsNotFound;
|
42
src/pages/search/index.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Hero from '../../components/hero';
|
||||||
|
import { getPets } from '../../api/petfinder';
|
||||||
|
import Pet from '../../components/pet';
|
||||||
|
// Import useSearchParams
|
||||||
|
|
||||||
|
const SearchPage = () => {
|
||||||
|
|
||||||
|
// Get searchParams object from useSearchParams
|
||||||
|
|
||||||
|
const petNameToFind = 'REPLACE ME'; // Get query parameter using searchParams object
|
||||||
|
|
||||||
|
const [pets, setPets] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getPetsData() {
|
||||||
|
const petsData = await getPets('', petNameToFind);
|
||||||
|
|
||||||
|
setPets(petsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPetsData();
|
||||||
|
}, [petNameToFind]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<Hero displayText={`Results for ${petNameToFind}`} />
|
||||||
|
|
||||||
|
<h3>Pets available for adoption near you</h3>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div className="grid">
|
||||||
|
{pets.map((pet) => (
|
||||||
|
<Pet animal={pet} key={pet.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchPage;
|
13
src/reportWebVitals.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const reportWebVitals = (onPerfEntry) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
5
src/setupTests.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|