Skip to content
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

Add git contributors page #464

Merged
merged 1 commit into from
Nov 6, 2024
Merged
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
152 changes: 152 additions & 0 deletions frontend/src/components/Contributors.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react';
import { Github, Loader2 } from 'lucide-react';

const Contributors = () => {
const [contributors, setContributors] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchContributors = async () => {
try {
setLoading(true);
const response = await fetch('https://api.github.com/repos/RamakrushnaBiswal/PlayCafe/contributors');
if (!response.ok) {
throw new Error('Failed to fetch contributors');
}
const data = await response.json();

// Fetch additional user details for each contributor
const contributorsWithDetails = await Promise.all(
data.map(async (contributor) => {
const userResponse = await fetch(contributor.url);
const userData = await userResponse.json();
return {
...contributor,
name: userData.name || userData.login,
bio: userData.bio,
location: userData.location,
company: userData.company
};
})
);

setContributors(contributorsWithDetails);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

fetchContributors();
}, []);
Comment on lines +9 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Improve API fetching strategy and error handling.

The current implementation has several issues that need to be addressed:

  1. Multiple API calls (one per contributor) could quickly hit GitHub's rate limits
  2. No error handling for individual user data fetches
  3. Hardcoded repository URL
  4. Missing cleanup for potential race conditions

Here's a suggested improvement:

 useEffect(() => {
+  const controller = new AbortController();
+  const REPO_OWNER = 'RamakrushnaBiswal';
+  const REPO_NAME = 'PlayCafe';
+
   const fetchContributors = async () => {
     try {
       setLoading(true);
-      const response = await fetch('https://api.github.com/repos/RamakrushnaBiswal/PlayCafe/contributors');
+      const response = await fetch(
+        `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contributors`,
+        { signal: controller.signal }
+      );
       if (!response.ok) {
         throw new Error('Failed to fetch contributors');
       }
       const data = await response.json();
       
-      // Fetch additional user details for each contributor
-      const contributorsWithDetails = await Promise.all(
-        data.map(async (contributor) => {
+      // Batch user details requests in groups of 10
+      const contributorsWithDetails = [];
+      for (let i = 0; i < data.length; i += 10) {
+        const batch = data.slice(i, i + 10);
+        const batchResults = await Promise.allSettled(
+          batch.map(async (contributor) => {
           const userResponse = await fetch(contributor.url);
+          if (!userResponse.ok) {
+            throw new Error(`Failed to fetch details for ${contributor.login}`);
+          }
           const userData = await userResponse.json();
           return {
             ...contributor,
             name: userData.name || userData.login,
             bio: userData.bio,
             location: userData.location,
             company: userData.company
           };
-        })
-      );
+        }));
+
+        // Handle individual failures gracefully
+        batchResults.forEach((result, index) => {
+          if (result.status === 'fulfilled') {
+            contributorsWithDetails.push(result.value);
+          } else {
+            contributorsWithDetails.push({
+              ...batch[index],
+              name: batch[index].login,
+              error: 'Failed to load additional details'
+            });
+          }
+        });
+
+        // Add delay between batches to respect rate limits
+        if (i + 10 < data.length) {
+          await new Promise(resolve => setTimeout(resolve, 1000));
+        }
+      }
       
       setContributors(contributorsWithDetails);
       setError(null);
     } catch (err) {
+      if (err.name === 'AbortError') return;
       setError(err.message);
     } finally {
       setLoading(false);
     }
   };

   fetchContributors();
+  return () => controller.abort();
 }, []);

Additionally, consider:

  1. Using environment variables for the repository details
  2. Implementing proper caching to avoid repeated fetches
  3. Adding retry logic for failed requests
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchContributors = async () => {
try {
setLoading(true);
const response = await fetch('https://api.github.com/repos/RamakrushnaBiswal/PlayCafe/contributors');
if (!response.ok) {
throw new Error('Failed to fetch contributors');
}
const data = await response.json();
// Fetch additional user details for each contributor
const contributorsWithDetails = await Promise.all(
data.map(async (contributor) => {
const userResponse = await fetch(contributor.url);
const userData = await userResponse.json();
return {
...contributor,
name: userData.name || userData.login,
bio: userData.bio,
location: userData.location,
company: userData.company
};
})
);
setContributors(contributorsWithDetails);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchContributors();
}, []);
useEffect(() => {
const controller = new AbortController();
const REPO_OWNER = 'RamakrushnaBiswal';
const REPO_NAME = 'PlayCafe';
const fetchContributors = async () => {
try {
setLoading(true);
const response = await fetch(
`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contributors`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Failed to fetch contributors');
}
const data = await response.json();
// Batch user details requests in groups of 10
const contributorsWithDetails = [];
for (let i = 0; i < data.length; i += 10) {
const batch = data.slice(i, i + 10);
const batchResults = await Promise.allSettled(
batch.map(async (contributor) => {
const userResponse = await fetch(contributor.url);
if (!userResponse.ok) {
throw new Error(`Failed to fetch details for ${contributor.login}`);
}
const userData = await userResponse.json();
return {
...contributor,
name: userData.name || userData.login,
bio: userData.bio,
location: userData.location,
company: userData.company
};
}));
// Handle individual failures gracefully
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
contributorsWithDetails.push(result.value);
} else {
contributorsWithDetails.push({
...batch[index],
name: batch[index].login,
error: 'Failed to load additional details'
});
}
});
// Add delay between batches to respect rate limits
if (i + 10 < data.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
setContributors(contributorsWithDetails);
setError(null);
} catch (err) {
if (err.name === 'AbortError') return;
setError(err.message);
} finally {
setLoading(false);
}
};
fetchContributors();
return () => controller.abort();
}, []);


