Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 110 additions & 6 deletions src/App.tsx
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}`);
}
Comment on lines +51 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Surface partial-org failures instead of silently dropping them.

Rejected org fetches are filtered out, but error is only set when every org fails. That lets the dashboard look complete while one or more organizations are missing. Wrap each promise with its org name, track the rejected orgs, and show a non-blocking warning alongside the partial results.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 37 - 49, The current logic drops rejected org
fetches and only sets error when all orgs fail; modify the fetch flow that uses
ORGS and fetchOrgRepos so each promise is paired with its org name (e.g., map
ORGS to {org, promise}) and after Promise.allSettled inspect results to build
two things: (1) successfulRepos (as you already do) and call
setRepos(successfulRepos), and (2) a list of failedOrgs derived from rejected
results; introduce or reuse a state like setWarning/setPartialFetchWarning and
set a non-blocking warning message describing failedOrgs (instead of only
calling setError when successfulRepos.length === 0), so partial results are
shown while failed orgs are surfaced to the UI.

} catch (err) {
setError(err instanceof Error ? err.message : UI_STRINGS.UNKNOWN_ERROR);
} finally {
setLoading(false);
}
};

fetchAllData();
}, []);
Comment on lines +16 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding AbortController for fetch cleanup.

The useEffect lacks cleanup logic to abort pending fetch requests if the component unmounts mid-fetch. While less critical for the root App component, this is a React best practice that prevents potential memory leaks and "setState on unmounted component" warnings.

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
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 16 - 75, The effect should use an AbortController
to cancel in-flight fetches on unmount: create const controller = new
AbortController() in the useEffect, pass controller.signal into fetchOrgRepos
(add a signal param to fetchOrgRepos and include it in the fetch options), and
return a cleanup function that calls controller.abort(). Inside fetchOrgRepos
and fetchAllData, handle aborts by checking controller.signal.aborted (or
catching an error with name === 'AbortError') and bail out early so you don't
call setLoading, setRepos, or setError after unmount; also, before applying
results after Promise.allSettled in fetchAllData, check
controller.signal.aborted and return if true. Ensure references: fetchOrgRepos,
fetchAllData, ORGS, setLoading, setRepos, setError, and useEffect are updated
accordingly.


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;
42 changes: 42 additions & 0 deletions src/components/RepositoryCard.tsx
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>
);
};
10 changes: 10 additions & 0 deletions src/constants/strings.ts
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",
};
12 changes: 12 additions & 0 deletions src/types/github.ts
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;
};
}
Loading
Loading