Skip to content
Merged
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
113 changes: 107 additions & 6 deletions src/pages/Contributors/Contributors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,45 @@ interface Contributor {
html_url: string;
}

interface FetchError {
message: string;
isRateLimited: boolean;
statusCode?: number;
}

// Custom error class for Contributors fetch errors
class ContributorsError extends Error {
constructor(
message: string,
public isRateLimited = false,
public statusCode?: number
) {
super(message);
this.name = "ContributorsError";
}
}

// Type guard to validate if data is Contributor[]
const isContributorArray = (data: unknown): data is Contributor[] => {
if (!Array.isArray(data)) return false;
return data.every((item) => {
if (typeof item !== "object" || item === null) return false;

// Validate all required fields with correct types
return (
typeof item.id === "number" &&
typeof item.login === "string" &&
typeof item.avatar_url === "string" &&
typeof item.contributions === "number" &&
typeof item.html_url === "string"
);
});
};
Comment thread
Tanayajadhav1 marked this conversation as resolved.

const ContributorsPage = () => {
const [contributors, setContributors] = useState<Contributor[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [copiedId, setCopiedId] = useState<number | null>(null);
const [error, setError] = useState<FetchError | null>(null);

const handleCopy = async (contributor: Contributor) => {
await navigator.clipboard.writeText(contributor.html_url);
Expand All @@ -43,12 +77,60 @@ const handleCopy = async (contributor: Contributor) => {
useEffect(() => {
const fetchContributors = async () => {
try {
setLoading(true);
setError(null);

const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, {
withCredentials: false,
timeout: 10000,
});

// ✅ Validate response structure matches Contributor[]
if (!isContributorArray(response.data)) {
throw new ContributorsError(
"Invalid API response structure. Expected array of contributors.",
false
);
}

setContributors(response.data);
} catch {
setError("Failed to fetch contributors. Please try again later.");
} catch (err) {
const fetchError: FetchError = {
message: "Failed to fetch contributors. Please try again later.",
isRateLimited: false,
};

// Handle ContributorsError instances
if (err instanceof ContributorsError) {
fetchError.message = err.message;
fetchError.isRateLimited = err.isRateLimited;
fetchError.statusCode = err.statusCode;
} else if (axios.isAxiosError(err)) {
// Handle Axios errors
if (err.response?.status === 403) {
fetchError.message =
"GitHub API rate limit exceeded. Try again later.";
fetchError.isRateLimited = true;
fetchError.statusCode = 403;
} else if (err.response?.status === 404) {
Comment thread
Tanayajadhav1 marked this conversation as resolved.
Comment thread
Tanayajadhav1 marked this conversation as resolved.
fetchError.message = "Repository not found.";
fetchError.statusCode = 404;
} else if (err.code === "ECONNABORTED") {
fetchError.message = "Request timeout. Server took too long to respond.";
fetchError.statusCode = 408;
} else if (err.response?.status) {
fetchError.message = `HTTP ${err.response.status}: Failed to fetch contributors`;
fetchError.statusCode = err.response.status;
} else if (err.message) {
fetchError.message = err.message;
}
} else if (err instanceof Error) {
fetchError.message = err.message || fetchError.message;
}

setError(fetchError);
console.error("Contributors fetch error:", fetchError);
setContributors([]);
} finally {
setLoading(false);
}
Expand All @@ -67,8 +149,27 @@ const handleCopy = async (contributor: Contributor) => {

if (error) {
return (
<Box sx={{ mt: 4 }}>
<Alert severity="error">{error}</Alert>
<Box sx={{ mt: 4, mx: 2 }}>
<Alert severity="error" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: "bold" }}>
⚠️ {error.message}
</Typography>
Comment thread
Tanayajadhav1 marked this conversation as resolved.
{error.isRateLimited && (
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
You've hit GitHub's API rate limit. The limit resets in 1 hour.
</Typography>
)}
{error.statusCode === 404 && (
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
Please verify the repository exists and is accessible.
</Typography>
)}
{error.statusCode === 408 && (
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
The server took too long to respond. Please try again.
</Typography>
)}
</Alert>
</Box>
);
}
Expand Down
Loading