if (error) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
<div className="max-w-3xl mx-auto text-center">
<div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
Error loading contributors: {error}
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
{/* Header Section */}
<div className="max-w-3xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4 flex items-center justify-center gap-2">
<Github className="w-8 h-8" />
GitHub Contributors
</h1>

{/* Gradient Bar */}
<div className="h-1 w-full bg-gradient-to-r from-red-500 via-yellow-500 via-green-500 to-blue-500 mb-6"></div>

<p className="text-gray-600 dark:text-gray-300 text-lg mb-8">
Thanks to our amazing contributors who help build and improve this project! 🎉
</p>
</div>

{/* Loading State */}
{loading ? (
<div className="flex justify-center items-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
</div>
) : (
/* Contributors List */
<div className="space-y-3">
{contributors.map((contributor) => (
<div
key={contributor.id}
className="flex items-center bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow duration-200"
>
<img
src={contributor.avatar_url}
alt={contributor.login}
className="w-12 h-12 rounded-full"
/>
<div className="ml-4 flex-1">
<div className="flex items-center justify-between">
<div>
<a
href={contributor.html_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
{contributor.name || contributor.login}
</a>
<p className="text-sm text-gray-500 dark:text-gray-400">
@{contributor.login}
</p>
</div>
<span className="text-green-600 dark:text-green-400 font-semibold">
{contributor.contributions} contributions
</span>
</div>
{(contributor.bio || contributor.location) && (
<div className="mt-2 text-sm text-gray-600 dark:text-gray-300">
{contributor.bio && <p>{contributor.bio}</p>}
{contributor.location && (
<p className="text-gray-500 dark:text-gray-400 mt-1">
📍 {contributor.location}
</p>
)}
</div>
)}
</div>
</div>
))}
</div>
)}

{/* Repository Link */}
<div className="mt-12 text-center">
<div className="text-gray-700 dark:text-gray-300 font-medium mb-4">
Explore more on our GitHub repository:
</div>
<a
href="https://github.com/RamakrushnaBiswal/PlayCafe"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-gray-800 dark:bg-gray-700 text-white px-6 py-3 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-600 transition-colors duration-200"
>
<Github className="w-5 h-5" />
Visit Repository
</a>
</div>

{/* Bottom Gradient Bar */}
<div className="h-1 w-full bg-gradient-to-r from-red-500 via-yellow-500 via-green-500 to-blue-500 mt-12"></div>
</div>
</div>
);
Comment on lines +58 to +149
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize rendering performance and enhance accessibility.

The UI implementation is clean, but there are opportunities for improvement:

  1. List rendering optimization
  2. Image loading optimization
  3. Keyboard navigation
  4. Screen reader support

