Skip to content

feat(partners): add skeleton before load - #21

Merged
calebephrem merged 1 commit into
open-devhub:mainfrom
calebephrem:main
Jul 9, 2026
Merged

feat(partners): add skeleton before load#21
calebephrem merged 1 commit into
open-devhub:mainfrom
calebephrem:main

Conversation

@calebephrem

Copy link
Copy Markdown
Member
  • add skeleton before load
  • add github button
  • add trait tags (eg. Community, Discoverable) (from discord api)

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

@calebephrem is attempting to deploy a commit to the aditya ojha's projects Team on Vercel.

A member of the Team first needs to authorize it.

@devhub-bot devhub-bot Bot added the feat New feature label Jul 9, 2026
@beetle-ai

beetle-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary by Beetle

This PR enhances the partners page with improved user experience through skeleton loading states, additional partner metadata, and visual trait badges. The changes transform the page from a simple loading state to a more sophisticated, progressive loading experience that displays individual partner cards as their data becomes available from the Discord API. New features include GitHub links, Discord server trait badges (Verified, Partnered, Community, Discoverable), and a refined component architecture.

📁 File Changes Summary (Consolidated across all commits):

File Status Changes Description
app/partners/page.tsx Modified +468/-329 Complete refactor of the partners page: added skeleton loading components (PartnerCardSkeleton, SkeletonBlock, CornerBrackets), extracted partner card logic into dedicated PartnerCard component, implemented per-card loading states instead of global loading, added GitHub button support, integrated Discord server trait badges (Verified, Partnered, Community, Discoverable) with icons, and improved data fetching to load partners individually rather than in batch

Total Changes: 1 file changed, +468 additions, -329 deletions

🎯 Key Changes:

  • Progressive Loading Architecture: Replaced single global loading state with per-partner loadingMap, enabling individual cards to render as soon as their data is available rather than waiting for all partners to load
  • Skeleton Loading Components: Introduced PartnerCardSkeleton with animated pulse effects that mirror the final card structure, providing visual feedback during data fetching
  • Component Extraction: Refactored monolithic JSX into reusable components (PartnerCard, CornerBrackets, SkeletonBlock) for better maintainability and separation of concerns
  • Discord Trait Badges: Added visual badges for Discord server features (Verified, Partnered, Community, Discoverable) pulled from the Discord API's guild.features array, displayed with corresponding Lucide icons
  • GitHub Integration: Added optional githubUrl field to partner data structure with dedicated GitHub button in the UI
  • Improved Data Fetching: Changed from Promise.allSettled batch approach to individual async fetches per partner, allowing faster initial render and better error isolation

📊 Impact Assessment:

  • Security: ✅ No security concerns. External API calls remain to Discord's public API with no authentication changes. GitHub and website URLs are properly sanitized with rel="noopener noreferrer" on external links.
  • Performance: ⚡ Significant improvement. Progressive loading means users see content faster—first partner card appears immediately when its data loads rather than waiting for all partners. Individual fetch approach prevents one slow/failed request from blocking others. Skeleton states provide perceived performance boost by showing structure immediately.
  • Maintainability: ✅ Improved. Component extraction (PartnerCard, SkeletonBlock, CornerBrackets) makes code more modular and testable. Clear separation between loading and loaded states. Type safety maintained with DiscordData interface extension for traits field. However, the PartnerCard component is still quite large (~150 lines) and could benefit from further decomposition.
  • Testing: ⚠️ Needs attention. No tests included for new components. Key areas requiring test coverage:
  • Skeleton loading state rendering
  • Progressive loading behavior with loadingMap
  • Trait badge rendering logic and icon mapping
  • Error handling when Discord API fails
  • Fallback behavior for missing GitHub URLs or traits
  • Component prop validation for PartnerCard
⚙️ Settings

Severity Threshold: Medium — Balanced feedback — medium and high severity issues only.Change in Settings
Custom Rules: Define your own review rules — Set Custom Rules
PR Summary: Configure PR summary — Change in Settings

📖 User Guide
  • Once repos are connected, PR analysis is automatically enabled. You can disable analysis for this repo from beetleai.dev/analysis
  • Comment @beetle on any PR to start analysis manually
  • Comment @beetle stop to stop any ongoing analysis

Follow us: Beetle · X · LinkedIn

Comment thread app/partners/page.tsx Outdated
Comment on lines +530 to +570
partners.forEach(async (p) => {
try {
const res = await fetch(
`https://discord.com/api/v10/invites/${p.inviteCode}?with_counts=true`,
);
const data = await res.json();
const guild = data.guild;

if (!guild) {
map[code] = null;
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
return;
}

map[code] = {
guildId: guild.id,

name: guild.name,
description: guild.description ?? null,

memberCount: data.approximate_member_count ?? 0,
onlineCount: data.approximate_presence_count ?? 0,

iconUrl: guild.icon
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=128`
: null,

bannerUrl: guild.banner
? `https://cdn.discordapp.com/banners/${guild.id}/${guild.banner}.png?size=1024`
: null,
};
});

setDiscordData(map);
setLoading(false);
}

