First
This commit is contained in:
parent
9e685b8522
commit
a42c9f5875
17350
package-lock.json
generated
Normal file
17350
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
public/index.html
Normal file
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
BIN
public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
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
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
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
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
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
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')
|
||||
.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();
|
||||
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();
|
||||
const nameB = `${b.name.first} ${b.name.last}`.toLowerCase();
|
||||
if (sortOrder === 'asc') {
|
||||
return nameA.localeCompare(nameB);
|
||||
} 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);
|
||||
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] !== '...') {
|
||||
pageNumbers.push('...');
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle clicking on More Info
|
||||
const handleMoreInfo = (user) => {
|
||||
setUserDetail(user);
|
||||
navigate(`/user-details`);
|
||||
};
|
||||
|
||||
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 => (
|
||||
<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
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
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>
|
||||
<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
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,
|
||||
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(),
|
||||
user.nat,
|
||||
user.location.timezone.description,
|
||||
`${user.location.coordinates.latitude}, ${user.location.coordinates.longitude}`,
|
||||
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`);
|
||||
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>
|
||||
<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>
|
||||
<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}
|
||||
</div>
|
||||
<div><strong>Date of Birth:</strong> {new Date(user.dob.date).toLocaleDateString()}</div>
|
||||
<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}
|
||||
</div>
|
||||
<div><strong>Username:</strong> {user.login.username}</div>
|
||||
</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
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
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
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
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';
|
Loading…
Reference in a new issue