Apply these improvements:

 {contributors.map((contributor) => (
   <div
     key={contributor.id}
     className="flex items-center bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow duration-200"
+    role="article"
+    aria-labelledby={`contributor-${contributor.id}`}
   >
     <img
       src={contributor.avatar_url}
+      loading="lazy"
       alt={contributor.login}
       className="w-12 h-12 rounded-full"
     />
     <div className="ml-4 flex-1">
       <div className="flex items-center justify-between">
         <div>
           <a
+            id={`contributor-${contributor.id}`}
             href={contributor.html_url}
             target="_blank"
             rel="noopener noreferrer"
             className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
+            aria-label={`View ${contributor.name || contributor.login}'s GitHub profile`}
           >
             {contributor.name || contributor.login}
           </a>
-          <p className="text-sm text-gray-500 dark:text-gray-400">
+          <p className="text-sm text-gray-500 dark:text-gray-400" aria-label="GitHub username">
             @{contributor.login}
           </p>
         </div>
-        <span className="text-green-600 dark:text-green-400 font-semibold">
+        <span className="text-green-600 dark:text-green-400 font-semibold" aria-label="Number of contributions">
           {contributor.contributions} contributions
         </span>
       </div>

Consider adding these performance optimizations:

  1. Implement virtualization for large lists using react-window or react-virtualized
  2. Add pagination or infinite scroll for better performance with large datasets
  3. Memoize contributor cards using React.memo

Committable suggestion skipped: line range outside the PR's diff.

};

export default Contributors;
57 changes: 28 additions & 29 deletions frontend/src/components/Shared/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const Navbar = () => {
{ name: 'BOARDGAMES', path: '/boardgame' },
{ name: 'MEMBERSHIP', path: '/membership' }, // Add Membership here
{ name: 'PROFILE', path: '/dashboard' }, // Add Membership here
{ name: 'CONTRIBUTORS', path: '/contributors' },
];

useEffect(() => {
Expand Down Expand Up @@ -83,16 +84,15 @@ const Navbar = () => {

return (
<nav
className={`w-full fixed top-0 z-50 transition duration-300 ${
isScrolled
? 'bg-background-light dark:bg-background-dark shadow-lg text-black dark:text-white'
: 'bg-transparent text-black dark:text-white'
}`}
>
className={`w-full fixed top-0 z-50 transition duration-300 ${isScrolled
? 'bg-background-light dark:bg-background-dark shadow-lg text-black dark:text-white'
: 'bg-transparent text-black dark:text-white'
}`}
>

<div className="mx-auto px-6">
<div className="flex justify-between items-center lg:h-16">

<Link to="/">
<div className="flex-shrink-0">
<img
Expand Down Expand Up @@ -120,11 +120,11 @@ const Navbar = () => {
</li>
))}
</ul>

</div>

<div className="hidden md:flex font-semibold Poppins text-lg space-x-4 ">
<ThemeToggle />
<ThemeToggle />
{token ? (
<button
className={`${baseTextColorClass} ${hoverTextColorClass} px-4 py-1 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] font-semibold`}
Expand All @@ -141,7 +141,7 @@ const Navbar = () => {
>
LOGIN
</button>

)}
</div>

Expand All @@ -151,7 +151,7 @@ const Navbar = () => {
onClick={toggleMenu}
className={`${buttonTextClass} focus:outline-none`}
>

{isMenuOpen ? (
<svg
className="h-6 w-6 stroke-black dark:stroke-white"
Expand All @@ -168,32 +168,31 @@ const Navbar = () => {
</svg>
) : (
<svg
className="h-6 w-6 stroke-black dark:stroke-white"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
className="h-6 w-6 stroke-black dark:stroke-white"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>

)}
</button>
</div>

</div>
</div>

{/* Mobile Menu */}
{isMenuOpen && (
<div
className={`md:hidden ${
isScrolled ? 'bg-amber-100 shadow-lg' : 'bg-[#E0F0B1] shadow-lg'
} dark:bg-black `}
className={`md:hidden ${isScrolled ? 'bg-amber-100 shadow-lg' : 'bg-[#E0F0B1] shadow-lg'
} dark:bg-black `}
>
<div className="px-4 pt-4 pb-4 space-y-2">
{menuItems.map((item) => (
Expand Down Expand Up @@ -224,7 +223,7 @@ const Navbar = () => {
Log In
</button>
)}
<ThemeToggle />
<ThemeToggle />
</div>
</div>
)}
Expand Down
21 changes: 13 additions & 8 deletions frontend/src/router/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ import AdminLogin from '../components/Pages/Admin/AdminLogin';
import AdminSignup from '../components/Pages/Admin/AdminSignup';
import ProtectedRoute from './ProtectedRoute';
import Profile from '../components/Pages/Dashboard';
import HelpAndSupport from '../components/Pages/HelpAndSupport';
import Contributors from '../components/Contributors';

const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
<Route path="/" element={<App />}>
<Route index={true} path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/menu" element={<Menu />} />
Expand All @@ -41,12 +44,12 @@ const router = createBrowserRouter(
<Route path="/login" element={<Login />} />
<Route path="/reset-password/:id" element={<ResetPassword />} />
<Route
path="/admin"
element={
<ProtectedRoute>
<Admin />
</ProtectedRoute>
}
path="/admin"
element={
<ProtectedRoute>
<Admin />
</ProtectedRoute>
}
/>
<Route path="/admin-login" element={<AdminLogin />} />
<Route path="/admin-signup" element={<AdminSignup />} />
Expand All @@ -55,7 +58,9 @@ const router = createBrowserRouter(
<Route path="/otp-verify" element={<OtpRegisterVerify />} />
<Route path="/membership" element={<Membership />} />
<Route path="/dashboard" element={<Profile />} />


<Route path="/help" element={<HelpAndSupport />} />
<Route path="/contributors" element={<Contributors />} />
</Route>
)
);
Expand Down
Loading