diff --git a/.gitignore b/.gitignore index 7061a1d8a..d1a72ba02 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ public/rss.xml /.cache/ # Directory for Stride Conductor local files .conductor/config.yml -.conductor/story_context.md \ No newline at end of file +.conductor/story_context.md +.early.coverage \ No newline at end of file diff --git a/src/components/DocumentationStatistics.tsx b/src/components/DocumentationStatistics.tsx new file mode 100644 index 000000000..7faa920a7 --- /dev/null +++ b/src/components/DocumentationStatistics.tsx @@ -0,0 +1,586 @@ +import React, {useState} from 'react'; +import {useRouter} from 'next/router'; + +// Simulated analytics data for demonstration purposes +const pageViewData = [ + { + path: '/learn/describing-the-ui', + views: 143500, + completionRate: 0.72, + avgTimeSpent: 340, + lastUpdated: '2024-12-10', + difficulty: 'beginner', + }, + { + path: '/learn/adding-interactivity', + views: 98200, + completionRate: 0.68, + avgTimeSpent: 450, + lastUpdated: '2024-11-25', + difficulty: 'beginner', + }, + { + path: '/learn/managing-state', + views: 120400, + completionRate: 0.65, + avgTimeSpent: 520, + lastUpdated: '2024-12-05', + difficulty: 'intermediate', + }, + { + path: '/learn/escape-hatches', + views: 67800, + completionRate: 0.58, + avgTimeSpent: 380, + lastUpdated: '2024-10-15', + difficulty: 'advanced', + }, + { + path: '/reference/react', + views: 215600, + completionRate: 0.81, + avgTimeSpent: 290, + lastUpdated: '2025-01-20', + difficulty: 'intermediate', + }, + { + path: '/reference/react-dom', + views: 187300, + completionRate: 0.77, + avgTimeSpent: 310, + lastUpdated: '2025-01-05', + difficulty: 'intermediate', + }, + { + path: '/reference/react/useState', + views: 198700, + completionRate: 0.79, + avgTimeSpent: 270, + lastUpdated: '2025-02-10', + difficulty: 'beginner', + }, + { + path: '/reference/react/useEffect', + views: 185400, + completionRate: 0.75, + avgTimeSpent: 340, + lastUpdated: '2025-02-10', + difficulty: 'intermediate', + }, + { + path: '/reference/react/useContext', + views: 112800, + completionRate: 0.67, + avgTimeSpent: 380, + lastUpdated: '2025-01-15', + difficulty: 'intermediate', + }, + { + path: '/reference/react/useReducer', + views: 89500, + completionRate: 0.64, + avgTimeSpent: 420, + lastUpdated: '2025-01-18', + difficulty: 'advanced', + }, + { + path: '/reference/react/useMemo', + views: 103700, + completionRate: 0.71, + avgTimeSpent: 300, + lastUpdated: '2025-01-25', + difficulty: 'advanced', + }, + { + path: '/reference/react/useCallback', + views: 98200, + completionRate: 0.7, + avgTimeSpent: 310, + lastUpdated: '2025-01-25', + difficulty: 'advanced', + }, + { + path: '/reference/react/useRef', + views: 115900, + completionRate: 0.73, + avgTimeSpent: 290, + lastUpdated: '2025-01-20', + difficulty: 'intermediate', + }, + { + path: '/blog/2023/03/16/introducing-react-dev', + views: 85400, + completionRate: 0.62, + avgTimeSpent: 540, + lastUpdated: '2023-03-16', + difficulty: 'beginner', + }, + { + path: '/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022', + views: 72600, + completionRate: 0.58, + avgTimeSpent: 620, + lastUpdated: '2022-06-15', + difficulty: 'intermediate', + }, + { + path: '/community/conferences', + views: 28900, + completionRate: 0.51, + avgTimeSpent: 180, + lastUpdated: '2024-12-20', + difficulty: 'beginner', + }, + { + path: '/community/meetups', + views: 19500, + completionRate: 0.48, + avgTimeSpent: 150, + lastUpdated: '2024-11-10', + difficulty: 'beginner', + }, + // ... assume more entries exist +]; + +const DocumentationStatistics = ({ + sections = ['learn', 'reference', 'blog', 'community'], + sortBy = 'views', + sortOrder = 'desc', + difficultyFilter = 'all', + includeOutdated = true, + minimumViews = 0, + minimumCompletionRate = 0, + displayLimit = 10, +}) => { + const router = useRouter(); + const [selectedDifficulty, setSelectedDifficulty] = + useState(difficultyFilter); + const [selectedSortMethod, setSelectedSortMethod] = useState(sortBy); + const [hoveredRow, setHoveredRow] = useState(null); + + 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); + +// Compute freshness factor and engagement score using useMemo +const { freshnessScore, difficultyMultiplier, engagementScore } = React.useMemo(() => { + // 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 { freshnessScore, difficultyMultiplier, engagementScore }; +}, [page.lastUpdated, page.difficulty, viewsNorm, completionNorm, timeSpentNorm]); + + 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 filterControls = ( +
+
+ + +
+ +
+ +
+ + +
+ ); + + const getDifficultyColor = (difficulty) => { + switch (difficulty) { + case 'beginner': + return '#4CAF50'; + case 'intermediate': + return '#2196F3'; + case 'advanced': + return '#F44336'; + default: + return 'grey'; + } + }; + + const getEfficiencyClass = (completionRate, timeSpent) => { + const efficiency = (completionRate * 100) / (timeSpent / 60); + if (efficiency > 15) return '#4CAF50'; + if (efficiency > 10) return '#FFC107'; + return '#F44336'; + }; + + return ( +
+

+ Documentation Analytics Dashboard +

+ +
+

+ Showing top {processedData.length} pages filtered by{' '} + {selectedDifficulty !== 'all' + ? selectedDifficulty + : 'all difficulties'} + , sorted by {selectedSortMethod} +

+
+ + {filterControls} + +
+ {processedData.slice(0, 7).map((page, idx) => ( +
router.push(page.path)} + onMouseEnter={() => setHoveredRow(page.path)} + onMouseLeave={() => setHoveredRow(null)}> + {hoveredRow === page.path && ( +
+ {page.path.split('/').pop()}: {page.views.toLocaleString()}{' '} + views +
+ )} +
+ ))} +
+ +
+ + + + + + + + + + + + + + {processedData.map((page, idx) => ( + setHoveredRow(page.path)} + onMouseLeave={() => setHoveredRow(null)} + onClick={() => router.push(page.path)}> + + + + + + + + + ))} + +
+ Page + + Views + + Completion + + Time Spent + + Difficulty + + Engagement + + Last Updated +
+ {page.path} + + {page.views.toLocaleString()} + + {(page.completionRate * 100).toFixed(1)}% + + {page.avgTimeSpent}s + + + {page.difficulty} + + {page.engagementScore.toFixed(3)} + + {page.lastUpdated} +
+
+ +
+ +
+
+ ); +}; + +export default DocumentationStatistics; diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx index cc5c654e3..995612b68 100644 --- a/src/components/Layout/TopNav/TopNav.tsx +++ b/src/components/Layout/TopNav/TopNav.tsx @@ -328,12 +328,15 @@ export default function TopNav({ url="/reference/react"> Reference - - Community - + Community Blog + + Analytics +
@@ -423,6 +426,7 @@ export default function TopNav({ Blog + Analytics
, Diagram, DiagramGroup, + DocumentationStatistics, FullWidth({children}: {children: any}) { return children; }, diff --git a/src/content/community/statistics.md b/src/content/community/statistics.md new file mode 100644 index 000000000..adbe8f636 --- /dev/null +++ b/src/content/community/statistics.md @@ -0,0 +1,15 @@ +--- +title: Documentation Analytics +--- + + +This page displays analytics information about the React documentation. It shows which pages receive the most views, completion rates, time spent, and other metrics to help understand documentation usage patterns. + + +## Documentation Analytics Dashboard {/*documentation-analytics-dashboard*/} + +The dashboard below shows engagement metrics across the React documentation, helping to identify the most popular pages and areas that might need improvement. + + + +*Note: Data shown is for demonstration purposes only.* \ No newline at end of file diff --git a/src/sidebarCommunity.json b/src/sidebarCommunity.json index ac8a172d5..e1f01ae1f 100644 --- a/src/sidebarCommunity.json +++ b/src/sidebarCommunity.json @@ -44,6 +44,14 @@ "path": "/community/versioning-policy" } ] + }, + { + "hasSectionHeader": true, + "sectionHeader": "ANALYTICS" + }, + { + "title": "Documentation Analytics", + "path": "/community/statistics" } ] }