Skip to content

Commit 9c3975b

Browse files
committed
feat: add user search and search in their public repositories
1 parent 3e0e80e commit 9c3975b

4 files changed

Lines changed: 238 additions & 34 deletions

File tree

src/App.jsx

Lines changed: 141 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,155 @@
1-
import { useEffect, useState } from 'react';
1+
import { useState } from 'react';
22
import Button from 'react-bootstrap/Button';
33
import Card from 'react-bootstrap/Card';
4+
import ListGroup from 'react-bootstrap/ListGroup';
5+
import Container from 'react-bootstrap/Container';
6+
import Row from 'react-bootstrap/Row';
7+
import Col from 'react-bootstrap/Col';
8+
import Form from 'react-bootstrap/Form';
49
import Header from './components/Header/Header';
510

611
function App() {
7-
const [avatarURL, setAvatarURL] = useState();
8-
const [login, setLogin] = useState();
9-
const [followersURL, setFollowersURL] = useState();
12+
const [user, setUser] = useState(null);
13+
const [repos, setRepos] = useState([]);
14+
const [searchRepo, setSearchRepo] = useState('');
15+
const [error, setError] = useState('');
1016

11-
useEffect(() => {
12-
fetch('https://api.github.com/users/NJul')
17+
const fetchUser = username => {
18+
fetch(`https://api.github.com/users/${username}`)
1319
.then(res => res.json())
14-
.then(
15-
result => {
16-
console.log(result);
17-
setAvatarURL(result.avatar_url);
18-
setLogin(result.login);
19-
setFollowersURL(result.followers_url);
20-
},
21-
error => {
22-
console.log(error);
20+
.then(result => {
21+
if (result.message === 'Not Found') {
22+
setError('User not found');
23+
setUser(null);
24+
setRepos([]);
25+
} else {
26+
setError('');
27+
setUser(result);
28+
setRepos([]);
2329
}
24-
);
25-
}, []);
30+
})
31+
.catch(() => {
32+
setError('Something went wrong');
33+
setUser(null);
34+
setRepos([]);
35+
});
36+
};
37+
38+
const fetchRepos = username => {
39+
fetch(`https://api.github.com/users/${username}/repos`)
40+
.then(res => res.json())
41+
.then(result => {
42+
if (Array.isArray(result)) {
43+
setRepos(result);
44+
} else {
45+
setRepos([]);
46+
}
47+
})
48+
.catch(() => setRepos([]));
49+
};
50+
51+
const filteredRepos = repos.filter(
52+
repo =>
53+
repo.name.toLowerCase().includes(searchRepo.toLowerCase()) ||
54+
(repo.description &&
55+
repo.description.toLowerCase().includes(searchRepo.toLowerCase()))
56+
);
2657

2758
return (
2859
<>
29-
<Header />
30-
31-
<div className='w-100 min-vh-100 d-flex justify-content-center align-items-center'>
32-
<Card style={{ width: '18rem' }}>
33-
<Card.Img variant='top' src={avatarURL} />
34-
<Card.Body className='text-center'>
35-
<Card.Title>{login}</Card.Title>
36-
<Card.Text>
37-
<a href={followersURL} target='_blank' rel='noopener noreferrer'>
38-
Followers
39-
</a>
40-
</Card.Text>
41-
<Button variant='primary'>List my public repos</Button>
42-
</Card.Body>
43-
</Card>
44-
</div>
60+
<Header onSearch={fetchUser} />
61+
62+
<Container fluid className='py-4'>
63+
<Row className='justify-content-center'>
64+
{error && <p className='text-danger text-center'>{error}</p>}
65+
66+
{user && (
67+
<Col xs={12} sm={10} md={6} lg={4} className='mb-4'>
68+
<Card className='shadow-sm h-100'>
69+
<Card.Img variant='top' src={user.avatar_url} />
70+
<Card.Body className='text-center'>
71+
<Card.Title>{user.login}</Card.Title>
72+
<Card.Text>
73+
<a
74+
href={user.html_url}
75+
target='_blank'
76+
rel='noopener noreferrer'
77+
>
78+
View Profile
79+
</a>
80+
</Card.Text>
81+
<Card.Text>
82+
Followers: {user.followers} | Following: {user.following}
83+
</Card.Text>
84+
<Button
85+
variant='primary'
86+
onClick={() => fetchRepos(user.login)}
87+
>
88+
Show Public Repos
89+
</Button>
90+
</Card.Body>
91+
</Card>
92+
</Col>
93+
)}
94+
</Row>
95+
96+
{repos.length > 0 && (
97+
<Row className='justify-content-center'>
98+
<Col xs={12} sm={11} md={10} lg={8}>
99+
<Card className='shadow-sm'>
100+
<Card.Body>
101+
<Card.Title className='text-center mb-3'>
102+
Public Repositories
103+
</Card.Title>
104+
105+
<Form className='mb-3'>
106+
<Form.Control
107+
type='text'
108+
placeholder='Search repositories ...'
109+
value={searchRepo}
110+
onChange={e => setSearchRepo(e.target.value)}
111+
/>
112+
</Form>
113+
114+
<ListGroup variant='flush'>
115+
{filteredRepos.length > 0 ? (
116+
filteredRepos.map(repo => (
117+
<ListGroup.Item
118+
key={repo.id}
119+
className='d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center'
120+
>
121+
<div>
122+
<a
123+
href={repo.html_url}
124+
target='_blank'
125+
rel='noopener noreferrer'
126+
className='fw-semibold'
127+
>
128+
{repo.name}
129+
</a>
130+
{repo.description && (
131+
<p className='mb-1 small text-muted'>
132+
{repo.description}
133+
</p>
134+
)}
135+
</div>
136+
<div className='repo-stats text-muted small mt-2 mt-md-0'>
137+
{repo.stargazers_count} | 🍴 {repo.forks_count}
138+
</div>
139+
</ListGroup.Item>
140+
))
141+
) : (
142+
<p className='text-center text-muted'>
143+
No repositories found
144+
</p>
145+
)}
146+
</ListGroup>
147+
</Card.Body>
148+
</Card>
149+
</Col>
150+
</Row>
151+
)}
152+
</Container>
45153
</>
46154
);
47155
}

src/assets/styles/globals.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
--surface-color: #ffffff;
88
--text-color: #000000;
99

10+
/* UI colors */
11+
--input-border-color: #ccc;
12+
--button-text-color: #ffffff;
13+
1014
/* Shadows */
1115
--shadow-color: rgba(0, 0, 0, 0.1);
1216
}
@@ -17,3 +21,29 @@ body {
1721
font-family: 'Segoe UI', sans-serif;
1822
color: var(--text-color);
1923
}
24+
25+
.card {
26+
border-radius: 12px;
27+
}
28+
29+
.list-group-item {
30+
padding: 0.8rem 1rem;
31+
}
32+
33+
.repo-stats {
34+
white-space: nowrap;
35+
}
36+
37+
@media (max-width: 576px) {
38+
.list-group-item {
39+
font-size: 0.9rem;
40+
}
41+
42+
.list-group-item a {
43+
font-size: 1rem;
44+
}
45+
46+
.card-title {
47+
font-size: 1.2rem;
48+
}
49+
}

