-
Notifications
You must be signed in to change notification settings - Fork 0
New stats page demo v1 #64
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
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: conductor-codes[bot] <207893014+conductor-codes[bot]@users.noreply.github.com>
Co-authored-by: conductor-codes[bot] <207893014+conductor-codes[bot]@users.noreply.github.com>
Co-authored-by: conductor-codes[bot] <207893014+conductor-codes[bot]@users.noreply.github.com>
Co-authored-by: conductor-codes[bot] <207893014+conductor-codes[bot]@users.noreply.github.com>
| const processedData = data | ||
| // Filter by sections (based on the first segment of the path) | ||
| .filter((page) => { | ||
| const pageSection = page.path.split('/')[1]; | ||
| return sections.includes(pageSection); | ||
| }) | ||
| // Filter by difficulty if specified | ||
| .filter((page) => { | ||
| return ( | ||
| selectedDifficulty === 'all' || page.difficulty === selectedDifficulty | ||
| ); | ||
| }) | ||
| // Optionally filter out outdated content | ||
| .filter((page) => { | ||
| if (includeOutdated) return true; | ||
| const lastUpdated = new Date(page.lastUpdated); | ||
| const sixMonthsAgo = new Date(); | ||
| sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); | ||
| return lastUpdated > sixMonthsAgo; | ||
| }) | ||
| // Apply metric thresholds | ||
| .filter((page) => { | ||
| if (minimumViews > 0 && page.views < minimumViews) return false; | ||
| if ( | ||
| minimumCompletionRate > 0 && | ||
| page.completionRate < minimumCompletionRate / 100 | ||
| ) | ||
| return false; | ||
| return true; | ||
| }) | ||
| .map((page) => { | ||
| const viewsNorm = Math.min(page.views / 250000, 1); | ||
| const completionNorm = page.completionRate; | ||
| const engagementScore = 0.6 * viewsNorm + 0.4 * completionNorm; | ||
|
|
||
| return { | ||
| ...page, | ||
| engagementScore, | ||
| }; | ||
| }) | ||
| // Sort the computed results | ||
| .sort((a, b) => { | ||
| let comparison = 0; | ||
| if (selectedSortMethod === 'views') { | ||
| comparison = b.views - a.views; | ||
| } else if (selectedSortMethod === 'completion') { | ||
| comparison = b.completionRate - a.completionRate; | ||
| } else if (selectedSortMethod === 'timeSpent') { | ||
| comparison = b.avgTimeSpent - a.avgTimeSpent; | ||
| } else if (selectedSortMethod === 'engagement') { | ||
| comparison = b.engagementScore - a.engagementScore; | ||
| } else if (selectedSortMethod === 'lastUpdated') { | ||
| comparison = new Date(b.lastUpdated) - new Date(a.lastUpdated); | ||
| } | ||
| return sortOrder === 'desc' ? comparison : -comparison; | ||
| }) | ||
| // Limit the results to displayLimit entries. | ||
| .slice(0, displayLimit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const processedData = data | |
| // Filter by sections (based on the first segment of the path) | |
| .filter((page) => { | |
| const pageSection = page.path.split('/')[1]; | |
| return sections.includes(pageSection); | |
| }) | |
| // Filter by difficulty if specified | |
| .filter((page) => { | |
| return ( | |
| selectedDifficulty === 'all' || page.difficulty === selectedDifficulty | |
| ); | |
| }) | |
| // Optionally filter out outdated content | |
| .filter((page) => { | |
| if (includeOutdated) return true; | |
| const lastUpdated = new Date(page.lastUpdated); | |
| const sixMonthsAgo = new Date(); | |
| sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); | |
| return lastUpdated > sixMonthsAgo; | |
| }) | |
| // Apply metric thresholds | |
| .filter((page) => { | |
| if (minimumViews > 0 && page.views < minimumViews) return false; | |
| if ( | |
| minimumCompletionRate > 0 && | |
| page.completionRate < minimumCompletionRate / 100 | |
| ) | |
| return false; | |
| return true; | |
| }) | |
| .map((page) => { | |
| const viewsNorm = Math.min(page.views / 250000, 1); | |
| const completionNorm = page.completionRate; | |
| const engagementScore = 0.6 * viewsNorm + 0.4 * completionNorm; | |
| return { | |
| ...page, | |
| engagementScore, | |
| }; | |
| }) | |
| // Sort the computed results | |
| .sort((a, b) => { | |
| let comparison = 0; | |
| if (selectedSortMethod === 'views') { | |
| comparison = b.views - a.views; | |
| } else if (selectedSortMethod === 'completion') { | |
| comparison = b.completionRate - a.completionRate; | |
| } else if (selectedSortMethod === 'timeSpent') { | |
| comparison = b.avgTimeSpent - a.avgTimeSpent; | |
| } else if (selectedSortMethod === 'engagement') { | |
| comparison = b.engagementScore - a.engagementScore; | |
| } else if (selectedSortMethod === 'lastUpdated') { | |
| comparison = new Date(b.lastUpdated) - new Date(a.lastUpdated); | |
| } | |
| return sortOrder === 'desc' ? comparison : -comparison; | |
| }) | |
| // Limit the results to displayLimit entries. | |
| .slice(0, displayLimit); | |
| const processedData = useMemo(() => { | |
| return data | |
| // Filter by sections (based on the first segment of the path) | |
| .filter((page) => { | |
| const pageSection = page.path.split('/')[1]; | |
| return sections.includes(pageSection); | |
| }) | |
| // Filter by difficulty if specified | |
| .filter((page) => { | |
| return ( | |
| selectedDifficulty === 'all' || page.difficulty === selectedDifficulty | |
| ); | |
| }) | |
| // Optionally filter out outdated content | |
| .filter((page) => { | |
| if (includeOutdated) return true; | |
| const lastUpdated = new Date(page.lastUpdated); | |
| const sixMonthsAgo = new Date(); | |
| sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); | |
| return lastUpdated > sixMonthsAgo; | |
| }) | |
| // Apply metric thresholds | |
| .filter((page) => { | |
| if (minimumViews > 0 && page.views < minimumViews) return false; | |
| if ( | |
| minimumCompletionRate > 0 && | |
| page.completionRate < minimumCompletionRate / 100 | |
| ) | |
| return false; | |
| return true; | |
| }) | |
| .map((page) => { | |
| const viewsNorm = Math.min(page.views / 250000, 1); | |
| const completionNorm = page.completionRate; | |
| const engagementScore = 0.6 * viewsNorm + 0.4 * completionNorm; | |
| return { | |
| ...page, | |
| engagementScore, | |
| }; | |
| }) | |
| // Sort the computed results | |
| .sort((a, b) => { | |
| let comparison = 0; | |
| if (selectedSortMethod === 'views') { | |
| comparison = b.views - a.views; | |
| } else if (selectedSortMethod === 'completion') { | |
| comparison = b.completionRate - a.completionRate; | |
| } else if (selectedSortMethod === 'timeSpent') { | |
| comparison = b.avgTimeSpent - a.avgTimeSpent; | |
| } else if (selectedSortMethod === 'engagement') { | |
| comparison = b.engagementScore - a.engagementScore; | |
| } else if (selectedSortMethod === 'lastUpdated') { | |
| comparison = new Date(b.lastUpdated) - new Date(a.lastUpdated); | |
| } | |
| return sortOrder === 'desc' ? comparison : -comparison; | |
| }) | |
| // Limit the results to displayLimit entries. | |
| .slice(0, displayLimit); | |
| }, [data, sections, selectedDifficulty, includeOutdated, minimumViews, minimumCompletionRate, selectedSortMethod, sortOrder, displayLimit]); |
This component performs heavy computations directly in the render cycle. The processedData variable includes multiple filter operations, a map operation with calculations, and a sort operation, all executed on every render. As the dataset grows larger, these operations will become more expensive and potentially cause performance issues, impacting Core Web Vitals metrics (FCP and LCP).
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| <img | ||
| src="/images/meta-gradient.png" | ||
| width="800" | ||
| height="3" | ||
| alt="" | ||
| style={{height: '3px', width: '50%'}} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <img | |
| src="/images/meta-gradient.png" | |
| width="800" | |
| height="3" | |
| alt="" | |
| style={{height: '3px', width: '50%'}} | |
| /> | |
| <img | |
| src="/images/meta-gradient.png" | |
| width="1000" | |
| height="6" | |
| alt="" | |
| style={{width: '50%'}} | |
| /> |
The image has both HTML attributes (width, height) and inline styles that override those dimensions, causing potential layout shifts. When the browser initially parses the image, it allocates space based on the HTML width/height attributes (800x3), but then the CSS styles change these dimensions (50% width, 3px height). This inconsistency can cause Cumulative Layout Shift (CLS) issues as the page renders.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| <div | ||
| className="stats-chart" | ||
| style={{ | ||
| marginBottom: '30px', | ||
| height: '120px', | ||
| display: 'flex', | ||
| alignItems: 'flex-end', | ||
| gap: '8px', | ||
| }}> | ||
| {processedData.slice(0, 7).map((page, idx) => ( | ||
| <button | ||
| key={idx} | ||
| style={{ | ||
| height: `${Math.max(5, (page.views / 250000) * 100)}%`, | ||
| backgroundColor: getDifficultyColor(page.difficulty), | ||
| flex: 1, | ||
| borderRadius: '4px 4px 0 0', | ||
| position: 'relative', | ||
| cursor: 'pointer', | ||
| minHeight: '4px', | ||
| transition: 'height 0.3s ease-out', | ||
| }} | ||
| onClick={() => router.push(page.path)} | ||
| onMouseEnter={() => setHoveredRow(page.path)} | ||
| onMouseLeave={() => setHoveredRow(null)}> | ||
| {hoveredRow === page.path && ( | ||
| <div | ||
| style={{ | ||
| position: 'absolute', | ||
| bottom: '100%', | ||
| left: '50%', | ||
| transform: 'translateX(-50%)', | ||
| backgroundColor: 'rgba(0,0,0,0.7)', | ||
| color: '#ddd', | ||
| padding: '4px 8px', | ||
| borderRadius: '4px', | ||
| fontSize: '0.7rem', | ||
| whiteSpace: 'nowrap', | ||
| zIndex: 10, | ||
| }}> | ||
| {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} | ||
| views | ||
| </div> | ||
| )} | ||
| </button> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <div | |
| className="stats-chart" | |
| style={{ | |
| marginBottom: '30px', | |
| height: '120px', | |
| display: 'flex', | |
| alignItems: 'flex-end', | |
| gap: '8px', | |
| }}> | |
| {processedData.slice(0, 7).map((page, idx) => ( | |
| <button | |
| key={idx} | |
| style={{ | |
| height: `${Math.max(5, (page.views / 250000) * 100)}%`, | |
| backgroundColor: getDifficultyColor(page.difficulty), | |
| flex: 1, | |
| borderRadius: '4px 4px 0 0', | |
| position: 'relative', | |
| cursor: 'pointer', | |
| minHeight: '4px', | |
| transition: 'height 0.3s ease-out', | |
| }} | |
| onClick={() => router.push(page.path)} | |
| onMouseEnter={() => setHoveredRow(page.path)} | |
| onMouseLeave={() => setHoveredRow(null)}> | |
| {hoveredRow === page.path && ( | |
| <div | |
| style={{ | |
| position: 'absolute', | |
| bottom: '100%', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| backgroundColor: 'rgba(0,0,0,0.7)', | |
| color: '#ddd', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '0.7rem', | |
| whiteSpace: 'nowrap', | |
| zIndex: 10, | |
| }}> | |
| {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} | |
| views | |
| </div> | |
| )} | |
| </button> | |
| ))} | |
| </div> | |
| <div | |
| className="stats-chart" | |
| style={{ | |
| marginBottom: '30px', | |
| height: '120px', | |
| display: 'flex', | |
| alignItems: 'flex-end', | |
| gap: '8px', | |
| }}> | |
| {processedData.length === 0 ? ( | |
| // Placeholder with fixed dimensions to prevent layout shift | |
| Array(7).fill(0).map((_, idx) => ( | |
| <div | |
| key={idx} | |
| style={{ | |
| height: '85%', // Approximate height based on max data | |
| backgroundColor: '#e0e0e0', | |
| flex: 1, | |
| borderRadius: '4px 4px 0 0', | |
| minHeight: '85px', | |
| }} | |
| /> | |
| )) | |
| ) : ( | |
| processedData.slice(0, 7).map((page, idx) => ( | |
| <button | |
| key={idx} | |
| style={{ | |
| height: `${Math.max(5, (page.views / 250000) * 100)}%`, | |
| backgroundColor: getDifficultyColor(page.difficulty), | |
| flex: 1, | |
| borderRadius: '4px 4px 0 0', | |
| position: 'relative', | |
| cursor: 'pointer', | |
| minHeight: '85px', // Set minimum height to prevent layout shift | |
| }} | |
| onClick={() => router.push(page.path)} | |
| onMouseEnter={() => setHoveredRow(page.path)} | |
| onMouseLeave={() => setHoveredRow(null)}> | |
| {hoveredRow === page.path && ( | |
| <div | |
| style={{ | |
| position: 'absolute', | |
| bottom: '100%', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| backgroundColor: 'rgba(0,0,0,0.7)', | |
| color: '#ddd', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '0.7rem', | |
| whiteSpace: 'nowrap', | |
| zIndex: 10, | |
| }}> | |
| {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} | |
| views | |
| </div> | |
| )} | |
| </button> | |
| )) | |
| )} | |
| </div> |
The stats-chart visualization dynamically calculates heights based on data loaded asynchronously, without reserving proper space beforehand. The initial render will show minimal or no content, then expand to accommodate the data when it loads. This creates a significant layout shift that negatively impacts the Cumulative Layout Shift (CLS) metric. While there is a minHeight set, it's only 4px, which is much smaller than the final rendered heights.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| useEffect(() => { | ||
| const loadData = async () => { | ||
| try { | ||
| // This will code-split the data into a separate chunk | ||
| const module = await import('../data/pageViewData'); | ||
| setData(module.pageViewData); | ||
| } catch (error) { | ||
| console.error('Failed to load page view data:', error); | ||
| } | ||
| }; | ||
| loadData(); | ||
| }, []); | ||
|
|
||
| const processedData = data | ||
| // Filter by sections (based on the first segment of the path) | ||
| .filter((page) => { | ||
| const pageSection = page.path.split('/')[1]; | ||
| return sections.includes(pageSection); | ||
| }) | ||
| // Filter by difficulty if specified | ||
| .filter((page) => { | ||
| return ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| useEffect(() => { | |
| const loadData = async () => { | |
| try { | |
| // This will code-split the data into a separate chunk | |
| const module = await import('../data/pageViewData'); | |
| setData(module.pageViewData); | |
| } catch (error) { | |
| console.error('Failed to load page view data:', error); | |
| } | |
| }; | |
| loadData(); | |
| }, []); | |
| const processedData = data | |
| // Filter by sections (based on the first segment of the path) | |
| .filter((page) => { | |
| const pageSection = page.path.split('/')[1]; | |
| return sections.includes(pageSection); | |
| }) | |
| // Filter by difficulty if specified | |
| .filter((page) => { | |
| return ( | |
| const [data, setData] = useState([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| useEffect(() => { | |
| const loadData = async () => { | |
| try { | |
| // This will code-split the data into a separate chunk | |
| const module = await import('../data/pageViewData'); | |
| setData(module.pageViewData); | |
| } catch (error) { | |
| console.error('Failed to load page view data:', error); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| loadData(); | |
| }, []); | |
| // Create placeholder data for the loading state | |
| const placeholderData = isLoading | |
| ? Array(displayLimit).fill().map((_, i) => ({ | |
| path: `/placeholder/item-${i + 1}`, | |
| views: 0, | |
| completionRate: 0, | |
| avgTimeSpent: 0, | |
| lastUpdated: '2000-01-01', | |
| difficulty: 'beginner', | |
| engagementScore: 0, | |
| })) | |
| : []; | |
| // Use real data when loaded, placeholder data when loading | |
| const processedData = isLoading | |
| ? placeholderData | |
| : data |
The component loads data asynchronously without providing appropriate placeholders or skeleton loaders. Initially, the table renders empty or with default data, then re-renders with actual data once loaded. This causes significant layout shifts as the table suddenly populates with rows, negatively impacting the Cumulative Layout Shift (CLS) metric. There's no skeleton UI or space reservation to mitigate this layout shift during the loading phase.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| {processedData.map((page, idx) => ( | ||
| <tr | ||
| key={page.path} | ||
| style={{ | ||
| backgroundColor: idx % 2 === 0 ? '#ffffff' : '#f8fafc', | ||
| cursor: 'pointer', | ||
| transition: 'background-color 0.2s', | ||
| }} | ||
| onMouseEnter={() => setHoveredRow(page.path)} | ||
| onMouseLeave={() => setHoveredRow(null)} | ||
| onClick={() => router.push(page.path)}> | ||
| <td | ||
| style={{ | ||
| padding: '12px 16px', | ||
| borderBottom: '1px solid #e2e8f0', | ||
| color: '#0969da', | ||
| fontWeight: hoveredRow === page.path ? 'bold' : 'normal', | ||
| }}> | ||
| {page.path} | ||
| </td> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| {processedData.map((page, idx) => ( | |
| <tr | |
| key={page.path} | |
| style={{ | |
| backgroundColor: idx % 2 === 0 ? '#ffffff' : '#f8fafc', | |
| cursor: 'pointer', | |
| transition: 'background-color 0.2s', | |
| }} | |
| onMouseEnter={() => setHoveredRow(page.path)} | |
| onMouseLeave={() => setHoveredRow(null)} | |
| onClick={() => router.push(page.path)}> | |
| <td | |
| style={{ | |
| padding: '12px 16px', | |
| borderBottom: '1px solid #e2e8f0', | |
| color: '#0969da', | |
| fontWeight: hoveredRow === page.path ? 'bold' : 'normal', | |
| }}> | |
| {page.path} | |
| </td> | |
| {processedData.map((page, idx) => ( | |
| <tr | |
| key={page.path} | |
| style={{ | |
| backgroundColor: idx % 2 === 0 ? '#ffffff' : '#f8fafc', | |
| cursor: 'pointer', | |
| transition: 'background-color 0.2s', | |
| }} | |
| onMouseEnter={() => setHoveredRow(page.path)} | |
| onMouseLeave={() => setHoveredRow(null)} | |
| onClick={() => router.push(page.path)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| router.push(page.path); | |
| } | |
| }} | |
| tabIndex={0} | |
| role="link" | |
| aria-label={`Go to ${page.path}`}> | |
| <td | |
| style={{ | |
| padding: '12px 16px', | |
| borderBottom: '1px solid #e2e8f0', | |
| color: '#0969da', | |
| fontWeight: hoveredRow === page.path ? 'bold' : 'normal', | |
| }}> | |
| {page.path} | |
| </td> |
The table lacks proper accessibility features. The table rows are clickable without proper semantic indication to screen readers or keyboard users that they are interactive elements. Using onClick handlers on tr elements without additional keyboard handling creates an accessibility barrier for keyboard-only users.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| <button | ||
| onClick={() => { | ||
| setSelectedDifficulty('all'); | ||
| setSelectedSortMethod('views'); | ||
| }} | ||
| style={{ | ||
| padding: '8px 16px', | ||
| backgroundColor: '#0969da', | ||
| color: 'white', | ||
| border: 'none', | ||
| borderRadius: '6px', | ||
| cursor: 'pointer', | ||
| }}> | ||
| <span className="icon">↺</span> | ||
| </button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <button | |
| onClick={() => { | |
| setSelectedDifficulty('all'); | |
| setSelectedSortMethod('views'); | |
| }} | |
| style={{ | |
| padding: '8px 16px', | |
| backgroundColor: '#0969da', | |
| color: 'white', | |
| border: 'none', | |
| borderRadius: '6px', | |
| cursor: 'pointer', | |
| }}> | |
| <span className="icon">↺</span> | |
| </button> | |
| <button | |
| aria-label="Reset filters" | |
| onClick={() => { | |
| setSelectedDifficulty('all'); | |
| setSelectedSortMethod('views'); | |
| }} | |
| style={{ | |
| padding: '8px 16px', | |
| backgroundColor: '#0969da', | |
| color: 'white', | |
| border: 'none', | |
| borderRadius: '6px', | |
| cursor: 'pointer', | |
| }}> | |
| <span className="icon">↺</span> | |
| </button> |
The reset button uses an icon (↺) without an accessible name. This makes it impossible for screen reader users to understand the button's purpose. Buttons should have descriptive accessible names through aria-label, aria-labelledby, or meaningful text content.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| {processedData.slice(0, 7).map((page, idx) => ( | ||
| <button | ||
| key={idx} | ||
| style={{ | ||
| height: `${Math.max(5, (page.views / 250000) * 100)}%`, | ||
| backgroundColor: getDifficultyColor(page.difficulty), | ||
| flex: 1, | ||
| borderRadius: '4px 4px 0 0', | ||
| position: 'relative', | ||
| cursor: 'pointer', | ||
| minHeight: '4px', | ||
| transition: 'height 0.3s ease-out', | ||
| }} | ||
| onClick={() => router.push(page.path)} | ||
| onMouseEnter={() => setHoveredRow(page.path)} | ||
| onMouseLeave={() => setHoveredRow(null)}> | ||
| {hoveredRow === page.path && ( | ||
| <div | ||
| style={{ | ||
| position: 'absolute', | ||
| bottom: '100%', | ||
| left: '50%', | ||
| transform: 'translateX(-50%)', | ||
| backgroundColor: 'rgba(0,0,0,0.7)', | ||
| color: '#ddd', | ||
| padding: '4px 8px', | ||
| borderRadius: '4px', | ||
| fontSize: '0.7rem', | ||
| whiteSpace: 'nowrap', | ||
| zIndex: 10, | ||
| }}> | ||
| {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} | ||
| views | ||
| </div> | ||
| )} | ||
| </button> | ||
| ))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| {processedData.slice(0, 7).map((page, idx) => ( | |
| <button | |
| key={idx} | |
| style={{ | |
| height: `${Math.max(5, (page.views / 250000) * 100)}%`, | |
| backgroundColor: getDifficultyColor(page.difficulty), | |
| flex: 1, | |
| borderRadius: '4px 4px 0 0', | |
| position: 'relative', | |
| cursor: 'pointer', | |
| minHeight: '4px', | |
| transition: 'height 0.3s ease-out', | |
| }} | |
| onClick={() => router.push(page.path)} | |
| onMouseEnter={() => setHoveredRow(page.path)} | |
| onMouseLeave={() => setHoveredRow(null)}> | |
| {hoveredRow === page.path && ( | |
| <div | |
| style={{ | |
| position: 'absolute', | |
| bottom: '100%', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| backgroundColor: 'rgba(0,0,0,0.7)', | |
| color: '#ddd', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '0.7rem', | |
| whiteSpace: 'nowrap', | |
| zIndex: 10, | |
| }}> | |
| {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} | |
| views | |
| </div> | |
| )} | |
| </button> | |
| ))} | |
| {processedData.slice(0, 7).map((page, idx) => ( | |
| <button | |
| key={idx} | |
| style={{ | |
| height: `${Math.max(5, (page.views / 250000) * 100)}%`, | |
| backgroundColor: getDifficultyColor(page.difficulty), | |
| flex: 1, | |
| borderRadius: '4px 4px 0 0', | |
| position: 'relative', | |
| cursor: 'pointer', | |
| minHeight: '4px', | |
| transition: 'height 0.3s ease-out', | |
| }} | |
| aria-label={`${page.path.split('/').pop()} page: ${page.views.toLocaleString()} views, ${page.difficulty} difficulty level. Click to navigate to ${page.path}`} | |
| onClick={() => router.push(page.path)} | |
| onMouseEnter={() => setHoveredRow(page.path)} | |
| onMouseLeave={() => setHoveredRow(null)}> | |
| {hoveredRow === page.path && ( | |
| <div | |
| style={{ | |
| position: 'absolute', | |
| bottom: '100%', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| backgroundColor: 'rgba(0,0,0,0.7)', | |
| color: '#ddd', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '0.7rem', | |
| whiteSpace: 'nowrap', | |
| zIndex: 10, | |
| }}> | |
| {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} | |
| views | |
| </div> | |
| )} | |
| </button> | |
| ))} |
The bar chart buttons lack proper accessibility features. These interactive elements are missing accessible names and are not properly labeled. Screen reader users won't understand what these bars represent, and keyboard users will have difficulty understanding the context when navigating to these elements.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| <NavItem | ||
| isActive={section === 'community'} | ||
| url="/community/statistics"> | ||
| Analytics | ||
| </NavItem> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <NavItem | |
| isActive={section === 'community'} | |
| url="/community/statistics"> | |
| Analytics | |
| </NavItem> | |
| <NavItem | |
| isActive={section === 'community' && asPath.includes('/statistics')} | |
| url="/community/statistics"> | |
| Analytics | |
| </NavItem> |
The Analytics nav item is marked with isActive={section === 'community'} which is inconsistent with its URL ("/community/statistics"). This creates a confusing experience for screen reader users and those using assistive technologies as the active state won't match the actual page content.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
No description provided.