From fe53e32906cd346c0255d124fd11a5c88f8def4d Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Mon, 3 Aug 2026 23:34:56 +0300 Subject: [PATCH 1/6] feat: insights screen --- .gitignore | 2 + config/webpack/webpack-js.ts | 1 + src/css/insights.scss | 190 +++++++++++++++++ .../InsightsMenu/InsightsChartViewToggle.tsx | 39 ++++ .../InsightsMenu/InsightsCharts.tsx | 148 ++++++++++++++ .../InsightsMenu/InsightsDashboard.tsx | 63 ++++++ .../components/InsightsMenu/InsightsMenu.tsx | 19 ++ src/js/components/common/Toolbar.tsx | 6 + src/js/entries/insights.ts | 4 + src/js/hooks/useInsightsChartViews.ts | 40 ++++ src/js/types/Insights.ts | 34 ++++ src/js/types/Window.ts | 4 + src/php/Admin/Bootstrap_Admin.php | 2 + .../Admin/Menus/Insights/Insights_Menu.php | 40 ++++ .../Menus/Insights/Insights_Menu_Assets.php | 66 ++++++ .../Admin/Menus/Insights/Insights_Summary.php | 167 +++++++++++++++ src/php/Admin/Menus/Manage/Manage_Menu.php | 1 + src/php/Plugin.php | 5 + .../Snippets/Preferences_REST_Controller.php | 128 ++++++++++++ tests/e2e/code-snippets-insights.spec.ts | 192 ++++++++++++++++++ .../Insights/Insights_Menu_Assets_Test.php | 192 ++++++++++++++++++ .../REST_API/REST_API_Preferences_Test.php | 100 +++++++++ 22 files changed, 1443 insertions(+) create mode 100644 src/css/insights.scss create mode 100644 src/js/components/InsightsMenu/InsightsChartViewToggle.tsx create mode 100644 src/js/components/InsightsMenu/InsightsCharts.tsx create mode 100644 src/js/components/InsightsMenu/InsightsDashboard.tsx create mode 100644 src/js/components/InsightsMenu/InsightsMenu.tsx create mode 100644 src/js/entries/insights.ts create mode 100644 src/js/hooks/useInsightsChartViews.ts create mode 100644 src/js/types/Insights.ts create mode 100644 src/php/Admin/Menus/Insights/Insights_Menu.php create mode 100644 src/php/Admin/Menus/Insights/Insights_Menu_Assets.php create mode 100644 src/php/Admin/Menus/Insights/Insights_Summary.php create mode 100644 tests/e2e/code-snippets-insights.spec.ts create mode 100644 tests/unit/Admin/Menus/Insights/Insights_Menu_Assets_Test.php diff --git a/.gitignore b/.gitignore index fd2e1b3b6..453a38177 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ tmp .codex # Local environment overrides (license keys, secrets) /.env +.superpowers/ +docs/superpowers/ diff --git a/config/webpack/webpack-js.ts b/config/webpack/webpack-js.ts index 5c88861e8..1fd7741ae 100644 --- a/config/webpack/webpack-js.ts +++ b/config/webpack/webpack-js.ts @@ -28,6 +28,7 @@ export const jsWebpackConfig: Configuration = { 'edit': { import: `${SOURCE_DIR}/edit.ts`, dependOn: 'editor' }, 'editor': `${SOURCE_DIR}/editor.ts`, 'import': `${SOURCE_DIR}/import.ts`, + 'insights': `${SOURCE_DIR}/insights.ts`, 'manage': `${SOURCE_DIR}/manage.ts`, 'mce': `${SOURCE_DIR}/mce.ts`, 'prism': `${SOURCE_DIR}/prism.ts`, diff --git a/src/css/insights.scss b/src/css/insights.scss new file mode 100644 index 000000000..270cc83e2 --- /dev/null +++ b/src/css/insights.scss @@ -0,0 +1,190 @@ +@use 'common/theme'; +@use 'common/toolbar'; +@use 'common/wp-admin'; + +.code-snippets-insights { + margin-block: 50px; + margin-inline: auto; + color: #2c3337; + + h1 { + padding: 0; + margin: 0; + font-size: 32px; + font-weight: 510; + line-height: 1.25; + } +} + +.insights-chart-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-block: 16px; +} + +.insights-chart-card { + box-sizing: border-box; + border: 1px solid #dcdcde; + border-radius: 8px; + background: #fff; + padding: 24px; + + .insights-chart-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-block-end: 20px; + } + + h2 { + margin: 0; + font-size: 18px; + } +} + +.insights-chart-view-toggle { + display: flex; + align-items: center; + gap: 4px; + + .insights-chart-view-toggle-option { + display: flex; + align-items: center; + justify-content: center; + inline-size: 26px; + block-size: 26px; + box-sizing: border-box; + padding: 0; + margin: 0; + background: #f0f0f1; + color: #646970; + border: none; + border-radius: 4.5px; + cursor: pointer; + + > .dashicons { + font-size: 18px; + } + + &:hover, + &:focus { + color: #2c3337; + } + + &:focus-visible { + outline: 2px solid theme.$accent; + outline-offset: -2px; + } + + &:focus:not(:focus-visible) { + outline: none; + } + + &.active-view { + background: theme.$accent; + color: #fff; + + &:hover, + &:focus { + color: #fff; + } + } + } +} + +.insights-bar-chart, +.insights-pie-chart-legend { + padding: 0; + margin: 0; + list-style: none; +} + +.insights-bar-chart { + display: grid; + gap: 12px; + + li { + display: grid; + grid-template-columns: minmax(110px, 1fr) minmax(80px, 3fr) auto; + align-items: center; + gap: 10px; + } + + strong { + min-inline-size: 2ch; + text-align: end; + } +} + +.insights-bar-track { + overflow: hidden; + block-size: 10px; + border-radius: 999px; + background: #e2e5e5; +} + +.insights-bar-fill { + block-size: 100%; + border-radius: inherit; +} + +.insights-pie-chart-content { + display: flex; + align-items: center; + gap: 24px; +} + +.insights-pie-chart { + position: relative; + flex: 0 0 auto; + inline-size: 112px; + block-size: 112px; + border-radius: 50%; + background: #e2e5e5; + + &::after { + position: absolute; + inset: 25px; + border-radius: 50%; + background: #fff; + content: ''; + } +} + +.insights-pie-chart-legend { + display: grid; + gap: 12px; + inline-size: 100%; + + li { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + } + + span { + display: flex; + align-items: center; + } + + i { + display: inline-block; + inline-size: 10px; + block-size: 10px; + border-radius: 50%; + margin-inline-end: 8px; + } +} + +@media (width <= 782px) { + .insights-chart-grid { + grid-template-columns: 1fr; + } + + .insights-bar-chart li { + grid-template-columns: minmax(90px, 1fr) minmax(64px, 2fr) auto; + } +} diff --git a/src/js/components/InsightsMenu/InsightsChartViewToggle.tsx b/src/js/components/InsightsMenu/InsightsChartViewToggle.tsx new file mode 100644 index 000000000..56fe17ee5 --- /dev/null +++ b/src/js/components/InsightsMenu/InsightsChartViewToggle.tsx @@ -0,0 +1,39 @@ +import { __, sprintf } from '@wordpress/i18n' +import classnames from 'classnames' +import React from 'react' +import type { InsightsChartView } from '../../types/Insights' + +export interface InsightsChartViewToggleProps { + title: string + view: InsightsChartView + setView: (view: InsightsChartView) => void +} + +export const InsightsChartViewToggle: React.FC = ({ title, view, setView }) => +
+ + + +
diff --git a/src/js/components/InsightsMenu/InsightsCharts.tsx b/src/js/components/InsightsMenu/InsightsCharts.tsx new file mode 100644 index 000000000..6886ade16 --- /dev/null +++ b/src/js/components/InsightsMenu/InsightsCharts.tsx @@ -0,0 +1,148 @@ +import classnames from 'classnames' +import React from 'react' +import { InsightsChartViewToggle } from './InsightsChartViewToggle' +import type { InsightsChartEntry, InsightsChartKey, InsightsChartView } from '../../types/Insights' + +const PERCENTAGE_MAX = 100 + +export const INSIGHTS_TYPE_COLORS: Readonly> = { + php: '#2271b1', + html: '#cd4510', + css: '#9b59b6', + js: '#f7d67a', + cond: '#22826f' +} + +export const INSIGHTS_ACTIVATION_COLORS: Readonly> = { + active: '#118822', + inactive: '#cd4510' +} + +export const INSIGHTS_LOCATION_COLORS: Readonly> = { + 'global': '#ff9800', + 'admin': '#03c7d2', + 'front-end': '#d46f4d', + 'single-use': '#00bcd4', + 'content': '#2271b1', + 'head-content': '#5865f2', + 'body-content': '#3b5998', + 'footer-content': '#9b59b6', + 'admin-css': '#22826f', + 'site-css': '#cd4510', + 'site-head-js': '#f7d67a', + 'site-footer-js': '#2b71a3' +} + +interface InsightsChartProps { + chart: InsightsChartKey + entries: Readonly> + title: string + view: InsightsChartView + setView: (view: InsightsChartView) => void + colors: Readonly> +} + +const getCount = (entry: InsightsChartEntry): number => Number(entry.count) + +const getLargestCount = (entries: Readonly>): number => + Math.max(1, ...Object.values(entries).map(getCount)) + +const getColor = (colors: Readonly>, key: string): string => + colors[key] ?? '#646970' + +const getTotalCount = (entries: Readonly>): number => + Object.values(entries).reduce((count, entry) => count + getCount(entry), 0) + +const getPieBackground = ( + entries: Readonly>, + colors: Readonly>, + totalCount: number +): string => { + if (0 === totalCount) { + return '#e2e5e5' + } + + let start = 0 + const segments = Object.entries(entries) + .filter(([, entry]) => 0 < getCount(entry)) + .map(([key, entry]) => { + const end = start + getCount(entry) / totalCount * PERCENTAGE_MAX + const segment = `${getColor(colors, key)} ${start}% ${end}%` + + start = end + return segment + }) + + return `conic-gradient(${segments.join(', ')})` +} + +interface BarChartProps extends Pick { + largestCount: number +} + +const BarChart: React.FC = ({ colors, entries, largestCount }) => ( +
    + {Object.entries(entries).map(([key, entry]) => +
  • + {entry.label} +
  • )} +
+) + +interface PieChartProps extends Pick { + totalCount: number +} + +const PieChart: React.FC = ({ colors, entries, totalCount }) => { + const isEmpty = 0 === totalCount + + return
+