src/components/Header/Header.jsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1+
import { useState } from 'react';
12
import styles from './Header.module.css';
23

3-
export default function Header() {
4+
export default function Header({ onSearch }) {
5+
const [username, setUsername] = useState('');
6+
7+
const handleSubmit = e => {
8+
e.preventDefault();
9+
10+
if (username.trim()) {
11+
onSearch(username.trim());
12+
}
13+
};
14+
415
return (
516
<header className={styles.header}>
617
<h1 className={styles.title}>With GitHub REST API</h1>
18+
<form className={styles.searchForm} onSubmit={handleSubmit}>
19+
<input
20+
type='text'
21+
className={styles.searchInput}
22+
placeholder='Search GitHub user ...'
23+
value={username}
24+
onChange={e => setUsername(e.target.value)}
25+
/>
26+
<button type='submit' className={styles.searchButton}>
27+
Search
28+
</button>
29+
</form>
730
</header>
831
);
932
}

src/components/Header/Header.module.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,53 @@
22
background: var(--surface-color);
33
padding: 1rem;
44
box-shadow: 0 2px 6px var(--shadow-color);
5+
display: flex;
6+
justify-content: space-between;
7+
align-items: center;
8+
flex-wrap: wrap;
59
}
610

711
.title {
812
color: var(--primary-color);
913
font-size: 1.5rem;
1014
font-weight: bold;
15+
margin-bottom: 0.5rem;
16+
}
17+
18+
.searchForm {
19+
display: flex;
20+
gap: 0.5rem;
21+
}
22+
23+
.searchInput {
24+
padding: 0.4rem 0.6rem;
25+
border: 1px solid var(--input-border-color);
26+
border-radius: 6px;
27+
}
28+
29+
.searchButton {
30+
background-color: var(--primary-color);
31+
color: var(--button-text-color);
32+
border: none;
33+
padding: 0.4rem 0.8rem;
34+
border-radius: 6px;
35+
cursor: pointer;
36+
}
37+
38+
@media (max-width: 600px) {
39+
.header {
40+
flex-direction: column;
41+
align-items: stretch;
42+
text-align: center;
43+
}
44+
45+
.searchForm {
46+
width: 100%;
47+
flex-direction: column;
48+
}
49+
50+
.searchInput,
51+
.searchButton {
52+
width: 100%;
53+
}
1154
}

0 commit comments

Comments
 (0)