fetchAll();
setDiscordData((prev) => ({
...prev,
[p.inviteCode]: {
guildId: guild.id,

name: guild.name,
description: guild.description ?? null,

memberCount: data.approximate_member_count ?? 0,
onlineCount: data.approximate_presence_count ?? 0,

iconUrl: guild.icon
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=128`
: null,

bannerUrl: guild.banner
? `https://cdn.discordapp.com/banners/${guild.id}/${guild.banner}.png?size=1024`
: null,

traits: guild.features ?? [],
},
}));
} catch {
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
} finally {
setLoadingMap((prev) => ({ ...prev, [p.inviteCode]: false }));
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using forEach with async callbacks creates uncontrolled concurrent API calls and potential race conditions. The async callbacks inside forEach are fire-and-forget, meaning they all execute simultaneously without coordination. This can cause:

  1. Race conditions: Multiple setDiscordData and setLoadingMap calls updating state concurrently
  2. Unpredictable state updates: No guarantee of update order
  3. Memory leaks: If component unmounts during fetches, state updates will still attempt to execute
    Additionally, the fetch call doesn't validate the HTTP response status before parsing JSON, which can cause errors if the Discord API returns error responses.

Confidence: 5/5

Suggested Fix

Replace forEach with a proper async pattern and add response validation:

Suggested change
partners.forEach(async (p) => {
try {
const res = await fetch(
`https://discord.com/api/v10/invites/${p.inviteCode}?with_counts=true`,
);
const data = await res.json();
const guild = data.guild;
if (!guild) {
map[code] = null;
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
return;
}
map[code] = {
guildId: guild.id,
name: guild.name,
description: guild.description ?? null,
memberCount: data.approximate_member_count ?? 0,
onlineCount: data.approximate_presence_count ?? 0,
iconUrl: guild.icon
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=128`
: null,
bannerUrl: guild.banner
? `https://cdn.discordapp.com/banners/${guild.id}/${guild.banner}.png?size=1024`
: null,
};
});
setDiscordData(map);
setLoading(false);
}
fetchAll();
setDiscordData((prev) => ({
...prev,
[p.inviteCode]: {
guildId: guild.id,
name: guild.name,
description: guild.description ?? null,
memberCount: data.approximate_member_count ?? 0,
onlineCount: data.approximate_presence_count ?? 0,
iconUrl: guild.icon
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=128`
: null,
bannerUrl: guild.banner
? `https://cdn.discordapp.com/banners/${guild.id}/${guild.banner}.png?size=1024`
: null,
traits: guild.features ?? [],
},
}));
} catch {
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
} finally {
setLoadingMap((prev) => ({ ...prev, [p.inviteCode]: false }));
}
});
useEffect(() => {
const controller = new AbortController();
const fetchPartnerData = async () => {
await Promise.all(
partners.map(async (p) => {
try {
const res = await fetch(
`https://discord.com/api/v10/invites/${p.inviteCode}?with_counts=true`,
{ signal: controller.signal }
);
if (!res.ok) {
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
return;
}
const data = await res.json();
const guild = data.guild;
if (!guild) {
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
return;
}
setDiscordData((prev) => ({
...prev,
[p.inviteCode]: {
guildId: guild.id,
name: guild.name,
description: guild.description ?? null,
memberCount: data.approximate_member_count ?? 0,
onlineCount: data.approximate_presence_count ?? 0,
iconUrl: guild.icon
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=128`
: null,
bannerUrl: guild.banner
? `https://cdn.discordapp.com/banners/${guild.id}/${guild.banner}.png?size=1024`
: null,
traits: guild.features ?? [],
},
}));
} catch (error) {
if (error.name !== 'AbortError') {
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
}
} finally {
setLoadingMap((prev) => ({ ...prev, [p.inviteCode]: false }));
}
})
);
};
fetchPartnerData();
return () => controller.abort();
}, []);

Key improvements:

  • Use Promise.all with map instead of forEach for proper async handling
  • Add abort controller for cleanup on unmount (prevents memory leaks)
  • Validate HTTP response with res.ok before parsing JSON
  • Handle abort errors separately to avoid unnecessary state updates
Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In app/partners/page.tsx around line 530, the useEffect uses forEach with async callbacks which creates race conditions and uncontrolled concurrent API calls. Replace forEach with Promise.all and map to properly handle async operations. Add an AbortController to cleanup pending requests on unmount. Also add HTTP response validation with res.ok before calling res.json() to handle API errors properly. The fix should ensure all async operations are coordinated and component unmount is handled safely.

📍 This suggestion applies to lines 530-570

@devhub-bot

devhub-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Note

Linting checks passed successfully 🎉

