diff --git a/package-lock.json b/package-lock.json index 3c65e57..adf834c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -3085,6 +3087,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4951,6 +4961,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -7907,9 +7940,9 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -13669,6 +13702,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -13971,6 +14009,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "dependencies": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -18835,6 +18903,11 @@ "source-map": "^0.7.3" } }, + "@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -20227,6 +20300,28 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz", "integrity": "sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==" }, + "axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -22414,9 +22509,9 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" }, "fork-ts-checker-webpack-plugin": { "version": "6.5.2", @@ -26369,6 +26464,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -26585,6 +26685,23 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, + "react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "requires": { + "@remix-run/router": "1.20.0" + } + }, + "react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "requires": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + } + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/package.json b/package.json index 022d9bf..c5082c5 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/src/App.css b/src/App.css index 74b5e05..7106eb2 100644 --- a/src/App.css +++ b/src/App.css @@ -1,38 +1,231 @@ -.App { - text-align: center; +/* App.css */ + +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f8f8f8; } -.App-logo { - height: 40vmin; - pointer-events: none; +.header { + background-color: #00b0ff; + padding: 10px; + display: flex; + justify-content: center; } -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } +.header img { + width: 50px; + cursor: pointer; } -.App-header { - background-color: #282c34; - min-height: 100vh; +.beer-list { display: flex; flex-direction: column; align-items: center; + margin: 20px; +} + +.beer-item { + display: flex; + align-items: center; + width: 90%; + background-color: #ffffff; + padding: 15px; + margin: 10px 0; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.beer-item img { + width: 50px; + height: auto; + margin-right: 15px; +} + +.beer-item h2 { + margin: 0; + font-size: 1.2rem; + color: #333; +} + +.beer-item p { + margin: 5px 0; + color: #666; +} + +.beer-item .contributed-by { + font-size: 0.9rem; + color: #999; +} + +.beer-item a { + color: #00b0ff; + text-decoration: none; +} +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f8f8f8; +} + +/* Header styling */ +.header { + background-color: #00b0ff; + padding: 10px; + display: flex; justify-content: center; - font-size: calc(10px + 2vmin); +} + +.header img { + width: 50px; + cursor: pointer; +} + +/* Form container styling */ +.form-container { + display: flex; + flex-direction: column; + align-items: center; + margin: 20px; +} + +.form-item { + width: 90%; + background-color: #ffffff; + padding: 15px; + margin: 10px 0; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Input field styling */ +input[type="text"], +textarea { + width: 100%; + padding: 10px; + margin: 5px 0; + border: 1px solid #ddd; + border-radius: 20px; + outline: none; +} + +textarea { + resize: none; + height: 100px; +} + +label { + font-weight: bold; + margin-top: 10px; +} + +/* Button styling */ +button { + background-color: #00b0ff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 20px; + cursor: pointer; + font-size: 1rem; + width: 50%; + margin-top: 20px; +} + +button:hover { + background-color: #008ecc; +} +/* App.css */ + +/* Global page styling */ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f0f2f5; +} + +/* Home page container */ +.home-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +/* Header styling */ +.home-header { + background-color: #00b0ff; + width: 100%; + padding: 15px; + display: flex; + justify-content: center; + align-items: center; color: white; + font-size: 1.5rem; + font-weight: bold; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.home-header img { + width: 40px; + height: 40px; + margin-right: 10px; +} + +/* Section styling */ +.section-title { + font-size: 1.8rem; + margin: 20px 0; + color: #333; + text-align: center; +} + +.card-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + margin-top: 20px; +} + +.card { + background-color: #ffffff; + width: 250px; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.card:hover { + transform: scale(1.05); +} + +.card-title { + font-size: 1.2rem; + color: #00b0ff; + margin-bottom: 10px; } -.App-link { - color: #61dafb; +.card-description { + font-size: 0.9rem; + color: #666; + margin-bottom: 15px; +} + +.card-button { + background-color: #00b0ff; + color: white; + border: none; + padding: 8px 16px; + border-radius: 20px; + cursor: pointer; + text-align: center; } -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +.card-button:hover { + background-color: #008ecc; } diff --git a/src/App.js b/src/App.js index 3784575..1d8a284 100644 --- a/src/App.js +++ b/src/App.js @@ -1,25 +1,21 @@ -import logo from './logo.svg'; -import './App.css'; +import React from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import Home from './pages/Home'; +import ListBeers from './pages/ListBeers'; +import RandomBeer from './pages/RandomBeer'; +import NewBeer from './pages/NewBeer'; +import SingleBeer from './pages/SingleBeer'; -function App() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); -} +const App = () => ( + + + } /> + } /> + } /> + } /> + } /> + + +); export default App; diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..847bf11 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const Header = () => { + return ( +
+ +
+ ); +}; + +export default Header; diff --git a/src/pages/Home.js b/src/pages/Home.js new file mode 100644 index 0000000..19e76f9 --- /dev/null +++ b/src/pages/Home.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import allBeersImg from '../assets/beers.png'; +import randomBeerImg from '../assets/random-beer.png'; +import newBeerImg from '../assets/new-beer.png'; + +const Home = () => { + return ( +
+

Welcome to the Beer App

+ + All Beers +

All Beers

+ + + Random Beer +

Random Beer

+ + + New Beer +

New Beer

+ +
+ ); +}; + +export default Home; diff --git a/src/pages/ListBeers.js b/src/pages/ListBeers.js new file mode 100644 index 0000000..9a4ba9c --- /dev/null +++ b/src/pages/ListBeers.js @@ -0,0 +1,62 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import '../App.css'; +import Header from '../components/Header'; + +const BeersPage = () => { + const [beers, setBeers] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + + // Fetch beers based on the search query or fetch all if empty + useEffect(() => { + const fetchBeers = async () => { + try { + const apiUrl = searchQuery + ? `https://ih-beers-api2.herokuapp.com/beers/search?q=${searchQuery}` + : 'https://ih-beers-api2.herokuapp.com/beers'; + const response = await axios.get(apiUrl); + setBeers(response.data); + } catch (error) { + console.error('Error fetching the beers:', error); + } + }; + + fetchBeers(); + }, [searchQuery]); + + const handleSearchChange = (event) => { + setSearchQuery(event.target.value); + }; + + return ( + <> +
+
+

Explore Our Beers

+ + +
+ {beers.length > 0 ? ( + beers.map((beer) => ( +
+

{beer.name}

+

{beer.tagline}

+ {beer.name} +
+ )) + ) : ( +

No beers found.

+ )} +
+
+ + ); +}; + +export default BeersPage; diff --git a/src/pages/NewBeer.js b/src/pages/NewBeer.js new file mode 100644 index 0000000..95d4c42 --- /dev/null +++ b/src/pages/NewBeer.js @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { createNewBeer } from '../services/beerService'; +import Header from '../components/Header'; + +const NewBeer = () => { + const [formData, setFormData] = useState({ + name: '', + tagline: '', + description: '', + first_brewed: '', + brewers_tips: '', + attenuation_level: '', + contributed_by: '', + }); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + createNewBeer(formData).then(() => { + alert('New beer successfully saved to database!'); + }); + }; + + return ( + <> +
+
+

Create a New Beer

+
+ + + + + + + + +
+
+ + ); +}; + +export default NewBeer; diff --git a/src/pages/RandomBeer.js b/src/pages/RandomBeer.js new file mode 100644 index 0000000..88403fd --- /dev/null +++ b/src/pages/RandomBeer.js @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from 'react'; +import { fetchRandomBeer } from '../services/beerService'; +import Header from '../components/Header'; + +const RandomBeer = () => { + const [beer, setBeer] = useState(null); + + useEffect(() => { + fetchRandomBeer().then((data) => setBeer(data)); + }, []); + + return ( + <> +
+ {beer && ( +
+ {beer.name} +

{beer.name}

+

{beer.tagline}

+

First Brewed: {beer.first_brewed}

+

Attenuation Level: {beer.attenuation_level}

+

{beer.description}

+

Contributed by: {beer.contributed_by}

+
+ )} + + ); +}; + +export default RandomBeer; diff --git a/src/pages/SingleBeer.js b/src/pages/SingleBeer.js new file mode 100644 index 0000000..cdddbff --- /dev/null +++ b/src/pages/SingleBeer.js @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { fetchSingleBeer } from '../services/beerService'; +import Header from '../components/Header'; + +const SingleBeer = () => { + const { beerId } = useParams(); + const [beer, setBeer] = useState(null); + + useEffect(() => { + fetchSingleBeer(beerId).then((data) => setBeer(data)); + }, [beerId]); + + return ( + <> +
+ {beer && ( +
+ {beer.name} +

{beer.name}

+

{beer.tagline}

+

First Brewed: {beer.first_brewed}

+

Attenuation Level: {beer.attenuation_level}

+

{beer.description}

+

Contributed by: {beer.contributed_by}

+
+ )} + + ); +}; + +export default SingleBeer; diff --git a/src/services/beerService.js b/src/services/beerService.js new file mode 100644 index 0000000..5dd4bad --- /dev/null +++ b/src/services/beerService.js @@ -0,0 +1,41 @@ +const API_URL = 'https://ih-beers-api2.herokuapp.com/beers'; + +export const fetchAllBeers = async () => { + try { + const response = await fetch(API_URL); + return await response.json(); + } catch (error) { + console.error('Error fetching beers:', error); + } +}; + +export const fetchSingleBeer = async (beerId) => { + try { + const response = await fetch(`${API_URL}/${beerId}`); + return await response.json(); + } catch (error) { + console.error('Error fetching beer:', error); + } +}; + +export const fetchRandomBeer = async () => { + try { + const response = await fetch(`${API_URL}/random`); + return await response.json(); + } catch (error) { + console.error('Error fetching random beer:', error); + } +}; + +export const createNewBeer = async (beerData) => { + try { + const response = await fetch(`${API_URL}/new`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(beerData), + }); + return await response.json(); + } catch (error) { + console.error('Error creating new beer:', error); + } +};