diff --git a/package.json b/package.json index ded26275..20b30ee6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dotenv": "^16.5.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", diff --git a/src/components/FloatingContributors/FloatingContributors.css b/src/components/FloatingContributors/FloatingContributors.css index 4194e600..d9720e52 100644 --- a/src/components/FloatingContributors/FloatingContributors.css +++ b/src/components/FloatingContributors/FloatingContributors.css @@ -43,6 +43,113 @@ min-width: 400px; } +/* New activity feed styles */ +.contributors-activities { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; + max-height: 240px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; +} + +.contributors-activities::-webkit-scrollbar { + width: 4px; +} + +.contributors-activities::-webkit-scrollbar-track { + background: transparent; +} + +.contributors-activities::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.2); + border-radius: 4px; +} + +.contributor-activity-item { + display: flex; + align-items: center; + padding: 8px 10px; + border-radius: 10px; + background-color: rgba(255, 255, 255, 0.08); + transition: background-color 0.2s ease, transform 0.2s ease; + cursor: pointer; /* Add pointer cursor to indicate clickable */ +} + +.contributor-activity-item:hover { + background-color: rgba(255, 255, 255, 0.15); + transform: translateY(-2px); /* Slight lift effect on hover */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.contributor-activity-item:focus { + outline: 2px solid rgba(102, 126, 234, 0.6); + outline-offset: 2px; +} + +.contributor-activity-item:active { + transform: translateY(0); + background-color: rgba(255, 255, 255, 0.1); +} + +.activity-item-avatar { + margin-right: 12px; + position: relative; +} + +.activity-item-img { + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.activity-item-content { + flex: 1; + min-width: 0; +} + +.activity-item-user { + display: flex; + justify-content: space-between; + align-items: center; +} + +.activity-item-username { + font-weight: 600; + font-size: 0.9rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255, 255, 255, 0.95); +} + +.activity-item-badge { + background-color: rgba(59, 130, 246, 0.2); + padding: 2px 6px; + border-radius: 12px; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.9); +} + +.activity-item-action { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.7); + margin-top: 2px; +} + +.activities-more { + text-align: center; + padding: 8px; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.05); + border-radius: 8px; + margin-top: 4px; +} + /* Header embedded version - larger size */ .floating-contributors-container.header-embedded .floating-contributors-card { min-width: 450px; @@ -175,12 +282,43 @@ border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; + position: relative; + overflow: hidden; } .floating-contributors-activity:hover { background: rgba(255, 255, 255, 0.08); border-color: rgba(102, 126, 234, 0.3); + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); +} + +.floating-contributors-activity:focus { + outline: 2px solid rgba(102, 126, 234, 0.6); + outline-offset: 2px; +} + +.floating-contributors-activity:active { transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* Subtle link indicator */ +.floating-contributors-activity::after { + content: "→"; + position: absolute; + right: 15px; + top: 50%; + transform: translateY(-50%) translateX(20px); + opacity: 0; + font-size: 16px; + color: rgba(102, 126, 234, 0.8); + transition: all 0.3s ease; +} + +.floating-contributors-activity:hover::after { + transform: translateY(-50%) translateX(0); + opacity: 1; } [data-theme='light'] .floating-contributors-activity { @@ -369,6 +507,30 @@ transform: scale(1.1); } +.contributor-link { + position: relative; + display: block; + transition: all 0.3s ease; +} + +.contributor-link::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + border: 2px solid transparent; + transition: all 0.3s ease; +} + +.contributor-link:focus { + outline: none; +} + +.contributor-link:focus::after { + border-color: rgba(102, 126, 234, 0.8); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); +} + [data-theme='light'] .contributor-avatar { border-color: rgba(0, 0, 0, 0.2); } @@ -414,7 +576,8 @@ transform: translateX(-50%) translateY(-8px); } -.tooltip-name { +.tooltip-name, +.tooltip-username { font-weight: 600; margin-bottom: 2px; } diff --git a/src/components/FloatingContributors/README.md b/src/components/FloatingContributors/README.md new file mode 100644 index 00000000..dc057778 --- /dev/null +++ b/src/components/FloatingContributors/README.md @@ -0,0 +1,97 @@ +# FloatingContributors Component + +The `FloatingContributors` component is a dynamic React component that displays live GitHub activity and a list of contributors for the `recodehive/recode-website` repository. It fetches data from GitHub APIs, processes it, and renders it in an interactive and animated UI. + +--- + +## Features + +### 1. **Live GitHub Activity** +- Displays recent events such as pushes, pull requests, comments, and more. +- Cycles through activities every 4 seconds. + +### 2. **Contributors Grid** +- Shows the top contributors with their avatars, GitHub profiles, and contribution counts. +- Displays tooltips with additional contributor details. + +### 3. **Animations** +- Smooth animations for floating effects, hover interactions, and transitions using `framer-motion`. + +### 4. **Fallback Mechanism** +- Uses hardcoded fallback data when GitHub API calls fail. + +### 5. **Caching** +- Caches API responses in `localStorage` for 2 minutes to reduce API calls. + +--- + +## API Integration + +### GitHub Events API +- **Endpoint**: `https://api.github.com/repos/recodehive/recode-website/events?per_page=30` +- **Purpose**: Fetches live activity data (e.g., pushes, pull requests, comments). + +### GitHub Contributors API +- **Endpoint**: `https://api.github.com/repos/recodehive/recode-website/contributors?per_page=100` +- **Purpose**: Fetches contributor data (e.g., avatars, contribution counts). + +--- + +## Key Functions + +### `formatTimeAgo` +- Formats timestamps into relative time strings (e.g., "2 hours ago"). + +### `getGitHubEventUrl` +- Generates URLs for GitHub events based on the action type. + +### `getActionIcon` and `getActionText` +- Maps action types to icons and descriptive text. + +--- + +## Potential Enhancements + +### API Integration +- Add support for more event types (e.g., `IssuesEvent` for issue creation). +- Use authentication tokens to increase API rate limits. + +### UI/UX Features +- Add user controls to pause or skip activities. +- Allow users to view all contributors in a modal or separate page. + +### Styling +- Convert CSS to a CSS-in-JS solution (e.g., styled-components). +- Add theme support for light and dark modes. + +### Error Handling +- Display error messages in the UI. +- Retry failed API calls with exponential backoff. + +--- + +## Usage + +1. Import the component: + ```tsx + import FloatingContributors from './FloatingContributors'; + ``` + +2. Use it in your application: + ```tsx + + ``` + +--- + +## Dependencies +- `react` +- `framer-motion` + +--- + +## Notes +- The component auto-refreshes data every 60 seconds. +- It uses `localStorage` for caching API responses. + +--- \ No newline at end of file diff --git a/src/components/FloatingContributors/index.tsx b/src/components/FloatingContributors/index.tsx index 8ae8a366..3a856941 100644 --- a/src/components/FloatingContributors/index.tsx +++ b/src/components/FloatingContributors/index.tsx @@ -1,19 +1,94 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import './FloatingContributors.css'; +// Format relative time (e.g., "2 hours ago") +const formatTimeAgo = (date: Date): string => { + const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); + + let interval = Math.floor(seconds / 31536000); + if (interval > 1) return `${interval} years ago`; + + interval = Math.floor(seconds / 2592000); + if (interval > 1) return `${interval} months ago`; + + interval = Math.floor(seconds / 86400); + if (interval > 1) return `${interval} days ago`; + if (interval === 1) return `1 day ago`; + + interval = Math.floor(seconds / 3600); + if (interval > 1) return `${interval} hours ago`; + if (interval === 1) return `1 hour ago`; + + interval = Math.floor(seconds / 60); + if (interval > 1) return `${interval} minutes ago`; + if (interval === 1) return `1 minute ago`; + + return `just now`; +}; + +// Type definitions interface Contributor { id: string; login: string; avatar_url: string; contributions: number; html_url: string; - lastActivity: string; +} + +interface GitHubEvent { + id: string; + type: string; + created_at: string; + actor: { + id: number; + login: string; + avatar_url: string; + url: string; + }; + repo: { + id: number; + name: string; + url: string; + }; + payload: { + action?: string; + ref?: string; + ref_type?: string; + push_id?: number; + size?: number; + distinct_size?: number; + head?: string; + commits?: Array<{ + sha: string; + message: string; + url: string; + }>; + issue?: { + number: number; + title: string; + }; + pull_request?: { + merged: boolean; + title: string; + number: number; + }; + comment?: { + body: string; + }; + }; } interface ContributorActivity { - contributor: Contributor; - action: 'checked-in' | 'contributed' | 'starred' | 'forked'; + id: string; + contributor: { + login: string; + avatar_url: string; + html_url: string; + }; + action: 'pushed' | 'created' | 'merged' | 'opened' | 'commented' | 'closed' | 'other'; + message?: string; + timestamp: Date; timeAgo: string; } @@ -27,208 +102,348 @@ const FloatingContributors: React.FC = ({ headerEmbed const [currentActivityIndex, setCurrentActivityIndex] = useState(0); const [isVisible, setIsVisible] = useState(true); const [loading, setLoading] = useState(true); - - - - // Initialize with fallback data immediately to ensure toaster always appears - useEffect(() => { - // Set fallback data immediately - const initializeFallbackData = () => { - const demoContributors: Contributor[] = [ - { - id: '1', - login: 'sanjay-kv', - avatar_url: 'https://avatars.githubusercontent.com/u/30715153?v=4', - contributions: 127, - html_url: 'https://github.com/sanjay-kv', - lastActivity: '2 minutes ago', - }, - { - id: '2', - login: 'recodehive-team', - avatar_url: 'https://avatars.githubusercontent.com/u/150000000?v=4', - contributions: 89, - html_url: 'https://github.com/recodehive', - lastActivity: '5 minutes ago', - }, - { - id: '3', - login: 'contributor-1', - avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', - contributions: 64, - html_url: 'https://github.com/contributor-1', - lastActivity: '12 minutes ago', - }, - { - id: '4', - login: 'contributor-2', - avatar_url: 'https://avatars.githubusercontent.com/u/2?v=4', - contributions: 45, - html_url: 'https://github.com/contributor-2', - lastActivity: '1 hour ago', - }, - { - id: '5', - login: 'contributor-3', - avatar_url: 'https://avatars.githubusercontent.com/u/3?v=4', - contributions: 32, - html_url: 'https://github.com/contributor-3', - lastActivity: '3 hours ago', + const [lastFetched, setLastFetched] = useState(null); + const refreshTimerRef = useRef(null); + + // Create fallback activities for when API fails + const createFallbackActivities = useCallback((): ContributorActivity[] => { + const fallbackContributors = [ + { + login: 'sanjay-kv', + avatar_url: 'https://avatars.githubusercontent.com/u/30715153?v=4', + html_url: 'https://github.com/sanjay-kv', + }, + { + login: 'recodehive-team', + avatar_url: 'https://avatars.githubusercontent.com/u/150000000?v=4', + html_url: 'https://github.com/recodehive', + }, + { + login: 'open-source-contributor', + avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4', + html_url: 'https://github.com/open-source-contributor', + }, + { + login: 'developer', + avatar_url: 'https://avatars.githubusercontent.com/u/9919?v=4', + html_url: 'https://github.com/developer', + }, + { + login: 'coder', + avatar_url: 'https://avatars.githubusercontent.com/u/6154722?v=4', + html_url: 'https://github.com/coder', + }, + ]; + + const actions: ContributorActivity['action'][] = ['pushed', 'created', 'merged', 'opened', 'commented']; + const timeOffsets = [5, 10, 30, 60, 120, 240, 480]; // minutes + const messages = [ + 'Updated documentation', + 'Fixed styling issues', + 'Added new feature', + 'Resolved conflict in package.json', + 'Implemented responsive design', + 'Updated dependencies', + 'Fixed typo in README' + ]; + + return fallbackContributors.map((contributor, index) => { + const now = new Date(); + const timestamp = new Date(now.getTime() - (timeOffsets[index % timeOffsets.length] * 60 * 1000)); + + return { + id: `fallback-${index}`, + contributor: { + login: contributor.login, + avatar_url: contributor.avatar_url, + html_url: contributor.html_url, }, - ]; - - setContributors(demoContributors); - setActivities(demoContributors.map(contributor => ({ - contributor, - action: getRandomAction(), - timeAgo: contributor.lastActivity, - }))); - setLoading(false); - }; - - // Initialize with fallback data immediately - initializeFallbackData(); - - // Then try to fetch real data - const fetchContributors = async () => { - try { - - // Fetch repositories from RecodeHive organization - const reposResponse = await fetch('https://api.github.com/orgs/recodehive/repos?type=public&per_page=10&sort=updated'); - - // Check if the response is ok - if (!reposResponse.ok) { - console.warn(`GitHub API rate limit or error: ${reposResponse.status}`); - throw new Error(`GitHub API error: ${reposResponse.status}`); + action: actions[index % actions.length], + message: messages[index % messages.length], + timestamp, + timeAgo: formatTimeAgo(timestamp), + }; + }); + }, []); + + // Fetch live data from GitHub + const fetchLiveData = useCallback(async () => { + try { + // Use specific cache key for this repository's events + const CACHE_KEY = 'recodehive_website_events'; + const CACHE_DURATION = 2 * 60 * 1000; // 2 minutes - short for "live" data + + // Check if we have recent data already + const now = Date.now(); + if (lastFetched && now - lastFetched < 30000) { + // Don't fetch more than once every 30 seconds + return; + } + + // Check for cached events + let events: GitHubEvent[] = []; + if (typeof window !== 'undefined') { + try { + const cachedData = localStorage.getItem(CACHE_KEY); + if (cachedData) { + const { data, timestamp } = JSON.parse(cachedData); + if (now - timestamp < CACHE_DURATION) { + events = data; + } + } + } catch (e) { + console.warn('Error retrieving cached events', e); } - - const repos = await reposResponse.json(); - - if (!Array.isArray(repos)) { - throw new Error('Invalid repos response'); + } + + // If no valid cache, fetch fresh data + if (events.length === 0) { + setLoading(true); + + // Fetch repository events from GitHub API + const eventsResponse = await fetch('https://api.github.com/repos/recodehive/recode-website/events?per_page=30'); + + if (!eventsResponse.ok) { + throw new Error(`GitHub API error: ${eventsResponse.status}`); } - - // Collect contributors from multiple repositories - const contributorsMap = new Map(); - for (const repo of repos.slice(0, 5)) { // Limit to top 5 repos for performance + events = await eventsResponse.json(); + + // Save to cache + if (typeof window !== 'undefined' && Array.isArray(events)) { try { - const contributorsResponse = await fetch(`https://api.github.com/repos/${repo.full_name}/contributors?per_page=20`); + localStorage.setItem(CACHE_KEY, JSON.stringify({ + data: events, + timestamp: now, + })); + } catch (e) { + console.warn('Error caching events data', e); + } + } + } + + // Process events into activities + if (Array.isArray(events) && events.length > 0) { + // Convert GitHub events to our activity format + const newActivities: ContributorActivity[] = events.map((event) => { + // Map GitHub event types to our action types + let action: ContributorActivity['action'] = 'other'; + let message: string | undefined; + + switch (event.type) { + case 'PushEvent': + action = 'pushed'; + message = event.payload.commits && event.payload.commits[0]?.message; + break; + case 'PullRequestEvent': + if (event.payload.action === 'opened') action = 'opened'; + else if (event.payload.action === 'closed' && event.payload.pull_request?.merged) action = 'merged'; + else if (event.payload.action === 'closed') action = 'closed'; + break; + case 'CreateEvent': + action = 'created'; + break; + case 'IssueCommentEvent': + case 'CommitCommentEvent': + case 'PullRequestReviewCommentEvent': + action = 'commented'; + message = event.payload.comment?.body?.slice(0, 60); + break; + default: + action = 'other'; + } + + const timestamp = new Date(event.created_at); + + return { + id: event.id, + contributor: { + login: event.actor.login, + avatar_url: event.actor.avatar_url, + html_url: `https://github.com/${event.actor.login}`, + }, + action, + message: message?.slice(0, 60), // Limit message length + timestamp, + timeAgo: formatTimeAgo(timestamp), + }; + }); + + // Update only if we have events + if (newActivities.length > 0) { + setActivities(newActivities); + + // Extract contributors from these events + const contributorsMap = new Map(); + + // Also fetch contributors directly for contribution counts + try { + const contributorsResponse = await fetch('https://api.github.com/repos/recodehive/recode-website/contributors?per_page=100'); if (contributorsResponse.ok) { - const repoContributors = await contributorsResponse.json(); + const contributorsData = await contributorsResponse.json(); - if (Array.isArray(repoContributors)) { - repoContributors.forEach(contributor => { + if (Array.isArray(contributorsData)) { + contributorsData.forEach(contributor => { if (contributor.login && contributor.type === 'User') { - const existing = contributorsMap.get(contributor.login); - if (existing) { - existing.contributions += contributor.contributions; - } else { - contributorsMap.set(contributor.login, { - id: contributor.id.toString(), - login: contributor.login, - avatar_url: contributor.avatar_url, - contributions: contributor.contributions, - html_url: contributor.html_url, - lastActivity: generateRandomTimeAgo(), - }); - } + contributorsMap.set(contributor.login, { + id: contributor.id.toString(), + login: contributor.login, + avatar_url: contributor.avatar_url, + contributions: contributor.contributions, + html_url: contributor.html_url, + }); } }); } } } catch (error) { - console.warn(`Error fetching contributors for ${repo.name}:`, error); + console.warn('Error fetching contributors:', error); + + // If we couldn't get contributors data, at least use actors from events + events.forEach(event => { + const login = event.actor.login; + if (!contributorsMap.has(login)) { + contributorsMap.set(login, { + id: event.actor.id.toString(), + login, + avatar_url: event.actor.avatar_url, + contributions: 1, // We don't know the actual count + html_url: `https://github.com/${login}`, + }); + } + }); + } + + // Update contributors if we found any + if (contributorsMap.size > 0) { + setContributors(Array.from(contributorsMap.values())); } } - - const contributorsList = Array.from(contributorsMap.values()) - .sort((a, b) => b.contributions - a.contributions) - .slice(0, 12); // Top 12 contributors - - // Only update if we got real data - if (contributorsList.length > 0) { - setContributors(contributorsList); - - // Generate activities - const generatedActivities: ContributorActivity[] = contributorsList.map(contributor => ({ - contributor, - action: getRandomAction(), - timeAgo: generateRandomTimeAgo(), - })); - - setActivities(generatedActivities); - } + } + + setLastFetched(now); + setLoading(false); + + // Set up next refresh + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + refreshTimerRef.current = setTimeout(() => { + fetchLiveData(); + }, 60000); // Refresh every minute + + } catch (error) { + console.warn('Error fetching GitHub events:', error); + + // Use fallback data if we have no activities yet + if (activities.length === 0) { + const fallbackActivities = createFallbackActivities(); + setActivities(fallbackActivities); - } catch (error) { - // Silently handle GitHub API errors (rate limits, etc.) - console.warn('Using fallback contributor data due to GitHub API limitations'); - // Fallback data is already initialized, so no need to set it again + // Create fallback contributors + const contributorsMap = new Map(); + fallbackActivities.forEach(activity => { + const login = activity.contributor.login; + if (!contributorsMap.has(login)) { + contributorsMap.set(login, { + id: `fallback-${login}`, + login, + avatar_url: activity.contributor.avatar_url, + contributions: Math.floor(Math.random() * 50) + 10, + html_url: activity.contributor.html_url, + }); + } + }); + + setContributors(Array.from(contributorsMap.values())); + } + + setLoading(false); + } + }, [activities.length, createFallbackActivities, lastFetched]); + + // Initialize component and start data fetching + useEffect(() => { + // Set loading state + setLoading(true); + + // Fetch data immediately + fetchLiveData(); + + // Clean up on unmount + return () => { + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); } }; - - fetchContributors(); - }, []); - + }, [fetchLiveData]); + // Cycle through activities useEffect(() => { - if (activities.length === 0) return; + if (activities.length <= 1) return; const interval = setInterval(() => { setCurrentActivityIndex((prev) => (prev + 1) % activities.length); }, 4000); - + return () => clearInterval(interval); }, [activities.length]); - - - const generateRandomTimeAgo = (): string => { - const timeOptions = [ - 'a few seconds ago', - '2 minutes ago', - '5 minutes ago', - '10 minutes ago', - '30 minutes ago', - '1 hour ago', - '2 hours ago', - 'a day ago', - '2 days ago', - ]; - return timeOptions[Math.floor(Math.random() * timeOptions.length)]; - }; - - const getRandomAction = (): ContributorActivity['action'] => { - const actions: ContributorActivity['action'][] = ['checked-in', 'contributed', 'starred', 'forked']; - return actions[Math.floor(Math.random() * actions.length)]; + + // Get GitHub URL for event + const getGitHubEventUrl = (activity: ContributorActivity): string => { + const repoUrl = 'https://github.com/recodehive/recode-website'; + + switch (activity.action) { + case 'pushed': + return `${repoUrl}/commits`; + case 'merged': + case 'opened': + case 'closed': + return `${repoUrl}/pulls`; + case 'commented': + return `${repoUrl}/issues`; + case 'created': + return repoUrl; + default: + return repoUrl; + } }; - + + // Get icon for action type const getActionIcon = (action: ContributorActivity['action']): string => { switch (action) { - case 'checked-in': return '✅'; - case 'contributed': return '🚀'; - case 'starred': return '⭐'; - case 'forked': return '🍴'; + case 'pushed': return '🚀'; + case 'created': return '✨'; + case 'merged': return '🔄'; + case 'opened': return '📝'; + case 'commented': return '💬'; + case 'closed': return '✅'; default: return '💻'; } }; - + + // Get text for action type const getActionText = (action: ContributorActivity['action']): string => { switch (action) { - case 'checked-in': return 'Checked-in'; - case 'contributed': return 'Contributed'; - case 'starred': return 'Starred'; - case 'forked': return 'Forked'; + case 'pushed': return 'Pushed code'; + case 'created': return 'Created'; + case 'merged': return 'Merged PR'; + case 'opened': return 'Opened PR'; + case 'commented': return 'Commented'; + case 'closed': return 'Closed'; default: return 'Active'; } }; - - if (loading || activities.length === 0) { + + // Don't render anything while initial loading + if (loading && activities.length === 0) { return null; } - + + // Get current activity to display const currentActivity = activities[currentActivityIndex]; - + return ( {isVisible && ( @@ -274,7 +489,7 @@ const FloatingContributors: React.FC = ({ headerEmbed Live Activity
- RecodeHive Community + recodehive/recode-website
@@ -282,11 +497,15 @@ const FloatingContributors: React.FC = ({ headerEmbed window.open(getGitHubEventUrl(currentActivity), '_blank')} + tabIndex={0} + role="link" + aria-label={`View ${currentActivity.contributor.login}'s ${currentActivity.action} activity on GitHub`} >
= ({ headerEmbed {getActionText(currentActivity.action)}
+ {currentActivity.message && ( +
{currentActivity.message}
+ )}
{currentActivity.timeAgo}
@@ -322,40 +544,46 @@ const FloatingContributors: React.FC = ({ headerEmbed
- {contributors.slice(0, 8).map((contributor, index) => ( - - {contributor.login} -
-
@{contributor.login}
-
{contributor.contributions} contributions
-
-
- ))} + {contributors + .sort((a, b) => b.contributions - a.contributions) // Sort contributors by contributions in descending order + .slice(0, 5) // Limit to top 5 contributors + .map((contributor, index) => ( + + + {contributor.login} +
+
@{contributor.login}
+
{contributor.contributions || 0} contributions
+
+
+
+ ))} - {contributors.length > 8 && ( + {contributors.length > 12 && (
- +{contributors.length - 8} + +{contributors.length - 12} more
)}
@@ -364,15 +592,16 @@ const FloatingContributors: React.FC = ({ headerEmbed {/* Footer */}
🚀 - Join the Community + View Repository on GitHub
@@ -408,4 +637,4 @@ const FloatingContributors: React.FC = ({ headerEmbed ); }; -export default FloatingContributors; \ No newline at end of file +export default FloatingContributors;