Skip to content

Commit e639076

Browse files
Convert the Popular component to TypeScript.
1 parent d49d908 commit e639076

File tree

3 files changed

+173
-134
lines changed

3 files changed

+173
-134
lines changed

app/components/Popular.js

-134
This file was deleted.

app/components/Popular.tsx

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import { fetchPopularRepos, Repo } from "../utils/api";
4+
import {
5+
FaUser,
6+
FaStar,
7+
FaCodeBranch,
8+
FaExclamationTriangle,
9+
} from "react-icons/fa";
10+
import Card from "./Card";
11+
import Loading from "./Loading";
12+
import Tooltip from "./Tooltip";
13+
14+
type Languages = "All" | "JavaScript" | "Ruby" | "Java" | "CSS" | "Python";
15+
function LangaugesNav({
16+
selected,
17+
onUpdateLanguage,
18+
}: {
19+
selected: Languages;
20+
onUpdateLanguage: (lang: Languages) => void;
21+
}) {
22+
const languages: Languages[] = [
23+
"All",
24+
"JavaScript",
25+
"Ruby",
26+
"Java",
27+
"CSS",
28+
"Python",
29+
];
30+
31+
return (
32+
<ul className="flex-center">
33+
{languages.map((language) => (
34+
<li key={language}>
35+
<button
36+
className="btn-clear nav-link"
37+
style={
38+
language === selected ? { color: "rgb(187, 46, 31)" } : undefined
39+
}
40+
onClick={() => onUpdateLanguage(language)}
41+
>
42+
{language}
43+
</button>
44+
</li>
45+
))}
46+
</ul>
47+
);
48+
}
49+
50+
LangaugesNav.propTypes = {
51+
selected: PropTypes.string.isRequired,
52+
onUpdateLanguage: PropTypes.func.isRequired,
53+
};
54+
55+
function ReposGrid({ repos }: { repos: Repo[] }) {
56+
return (
57+
<ul className="grid space-around">
58+
{repos.map((repo, index) => {
59+
const {
60+
name,
61+
owner,
62+
html_url,
63+
stargazers_count,
64+
forks,
65+
open_issues,
66+
} = repo;
67+
const { login, avatar_url } = owner;
68+
69+
return (
70+
<li key={html_url}>
71+
<Card
72+
header={`#${index + 1}`}
73+
avatar={avatar_url}
74+
href={html_url}
75+
name={login}
76+
>
77+
<ul className="card-list">
78+
<li>
79+
<Tooltip text="Github username">
80+
<FaUser color="rgb(255, 191, 116)" size={22} />
81+
<a href={`https://github.com/${login}`}>{login}</a>
82+
</Tooltip>
83+
</li>
84+
<li>
85+
<FaStar color="rgb(255, 215, 0)" size={22} />
86+
{stargazers_count.toLocaleString()} stars
87+
</li>
88+
<li>
89+
<FaCodeBranch color="rgb(129, 195, 245)" size={22} />
90+
{forks.toLocaleString()} forks
91+
</li>
92+
<li>
93+
<FaExclamationTriangle color="rgb(241, 138, 147)" size={22} />
94+
{open_issues.toLocaleString()} open
95+
</li>
96+
</ul>
97+
</Card>
98+
</li>
99+
);
100+
})}
101+
</ul>
102+
);
103+
}
104+
105+
ReposGrid.propTypes = {
106+
repos: PropTypes.array.isRequired,
107+
};
108+
interface PopularState extends Partial<Record<Languages, Repo[]>> {
109+
error: null | string;
110+
}
111+
type PopularReducerActions =
112+
| { type: "success"; selectedLanguage: Languages; repos: Repo[] }
113+
| { type: "error"; error: Error };
114+
function popularReducer(state: PopularState, action: PopularReducerActions) {
115+
if (action.type === "success") {
116+
return {
117+
...state,
118+
[action.selectedLanguage]: action.repos,
119+
error: null,
120+
};
121+
} else if (action.type === "error") {
122+
return {
123+
...state,
124+
error: action.error.message,
125+
};
126+
} else {
127+
throw new Error(`That action type isn't supported.`);
128+
}
129+
}
130+
131+
export default function Popular() {
132+
const [selectedLanguage, setSelectedLanguage] = React.useState<Languages>(
133+
"All"
134+
);
135+
const [state, dispatch] = React.useReducer(popularReducer, { error: null });
136+
137+
const fetchedLanguages = React.useRef<string[]>([]);
138+
139+
React.useEffect(() => {
140+
if (fetchedLanguages.current.includes(selectedLanguage) === false) {
141+
fetchedLanguages.current.push(selectedLanguage);
142+
143+
fetchPopularRepos(selectedLanguage)
144+
.then((repos) => dispatch({ type: "success", selectedLanguage, repos }))
145+
.catch((error) => dispatch({ type: "error", error }));
146+
}
147+
}, [fetchedLanguages, selectedLanguage]);
148+
149+
const isLoading = () => !state[selectedLanguage] && state.error === null;
150+
151+
const selectedRepos = state[selectedLanguage];
152+
return (
153+
<React.Fragment>
154+
<LangaugesNav
155+
selected={selectedLanguage}
156+
onUpdateLanguage={setSelectedLanguage}
157+
/>
158+
159+
{isLoading() && <Loading text="Fetching Repos" />}
160+
161+
{state.error && <p className="center-text error">{state.error}</p>}
162+
163+
{selectedRepos && <ReposGrid repos={selectedRepos} />}
164+
</React.Fragment>
165+
);
166+
}

app/utils/api.ts

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ function getErrorMsg(message: string, username: string) {
1313
export interface User {
1414
id: string;
1515
followers: number;
16+
login: string;
17+
avatar_url: string;
1618
}
1719
function getProfile(username: string): Promise<User> {
1820
return fetch(`https://api.github.com/users/${username}${params}`)
@@ -28,6 +30,11 @@ function getProfile(username: string): Promise<User> {
2830

2931
export interface Repo {
3032
id: string;
33+
name: string;
34+
owner: User;
35+
html_url: string;
36+
forks: number;
37+
open_issues: number;
3138
stargazers_count: number;
3239
}
3340

0 commit comments

Comments
 (0)