All formatting and code quality checks are clean.

You're good to merge 🚀

- add skeleton before load
- add github button
- add trait tags (eg. Community, Discoverable) (from discord api)
@beetle-ai

beetle-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary by Beetle

This PR enhances the Partners page with improved user experience through skeleton loading states, additional partner metadata, and visual trait badges. The changes focus on providing immediate visual feedback during data fetching and enriching partner information with GitHub links and Discord server traits (Verified, Partnered, Community, Discoverable).

📁 File Changes Summary (Consolidated across all commits):

File Status Changes Description
app/partners/page.tsx Modified +490/-329 Complete refactor of the Partners page: added skeleton loading components for better UX during data fetching, introduced trait badges (Verified, Partnered, Community, Discoverable) from Discord API, added GitHub button support for partners, extracted reusable components (CornerBrackets, SkeletonBlock, PartnerCardSkeleton, PartnerCard), improved data fetching with per-partner loading states and abort controller for cleanup, and enhanced visual hierarchy with trait icons and improved layout structure.

Total Changes: 1 file changed, +490 additions, -329 deletions

🎯 Key Changes:

  • Skeleton Loading States: Introduced PartnerCardSkeleton component that displays animated placeholder cards while Discord API data is being fetched, providing immediate visual feedback and preventing layout shifts
  • Trait Badge System: Added visual badges for Discord server traits (Verified, Partnered, Community, Discoverable) with corresponding icons from lucide-react, pulled directly from Discord's API guild.features array
  • GitHub Integration: Added githubUrl field to partner data structure and corresponding GitHub button in partner cards, enabling direct links to partner GitHub organizations
  • Component Extraction: Refactored monolithic render logic into reusable components (CornerBrackets, SkeletonBlock, PartnerCardSkeleton, PartnerCard) for better maintainability and separation of concerns
  • Improved Data Fetching: Replaced single loading state with per-partner loading map, added AbortController for proper cleanup on unmount, and enhanced error handling with individual partner failure isolation
  • Enhanced Visual Hierarchy: Trait badges appear above description with distinct styling, GitHub button follows website button in action row, and improved spacing and layout consistency

📊 Impact Assessment:

  • Security: ✅ Positive - Added AbortController for proper request cleanup preventing memory leaks; maintained existing security practices with rel="noopener noreferrer" on external links; no new security vulnerabilities introduced
  • Performance: ✅ Positive - Per-partner loading states prevent blocking UI on slow API responses; AbortController prevents unnecessary network requests on component unmount; skeleton loading provides perceived performance improvement; component extraction enables better React memoization opportunities
  • Maintainability: ✅ Significantly Improved - Component extraction reduces complexity from 600+ line monolithic component to focused, single-responsibility components; trait badge system is easily extensible with new Discord features; loading state management is more granular and debuggable; type safety maintained with TypeScript interfaces
  • Testing: ⚠️ Consideration Needed - No test coverage added for new components; skeleton loading states should be tested for proper animation timing; trait badge rendering should be tested with various Discord feature combinations; error handling paths (failed API calls, missing data) should have test coverage; GitHub button conditional rendering needs testing
⚙️ Settings

Severity Threshold: Medium — Balanced feedback — medium and high severity issues only.Change in Settings
Custom Rules: Define your own review rules — Set Custom Rules
PR Summary: Configure PR summary — Change in Settings

📖 User Guide
  • Once repos are connected, PR analysis is automatically enabled. You can disable analysis for this repo from beetleai.dev/analysis
  • Comment @beetle on any PR to start analysis manually
  • Comment @beetle stop to stop any ongoing analysis

Follow us: Beetle · X · LinkedIn

Comment thread app/partners/page.tsx
return;
}

const data = await res.json();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The await res.json() call can throw an error if the response body is not valid JSON. This is not caught by the try-catch block's error handling logic, which could cause the entire Promise.all to fail and leave some partners in a perpetual loading state.

Confidence: 5/5

Suggested Fix

Add explicit error handling for JSON parsing:

Suggested change
const data = await res.json();
const data = await res.json().catch(() => null);
if (!data) {
setDiscordData((prev) => ({ ...prev, [p.inviteCode]: null }));
return;
}
const guild = data.guild;

This ensures that JSON parsing errors are handled gracefully and the loading state is properly updated even when the API returns invalid JSON.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In app/partners/page.tsx around line 546, the await res.json() call can throw an error if the response is not valid JSON, which is not properly handled and could cause Promise.all to fail; wrap the json() call with .catch(() => null) and add a null check immediately after to set the discord data to null and return early if JSON parsing fails, ensuring graceful degradation for all partners.

@devhub-bot

devhub-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Note

Linting checks passed successfully 🎉

All formatting and code quality checks are clean.

You're good to merge 🚀

@calebephrem
calebephrem merged commit a2aec73 into open-devhub:main Jul 9, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant