Skip to content

Commit d6a0fb5

Browse files
committed
feat: add Dark mode
1 parent 9c3975b commit d6a0fb5

5 files changed

Lines changed: 243 additions & 72 deletions

File tree

src/App.jsx

Lines changed: 101 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,66 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
22
import Button from 'react-bootstrap/Button';
33
import Card from 'react-bootstrap/Card';
44
import ListGroup from 'react-bootstrap/ListGroup';
55
import Container from 'react-bootstrap/Container';
66
import Row from 'react-bootstrap/Row';
77
import Col from 'react-bootstrap/Col';
88
import Form from 'react-bootstrap/Form';
9+
import Spinner from 'react-bootstrap/Spinner';
910
import Header from './components/Header/Header';
1011

1112
function App() {
1213
const [user, setUser] = useState(null);
1314
const [repos, setRepos] = useState([]);
1415
const [searchRepo, setSearchRepo] = useState('');
16+
const [loading, setLoading] = useState(false);
17+
const [reposLoading, setReposLoading] = useState(false);
1518
const [error, setError] = useState('');
19+
const [lastSearchedUser, setLastSearchedUser] = useState('');
20+
const [lastFetchedReposUser, setLastFetchedReposUser] = useState('');
1621

17-
const fetchUser = username => {
18-
fetch(`https://api.github.com/users/${username}`)
19-
.then(res => res.json())
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([]);
29-
}
30-
})
31-
.catch(() => {
32-
setError('Something went wrong');
33-
setUser(null);
34-
setRepos([]);
35-
});
22+
const fetchUser = async username => {
23+
if (username.toLowerCase() === lastSearchedUser.toLowerCase()) return;
24+
25+
setLoading(true);
26+
setError('');
27+
setUser(null);
28+
setRepos([]);
29+
setLastFetchedReposUser('');
30+
31+
try {
32+
const res = await fetch(`https://api.github.com/users/${username}`);
33+
const result = await res.json();
34+
35+
if (result.message === 'Not Found') {
36+
setError('User not found');
37+
} else {
38+
setUser(result);
39+
setLastSearchedUser(username);
40+
}
41+
} catch (err) {
42+
console.error(err);
43+
setError('Something went wrong');
44+
} finally {
45+
setLoading(false);
46+
}
3647
};
3748

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+
const fetchRepos = async username => {
50+
if (username.toLowerCase() === lastFetchedReposUser.toLowerCase()) return;
51+
52+
setReposLoading(true);
53+
try {
54+
const res = await fetch(`https://api.github.com/users/${username}/repos`);
55+
const result = await res.json();
56+
setRepos(Array.isArray(result) ? result : []);
57+
setLastFetchedReposUser(username);
58+
} catch (err) {
59+
console.error(err);
60+
setRepos([]);
61+
} finally {
62+
setReposLoading(false);
63+
}
4964
};
5065

5166
const filteredRepos = repos.filter(
@@ -55,6 +70,10 @@ function App() {
5570
repo.description.toLowerCase().includes(searchRepo.toLowerCase()))
5671
);
5772

