├── .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 |
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 |

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