-
-
Notifications
You must be signed in to change notification settings - Fork 38
feat: implement initial repository aggregator with TypeScript and modular architecture #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8044abf
f24e441
776f727
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,116 @@ | ||
| import './App.css' | ||
| import { useEffect, useState } from 'react'; | ||
| import { type Repository } from './types/github'; | ||
| import { RepositoryCard } from './components/RepositoryCard'; | ||
| import { UI_STRINGS } from './constants/strings'; | ||
|
|
||
| const ORGS = ['AOSSIE-Org', 'StabilityNexus', 'DjedAlliance']; | ||
|
|
||
|
|
||
| const GITHUB_TOKEN = import.meta.env.VITE_GITHUB_TOKEN; | ||
|
|
||
| function App() { | ||
| const [repos, setRepos] = useState<Repository[]>([]); | ||
| const [loading, setLoading] = useState<boolean>(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
|
|
||
| const fetchOrgRepos = async (org: string): Promise<Repository[]> => { | ||
| let allOrgRepos: Repository[] = []; | ||
| let page = 1; | ||
| let hasNextPage = true; | ||
|
|
||
| while (hasNextPage) { | ||
| const response = await fetch( | ||
| `https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}`, | ||
| { | ||
| headers: GITHUB_TOKEN ? { Authorization: `token ${GITHUB_TOKEN}` } : {}, | ||
| } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`${response.status} while fetching ${org}`); | ||
| } | ||
|
|
||
| const data: Repository[] = await response.json(); | ||
| allOrgRepos = [...allOrgRepos, ...data]; | ||
|
|
||
| if (data.length === 100) { | ||
| page++; | ||
| } else { | ||
| hasNextPage = false; | ||
| } | ||
| } | ||
| return allOrgRepos; | ||
| }; | ||
|
|
||
| const fetchAllData = async () => { | ||
| try { | ||
| setLoading(true); | ||
|
|
||
| const results = await Promise.allSettled(ORGS.map(org => fetchOrgRepos(org))); | ||
|
|
||
| const successfulRepos = results | ||
| .filter((result): result is PromiseFulfilledResult<Repository[]> => result.status === 'fulfilled') | ||
| .map(result => result.value) | ||
| .flat() | ||
| .sort((a, b) => b.stargazers_count - a.stargazers_count); | ||
|
|
||
| setRepos(successfulRepos); | ||
|
|
||
| if (successfulRepos.length === 0) { | ||
| const failureReasons = results | ||
| .filter((r): r is PromiseRejectedResult => r.status === 'rejected') | ||
| .map(r => r.reason.message).join(', '); | ||
| setError(`Failed to fetch data: ${failureReasons}`); | ||
| } | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : UI_STRINGS.UNKNOWN_ERROR); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| fetchAllData(); | ||
| }, []); | ||
|
Comment on lines
+16
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding AbortController for fetch cleanup. The Suggested pattern useEffect(() => {
+ const controller = new AbortController();
+
const fetchOrgRepos = async (org: string): Promise<Repository[]> => {
// ...
const response = await fetch(
`https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}`,
{
headers: GITHUB_TOKEN ? { Authorization: `token ${GITHUB_TOKEN}` } : {},
+ signal: controller.signal,
}
);
// ...
};
// ... fetchAllData ...
fetchAllData();
+
+ return () => controller.abort();
}, []);🤖 Prompt for AI Agents |
||
|
|
||
| if (loading) return ( | ||
| <div style={styles.centerContainer}> | ||
| <h2>{UI_STRINGS.LOADING}</h2> | ||
| </div> | ||
| ); | ||
|
|
||
| if (error) return ( | ||
| <div style={styles.errorText}> | ||
| {UI_STRINGS.ERROR_PREFIX} {error} | ||
| </div> | ||
| ); | ||
|
|
||
| return ( | ||
| <> | ||
| <h1>Hello, OrgExplorer!</h1> | ||
| </> | ||
| ) | ||
| <div style={styles.pageContainer}> | ||
| <header style={styles.header}> | ||
| <h1 style={{ fontSize: '2.5rem', margin: '0' }}>{UI_STRINGS.DASHBOARD_TITLE}</h1> | ||
| <p style={{ color: '#aaa' }}> | ||
| {UI_STRINGS.SUBTITLE_PREFIX} {repos.length} {UI_STRINGS.SUBTITLE_SUFFIX} | ||
| </p> | ||
| </header> | ||
|
|
||
| <div style={styles.grid}> | ||
| {repos.map(repo => ( | ||
| <RepositoryCard key={repo.id} repo={repo} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default App | ||
|
|
||
| const styles = { | ||
| centerContainer: { display: 'flex' as const, justifyContent: 'center' as const, alignItems: 'center' as const, height: '100vh', backgroundColor: '#242424', color: 'white' }, | ||
| errorText: { color: 'red', textAlign: 'center' as const, padding: '50px', backgroundColor: '#242424', minHeight: '100vh' }, | ||
| pageContainer: { padding: '40px', backgroundColor: '#242424', color: 'white', minHeight: '100vh' }, | ||
| header: { marginBottom: '40px', borderBottom: '1px solid #444', paddingBottom: '20px' }, | ||
| grid: { display: 'grid' as const, gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '25px' } | ||
| }; | ||
|
|
||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import type { Repository } from '../types/github'; | ||
| import { UI_STRINGS } from '../constants/strings'; | ||
|
|
||
| interface Props { | ||
| repo: Repository; | ||
| } | ||
|
|
||
| export const RepositoryCard = ({ repo }: Props) => { | ||
| return ( | ||
| <div style={{ | ||
| border: '1px solid #333', | ||
| padding: '20px', | ||
| borderRadius: '12px', | ||
| backgroundColor: '#1a1a1a', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| justifyContent: 'space-between' | ||
| }}> | ||
| <div> | ||
| <span style={{ fontSize: '0.7rem', color: '#ffd700', textTransform: 'uppercase' }}> | ||
| {repo.owner.login} | ||
| </span> | ||
| <h3 style={{ margin: '5px 0 10px 0' }}> | ||
| <a href={repo.html_url} target="_blank" rel="noreferrer" style={{ color: '#646cff', textDecoration: 'none' }}> | ||
| {repo.name} | ||
| </a> | ||
| </h3> | ||
| <p style={{ fontSize: '0.9rem', color: '#ccc', height: '45px', overflow: 'hidden' }}> | ||
| {repo.description || UI_STRINGS.NO_DESCRIPTION} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div style={{ marginTop: '15px', display: 'flex', justifyContent: 'space-between', fontSize: '0.8rem', color: '#aaa' }}> | ||
| <div> | ||
| <span style={{ marginRight: '10px' }}>⭐ {repo.stargazers_count}</span> | ||
| <span>🍴 {repo.forks_count}</span> | ||
| </div> | ||
| <span style={{ fontWeight: 'bold' }}>{repo.language || UI_STRINGS.FALLBACK_LANGUAGE}</span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export const UI_STRINGS = { | ||
| DASHBOARD_TITLE: "AOSSIE Org Explorer", | ||
| LOADING: "Loading AOSSIE Ecosystem...", | ||
| ERROR_PREFIX: "Error: ", | ||
| UNKNOWN_ERROR: "An unknown error occurred", | ||
| SUBTITLE_PREFIX: "Aggregating ", | ||
| SUBTITLE_SUFFIX: " repositories across foundational organizations", | ||
| NO_DESCRIPTION: "No description provided.", | ||
| FALLBACK_LANGUAGE: "Docs", | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| export interface Repository { | ||
| id: number; | ||
| name: string; | ||
| description: string | null; | ||
| html_url: string; | ||
| stargazers_count: number; | ||
| forks_count: number; | ||
| language: string | null; | ||
| owner: { | ||
| login: string; | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surface partial-org failures instead of silently dropping them.
Rejected org fetches are filtered out, but
erroris only set when every org fails. That lets the dashboard look complete while one or more organizations are missing. Wrap each promise with itsorgname, track the rejected orgs, and show a non-blocking warning alongside the partial results.🤖 Prompt for AI Agents