73+
useEffect(() => {
74+
fetchUser('NJul');
75+
}, []);
76+
5877
return (
5978
<>
6079
<Header onSearch={fetchUser} />
@@ -63,37 +82,53 @@ function App() {
6382
<Row className='justify-content-center'>
6483
{error && <p className='text-danger text-center'>{error}</p>}
6584

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'
85+
{loading ? (
86+
<div className='d-flex justify-content-center align-items-center py-5'>
87+
<Spinner animation='border' role='status' variant='primary'>
88+
<span className='visually-hidden'>Loading user ...</span>
89+
</Spinner>
90+
</div>
91+
) : (
92+
user && (
93+
<Col xs={12} sm={10} md={6} lg={4} className='mb-4'>
94+
<Card className='shadow-sm h-100'>
95+
<Card.Img variant='top' src={user.avatar_url} />
96+
<Card.Body className='text-center'>
97+
<Card.Title>{user.login}</Card.Title>
98+
<Card.Text>
99+
<a
100+
href={user.html_url}
101+
target='_blank'
102+
rel='noopener noreferrer'
103+
>
104+
View Profile
105+
</a>
106+
</Card.Text>
107+
<Card.Text>
108+
Followers: {user.followers} | Following: {user.following}
109+
</Card.Text>
110+
<Button
111+
variant='primary'
112+
onClick={() => fetchRepos(user.login)}
77113
>
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>
114+
Show Public Repos
115+
</Button>
116+
</Card.Body>
117+
</Card>
118+
</Col>
119+
)
93120
)}
94121
</Row>
95122

96-
{repos.length > 0 && (
123+
{reposLoading && (
124+
<div className='d-flex justify-content-center align-items-center py-4'>
125+
<Spinner animation='border' role='status' variant='secondary'>
126+
<span className='visually-hidden'>Loading repositories...</span>
127+
</Spinner>
128+
</div>
129+
)}
130+
131+
{!reposLoading && repos.length > 0 && (
97132
<Row className='justify-content-center'>
98133
<Col xs={12} sm={11} md={10} lg={8}>
99134
<Card className='shadow-sm'>
@@ -103,7 +138,15 @@ function App() {
103138
</Card.Title>
104139

105140
<Form className='mb-3'>
141+
<Form.Label
142+
htmlFor='repo-search'
143+
className='visually-hidden'
144+
>
145+
Search Repositories
146+
</Form.Label>
106147
<Form.Control
148+
id='repo-search'
149+
name='repoSearch'
107150
type='text'
108151
placeholder='Search repositories ...'
109152
value={searchRepo}

src/assets/styles/globals.css

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,65 @@
88
--text-color: #000000;
99

1010
/* UI colors */
11-
--input-border-color: #ccc;
11+
--input-border-color: #cccccc;
1212
--button-text-color: #ffffff;
1313

1414
/* Shadows */
1515
--shadow-color: rgba(0, 0, 0, 0.1);
1616
}
1717

18+
/* 🌙 Dark mode */
19+
:root.dark {
20+
--background-color: #181a1b;
21+
--surface-color: #242526;
22+
--text-color: #f5f5f5;
23+
24+
--input-border-color: #555;
25+
--button-text-color: #ffffff;
26+
27+
--shadow-color: rgba(255, 255, 255, 0.1);
28+
}
29+
1830
body {
1931
margin: 0;
2032
background-color: var(--background-color);
2133
font-family: 'Segoe UI', sans-serif;
2234
color: var(--text-color);
35+
transition: background-color 0.3s ease, color 0.3s ease;
36+
}
37+
38+
a {
39+
color: var(--primary-color);
40+
text-decoration: none;
41+
transition: color 0.3s ease, background-color 0.3s ease;
42+
}
43+
44+
a:hover {
45+
text-decoration: underline;
2346
}
2447

2548
.card {
2649
border-radius: 12px;
50+
transition: background-color 0.3s, color 0.3s;
2751
}
2852

2953
.list-group-item {
3054
padding: 0.8rem 1rem;
55+
transition: background-color 0.3s, color 0.3s;
3156
}
3257

3358
.repo-stats {
3459
white-space: nowrap;
3560
}
3661

62+
.card,
63+
.list-group-item,
64+
header,
65+
.searchInput {
66+
transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease,
67+
border-color 0.3s ease;
68+
}
69+
3770
@media (max-width: 576px) {
3871
.list-group-item {
3972
font-size: 0.9rem;

src/assets/styles/overrides.css

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
1-
/* Change the color of all primary buttons to slateblue */
1+
button,
2+
.btn,
3+
.btn-primary,
4+
.searchButton {
5+
background-color: var(--primary-color) !important;
6+
border: 1px solid var(--primary-color) !important;
7+
color: var(--button-text-color) !important;
8+
border-radius: 6px;
9+
cursor: pointer;
10+
transition: background-color 0.3s ease, color 0.3s ease,
11+
border-color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease;
12+
}
13+
14+
button:hover,
15+
.btn:hover,
16+
.btn-primary:hover,
17+
.searchButton:hover {
18+
background-color: color-mix(
19+
in srgb,
20+
var(--primary-color) 85%,
21+
white
22+
) !important;
23+
border-color: color-mix(in srgb, var(--primary-color) 85%, white) !important;
24+
transform: translateY(-2px);
25+
}
26+
27+
button:active,
28+
.btn:active,
29+
.btn-primary:active,
30+
.searchButton:active,
31+
button:focus,
32+
.btn:focus,
33+
.btn-primary:focus,
34+
.searchButton:focus,
35+
button:focus-visible,
36+
.btn:focus-visible,
37+
.btn-primary:focus-visible,
38+
.searchButton:focus-visible {
39+
background-color: var(--primary-color) !important;
40+
border-color: var(--primary-color) !important;
41+
transform: translateY(0);
42+
box-shadow: 0 0 0 0.25rem
43+
color-mix(in srgb, var(--primary-color) 40%, transparent) !important;
44+
outline: none !important;
45+
}
46+
47+
.form-check-input:checked {
48+
background-color: var(--primary-color) !important;
49+
border-color: var(--primary-color) !important;
50+
}
251

3-
.btn-primary {
4-
background-color: var(--primary-color);
5-
border-color: var(--primary-color);
52+
.form-check-input:focus {
53+
box-shadow: 0 0 0 0.25rem
54+
color-mix(in srgb, var(--primary-color) 40%, transparent) !important;
655
}

0 commit comments

Comments
 (0)