First commit

This commit is contained in:
Dino Tutic 2023-07-13 19:55:16 +02:00
commit 60d6d23fe4
38 changed files with 46420 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

21
LICENSE Normal file
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

203
public/index.css Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

322
public/mockServiceWorker.js Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

27
src/App.js Normal file
View 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
View 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();
});

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View 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';
}
};

View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1379
src/mocks/data/details.json Normal file

File diff suppressed because it is too large Load Diff

120
src/mocks/data/types.json Normal file
View 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
View 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
View 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
View 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;

View 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
View 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
View 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
View 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';

9632
yarn.lock Normal file

File diff suppressed because it is too large Load Diff