First #1
17350
package-lock.json
generated
Normal file
39
package.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "user",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cra-template": "1.2.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"react-scripts": "5.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"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": {
|
||||||
|
"web-vitals": "^4.2.4"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
43
public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!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" />
|
||||||
|
<!--
|
||||||
|
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
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal 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"
|
||||||
|
}
|
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
250
src/App.css
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
/* Global Styles */
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
background-color: #f4f7fc;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 4px solid #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search and Filter Section */
|
||||||
|
.search-filter-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin: 30px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter-container input,
|
||||||
|
.search-filter-container select,
|
||||||
|
.sort-options select {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: 200px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter-container input:focus,
|
||||||
|
.search-filter-container select:focus,
|
||||||
|
.sort-options select:focus{
|
||||||
|
border-color: #007bff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Grid Styles */
|
||||||
|
.user-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
padding: 0 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #333;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info p {
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* View Details Button */
|
||||||
|
.view-details-btn {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-details-btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination Styles */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button:hover {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button.active {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Details Page Styles */
|
||||||
|
.user-details {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
top: 160px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details h2 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-image {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info div {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info strong {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to User List Button */
|
||||||
|
.back-btn ,.export-btn{
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
margin-top: 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover,.export-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.view-detail
|
||||||
|
{
|
||||||
|
background-color: #007bff;
|
||||||
|
width: 120px;
|
||||||
|
margin-left: 50px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
.view-detail:hover
|
||||||
|
{
|
||||||
|
color: #fdfbfb;
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section button{
|
||||||
|
background-color: #6aabf1;
|
||||||
|
width: 120px;
|
||||||
|
margin-left: 50px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
.hero-section button:hover
|
||||||
|
{
|
||||||
|
color: #fdfbfb;
|
||||||
|
background-color: #0056b3;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
30
src/App.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// src/App.js
|
||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import './App.css';
|
||||||
|
import Home from './Home'; // Import the Home component
|
||||||
|
import UserDetails from './UserDetail';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<h1>Random Users</h1>
|
||||||
|
<p>Explore a diverse set of random users from all over the world!</p>
|
||||||
|
<div className="hero-section">
|
||||||
|
<button onClick={() => window.location.href = '/home'}>View Users</button>
|
||||||
|
|||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<div>Landing Page</div>} />
|
||||||
|
<Route path="/home" element={<Home />} />
|
||||||
|
<Route path="/user-details" element={<UserDetails />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
8
src/App.test.js
Normal 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();
|
||||||
|
});
|
167
src/Home.js
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// src/Home.js
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import UserCard from './UserCard';
|
||||||
|
import SearchFilter from './SearchFilter';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [filteredData, setFilteredData] = useState([]);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedGender, setSelectedGender] = useState('');
|
||||||
|
const [selectedAgeRange, setSelectedAgeRange] = useState('');
|
||||||
|
const [selectedNationality, setSelectedNationality] = useState('');
|
||||||
|
const [sortBy, setSortBy] = useState('name'); // Default sort by name
|
||||||
|
const [sortOrder, setSortOrder] = useState('asc'); // Default sort order: ascending
|
||||||
|
const [setUserDetail] = useState(null);
|
||||||
|
const usersPerPage = 12;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Fetch data from the API
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('https://randomuser.me/api/?results=200')
|
||||||
sumitdml123
commented
error handling is missing from the function use try catch error handling is missing from the function use try catch
|
|||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
setData(data.results);
|
||||||
|
setFilteredData(data.results); // Set initial filtered data as the full list
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filter users based on search query and selected filters
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = data;
|
||||||
|
|
||||||
|
// Search by name, email, or location
|
||||||
|
if (searchQuery) {
|
||||||
|
filtered = filtered.filter(user =>
|
||||||
|
user.name.first.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
user.name.last.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
`${user.location.city} ${user.location.country}`.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by gender
|
||||||
|
if (selectedGender) {
|
||||||
|
filtered = filtered.filter(user => user.gender === selectedGender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by age range
|
||||||
|
if (selectedAgeRange) {
|
||||||
|
const [minAge, maxAge] = selectedAgeRange.split('-').map(Number);
|
||||||
|
filtered = filtered.filter(user => {
|
||||||
|
const userAge = new Date().getFullYear() - new Date(user.dob.date).getFullYear();
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
return userAge >= minAge && userAge <= maxAge;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by nationality
|
||||||
|
if (selectedNationality) {
|
||||||
|
filtered = filtered.filter(user => user.nat === selectedNationality);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting logic
|
||||||
|
if (sortBy === 'name') {
|
||||||
|
filtered = filtered.sort((a, b) => {
|
||||||
|
const nameA = `${a.name.first} ${a.name.last}`.toLowerCase();
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
const nameB = `${b.name.first} ${b.name.last}`.toLowerCase();
|
||||||
|
if (sortOrder === 'asc') {
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
sumitdml123
commented
naming convention is not right use proper variable names naming convention is not right use proper variable names
|
|||||||
|
} else {
|
||||||
|
return nameB.localeCompare(nameA);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (sortBy === 'age') {
|
||||||
|
filtered = filtered.sort((a, b) => {
|
||||||
|
const ageA = new Date().getFullYear() - new Date(a.dob.date).getFullYear();
|
||||||
|
const ageB = new Date().getFullYear() - new Date(b.dob.date).getFullYear();
|
||||||
|
if (sortOrder === 'asc') {
|
||||||
|
return ageA - ageB;
|
||||||
|
} else {
|
||||||
|
return ageB - ageA;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredData(filtered);
|
||||||
|
setCurrentPage(1); // Reset to first page whenever filters or sorting change
|
||||||
|
}, [searchQuery, selectedGender, selectedAgeRange, selectedNationality, sortBy, sortOrder, data]);
|
||||||
|
|
||||||
|
// Pagination logic
|
||||||
|
const indexOfLastUser = currentPage * usersPerPage;
|
||||||
|
const currentUsers = filteredData.slice(indexOfLastUser - usersPerPage, indexOfLastUser);
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
const totalPages = Math.ceil(filteredData.length / usersPerPage);
|
||||||
|
|
||||||
|
const pageNumbers = [];
|
||||||
|
const maxPageRange = 2;
|
||||||
|
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
if (i <= 2 || i > totalPages - 2 || (i >= currentPage - maxPageRange && i <= currentPage + maxPageRange)) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
} else if (pageNumbers[pageNumbers.length - 1] !== '...') {
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
pageNumbers.push('...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle clicking on More Info
|
||||||
|
const handleMoreInfo = (user) => {
|
||||||
|
setUserDetail(user);
|
||||||
|
navigate(`/user-details`);
|
||||||
sumitdml123
commented
You're using const [setUserDetail] = useState(null);, but setUserDetail is not required as a state. Instead, you can directly navigate with the user details using state in useNavigate. try this and check if it works And in the UserDetails component, you can access this user via location.state.user. You're using const [setUserDetail] = useState(null);, but setUserDetail is not required as a state. Instead, you can directly navigate with the user details using state in useNavigate.
try this and check if it works
const handleMoreInfo = (user) => {
navigate(`/user-details`, { state: { user } });
};
And in the UserDetails component, you can access this user via location.state.user.
|
|||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Home">
|
||||||
|
<SearchFilter
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
setSearchQuery={setSearchQuery}
|
||||||
|
selectedGender={selectedGender}
|
||||||
|
setSelectedGender={setSelectedGender}
|
||||||
|
selectedAgeRange={selectedAgeRange}
|
||||||
|
setSelectedAgeRange={setSelectedAgeRange}
|
||||||
|
selectedNationality={selectedNationality}
|
||||||
|
setSelectedNationality={setSelectedNationality}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Sort Options UI */}
|
||||||
|
<div className="sort-options">
|
||||||
|
<select onChange={(e) => setSortBy(e.target.value)} value={sortBy}>
|
||||||
|
<option value="name">Sort by Name</option>
|
||||||
|
<option value="age">Sort by Age</option>
|
||||||
|
</select>
|
||||||
|
<select onChange={(e) => setSortOrder(e.target.value)} value={sortOrder}>
|
||||||
|
<option value="asc">Ascending</option>
|
||||||
|
<option value="desc">Descending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="user-grid">
|
||||||
|
{currentUsers.map(user => (
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
<UserCard key={user.email} user={user} handleMoreInfo={handleMoreInfo} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="pagination">
|
||||||
|
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1}>Prev</button>
|
||||||
|
{pageNumbers.map((number, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => number !== '...' && setCurrentPage(number)}
|
||||||
|
className={currentPage === number ? 'active' : ''}
|
||||||
|
disabled={number === '...'}
|
||||||
|
>
|
||||||
|
{number}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages}>Next</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>Loading...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
37
src/SearchFilter.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// src/SearchFilter.js
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const SearchFilter = ({ searchQuery, setSearchQuery, selectedGender, setSelectedGender, selectedAgeRange, setSelectedAgeRange, selectedNationality, setSelectedNationality }) => {
|
||||||
|
return (
|
||||||
|
<div className="search-filter-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by name, email, or location"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<select value={selectedGender} onChange={(e) => setSelectedGender(e.target.value)}>
|
||||||
|
<option value="">All Genders</option>
|
||||||
|
<option value="male">Male</option>
|
||||||
|
<option value="female">Female</option>
|
||||||
|
</select>
|
||||||
|
<select value={selectedAgeRange} onChange={(e) => setSelectedAgeRange(e.target.value)}>
|
||||||
|
<option value="">All Ages</option>
|
||||||
|
<option value="18-30">18-30</option>
|
||||||
|
<option value="31-45">31-45</option>
|
||||||
|
<option value="46-60">46-60</option>
|
||||||
|
<option value="61+">61+</option>
|
||||||
|
</select>
|
||||||
|
<select value={selectedNationality} onChange={(e) => setSelectedNationality(e.target.value)}>
|
||||||
|
<option value="">All Nationalities</option>
|
||||||
|
<option value="US">US</option>
|
||||||
|
<option value="GB">GB</option>
|
||||||
|
<option value="IN">IN</option>
|
||||||
|
<option value="CA">CA</option>
|
||||||
|
{/* Add more nationality options as needed */}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchFilter;
|
28
src/UserCard.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// src/UserCard.js
|
||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const UserCard = ({ user }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
console.log('usersss', user)
|
||||||
|
|
||||||
|
const onClickHandler = () => {
|
||||||
|
navigate(`/user-details`, { state: user});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="user-card" key={user.email}>
|
||||||
|
<img src={user.picture.medium} alt={user.name.first} />
|
||||||
|
<div className="user-info">
|
||||||
|
<h3>{user.name.first} {user.name.last}</h3>
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
<p>{user.email}</p>
|
||||||
|
{/* Link to the UserDetails page, passing the user data via state */}
|
||||||
|
<p className='view-detail' onClick={() => onClickHandler()}>
|
||||||
|
View Details
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserCard;
|
84
src/UserDetail.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useLocation, Link } from 'react-router-dom'; // useLocation to access state passed through the router
|
||||||
|
import './App.css'; // Import the CSS for styling
|
||||||
|
|
||||||
|
const UserDetails = () => {
|
||||||
|
// Use useLocation to access the user data passed from UserCard
|
||||||
|
const location = useLocation();
|
||||||
|
if (!location.state) {
|
||||||
|
return <p>No user data available!</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = location.state;
|
||||||
|
|
||||||
|
// Function to export user data to CSV
|
||||||
|
const exportToCSV = () => {
|
||||||
|
// Create a CSV string with the user details
|
||||||
|
const userData = [
|
||||||
|
['First Name', 'Last Name', 'Email', 'Phone', 'Cell', 'Address', 'Date of Birth', 'Nationality', 'Timezone', 'Coordinates', 'Username'],
|
||||||
|
[
|
||||||
|
user.name.first,
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
user.name.last,
|
||||||
|
user.email,
|
||||||
|
user.phone,
|
||||||
|
user.cell,
|
||||||
|
`${user.location.street.number} ${user.location.street.name}, ${user.location.city}, ${user.location.state}, ${user.location.country} - ${user.location.postcode}`,
|
||||||
|
new Date(user.dob.date).toLocaleDateString(),
|
||||||
sumitdml123
commented
toLocaleDateString('en-US') use like this toLocaleDateString('en-US') use like this
|
|||||||
|
user.nat,
|
||||||
|
user.location.timezone.description,
|
||||||
|
`${user.location.coordinates.latitude}, ${user.location.coordinates.longitude}`,
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
user.login.username
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convert to CSV format
|
||||||
|
const csvContent = userData.map(row => row.join(',')).join('\n');
|
||||||
|
|
||||||
|
// Create a Blob from the CSV data
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
|
||||||
|
// Create a link to download the CSV file
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `${user.name.first}_${user.name.last}_details.csv`);
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
// Append the link to the document body and trigger the download
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="user-details">
|
||||||
|
<h2>{user.name.first} {user.name.last}</h2>
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
<img src={user.picture.large} alt={user.name.first} className="user-image" />
|
||||||
|
|
||||||
|
{/* Display user details */}
|
||||||
|
<div className="user-info">
|
||||||
|
<div><strong>Email:</strong> {user.email}</div>
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
<div><strong>Phone:</strong> {user.phone}</div>
|
||||||
|
<div><strong>Cell:</strong> {user.cell}</div>
|
||||||
|
<div>
|
||||||
|
<strong>Location:</strong> {user.location.street.number} {user.location.street.name}, {user.location.city}, {user.location.state}, {user.location.country} - {user.location.postcode}
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
</div>
|
||||||
|
<div><strong>Date of Birth:</strong> {new Date(user.dob.date).toLocaleDateString()}</div>
|
||||||
sumitdml123
commented
toLocaleDateString('en-US') use this toLocaleDateString('en-US') use this
|
|||||||
|
<div><strong>Nationality:</strong> {user.nat}</div>
|
||||||
|
<div><strong>Timezone:</strong> {user.location.timezone.description}</div>
|
||||||
|
<div>
|
||||||
|
<strong>Coordinates:</strong> {user.location.coordinates.latitude}, {user.location.coordinates.longitude}
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
</div>
|
||||||
|
<div><strong>Username:</strong> {user.login.username}</div>
|
||||||
sumitdml123
commented
optional chaining is missing optional chaining is missing
|
|||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Link to go back to the user list */}
|
||||||
|
<Link to="/home" className="back-btn">Back to User List</Link>
|
||||||
|
|
||||||
|
{/* Export Button to download user data as CSV */}
|
||||||
|
<button onClick={exportToCSV} className="export-btn">Export to CSV</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserDetails;
|
13
src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
12
src/index.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
13
src/reportWebVitals.js
Normal 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
|
@ -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';
|
You're using window.location.href for navigation in the button click event. It's better to use the useNavigate hook from react-router-dom for navigation within a React app. This ensures that the navigation is handled by React's routing system, preventing a full page reload.