-
Notifications
You must be signed in to change notification settings - Fork 0
New Stats Page #58
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?
New Stats Page #58
Conversation
| <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> |
This button does not have an accessible name, which means screen readers won't be able to convey its purpose to users. To fix this, you can add an aria-label that describes the action clearly. In this case, since it resets filters, a label like "Reset filters" would work well.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| <img | ||
| src="/images/meta-gradient.png" | ||
| 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" | |
| style={{height: '3px', width: '50%'}} | |
| /> | |
| <img | |
| src="/images/meta-gradient.png" | |
| alt="" | |
| width="600" | |
| height="3" | |
| style={{ height: '3px', width: '50%' }} | |
| /> |
The img element is missing width and height attributes. This can cause layout shifts (CLS) as the browser renders the page, especially if the image loads after the initial layout. Provide explicit dimensions or use CSS aspect-ratio to reserve space. Since the image appears decorative (a gradient line), consider using CSS for this effect or provide appropriate dimensions if it conveys meaning.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
| const processedData = pageViewData | ||
| // 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; | ||
| }) | ||
| // Compute an engagement score for demonstration purposes | ||
| .map((page) => { | ||
| const viewsNorm = Math.min(page.views / 250000, 1); | ||
| const completionNorm = page.completionRate; | ||
| const timeSpentNorm = Math.min(page.avgTimeSpent / 600, 1); | ||
|
|
||
| // Freshness factor computed from last update date | ||
| const lastUpdated = new Date(page.lastUpdated); | ||
| const monthsOld = (new Date() - lastUpdated) / (30 * 24 * 60 * 60 * 1000); | ||
| const freshnessScore = Math.max(0, 1 - monthsOld / 24); | ||
|
|
||
| // Difficulty multiplier for weighting | ||
| let difficultyMultiplier = 1; | ||
| if (page.difficulty === 'beginner') difficultyMultiplier = 0.9; | ||
| if (page.difficulty === 'intermediate') difficultyMultiplier = 1.0; | ||
| if (page.difficulty === 'advanced') difficultyMultiplier = 1.2; | ||
|
|
||
| // Compute weighted engagement score | ||
| const engagementScore = | ||
| (0.4 * viewsNorm + | ||
| 0.3 * completionNorm + | ||
| 0.2 * timeSpentNorm + | ||
| 0.1 * Math.pow(freshnessScore, 2)) * | ||
| difficultyMultiplier; | ||
|
|
||
| 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 = pageViewData | |
| // 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; | |
| }) | |
| // Compute an engagement score for demonstration purposes | |
| .map((page) => { | |
| const viewsNorm = Math.min(page.views / 250000, 1); | |
| const completionNorm = page.completionRate; | |
| const timeSpentNorm = Math.min(page.avgTimeSpent / 600, 1); | |
| // Freshness factor computed from last update date | |
| const lastUpdated = new Date(page.lastUpdated); | |
| const monthsOld = (new Date() - lastUpdated) / (30 * 24 * 60 * 60 * 1000); | |
| const freshnessScore = Math.max(0, 1 - monthsOld / 24); | |
| // Difficulty multiplier for weighting | |
| let difficultyMultiplier = 1; | |
| if (page.difficulty === 'beginner') difficultyMultiplier = 0.9; | |
| if (page.difficulty === 'intermediate') difficultyMultiplier = 1.0; | |
| if (page.difficulty === 'advanced') difficultyMultiplier = 1.2; | |
| // Compute weighted engagement score | |
| const engagementScore = | |
| (0.4 * viewsNorm + | |
| 0.3 * completionNorm + | |
| 0.2 * timeSpentNorm + | |
| 0.1 * Math.pow(freshnessScore, 2)) * | |
| difficultyMultiplier; | |
| 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(() => | |
| pageViewData | |
| // 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; | |
| }) | |
| // Compute an engagement score for demonstration purposes | |
| .map((page) => { | |
| const viewsNorm = Math.min(page.views / 250000, 1); | |
| const completionNorm = page.completionRate; | |
| const timeSpentNorm = Math.min(page.avgTimeSpent / 600, 1); | |
| const lastUpdated = new Date(page.lastUpdated); | |
| const monthsOld = (new Date() - lastUpdated) / (30 * 24 * 60 * 60 * 1000); | |
| const freshnessScore = Math.max(0, 1 - monthsOld / 24); | |
| let difficultyMultiplier = 1; | |
| if (page.difficulty === 'beginner') difficultyMultiplier = 0.9; | |
| if (page.difficulty === 'intermediate') difficultyMultiplier = 1.0; | |
| if (page.difficulty === 'advanced') difficultyMultiplier = 1.2; | |
| const engagementScore = | |
| (0.4 * viewsNorm + | |
| 0.3 * completionNorm + | |
| 0.2 * timeSpentNorm + | |
| 0.1 * Math.pow(freshnessScore, 2)) * | |
| difficultyMultiplier; | |
| 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), | |
| [pageViewData, sections, selectedDifficulty, includeOutdated, minimumViews, minimumCompletionRate, selectedSortMethod, sortOrder, displayLimit] | |
| ); |
This data processing pipeline (filtering, mapping, sorting) runs on every render, which can become computationally expensive as the dataset grows. This may negatively affect performance metrics like First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
To optimize, consider wrapping this logic in React.useMemo to ensure it only recalculates when relevant dependencies change. This helps avoid unnecessary recomputation and keeps the main thread more responsive.
Review by Conductor
Is this review helpful? React 👍 or 👎 to let us know!
No description provided.