From 9266b22fb4360e88353f3e616961e8a43696ad98 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:49:47 +0100 Subject: [PATCH] Fix NaN rendering error in About section from Directus CMS data (#62) * Initial plan * Fix NaN error in gaming and custom activities rendering Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Add unit tests for NaN handling in ActivityFeed Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Refactor: Extract gaming text logic to helper function Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Address code review feedback: improve types and tests Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix NaN error in About component (tech stack and hobbies) Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Update TypeScript interfaces to match actual data types Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Improve NaN handling with defensive logging and better null checks Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --- .../components/ActivityFeed.test.tsx | 150 ++++++++++++++++++ app/components/About.tsx | 67 ++++++-- app/components/ActivityFeed.tsx | 32 ++-- 3 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 app/__tests__/components/ActivityFeed.test.tsx diff --git a/app/__tests__/components/ActivityFeed.test.tsx b/app/__tests__/components/ActivityFeed.test.tsx new file mode 100644 index 0000000..6ee4d13 --- /dev/null +++ b/app/__tests__/components/ActivityFeed.test.tsx @@ -0,0 +1,150 @@ +import '@testing-library/jest-dom'; + +/** + * Unit tests for ActivityFeed NaN handling + * + * This test suite validates that the ActivityFeed component correctly handles + * NaN and numeric values in gaming and custom activity data to prevent + * "Received NaN for the children attribute" React errors. + */ +describe('ActivityFeed NaN Handling', () => { + describe('Gaming activity rendering logic', () => { + // Helper function to simulate getSafeGamingText behavior + const getSafeGamingText = (details: string | number | undefined, state: string | number | undefined, fallback: string): string => { + if (typeof details === 'string' && details.trim().length > 0) return details; + if (typeof state === 'string' && state.trim().length > 0) return state; + if (typeof details === 'number' && !isNaN(details)) return String(details); + if (typeof state === 'number' && !isNaN(state)) return String(state); + return fallback; + }; + + it('should safely handle NaN in gaming.details', () => { + const result = getSafeGamingText(NaN, 'Playing', 'Playing...'); + expect(result).toBe('Playing'); // Should fall through NaN to state + expect(result).not.toBe(NaN); + expect(typeof result).toBe('string'); + }); + + it('should safely handle NaN in both gaming.details and gaming.state', () => { + const result = getSafeGamingText(NaN, NaN, 'Playing...'); + expect(result).toBe('Playing...'); // Should use fallback + expect(typeof result).toBe('string'); + }); + + it('should prioritize string details over numeric state', () => { + const result = getSafeGamingText('Details text', 42, 'Playing...'); + expect(result).toBe('Details text'); // String details takes precedence + expect(typeof result).toBe('string'); + }); + + it('should prioritize string state over numeric details', () => { + const result = getSafeGamingText(42, 'State text', 'Playing...'); + expect(result).toBe('State text'); // String state takes precedence over numeric details + expect(typeof result).toBe('string'); + }); + + it('should convert valid numeric details to string', () => { + const result = getSafeGamingText(42, undefined, 'Playing...'); + expect(result).toBe('42'); + expect(typeof result).toBe('string'); + }); + + it('should handle empty strings correctly', () => { + const result1 = getSafeGamingText('', 'Playing', 'Playing...'); + expect(result1).toBe('Playing'); // Empty string should fall through to state + + const result2 = getSafeGamingText(' ', 'Playing', 'Playing...'); + expect(result2).toBe('Playing'); // Whitespace-only should fall through to state + }); + + it('should convert gaming.name to string safely', () => { + const validName = String('Test Game' || ''); + expect(validName).toBe('Test Game'); + expect(typeof validName).toBe('string'); + + // In the actual code, we use String(data.gaming.name || '') + // If data.gaming.name is NaN, (NaN || '') evaluates to '' because NaN is falsy + const nanName = String(NaN || ''); + expect(nanName).toBe(''); // NaN is falsy, so it falls back to '' + expect(typeof nanName).toBe('string'); + }); + }); + + describe('Custom activities progress handling', () => { + it('should only render progress bar when progress is a valid number', () => { + const validProgress = 75; + const shouldRender = validProgress !== undefined && + typeof validProgress === 'number' && + !isNaN(validProgress); + expect(shouldRender).toBe(true); + }); + + it('should not render progress bar when progress is NaN', () => { + const invalidProgress = NaN; + const shouldRender = invalidProgress !== undefined && + typeof invalidProgress === 'number' && + !isNaN(invalidProgress); + expect(shouldRender).toBe(false); + }); + + it('should not render progress bar when progress is undefined', () => { + const undefinedProgress = undefined; + const shouldRender = undefinedProgress !== undefined && + typeof undefinedProgress === 'number' && + !isNaN(undefinedProgress); + expect(shouldRender).toBe(false); + }); + }); + + describe('Custom activities dynamic field rendering', () => { + it('should safely convert valid numeric values to string', () => { + const value = 42; + const shouldRender = typeof value === 'string' || + (typeof value === 'number' && !isNaN(value)); + + expect(shouldRender).toBe(true); + + if (shouldRender) { + const stringValue = String(value); + expect(stringValue).toBe('42'); + expect(typeof stringValue).toBe('string'); + } + }); + + it('should not render NaN values', () => { + const value = NaN; + const shouldRender = typeof value === 'string' || + (typeof value === 'number' && !isNaN(value)); + + expect(shouldRender).toBe(false); + }); + + it('should render valid string values', () => { + const value = 'Test String'; + const shouldRender = typeof value === 'string' || + (typeof value === 'number' && !isNaN(value)); + + expect(shouldRender).toBe(true); + + if (shouldRender) { + const stringValue = String(value); + expect(stringValue).toBe('Test String'); + expect(typeof stringValue).toBe('string'); + } + }); + + it('should render zero as a valid numeric value', () => { + const value = 0; + const shouldRender = typeof value === 'string' || + (typeof value === 'number' && !isNaN(value)); + + expect(shouldRender).toBe(true); + + if (shouldRender) { + const stringValue = String(value); + expect(stringValue).toBe('0'); + expect(typeof stringValue).toBe('string'); + } + }); + }); +}); diff --git a/app/components/About.tsx b/app/components/About.tsx index 27c8960..7888e7e 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -11,7 +11,7 @@ import CurrentlyReading from "./CurrentlyReading"; // Type definitions for CMS data interface TechStackItem { id: string; - name: string; + name: string | number | null | undefined; url?: string; icon_url?: string; sort: number; @@ -30,7 +30,7 @@ interface Hobby { id: string; key: string; icon: string; - title: string; + title: string | number | null | undefined; description?: string; } @@ -172,20 +172,59 @@ const About = () => { // Use CMS Hobbies if available, otherwise fallback const hobbies = hobbiesFromCMS - ? hobbiesFromCMS.map((hobby: Hobby) => ({ - icon: iconMap[hobby.icon] || Code, - text: hobby.title - })) + ? hobbiesFromCMS + .map((hobby: Hobby) => { + // Convert to string, handling NaN/null/undefined + const text = hobby.title == null || (typeof hobby.title === 'number' && isNaN(hobby.title)) + ? '' + : String(hobby.title); + return { + icon: iconMap[hobby.icon] || Code, + text + }; + }) + .filter(h => { + const isValid = h.text.trim().length > 0; + if (!isValid && process.env.NODE_ENV === 'development') { + console.log('[About] Filtered out invalid hobby:', h); + } + return isValid; + }) : hobbiesFallback; // Use CMS Tech Stack if available, otherwise fallback const techStack = techStackFromCMS - ? techStackFromCMS.map((cat: TechStackCategory) => ({ - key: cat.key, - category: cat.name, - icon: iconMap[cat.icon] || Code, - items: cat.items.map((item: TechStackItem) => item.name) - })) + ? techStackFromCMS.map((cat: TechStackCategory) => { + const items = cat.items + .map((item: TechStackItem) => { + // Convert to string, handling NaN/null/undefined + if (item.name == null || (typeof item.name === 'number' && isNaN(item.name))) { + if (process.env.NODE_ENV === 'development') { + console.log('[About] Invalid item.name in category', cat.key, ':', item); + } + return ''; + } + return String(item.name); + }) + .filter(name => { + const isValid = name.trim().length > 0; + if (!isValid && process.env.NODE_ENV === 'development') { + console.log('[About] Filtered out empty item name in category', cat.key); + } + return isValid; + }); + + if (items.length === 0 && process.env.NODE_ENV === 'development') { + console.warn('[About] Category has no valid items after filtering:', cat.key); + } + + return { + key: cat.key, + category: cat.name, + icon: iconMap[cat.icon] || Code, + items + }; + }) : techStackFallback; return ( @@ -297,7 +336,7 @@ const About = () => { : "bg-liquid-sky/10 border-liquid-sky/30 hover:bg-liquid-sky/20 hover:border-liquid-sky/50" }`} > - {item} + {String(item)} ))} @@ -336,7 +375,7 @@ const About = () => { > - {hobby.text} + {String(hobby.text)} ))} diff --git a/app/components/ActivityFeed.tsx b/app/components/ActivityFeed.tsx index cce9d27..8406c54 100644 --- a/app/components/ActivityFeed.tsx +++ b/app/components/ActivityFeed.tsx @@ -37,8 +37,8 @@ interface StatusData { isPlaying: boolean; name: string; image: string | null; - state?: string; - details?: string; + state?: string | number; + details?: string | number; } | null; coding: { isActive: boolean; @@ -54,6 +54,20 @@ interface StatusData { customActivities?: Record; // Dynamisch! } +/** + * Safely converts gaming activity details/state to a string, filtering out NaN values. + * Priority: string details > string state > numeric details > numeric state > fallback + */ +function getSafeGamingText(details: string | number | undefined, state: string | number | undefined, fallback: string): string { + // Check string values first (maintain original precedence) + if (typeof details === 'string' && details.trim().length > 0) return details; + if (typeof state === 'string' && state.trim().length > 0) return state; + // Check numeric values, but filter out NaN + if (typeof details === 'number' && !isNaN(details)) return String(details); + if (typeof state === 'number' && !isNaN(state)) return String(state); + return fallback; +} + export default function ActivityFeed() { const [data, setData] = useState(null); const [isExpanded, setIsExpanded] = useState(true); @@ -1803,12 +1817,10 @@ export default function ActivityFeed() {

- {data.gaming.name} + {String(data.gaming.name || "")}

- {data.gaming.details || - data.gaming.state || - "Playing..."} + {getSafeGamingText(data.gaming.details, data.gaming.state, "Playing...")}

@@ -1967,7 +1979,7 @@ export default function ActivityFeed() { )} {/* Progress Bar wenn vorhanden */} - {activity.progress !== undefined && typeof activity.progress === 'number' && ( + {activity.progress !== undefined && typeof activity.progress === 'number' && !isNaN(activity.progress) && (
{key.replace(/_/g, ' ')}: - {value} + {String(value)}
); }