Skip to content

Commit

Permalink
components updated
Browse files Browse the repository at this point in the history
  • Loading branch information
AndraTodor committed Sep 10, 2024
1 parent 7ecce5d commit 7c3d3c5
Show file tree
Hide file tree
Showing 13 changed files with 1,165 additions and 444 deletions.
1,097 changes: 857 additions & 240 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"tailwindcss": "^3.4.10"
}
}
75 changes: 72 additions & 3 deletions src/components/App.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,72 @@
export const App = () => {
return <div>React homework template</div>;
};
import React, { useState, useEffect } from 'react';
import Searchbar from './Searchbar';
import ImageGallery from './ImageGallery';
import Button from './Button';
import Loader from './Loader';
import Modal from './Modal';
import { fetchImages } from './pixabay';

function App() {
const [images, setImages] = useState([]);
const [query, setQuery] = useState('');
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [largeImageURL, setLargeImageURL] = useState('');

useEffect(() => {
if (!query) return;

const getImages = async () => {
setIsLoading(true);
try {
const newImages = await fetchImages(query, page);
setImages(prevImages =>
page === 1 ? newImages : [...prevImages, ...newImages]
);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};

getImages();
}, [query, page]);

const handleSearchSubmit = newQuery => {
setQuery(newQuery);
setPage(1);
setImages([]);
};

const handleLoadMore = () => {
setPage(prevPage => prevPage + 1);
};

const handleImageClick = url => {
setLargeImageURL(url);
setShowModal(true);
};

const closeModal = () => {
setShowModal(false);
setLargeImageURL('');
};

return (
<div className="App">
<Searchbar onSubmit={handleSearchSubmit} />
{images.length > 0 && (
<ImageGallery images={images} onImageClick={handleImageClick} />
)}
{isLoading && <Loader />}
{images.length > 0 && !isLoading && <Button onClick={handleLoadMore} />}
{showModal && (
<Modal largeImageURL={largeImageURL} onClose={closeModal} />
)}
</div>
);
}

export default App;
22 changes: 22 additions & 0 deletions src/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';

function Button({ onClick }) {
return (
<div className="flex justify-center my-8">
<button
type="button"
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
onClick={onClick}
>
Load more
</button>
</div>
);
}

Button.propTypes = {
onClick: PropTypes.func.isRequired,
};

export default Button;
31 changes: 31 additions & 0 deletions src/components/ImageGallery.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImageGalleryItem from './ImageGalleryItem';

function ImageGallery({ images, onImageClick }) {
return (
<ul className="gallery grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
{images.map(({ id, webformatURL, largeImageURL }) => (
<ImageGalleryItem
key={id}
webformatURL={webformatURL}
largeImageURL={largeImageURL}
onClick={onImageClick}
/>
))}
</ul>
);
}

ImageGallery.propTypes = {
images: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
webformatURL: PropTypes.string.isRequired,
largeImageURL: PropTypes.string.isRequired,
})
).isRequired,
onImageClick: PropTypes.func.isRequired,
};

export default ImageGallery;
25 changes: 25 additions & 0 deletions src/components/ImageGalleryItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';

function ImageGalleryItem({ webformatURL, largeImageURL, onClick }) {
return (
<li
className="gallery-item cursor-pointer"
onClick={() => onClick(largeImageURL)}
>
<img
src={webformatURL}
alt=""
className="rounded-lg w-full h-48 object-cover shadow-md hover:shadow-xl transition-shadow duration-300"
/>
</li>
);
}

ImageGalleryItem.propTypes = {
webformatURL: PropTypes.string.isRequired,
largeImageURL: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};

export default ImageGalleryItem;
21 changes: 21 additions & 0 deletions src/components/Loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { Oval } from 'react-loader-spinner';

function Loader() {
return (
<div className="loader">
<Oval
height={80}
width={80}
color="#3B82F6"
visible={true}
ariaLabel="oval-loading"
secondaryColor="#3B82F6"
strokeWidth={2}
strokeWidthSecondary={2}
/>
</div>
);
}

export default Loader;
41 changes: 41 additions & 0 deletions src/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';

function Modal({ largeImageURL, onClose }) {
useEffect(() => {
const handleEsc = event => {
if (event.key === 'Escape') {
onClose();
}
};

window.addEventListener('keydown', handleEsc);
return () => {
window.removeEventListener('keydown', handleEsc);
};
}, [onClose]);

const handleBackdropClick = e => {
if (e.currentTarget === e.target) {
onClose();
}
};

return (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
onClick={handleBackdropClick}
>
<div className="modal max-w-3xl p-4 bg-white rounded-lg shadow-lg">
<img src={largeImageURL} alt="" className="w-full h-auto" />
</div>
</div>
);
}

Modal.propTypes = {
largeImageURL: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};

export default Modal;
48 changes: 48 additions & 0 deletions src/components/Searchbar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';

function Searchbar({ onSubmit }) {
const [query, setQuery] = useState('');

const handleChange = e => {
setQuery(e.target.value);
};

const handleSubmit = e => {
e.preventDefault();
if (query.trim() === '') return;
onSubmit(query);
setQuery('');
};

return (
<header className="searchbar flex justify-center my-8">
<form
onSubmit={handleSubmit}
className="form flex items-center space-x-5"
>
<button
type="submit"
className="button px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
>
<span className="button-label">Search</span>
</button>
<input
className="input w-80 p-2 border border-gray-300 rounded-lg"
type="text"
value={query}
onChange={handleChange}
placeholder="Search images and photos"
autoComplete="off"
autoFocus
/>
</form>
</header>
);
}

Searchbar.propTypes = {
onSubmit: PropTypes.func.isRequired,
};

export default Searchbar;
18 changes: 18 additions & 0 deletions src/components/pixabay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios';

const API_KEY = '45001008-a8478de3e072fcb427e163bfe';
const BASE_URL = 'https://pixabay.com/api/';

export const fetchImages = async (query, page = 1) => {
const response = await axios.get(`${BASE_URL}`, {
params: {
q: query,
page,
key: API_KEY,
image_type: 'photo',
orientation: 'horizontal',
per_page: 12,
},
});
return response.data.hits;
};
Loading

0 comments on commit 7c3d3c5

Please sign in to comment.