Skip to content

Commit 349c936

Browse files
feat: render fetched repositories (#26)
1 parent 3a2f1dc commit 349c936

5 files changed

Lines changed: 191 additions & 17 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { QueryParamProvider } from "use-query-params";
66
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
77

88
import { RepoDetail } from "./components/repo-detail";
9+
import { OrgListing } from "./components/org-listing";
910
import { Home } from "./components/home";
1011
import { useProgressBar } from "./hooks";
1112

@@ -20,6 +21,7 @@ function App() {
2021
<QueryParamProvider ReactRouterRoute={Route}>
2122
<Switch>
2223
<Route exact path="/" component={Home} />
24+
<Route exact path="/:org/" component={OrgListing} />
2325
<Route path="/:owner/:name" component={RepoDetail} />
2426
</Switch>
2527
</QueryParamProvider>

src/api/index.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Endpoints } from "@octokit/types";
33
import store from "store2";
44
import YAML from "yaml";
55

6-
import { Repo } from "../types";
6+
import { Repo, Repository } from "../types";
77
import { csvParse, tsvParse } from "d3-dsv";
88

99
export type listCommitsResponse =
@@ -52,7 +52,15 @@ const getFilesFromRes = (res: any) => {
5252
.map((file: any) => file.path)
5353
.filter((path: string) => {
5454
const extension = path.split(".").pop() || "";
55-
const validExtensions = ["csv", "tsv", "json", "geojson", "topojson", "yml", "yaml"];
55+
const validExtensions = [
56+
"csv",
57+
"tsv",
58+
"json",
59+
"geojson",
60+
"topojson",
61+
"yml",
62+
"yaml",
63+
];
5664
return (
5765
validExtensions.includes(extension) &&
5866
!ignoredFiles.includes(path.split("/").slice(-1)[0]) &&
@@ -129,7 +137,15 @@ export function fetchDataFile(params: FileParamsWithSHA) {
129137
const { filename, name, owner, sha } = params;
130138
if (!filename) return [];
131139
const fileType = filename.split(".").pop() || "";
132-
const validTypes = ["csv", "tsv", "json", "geojson", "topojson", "yml", "yaml"];
140+
const validTypes = [
141+
"csv",
142+
"tsv",
143+
"json",
144+
"geojson",
145+
"topojson",
146+
"yml",
147+
"yaml",
148+
];
133149
if (!validTypes.includes(fileType)) return [];
134150

135151
return wretch()
@@ -145,24 +161,27 @@ export function fetchDataFile(params: FileParamsWithSHA) {
145161
try {
146162
if (fileType === "csv") {
147163
data = csvParse(res);
148-
} else if (["geojson", "topojson"].includes(fileType) || filename.endsWith(".geo.json")) {
164+
} else if (
165+
["geojson", "topojson"].includes(fileType) ||
166+
filename.endsWith(".geo.json")
167+
) {
149168
data = JSON.parse(res);
150169
if (data.features) {
151170
const features = data.features.map((feature: any) => {
152-
let geometry = {} as Record<string, any>
153-
Object.keys(feature?.geometry).forEach(key => {
171+
let geometry = {} as Record<string, any>;
172+
Object.keys(feature?.geometry).forEach((key) => {
154173
geometry[`geometry.${key}`] = feature.geometry[key];
155-
})
156-
let properties = {} as Record<string, any>
157-
Object.keys(feature?.properties).forEach(key => {
174+
});
175+
let properties = {} as Record<string, any>;
176+
Object.keys(feature?.properties).forEach((key) => {
158177
properties[`properties.${key}`] = feature.properties[key];
159-
})
160-
const {geometry: g, properties: p, ...restOfKeys} = feature;
161-
return {...restOfKeys, ...geometry, ...properties};
162-
})
178+
});
179+
const { geometry: g, properties: p, ...restOfKeys } = feature;
180+
return { ...restOfKeys, ...geometry, ...properties };
181+
});
163182
// make features the first key of the object
164-
const { features: f, ...restOfData } = data
165-
data = { features, ...restOfData }
183+
const { features: f, ...restOfData } = data;
184+
data = { features, ...restOfData };
166185
}
167186
} else if (fileType === "json") {
168187
data = JSON.parse(res);
@@ -246,6 +265,16 @@ export function fetchDataFile(params: FileParamsWithSHA) {
246265
});
247266
}
248267

268+
export async function fetchOrgRepos(orgName: string) {
269+
const res = await githubWretch
270+
.url(`/search/repositories`)
271+
.query({ q: `topic:flat-data org:${orgName}`, per_page: 100 })
272+
.get()
273+
.json();
274+
275+
return res.items;
276+
}
277+
249278
const stringifyValue = (data: any) => {
250279
if (typeof data === "object") return JSON.stringify(data, undefined, 2);
251280
return data.toString();

src/components/org-listing.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React from "react";
2+
import { RouteComponentProps } from "react-router";
3+
import { Link } from "react-router-dom";
4+
import formatDistance from "date-fns/formatDistance";
5+
import { GoStar } from "react-icons/go";
6+
7+
import { useOrgFlatRepos } from "../hooks";
8+
import { ErrorState } from "./error-state";
9+
import { Spinner } from "./spinner";
10+
import Bug from "../bug.svg";
11+
import { Repository } from "../types";
12+
13+
interface OrgListingProps extends RouteComponentProps<{ org: string }> {}
14+
15+
interface RepoListingProps {
16+
repos: Repository[];
17+
org: string;
18+
}
19+
20+
function RepoListing(props: RepoListingProps) {
21+
return (
22+
<ul className="divide-y rounded-xl overflow-hidden">
23+
{props.repos.map((repo) => {
24+
const lastUpdated = formatDistance(
25+
Date.parse(repo.updated_at),
26+
new Date(),
27+
{
28+
addSuffix: true,
29+
}
30+
);
31+
32+
return (
33+
<li className="bg-white hover:bg-opacity-70" key={repo.name}>
34+
<Link className="p-4 block" to={`/${props.org}/${repo.name}`}>
35+
<span className="text-indigo-600 font-medium">
36+
{props.org}/{repo.name}
37+
</span>
38+
<div className="mt-1 mb-2 text-lg">
39+
<p>{repo.description}</p>
40+
</div>
41+
<ul className="flex items-center space-x-3 text-sm text-gray-600">
42+
<li className="flex space-x-1 items-center">
43+
<GoStar className="text-gray-400" />
44+
<span>{repo.stargazers_count}</span>
45+
</li>
46+
<li>{repo.language}</li>
47+
{Boolean(repo.license) && <li>{repo.license.name}</li>}
48+
<li>Updated {lastUpdated}</li>
49+
</ul>
50+
</Link>
51+
</li>
52+
);
53+
})}
54+
</ul>
55+
);
56+
}
57+
58+
export function OrgListing(props: OrgListingProps) {
59+
const { match } = props;
60+
const { org } = match.params;
61+
const { data = [], status } = useOrgFlatRepos(org);
62+
63+
return (
64+
<div className="flex flex-col h-full">
65+
<div className="p-4 bg-indigo-600 flex-shrink-0">
66+
<header className="flex justify-between items-center">
67+
<div className="text-indigo-100 font-light text-sm">
68+
<strong className="font-bold">Flat Viewer</strong> a simple tool for
69+
exploring flat data files in GitHub repositories.
70+
</div>
71+
</header>
72+
</div>
73+
{status === "loading" && (
74+
<div className="flex items-center justify-center h-full">
75+
<div className="max-w-sm flex items-center justify-center space-x-4">
76+
<Spinner />
77+
<p>Loading organization...</p>
78+
</div>
79+
</div>
80+
)}
81+
{status === "success" &&
82+
(data.length > 0 ? (
83+
<div className="p-4 overflow-scroll">
84+
<div className="mb-4">
85+
Repositories tagged{" "}
86+
<span className="bg-indigo-600 text-white text-xs p-1 rounded">
87+
flat-data
88+
</span>{" "}
89+
in the <span className="font-medium">{org}</span> organization.
90+
</div>
91+
<RepoListing repos={data} org={org} />
92+
</div>
93+
) : (
94+
<ErrorState img={Bug} alt="Bug icon">
95+
<div className="max-w-sm mx-auto">
96+
Hmm, we couldn't find any repos with the topic{" "}
97+
<span className="bg-indigo-600 text-white text-xs p-1 rounded">
98+
flat-data
99+
</span>{" "}
100+
in this organization
101+
</div>
102+
</ErrorState>
103+
))}
104+
{status === "error" && (
105+
<div className="flex items-center justify-center h-full">
106+
<ErrorState img={Bug} alt="Bug icon">
107+
Hmm, we could not load the organization.
108+
</ErrorState>
109+
</div>
110+
)}
111+
</div>
112+
);
113+
}

src/hooks/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
FileParamsWithSHA,
1010
listCommitsResponse,
1111
fetchFilesFromRepo,
12+
fetchOrgRepos,
1213
} from "../api";
13-
import { Repo, FlatDataTab } from "../types";
14+
import { Repo, FlatDataTab, Repository } from "../types";
1415
import React from "react";
1516

1617
// Hooks
@@ -70,3 +71,14 @@ export function useGetFiles(
7071
}
7172
);
7273
}
74+
75+
export function useOrgFlatRepos(
76+
orgName: string,
77+
config?: UseQueryOptions<Repository[]>
78+
) {
79+
return useQuery(["org", orgName], () => fetchOrgRepos(orgName), {
80+
retry: false,
81+
refetchOnWindowFocus: false,
82+
...config,
83+
});
84+
}

src/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Endpoints } from "@octokit/types";
22

3-
export type Commit = Endpoints["GET /repos/{owner}/{repo}/commits"]["response"]["data"][0];
3+
export type Commit =
4+
Endpoints["GET /repos/{owner}/{repo}/commits"]["response"]["data"][0];
45

56
export type Repo = {
67
owner: string;
@@ -12,3 +13,20 @@ export interface FlatDataTab {
1213
value?: object[];
1314
invalidValue?: string;
1415
}
16+
17+
interface RepositoryLicense {
18+
key: string;
19+
name: string;
20+
url: string;
21+
}
22+
23+
export interface Repository {
24+
name: string;
25+
description: string;
26+
id: string;
27+
topics?: string[];
28+
stargazers_count: number;
29+
language: string;
30+
updated_at: string;
31+
license: RepositoryLicense;
32+
}

0 commit comments

Comments
 (0)