feat(partners): add skeleton before load - #21
Conversation
calebephrem
commented
Jul 9, 2026
- add skeleton before load
- add github button
- add trait tags (eg. Community, Discoverable) (from discord api)
|
@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. |
Summary by BeetleThis 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):
Total Changes: 1 file changed, +468 additions, -329 deletions 🎯 Key Changes:
📊 Impact Assessment:
⚙️ SettingsSeverity Threshold: 📖 User Guide
|
| 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 })); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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:
- Race conditions: Multiple
setDiscordDataandsetLoadingMapcalls updating state concurrently - Unpredictable state updates: No guarantee of update order
- 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:
| 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.allwithmapinstead offorEachfor proper async handling - Add abort controller for cleanup on unmount (prevents memory leaks)
- Validate HTTP response with
res.okbefore 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
|
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)
Summary by BeetleThis 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):
Total Changes: 1 file changed, +490 additions, -329 deletions 🎯 Key Changes:
📊 Impact Assessment:
⚙️ SettingsSeverity Threshold: 📖 User Guide
|
| return; | ||
| } | ||
|
|
||
| const data = await res.json(); |
There was a problem hiding this comment.
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:
| 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.
|
Note Linting checks passed successfully 🎉 All formatting and code quality checks are clean. You're good to merge 🚀 |