Skip to content

Commit 02ffaf4

Browse files
Merge branch 'main' into bug
2 parents 0df3a6b + 9ec382f commit 02ffaf4

7 files changed

Lines changed: 644 additions & 891 deletions

File tree

README.md

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**ReactVerse** is an open-source API dashboard built with **React + Vite**, designed to help developers explore, visualize, and learn how to integrate public APIs safely and responsibly.
44
It’s more than just a project — it’s a **community-driven learning space** for understanding how APIs work, managing data securely, and contributing meaningfully to open source.
55

6-
Perfect for **Hacktoberfest**, **beginners**, and **API enthusiasts** who want to get hands-on experience using real-world APIs.
6+
Perfect for **beginners**, and **API enthusiasts** who want to get hands-on experience using real-world APIs.
77

88
---
99

@@ -76,22 +76,6 @@ We welcome contributions of all levels — from design tweaks to feature enhance
7676

7777
---
7878

79-
## 📚 API Reference
80-
81-
| Category | API | Documentation |
82-
| -------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
83-
| Weather | wttr.in | [https://wttr.in/:help](https://wttr.in/:help) |
84-
| Crypto | CoinGecko | [https://www.coingecko.com/en/api](https://www.coingecko.com/en/api) |
85-
| Space | Open Notify | [http://open-notify.org/Open-Notify-API/](http://open-notify.org/Open-Notify-API/) |
86-
| Movies | Studio Ghibli | [https://ghibliapi.vercel.app/](https://ghibliapi.vercel.app/) |
87-
| Recipes | TheMealDB | [https://www.themealdb.com/api.php](https://www.themealdb.com/api.php) |
88-
| Trivia | Open Trivia DB | [https://opentdb.com/api_config.php](https://opentdb.com/api_config.php) |
89-
| Jokes | Official Joke API | [https://official-joke-api.appspot.com/](https://official-joke-api.appspot.com/) |
90-
| Quotes | Quotable | [https://github.com/lukePeavey/quotable](https://github.com/lukePeavey/quotable) |
91-
| Pets | Dog CEO / Cataas | [https://dog.ceo/dog-api/](https://dog.ceo/dog-api/) / [https://cataas.com/#/](https://cataas.com/#/) |
92-
| COVID-19 | COVID19 API | [https://covid19api.com/](https://covid19api.com/) |
93-
94-
---
9579

9680
## 🪪 License
9781

src/App.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default function App() {
6565
<Route path="/trivia" element={<Trivia />} />
6666
<Route path="/jokes-quotes" element={<JokesQuotes />} />
6767
<Route path="/pets" element={<Pets />} />
68-
<Route path="/covid" element={<Covid />} />
68+
<Route path="/covid" element={<Covid />} />
6969
</Routes>
7070
</main>
7171
</div>

src/components/Skeleton.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
3+
const Skeleton = ({width, height}) => {
4+
return (
5+
<span className="skeleton" style={{width:`${width}`, height:`${height}`}}></span>
6+
)
7+
}
8+
9+
export default Skeleton;

src/pages/Covid.jsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* - [ ] Offline cache last fetch
1818
* - [ ] Extract service + hook (useCovidSummary, useCountryTrends)
1919
*/
20-
import { useEffect, useState } from 'react';
20+
import { useEffect, useState, useCallback } from 'react';
2121
import Loading from '../components/Loading.jsx';
2222
import ErrorMessage from '../components/ErrorMessage.jsx';
2323
import Card from '../components/Card.jsx';
@@ -28,26 +28,39 @@ export default function Covid() {
2828
const [error, setError] = useState(null);
2929
const [country, setCountry] = useState('');
3030

31-
useEffect(() => { fetchSummary(); }, []);
32-
33-
async function fetchSummary() {
31+
const fetchSummary = useCallback(async () => {
32+
if (loading) return;
3433
try {
35-
setLoading(true); setError(null);
34+
setLoading(true);
35+
setError(null);
3636
const res = await fetch('https://api.covid19api.com/summary');
3737
if (!res.ok) throw new Error('Failed to fetch');
3838
const json = await res.json();
3939
setSummary(json);
40-
} catch (e) { setError(e); } finally { setLoading(false); }
41-
}
40+
} catch (e) {
41+
setError(e);
42+
} finally {
43+
setLoading(false);
44+
}
45+
}, [loading]);
46+
47+
useEffect(() => {
48+
fetchSummary();
49+
}, []);
4250

4351
const global = summary?.Global;
4452
const countries = summary?.Countries || [];
4553
const selected = countries.find(c => c.Slug === country);
4654

4755
return (
4856
<div>
57+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
4958
<h2>COVID-19 Tracker</h2>
50-
{loading && <Loading />}
59+
<button onClick={fetchSummary} disabled={loading}>
60+
{loading ? 'Refreshing...' : 'Refresh'}
61+
</button>
62+
</div>
63+
{loading && !summary && <Loading />}
5164
<ErrorMessage error={error} />
5265
{global && (
5366
<Card title="Global Stats">

src/pages/Trivia.jsx

Lines changed: 179 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,62 +28,216 @@ export default function Trivia() {
2828
const [loading, setLoading] = useState(false);
2929
const [error, setError] = useState(null);
3030
const [score, setScore] = useState(0);
31+
const [showReview, setShowReview] = useState(false);
3132

32-
useEffect(() => { fetchQuestions(); }, [category]);
33+
useEffect(() => {
34+
fetchQuestions();
35+
}, [category]);
3336

3437
async function fetchQuestions() {
3538
try {
36-
setLoading(true); setError(null); setScore(0);
37-
const res = await fetch(`https://opentdb.com/api.php?amount=5&category=${category}&type=multiple`);
39+
setLoading(true);
40+
setError(null);
41+
setScore(0);
42+
setShowReview(false);
43+
44+
const res = await fetch(
45+
`https://opentdb.com/api.php?amount=5&category=${category}&type=multiple`
46+
);
3847
if (!res.ok) throw new Error('Failed to fetch');
3948
const json = await res.json();
40-
const qs = json.results.map(q => ({
49+
50+
const qs = json.results.map((q) => ({
4151
...q,
4252
answers: shuffle([q.correct_answer, ...q.incorrect_answers]),
43-
picked: null
53+
picked: null,
4454
}));
4555
setQuestions(qs);
46-
} catch (e) { setError(e); } finally { setLoading(false); }
56+
} catch (e) {
57+
setError(e);
58+
} finally {
59+
setLoading(false);
60+
}
4761
}
4862

49-
function shuffle(arr) { return arr.sort(() => Math.random() - 0.5); }
63+
function shuffle(arr) {
64+
return arr.sort(() => Math.random() - 0.5);
65+
}
5066

5167
function pick(qIndex, answer) {
52-
setQuestions(qs => qs.map((q,i) => i===qIndex ? { ...q, picked: answer } : q));
53-
if (questions[qIndex].correct_answer === answer) setScore(s => s+1);
68+
setQuestions((qs) =>
69+
qs.map((q, i) => (i === qIndex ? { ...q, picked: answer } : q))
70+
);
71+
if (questions[qIndex].correct_answer === answer) {
72+
setScore((s) => s + 1);
73+
}
74+
}
75+
76+
function decodeHtml(html) {
77+
const txt = document.createElement('textarea');
78+
txt.innerHTML = html;
79+
return txt.value;
5480
}
5581

82+
const answeredCount = questions.filter((q) => q.picked !== null).length;
83+
const totalQuestions = questions.length;
84+
const progressPercent =
85+
totalQuestions > 0 ? (answeredCount / totalQuestions) * 100 : 0;
86+
87+
const allAnswered = answeredCount === totalQuestions && totalQuestions > 0;
88+
5689
return (
57-
<div>
90+
<div style={{ padding: '20px' }}>
5891
<h2>Trivia Quiz</h2>
59-
<label>Category:
60-
<select value={category} onChange={e => setCategory(e.target.value)}>
92+
93+
{/* Category Selector */}
94+
<label>
95+
Category:{' '}
96+
<select
97+
value={category}
98+
onChange={(e) => setCategory(e.target.value)}
99+
disabled={showReview}
100+
>
61101
<option value="18">Science: Computers</option>
62102
<option value="21">Sports</option>
63103
<option value="23">History</option>
64104
</select>
65105
</label>
106+
107+
{/* Loading / Error */}
66108
{loading && <Loading />}
67109
<ErrorMessage error={error} />
68-
<p>Score: {score}</p>
110+
111+
{/* Progress Bar */}
112+
{totalQuestions > 0 && (
113+
<div style={{ margin: '15px 0' }}>
114+
<p>
115+
Progress: {answeredCount} / {totalQuestions} answered
116+
</p>
117+
<div
118+
style={{
119+
height: '10px',
120+
width: '100%',
121+
background: '#ddd',
122+
borderRadius: '8px',
123+
overflow: 'hidden',
124+
}}
125+
>
126+
<div
127+
style={{
128+
width: `${progressPercent}%`,
129+
height: '100%',
130+
background: '#4caf50',
131+
transition: 'width 0.3s ease',
132+
}}
133+
></div>
134+
</div>
135+
</div>
136+
)}
137+
138+
{/* Score + Review Button */}
139+
<div
140+
style={{
141+
display: 'flex',
142+
alignItems: 'center',
143+
gap: '10px',
144+
marginBottom: '15px',
145+
}}
146+
>
147+
<p style={{ margin: 0, fontWeight: 'bold' }}>Score: {score}</p>
148+
<button
149+
onClick={() => setShowReview(true)}
150+
disabled={!allAnswered || showReview}
151+
style={{
152+
background: allAnswered ? '#007bff' : '#ccc',
153+
color: '#fff',
154+
padding: '6px 12px',
155+
border: 'none',
156+
borderRadius: '6px',
157+
cursor: allAnswered && !showReview ? 'pointer' : 'not-allowed',
158+
}}
159+
>
160+
Review Answers
161+
</button>
162+
</div>
163+
164+
{/* Quiz Cards */}
69165
{questions.map((q, idx) => (
70-
<Card key={idx} title={`Q${idx+1}: ${decodeHtml(q.question)}`}>
166+
<Card key={idx} title={`Q${idx + 1}: ${decodeHtml(q.question)}`}>
71167
<ul>
72-
{q.answers.map(a => (
73-
<li key={a}>
74-
<button disabled={q.picked} className={a===q.correct_answer ? (q.picked && a===q.picked ? 'correct' : '') : (q.picked===a ? 'wrong':'')} onClick={() => pick(idx, a)}>{decodeHtml(a)}</button>
75-
</li>
76-
))}
168+
{q.answers.map((a) => {
169+
const isPicked = q.picked === a;
170+
const isCorrect = a === q.correct_answer;
171+
let btnClass = '';
172+
173+
if (showReview) {
174+
btnClass = isCorrect
175+
? 'correct'
176+
: isPicked
177+
? 'wrong'
178+
: 'neutral';
179+
} else if (isPicked) {
180+
btnClass = isCorrect ? 'correct' : 'wrong';
181+
}
182+
183+
return (
184+
<li key={a}>
185+
<button
186+
disabled={!!q.picked || showReview}
187+
onClick={() => pick(idx, a)}
188+
style={{
189+
margin: '5px',
190+
padding: '8px 12px',
191+
borderRadius: '6px',
192+
cursor: q.picked || showReview ? 'default' : 'pointer',
193+
border:
194+
btnClass === 'correct'
195+
? '2px solid green'
196+
: btnClass === 'wrong'
197+
? '2px solid red'
198+
: '1px solid #ccc',
199+
background:
200+
btnClass === 'correct'
201+
? '#c8e6c9'
202+
: btnClass === 'wrong'
203+
? '#ffcdd2'
204+
: '#fff',
205+
color: 'black', // Always black text
206+
fontWeight: '500',
207+
}}
208+
>
209+
{decodeHtml(a)}
210+
</button>
211+
</li>
212+
);
213+
})}
77214
</ul>
78215
</Card>
79216
))}
80-
{/* TODO: Add scoreboard persistence */}
217+
218+
{/* Review Section */}
219+
{showReview && (
220+
<div style={{ marginTop: '20px', textAlign: 'center' }}>
221+
<h3>🎯 Quiz Complete!</h3>
222+
<p>
223+
Final Score: {score} / {totalQuestions}
224+
</p>
225+
<button
226+
onClick={fetchQuestions}
227+
style={{
228+
background: '#007bff',
229+
color: '#fff',
230+
padding: '10px 16px',
231+
border: 'none',
232+
borderRadius: '8px',
233+
cursor: 'pointer',
234+
}}
235+
>
236+
Play Again
237+
</button>
238+
</div>
239+
)}
81240
</div>
82241
);
83242
}
84243

85-
function decodeHtml(html) {
86-
const txt = document.createElement('textarea');
87-
txt.innerHTML = html;
88-
return txt.value;
89-
}

0 commit comments

Comments
 (0)