├── .env ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package.json ├── public └── vite.svg ├── src ├── App.jsx ├── assets │ └── react.svg ├── index.css └── main.jsx └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | VITE_API_KEY=add_your_unsplash_api_access_key -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:react/jsx-runtime', 7 | 'plugin:react-hooks/recommended' 8 | ], 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | settings: { react: { version: '18.2' } }, 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': 'warn', 14 | 'react/prop-types': 'off' 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Steps to run this application 2 | 3 | - Add your Unsplash Api key in `.env` file 4 | - Execute `npm install` or `yarn install` command to install packages 5 | - Execute `npm run dev` or `yarn run dev` command to start the application 6 | - Access the application at the URL displayed in the terminal 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image Search 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unsplash_image_search", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.4.0", 14 | "bootstrap": "^5.3.0", 15 | "react": "^18.2.0", 16 | "react-bootstrap": "^2.7.4", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.0.37", 21 | "@types/react-dom": "^18.0.11", 22 | "@vitejs/plugin-react": "^4.0.0", 23 | "eslint": "^8.38.0", 24 | "eslint-plugin-react": "^7.32.2", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "eslint-plugin-react-refresh": "^0.3.4", 27 | "vite": "^4.3.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useCallback, useEffect, useRef, useState } from 'react'; 3 | import { Button, Form } from 'react-bootstrap'; 4 | 5 | const API_URL = 'https://api.unsplash.com/search/photos'; 6 | const IMAGES_PER_PAGE = 20; 7 | 8 | function App() { 9 | const searchInput = useRef(null); 10 | const [images, setImages] = useState([]); 11 | const [page, setPage] = useState(1); 12 | const [totalPages, setTotalPages] = useState(0); 13 | const [errorMsg, setErrorMsg] = useState(''); 14 | const [loading, setLoading] = useState(false); 15 | 16 | const fetchImages = useCallback(async () => { 17 | try { 18 | if (searchInput.current.value) { 19 | setErrorMsg(''); 20 | setLoading(true); 21 | const { data } = await axios.get( 22 | `${API_URL}?query=${ 23 | searchInput.current.value 24 | }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ 25 | import.meta.env.VITE_API_KEY 26 | }` 27 | ); 28 | setImages(data.results); 29 | setTotalPages(data.total_pages); 30 | setLoading(false); 31 | } 32 | } catch (error) { 33 | setErrorMsg('Error fetching images. Try again later.'); 34 | console.log(error); 35 | setLoading(false); 36 | } 37 | }, [page]); 38 | 39 | useEffect(() => { 40 | fetchImages(); 41 | }, [fetchImages]); 42 | 43 | const resetSearch = () => { 44 | setPage(1); 45 | fetchImages(); 46 | }; 47 | 48 | const handleSearch = (event) => { 49 | event.preventDefault(); 50 | resetSearch(); 51 | }; 52 | 53 | const handleSelection = (selection) => { 54 | searchInput.current.value = selection; 55 | resetSearch(); 56 | }; 57 | 58 | return ( 59 |
60 |

Image Search

61 | {errorMsg &&

{errorMsg}

} 62 |
63 |
64 | 70 | 71 |
72 |
73 |
handleSelection('nature')}>Nature
74 |
handleSelection('birds')}>Birds
75 |
handleSelection('cats')}>Cats
76 |
handleSelection('shoes')}>Shoes
77 |
78 | {loading ? ( 79 |

Loading...

80 | ) : ( 81 | <> 82 |
83 | {images.map((image) => ( 84 | {image.alt_description} 90 | ))} 91 |
92 |
93 | {page > 1 && ( 94 | 95 | )} 96 | {page < totalPages && ( 97 | 98 | )} 99 |
100 | 101 | )} 102 |
103 | ); 104 | } 105 | 106 | export default App; 107 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | --default-spacing: 10px; 6 | --default-margin: 1rem; 7 | --medium-margin: 3rem; 8 | --larger-margin: 5rem; 9 | --primary-color: #7676d7; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | body { 19 | color: var(--primary-color); 20 | } 21 | 22 | /* common css starts */ 23 | 24 | .container { 25 | margin-left: auto; 26 | margin-right: auto; 27 | display: flex; 28 | justify-content: center; 29 | flex-direction: column; 30 | min-height: 100vh; 31 | } 32 | 33 | .title { 34 | text-align: center; 35 | margin-top: var(--default-margin); 36 | color: #7676d7; 37 | } 38 | 39 | .buttons { 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | gap: var(--default-margin); 44 | margin-top: var(--medium-margin); 45 | margin-bottom: var(--larger-margin); 46 | } 47 | 48 | .buttons .btn, 49 | .buttons .btn:active, 50 | .buttons .btn:focus { 51 | background-color: var(--primary-color); 52 | box-shadow: none; 53 | outline: none; 54 | border: none; 55 | } 56 | 57 | .error-msg { 58 | color: #ff0000; 59 | text-align: center; 60 | } 61 | 62 | .loading { 63 | color: #6565d4; 64 | text-align: center; 65 | margin-top: 20px; 66 | font-size: 20px; 67 | } 68 | 69 | /* common css ends */ 70 | 71 | /* search section starts */ 72 | 73 | .search-section { 74 | display: flex; 75 | justify-content: center; 76 | align-items: center; 77 | margin-top: var(--default-margin); 78 | } 79 | 80 | .search-section .search-input { 81 | min-width: 500px; 82 | padding: var(--default-spacing); 83 | } 84 | 85 | .search-section .search-btn { 86 | margin-left: var(--default-spacing); 87 | } 88 | 89 | /* search section ends */ 90 | 91 | /* filters section starts */ 92 | 93 | .filters { 94 | display: flex; 95 | justify-content: center; 96 | flex-wrap: wrap; 97 | align-items: center; 98 | gap: 1rem; 99 | margin-top: var(--default-margin); 100 | } 101 | 102 | .filters > * { 103 | padding: 5px 10px; 104 | background: #7676d7; 105 | color: #fff; 106 | border-radius: 5px; 107 | cursor: pointer; 108 | } 109 | 110 | /* filters section ends */ 111 | 112 | /* images section starts */ 113 | 114 | .images { 115 | margin-top: var(--medium-margin); 116 | display: grid; 117 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 118 | grid-gap: var(--default-spacing); 119 | justify-content: center; 120 | align-items: center; 121 | } 122 | 123 | .images .image { 124 | width: 200px; 125 | height: 200px; 126 | justify-self: center; 127 | align-self: center; 128 | margin-left: 2rem; 129 | border-radius: 10px; 130 | transition: transform 0.5s; 131 | } 132 | 133 | .images .image:hover { 134 | transform: translateY(-3px); 135 | } 136 | 137 | /* images section ends */ 138 | 139 | /* Responsive adjustments */ 140 | @media (max-width: 768px) { 141 | .images { 142 | grid-template-columns: repeat(2, 1fr); 143 | } 144 | } 145 | 146 | @media (max-width: 480px) { 147 | .search-section .search-input { 148 | width: 100%; 149 | min-width: unset; 150 | margin: 0 var(--default-margin); 151 | } 152 | 153 | .images { 154 | grid-template-columns: 1fr; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import './index.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')).render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------