From 36228ddca17c8f1fdf7722b7fc6506199197f1a8 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 14:26:26 +0200 Subject: [PATCH 001/275] content-activity --- .eslintrc.js | 3 + .gitignore | 3 +- assets/src/components/BigCounter/index.js | 117 ++++++ .../src/components/LineChart/ChartFilters.js | 86 +++++ assets/src/components/LineChart/index.js | 363 ++++++++++++++++++ assets/src/content-activity.js | 15 + .../widgets/ContentActivity/ActivityTable.js | 116 ++++++ assets/src/widgets/ContentActivity/index.js | 128 ++++++ build/content-activity.asset.php | 1 + build/content-activity.js | 1 + .../admin/widgets/class-content-activity.php | 26 ++ classes/class-base.php | 2 + classes/rest/class-content-activity.php | 165 ++++++++ package-lock.json | 230 +++++++++-- package.json | 9 +- phpcs.xml.dist | 3 + views/page-widgets/content-activity.php | 109 +----- webpack.config.js | 13 + 18 files changed, 1249 insertions(+), 141 deletions(-) create mode 100644 assets/src/components/BigCounter/index.js create mode 100644 assets/src/components/LineChart/ChartFilters.js create mode 100644 assets/src/components/LineChart/index.js create mode 100644 assets/src/content-activity.js create mode 100644 assets/src/widgets/ContentActivity/ActivityTable.js create mode 100644 assets/src/widgets/ContentActivity/index.js create mode 100644 build/content-activity.asset.php create mode 100644 build/content-activity.js create mode 100644 classes/rest/class-content-activity.php create mode 100644 webpack.config.js diff --git a/.eslintrc.js b/.eslintrc.js index 545adecb43..5c321e42d6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,9 @@ module.exports = { ], parserOptions: { ecmaVersion: 'latest', + ecmaFeatures: { + jsx: true, + }, }, rules: { 'no-console': 'off', diff --git a/.gitignore b/.gitignore index 83c571da28..539073baa6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ playwright/.cache/ auth.json # Environment variables -.env \ No newline at end of file +.env +.claude/ diff --git a/assets/src/components/BigCounter/index.js b/assets/src/components/BigCounter/index.js new file mode 100644 index 0000000000..466c7c08dc --- /dev/null +++ b/assets/src/components/BigCounter/index.js @@ -0,0 +1,117 @@ +/** + * BigCounter Component + * + * Displays a large counter with a label, with responsive text sizing. + */ + +import { useEffect, useRef, useCallback } from '@wordpress/element'; + +/** + * BigCounter component. + * + * @param {Object} props - Component props. + * @param {string} props.number - The number to display. + * @param {string} props.label - The label text below the number. + * @param {string} props.backgroundColor - Background color (CSS value). + * @return {JSX.Element} The BigCounter component. + */ +export default function BigCounter( { + number, + label, + backgroundColor = 'var(--prpl-background-content)', +} ) { + const containerRef = useRef( null ); + const labelRef = useRef( null ); + + const resizeFont = useCallback( () => { + const labelElement = labelRef.current; + const containerElement = containerRef.current; + + if ( ! labelElement || ! containerElement ) { + return; + } + + // Reset to 100% first + labelElement.style.fontSize = '100%'; + labelElement.style.width = 'max-content'; + + const containerWidth = containerElement.clientWidth; + let size = 100; + + // Shrink the font until it fits or reaches minimum size + while ( labelElement.clientWidth > containerWidth && size > 80 ) { + size -= 1; + labelElement.style.fontSize = size + '%'; + } + + // If we hit minimum size, set width to 100% for wrapping + if ( size <= 80 ) { + labelElement.style.fontSize = '80%'; + labelElement.style.width = '100%'; + } + }, [] ); + + useEffect( () => { + resizeFont(); + window.addEventListener( 'resize', resizeFont ); + + return () => { + window.removeEventListener( 'resize', resizeFont ); + }; + }, [ resizeFont, label ] ); + + const containerStyle = { + backgroundColor, + padding: 'var(--prpl-padding)', + borderRadius: 'var(--prpl-border-radius-big)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center', + alignContent: 'center', + justifyContent: 'center', + height: 'calc(var(--prpl-font-size-5xl) + var(--prpl-font-size-2xl) + var(--prpl-padding) * 2)', + marginBottom: 'var(--prpl-padding)', + }; + + const numberStyle = { + fontSize: 'var(--prpl-font-size-5xl)', + lineHeight: 1, + fontWeight: 600, + }; + + const labelWrapperStyle = { + fontSize: 'var(--prpl-font-size-2xl)', + }; + + const labelStyle = { + fontSize: '100%', + display: 'inline-block', + width: 'max-content', + }; + + return ( +
+
+ + { number } + + + + { label } + + +
+ ); +} diff --git a/assets/src/components/LineChart/ChartFilters.js b/assets/src/components/LineChart/ChartFilters.js new file mode 100644 index 0000000000..18898d4526 --- /dev/null +++ b/assets/src/components/LineChart/ChartFilters.js @@ -0,0 +1,86 @@ +/** + * ChartFilters Component + * + * Displays filter checkboxes for toggling chart series visibility. + */ + +/** + * ChartFilters component. + * + * @param {Object} props - Component props. + * @param {Object} props.dataArgs - Data arguments with color and label per series. + * @param {string[]} props.visibleSeries - Array of visible series keys. + * @param {string} props.filtersLabel - Optional label to show before filters. + * @param {Function} props.onToggle - Callback when a series is toggled. + * @return {JSX.Element} The ChartFilters component. + */ +export default function ChartFilters( { + dataArgs, + visibleSeries, + filtersLabel, + onToggle, +} ) { + const containerStyle = { + display: 'flex', + gap: '1em', + marginBottom: '1em', + justifyContent: 'space-between', + fontSize: '0.85rem', + }; + + const labelStyle = { + display: 'flex', + alignItems: 'center', + gap: '0.25em', + cursor: 'pointer', + }; + + const getCheckboxColorStyle = ( key ) => ( { + backgroundColor: visibleSeries.includes( key ) + ? dataArgs[ key ].color + : 'transparent', + width: '1em', + height: '1em', + borderRadius: '0.25em', + outline: `1px solid ${ dataArgs[ key ].color }`, + border: '1px solid #fff', + } ); + + const hiddenInputStyle = { + display: 'none', + }; + + return ( +
+ { filtersLabel && ( + + ) } + { Object.keys( dataArgs ).map( ( key ) => ( + + ) ) } +
+ ); +} diff --git a/assets/src/components/LineChart/index.js b/assets/src/components/LineChart/index.js new file mode 100644 index 0000000000..c45b1306d9 --- /dev/null +++ b/assets/src/components/LineChart/index.js @@ -0,0 +1,363 @@ +/** + * LineChart Component + * + * Displays an SVG line chart with multiple series and filter checkboxes. + */ + +import { useState, useMemo, useCallback } from '@wordpress/element'; +import ChartFilters from './ChartFilters'; + +/** + * Default options for the chart. + */ +const DEFAULT_OPTIONS = { + aspectRatio: 2, + height: 300, + axisOffset: 16, + strokeWidth: 4, + dataArgs: {}, + axisColor: 'var(--prpl-color-border)', + rulersColor: 'var(--prpl-color-border)', + filtersLabel: '', +}; + +/** + * LineChart component. + * + * @param {Object} props - Component props. + * @param {Object} props.data - Chart data object with series keys. + * @param {Object} props.options - Chart options. + * @return {JSX.Element} The LineChart component. + */ +export default function LineChart( { data, options: propOptions } ) { + const options = useMemo( + () => ( { + ...DEFAULT_OPTIONS, + ...propOptions, + } ), + [ propOptions ] + ); + + const [ visibleSeries, setVisibleSeries ] = useState( () => + Object.keys( options.dataArgs ) + ); + + /** + * Toggle series visibility. + * + * @param {string} key - The series key to toggle. + */ + const toggleSeries = useCallback( ( key ) => { + setVisibleSeries( ( prev ) => + prev.includes( key ) + ? prev.filter( ( k ) => k !== key ) + : [ ...prev, key ] + ); + }, [] ); + + /** + * Get the maximum value from visible series data. + * + * @return {number} The maximum value. + */ + const getMaxValue = useCallback( () => { + return Object.keys( data ).reduce( ( max, key ) => { + if ( visibleSeries.includes( key ) ) { + return Math.max( + max, + data[ key ].reduce( + ( _max, item ) => Math.max( _max, item.score ), + 0 + ) + ); + } + return max; + }, 0 ); + }, [ data, visibleSeries ] ); + + /** + * Get padded maximum value for axis scaling. + * + * @return {number} The padded maximum value. + */ + const getMaxValuePadded = useCallback( () => { + const max = getMaxValue(); + const maxValue = 100 > max && 70 < max ? 100 : max; + return Math.max( + 100 === maxValue ? 100 : parseInt( maxValue * 1.1, 10 ), + 1 + ); + }, [ getMaxValue ] ); + + /** + * Get the optimal Y-axis step divider (3, 4, or 5). + * + * @return {number} The step divider. + */ + const getYLabelsStepsDivider = useCallback( () => { + const maxValuePadded = getMaxValuePadded(); + const stepsRemainders = { + 4: maxValuePadded % 4, + 5: maxValuePadded % 5, + 3: maxValuePadded % 3, + }; + const smallestRemainder = Math.min( + ...Object.values( stepsRemainders ) + ); + return parseInt( + Object.keys( stepsRemainders ).find( + ( key ) => stepsRemainders[ key ] === smallestRemainder + ), + 10 + ); + }, [ getMaxValuePadded ] ); + + /** + * Get Y-axis labels. + * + * @return {number[]} Array of Y-axis label values. + */ + const getYLabels = useCallback( () => { + const maxValuePadded = getMaxValuePadded(); + const yLabelsStepsDivider = getYLabelsStepsDivider(); + const yLabelsStep = maxValuePadded / yLabelsStepsDivider; + const yLabels = []; + + if ( 100 === maxValuePadded || 15 > maxValuePadded ) { + for ( let i = 0; i <= yLabelsStepsDivider; i++ ) { + yLabels.push( parseInt( yLabelsStep * i, 10 ) ); + } + } else { + for ( let i = 0; i <= yLabelsStepsDivider; i++ ) { + yLabels.push( + Math.min( maxValuePadded, Math.round( yLabelsStep * i ) ) + ); + } + } + + return yLabels; + }, [ getMaxValuePadded, getYLabelsStepsDivider ] ); + + /** + * Calculate Y coordinate for a value. + * + * @param {number} value - The data value. + * @return {number} The Y coordinate. + */ + const calcYCoordinate = useCallback( + ( value ) => { + const maxValuePadded = getMaxValuePadded(); + const multiplier = + ( options.height - options.axisOffset * 2 ) / options.height; + const yCoordinate = + ( maxValuePadded - value * multiplier ) * + ( options.height / maxValuePadded ) - + options.axisOffset; + return yCoordinate - options.strokeWidth / 2; + }, + [ + getMaxValuePadded, + options.height, + options.axisOffset, + options.strokeWidth, + ] + ); + + /** + * Get distance between X-axis points. + * + * @return {number} The distance. + */ + const getXDistanceBetweenPoints = useCallback( () => { + const firstKey = Object.keys( data )[ 0 ]; + if ( ! firstKey || ! data[ firstKey ] ) { + return 0; + } + return Math.round( + ( options.height * options.aspectRatio - 3 * options.axisOffset ) / + ( data[ firstKey ].length - 1 ) + ); + }, [ data, options.height, options.aspectRatio, options.axisOffset ] ); + + // Calculate SVG viewBox dimensions + const svgWidth = parseInt( + options.height * options.aspectRatio + options.axisOffset * 2, + 10 + ); + const svgHeight = parseInt( options.height + options.axisOffset * 2, 10 ); + + // Get X-axis labels data + const firstSeriesKey = Object.keys( data )[ 0 ]; + const firstSeriesData = firstSeriesKey ? data[ firstSeriesKey ] : []; + const dataLength = firstSeriesData.length; + const labelsXDivider = Math.max( 1, Math.round( dataLength / 6 ) ); + + const containerStyle = { + width: '100%', + }; + + const svgContainerStyle = { + width: '100%', + }; + + return ( +
+ { Object.keys( options.dataArgs ).length > 1 && ( + + ) } +
+ + { /* X Axis Line */ } + + + + + { /* Y Axis Line */ } + + + + + { /* X Axis Labels and Rulers */ } + { firstSeriesData.map( ( item, index ) => { + const labelXCoordinate = + getXDistanceBetweenPoints() * index + + options.axisOffset * 2; + + // Only show up to 6 labels + if ( + dataLength > 6 && + index !== 0 && + index % labelsXDivider !== 0 + ) { + return null; + } + + return ( + + + { item.label } + + { index !== 0 && ( + + ) } + + ); + } ) } + + { /* Y Axis Labels and Rulers */ } + { getYLabels().map( ( yLabel, index ) => { + const yLabelCoordinate = calcYCoordinate( yLabel ); + + return ( + + + { yLabel } + + { index !== 0 && ( + + ) } + + ); + } ) } + + { /* Polylines for each series */ } + { Object.keys( data ).map( ( key ) => { + if ( ! visibleSeries.includes( key ) ) { + return null; + } + + const points = data[ key ] + .map( ( item, index ) => { + const xCoordinate = + options.axisOffset * 3 + + getXDistanceBetweenPoints() * index; + const yCoordinate = calcYCoordinate( + item.score + ); + return `${ xCoordinate },${ yCoordinate }`; + } ) + .join( ' ' ); + + return ( + + + + ); + } ) } + +
+
+ ); +} diff --git a/assets/src/content-activity.js b/assets/src/content-activity.js new file mode 100644 index 0000000000..7d693b6c02 --- /dev/null +++ b/assets/src/content-activity.js @@ -0,0 +1,15 @@ +/** + * Content Activity Widget Entry Point + * + * This is the main entry point for the Content Activity widget React application. + */ + +import { createRoot } from '@wordpress/element'; +import ContentActivity from './widgets/ContentActivity'; + +document.addEventListener( 'DOMContentLoaded', () => { + const container = document.getElementById( 'prpl-content-activity-root' ); + if ( container ) { + createRoot( container ).render( ); + } +} ); diff --git a/assets/src/widgets/ContentActivity/ActivityTable.js b/assets/src/widgets/ContentActivity/ActivityTable.js new file mode 100644 index 0000000000..e46fcee617 --- /dev/null +++ b/assets/src/widgets/ContentActivity/ActivityTable.js @@ -0,0 +1,116 @@ +/** + * ActivityTable Component + * + * Displays a table with weekly activity breakdown. + */ + +import { __ } from '@wordpress/i18n'; + +/** + * ActivityTable component. + * + * @param {Object} props - Component props. + * @param {Object} props.activityTypes - Activity types with labels. + * @param {Object} props.weeklyActivity - Weekly activity counts per type. + * @param {number} props.totalCount - Total count of all activities. + * @return {JSX.Element} The ActivityTable component. + */ +export default function ActivityTable( { + activityTypes, + weeklyActivity, + totalCount, +} ) { + const tableStyle = { + width: '100%', + marginBottom: '1em', + borderSpacing: '6px 0', + }; + + const cellStyle = { + border: 'none', + padding: '0.5em', + }; + + const centeredCellStyle = { + ...cellStyle, + textAlign: 'center', + }; + + const footerCellStyle = { + ...cellStyle, + borderTop: '1px solid var(--prpl-color-border)', + }; + + const footerCenteredCellStyle = { + ...centeredCellStyle, + borderTop: '1px solid var(--prpl-color-border)', + }; + + return ( + + + + + + + + + { Object.keys( activityTypes ).map( ( key, index ) => { + const rowStyle = { + backgroundColor: + index % 2 === 0 + ? 'var(--prpl-background-table)' + : 'transparent', + }; + + return ( + + + + + ); + } ) } + + + + + + + +
+ { __( 'Content managed', 'progress-planner' ) } + + { __( 'Last week', 'progress-planner' ) } +
+ { activityTypes[ key ].label } + + { weeklyActivity[ key ] || 0 } +
+ { __( 'Total', 'progress-planner' ) } + + { totalCount } +
+ ); +} diff --git a/assets/src/widgets/ContentActivity/index.js b/assets/src/widgets/ContentActivity/index.js new file mode 100644 index 0000000000..65e6996cdb --- /dev/null +++ b/assets/src/widgets/ContentActivity/index.js @@ -0,0 +1,128 @@ +/** + * ContentActivity Widget + * + * Main widget component for displaying content activity statistics. + */ + +import { useState, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import BigCounter from '../../components/BigCounter'; +import LineChart from '../../components/LineChart'; +import ActivityTable from './ActivityTable'; + +/** + * Loading spinner component. + * + * @return {JSX.Element} The loading spinner. + */ +function LoadingSpinner() { + const spinnerStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + padding: '2em', + }; + + return ( +
+ { __( 'Loading…', 'progress-planner' ) } +
+ ); +} + +/** + * Error display component. + * + * @param {Object} props - Component props. + * @param {string} props.message - Error message. + * @return {JSX.Element} The error display. + */ +function ErrorDisplay( { message } ) { + const errorStyle = { + padding: '1em', + backgroundColor: 'var(--prpl-color-error-background, #fee)', + color: 'var(--prpl-color-error, #c00)', + borderRadius: 'var(--prpl-border-radius)', + }; + + return ( +
+ { message } +
+ ); +} + +/** + * ContentActivity widget component. + * + * @return {JSX.Element} The ContentActivity widget. + */ +export default function ContentActivity() { + const [ data, setData ] = useState( null ); + const [ loading, setLoading ] = useState( true ); + const [ error, setError ] = useState( null ); + + useEffect( () => { + apiFetch( { path: '/progress-planner/v1/content-activity' } ) + .then( ( response ) => { + setData( response ); + setError( null ); + } ) + .catch( ( err ) => { + setError( + err.message || + __( + 'Failed to load content activity data.', + 'progress-planner' + ) + ); + } ) + .finally( () => { + setLoading( false ); + } ); + }, [] ); + + if ( loading ) { + return ; + } + + if ( error ) { + return ; + } + + if ( ! data ) { + return null; + } + + const graphWrapperStyle = { + marginBottom: 'var(--prpl-padding)', + }; + + return ( +
+ +
+ +
+ +
+ ); +} diff --git a/build/content-activity.asset.php b/build/content-activity.asset.php new file mode 100644 index 0000000000..a3fda82f31 --- /dev/null +++ b/build/content-activity.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'f6e1a398f434510b7d28'); diff --git a/build/content-activity.js b/build/content-activity.js new file mode 100644 index 0000000000..cf6bd26d62 --- /dev/null +++ b/build/content-activity.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={n:t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return e.d(r,{a:r}),r},d:(t,r)=>{for(var s in r)e.o(r,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:r[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,r=window.wp.i18n,s=window.wp.apiFetch;var l=e.n(s);const a=window.ReactJSXRuntime;function n({number:e,label:r,backgroundColor:s="var(--prpl-background-content)"}){const l=(0,t.useRef)(null),n=(0,t.useRef)(null),i=(0,t.useCallback)(()=>{const e=n.current,t=l.current;if(!e||!t)return;e.style.fontSize="100%",e.style.width="max-content";const r=t.clientWidth;let s=100;for(;e.clientWidth>r&&s>80;)s-=1,e.style.fontSize=s+"%";s<=80&&(e.style.fontSize="80%",e.style.width="100%")},[]);(0,t.useEffect)(()=>(i(),window.addEventListener("resize",i),()=>{window.removeEventListener("resize",i)}),[i,r]);const o={backgroundColor:s,padding:"var(--prpl-padding)",borderRadius:"var(--prpl-border-radius-big)",display:"flex",flexDirection:"column",alignItems:"center",textAlign:"center",alignContent:"center",justifyContent:"center",height:"calc(var(--prpl-font-size-5xl) + var(--prpl-font-size-2xl) + var(--prpl-padding) * 2)",marginBottom:"var(--prpl-padding)"};return(0,a.jsxs)("div",{className:"prpl-big-counter",style:o,children:[(0,a.jsx)("div",{className:"prpl-big-counter__width-reference",ref:l,style:{width:"100%"}}),(0,a.jsx)("span",{className:"prpl-big-counter__number",style:{fontSize:"var(--prpl-font-size-5xl)",lineHeight:1,fontWeight:600},children:e}),(0,a.jsx)("span",{className:"prpl-big-counter__label-wrapper",style:{fontSize:"var(--prpl-font-size-2xl)"},children:(0,a.jsx)("span",{className:"prpl-big-counter__label",ref:n,style:{fontSize:"100%",display:"inline-block",width:"max-content"},children:r})})]})}function i({dataArgs:e,visibleSeries:t,filtersLabel:r,onToggle:s}){const l={display:"flex",alignItems:"center",gap:"0.25em",cursor:"pointer"},n=r=>({backgroundColor:t.includes(r)?e[r].color:"transparent",width:"1em",height:"1em",borderRadius:"0.25em",outline:`1px solid ${e[r].color}`,border:"1px solid #fff"}),i={display:"none"};return(0,a.jsxs)("div",{className:"prpl-line-chart__filters",style:{display:"flex",gap:"1em",marginBottom:"1em",justifyContent:"space-between",fontSize:"0.85rem"},children:[r&&(0,a.jsx)("span",{className:"prpl-line-chart__filters-label",dangerouslySetInnerHTML:{__html:r}}),Object.keys(e).map(r=>(0,a.jsxs)("label",{htmlFor:`prpl-chart-filter-${r}`,className:`prpl-line-chart__filter prpl-line-chart__filter--${r}`,style:l,children:[(0,a.jsx)("span",{className:"prpl-line-chart__filter-color",style:n(r)}),(0,a.jsx)("input",{type:"checkbox",id:`prpl-chart-filter-${r}`,name:r,value:r,checked:t.includes(r),onChange:()=>s(r),style:i}),e[r].label]},r))]})}const o={aspectRatio:2,height:300,axisOffset:16,strokeWidth:4,dataArgs:{},axisColor:"var(--prpl-color-border)",rulersColor:"var(--prpl-color-border)",filtersLabel:""};function c({data:e,options:r}){const s=(0,t.useMemo)(()=>({...o,...r}),[r]),[l,n]=(0,t.useState)(()=>Object.keys(s.dataArgs)),c=(0,t.useCallback)(e=>{n(t=>t.includes(e)?t.filter(t=>t!==e):[...t,e])},[]),p=(0,t.useCallback)(()=>Object.keys(e).reduce((t,r)=>l.includes(r)?Math.max(t,e[r].reduce((e,t)=>Math.max(e,t.score),0)):t,0),[e,l]),d=(0,t.useCallback)(()=>{const e=p(),t=100>e&&70{const e=d(),t={4:e%4,5:e%5,3:e%3},r=Math.min(...Object.values(t));return parseInt(Object.keys(t).find(e=>t[e]===r),10)},[d]),x=(0,t.useCallback)(()=>{const e=d(),t=h(),r=e/t,s=[];if(100===e||15>e)for(let e=0;e<=t;e++)s.push(parseInt(r*e,10));else for(let l=0;l<=t;l++)s.push(Math.min(e,Math.round(r*l)));return s},[d,h]),f=(0,t.useCallback)(e=>{const t=d();return(t-e*((s.height-2*s.axisOffset)/s.height))*(s.height/t)-s.axisOffset-s.strokeWidth/2},[d,s.height,s.axisOffset,s.strokeWidth]),u=(0,t.useCallback)(()=>{const t=Object.keys(e)[0];return t&&e[t]?Math.round((s.height*s.aspectRatio-3*s.axisOffset)/(e[t].length-1)):0},[e,s.height,s.aspectRatio,s.axisOffset]),y=parseInt(s.height*s.aspectRatio+2*s.axisOffset,10),g=parseInt(s.height+2*s.axisOffset,10),b=Object.keys(e)[0],m=b?e[b]:[],_=m.length,v=Math.max(1,Math.round(_/6));return(0,a.jsxs)("div",{className:"prpl-line-chart",style:{width:"100%"},children:[Object.keys(s.dataArgs).length>1&&(0,a.jsx)(i,{dataArgs:s.dataArgs,visibleSeries:l,filtersLabel:s.filtersLabel,onToggle:c}),(0,a.jsx)("div",{className:"prpl-line-chart__svg-container",style:{width:"100%"},children:(0,a.jsxs)("svg",{className:"prpl-line-chart__svg",viewBox:`0 0 ${y} ${g}`,children:[(0,a.jsx)("g",{className:"prpl-line-chart__x-axis",children:(0,a.jsx)("line",{x1:3*s.axisOffset,x2:s.aspectRatio*s.height,y1:s.height-s.axisOffset,y2:s.height-s.axisOffset,stroke:s.axisColor,strokeWidth:"1"})}),(0,a.jsx)("g",{className:"prpl-line-chart__y-axis",children:(0,a.jsx)("line",{x1:3*s.axisOffset,x2:3*s.axisOffset,y1:s.axisOffset,y2:s.height-s.axisOffset,stroke:s.axisColor,strokeWidth:"1"})}),m.map((e,t)=>{const r=u()*t+2*s.axisOffset;return _>6&&0!==t&&t%v!==0?null:(0,a.jsxs)("g",{className:"prpl-line-chart__x-label",children:[(0,a.jsx)("text",{x:r,y:s.height+s.axisOffset,children:e.label}),0!==t&&(0,a.jsx)("line",{x1:r+s.axisOffset,x2:r+s.axisOffset,y1:s.axisOffset,y2:s.height-s.axisOffset,stroke:s.rulersColor,strokeWidth:"1"})]},`x-label-${t}`)}),x().map((e,t)=>{const r=f(e);return(0,a.jsxs)("g",{className:"prpl-line-chart__y-label",children:[(0,a.jsx)("text",{x:"0",y:r+s.axisOffset/2,children:e}),0!==t&&(0,a.jsx)("line",{x1:3*s.axisOffset,x2:s.aspectRatio*s.height,y1:r,y2:r,stroke:s.rulersColor,strokeWidth:"1"})]},`y-label-${t}`)}),Object.keys(e).map(t=>{if(!l.includes(t))return null;const r=e[t].map((e,t)=>`${3*s.axisOffset+u()*t},${f(e.score)}`).join(" ");return(0,a.jsx)("g",{className:`prpl-line-chart__series prpl-line-chart__series--${t}`,children:(0,a.jsx)("polyline",{fill:"none",stroke:s.dataArgs[t]?.color,strokeWidth:s.strokeWidth,points:r})},`series-${t}`)})]})})]})}function p({activityTypes:e,weeklyActivity:t,totalCount:s}){const l={border:"none",padding:"0.5em"},n={...l,textAlign:"center"},i={...l,borderTop:"1px solid var(--prpl-color-border)"},o={...n,borderTop:"1px solid var(--prpl-color-border)"};return(0,a.jsxs)("table",{className:"prpl-content-activity__table",style:{width:"100%",marginBottom:"1em",borderSpacing:"6px 0"},children:[(0,a.jsx)("thead",{className:"prpl-content-activity__table-head",children:(0,a.jsxs)("tr",{className:"prpl-content-activity__table-row",children:[(0,a.jsx)("th",{className:"prpl-content-activity__table-header",style:l,children:(0,r.__)("Content managed","progress-planner")}),(0,a.jsx)("th",{className:"prpl-content-activity__table-header",style:n,children:(0,r.__)("Last week","progress-planner")})]})}),(0,a.jsx)("tbody",{className:"prpl-content-activity__table-body",children:Object.keys(e).map((r,s)=>{const i={backgroundColor:s%2==0?"var(--prpl-background-table)":"transparent"};return(0,a.jsxs)("tr",{className:`prpl-content-activity__table-row prpl-content-activity__table-row--${r}`,style:i,children:[(0,a.jsx)("th",{className:"prpl-content-activity__table-cell",style:l,children:e[r].label}),(0,a.jsx)("td",{className:"prpl-content-activity__table-cell",style:n,children:t[r]||0})]},r)})}),(0,a.jsx)("tfoot",{className:"prpl-content-activity__table-foot",children:(0,a.jsxs)("tr",{className:"prpl-content-activity__table-row prpl-content-activity__table-row--total",children:[(0,a.jsx)("th",{className:"prpl-content-activity__table-cell",style:i,children:(0,r.__)("Total","progress-planner")}),(0,a.jsx)("td",{className:"prpl-content-activity__table-cell",style:o,children:s})]})})]})}function d(){return(0,a.jsx)("div",{className:"prpl-content-activity__loading",style:{display:"flex",justifyContent:"center",alignItems:"center",padding:"2em"},children:(0,r.__)("Loading…","progress-planner")})}function h({message:e}){return(0,a.jsx)("div",{className:"prpl-content-activity__error",style:{padding:"1em",backgroundColor:"var(--prpl-color-error-background, #fee)",color:"var(--prpl-color-error, #c00)",borderRadius:"var(--prpl-border-radius)"},children:e})}function x(){const[e,s]=(0,t.useState)(null),[i,o]=(0,t.useState)(!0),[x,f]=(0,t.useState)(null);return(0,t.useEffect)(()=>{l()({path:"/progress-planner/v1/content-activity"}).then(e=>{s(e),f(null)}).catch(e=>{f(e.message||(0,r.__)("Failed to load content activity data.","progress-planner"))}).finally(()=>{o(!1)})},[]),i?(0,a.jsx)(d,{}):x?(0,a.jsx)(h,{message:x}):e?(0,a.jsxs)("div",{className:"prpl-content-activity",children:[(0,a.jsx)(n,{number:e.totalCount,label:e.i18n?.piecesOfContentManaged||(0,r.__)("pieces of content managed","progress-planner"),backgroundColor:"var(--prpl-background-content)"}),(0,a.jsx)("div",{className:"prpl-content-activity__graph-wrapper",style:{marginBottom:"var(--prpl-padding)"},children:(0,a.jsx)(c,{data:e.chartData,options:e.chartOptions})}),(0,a.jsx)(p,{activityTypes:e.activityTypes,weeklyActivity:e.weeklyActivity,totalCount:e.weeklyTotalCount})]}):null}document.addEventListener("DOMContentLoaded",()=>{const e=document.getElementById("prpl-content-activity-root");e&&(0,t.createRoot)(e).render((0,a.jsx)(x,{}))})})(); \ No newline at end of file diff --git a/classes/admin/widgets/class-content-activity.php b/classes/admin/widgets/class-content-activity.php index 0c6198b830..e188894a1c 100644 --- a/classes/admin/widgets/class-content-activity.php +++ b/classes/admin/widgets/class-content-activity.php @@ -19,6 +19,32 @@ final class Content_Activity extends Widget { */ protected $id = 'content-activity'; + /** + * Enqueue scripts for this widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/content-activity.asset.php'; + + // Check if the React build exists. + if ( ! \file_exists( $asset_file ) ) { + // Fall back to old web components. + parent::enqueue_scripts(); + return; + } + + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/content-activity', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/content-activity.js', + $asset['dependencies'], + $asset['version'], + true + ); + } + /** * Get the chart args. * diff --git a/classes/class-base.php b/classes/class-base.php index e31bd8e6bf..eef3905a3b 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -18,6 +18,7 @@ * @method \Progress_Planner\Page_Types get_page_types() * @method \Progress_Planner\Rest\Stats get_rest__stats() * @method \Progress_Planner\Rest\Tasks get_rest__tasks() + * @method \Progress_Planner\Rest\Content_Activity get_rest__content_activity() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -121,6 +122,7 @@ public function init() { // REST API. $this->get_rest__stats(); $this->get_rest__tasks(); + $this->get_rest__content_activity(); // Onboarding. $this->get_utils__onboard(); diff --git a/classes/rest/class-content-activity.php b/classes/rest/class-content-activity.php new file mode 100644 index 0000000000..2978568cf0 --- /dev/null +++ b/classes/rest/class-content-activity.php @@ -0,0 +1,165 @@ + 'GET', + 'callback' => [ $this, 'get_content_activity' ], + 'permission_callback' => [ $this, 'check_permissions' ], + 'args' => [ + 'range' => [ + 'type' => 'string', + 'default' => '-6 months', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'frequency' => [ + 'type' => 'string', + 'default' => 'monthly', + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ], + ] + ); + } + + /** + * Check if the current user has permission to access this endpoint. + * + * @return bool + */ + public function check_permissions() { + return \current_user_can( 'edit_posts' ); + } + + /** + * Get content activity data. + * + * @param \WP_REST_Request $request The REST request object. + * + * @return \WP_REST_Response + */ + public function get_content_activity( $request ) { + $widget = \progress_planner()->get_admin__widgets__content_activity(); + + // Activity types configuration. + $activity_types = [ + 'publish' => [ + 'label' => \__( 'published', 'progress-planner' ), + 'color' => 'var(--prpl-color-monthly)', + ], + 'update' => [ + 'label' => \__( 'updated', 'progress-planner' ), + 'color' => 'var(--prpl-graph-color-3)', + ], + 'delete' => [ + 'label' => \__( 'deleted', 'progress-planner' ), + 'color' => 'var(--prpl-color-headings)', + ], + ]; + + $tracked_post_types = \progress_planner()->get_activities__content_helpers()->get_post_types_names(); + + // Prepare chart data and options. + $chart_data = []; + $chart_options = [ + 'dataArgs' => [], + 'chartId' => 'prpl-chart-content-activity', + 'axisColor' => 'var(--prpl-color-border)', + 'rulersColor' => 'var(--prpl-color-border)', + 'filtersLabel' => '' . \__( 'show:', 'progress-planner' ) . '', + ]; + + foreach ( $activity_types as $activity_type => $activity_data ) { + $chart_data[ $activity_type ] = \progress_planner() + ->get_ui__chart() + ->get_chart_data( + $widget->get_chart_args_content_count( + $activity_type, + $activity_data['color'] + ) + ); + + $chart_options['dataArgs'][ $activity_type ] = [ + 'color' => $activity_data['color'], + 'label' => $activity_data['label'], + ]; + } + + // Calculate weekly activity counts. + $weekly_activity = []; + $weekly_total = 0; + + foreach ( \array_keys( $activity_types ) as $activity_type ) { + $weekly_activity[ $activity_type ] = 0; + + $activities = \progress_planner()->get_activities__query()->query_activities( + [ + 'category' => 'content', + 'start_date' => \gmdate( 'Y-m-d', \strtotime( '-1 week' ) ), + 'end_date' => \gmdate( 'Y-m-d' ), + 'type' => $activity_type, + ] + ); + + if ( $activities ) { + if ( 'delete' !== $activity_type ) { + // Filter to only include tracked post types. + $activities = \array_filter( + $activities, + fn( $activity ) => \in_array( + \get_post_type( $activity->data_id ), + $tracked_post_types, + true + ) + ); + } + + $weekly_activity[ $activity_type ] = \count( $activities ); + } + + $weekly_total += $weekly_activity[ $activity_type ]; + } + + // Response data. + $response_data = [ + 'chartData' => $chart_data, + 'chartOptions' => $chart_options, + 'activityTypes' => $activity_types, + 'weeklyActivity' => $weekly_activity, + 'weeklyTotalCount' => $weekly_total, + 'totalCount' => \number_format_i18n( $weekly_total ), + 'i18n' => [ + 'title' => \__( 'Content activity', 'progress-planner' ), + 'description' => \__( 'Here are the updates you made to your content last week. Whether you published something new, updated an existing post, or removed outdated content, it all helps you stay on top of your site!', 'progress-planner' ), + 'piecesOfContentManaged' => \__( 'pieces of content managed', 'progress-planner' ), + 'contentManaged' => \__( 'Content managed', 'progress-planner' ), + 'lastWeek' => \__( 'Last week', 'progress-planner' ), + 'total' => \__( 'Total', 'progress-planner' ), + 'show' => \__( 'show:', 'progress-planner' ), + ], + ]; + + return new \WP_REST_Response( $response_data ); + } +} diff --git a/package-lock.json b/package-lock.json index da733a5a3c..a73db77d2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "1.2.0", "license": "GPL-3.0-or-later", "dependencies": { + "@wordpress/api-fetch": "*", + "@wordpress/element": "*", + "@wordpress/i18n": "*", "driver.js": "^1.3.1" }, "devDependencies": { @@ -4069,6 +4072,43 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tannin/compile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", + "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", + "license": "MIT", + "dependencies": { + "@tannin/evaluate": "^1.2.0", + "@tannin/postfix": "^1.1.0" + } + }, + "node_modules/@tannin/evaluate": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", + "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==", + "license": "MIT" + }, + "node_modules/@tannin/plural-forms": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", + "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", + "license": "MIT", + "dependencies": { + "@tannin/compile": "^1.1.0" + } + }, + "node_modules/@tannin/postfix": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", + "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", + "license": "MIT" + }, + "node_modules/@tannin/sprintf": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@tannin/sprintf/-/sprintf-1.3.3.tgz", + "integrity": "sha512-RwARl+hFwhzy0tg9atWcchLFvoQiOh4rrP7uG2N5E4W80BPCUX0ElcUR9St43fxB9EfjsW2df9Qp+UsTbvQDjA==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4396,6 +4436,12 @@ "@types/pg": "*" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -4408,6 +4454,25 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -5018,6 +5083,20 @@ } } }, + "node_modules/@wordpress/api-fetch": { + "version": "7.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-7.36.0.tgz", + "integrity": "sha512-71yTZi1tSqYbfzT5O+Cx2L2gWpp3y+twdch8mGIzpRmNDz6L/NvntIko7Qmc73tu3dSVC7KakvEmCduOaDNKRQ==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/i18n": "^6.9.0", + "@wordpress/url": "^4.36.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/babel-preset-default": { "version": "8.28.0", "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.28.0.tgz", @@ -5276,6 +5355,35 @@ "@playwright/test": ">=1" } }, + "node_modules/@wordpress/element": { + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.36.0.tgz", + "integrity": "sha512-6Ym/Ucik49skz1XJ2GRXENoMjJx7EYnY+fbfor9KtChiCd9/3H4/rI4sZgewVPIO//fCKEk7G30HoR+xB7GZMQ==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@wordpress/escape-html": "^3.36.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/escape-html": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.36.0.tgz", + "integrity": "sha512-0FvvlVPv+7X8lX5ExcTh6ib/xckGIuVXdnHglR3rZC1MJI682cx4JRUR0Igk6nKyPS8UiSQCKtN3U1aSPtZaCg==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/eslint-plugin": { "version": "22.14.0", "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.14.0.tgz", @@ -5335,6 +5443,36 @@ "node": ">=10" } }, + "node_modules/@wordpress/hooks": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.36.0.tgz", + "integrity": "sha512-9kB2lanmVrubJEqWDSHtyUx7q4ZAWGArakY/GsUdlFsnf9m+VmQLQl92uCpHWYjKzHec1hwcBhBB3Tu9aBWDtQ==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/i18n": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.9.0.tgz", + "integrity": "sha512-ke4BPQUHmj82mwYoasotKt3Sghf0jK4vec56cWxwnzUvqq7LMy/0H7F5NzJ4CY378WS+TOdLbqmIb4sj+f7eog==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@tannin/sprintf": "^1.3.2", + "@wordpress/hooks": "^4.36.0", + "gettext-parser": "^1.3.1", + "memize": "^2.1.0", + "tannin": "^1.2.0" + }, + "bin": { + "pot-to-php": "tools/pot-to-php.js" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/jest-console": { "version": "8.28.0", "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.28.0.tgz", @@ -5518,6 +5656,19 @@ "stylelint-scss": "^6.4.0" } }, + "node_modules/@wordpress/url": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.36.0.tgz", + "integrity": "sha512-b61pCnJCjaxIiiH/+leR3IVZlKUlSP/PnYCFg1cLa9Qv8TQBr5REnmtBDnrfNzaHEP7uE+A81BJe5lVFP/AQgw==", + "license": "GPL-2.0-or-later", + "dependencies": { + "remove-accents": "^0.5.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/warning": { "version": "3.28.0", "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.28.0.tgz", @@ -6736,7 +6887,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -6816,7 +6966,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -6843,7 +6992,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", @@ -7199,7 +7347,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -7696,6 +7843,12 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/cwd": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", @@ -8193,7 +8346,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -8311,6 +8463,15 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -10275,6 +10436,16 @@ "node": ">= 14" } }, + "node_modules/gettext-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", + "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", + "license": "MIT", + "dependencies": { + "encoding": "^0.1.12", + "safe-buffer": "^5.1.1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -10578,7 +10749,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" @@ -10856,7 +11026,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -11491,7 +11660,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12427,8 +12595,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -13014,7 +13181,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -13026,7 +13192,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -13283,6 +13448,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memize": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.1.tgz", + "integrity": "sha512-8Nl+i9S5D6KXnruM03Jgjb+LwSupvR13WBr4hJegaaEyobvowCVupi79y2WSiWvO1mzBWxPwEYE5feCe8vyA5w==", + "license": "MIT" + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -13643,7 +13814,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -14232,7 +14402,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -14320,7 +14489,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -14330,7 +14498,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -15709,7 +15876,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15721,8 +15887,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -15988,6 +16152,12 @@ "node": ">=6" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -16245,7 +16415,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -16297,8 +16466,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.90.0", @@ -16376,8 +16544,6 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -16527,7 +16693,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -16901,7 +17066,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -17983,6 +18147,15 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/tannin": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", + "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", + "license": "MIT", + "dependencies": { + "@tannin/plural-forms": "^1.1.0" + } + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -18358,8 +18531,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -18654,7 +18826,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -18663,7 +18834,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } diff --git a/package.json b/package.json index add3622958..3a81b574d4 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,13 @@ "husky": "*" }, "scripts": { + "build": "wp-scripts build", + "start": "wp-scripts start", "format": "wp-scripts format ./assets", "lint:css": "wp-scripts lint-style \"**/*.css\"", "lint:css:fix": "npm run lint:css -- --fix", - "lint:js": "wp-scripts lint-js ./assets/js/*.js && wp-scripts lint-js ./assets/js/web-components/*.js && wp-scripts lint-js ./assets/js/widgets/*.js && wp-scripts lint-js ./assets/js/recommendations/*.js && wp-scripts lint-js ./tests/**/*.js", - "lint:js:fix": "wp-scripts lint-js ./assets/js/*.js --fix && wp-scripts lint-js ./assets/js/web-components/*.js --fix && wp-scripts lint-js ./assets/js/widgets/*.js --fix && wp-scripts lint-js ./assets/js/recommendations/*.js --fix && wp-scripts lint-js ./tests/**/*.js --fix", + "lint:js": "wp-scripts lint-js ./assets/js/*.js && wp-scripts lint-js ./assets/js/web-components/*.js && wp-scripts lint-js ./assets/js/widgets/*.js && wp-scripts lint-js ./assets/js/recommendations/*.js && wp-scripts lint-js ./assets/src/**/*.js && wp-scripts lint-js ./tests/**/*.js", + "lint:js:fix": "wp-scripts lint-js ./assets/js/*.js --fix && wp-scripts lint-js ./assets/js/web-components/*.js --fix && wp-scripts lint-js ./assets/js/widgets/*.js --fix && wp-scripts lint-js ./assets/js/recommendations/*.js --fix && wp-scripts lint-js ./assets/src/**/*.js --fix && wp-scripts lint-js ./tests/**/*.js --fix", "prepare": "husky", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", @@ -36,6 +38,9 @@ "test": "npm run test:sequential && npm run test:parallel" }, "dependencies": { + "@wordpress/api-fetch": "*", + "@wordpress/element": "*", + "@wordpress/i18n": "*", "driver.js": "^1.3.1" } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 69a6fdc2c4..99cf261f10 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -21,6 +21,9 @@ /coverage/* + + /build/* + *.js *.css diff --git a/views/page-widgets/content-activity.php b/views/page-widgets/content-activity.php index b4a2755057..cd6e96b21f 100644 --- a/views/page-widgets/content-activity.php +++ b/views/page-widgets/content-activity.php @@ -8,79 +8,6 @@ if ( ! \defined( 'ABSPATH' ) ) { exit; } - -$prpl_widget = \progress_planner()->get_admin__widgets__content_activity(); - -$prpl_activity_types = [ - 'publish' => [ - 'label' => \__( 'published', 'progress-planner' ), - 'color' => 'var(--prpl-color-monthly)', - ], - 'update' => [ - 'label' => \__( 'updated', 'progress-planner' ), - 'color' => 'var(--prpl-graph-color-3)', - ], - 'delete' => [ - 'label' => \__( 'deleted', 'progress-planner' ), - 'color' => 'var(--prpl-color-headings)', - ], -]; - -$prpl_tracked_post_types = \progress_planner()->get_activities__content_helpers()->get_post_types_names(); -$prpl_activities_count = [ - 'all' => 0, -]; - -$prpl_chart_data = []; -$prpl_chart_options = [ - 'dataArgs' => [], - 'chartId' => 'prpl-chart-content-activity', - 'axisColor' => 'var(--prpl-color-border)', - 'rulersColor' => 'var(--prpl-color-border)', - 'filtersLabel' => '' . \__( 'show:', 'progress-planner' ) . '', -]; -foreach ( $prpl_activity_types as $prpl_activity_type => $prpl_activity_data ) { - $prpl_chart_data[ $prpl_activity_type ] = \progress_planner() - ->get_ui__chart() - ->get_chart_data( - $prpl_widget->get_chart_args_content_count( - $prpl_activity_type, - $prpl_activity_data['color'] - ) - ); - - $prpl_chart_options['dataArgs'][ $prpl_activity_type ] = [ - 'color' => $prpl_activity_data['color'], - 'label' => $prpl_activity_data['label'], - ]; -} - -foreach ( \array_keys( $prpl_activity_types ) as $prpl_activity_type ) { - // Default count. - $prpl_activities_count[ $prpl_activity_type ] = 0; - - // Get the activities. - $prpl_activities = \progress_planner()->get_activities__query()->query_activities( - [ - 'category' => 'content', - 'start_date' => \gmdate( 'Y-m-d', \strtotime( '-1 week' ) ), - 'end_date' => \gmdate( 'Y-m-d' ), - 'type' => $prpl_activity_type, - ] - ); - - if ( $prpl_activities ) { - if ( 'delete' !== $prpl_activity_type ) { - // Filter the activities to only include the tracked post types. - $prpl_activities = \array_filter( $prpl_activities, fn( $activity ) => \in_array( \get_post_type( $activity->data_id ), $prpl_tracked_post_types, true ) ); - } - - // Update the count. - $prpl_activities_count[ $prpl_activity_type ] = \count( $prpl_activities ); - } - - $prpl_activities_count['all'] += $prpl_activities_count[ $prpl_activity_type ]; -} ?>

@@ -93,38 +20,4 @@

- - -
- -
- - - - - - - - - - $prpl_activity_data ) : ?> - - - - - - - - - - - - -
+
diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000000..36184cda54 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,13 @@ +const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); +const path = require( 'path' ); + +module.exports = { + ...defaultConfig, + entry: { + 'content-activity': './assets/src/content-activity.js', + }, + output: { + path: path.resolve( __dirname, 'build' ), + filename: '[name].js', + }, +}; From 54a4c46292d07aed700c23cf68d55288a94650a5 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 19:00:45 +0200 Subject: [PATCH 002/275] monthly-badges --- assets/js/web-components/prpl-gauge.js | 7 + assets/src/components/Badge/index.js | 79 +++++++ .../src/components/BadgeProgressBar/index.js | 125 ++++++++++ assets/src/components/Gauge/index.js | 137 +++++++++++ assets/src/monthly-badges.js | 26 ++ .../widgets/MonthlyBadges/PointsCounter.js | 49 ++++ assets/src/widgets/MonthlyBadges/index.js | 223 ++++++++++++++++++ build/monthly-badges.asset.php | 1 + build/monthly-badges.js | 1 + .../admin/widgets/class-monthly-badges.php | 25 +- classes/class-base.php | 2 + classes/rest/class-monthly-badges.php | 94 ++++++++ views/page-widgets/monthly-badges.php | 99 +------- webpack.config.js | 1 + 14 files changed, 766 insertions(+), 103 deletions(-) create mode 100644 assets/src/components/Badge/index.js create mode 100644 assets/src/components/BadgeProgressBar/index.js create mode 100644 assets/src/components/Gauge/index.js create mode 100644 assets/src/monthly-badges.js create mode 100644 assets/src/widgets/MonthlyBadges/PointsCounter.js create mode 100644 assets/src/widgets/MonthlyBadges/index.js create mode 100644 build/monthly-badges.asset.php create mode 100644 build/monthly-badges.js create mode 100644 classes/rest/class-monthly-badges.php diff --git a/assets/js/web-components/prpl-gauge.js b/assets/js/web-components/prpl-gauge.js index a2786104aa..087ce01b69 100644 --- a/assets/js/web-components/prpl-gauge.js +++ b/assets/js/web-components/prpl-gauge.js @@ -240,6 +240,13 @@ const prplUpdateRaviGauge = ( pointsDiff ) => { return; } + // Dispatch event for React components. + document.dispatchEvent( + new CustomEvent( 'prpl-task-completed', { + detail: { points: pointsDiff }, + } ) + ); + // Get the gauge. const controllerGauge = document.getElementById( 'prpl-gauge-ravi' ); diff --git a/assets/src/components/Badge/index.js b/assets/src/components/Badge/index.js new file mode 100644 index 0000000000..488ff2b631 --- /dev/null +++ b/assets/src/components/Badge/index.js @@ -0,0 +1,79 @@ +/** + * Badge Component + * + * Displays a badge image fetched from the remote SaaS server. + */ + +import { useState, useCallback } from '@wordpress/element'; + +/** + * Badge component. + * + * @param {Object} props - Component props. + * @param {string} props.badgeId - The badge ID (e.g., "monthly-2025-m12"). + * @param {string} props.badgeName - The badge name for alt text. + * @param {number} props.brandingId - Optional branding ID. + * @param {string} props.remoteServerUrl - Remote server URL for badge SVGs. + * @param {string} props.placeholderUrl - Placeholder image URL for errors. + * @param {boolean} props.isComplete - Whether the badge is complete. + * @return {JSX.Element} The Badge component. + */ +export default function Badge( { + badgeId, + badgeName, + brandingId = 0, + remoteServerUrl, + placeholderUrl, + isComplete = false, +} ) { + const [ hasError, setHasError ] = useState( false ); + + /** + * Build the badge SVG URL. + * + * @return {string} The badge URL. + */ + const getBadgeUrl = useCallback( () => { + let url = `${ remoteServerUrl }/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${ badgeId }`; + if ( brandingId ) { + url += `&branding_id=${ brandingId }`; + } + return url; + }, [ badgeId, brandingId, remoteServerUrl ] ); + + /** + * Handle image load error. + */ + const handleError = useCallback( () => { + if ( ! hasError && placeholderUrl ) { + setHasError( true ); + } + }, [ hasError, placeholderUrl ] ); + + const containerStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + + const imageStyle = { + maxWidth: '100%', + height: 'auto', + }; + + const className = `prpl-badge${ + isComplete ? ' prpl-badge--complete' : '' + }`; + + return ( +
+ { +
+ ); +} diff --git a/assets/src/components/BadgeProgressBar/index.js b/assets/src/components/BadgeProgressBar/index.js new file mode 100644 index 0000000000..97d76c3063 --- /dev/null +++ b/assets/src/components/BadgeProgressBar/index.js @@ -0,0 +1,125 @@ +/** + * BadgeProgressBar Component + * + * Displays a progress bar with a badge icon for incomplete previous months. + */ + +import { useMemo } from '@wordpress/element'; +import Badge from '../Badge'; + +/** + * BadgeProgressBar component. + * + * @param {Object} props - Component props. + * @param {string} props.badgeId - The badge ID. + * @param {string} props.badgeName - The badge name. + * @param {number} props.points - Current points. + * @param {number} props.maxPoints - Maximum points (default 10). + * @param {number} props.brandingId - Branding ID. + * @param {string} props.remoteServerUrl - Remote server URL for badge SVGs. + * @param {string} props.placeholderUrl - Placeholder image URL. + * @return {JSX.Element} The BadgeProgressBar component. + */ +export default function BadgeProgressBar( { + badgeId, + badgeName, + points = 0, + maxPoints = 10, + brandingId = 0, + remoteServerUrl, + placeholderUrl, +} ) { + /** + * Calculate progress percentage. + */ + const progressPercent = useMemo( () => { + if ( maxPoints === 0 ) { + return 0; + } + return ( points / maxPoints ) * 100; + }, [ points, maxPoints ] ); + + const containerStyle = { + padding: '1rem 0', + }; + + const barStyle = { + width: '100%', + height: '1rem', + backgroundColor: 'var(--prpl-color-gauge-remain)', + borderRadius: '0.5rem', + position: 'relative', + }; + + const progressStyle = { + height: '100%', + backgroundColor: 'var(--prpl-color-monthly)', + borderRadius: '0.5rem', + transition: 'width 0.4s ease', + width: `${ progressPercent }%`, + }; + + const badgeWrapperStyle = { + display: 'flex', + width: '7.5rem', + height: 'auto', + position: 'absolute', + top: '-2.5rem', + transition: 'left 0.4s ease', + left: `calc(${ progressPercent }% - 3.75rem)`, + }; + + const alertIndicatorStyle = { + content: '"!"', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '20px', + height: '20px', + backgroundColor: 'var(--prpl-color-alert-error)', + border: '2px solid #fff', + borderRadius: '50%', + position: 'absolute', + top: '10%', + right: '25%', + color: '#fff', + fontSize: '12px', + fontWeight: 'bold', + }; + + const isComplete = points >= maxPoints; + const className = `prpl-badge-progress-bar${ + isComplete ? ' prpl-badge-progress-bar--complete' : '' + }`; + + return ( +
+
+
+
+ + { ! isComplete && ( + + ! + + ) } +
+
+
+ ); +} diff --git a/assets/src/components/Gauge/index.js b/assets/src/components/Gauge/index.js new file mode 100644 index 0000000000..fbeefc5ffa --- /dev/null +++ b/assets/src/components/Gauge/index.js @@ -0,0 +1,137 @@ +/** + * Gauge Component + * + * Displays a semi-circular progress gauge using CSS conic-gradient. + */ + +import { useMemo } from '@wordpress/element'; + +/** + * Gauge component. + * + * @param {Object} props - Component props. + * @param {number} props.value - Current progress value. + * @param {number} props.max - Maximum value (default 10). + * @param {string} props.backgroundColor - Background color CSS variable. + * @param {string} props.color - Primary progress color CSS variable. + * @param {string} props.color2 - Secondary progress color CSS variable. + * @param {JSX.Element} props.children - Content to display in the gauge center. + * @return {JSX.Element} The Gauge component. + */ +export default function Gauge( { + value = 0, + max = 10, + backgroundColor = 'var(--prpl-background-monthly)', + color = 'var(--prpl-color-monthly)', + color2 = 'var(--prpl-color-monthly-2)', + children, +} ) { + const maxDeg = '180deg'; + const start = '270deg'; + const cutout = '57%'; + + /** + * Calculate the conic gradient color transitions. + */ + const colorTransitions = useMemo( () => { + const progress = max > 0 ? value / max : 0; + let transitions; + + // If progress is less than 50%, use single color (no gradient) + if ( progress <= 0.5 ) { + transitions = `${ color } calc(${ maxDeg } * ${ progress })`; + } else { + // Show first color for 0.5, then second color + transitions = `${ color } calc(${ maxDeg } * 0.5)`; + transitions += `, ${ color2 } calc(${ maxDeg } * ${ progress })`; + } + + // Add remaining (unfilled) color + transitions += `, var(--prpl-color-gauge-remain) calc(${ maxDeg } * ${ progress }) ${ maxDeg }`; + + return transitions; + }, [ value, max, color, color2 ] ); + + const containerStyle = { + padding: + 'var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)', + background: backgroundColor, + borderRadius: 'var(--prpl-border-radius-big)', + aspectRatio: '2 / 1', + overflow: 'hidden', + position: 'relative', + marginBottom: 'var(--prpl-padding)', + }; + + const gaugeStyle = { + width: '100%', + aspectRatio: '1 / 1', + borderRadius: '100%', + position: 'relative', + background: `radial-gradient(${ backgroundColor } 0 ${ cutout }, transparent ${ cutout } 100%), conic-gradient(from ${ start }, ${ colorTransitions }, transparent ${ maxDeg })`, + textAlign: 'center', + }; + + const labelStyle = { + fontSize: 'var(--prpl-font-size-small)', + position: 'absolute', + top: '50%', + color: 'var(--prpl-color-text)', + width: '10%', + textAlign: 'center', + }; + + const leftLabelStyle = { + ...labelStyle, + left: 0, + }; + + const rightLabelStyle = { + ...labelStyle, + right: 0, + }; + + const contentStyle = { + fontSize: 'var(--prpl-font-size-6xl)', + bottom: '50%', + display: 'block', + fontWeight: 600, + textAlign: 'center', + position: 'absolute', + color: 'var(--prpl-color-text)', + width: '100%', + lineHeight: 1.2, + }; + + const contentInnerStyle = { + display: 'inline-block', + width: '50%', + }; + + return ( +
+
+ + 0 + + + + { children } + + + + { max } + +
+
+ ); +} diff --git a/assets/src/monthly-badges.js b/assets/src/monthly-badges.js new file mode 100644 index 0000000000..909eecdf33 --- /dev/null +++ b/assets/src/monthly-badges.js @@ -0,0 +1,26 @@ +/** + * Monthly Badges Entry Point + * + * Mounts the MonthlyBadges React widget to the DOM. + */ + +import { createRoot } from '@wordpress/element'; +import MonthlyBadges from './widgets/MonthlyBadges'; + +/** + * Initialize the Monthly Badges widget. + */ +function init() { + const container = document.getElementById( 'prpl-monthly-badges-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/widgets/MonthlyBadges/PointsCounter.js b/assets/src/widgets/MonthlyBadges/PointsCounter.js new file mode 100644 index 0000000000..37a2e20623 --- /dev/null +++ b/assets/src/widgets/MonthlyBadges/PointsCounter.js @@ -0,0 +1,49 @@ +/** + * PointsCounter Component + * + * Displays the current points and remaining points needed. + */ + +import { __ } from '@wordpress/i18n'; + +/** + * PointsCounter component. + * + * @param {Object} props - Component props. + * @param {number} props.points - Current points. + * @param {string} props.label - Label text. + * @param {boolean} props.showUnit - Whether to show "pt" unit. + * @return {JSX.Element} The PointsCounter component. + */ +export default function PointsCounter( { + points, + label = __( 'Progress monthly badge', 'progress-planner' ), + showUnit = true, +} ) { + const containerStyle = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }; + + const numberStyle = { + fontSize: 'var(--prpl-font-size-3xl)', + fontWeight: 600, + }; + + return ( +
+ { label } + + { points } + { showUnit && 'pt' } + +
+ ); +} diff --git a/assets/src/widgets/MonthlyBadges/index.js b/assets/src/widgets/MonthlyBadges/index.js new file mode 100644 index 0000000000..16c7adfe64 --- /dev/null +++ b/assets/src/widgets/MonthlyBadges/index.js @@ -0,0 +1,223 @@ +/** + * MonthlyBadges Widget + * + * Main widget component that displays the monthly badge gauge + * with real-time updates on task completion. + */ + +import { useState, useEffect, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import Gauge from '../../components/Gauge'; +import Badge from '../../components/Badge'; +import BadgeProgressBar from '../../components/BadgeProgressBar'; +import PointsCounter from './PointsCounter'; + +/** + * MonthlyBadges widget component. + * + * @return {JSX.Element} The MonthlyBadges widget. + */ +export default function MonthlyBadges() { + const [ isLoading, setIsLoading ] = useState( true ); + const [ error, setError ] = useState( null ); + const [ gaugeValue, setGaugeValue ] = useState( 0 ); + const [ maxPoints, setMaxPoints ] = useState( 10 ); + const [ currentBadge, setCurrentBadge ] = useState( null ); + const [ previousBadges, setPreviousBadges ] = useState( [] ); + const [ config, setConfig ] = useState( { + brandingId: 0, + remoteServerUrl: '', + placeholderUrl: '', + } ); + + /** + * Calculate if the current badge is complete. + */ + const isComplete = gaugeValue >= maxPoints; + + /** + * Update progress when tasks are completed. + * Fills gauge first, then overflows to previous month progress bars. + * + * @param {number} amount - Points to add. + */ + const updateProgress = useCallback( + ( amount ) => { + let remaining = amount; + + // First, fill the gauge + setGaugeValue( ( prevValue ) => { + const newValue = Math.min( prevValue + remaining, maxPoints ); + remaining -= newValue - prevValue; + return newValue; + } ); + + // If there's overflow and previous badges exist, fill them + if ( remaining > 0 && previousBadges.length > 0 ) { + setPreviousBadges( ( prevBadges ) => { + return prevBadges.map( ( badge ) => { + if ( remaining <= 0 ) { + return badge; + } + const badgeMax = badge.maxPoints || 10; + const newPoints = Math.min( + badge.points + remaining, + badgeMax + ); + remaining -= newPoints - badge.points; + return { ...badge, points: newPoints }; + } ); + } ); + } + }, + [ maxPoints, previousBadges.length ] + ); + + /** + * Fetch initial data from REST API. + */ + useEffect( () => { + const fetchData = async () => { + try { + const response = await apiFetch( { + path: '/progress-planner/v1/monthly-badges', + } ); + + setGaugeValue( response.score?.score || 0 ); + setMaxPoints( response.score?.target || 10 ); + setCurrentBadge( response.currentBadge || null ); + setPreviousBadges( response.previousIncompleteBadges || [] ); + setConfig( { + brandingId: response.brandingId || 0, + remoteServerUrl: response.remoteServerUrl || '', + placeholderUrl: response.placeholderUrl || '', + } ); + setIsLoading( false ); + } catch ( err ) { + setError( + err.message || + __( 'Failed to load data', 'progress-planner' ) + ); + setIsLoading( false ); + } + }; + + fetchData(); + }, [] ); + + /** + * Listen for task completion events. + */ + useEffect( () => { + const handleTaskComplete = ( event ) => { + const { points } = event.detail || {}; + if ( points && typeof points === 'number' ) { + updateProgress( points ); + } + }; + + document.addEventListener( 'prpl-task-completed', handleTaskComplete ); + return () => { + document.removeEventListener( + 'prpl-task-completed', + handleTaskComplete + ); + }; + }, [ updateProgress ] ); + + const containerStyle = { + display: 'flex', + flexDirection: 'column', + gap: '1rem', + }; + + const progressBarsContainerStyle = { + display: 'flex', + flexDirection: 'column', + gap: '0.5rem', + }; + + const loadingStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '200px', + color: 'var(--prpl-color-text)', + }; + + const errorStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '200px', + color: 'var(--prpl-color-alert-error)', + }; + + if ( isLoading ) { + return ( +
+ { __( 'Loading…', 'progress-planner' ) } +
+ ); + } + + if ( error ) { + return ( +
+ { error } +
+ ); + } + + return ( +
+ { /* Progress bars for previous incomplete months */ } + { previousBadges.length > 0 && ( +
+ { previousBadges.map( ( badge ) => ( + + ) ) } +
+ ) } + + { /* Main gauge with current badge */ } + + { currentBadge && ( + + ) } + + + { /* Points counter */ } + +
+ ); +} diff --git a/build/monthly-badges.asset.php b/build/monthly-badges.asset.php new file mode 100644 index 0000000000..b54d7ea8ac --- /dev/null +++ b/build/monthly-badges.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'dd76b1605f039962ad4b'); diff --git a/build/monthly-badges.js b/build/monthly-badges.js new file mode 100644 index 0000000000..eec85a784f --- /dev/null +++ b/build/monthly-badges.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var l in a)e.o(a,l)&&!e.o(r,l)&&Object.defineProperty(r,l,{enumerable:!0,get:a[l]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,l=window.wp.apiFetch;var t=e.n(l);const n=window.ReactJSXRuntime;function s({value:e=0,max:a=10,backgroundColor:l="var(--prpl-background-monthly)",color:t="var(--prpl-color-monthly)",color2:s="var(--prpl-color-monthly-2)",children:o}){const p="180deg",i={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:l,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${l} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let l;return r<=.5?l=`${t} calc(${p} * ${r})`:(l=`${t} calc(${p} * 0.5)`,l+=`, ${s} calc(${p} * ${r})`),l+=`, var(--prpl-color-gauge-remain) calc(${p} * ${r}) ${p}`,l},[e,a,t,s])}, transparent ${p})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,n.jsx)("div",{className:"prpl-gauge",style:i,children:(0,n.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,n.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,n.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:o})}),(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function o({badgeId:e,badgeName:a,brandingId:l=0,remoteServerUrl:t,placeholderUrl:s,isComplete:o=!1}){const[p,i]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${t}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return l&&(r+=`&branding_id=${l}`),r},[e,l,t]),c=(0,r.useCallback)(()=>{!p&&s&&i(!0)},[p,s]),g="prpl-badge"+(o?" prpl-badge--complete":"");return(0,n.jsx)("div",{className:g,style:{display:"flex",alignItems:"center",justifyContent:"center"},children:(0,n.jsx)("img",{className:"prpl-badge__image",src:p?s:d(),alt:a||"Badge",onError:c,style:{maxWidth:"100%",height:"auto"}})})}function p({badgeId:e,badgeName:a,points:l=0,maxPoints:t=10,brandingId:s=0,remoteServerUrl:p,placeholderUrl:i}){const d=(0,r.useMemo)(()=>0===t?0:l/t*100,[l,t]),c={height:"100%",backgroundColor:"var(--prpl-color-monthly)",borderRadius:"0.5rem",transition:"width 0.4s ease",width:`${d}%`},g={display:"flex",width:"7.5rem",height:"auto",position:"absolute",top:"-2.5rem",transition:"left 0.4s ease",left:`calc(${d}% - 3.75rem)`},m=l>=t,b="prpl-badge-progress-bar"+(m?" prpl-badge-progress-bar--complete":"");return(0,n.jsx)("div",{className:b,style:{padding:"1rem 0"},children:(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__bar",style:{width:"100%",height:"1rem",backgroundColor:"var(--prpl-color-gauge-remain)",borderRadius:"0.5rem",position:"relative"},children:[(0,n.jsx)("div",{className:"prpl-badge-progress-bar__progress",style:c}),(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__badge-wrapper",style:g,children:[(0,n.jsx)(o,{badgeId:e,badgeName:a,brandingId:s,remoteServerUrl:p,placeholderUrl:i}),!m&&(0,n.jsx)("span",{className:"prpl-badge-progress-bar__alert",style:{content:'"!"',display:"flex",alignItems:"center",justifyContent:"center",width:"20px",height:"20px",backgroundColor:"var(--prpl-color-alert-error)",border:"2px solid #fff",borderRadius:"50%",position:"absolute",top:"10%",right:"25%",color:"#fff",fontSize:"12px",fontWeight:"bold"},children:"!"})]})]})})}function i({points:e,label:r=(0,a.__)("Progress monthly badge","progress-planner"),showUnit:l=!0}){return(0,n.jsxs)("div",{className:"prpl-monthly-badges__points-counter",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,n.jsx)("span",{className:"prpl-monthly-badges__points-label",children:r}),(0,n.jsxs)("span",{className:"prpl-monthly-badges__points-number",style:{fontSize:"var(--prpl-font-size-3xl)",fontWeight:600},children:[e,l&&"pt"]})]})}function d(){const[e,l]=(0,r.useState)(!0),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)(0),[b,u]=(0,r.useState)(10),[h,v]=(0,r.useState)(null),[y,x]=(0,r.useState)([]),[f,_]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""}),j=g>=b,w=(0,r.useCallback)(e=>{let r=e;m(e=>{const a=Math.min(e+r,b);return r-=a-e,a}),r>0&&y.length>0&&x(e=>e.map(e=>{if(r<=0)return e;const a=e.maxPoints||10,l=Math.min(e.points+r,a);return r-=l-e.points,{...e,points:l}}))},[b,y.length]);(0,r.useEffect)(()=>{(async()=>{try{const e=await t()({path:"/progress-planner/v1/monthly-badges"});m(e.score?.score||0),u(e.score?.target||10),v(e.currentBadge||null),x(e.previousIncompleteBadges||[]),_({brandingId:e.brandingId||0,remoteServerUrl:e.remoteServerUrl||"",placeholderUrl:e.placeholderUrl||""}),l(!1)}catch(e){c(e.message||(0,a.__)("Failed to load data","progress-planner")),l(!1)}})()},[]),(0,r.useEffect)(()=>{const e=e=>{const{points:r}=e.detail||{};r&&"number"==typeof r&&w(r)};return document.addEventListener("prpl-task-completed",e),()=>{document.removeEventListener("prpl-task-completed",e)}},[w]);return e?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--loading",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-text)"},children:(0,a.__)("Loading…","progress-planner")}):d?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--error",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-alert-error)"},children:d}):(0,n.jsxs)("div",{className:"prpl-monthly-badges",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[y.length>0&&(0,n.jsx)("div",{className:"prpl-monthly-badges__previous-badges",style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:y.map(e=>(0,n.jsx)(p,{badgeId:e.id,badgeName:e.name,points:e.points,maxPoints:e.maxPoints||10,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl},e.id))}),(0,n.jsx)(s,{value:g,max:b,children:h&&(0,n.jsx)(o,{badgeId:h.id,badgeName:h.name,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl,isComplete:j})}),(0,n.jsx)(i,{points:g,label:(0,a.__)("Progress monthly badge","progress-planner")})]})}function c(){const e=document.getElementById("prpl-monthly-badges-root");e&&(0,r.createRoot)(e).render((0,n.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()})(); \ No newline at end of file diff --git a/classes/admin/widgets/class-monthly-badges.php b/classes/admin/widgets/class-monthly-badges.php index b50c2050f1..66c0e1538e 100644 --- a/classes/admin/widgets/class-monthly-badges.php +++ b/classes/admin/widgets/class-monthly-badges.php @@ -72,21 +72,20 @@ public function get_previous_incomplete_months_badges() { } /** - * Get the stylesheet dependencies. + * Enqueue scripts for this widget. * - * @return array + * @return void */ - public function get_stylesheet_dependencies() { - // Register styles for the web-component. - \wp_register_style( - 'progress-planner-suggested-task', - \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/suggested-task.css', - [], - \progress_planner()->get_file_version( \constant( 'PROGRESS_PLANNER_DIR' ) . '/assets/css/suggested-task.css' ) - ); + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/monthly-badges.asset.php'; + $asset = include $asset_file; - return [ - 'progress-planner-suggested-task', - ]; + \wp_enqueue_script( + 'progress-planner/monthly-badges', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/monthly-badges.js', + $asset['dependencies'], + $asset['version'], + true + ); } } diff --git a/classes/class-base.php b/classes/class-base.php index eef3905a3b..01b76d0d90 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -19,6 +19,7 @@ * @method \Progress_Planner\Rest\Stats get_rest__stats() * @method \Progress_Planner\Rest\Tasks get_rest__tasks() * @method \Progress_Planner\Rest\Content_Activity get_rest__content_activity() + * @method \Progress_Planner\Rest\Monthly_Badges get_rest__monthly_badges() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -123,6 +124,7 @@ public function init() { $this->get_rest__stats(); $this->get_rest__tasks(); $this->get_rest__content_activity(); + $this->get_rest__monthly_badges(); // Onboarding. $this->get_utils__onboard(); diff --git a/classes/rest/class-monthly-badges.php b/classes/rest/class-monthly-badges.php new file mode 100644 index 0000000000..e8e90db7bc --- /dev/null +++ b/classes/rest/class-monthly-badges.php @@ -0,0 +1,94 @@ + 'GET', + 'callback' => [ $this, 'get_monthly_badges' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + } + + /** + * Check if the current user has permission to access this endpoint. + * + * @return bool + */ + public function check_permissions() { + return \current_user_can( 'edit_posts' ); + } + + /** + * Get monthly badges data. + * + * @param \WP_REST_Request $request The REST request object. + * + * @return \WP_REST_Response + */ + public function get_monthly_badges( $request ) { + $widget = \progress_planner()->get_admin__widgets__monthly_badges(); + + // Get current month score. + $score = $widget->get_score(); + + // Get current month badge. + $current_date = new \DateTime(); + $current_badge_id = Monthly::get_badge_id_from_date( $current_date ); + $current_badge = Monthly::get_instance_from_id( $current_badge_id ); + $current_badge_name = $current_badge ? $current_badge->get_name() : ''; + $current_badge_data = [ + 'id' => $current_badge_id, + 'name' => $current_badge_name, + ]; + + // Get previous incomplete badges. + $previous_incomplete = []; + $previous_badges = $widget->get_previous_incomplete_months_badges(); + + foreach ( $previous_badges as $badge ) { + $progress = $badge->progress_callback( [ 'no_next_badge_points' => true ] ); + $previous_incomplete[] = [ + 'id' => $badge->get_id(), + 'name' => $badge->get_name(), + 'points' => $progress['points'], + 'maxPoints' => Monthly::TARGET_POINTS, + ]; + } + + // Build response data. + $response_data = [ + 'score' => $score, + 'currentBadge' => $current_badge_data, + 'previousIncompleteBadges' => $previous_incomplete, + 'brandingId' => (int) \progress_planner()->get_ui__branding()->get_branding_id(), + 'remoteServerUrl' => \progress_planner()->get_remote_server_root_url(), + 'placeholderUrl' => \progress_planner()->get_placeholder_svg( 200, 200 ), + ]; + + return new \WP_REST_Response( $response_data ); + } +} diff --git a/views/page-widgets/monthly-badges.php b/views/page-widgets/monthly-badges.php index ac962716e6..b3e39574e9 100644 --- a/views/page-widgets/monthly-badges.php +++ b/views/page-widgets/monthly-badges.php @@ -5,52 +5,21 @@ * @package Progress_Planner */ -use Progress_Planner\Badges\Monthly; - if ( ! \defined( 'ABSPATH' ) ) { exit; } - -$prpl_widget = \progress_planner()->get_admin__widgets__monthly_badges(); -$prpl_badge = \progress_planner()->get_badges()->get_badge( Monthly::get_badge_id_from_date( new \DateTime() ) ); - ?> - -

- get_ui__branding()->get_widget_title( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - 'monthly-badges', - \esc_html__( 'Your monthly badge', 'progress-planner' ) - ); - ?> -

- - - - -
- - - get_score()['target_score']; ?>pt - -
+

+ get_ui__branding()->get_widget_title( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'monthly-badges', + \esc_html__( 'Your monthly badge', 'progress-planner' ) + ); + ?> +

-
- +
get_ui__popover()->the_popover( 'monthly-badges' )->render(); ?>
- -get_previous_incomplete_months_badges() ) ) : ?> -
- - get_score()['target'] - $prpl_widget->get_score()['target_score']; - $prpl_days_remaining = (int) \gmdate( 't' ) - (int) \gmdate( 'j' ); - ?> -
-

-

- Collect the surplus of points you earn, and get your badge!', 'progress-planner' ) ); ?> -

- get_previous_incomplete_months_badges() as $prpl_previous_incomplete_month_badge ) : ?> - progress_callback()['remaining']; ?> -
- - -
- - progress_callback()['points']; ?>pt - - - ' . (int) $prpl_remaining_points . '', - (int) $prpl_days_remaining - ); - ?> - -
-
- -
- diff --git a/webpack.config.js b/webpack.config.js index 36184cda54..ef00379a83 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ module.exports = { ...defaultConfig, entry: { 'content-activity': './assets/src/content-activity.js', + 'monthly-badges': './assets/src/monthly-badges.js', }, output: { path: path.resolve( __dirname, 'build' ), From 5eae1ae67bbfc4cd795a3b5be31296b7d436513f Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 19:26:10 +0200 Subject: [PATCH 003/275] content-badges --- assets/src/components/Badge/index.js | 34 ++-- assets/src/content-badges.js | 26 +++ assets/src/widgets/ContentBadges/index.js | 163 ++++++++++++++++++ build/content-badges.asset.php | 1 + build/content-badges.js | 3 + build/monthly-badges.asset.php | 2 +- build/monthly-badges.js | 2 +- .../widgets/class-badge-streak-content.php | 18 ++ classes/class-base.php | 2 + classes/rest/class-content-badges.php | 93 ++++++++++ views/page-widgets/badge-streak-content.php | 72 +------- webpack.config.js | 1 + 12 files changed, 323 insertions(+), 94 deletions(-) create mode 100644 assets/src/content-badges.js create mode 100644 assets/src/widgets/ContentBadges/index.js create mode 100644 build/content-badges.asset.php create mode 100644 build/content-badges.js create mode 100644 classes/rest/class-content-badges.php diff --git a/assets/src/components/Badge/index.js b/assets/src/components/Badge/index.js index 488ff2b631..75c8162d00 100644 --- a/assets/src/components/Badge/index.js +++ b/assets/src/components/Badge/index.js @@ -24,7 +24,7 @@ export default function Badge( { brandingId = 0, remoteServerUrl, placeholderUrl, - isComplete = false, + isComplete = true, } ) { const [ hasError, setHasError ] = useState( false ); @@ -50,30 +50,22 @@ export default function Badge( { } }, [ hasError, placeholderUrl ] ); - const containerStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }; - - const imageStyle = { + // Apply grayscale/opacity for incomplete badges (matching original CSS) + const imgStyle = { maxWidth: '100%', height: 'auto', + verticalAlign: 'bottom', + transition: 'opacity 0.3s ease-in-out, filter 0.3s ease-in-out', + ...( isComplete ? {} : { opacity: 0.25, filter: 'grayscale(1)' } ), }; - const className = `prpl-badge${ - isComplete ? ' prpl-badge--complete' : '' - }`; - return ( -
- { -
+ { ); } diff --git a/assets/src/content-badges.js b/assets/src/content-badges.js new file mode 100644 index 0000000000..331b27c6d0 --- /dev/null +++ b/assets/src/content-badges.js @@ -0,0 +1,26 @@ +/** + * Content Badges Entry Point + * + * Mounts the ContentBadges React widget to the DOM. + */ + +import { createRoot } from '@wordpress/element'; +import ContentBadges from './widgets/ContentBadges'; + +/** + * Initialize the Content Badges widget. + */ +function init() { + const container = document.getElementById( 'prpl-content-badges-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/widgets/ContentBadges/index.js b/assets/src/widgets/ContentBadges/index.js new file mode 100644 index 0000000000..3a570ce1cc --- /dev/null +++ b/assets/src/widgets/ContentBadges/index.js @@ -0,0 +1,163 @@ +/** + * ContentBadges Widget + * + * Displays the content badges widget with gauge and badge grid. + */ + +import { useState, useEffect } from '@wordpress/element'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import Gauge from '../../components/Gauge'; +import Badge from '../../components/Badge'; + +/** + * ContentBadges component. + * + * @return {JSX.Element} The ContentBadges component. + */ +export default function ContentBadges() { + const [ isLoading, setIsLoading ] = useState( true ); + const [ error, setError ] = useState( null ); + const [ currentBadge, setCurrentBadge ] = useState( null ); + const [ allBadges, setAllBadges ] = useState( [] ); + const [ config, setConfig ] = useState( { + brandingId: 0, + remoteServerUrl: '', + placeholderUrl: '', + } ); + + /** + * Fetch badge data from REST API. + */ + useEffect( () => { + const fetchData = async () => { + try { + const response = await apiFetch( { + path: '/progress-planner/v1/content-badges', + } ); + + setCurrentBadge( response.currentBadge ); + setAllBadges( response.allBadges || [] ); + setConfig( { + brandingId: response.brandingId, + remoteServerUrl: response.remoteServerUrl, + placeholderUrl: response.placeholderUrl, + } ); + setIsLoading( false ); + } catch ( err ) { + setError( err.message || 'Failed to load badge data' ); + setIsLoading( false ); + } + }; + + fetchData(); + }, [] ); + + if ( isLoading ) { + return

{ __( 'Loading…', 'progress-planner' ) }

; + } + + if ( error ) { + return

{ error }

; + } + + if ( ! currentBadge ) { + return

{ __( 'No badge data available.', 'progress-planner' ) }

; + } + + return ( + <> +

+ { __( + 'The more you work on meaninful content, the sooner you unlock new badges.', + 'progress-planner' + ) } +

+ +
+ + + +
+

+ + { sprintf( + /* translators: %s: The badge name. */ + __( 'Progress %s', 'progress-planner' ), + currentBadge.name + ) } + + + { currentBadge.progress }% + +

+

+ { sprintf( + /* translators: %s: The remaining number of posts or pages to write. */ + _n( + 'Write %s new post or page and earn your next badge!', + 'Write %s new posts or pages and earn your next badge!', + currentBadge.remaining, + 'progress-planner' + ), + currentBadge.remaining + ) } +

+
+
+ +
+ +
+
+ { allBadges.map( ( badge ) => ( + + +

{ badge.name }

+
+ ) ) } +
+
+ + ); +} diff --git a/build/content-badges.asset.php b/build/content-badges.asset.php new file mode 100644 index 0000000000..bee00bec16 --- /dev/null +++ b/build/content-badges.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'a7ca6917267c6cd92020'); diff --git a/build/content-badges.js b/build/content-badges.js new file mode 100644 index 0000000000..be56034354 --- /dev/null +++ b/build/content-badges.js @@ -0,0 +1,3 @@ +(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var n in a)e.o(a,n)&&!e.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:a[n]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,n=window.wp.apiFetch;var l=e.n(n);const t=window.ReactJSXRuntime;function o({value:e=0,max:a=10,backgroundColor:n="var(--prpl-background-monthly)",color:l="var(--prpl-color-monthly)",color2:o="var(--prpl-color-monthly-2)",children:s}){const d="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:n,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},i={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${n} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let n;return r<=.5?n=`${l} calc(${d} * ${r})`:(n=`${l} calc(${d} * 0.5)`,n+=`, ${o} calc(${d} * ${r})`),n+=`, var(--prpl-color-gauge-remain) calc(${d} * ${r}) ${d}`,n},[e,a,l,o])}, transparent ${d})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,t.jsx)("div",{className:"prpl-gauge",style:p,children:(0,t.jsxs)("div",{className:"prpl-gauge__ring",style:i,children:[(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,t.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,t.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:s})}),(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function s({badgeId:e,badgeName:a,brandingId:n=0,remoteServerUrl:l,placeholderUrl:o,isComplete:s=!0}){const[d,p]=(0,r.useState)(!1),i=(0,r.useCallback)(()=>{let r=`${l}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return n&&(r+=`&branding_id=${n}`),r},[e,n,l]),c=(0,r.useCallback)(()=>{!d&&o&&p(!0)},[d,o]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...s?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,t.jsx)("img",{className:"prpl-badge__image",src:d?o:i(),alt:a||"Badge",onError:c,style:g})}function d(){const[e,n]=(0,r.useState)(!0),[d,p]=(0,r.useState)(null),[i,c]=(0,r.useState)(null),[g,m]=(0,r.useState)([]),[u,b]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""});return(0,r.useEffect)(()=>{(async()=>{try{const e=await l()({path:"/progress-planner/v1/content-badges"});c(e.currentBadge),m(e.allBadges||[]),b({brandingId:e.brandingId,remoteServerUrl:e.remoteServerUrl,placeholderUrl:e.placeholderUrl}),n(!1)}catch(e){p(e.message||"Failed to load badge data"),n(!1)}})()},[]),e?(0,t.jsx)("p",{children:(0,a.__)("Loading…","progress-planner")}):d?(0,t.jsx)("p",{children:d}):i?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{children:(0,a.__)("The more you work on meaninful content, the sooner you unlock new badges.","progress-planner")}),(0,t.jsxs)("div",{className:"prpl-latest-badges-wrapper",children:[(0,t.jsx)(o,{value:i.progress,max:100,backgroundColor:i.background||"var(--prpl-background-content-badge)",color:"var(--prpl-color-monthly)",color2:"var(--prpl-color-monthly-2)",children:(0,t.jsx)(s,{badgeId:i.id,badgeName:i.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:!0})}),(0,t.jsxs)("div",{className:"prpl-badge-content-wrapper",children:[(0,t.jsxs)("p",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem",marginBottom:0},children:[(0,t.jsx)("span",{children:(0,a.sprintf)(/* translators: %s: The badge name. */ /* translators: %s: The badge name. */ +(0,a.__)("Progress %s","progress-planner"),i.name)}),(0,t.jsxs)("span",{style:{fontWeight:600,fontSize:"var(--prpl-font-size-3xl)"},children:[i.progress,"%"]})]}),(0,t.jsx)("p",{style:{marginTop:0},children:(0,a.sprintf)(/* translators: %s: The remaining number of posts or pages to write. */ /* translators: %s: The remaining number of posts or pages to write. */ +(0,a._n)("Write %s new post or page and earn your next badge!","Write %s new posts or pages and earn your next badge!",i.remaining,"progress-planner"),i.remaining)})]})]}),(0,t.jsx)("hr",{}),(0,t.jsx)("div",{className:"prpl-badges-container-achievements",children:(0,t.jsx)("div",{className:"progress-wrapper badge-group-content",children:g.map(e=>(0,t.jsxs)("span",{className:"prpl-badge","data-value":e.progress,children:[(0,t.jsx)(s,{badgeId:e.id,badgeName:e.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:e.isComplete}),(0,t.jsx)("p",{children:e.name})]},e.id))})})]}):(0,t.jsx)("p",{children:(0,a.__)("No badge data available.","progress-planner")})}function p(){const e=document.getElementById("prpl-content-badges-root");e&&(0,r.createRoot)(e).render((0,t.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/build/monthly-badges.asset.php b/build/monthly-badges.asset.php index b54d7ea8ac..63cf40e6ca 100644 --- a/build/monthly-badges.asset.php +++ b/build/monthly-badges.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'dd76b1605f039962ad4b'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '66ed1ef45f1197b5ec9e'); diff --git a/build/monthly-badges.js b/build/monthly-badges.js index eec85a784f..7f35ca3878 100644 --- a/build/monthly-badges.js +++ b/build/monthly-badges.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var l in a)e.o(a,l)&&!e.o(r,l)&&Object.defineProperty(r,l,{enumerable:!0,get:a[l]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,l=window.wp.apiFetch;var t=e.n(l);const n=window.ReactJSXRuntime;function s({value:e=0,max:a=10,backgroundColor:l="var(--prpl-background-monthly)",color:t="var(--prpl-color-monthly)",color2:s="var(--prpl-color-monthly-2)",children:o}){const p="180deg",i={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:l,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${l} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let l;return r<=.5?l=`${t} calc(${p} * ${r})`:(l=`${t} calc(${p} * 0.5)`,l+=`, ${s} calc(${p} * ${r})`),l+=`, var(--prpl-color-gauge-remain) calc(${p} * ${r}) ${p}`,l},[e,a,t,s])}, transparent ${p})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,n.jsx)("div",{className:"prpl-gauge",style:i,children:(0,n.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,n.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,n.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:o})}),(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function o({badgeId:e,badgeName:a,brandingId:l=0,remoteServerUrl:t,placeholderUrl:s,isComplete:o=!1}){const[p,i]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${t}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return l&&(r+=`&branding_id=${l}`),r},[e,l,t]),c=(0,r.useCallback)(()=>{!p&&s&&i(!0)},[p,s]),g="prpl-badge"+(o?" prpl-badge--complete":"");return(0,n.jsx)("div",{className:g,style:{display:"flex",alignItems:"center",justifyContent:"center"},children:(0,n.jsx)("img",{className:"prpl-badge__image",src:p?s:d(),alt:a||"Badge",onError:c,style:{maxWidth:"100%",height:"auto"}})})}function p({badgeId:e,badgeName:a,points:l=0,maxPoints:t=10,brandingId:s=0,remoteServerUrl:p,placeholderUrl:i}){const d=(0,r.useMemo)(()=>0===t?0:l/t*100,[l,t]),c={height:"100%",backgroundColor:"var(--prpl-color-monthly)",borderRadius:"0.5rem",transition:"width 0.4s ease",width:`${d}%`},g={display:"flex",width:"7.5rem",height:"auto",position:"absolute",top:"-2.5rem",transition:"left 0.4s ease",left:`calc(${d}% - 3.75rem)`},m=l>=t,b="prpl-badge-progress-bar"+(m?" prpl-badge-progress-bar--complete":"");return(0,n.jsx)("div",{className:b,style:{padding:"1rem 0"},children:(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__bar",style:{width:"100%",height:"1rem",backgroundColor:"var(--prpl-color-gauge-remain)",borderRadius:"0.5rem",position:"relative"},children:[(0,n.jsx)("div",{className:"prpl-badge-progress-bar__progress",style:c}),(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__badge-wrapper",style:g,children:[(0,n.jsx)(o,{badgeId:e,badgeName:a,brandingId:s,remoteServerUrl:p,placeholderUrl:i}),!m&&(0,n.jsx)("span",{className:"prpl-badge-progress-bar__alert",style:{content:'"!"',display:"flex",alignItems:"center",justifyContent:"center",width:"20px",height:"20px",backgroundColor:"var(--prpl-color-alert-error)",border:"2px solid #fff",borderRadius:"50%",position:"absolute",top:"10%",right:"25%",color:"#fff",fontSize:"12px",fontWeight:"bold"},children:"!"})]})]})})}function i({points:e,label:r=(0,a.__)("Progress monthly badge","progress-planner"),showUnit:l=!0}){return(0,n.jsxs)("div",{className:"prpl-monthly-badges__points-counter",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,n.jsx)("span",{className:"prpl-monthly-badges__points-label",children:r}),(0,n.jsxs)("span",{className:"prpl-monthly-badges__points-number",style:{fontSize:"var(--prpl-font-size-3xl)",fontWeight:600},children:[e,l&&"pt"]})]})}function d(){const[e,l]=(0,r.useState)(!0),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)(0),[b,u]=(0,r.useState)(10),[h,v]=(0,r.useState)(null),[y,x]=(0,r.useState)([]),[f,_]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""}),j=g>=b,w=(0,r.useCallback)(e=>{let r=e;m(e=>{const a=Math.min(e+r,b);return r-=a-e,a}),r>0&&y.length>0&&x(e=>e.map(e=>{if(r<=0)return e;const a=e.maxPoints||10,l=Math.min(e.points+r,a);return r-=l-e.points,{...e,points:l}}))},[b,y.length]);(0,r.useEffect)(()=>{(async()=>{try{const e=await t()({path:"/progress-planner/v1/monthly-badges"});m(e.score?.score||0),u(e.score?.target||10),v(e.currentBadge||null),x(e.previousIncompleteBadges||[]),_({brandingId:e.brandingId||0,remoteServerUrl:e.remoteServerUrl||"",placeholderUrl:e.placeholderUrl||""}),l(!1)}catch(e){c(e.message||(0,a.__)("Failed to load data","progress-planner")),l(!1)}})()},[]),(0,r.useEffect)(()=>{const e=e=>{const{points:r}=e.detail||{};r&&"number"==typeof r&&w(r)};return document.addEventListener("prpl-task-completed",e),()=>{document.removeEventListener("prpl-task-completed",e)}},[w]);return e?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--loading",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-text)"},children:(0,a.__)("Loading…","progress-planner")}):d?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--error",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-alert-error)"},children:d}):(0,n.jsxs)("div",{className:"prpl-monthly-badges",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[y.length>0&&(0,n.jsx)("div",{className:"prpl-monthly-badges__previous-badges",style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:y.map(e=>(0,n.jsx)(p,{badgeId:e.id,badgeName:e.name,points:e.points,maxPoints:e.maxPoints||10,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl},e.id))}),(0,n.jsx)(s,{value:g,max:b,children:h&&(0,n.jsx)(o,{badgeId:h.id,badgeName:h.name,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl,isComplete:j})}),(0,n.jsx)(i,{points:g,label:(0,a.__)("Progress monthly badge","progress-planner")})]})}function c(){const e=document.getElementById("prpl-monthly-badges-root");e&&(0,r.createRoot)(e).render((0,n.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()})(); \ No newline at end of file +(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var t in a)e.o(a,t)&&!e.o(r,t)&&Object.defineProperty(r,t,{enumerable:!0,get:a[t]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,t=window.wp.apiFetch;var l=e.n(t);const n=window.ReactJSXRuntime;function o({value:e=0,max:a=10,backgroundColor:t="var(--prpl-background-monthly)",color:l="var(--prpl-color-monthly)",color2:o="var(--prpl-color-monthly-2)",children:s}){const i="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:t,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${t} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let t;return r<=.5?t=`${l} calc(${i} * ${r})`:(t=`${l} calc(${i} * 0.5)`,t+=`, ${o} calc(${i} * ${r})`),t+=`, var(--prpl-color-gauge-remain) calc(${i} * ${r}) ${i}`,t},[e,a,l,o])}, transparent ${i})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,n.jsx)("div",{className:"prpl-gauge",style:p,children:(0,n.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,n.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,n.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:s})}),(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function s({badgeId:e,badgeName:a,brandingId:t=0,remoteServerUrl:l,placeholderUrl:o,isComplete:s=!0}){const[i,p]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${l}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return t&&(r+=`&branding_id=${t}`),r},[e,t,l]),c=(0,r.useCallback)(()=>{!i&&o&&p(!0)},[i,o]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...s?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,n.jsx)("img",{className:"prpl-badge__image",src:i?o:d(),alt:a||"Badge",onError:c,style:g})}function i({badgeId:e,badgeName:a,points:t=0,maxPoints:l=10,brandingId:o=0,remoteServerUrl:i,placeholderUrl:p}){const d=(0,r.useMemo)(()=>0===l?0:t/l*100,[t,l]),c={height:"100%",backgroundColor:"var(--prpl-color-monthly)",borderRadius:"0.5rem",transition:"width 0.4s ease",width:`${d}%`},g={display:"flex",width:"7.5rem",height:"auto",position:"absolute",top:"-2.5rem",transition:"left 0.4s ease",left:`calc(${d}% - 3.75rem)`},m=t>=l,b="prpl-badge-progress-bar"+(m?" prpl-badge-progress-bar--complete":"");return(0,n.jsx)("div",{className:b,style:{padding:"1rem 0"},children:(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__bar",style:{width:"100%",height:"1rem",backgroundColor:"var(--prpl-color-gauge-remain)",borderRadius:"0.5rem",position:"relative"},children:[(0,n.jsx)("div",{className:"prpl-badge-progress-bar__progress",style:c}),(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__badge-wrapper",style:g,children:[(0,n.jsx)(s,{badgeId:e,badgeName:a,brandingId:o,remoteServerUrl:i,placeholderUrl:p}),!m&&(0,n.jsx)("span",{className:"prpl-badge-progress-bar__alert",style:{content:'"!"',display:"flex",alignItems:"center",justifyContent:"center",width:"20px",height:"20px",backgroundColor:"var(--prpl-color-alert-error)",border:"2px solid #fff",borderRadius:"50%",position:"absolute",top:"10%",right:"25%",color:"#fff",fontSize:"12px",fontWeight:"bold"},children:"!"})]})]})})}function p({points:e,label:r=(0,a.__)("Progress monthly badge","progress-planner"),showUnit:t=!0}){return(0,n.jsxs)("div",{className:"prpl-monthly-badges__points-counter",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,n.jsx)("span",{className:"prpl-monthly-badges__points-label",children:r}),(0,n.jsxs)("span",{className:"prpl-monthly-badges__points-number",style:{fontSize:"var(--prpl-font-size-3xl)",fontWeight:600},children:[e,t&&"pt"]})]})}function d(){const[e,t]=(0,r.useState)(!0),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)(0),[b,u]=(0,r.useState)(10),[h,v]=(0,r.useState)(null),[y,x]=(0,r.useState)([]),[f,_]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""}),j=g>=b,w=(0,r.useCallback)(e=>{let r=e;m(e=>{const a=Math.min(e+r,b);return r-=a-e,a}),r>0&&y.length>0&&x(e=>e.map(e=>{if(r<=0)return e;const a=e.maxPoints||10,t=Math.min(e.points+r,a);return r-=t-e.points,{...e,points:t}}))},[b,y.length]);(0,r.useEffect)(()=>{(async()=>{try{const e=await l()({path:"/progress-planner/v1/monthly-badges"});m(e.score?.score||0),u(e.score?.target||10),v(e.currentBadge||null),x(e.previousIncompleteBadges||[]),_({brandingId:e.brandingId||0,remoteServerUrl:e.remoteServerUrl||"",placeholderUrl:e.placeholderUrl||""}),t(!1)}catch(e){c(e.message||(0,a.__)("Failed to load data","progress-planner")),t(!1)}})()},[]),(0,r.useEffect)(()=>{const e=e=>{const{points:r}=e.detail||{};r&&"number"==typeof r&&w(r)};return document.addEventListener("prpl-task-completed",e),()=>{document.removeEventListener("prpl-task-completed",e)}},[w]);return e?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--loading",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-text)"},children:(0,a.__)("Loading…","progress-planner")}):d?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--error",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-alert-error)"},children:d}):(0,n.jsxs)("div",{className:"prpl-monthly-badges",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[y.length>0&&(0,n.jsx)("div",{className:"prpl-monthly-badges__previous-badges",style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:y.map(e=>(0,n.jsx)(i,{badgeId:e.id,badgeName:e.name,points:e.points,maxPoints:e.maxPoints||10,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl},e.id))}),(0,n.jsx)(o,{value:g,max:b,children:h&&(0,n.jsx)(s,{badgeId:h.id,badgeName:h.name,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl,isComplete:j})}),(0,n.jsx)(p,{points:g,label:(0,a.__)("Progress monthly badge","progress-planner")})]})}function c(){const e=document.getElementById("prpl-monthly-badges-root");e&&(0,r.createRoot)(e).render((0,n.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()})(); \ No newline at end of file diff --git a/classes/admin/widgets/class-badge-streak-content.php b/classes/admin/widgets/class-badge-streak-content.php index eca6d5244a..cbc51c83b1 100644 --- a/classes/admin/widgets/class-badge-streak-content.php +++ b/classes/admin/widgets/class-badge-streak-content.php @@ -25,4 +25,22 @@ final class Badge_Streak_Content extends Badge_Streak { * @var bool */ protected $force_last_column = true; + + /** + * Enqueue scripts for this widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/content-badges.asset.php'; + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/content-badges', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/content-badges.js', + $asset['dependencies'], + $asset['version'], + true + ); + } } diff --git a/classes/class-base.php b/classes/class-base.php index 01b76d0d90..cd497edf92 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -20,6 +20,7 @@ * @method \Progress_Planner\Rest\Tasks get_rest__tasks() * @method \Progress_Planner\Rest\Content_Activity get_rest__content_activity() * @method \Progress_Planner\Rest\Monthly_Badges get_rest__monthly_badges() + * @method \Progress_Planner\Rest\Content_Badges get_rest__content_badges() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -125,6 +126,7 @@ public function init() { $this->get_rest__tasks(); $this->get_rest__content_activity(); $this->get_rest__monthly_badges(); + $this->get_rest__content_badges(); // Onboarding. $this->get_utils__onboard(); diff --git a/classes/rest/class-content-badges.php b/classes/rest/class-content-badges.php new file mode 100644 index 0000000000..509eaa4f88 --- /dev/null +++ b/classes/rest/class-content-badges.php @@ -0,0 +1,93 @@ + 'GET', + 'callback' => [ $this, 'get_content_badges' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + } + + /** + * Check if the current user has permission to access this endpoint. + * + * @return bool + */ + public function check_permissions() { + return \current_user_can( 'edit_posts' ); + } + + /** + * Get content badges data. + * + * @param \WP_REST_Request $request The REST request object. + * + * @return \WP_REST_Response + */ + public function get_content_badges( $request ) { + $widget = \progress_planner()->get_admin__widgets__badge_streak_content(); + + // Get the current badge in progress. + $current_badge_obj = $widget->get_details( 'content' ); + $current_badge_data = null; + + if ( $current_badge_obj ) { + $progress = $current_badge_obj->get_progress(); + $current_badge_data = [ + 'id' => $current_badge_obj->get_id(), + 'name' => $current_badge_obj->get_name(), + 'background' => $current_badge_obj->get_background(), + 'progress' => $progress['progress'], + 'remaining' => $progress['remaining'], + ]; + } + + // Get all content badges. + $all_badges = \progress_planner()->get_badges()->get_badges( 'content' ); + $all_badges_data = []; + + foreach ( $all_badges as $badge ) { + $progress = $badge->get_progress(); + $all_badges_data[] = [ + 'id' => $badge->get_id(), + 'name' => $badge->get_name(), + 'progress' => $progress['progress'], + 'isComplete' => 100 === (int) $progress['progress'], + ]; + } + + // Build response data. + $response_data = [ + 'currentBadge' => $current_badge_data, + 'allBadges' => $all_badges_data, + 'brandingId' => (int) \progress_planner()->get_ui__branding()->get_branding_id(), + 'remoteServerUrl' => \progress_planner()->get_remote_server_root_url(), + 'placeholderUrl' => \progress_planner()->get_placeholder_svg( 200, 200 ), + ]; + + return new \WP_REST_Response( $response_data ); + } +} diff --git a/views/page-widgets/badge-streak-content.php b/views/page-widgets/badge-streak-content.php index 381e1e5d7f..94dae71b42 100644 --- a/views/page-widgets/badge-streak-content.php +++ b/views/page-widgets/badge-streak-content.php @@ -8,11 +8,6 @@ if ( ! \defined( 'ABSPATH' ) ) { exit; } - -$prpl_widget_details = \progress_planner()->get_admin__widgets__badge_streak_content()->get_details( 'content' ); -if ( ! $prpl_widget_details ) { - return; -} ?>

@@ -29,72 +24,7 @@ ?>

-

- -
- - - -
-

- - get_name() ) ); - ?> - - get_progress()['progress']; ?>% -

- -

- get_progress()['remaining'], - 'progress-planner' - ) - ), - \esc_html( \number_format_i18n( $prpl_widget_details->get_progress()['remaining'] ) ) - ) - ?> -

-
-
- -
- -
-
- get_badges()->get_badges( 'content' ) as $prpl_badge ) : ?> - - -

get_name() ); ?>

-
- -
-
+
get_ui__popover()->the_popover( 'monthly-badges' )->render_button( diff --git a/webpack.config.js b/webpack.config.js index ef00379a83..854a4ff422 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,6 +6,7 @@ module.exports = { entry: { 'content-activity': './assets/src/content-activity.js', 'monthly-badges': './assets/src/monthly-badges.js', + 'content-badges': './assets/src/content-badges.js', }, output: { path: path.resolve( __dirname, 'build' ), From d519f0d418c36c45cd1d8d749fb60060a44a160a Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 19:38:32 +0200 Subject: [PATCH 004/275] Streak badges --- assets/src/streak-badges.js | 26 +++ assets/src/widgets/StreakBadges/index.js | 163 ++++++++++++++++++ build/streak-badges.asset.php | 1 + build/streak-badges.js | 3 + .../class-badge-streak-maintenance.php | 18 ++ classes/class-base.php | 2 + classes/rest/class-streak-badges.php | 93 ++++++++++ .../page-widgets/badge-streak-maintenance.php | 72 +------- webpack.config.js | 1 + 9 files changed, 308 insertions(+), 71 deletions(-) create mode 100644 assets/src/streak-badges.js create mode 100644 assets/src/widgets/StreakBadges/index.js create mode 100644 build/streak-badges.asset.php create mode 100644 build/streak-badges.js create mode 100644 classes/rest/class-streak-badges.php diff --git a/assets/src/streak-badges.js b/assets/src/streak-badges.js new file mode 100644 index 0000000000..d701396b38 --- /dev/null +++ b/assets/src/streak-badges.js @@ -0,0 +1,26 @@ +/** + * Streak Badges Entry Point + * + * Mounts the StreakBadges React widget to the DOM. + */ + +import { createRoot } from '@wordpress/element'; +import StreakBadges from './widgets/StreakBadges'; + +/** + * Initialize the Streak Badges widget. + */ +function init() { + const container = document.getElementById( 'prpl-streak-badges-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/widgets/StreakBadges/index.js b/assets/src/widgets/StreakBadges/index.js new file mode 100644 index 0000000000..b44065eae0 --- /dev/null +++ b/assets/src/widgets/StreakBadges/index.js @@ -0,0 +1,163 @@ +/** + * StreakBadges Widget + * + * Displays the streak badges widget with gauge and badge grid. + */ + +import { useState, useEffect } from '@wordpress/element'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import Gauge from '../../components/Gauge'; +import Badge from '../../components/Badge'; + +/** + * StreakBadges component. + * + * @return {JSX.Element} The StreakBadges component. + */ +export default function StreakBadges() { + const [ isLoading, setIsLoading ] = useState( true ); + const [ error, setError ] = useState( null ); + const [ currentBadge, setCurrentBadge ] = useState( null ); + const [ allBadges, setAllBadges ] = useState( [] ); + const [ config, setConfig ] = useState( { + brandingId: 0, + remoteServerUrl: '', + placeholderUrl: '', + } ); + + /** + * Fetch badge data from REST API. + */ + useEffect( () => { + const fetchData = async () => { + try { + const response = await apiFetch( { + path: '/progress-planner/v1/streak-badges', + } ); + + setCurrentBadge( response.currentBadge ); + setAllBadges( response.allBadges || [] ); + setConfig( { + brandingId: response.brandingId, + remoteServerUrl: response.remoteServerUrl, + placeholderUrl: response.placeholderUrl, + } ); + setIsLoading( false ); + } catch ( err ) { + setError( err.message || 'Failed to load badge data' ); + setIsLoading( false ); + } + }; + + fetchData(); + }, [] ); + + if ( isLoading ) { + return

{ __( 'Loading…', 'progress-planner' ) }

; + } + + if ( error ) { + return

{ error }

; + } + + if ( ! currentBadge ) { + return

{ __( 'No badge data available.', 'progress-planner' ) }

; + } + + return ( + <> +

+ { __( + 'Execute at least one website maintenance task every week.', + 'progress-planner' + ) } +

+ +
+ + + +
+

+ + { sprintf( + /* translators: %s: The badge name. */ + __( 'Progress %s', 'progress-planner' ), + currentBadge.name + ) } + + + { currentBadge.progress }% + +

+

+ { sprintf( + /* translators: %s: The remaining number of weeks. */ + _n( + '%s week to go to complete this streak!', + '%s weeks to go to complete this streak!', + currentBadge.remaining, + 'progress-planner' + ), + currentBadge.remaining + ) } +

+
+
+ +
+ +
+
+ { allBadges.map( ( badge ) => ( + + +

{ badge.name }

+
+ ) ) } +
+
+ + ); +} diff --git a/build/streak-badges.asset.php b/build/streak-badges.asset.php new file mode 100644 index 0000000000..798ecebb1e --- /dev/null +++ b/build/streak-badges.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '01c574ec2bb6e1576b11'); diff --git a/build/streak-badges.js b/build/streak-badges.js new file mode 100644 index 0000000000..a7aaa71a1f --- /dev/null +++ b/build/streak-badges.js @@ -0,0 +1,3 @@ +(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var l in a)e.o(a,l)&&!e.o(r,l)&&Object.defineProperty(r,l,{enumerable:!0,get:a[l]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,l=window.wp.apiFetch;var n=e.n(l);const t=window.ReactJSXRuntime;function s({value:e=0,max:a=10,backgroundColor:l="var(--prpl-background-monthly)",color:n="var(--prpl-color-monthly)",color2:s="var(--prpl-color-monthly-2)",children:o}){const i="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:l,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${l} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let l;return r<=.5?l=`${n} calc(${i} * ${r})`:(l=`${n} calc(${i} * 0.5)`,l+=`, ${s} calc(${i} * ${r})`),l+=`, var(--prpl-color-gauge-remain) calc(${i} * ${r}) ${i}`,l},[e,a,n,s])}, transparent ${i})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,t.jsx)("div",{className:"prpl-gauge",style:p,children:(0,t.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,t.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,t.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:o})}),(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function o({badgeId:e,badgeName:a,brandingId:l=0,remoteServerUrl:n,placeholderUrl:s,isComplete:o=!0}){const[i,p]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${n}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return l&&(r+=`&branding_id=${l}`),r},[e,l,n]),c=(0,r.useCallback)(()=>{!i&&s&&p(!0)},[i,s]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...o?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,t.jsx)("img",{className:"prpl-badge__image",src:i?s:d(),alt:a||"Badge",onError:c,style:g})}function i(){const[e,l]=(0,r.useState)(!0),[i,p]=(0,r.useState)(null),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)([]),[u,b]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""});return(0,r.useEffect)(()=>{(async()=>{try{const e=await n()({path:"/progress-planner/v1/streak-badges"});c(e.currentBadge),m(e.allBadges||[]),b({brandingId:e.brandingId,remoteServerUrl:e.remoteServerUrl,placeholderUrl:e.placeholderUrl}),l(!1)}catch(e){p(e.message||"Failed to load badge data"),l(!1)}})()},[]),e?(0,t.jsx)("p",{children:(0,a.__)("Loading…","progress-planner")}):i?(0,t.jsx)("p",{children:i}):d?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{children:(0,a.__)("Execute at least one website maintenance task every week.","progress-planner")}),(0,t.jsxs)("div",{className:"prpl-latest-badges-wrapper",children:[(0,t.jsx)(s,{value:d.progress,max:100,backgroundColor:d.background||"var(--prpl-background-streak)",color:"var(--prpl-color-monthly)",color2:"var(--prpl-color-monthly-2)",children:(0,t.jsx)(o,{badgeId:d.id,badgeName:d.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:!0})}),(0,t.jsxs)("div",{className:"prpl-badge-content-wrapper",children:[(0,t.jsxs)("p",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem",marginBottom:0},children:[(0,t.jsx)("span",{children:(0,a.sprintf)(/* translators: %s: The badge name. */ /* translators: %s: The badge name. */ +(0,a.__)("Progress %s","progress-planner"),d.name)}),(0,t.jsxs)("span",{style:{fontWeight:600,fontSize:"var(--prpl-font-size-3xl)"},children:[d.progress,"%"]})]}),(0,t.jsx)("p",{style:{marginTop:0},children:(0,a.sprintf)(/* translators: %s: The remaining number of weeks. */ /* translators: %s: The remaining number of weeks. */ +(0,a._n)("%s week to go to complete this streak!","%s weeks to go to complete this streak!",d.remaining,"progress-planner"),d.remaining)})]})]}),(0,t.jsx)("hr",{}),(0,t.jsx)("div",{className:"prpl-badges-container-achievements",children:(0,t.jsx)("div",{className:"progress-wrapper badge-group-maintenance",children:g.map(e=>(0,t.jsxs)("span",{className:"prpl-badge","data-value":e.progress,children:[(0,t.jsx)(o,{badgeId:e.id,badgeName:e.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:e.isComplete}),(0,t.jsx)("p",{children:e.name})]},e.id))})})]}):(0,t.jsx)("p",{children:(0,a.__)("No badge data available.","progress-planner")})}function p(){const e=document.getElementById("prpl-streak-badges-root");e&&(0,r.createRoot)(e).render((0,t.jsx)(i,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/classes/admin/widgets/class-badge-streak-maintenance.php b/classes/admin/widgets/class-badge-streak-maintenance.php index 9954103398..c7cbd7a45e 100644 --- a/classes/admin/widgets/class-badge-streak-maintenance.php +++ b/classes/admin/widgets/class-badge-streak-maintenance.php @@ -25,4 +25,22 @@ final class Badge_Streak_Maintenance extends Badge_Streak { * @var bool */ protected $force_last_column = true; + + /** + * Enqueue scripts for this widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/streak-badges.asset.php'; + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/streak-badges', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/streak-badges.js', + $asset['dependencies'], + $asset['version'], + true + ); + } } diff --git a/classes/class-base.php b/classes/class-base.php index cd497edf92..26aa63d532 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -21,6 +21,7 @@ * @method \Progress_Planner\Rest\Content_Activity get_rest__content_activity() * @method \Progress_Planner\Rest\Monthly_Badges get_rest__monthly_badges() * @method \Progress_Planner\Rest\Content_Badges get_rest__content_badges() + * @method \Progress_Planner\Rest\Streak_Badges get_rest__streak_badges() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -127,6 +128,7 @@ public function init() { $this->get_rest__content_activity(); $this->get_rest__monthly_badges(); $this->get_rest__content_badges(); + $this->get_rest__streak_badges(); // Onboarding. $this->get_utils__onboard(); diff --git a/classes/rest/class-streak-badges.php b/classes/rest/class-streak-badges.php new file mode 100644 index 0000000000..537766e92a --- /dev/null +++ b/classes/rest/class-streak-badges.php @@ -0,0 +1,93 @@ + 'GET', + 'callback' => [ $this, 'get_streak_badges' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + } + + /** + * Check if the current user has permission to access this endpoint. + * + * @return bool + */ + public function check_permissions() { + return \current_user_can( 'edit_posts' ); + } + + /** + * Get streak badges data. + * + * @param \WP_REST_Request $request The REST request object. + * + * @return \WP_REST_Response + */ + public function get_streak_badges( $request ) { + $widget = \progress_planner()->get_admin__widgets__badge_streak_maintenance(); + + // Get the current badge in progress. + $current_badge_obj = $widget->get_details( 'maintenance' ); + $current_badge_data = null; + + if ( $current_badge_obj ) { + $progress = $current_badge_obj->get_progress(); + $current_badge_data = [ + 'id' => $current_badge_obj->get_id(), + 'name' => $current_badge_obj->get_name(), + 'background' => $current_badge_obj->get_background(), + 'progress' => $progress['progress'], + 'remaining' => $progress['remaining'], + ]; + } + + // Get all maintenance badges. + $all_badges = \progress_planner()->get_badges()->get_badges( 'maintenance' ); + $all_badges_data = []; + + foreach ( $all_badges as $badge ) { + $progress = $badge->get_progress(); + $all_badges_data[] = [ + 'id' => $badge->get_id(), + 'name' => $badge->get_name(), + 'progress' => $progress['progress'], + 'isComplete' => 100 === (int) $progress['progress'], + ]; + } + + // Build response data. + $response_data = [ + 'currentBadge' => $current_badge_data, + 'allBadges' => $all_badges_data, + 'brandingId' => (int) \progress_planner()->get_ui__branding()->get_branding_id(), + 'remoteServerUrl' => \progress_planner()->get_remote_server_root_url(), + 'placeholderUrl' => \progress_planner()->get_placeholder_svg( 200, 200 ), + ]; + + return new \WP_REST_Response( $response_data ); + } +} diff --git a/views/page-widgets/badge-streak-maintenance.php b/views/page-widgets/badge-streak-maintenance.php index ca60be3cec..7fbce70155 100644 --- a/views/page-widgets/badge-streak-maintenance.php +++ b/views/page-widgets/badge-streak-maintenance.php @@ -8,11 +8,6 @@ if ( ! \defined( 'ABSPATH' ) ) { exit; } - -$prpl_widget_details = \progress_planner()->get_admin__widgets__badge_streak_maintenance()->get_details( 'maintenance' ); -if ( ! $prpl_widget_details ) { - return; -} ?>

@@ -29,72 +24,7 @@ ?>

-

- -
- - - -
-

- - get_name() ) ); - ?> - - get_progress()['progress']; ?>% -

- -

- get_progress()['remaining'], - 'progress-planner' - ) - ), - \esc_html( \number_format_i18n( $prpl_widget_details->get_progress()['remaining'] ) ) - ); - ?> -

-
-
- -
- -
-
- get_badges()->get_badges( 'maintenance' ) as $prpl_badge ) : ?> - - -

get_name() ); ?>

-
- -
-
+
get_ui__popover()->the_popover( 'monthly-badges' )->render_button( diff --git a/webpack.config.js b/webpack.config.js index 854a4ff422..ab52ead8ef 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,7 @@ module.exports = { 'content-activity': './assets/src/content-activity.js', 'monthly-badges': './assets/src/monthly-badges.js', 'content-badges': './assets/src/content-badges.js', + 'streak-badges': './assets/src/streak-badges.js', }, output: { path: path.resolve( __dirname, 'build' ), From 11d7e64608d8edd3f1757a3fcd09c20ba5290f46 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 19:48:48 +0200 Subject: [PATCH 005/275] website-activity-score --- assets/src/activity-scores.js | 26 +++ assets/src/components/BarChart/index.js | 139 +++++++++++++++ assets/src/components/Gauge/index.js | 4 +- assets/src/widgets/ActivityScores/index.js | 158 ++++++++++++++++++ build/activity-scores.asset.php | 1 + build/activity-scores.js | 10 ++ build/content-badges.asset.php | 2 +- build/content-badges.js | 2 +- build/monthly-badges.asset.php | 2 +- build/monthly-badges.js | 2 +- build/streak-badges.asset.php | 2 +- build/streak-badges.js | 2 +- .../admin/widgets/class-activity-scores.php | 18 ++ classes/class-base.php | 2 + classes/rest/class-activity-scores.php | 94 +++++++++++ views/page-widgets/activity-scores.php | 106 +----------- webpack.config.js | 1 + 17 files changed, 460 insertions(+), 111 deletions(-) create mode 100644 assets/src/activity-scores.js create mode 100644 assets/src/components/BarChart/index.js create mode 100644 assets/src/widgets/ActivityScores/index.js create mode 100644 build/activity-scores.asset.php create mode 100644 build/activity-scores.js create mode 100644 classes/rest/class-activity-scores.php diff --git a/assets/src/activity-scores.js b/assets/src/activity-scores.js new file mode 100644 index 0000000000..92935b2db4 --- /dev/null +++ b/assets/src/activity-scores.js @@ -0,0 +1,26 @@ +/** + * Activity Scores Entry Point + * + * Mounts the ActivityScores React widget to the DOM. + */ + +import { createRoot } from '@wordpress/element'; +import ActivityScores from './widgets/ActivityScores'; + +/** + * Initialize the Activity Scores widget. + */ +function init() { + const container = document.getElementById( 'prpl-activity-scores-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/components/BarChart/index.js b/assets/src/components/BarChart/index.js new file mode 100644 index 0000000000..c7ea2812ad --- /dev/null +++ b/assets/src/components/BarChart/index.js @@ -0,0 +1,139 @@ +/** + * BarChart Component + * + * Displays a bar chart with labels. + */ + +import { useEffect, useRef } from '@wordpress/element'; + +/** + * BarChart component. + * + * @param {Object} props - Component props. + * @param {Array} props.data - Array of data points with label, score, and color. + * @return {JSX.Element} The BarChart component. + */ +export default function BarChart( { data = [] } ) { + const chartRef = useRef( null ); + + // Calculate how many labels to show (max 6) + const labelsDivider = data.length > 6 ? Math.floor( data.length / 6 ) : 1; + + /** + * Adjust label positioning when there are many items. + */ + useEffect( () => { + if ( ! chartRef.current ) { + return; + } + + const invisibleLabels = + chartRef.current.querySelectorAll( '.label.invisible' ); + + if ( invisibleLabels.length === 0 ) { + return; + } + + const labelContainers = + chartRef.current.querySelectorAll( '.label-container' ); + const chartBar = chartRef.current.querySelector( '.chart-bar' ); + + labelContainers.forEach( ( container ) => { + const labelElement = container.querySelector( '.label' ); + if ( ! labelElement ) { + return; + } + + const labelWidth = labelElement.offsetWidth; + labelElement.style.display = 'block'; + labelElement.style.width = '0'; + + const marginLeft = ( container.offsetWidth - labelWidth ) / 2; + if ( labelElement.classList.contains( 'visible' ) ) { + labelElement.style.marginLeft = `${ marginLeft }px`; + } + } ); + + // Reduce gap between items to avoid overflows + const firstLabel = chartRef.current.querySelector( '.label' ); + if ( firstLabel && chartBar ) { + const newGap = Math.max( firstLabel.offsetWidth / 4, 1 ); + chartBar.style.gap = `${ Math.floor( newGap ) }px`; + } + }, [ data ] ); + + const containerStyle = { + display: 'flex', + maxWidth: '600px', + height: '200px', + width: '100%', + alignItems: 'flex-end', + gap: '5px', + margin: '1rem 0', + }; + + const barContainerStyle = { + flex: 'auto', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', + height: '100%', + }; + + const labelContainerStyle = { + height: '1rem', + overflow: 'visible', + textAlign: 'center', + display: 'block', + width: '100%', + fontSize: '0.75em', + }; + + return ( +
+
+ { data.map( ( item, index ) => { + const barStyle = { + display: 'block', + width: '100%', + height: `${ item.score }%`, + background: item.color, + }; + + const isLabelVisible = index % labelsDivider === 0; + const labelClass = isLabelVisible + ? 'label visible' + : 'label invisible'; + const labelStyle = isLabelVisible + ? {} + : { visibility: 'hidden' }; + + return ( +
+
+ + + { item.label } + + +
+ ); + } ) } +
+
+ ); +} diff --git a/assets/src/components/Gauge/index.js b/assets/src/components/Gauge/index.js index fbeefc5ffa..172320cdf7 100644 --- a/assets/src/components/Gauge/index.js +++ b/assets/src/components/Gauge/index.js @@ -15,6 +15,7 @@ import { useMemo } from '@wordpress/element'; * @param {string} props.backgroundColor - Background color CSS variable. * @param {string} props.color - Primary progress color CSS variable. * @param {string} props.color2 - Secondary progress color CSS variable. + * @param {string} props.contentFontSize - Font size for the content inside the gauge. * @param {JSX.Element} props.children - Content to display in the gauge center. * @return {JSX.Element} The Gauge component. */ @@ -24,6 +25,7 @@ export default function Gauge( { backgroundColor = 'var(--prpl-background-monthly)', color = 'var(--prpl-color-monthly)', color2 = 'var(--prpl-color-monthly-2)', + contentFontSize = 'var(--prpl-font-size-6xl)', children, } ) { const maxDeg = '180deg'; @@ -92,7 +94,7 @@ export default function Gauge( { }; const contentStyle = { - fontSize: 'var(--prpl-font-size-6xl)', + fontSize: contentFontSize, bottom: '50%', display: 'block', fontWeight: 600, diff --git a/assets/src/widgets/ActivityScores/index.js b/assets/src/widgets/ActivityScores/index.js new file mode 100644 index 0000000000..48a16df90a --- /dev/null +++ b/assets/src/widgets/ActivityScores/index.js @@ -0,0 +1,158 @@ +/** + * ActivityScores Widget + * + * Displays the website activity score widget with gauge, bar chart, + * and personal record. + */ + +import { useState, useEffect } from '@wordpress/element'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import Gauge from '../../components/Gauge'; +import BarChart from '../../components/BarChart'; +import BigCounter from '../../components/BigCounter'; + +/** + * Get the streak message based on the current and max streak values. + * + * @param {number} maxStreak - The maximum streak value. + * @param {number} currentStreak - The current streak value. + * @return {string} The streak message. + */ +function getStreakMessage( maxStreak, currentStreak ) { + if ( maxStreak === 0 ) { + return __( + 'This is the start of your first streak! Add content to your site every week and set a personal record!', + 'progress-planner' + ); + } + + if ( maxStreak <= currentStreak ) { + return sprintf( + // translators: %s: number of weeks. + _n( + "Congratulations! You're on a streak! You've consistently maintained your website for the past %s week!", + "Congratulations! You're on a streak! You've consistently maintained your website for the past %s weeks!", + currentStreak, + 'progress-planner' + ), + currentStreak + ); + } + + if ( currentStreak >= 1 ) { + const weeksToGo = maxStreak - currentStreak; + return sprintf( + // translators: %1$s: number of weeks for current streak. %2$s: number of weeks for max streak. %3$s: weeks to go. + _n( + "Keep it up! You've consistently maintained your website for the past %1$s week. Your longest streak was %2$s weeks, %3$s more to go to break your record!", + "Keep it up! You've consistently maintained your website for the past %1$s weeks. Your longest streak was %2$s weeks, %3$s more to go to break your record!", + currentStreak, + 'progress-planner' + ), + currentStreak, + maxStreak, + weeksToGo + ); + } + + return sprintf( + // translators: %s: number of weeks for max streak. + _n( + 'Get back to your streak! Your longest streak was %s week. Keep working on those website maintenance tasks every week and break your record!', + 'Get back to your streak! Your longest streak was %s weeks. Keep working on those website maintenance tasks every week and break your record!', + maxStreak, + 'progress-planner' + ), + maxStreak + ); +} + +/** + * ActivityScores component. + * + * @return {JSX.Element} The ActivityScores component. + */ +export default function ActivityScores() { + const [ isLoading, setIsLoading ] = useState( true ); + const [ error, setError ] = useState( null ); + const [ data, setData ] = useState( null ); + + /** + * Fetch activity scores data from REST API. + */ + useEffect( () => { + const fetchData = async () => { + try { + const response = await apiFetch( { + path: '/progress-planner/v1/activity-scores', + } ); + + setData( response ); + setIsLoading( false ); + } catch ( err ) { + setError( err.message || 'Failed to load activity data' ); + setIsLoading( false ); + } + }; + + fetchData(); + }, [] ); + + if ( isLoading ) { + return

{ __( 'Loading…', 'progress-planner' ) }

; + } + + if ( error ) { + return

{ error }

; + } + + if ( ! data ) { + return

{ __( 'No data available.', 'progress-planner' ) }

; + } + + const { score, gaugeColor, chartData, personalRecord } = data; + const streakMessage = getStreakMessage( + personalRecord.maxStreak, + personalRecord.currentStreak + ); + + return ( + <> +
+ + { score } + +
+ +
+ +

+ { __( + 'Check out your website activity in the past months:', + 'progress-planner' + ) } +

+
+ +
+ +
+ + + +
{ streakMessage }
+ + ); +} diff --git a/build/activity-scores.asset.php b/build/activity-scores.asset.php new file mode 100644 index 0000000000..f5082c5c90 --- /dev/null +++ b/build/activity-scores.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '22b772306ea72655f0fd'); diff --git a/build/activity-scores.js b/build/activity-scores.js new file mode 100644 index 0000000000..7ab75c2722 --- /dev/null +++ b/build/activity-scores.js @@ -0,0 +1,10 @@ +(()=>{"use strict";var e={n:r=>{var t=r&&r.__esModule?()=>r.default:()=>r;return e.d(t,{a:t}),t},d:(r,t)=>{for(var n in t)e.o(t,n)&&!e.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:t[n]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,t=window.wp.i18n,n=window.wp.apiFetch;var a=e.n(n);const l=window.ReactJSXRuntime;function s({value:e=0,max:t=10,backgroundColor:n="var(--prpl-background-monthly)",color:a="var(--prpl-color-monthly)",color2:s="var(--prpl-color-monthly-2)",contentFontSize:o="var(--prpl-font-size-6xl)",children:i}){const c="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:n,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${n} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=t>0?e/t:0;let n;return r<=.5?n=`${a} calc(${c} * ${r})`:(n=`${a} calc(${c} * 0.5)`,n+=`, ${s} calc(${c} * ${r})`),n+=`, var(--prpl-color-gauge-remain) calc(${c} * ${r}) ${c}`,n},[e,t,a,s])}, transparent ${c})`,textAlign:"center"},u={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...u,left:0},h={...u,right:0},b={fontSize:o,bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2};return(0,l.jsx)("div",{className:"prpl-gauge",style:p,children:(0,l.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,l.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,l.jsx)("span",{className:"prpl-gauge__content",style:b,children:(0,l.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:i})}),(0,l.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:h,children:t})]})})}function o({data:e=[]}){const t=(0,r.useRef)(null),n=e.length>6?Math.floor(e.length/6):1;(0,r.useEffect)(()=>{if(!t.current)return;if(0===t.current.querySelectorAll(".label.invisible").length)return;const e=t.current.querySelectorAll(".label-container"),r=t.current.querySelector(".chart-bar");e.forEach(e=>{const r=e.querySelector(".label");if(!r)return;const t=r.offsetWidth;r.style.display="block",r.style.width="0";const n=(e.offsetWidth-t)/2;r.classList.contains("visible")&&(r.style.marginLeft=`${n}px`)});const n=t.current.querySelector(".label");if(n&&r){const e=Math.max(n.offsetWidth/4,1);r.style.gap=`${Math.floor(e)}px`}},[e]);const a={flex:"auto",display:"flex",flexDirection:"column",justifyContent:"flex-end",height:"100%"},s={height:"1rem",overflow:"visible",textAlign:"center",display:"block",width:"100%",fontSize:"0.75em"};return(0,l.jsx)("div",{className:"prpl-bar-chart",ref:t,children:(0,l.jsx)("div",{className:"chart-bar",style:{display:"flex",maxWidth:"600px",height:"200px",width:"100%",alignItems:"flex-end",gap:"5px",margin:"1rem 0"},children:e.map((e,r)=>{const t={display:"block",width:"100%",height:`${e.score}%`,background:e.color},o=r%n===0,i=o?"label visible":"label invisible",c=o?{}:{visibility:"hidden"};return(0,l.jsxs)("div",{className:"prpl-bar-chart__bar-container",style:a,children:[(0,l.jsx)("div",{className:"prpl-bar-chart__bar",style:t,title:`${e.label} - ${e.score}%`}),(0,l.jsx)("span",{className:"label-container",style:s,children:(0,l.jsx)("span",{className:i,style:c,children:e.label})})]},r)})})})}function i({number:e,label:t,backgroundColor:n="var(--prpl-background-content)"}){const a=(0,r.useRef)(null),s=(0,r.useRef)(null),o=(0,r.useCallback)(()=>{const e=s.current,r=a.current;if(!e||!r)return;e.style.fontSize="100%",e.style.width="max-content";const t=r.clientWidth;let n=100;for(;e.clientWidth>t&&n>80;)n-=1,e.style.fontSize=n+"%";n<=80&&(e.style.fontSize="80%",e.style.width="100%")},[]);(0,r.useEffect)(()=>(o(),window.addEventListener("resize",o),()=>{window.removeEventListener("resize",o)}),[o,t]);const i={backgroundColor:n,padding:"var(--prpl-padding)",borderRadius:"var(--prpl-border-radius-big)",display:"flex",flexDirection:"column",alignItems:"center",textAlign:"center",alignContent:"center",justifyContent:"center",height:"calc(var(--prpl-font-size-5xl) + var(--prpl-font-size-2xl) + var(--prpl-padding) * 2)",marginBottom:"var(--prpl-padding)"};return(0,l.jsxs)("div",{className:"prpl-big-counter",style:i,children:[(0,l.jsx)("div",{className:"prpl-big-counter__width-reference",ref:a,style:{width:"100%"}}),(0,l.jsx)("span",{className:"prpl-big-counter__number",style:{fontSize:"var(--prpl-font-size-5xl)",lineHeight:1,fontWeight:600},children:e}),(0,l.jsx)("span",{className:"prpl-big-counter__label-wrapper",style:{fontSize:"var(--prpl-font-size-2xl)"},children:(0,l.jsx)("span",{className:"prpl-big-counter__label",ref:s,style:{fontSize:"100%",display:"inline-block",width:"max-content"},children:t})})]})}function c(){const[e,n]=(0,r.useState)(!0),[c,p]=(0,r.useState)(null),[d,u]=(0,r.useState)(null);if((0,r.useEffect)(()=>{(async()=>{try{const e=await a()({path:"/progress-planner/v1/activity-scores"});u(e),n(!1)}catch(e){p(e.message||"Failed to load activity data"),n(!1)}})()},[]),e)return(0,l.jsx)("p",{children:(0,t.__)("Loading…","progress-planner")});if(c)return(0,l.jsx)("p",{children:c});if(!d)return(0,l.jsx)("p",{children:(0,t.__)("No data available.","progress-planner")});const{score:g,gaugeColor:h,chartData:b,personalRecord:f}=d,y=function(e,r){if(0===e)return(0,t.__)("This is the start of your first streak! Add content to your site every week and set a personal record!","progress-planner");if(e<=r)return(0,t.sprintf)( +// translators: %s: number of weeks. +// translators: %s: number of weeks. +(0,t._n)("Congratulations! You're on a streak! You've consistently maintained your website for the past %s week!","Congratulations! You're on a streak! You've consistently maintained your website for the past %s weeks!",r,"progress-planner"),r);if(r>=1){const n=e-r;return(0,t.sprintf)( +// translators: %1$s: number of weeks for current streak. %2$s: number of weeks for max streak. %3$s: weeks to go. +// translators: %1$s: number of weeks for current streak. %2$s: number of weeks for max streak. %3$s: weeks to go. +(0,t._n)("Keep it up! You've consistently maintained your website for the past %1$s week. Your longest streak was %2$s weeks, %3$s more to go to break your record!","Keep it up! You've consistently maintained your website for the past %1$s weeks. Your longest streak was %2$s weeks, %3$s more to go to break your record!",r,"progress-planner"),r,e,n)}return(0,t.sprintf)( +// translators: %s: number of weeks for max streak. +// translators: %s: number of weeks for max streak. +(0,t._n)("Get back to your streak! Your longest streak was %s week. Keep working on those website maintenance tasks every week and break your record!","Get back to your streak! Your longest streak was %s weeks. Keep working on those website maintenance tasks every week and break your record!",e,"progress-planner"),e)}(f.maxStreak,f.currentStreak);return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("div",{style:{"--background":"var(--prpl-background-monthly)"},children:(0,l.jsx)(s,{value:g,max:100,backgroundColor:"var(--prpl-background-activity)",color:h,color2:h,contentFontSize:"var(--prpl-font-size-6xl)",children:g})}),(0,l.jsx)("hr",{}),(0,l.jsx)("p",{children:(0,t.__)("Check out your website activity in the past months:","progress-planner")}),(0,l.jsx)("div",{className:"prpl-graph-wrapper",children:(0,l.jsx)(o,{data:b})}),(0,l.jsx)("hr",{}),(0,l.jsx)(i,{number:String(f.maxStreak),label:(0,t.__)("personal record","progress-planner"),backgroundColor:"var(--prpl-background-activity)"}),(0,l.jsx)("div",{className:"prpl-widget-content",children:y})]})}function p(){const e=document.getElementById("prpl-activity-scores-root");e&&(0,r.createRoot)(e).render((0,l.jsx)(c,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/build/content-badges.asset.php b/build/content-badges.asset.php index bee00bec16..52c0b19725 100644 --- a/build/content-badges.asset.php +++ b/build/content-badges.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'a7ca6917267c6cd92020'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '512c86b4af39c61c9f7e'); diff --git a/build/content-badges.js b/build/content-badges.js index be56034354..c29afbbc03 100644 --- a/build/content-badges.js +++ b/build/content-badges.js @@ -1,3 +1,3 @@ -(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var n in a)e.o(a,n)&&!e.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:a[n]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,n=window.wp.apiFetch;var l=e.n(n);const t=window.ReactJSXRuntime;function o({value:e=0,max:a=10,backgroundColor:n="var(--prpl-background-monthly)",color:l="var(--prpl-color-monthly)",color2:o="var(--prpl-color-monthly-2)",children:s}){const d="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:n,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},i={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${n} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let n;return r<=.5?n=`${l} calc(${d} * ${r})`:(n=`${l} calc(${d} * 0.5)`,n+=`, ${o} calc(${d} * ${r})`),n+=`, var(--prpl-color-gauge-remain) calc(${d} * ${r}) ${d}`,n},[e,a,l,o])}, transparent ${d})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,t.jsx)("div",{className:"prpl-gauge",style:p,children:(0,t.jsxs)("div",{className:"prpl-gauge__ring",style:i,children:[(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,t.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,t.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:s})}),(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function s({badgeId:e,badgeName:a,brandingId:n=0,remoteServerUrl:l,placeholderUrl:o,isComplete:s=!0}){const[d,p]=(0,r.useState)(!1),i=(0,r.useCallback)(()=>{let r=`${l}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return n&&(r+=`&branding_id=${n}`),r},[e,n,l]),c=(0,r.useCallback)(()=>{!d&&o&&p(!0)},[d,o]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...s?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,t.jsx)("img",{className:"prpl-badge__image",src:d?o:i(),alt:a||"Badge",onError:c,style:g})}function d(){const[e,n]=(0,r.useState)(!0),[d,p]=(0,r.useState)(null),[i,c]=(0,r.useState)(null),[g,m]=(0,r.useState)([]),[u,b]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""});return(0,r.useEffect)(()=>{(async()=>{try{const e=await l()({path:"/progress-planner/v1/content-badges"});c(e.currentBadge),m(e.allBadges||[]),b({brandingId:e.brandingId,remoteServerUrl:e.remoteServerUrl,placeholderUrl:e.placeholderUrl}),n(!1)}catch(e){p(e.message||"Failed to load badge data"),n(!1)}})()},[]),e?(0,t.jsx)("p",{children:(0,a.__)("Loading…","progress-planner")}):d?(0,t.jsx)("p",{children:d}):i?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{children:(0,a.__)("The more you work on meaninful content, the sooner you unlock new badges.","progress-planner")}),(0,t.jsxs)("div",{className:"prpl-latest-badges-wrapper",children:[(0,t.jsx)(o,{value:i.progress,max:100,backgroundColor:i.background||"var(--prpl-background-content-badge)",color:"var(--prpl-color-monthly)",color2:"var(--prpl-color-monthly-2)",children:(0,t.jsx)(s,{badgeId:i.id,badgeName:i.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:!0})}),(0,t.jsxs)("div",{className:"prpl-badge-content-wrapper",children:[(0,t.jsxs)("p",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem",marginBottom:0},children:[(0,t.jsx)("span",{children:(0,a.sprintf)(/* translators: %s: The badge name. */ /* translators: %s: The badge name. */ +(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var n in a)e.o(a,n)&&!e.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:a[n]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,n=window.wp.apiFetch;var l=e.n(n);const t=window.ReactJSXRuntime;function o({value:e=0,max:a=10,backgroundColor:n="var(--prpl-background-monthly)",color:l="var(--prpl-color-monthly)",color2:o="var(--prpl-color-monthly-2)",contentFontSize:s="var(--prpl-font-size-6xl)",children:d}){const p="180deg",i={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:n,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},c={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${n} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let n;return r<=.5?n=`${l} calc(${p} * ${r})`:(n=`${l} calc(${p} * 0.5)`,n+=`, ${o} calc(${p} * ${r})`),n+=`, var(--prpl-color-gauge-remain) calc(${p} * ${r}) ${p}`,n},[e,a,l,o])}, transparent ${p})`,textAlign:"center"},g={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},m={...g,left:0},u={...g,right:0},b={fontSize:s,bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2};return(0,t.jsx)("div",{className:"prpl-gauge",style:i,children:(0,t.jsxs)("div",{className:"prpl-gauge__ring",style:c,children:[(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:m,children:"0"}),(0,t.jsx)("span",{className:"prpl-gauge__content",style:b,children:(0,t.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:d})}),(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:u,children:a})]})})}function s({badgeId:e,badgeName:a,brandingId:n=0,remoteServerUrl:l,placeholderUrl:o,isComplete:s=!0}){const[d,p]=(0,r.useState)(!1),i=(0,r.useCallback)(()=>{let r=`${l}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return n&&(r+=`&branding_id=${n}`),r},[e,n,l]),c=(0,r.useCallback)(()=>{!d&&o&&p(!0)},[d,o]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...s?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,t.jsx)("img",{className:"prpl-badge__image",src:d?o:i(),alt:a||"Badge",onError:c,style:g})}function d(){const[e,n]=(0,r.useState)(!0),[d,p]=(0,r.useState)(null),[i,c]=(0,r.useState)(null),[g,m]=(0,r.useState)([]),[u,b]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""});return(0,r.useEffect)(()=>{(async()=>{try{const e=await l()({path:"/progress-planner/v1/content-badges"});c(e.currentBadge),m(e.allBadges||[]),b({brandingId:e.brandingId,remoteServerUrl:e.remoteServerUrl,placeholderUrl:e.placeholderUrl}),n(!1)}catch(e){p(e.message||"Failed to load badge data"),n(!1)}})()},[]),e?(0,t.jsx)("p",{children:(0,a.__)("Loading…","progress-planner")}):d?(0,t.jsx)("p",{children:d}):i?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{children:(0,a.__)("The more you work on meaninful content, the sooner you unlock new badges.","progress-planner")}),(0,t.jsxs)("div",{className:"prpl-latest-badges-wrapper",children:[(0,t.jsx)(o,{value:i.progress,max:100,backgroundColor:i.background||"var(--prpl-background-content-badge)",color:"var(--prpl-color-monthly)",color2:"var(--prpl-color-monthly-2)",children:(0,t.jsx)(s,{badgeId:i.id,badgeName:i.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:!0})}),(0,t.jsxs)("div",{className:"prpl-badge-content-wrapper",children:[(0,t.jsxs)("p",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem",marginBottom:0},children:[(0,t.jsx)("span",{children:(0,a.sprintf)(/* translators: %s: The badge name. */ /* translators: %s: The badge name. */ (0,a.__)("Progress %s","progress-planner"),i.name)}),(0,t.jsxs)("span",{style:{fontWeight:600,fontSize:"var(--prpl-font-size-3xl)"},children:[i.progress,"%"]})]}),(0,t.jsx)("p",{style:{marginTop:0},children:(0,a.sprintf)(/* translators: %s: The remaining number of posts or pages to write. */ /* translators: %s: The remaining number of posts or pages to write. */ (0,a._n)("Write %s new post or page and earn your next badge!","Write %s new posts or pages and earn your next badge!",i.remaining,"progress-planner"),i.remaining)})]})]}),(0,t.jsx)("hr",{}),(0,t.jsx)("div",{className:"prpl-badges-container-achievements",children:(0,t.jsx)("div",{className:"progress-wrapper badge-group-content",children:g.map(e=>(0,t.jsxs)("span",{className:"prpl-badge","data-value":e.progress,children:[(0,t.jsx)(s,{badgeId:e.id,badgeName:e.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:e.isComplete}),(0,t.jsx)("p",{children:e.name})]},e.id))})})]}):(0,t.jsx)("p",{children:(0,a.__)("No badge data available.","progress-planner")})}function p(){const e=document.getElementById("prpl-content-badges-root");e&&(0,r.createRoot)(e).render((0,t.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/build/monthly-badges.asset.php b/build/monthly-badges.asset.php index 63cf40e6ca..9a5bf44d8d 100644 --- a/build/monthly-badges.asset.php +++ b/build/monthly-badges.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '66ed1ef45f1197b5ec9e'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'd159a3f9407a0fa3c0a3'); diff --git a/build/monthly-badges.js b/build/monthly-badges.js index 7f35ca3878..0a5ab9c46c 100644 --- a/build/monthly-badges.js +++ b/build/monthly-badges.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var t in a)e.o(a,t)&&!e.o(r,t)&&Object.defineProperty(r,t,{enumerable:!0,get:a[t]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,t=window.wp.apiFetch;var l=e.n(t);const n=window.ReactJSXRuntime;function o({value:e=0,max:a=10,backgroundColor:t="var(--prpl-background-monthly)",color:l="var(--prpl-color-monthly)",color2:o="var(--prpl-color-monthly-2)",children:s}){const i="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:t,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${t} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let t;return r<=.5?t=`${l} calc(${i} * ${r})`:(t=`${l} calc(${i} * 0.5)`,t+=`, ${o} calc(${i} * ${r})`),t+=`, var(--prpl-color-gauge-remain) calc(${i} * ${r}) ${i}`,t},[e,a,l,o])}, transparent ${i})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,n.jsx)("div",{className:"prpl-gauge",style:p,children:(0,n.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,n.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,n.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:s})}),(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function s({badgeId:e,badgeName:a,brandingId:t=0,remoteServerUrl:l,placeholderUrl:o,isComplete:s=!0}){const[i,p]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${l}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return t&&(r+=`&branding_id=${t}`),r},[e,t,l]),c=(0,r.useCallback)(()=>{!i&&o&&p(!0)},[i,o]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...s?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,n.jsx)("img",{className:"prpl-badge__image",src:i?o:d(),alt:a||"Badge",onError:c,style:g})}function i({badgeId:e,badgeName:a,points:t=0,maxPoints:l=10,brandingId:o=0,remoteServerUrl:i,placeholderUrl:p}){const d=(0,r.useMemo)(()=>0===l?0:t/l*100,[t,l]),c={height:"100%",backgroundColor:"var(--prpl-color-monthly)",borderRadius:"0.5rem",transition:"width 0.4s ease",width:`${d}%`},g={display:"flex",width:"7.5rem",height:"auto",position:"absolute",top:"-2.5rem",transition:"left 0.4s ease",left:`calc(${d}% - 3.75rem)`},m=t>=l,b="prpl-badge-progress-bar"+(m?" prpl-badge-progress-bar--complete":"");return(0,n.jsx)("div",{className:b,style:{padding:"1rem 0"},children:(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__bar",style:{width:"100%",height:"1rem",backgroundColor:"var(--prpl-color-gauge-remain)",borderRadius:"0.5rem",position:"relative"},children:[(0,n.jsx)("div",{className:"prpl-badge-progress-bar__progress",style:c}),(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__badge-wrapper",style:g,children:[(0,n.jsx)(s,{badgeId:e,badgeName:a,brandingId:o,remoteServerUrl:i,placeholderUrl:p}),!m&&(0,n.jsx)("span",{className:"prpl-badge-progress-bar__alert",style:{content:'"!"',display:"flex",alignItems:"center",justifyContent:"center",width:"20px",height:"20px",backgroundColor:"var(--prpl-color-alert-error)",border:"2px solid #fff",borderRadius:"50%",position:"absolute",top:"10%",right:"25%",color:"#fff",fontSize:"12px",fontWeight:"bold"},children:"!"})]})]})})}function p({points:e,label:r=(0,a.__)("Progress monthly badge","progress-planner"),showUnit:t=!0}){return(0,n.jsxs)("div",{className:"prpl-monthly-badges__points-counter",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,n.jsx)("span",{className:"prpl-monthly-badges__points-label",children:r}),(0,n.jsxs)("span",{className:"prpl-monthly-badges__points-number",style:{fontSize:"var(--prpl-font-size-3xl)",fontWeight:600},children:[e,t&&"pt"]})]})}function d(){const[e,t]=(0,r.useState)(!0),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)(0),[b,u]=(0,r.useState)(10),[h,v]=(0,r.useState)(null),[y,x]=(0,r.useState)([]),[f,_]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""}),j=g>=b,w=(0,r.useCallback)(e=>{let r=e;m(e=>{const a=Math.min(e+r,b);return r-=a-e,a}),r>0&&y.length>0&&x(e=>e.map(e=>{if(r<=0)return e;const a=e.maxPoints||10,t=Math.min(e.points+r,a);return r-=t-e.points,{...e,points:t}}))},[b,y.length]);(0,r.useEffect)(()=>{(async()=>{try{const e=await l()({path:"/progress-planner/v1/monthly-badges"});m(e.score?.score||0),u(e.score?.target||10),v(e.currentBadge||null),x(e.previousIncompleteBadges||[]),_({brandingId:e.brandingId||0,remoteServerUrl:e.remoteServerUrl||"",placeholderUrl:e.placeholderUrl||""}),t(!1)}catch(e){c(e.message||(0,a.__)("Failed to load data","progress-planner")),t(!1)}})()},[]),(0,r.useEffect)(()=>{const e=e=>{const{points:r}=e.detail||{};r&&"number"==typeof r&&w(r)};return document.addEventListener("prpl-task-completed",e),()=>{document.removeEventListener("prpl-task-completed",e)}},[w]);return e?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--loading",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-text)"},children:(0,a.__)("Loading…","progress-planner")}):d?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--error",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-alert-error)"},children:d}):(0,n.jsxs)("div",{className:"prpl-monthly-badges",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[y.length>0&&(0,n.jsx)("div",{className:"prpl-monthly-badges__previous-badges",style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:y.map(e=>(0,n.jsx)(i,{badgeId:e.id,badgeName:e.name,points:e.points,maxPoints:e.maxPoints||10,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl},e.id))}),(0,n.jsx)(o,{value:g,max:b,children:h&&(0,n.jsx)(s,{badgeId:h.id,badgeName:h.name,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl,isComplete:j})}),(0,n.jsx)(p,{points:g,label:(0,a.__)("Progress monthly badge","progress-planner")})]})}function c(){const e=document.getElementById("prpl-monthly-badges-root");e&&(0,r.createRoot)(e).render((0,n.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()})(); \ No newline at end of file +(()=>{"use strict";var e={n:r=>{var t=r&&r.__esModule?()=>r.default:()=>r;return e.d(t,{a:t}),t},d:(r,t)=>{for(var a in t)e.o(t,a)&&!e.o(r,a)&&Object.defineProperty(r,a,{enumerable:!0,get:t[a]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,t=window.wp.i18n,a=window.wp.apiFetch;var l=e.n(a);const n=window.ReactJSXRuntime;function o({value:e=0,max:t=10,backgroundColor:a="var(--prpl-background-monthly)",color:l="var(--prpl-color-monthly)",color2:o="var(--prpl-color-monthly-2)",contentFontSize:s="var(--prpl-font-size-6xl)",children:i}){const p="180deg",d={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:a,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},c={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${a} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=t>0?e/t:0;let a;return r<=.5?a=`${l} calc(${p} * ${r})`:(a=`${l} calc(${p} * 0.5)`,a+=`, ${o} calc(${p} * ${r})`),a+=`, var(--prpl-color-gauge-remain) calc(${p} * ${r}) ${p}`,a},[e,t,l,o])}, transparent ${p})`,textAlign:"center"},g={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},m={...g,left:0},b={...g,right:0},u={fontSize:s,bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2};return(0,n.jsx)("div",{className:"prpl-gauge",style:d,children:(0,n.jsxs)("div",{className:"prpl-gauge__ring",style:c,children:[(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:m,children:"0"}),(0,n.jsx)("span",{className:"prpl-gauge__content",style:u,children:(0,n.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:i})}),(0,n.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:b,children:t})]})})}function s({badgeId:e,badgeName:t,brandingId:a=0,remoteServerUrl:l,placeholderUrl:o,isComplete:s=!0}){const[i,p]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${l}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return a&&(r+=`&branding_id=${a}`),r},[e,a,l]),c=(0,r.useCallback)(()=>{!i&&o&&p(!0)},[i,o]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...s?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,n.jsx)("img",{className:"prpl-badge__image",src:i?o:d(),alt:t||"Badge",onError:c,style:g})}function i({badgeId:e,badgeName:t,points:a=0,maxPoints:l=10,brandingId:o=0,remoteServerUrl:i,placeholderUrl:p}){const d=(0,r.useMemo)(()=>0===l?0:a/l*100,[a,l]),c={height:"100%",backgroundColor:"var(--prpl-color-monthly)",borderRadius:"0.5rem",transition:"width 0.4s ease",width:`${d}%`},g={display:"flex",width:"7.5rem",height:"auto",position:"absolute",top:"-2.5rem",transition:"left 0.4s ease",left:`calc(${d}% - 3.75rem)`},m=a>=l,b="prpl-badge-progress-bar"+(m?" prpl-badge-progress-bar--complete":"");return(0,n.jsx)("div",{className:b,style:{padding:"1rem 0"},children:(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__bar",style:{width:"100%",height:"1rem",backgroundColor:"var(--prpl-color-gauge-remain)",borderRadius:"0.5rem",position:"relative"},children:[(0,n.jsx)("div",{className:"prpl-badge-progress-bar__progress",style:c}),(0,n.jsxs)("div",{className:"prpl-badge-progress-bar__badge-wrapper",style:g,children:[(0,n.jsx)(s,{badgeId:e,badgeName:t,brandingId:o,remoteServerUrl:i,placeholderUrl:p}),!m&&(0,n.jsx)("span",{className:"prpl-badge-progress-bar__alert",style:{content:'"!"',display:"flex",alignItems:"center",justifyContent:"center",width:"20px",height:"20px",backgroundColor:"var(--prpl-color-alert-error)",border:"2px solid #fff",borderRadius:"50%",position:"absolute",top:"10%",right:"25%",color:"#fff",fontSize:"12px",fontWeight:"bold"},children:"!"})]})]})})}function p({points:e,label:r=(0,t.__)("Progress monthly badge","progress-planner"),showUnit:a=!0}){return(0,n.jsxs)("div",{className:"prpl-monthly-badges__points-counter",style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,n.jsx)("span",{className:"prpl-monthly-badges__points-label",children:r}),(0,n.jsxs)("span",{className:"prpl-monthly-badges__points-number",style:{fontSize:"var(--prpl-font-size-3xl)",fontWeight:600},children:[e,a&&"pt"]})]})}function d(){const[e,a]=(0,r.useState)(!0),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)(0),[b,u]=(0,r.useState)(10),[h,v]=(0,r.useState)(null),[y,x]=(0,r.useState)([]),[f,_]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""}),j=g>=b,w=(0,r.useCallback)(e=>{let r=e;m(e=>{const t=Math.min(e+r,b);return r-=t-e,t}),r>0&&y.length>0&&x(e=>e.map(e=>{if(r<=0)return e;const t=e.maxPoints||10,a=Math.min(e.points+r,t);return r-=a-e.points,{...e,points:a}}))},[b,y.length]);(0,r.useEffect)(()=>{(async()=>{try{const e=await l()({path:"/progress-planner/v1/monthly-badges"});m(e.score?.score||0),u(e.score?.target||10),v(e.currentBadge||null),x(e.previousIncompleteBadges||[]),_({brandingId:e.brandingId||0,remoteServerUrl:e.remoteServerUrl||"",placeholderUrl:e.placeholderUrl||""}),a(!1)}catch(e){c(e.message||(0,t.__)("Failed to load data","progress-planner")),a(!1)}})()},[]),(0,r.useEffect)(()=>{const e=e=>{const{points:r}=e.detail||{};r&&"number"==typeof r&&w(r)};return document.addEventListener("prpl-task-completed",e),()=>{document.removeEventListener("prpl-task-completed",e)}},[w]);return e?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--loading",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-text)"},children:(0,t.__)("Loading…","progress-planner")}):d?(0,n.jsx)("div",{className:"prpl-monthly-badges prpl-monthly-badges--error",style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"200px",color:"var(--prpl-color-alert-error)"},children:d}):(0,n.jsxs)("div",{className:"prpl-monthly-badges",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[y.length>0&&(0,n.jsx)("div",{className:"prpl-monthly-badges__previous-badges",style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:y.map(e=>(0,n.jsx)(i,{badgeId:e.id,badgeName:e.name,points:e.points,maxPoints:e.maxPoints||10,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl},e.id))}),(0,n.jsx)(o,{value:g,max:b,children:h&&(0,n.jsx)(s,{badgeId:h.id,badgeName:h.name,brandingId:f.brandingId,remoteServerUrl:f.remoteServerUrl,placeholderUrl:f.placeholderUrl,isComplete:j})}),(0,n.jsx)(p,{points:g,label:(0,t.__)("Progress monthly badge","progress-planner")})]})}function c(){const e=document.getElementById("prpl-monthly-badges-root");e&&(0,r.createRoot)(e).render((0,n.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()})(); \ No newline at end of file diff --git a/build/streak-badges.asset.php b/build/streak-badges.asset.php index 798ecebb1e..84f7454172 100644 --- a/build/streak-badges.asset.php +++ b/build/streak-badges.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '01c574ec2bb6e1576b11'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'ce284ea7cb857d442ad2'); diff --git a/build/streak-badges.js b/build/streak-badges.js index a7aaa71a1f..60bcdbe5f2 100644 --- a/build/streak-badges.js +++ b/build/streak-badges.js @@ -1,3 +1,3 @@ -(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var l in a)e.o(a,l)&&!e.o(r,l)&&Object.defineProperty(r,l,{enumerable:!0,get:a[l]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,l=window.wp.apiFetch;var n=e.n(l);const t=window.ReactJSXRuntime;function s({value:e=0,max:a=10,backgroundColor:l="var(--prpl-background-monthly)",color:n="var(--prpl-color-monthly)",color2:s="var(--prpl-color-monthly-2)",children:o}){const i="180deg",p={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:l,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},d={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${l} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let l;return r<=.5?l=`${n} calc(${i} * ${r})`:(l=`${n} calc(${i} * 0.5)`,l+=`, ${s} calc(${i} * ${r})`),l+=`, var(--prpl-color-gauge-remain) calc(${i} * ${r}) ${i}`,l},[e,a,n,s])}, transparent ${i})`,textAlign:"center"},c={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},g={...c,left:0},m={...c,right:0};return(0,t.jsx)("div",{className:"prpl-gauge",style:p,children:(0,t.jsxs)("div",{className:"prpl-gauge__ring",style:d,children:[(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:g,children:"0"}),(0,t.jsx)("span",{className:"prpl-gauge__content",style:{fontSize:"var(--prpl-font-size-6xl)",bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2},children:(0,t.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:o})}),(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:m,children:a})]})})}function o({badgeId:e,badgeName:a,brandingId:l=0,remoteServerUrl:n,placeholderUrl:s,isComplete:o=!0}){const[i,p]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${n}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return l&&(r+=`&branding_id=${l}`),r},[e,l,n]),c=(0,r.useCallback)(()=>{!i&&s&&p(!0)},[i,s]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...o?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,t.jsx)("img",{className:"prpl-badge__image",src:i?s:d(),alt:a||"Badge",onError:c,style:g})}function i(){const[e,l]=(0,r.useState)(!0),[i,p]=(0,r.useState)(null),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)([]),[u,b]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""});return(0,r.useEffect)(()=>{(async()=>{try{const e=await n()({path:"/progress-planner/v1/streak-badges"});c(e.currentBadge),m(e.allBadges||[]),b({brandingId:e.brandingId,remoteServerUrl:e.remoteServerUrl,placeholderUrl:e.placeholderUrl}),l(!1)}catch(e){p(e.message||"Failed to load badge data"),l(!1)}})()},[]),e?(0,t.jsx)("p",{children:(0,a.__)("Loading…","progress-planner")}):i?(0,t.jsx)("p",{children:i}):d?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{children:(0,a.__)("Execute at least one website maintenance task every week.","progress-planner")}),(0,t.jsxs)("div",{className:"prpl-latest-badges-wrapper",children:[(0,t.jsx)(s,{value:d.progress,max:100,backgroundColor:d.background||"var(--prpl-background-streak)",color:"var(--prpl-color-monthly)",color2:"var(--prpl-color-monthly-2)",children:(0,t.jsx)(o,{badgeId:d.id,badgeName:d.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:!0})}),(0,t.jsxs)("div",{className:"prpl-badge-content-wrapper",children:[(0,t.jsxs)("p",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem",marginBottom:0},children:[(0,t.jsx)("span",{children:(0,a.sprintf)(/* translators: %s: The badge name. */ /* translators: %s: The badge name. */ +(()=>{"use strict";var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var l in a)e.o(a,l)&&!e.o(r,l)&&Object.defineProperty(r,l,{enumerable:!0,get:a[l]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,a=window.wp.i18n,l=window.wp.apiFetch;var n=e.n(l);const t=window.ReactJSXRuntime;function s({value:e=0,max:a=10,backgroundColor:l="var(--prpl-background-monthly)",color:n="var(--prpl-color-monthly)",color2:s="var(--prpl-color-monthly-2)",contentFontSize:o="var(--prpl-font-size-6xl)",children:i}){const p="180deg",d={padding:"var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)",background:l,borderRadius:"var(--prpl-border-radius-big)",aspectRatio:"2 / 1",overflow:"hidden",position:"relative",marginBottom:"var(--prpl-padding)"},c={width:"100%",aspectRatio:"1 / 1",borderRadius:"100%",position:"relative",background:`radial-gradient(${l} 0 57%, transparent 57% 100%), conic-gradient(from 270deg, ${(0,r.useMemo)(()=>{const r=a>0?e/a:0;let l;return r<=.5?l=`${n} calc(${p} * ${r})`:(l=`${n} calc(${p} * 0.5)`,l+=`, ${s} calc(${p} * ${r})`),l+=`, var(--prpl-color-gauge-remain) calc(${p} * ${r}) ${p}`,l},[e,a,n,s])}, transparent ${p})`,textAlign:"center"},g={fontSize:"var(--prpl-font-size-small)",position:"absolute",top:"50%",color:"var(--prpl-color-text)",width:"10%",textAlign:"center"},m={...g,left:0},u={...g,right:0},b={fontSize:o,bottom:"50%",display:"block",fontWeight:600,textAlign:"center",position:"absolute",color:"var(--prpl-color-text)",width:"100%",lineHeight:1.2};return(0,t.jsx)("div",{className:"prpl-gauge",style:d,children:(0,t.jsxs)("div",{className:"prpl-gauge__ring",style:c,children:[(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--min",style:m,children:"0"}),(0,t.jsx)("span",{className:"prpl-gauge__content",style:b,children:(0,t.jsx)("span",{className:"prpl-gauge__content-inner",style:{display:"inline-block",width:"50%"},children:i})}),(0,t.jsx)("span",{className:"prpl-gauge__label prpl-gauge__label--max",style:u,children:a})]})})}function o({badgeId:e,badgeName:a,brandingId:l=0,remoteServerUrl:n,placeholderUrl:s,isComplete:o=!0}){const[i,p]=(0,r.useState)(!1),d=(0,r.useCallback)(()=>{let r=`${n}/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=${e}`;return l&&(r+=`&branding_id=${l}`),r},[e,l,n]),c=(0,r.useCallback)(()=>{!i&&s&&p(!0)},[i,s]),g={maxWidth:"100%",height:"auto",verticalAlign:"bottom",transition:"opacity 0.3s ease-in-out, filter 0.3s ease-in-out",...o?{}:{opacity:.25,filter:"grayscale(1)"}};return(0,t.jsx)("img",{className:"prpl-badge__image",src:i?s:d(),alt:a||"Badge",onError:c,style:g})}function i(){const[e,l]=(0,r.useState)(!0),[i,p]=(0,r.useState)(null),[d,c]=(0,r.useState)(null),[g,m]=(0,r.useState)([]),[u,b]=(0,r.useState)({brandingId:0,remoteServerUrl:"",placeholderUrl:""});return(0,r.useEffect)(()=>{(async()=>{try{const e=await n()({path:"/progress-planner/v1/streak-badges"});c(e.currentBadge),m(e.allBadges||[]),b({brandingId:e.brandingId,remoteServerUrl:e.remoteServerUrl,placeholderUrl:e.placeholderUrl}),l(!1)}catch(e){p(e.message||"Failed to load badge data"),l(!1)}})()},[]),e?(0,t.jsx)("p",{children:(0,a.__)("Loading…","progress-planner")}):i?(0,t.jsx)("p",{children:i}):d?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{children:(0,a.__)("Execute at least one website maintenance task every week.","progress-planner")}),(0,t.jsxs)("div",{className:"prpl-latest-badges-wrapper",children:[(0,t.jsx)(s,{value:d.progress,max:100,backgroundColor:d.background||"var(--prpl-background-streak)",color:"var(--prpl-color-monthly)",color2:"var(--prpl-color-monthly-2)",children:(0,t.jsx)(o,{badgeId:d.id,badgeName:d.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:!0})}),(0,t.jsxs)("div",{className:"prpl-badge-content-wrapper",children:[(0,t.jsxs)("p",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"1rem",marginBottom:0},children:[(0,t.jsx)("span",{children:(0,a.sprintf)(/* translators: %s: The badge name. */ /* translators: %s: The badge name. */ (0,a.__)("Progress %s","progress-planner"),d.name)}),(0,t.jsxs)("span",{style:{fontWeight:600,fontSize:"var(--prpl-font-size-3xl)"},children:[d.progress,"%"]})]}),(0,t.jsx)("p",{style:{marginTop:0},children:(0,a.sprintf)(/* translators: %s: The remaining number of weeks. */ /* translators: %s: The remaining number of weeks. */ (0,a._n)("%s week to go to complete this streak!","%s weeks to go to complete this streak!",d.remaining,"progress-planner"),d.remaining)})]})]}),(0,t.jsx)("hr",{}),(0,t.jsx)("div",{className:"prpl-badges-container-achievements",children:(0,t.jsx)("div",{className:"progress-wrapper badge-group-maintenance",children:g.map(e=>(0,t.jsxs)("span",{className:"prpl-badge","data-value":e.progress,children:[(0,t.jsx)(o,{badgeId:e.id,badgeName:e.name,brandingId:u.brandingId,remoteServerUrl:u.remoteServerUrl,placeholderUrl:u.placeholderUrl,isComplete:e.isComplete}),(0,t.jsx)("p",{children:e.name})]},e.id))})})]}):(0,t.jsx)("p",{children:(0,a.__)("No badge data available.","progress-planner")})}function p(){const e=document.getElementById("prpl-streak-badges-root");e&&(0,r.createRoot)(e).render((0,t.jsx)(i,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/classes/admin/widgets/class-activity-scores.php b/classes/admin/widgets/class-activity-scores.php index 8adcde6448..2a2291940f 100644 --- a/classes/admin/widgets/class-activity-scores.php +++ b/classes/admin/widgets/class-activity-scores.php @@ -225,4 +225,22 @@ public function personal_record_callback() { public function get_cache_key() { return $this->cache_key; } + + /** + * Enqueue scripts for this widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/activity-scores.asset.php'; + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/activity-scores', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/activity-scores.js', + $asset['dependencies'], + $asset['version'], + true + ); + } } diff --git a/classes/class-base.php b/classes/class-base.php index 26aa63d532..28fca8f2e3 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -22,6 +22,7 @@ * @method \Progress_Planner\Rest\Monthly_Badges get_rest__monthly_badges() * @method \Progress_Planner\Rest\Content_Badges get_rest__content_badges() * @method \Progress_Planner\Rest\Streak_Badges get_rest__streak_badges() + * @method \Progress_Planner\Rest\Activity_Scores get_rest__activity_scores() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -129,6 +130,7 @@ public function init() { $this->get_rest__monthly_badges(); $this->get_rest__content_badges(); $this->get_rest__streak_badges(); + $this->get_rest__activity_scores(); // Onboarding. $this->get_utils__onboard(); diff --git a/classes/rest/class-activity-scores.php b/classes/rest/class-activity-scores.php new file mode 100644 index 0000000000..030683b12e --- /dev/null +++ b/classes/rest/class-activity-scores.php @@ -0,0 +1,94 @@ + 'GET', + 'callback' => [ $this, 'get_activity_scores' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + } + + /** + * Check if the current user has permission to access this endpoint. + * + * @return bool + */ + public function check_permissions() { + return \current_user_can( 'edit_posts' ); + } + + /** + * Get activity scores data. + * + * @param \WP_REST_Request $request The REST request object. + * + * @return \WP_REST_Response + */ + public function get_activity_scores( $request ) { + $widget = \progress_planner()->get_admin__widgets__activity_scores(); + $record = $widget->personal_record_callback(); + $score = $widget->get_score(); + + // Get chart data. + $chart = \progress_planner()->get_ui__chart(); + $chart_data = $chart->get_chart_data( + [ + 'type' => 'bar', + 'items_callback' => fn( $start_date, $end_date ) => \progress_planner()->get_activities__query()->query_activities( + [ + 'start_date' => $start_date, + 'end_date' => $end_date, + ] + ), + 'dates_params' => [ + 'start_date' => \DateTime::createFromFormat( 'Y-m-d', \gmdate( 'Y-m-01' ) )->modify( $widget->get_range() ), + 'end_date' => new \DateTime(), + 'frequency' => $widget->get_frequency(), + 'format' => 'M', + ], + 'count_callback' => fn( $activities, $date ) => \array_sum( \array_map( fn( $activity ) => $activity->get_points( $date ), $activities ) ) * 100 / Base::SCORE_TARGET, + 'normalized' => true, + 'color' => [ $widget, 'get_color' ], + 'max' => 100, + ] + ); + + // Build response data. + $response_data = [ + 'score' => $score, + 'gaugeColor' => $widget->get_gauge_color( $score ), + 'chartData' => $chart_data, + 'personalRecord' => [ + 'maxStreak' => (int) $record['max_streak'], + 'currentStreak' => (int) $record['current_streak'], + ], + ]; + + return new \WP_REST_Response( $response_data ); + } +} diff --git a/views/page-widgets/activity-scores.php b/views/page-widgets/activity-scores.php index b2da240b1f..4ec0210ac8 100644 --- a/views/page-widgets/activity-scores.php +++ b/views/page-widgets/activity-scores.php @@ -5,16 +5,11 @@ * @package Progress_Planner */ -use Progress_Planner\Base; - if ( ! \defined( 'ABSPATH' ) ) { exit; } - -$prpl_widget = \progress_planner()->get_admin__widgets__activity_scores(); -$prpl_record = $prpl_widget->personal_record_callback(); - ?> +

get_ui__branding()->get_widget_title( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped @@ -38,101 +33,4 @@

-
- - get_score() ); ?> - -
- -
- -

-
- get_ui__chart()->the_chart( - [ - 'type' => 'bar', - 'items_callback' => fn( $start_date, $end_date ) => \progress_planner()->get_activities__query()->query_activities( - [ - 'start_date' => $start_date, - 'end_date' => $end_date, - ] - ), - 'dates_params' => [ - 'start_date' => \DateTime::createFromFormat( 'Y-m-d', \gmdate( 'Y-m-01' ) )->modify( $prpl_widget->get_range() ), - 'end_date' => new \DateTime(), - 'frequency' => $prpl_widget->get_frequency(), - 'format' => 'M', - ], - 'count_callback' => fn( $activities, $date ) => \array_sum( \array_map( fn( $activity ) => $activity->get_points( $date ), $activities ) ) * 100 / Base::SCORE_TARGET, - 'normalized' => true, - 'color' => [ $prpl_widget, 'get_color' ], - 'max' => 100, - ] - ); - ?> -
- -
- - - -
- -
+
diff --git a/webpack.config.js b/webpack.config.js index ab52ead8ef..e8e98e715b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,6 +8,7 @@ module.exports = { 'monthly-badges': './assets/src/monthly-badges.js', 'content-badges': './assets/src/content-badges.js', 'streak-badges': './assets/src/streak-badges.js', + 'activity-scores': './assets/src/activity-scores.js', }, output: { path: path.resolve( __dirname, 'build' ), From 419ff850949835bd7d2f2e75ee55a6bbe78c3f2a Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 20:11:01 +0200 Subject: [PATCH 006/275] whats-new --- assets/src/whats-new.js | 27 ++++++ assets/src/widgets/WhatsNew/index.js | 100 ++++++++++++++++++++++ build/whats-new.asset.php | 1 + build/whats-new.js | 1 + classes/admin/widgets/class-whats-new.php | 21 +++++ classes/class-base.php | 2 + classes/rest/class-whats-new.php | 85 ++++++++++++++++++ views/page-widgets/whats-new.php | 33 +------ webpack.config.js | 1 + 9 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 assets/src/whats-new.js create mode 100644 assets/src/widgets/WhatsNew/index.js create mode 100644 build/whats-new.asset.php create mode 100644 build/whats-new.js create mode 100644 classes/rest/class-whats-new.php diff --git a/assets/src/whats-new.js b/assets/src/whats-new.js new file mode 100644 index 0000000000..28376f1b00 --- /dev/null +++ b/assets/src/whats-new.js @@ -0,0 +1,27 @@ +/** + * What's New widget entry point. + * + * This file initializes the React What's New widget component + * and mounts it to the DOM. + */ + +import { createRoot } from '@wordpress/element'; +import WhatsNew from './widgets/WhatsNew'; + +/** + * Initialize the What's New widget. + */ +function init() { + const container = document.getElementById( 'prpl-whats-new-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready. +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/widgets/WhatsNew/index.js b/assets/src/widgets/WhatsNew/index.js new file mode 100644 index 0000000000..f185efcbab --- /dev/null +++ b/assets/src/widgets/WhatsNew/index.js @@ -0,0 +1,100 @@ +/** + * What's New Widget Component. + * + * Displays blog posts from the Progress Planner blog RSS feed. + */ + +import { useState, useEffect, Fragment } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; + +/** + * What's New widget component. + * + * @return {JSX.Element|null} The widget component or null if no posts. + */ +export default function WhatsNew() { + const [ isLoading, setIsLoading ] = useState( true ); + const [ posts, setPosts ] = useState( [] ); + const [ blogUrl, setBlogUrl ] = useState( '' ); + + useEffect( () => { + apiFetch( { path: '/progress-planner/v1/whats-new' } ) + .then( ( response ) => { + setPosts( response.posts || [] ); + setBlogUrl( response.blogUrl || '' ); + setIsLoading( false ); + } ) + .catch( () => { + setIsLoading( false ); + } ); + }, [] ); + + // Show loading state. + if ( isLoading ) { + return ( + +
+

+ { __( 'Loading…', 'progress-planner' ) } +

+
+ ); + } + + // Return null if no posts (widget should not render content). + if ( posts.length === 0 ) { + return null; + } + + return ( + +
+
    + { posts.map( ( post, index ) => ( +
  • + { post.imageUrl && ( + +
    + + ) } +

    + + { post.title } + +

    +

    + { post.excerpt } +

    +
    +
  • + ) ) } +
+ +
+ ); +} diff --git a/build/whats-new.asset.php b/build/whats-new.asset.php new file mode 100644 index 0000000000..510adfe612 --- /dev/null +++ b/build/whats-new.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'b9e2d2facbb5b89b0b82'); diff --git a/build/whats-new.js b/build/whats-new.js new file mode 100644 index 0000000000..174bc3131e --- /dev/null +++ b/build/whats-new.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={n:r=>{var n=r&&r.__esModule?()=>r.default:()=>r;return e.d(n,{a:n}),n},d:(r,n)=>{for(var t in n)e.o(n,t)&&!e.o(r,t)&&Object.defineProperty(r,t,{enumerable:!0,get:n[t]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)};const r=window.wp.element,n=window.wp.apiFetch;var t=e.n(n);const a=window.wp.i18n,s=window.ReactJSXRuntime;function l(){const[e,n]=(0,r.useState)(!0),[l,o]=(0,r.useState)([]),[p,i]=(0,r.useState)("");return(0,r.useEffect)(()=>{t()({path:"/progress-planner/v1/whats-new"}).then(e=>{o(e.posts||[]),i(e.blogUrl||""),n(!1)}).catch(()=>{n(!1)})},[]),e?(0,s.jsxs)(r.Fragment,{children:[(0,s.jsx)("hr",{}),(0,s.jsx)("p",{className:"prpl-whats-new__loading",children:(0,a.__)("Loading…","progress-planner")})]}):0===l.length?null:(0,s.jsxs)(r.Fragment,{children:[(0,s.jsx)("hr",{}),(0,s.jsx)("ul",{className:"prpl-whats-new__list",style:{listStyle:"none",padding:0},children:l.map((e,r)=>(0,s.jsxs)("li",{className:"prpl-whats-new__item",children:[e.imageUrl&&(0,s.jsx)("a",{href:e.link,target:"_blank",rel:"noopener noreferrer",className:"prpl-whats-new__image-link",children:(0,s.jsx)("div",{className:"prpl-blog-post-image",style:{backgroundImage:`url(${e.imageUrl})`}})}),(0,s.jsx)("h3",{className:"prpl-whats-new__title",children:(0,s.jsx)("a",{href:e.link,target:"_blank",rel:"noopener noreferrer",children:e.title})}),(0,s.jsx)("p",{className:"prpl-whats-new__excerpt",children:e.excerpt}),(0,s.jsx)("hr",{})]},r))}),(0,s.jsx)("div",{className:"prpl-widget-footer",children:(0,s.jsx)("a",{href:p,target:"_blank",rel:"noopener noreferrer",children:(0,a.__)("Read all posts","progress-planner")})})]})}function o(){const e=document.getElementById("prpl-whats-new-root");e&&(0,r.createRoot)(e).render((0,s.jsx)(l,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",o):o()})(); \ No newline at end of file diff --git a/classes/admin/widgets/class-whats-new.php b/classes/admin/widgets/class-whats-new.php index 6a4b921a74..f3f6596e49 100644 --- a/classes/admin/widgets/class-whats-new.php +++ b/classes/admin/widgets/class-whats-new.php @@ -21,6 +21,27 @@ final class Whats_New extends Widget { */ protected $id = 'whats-new'; + /** + * Enqueue scripts for the widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/whats-new.asset.php'; + if ( ! \file_exists( $asset_file ) ) { + return; + } + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/whats-new', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/whats-new.js', + $asset['dependencies'], + $asset['version'], + true + ); + } + /** * Get the feed from the blog. * diff --git a/classes/class-base.php b/classes/class-base.php index 28fca8f2e3..68699c0a8b 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -23,6 +23,7 @@ * @method \Progress_Planner\Rest\Content_Badges get_rest__content_badges() * @method \Progress_Planner\Rest\Streak_Badges get_rest__streak_badges() * @method \Progress_Planner\Rest\Activity_Scores get_rest__activity_scores() + * @method \Progress_Planner\Rest\Whats_New get_rest__whats_new() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -131,6 +132,7 @@ public function init() { $this->get_rest__content_badges(); $this->get_rest__streak_badges(); $this->get_rest__activity_scores(); + $this->get_rest__whats_new(); // Onboarding. $this->get_utils__onboard(); diff --git a/classes/rest/class-whats-new.php b/classes/rest/class-whats-new.php new file mode 100644 index 0000000000..3cdbb8d57d --- /dev/null +++ b/classes/rest/class-whats-new.php @@ -0,0 +1,85 @@ + 'GET', + 'callback' => [ $this, 'get_whats_new' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + } + + /** + * Check if the current user has permission to access this endpoint. + * + * @return bool + */ + public function check_permissions() { + return \current_user_can( 'edit_posts' ); + } + + /** + * Get what's new data. + * + * @param \WP_REST_Request $request The REST request object. + * + * @return \WP_REST_Response + */ + public function get_whats_new( $request ) { + $widget = \progress_planner()->get_admin__widgets__whats_new(); + $posts = $widget->get_blog_feed(); + + // Return empty array if no posts. + if ( empty( $posts ) ) { + return new \WP_REST_Response( + [ + 'posts' => [], + 'blogUrl' => '', + ] + ); + } + + // Format posts for frontend. + $formatted_posts = \array_map( + function ( $post ) { + $image_url = $post['featured_media']['media_details']['sizes']['medium_large']['source_url'] ?? null; + return [ + 'title' => $post['title']['rendered'], + 'link' => $post['link'], + 'excerpt' => \wp_trim_words( \wp_strip_all_tags( $post['content']['rendered'] ), 55 ), + 'imageUrl' => $image_url, + ]; + }, + $posts + ); + + return new \WP_REST_Response( + [ + 'posts' => $formatted_posts, + 'blogUrl' => \progress_planner()->get_ui__branding()->get_url( 'https://prpl.fyi/blog' ), + ] + ); + } +} diff --git a/views/page-widgets/whats-new.php b/views/page-widgets/whats-new.php index c9826d5ebb..063564329d 100644 --- a/views/page-widgets/whats-new.php +++ b/views/page-widgets/whats-new.php @@ -16,8 +16,8 @@ if ( empty( $prpl_blog_posts ) ) { return; } - ?> +

get_ui__branding()->get_widget_title( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped @@ -26,32 +26,5 @@ ); ?>

-
-
    - get_blog_feed() as $prpl_blog_post ) : ?> - -
  • - - -
    -
    - -

    - - - -

    -

    -
    -
  • - -
- + +
diff --git a/webpack.config.js b/webpack.config.js index e8e98e715b..ea6d96b526 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ module.exports = { 'content-badges': './assets/src/content-badges.js', 'streak-badges': './assets/src/streak-badges.js', 'activity-scores': './assets/src/activity-scores.js', + 'whats-new': './assets/src/whats-new.js', }, output: { path: path.resolve( __dirname, 'build' ), From 7e6bc6ee219f4ce31617b56251e5166582f22c95 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 21:09:12 +0200 Subject: [PATCH 007/275] More migrations --- assets/js/celebrate.js | 2 +- assets/src/suggested-tasks.js | 27 + assets/src/todo.js | 26 + .../widgets/SuggestedTasks/PopoverManager.js | 672 ++++++++++++++++ .../src/widgets/SuggestedTasks/TaskActions.js | 151 ++++ assets/src/widgets/SuggestedTasks/TaskItem.js | 315 ++++++++ .../SuggestedTasks/hooks/useCelebration.js | 72 ++ .../SuggestedTasks/hooks/usePopoverForms.js | 285 +++++++ .../SuggestedTasks/hooks/useTasksApi.js | 245 ++++++ assets/src/widgets/SuggestedTasks/index.js | 411 ++++++++++ assets/src/widgets/TodoWidget/index.js | 734 ++++++++++++++++++ build/suggested-tasks.asset.php | 1 + build/suggested-tasks.js | 1 + build/todo.asset.php | 1 + build/todo.js | 1 + .../admin/class-dashboard-widget-score.php | 13 +- .../admin/widgets/class-suggested-tasks.php | 41 + classes/admin/widgets/class-todo.php | 102 +-- .../providers/class-tasks-interactive.php | 26 +- views/page-widgets/suggested-tasks.php | 32 +- webpack.config.js | 2 + 21 files changed, 3038 insertions(+), 122 deletions(-) create mode 100644 assets/src/suggested-tasks.js create mode 100644 assets/src/todo.js create mode 100644 assets/src/widgets/SuggestedTasks/PopoverManager.js create mode 100644 assets/src/widgets/SuggestedTasks/TaskActions.js create mode 100644 assets/src/widgets/SuggestedTasks/TaskItem.js create mode 100644 assets/src/widgets/SuggestedTasks/hooks/useCelebration.js create mode 100644 assets/src/widgets/SuggestedTasks/hooks/usePopoverForms.js create mode 100644 assets/src/widgets/SuggestedTasks/hooks/useTasksApi.js create mode 100644 assets/src/widgets/SuggestedTasks/index.js create mode 100644 assets/src/widgets/TodoWidget/index.js create mode 100644 build/suggested-tasks.asset.php create mode 100644 build/suggested-tasks.js create mode 100644 build/todo.asset.php create mode 100644 build/todo.js diff --git a/assets/js/celebrate.js b/assets/js/celebrate.js index 643d823b76..9f10e16c5d 100644 --- a/assets/js/celebrate.js +++ b/assets/js/celebrate.js @@ -4,7 +4,7 @@ * * A script that triggers confetti on the container element. * - * Dependencies: particles-confetti, progress-planner/suggested-task + * Dependencies: particles-confetti */ /* eslint-disable camelcase */ diff --git a/assets/src/suggested-tasks.js b/assets/src/suggested-tasks.js new file mode 100644 index 0000000000..fc47218092 --- /dev/null +++ b/assets/src/suggested-tasks.js @@ -0,0 +1,27 @@ +/** + * Suggested Tasks widget entry point. + * + * This file initializes the React Suggested Tasks widget component + * and mounts it to the DOM. + */ + +import { createRoot } from '@wordpress/element'; +import SuggestedTasks from './widgets/SuggestedTasks'; + +/** + * Initialize the Suggested Tasks widget. + */ +function init() { + const container = document.getElementById( 'prpl-suggested-tasks-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready. +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/todo.js b/assets/src/todo.js new file mode 100644 index 0000000000..239c6a8b23 --- /dev/null +++ b/assets/src/todo.js @@ -0,0 +1,26 @@ +/** + * Todo Widget Entry Point. + * + * Initializes the React-based Todo widget. + */ + +import { createRoot } from '@wordpress/element'; +import TodoWidget from './widgets/TodoWidget'; + +/** + * Initialize the Todo widget. + */ +function init() { + const container = document.getElementById( 'prpl-todo-root' ); + if ( container ) { + const root = createRoot( container ); + root.render( ); + } +} + +// Initialize when DOM is ready. +if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); +} else { + init(); +} diff --git a/assets/src/widgets/SuggestedTasks/PopoverManager.js b/assets/src/widgets/SuggestedTasks/PopoverManager.js new file mode 100644 index 0000000000..bb2d2871b3 --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/PopoverManager.js @@ -0,0 +1,672 @@ +/** + * Popover Manager Component. + * + * Sets up form submission handlers for all interactive task popovers. + * This replaces the vanilla JS files in assets/js/recommendations/. + */ + +import { useEffect, useCallback } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; +import { + submitSiteSettings, + submitPluginSettings, + deletePost, + closePopover, +} from './hooks/usePopoverForms'; + +/** + * Popover configuration for each task type. + * This maps task IDs to their form submission configuration. + */ +const POPOVER_CONFIG = { + // Core WordPress settings (siteSettings pattern) + 'core-blogdescription': { + type: 'siteSettings', + settingAPIKey: 'description', + setting: 'blogdescription', + }, + 'disable-comments': { + type: 'siteSettings', + settingAPIKey: 'default_comment_status', + setting: 'default_comment_status', + settingCallbackValue: () => 'closed', + }, + 'disable-comment-pagination': { + type: 'siteSettings', + settingAPIKey: 'page_comments', + setting: 'page_comments', + settingCallbackValue: () => false, + }, + 'select-locale': { + type: 'siteSettings', + settingAPIKey: 'WPLANG', + setting: 'WPLANG', + }, + 'select-timezone': { + type: 'siteSettings', + settingAPIKey: 'timezone_string', + setting: 'timezone_string', + }, + 'search-engine-visibility': { + type: 'pluginSettings', + setting: 'blog_public', + action: 'prpl_interactive_task_submit', + settingCallbackValue: () => '1', + }, + 'set-date-format': { + type: 'siteSettings', + settingAPIKey: 'date_format', + setting: 'date_format', + }, + 'core-permalink-structure': { + type: 'siteSettings', + settingAPIKey: 'permalink_structure', + setting: 'permalink_structure', + }, + 'rename-uncategorized-category': { + type: 'customSubmit', + // Custom handling needed - updates term name + }, + + // Custom submit patterns (delete posts/pages) + 'hello-world': { + type: 'customSubmit', + // Custom handling - delete post + }, + 'sample-page': { + type: 'customSubmit', + // Custom handling - delete page + }, + + // Yoast settings + 'yoast-author-archive': { + type: 'pluginSettings', + setting: 'wpseo_titles', + settingPath: JSON.stringify( [ 'disable-author' ] ), + settingCallbackValue: () => true, + }, + 'yoast-date-archive': { + type: 'pluginSettings', + setting: 'wpseo_titles', + settingPath: JSON.stringify( [ 'disable-date' ] ), + settingCallbackValue: () => true, + }, + 'yoast-format-archive': { + type: 'pluginSettings', + setting: 'wpseo_titles', + settingPath: JSON.stringify( [ 'disable-post_format' ] ), + settingCallbackValue: () => true, + }, + 'yoast-media-pages': { + type: 'pluginSettings', + setting: 'wpseo_titles', + settingPath: JSON.stringify( [ 'disable-attachment' ] ), + settingCallbackValue: () => true, + }, + 'yoast-crawl-settings-emoji-scripts': { + type: 'pluginSettings', + setting: 'wpseo', + settingPath: JSON.stringify( [ 'remove_emoji_scripts' ] ), + settingCallbackValue: () => true, + }, + 'yoast-crawl-settings-feed-authors': { + type: 'pluginSettings', + setting: 'wpseo', + settingPath: JSON.stringify( [ 'remove_feed_authors' ] ), + settingCallbackValue: () => true, + }, + 'yoast-crawl-settings-feed-global-comments': { + type: 'pluginSettings', + setting: 'wpseo', + settingPath: JSON.stringify( [ 'remove_feed_global_comments' ] ), + settingCallbackValue: () => true, + }, + 'yoast-organization-logo': { + type: 'customSubmit', + // Custom handling - media upload + }, + + // AIOSEO settings + 'aioseo-author-archive': { + type: 'pluginSettings', + setting: 'aioseo_options_search_appearance', + settingPath: JSON.stringify( [ 'archives', 'author', 'show' ] ), + settingCallbackValue: () => false, + }, + 'aioseo-date-archive': { + type: 'pluginSettings', + setting: 'aioseo_options_search_appearance', + settingPath: JSON.stringify( [ 'archives', 'date', 'show' ] ), + settingCallbackValue: () => false, + }, + 'aioseo-media-pages': { + type: 'pluginSettings', + setting: 'aioseo_options_search_appearance', + settingPath: JSON.stringify( [ 'postTypes', 'attachment', 'show' ] ), + settingCallbackValue: () => false, + }, + 'aioseo-crawl-settings-feed-authors': { + type: 'pluginSettings', + setting: 'aioseo_options_rss_content', + settingPath: JSON.stringify( [ 'authorFeed' ] ), + settingCallbackValue: () => false, + }, + 'aioseo-crawl-settings-feed-comments': { + type: 'pluginSettings', + setting: 'aioseo_options_rss_content', + settingPath: JSON.stringify( [ 'commentFeed' ] ), + settingCallbackValue: () => false, + }, + + // Complex custom handlers + 'core-siteicon': { + type: 'customSubmit', + // Custom handling - media upload for site icon + }, + 'update-term-description': { + type: 'customSubmit', + // Custom handling - dynamic term + }, + 'remove-terms-without-posts': { + type: 'customSubmit', + // Custom handling - multiple terms + }, +}; + +/** + * PopoverManager component. + * + * @param {Object} props Component props. + * @param {Array} props.tasks The list of tasks. + * @param {Function} props.onComplete Callback for completing a task. + * @return {null} This component renders nothing. + */ +export default function PopoverManager( { tasks, onComplete } ) { + /** + * Handle custom submit types. + */ + const handleCustomSubmit = useCallback( async ( taskId, popoverId ) => { + switch ( taskId ) { + case 'hello-world': { + const postId = window.helloWorldData?.postId; + if ( postId ) { + await deletePost( postId, 'posts' ); + } + return { success: true }; + } + + case 'sample-page': { + const pageId = window.samplePageData?.postId; + if ( pageId ) { + await deletePost( pageId, 'pages' ); + } + return { success: true }; + } + + case 'rename-uncategorized-category': { + const formElement = document.querySelector( + `#${ popoverId } form` + ); + if ( formElement ) { + const formData = new FormData( formElement ); + const newName = formData.get( 'category_name' ); + const termId = + window.renameUncategorizedCategoryData?.termId; + + if ( termId && newName ) { + await apiFetch( { + path: `/wp/v2/categories/${ termId }`, + method: 'POST', + data: { name: newName }, + } ); + } + } + return { success: true }; + } + + case 'core-siteicon': { + // Site icon is handled via media uploader + // The hidden field is populated by the media uploader + const formElement = document.querySelector( + `#${ popoverId } form` + ); + if ( formElement ) { + const formData = new FormData( formElement ); + const iconId = formData.get( 'site_icon' ); + + if ( iconId ) { + await apiFetch( { + path: '/wp/v2/settings', + method: 'POST', + data: { site_icon: parseInt( iconId ) }, + } ); + } + } + return { success: true }; + } + + case 'yoast-organization-logo': { + // Organization logo is handled via media uploader + const formElement = document.querySelector( + `#${ popoverId } form` + ); + if ( formElement ) { + const formData = new FormData( formElement ); + const logoId = formData.get( 'company_logo_id' ); + + if ( logoId ) { + await submitPluginSettings( { + setting: 'wpseo', + settingPath: JSON.stringify( [ + 'company_logo_id', + ] ), + popoverId, + settingCallbackValue: () => parseInt( logoId ), + } ); + } + } + return { success: true }; + } + + case 'update-term-description': { + const formElement = document.querySelector( + `#${ popoverId } form` + ); + if ( formElement ) { + const formData = new FormData( formElement ); + const description = formData.get( 'description' ); + const termId = window.updateTermDescriptionData?.termId; + const taxonomy = + window.updateTermDescriptionData?.taxonomy || + 'category'; + + if ( termId && description ) { + const taxonomyEndpoint = + taxonomy === 'category' ? 'categories' : taxonomy; + await apiFetch( { + path: `/wp/v2/${ taxonomyEndpoint }/${ termId }`, + method: 'POST', + data: { description }, + } ); + } + } + return { success: true }; + } + + case 'remove-terms-without-posts': { + const formElement = document.querySelector( + `#${ popoverId } form` + ); + if ( formElement ) { + const termIds = + window.removeTermsWithoutPostsData?.termIds || []; + const taxonomy = + window.removeTermsWithoutPostsData?.taxonomy || + 'category'; + + const taxonomyEndpoint = + taxonomy === 'category' ? 'categories' : taxonomy; + + // Delete each term + await Promise.all( + termIds.map( ( termId ) => + apiFetch( { + path: `/wp/v2/${ taxonomyEndpoint }/${ termId }?force=true`, + method: 'DELETE', + } ) + ) + ); + } + return { success: true }; + } + + default: + return { success: true }; + } + }, [] ); + + /** + * Handle form submission for a task. + */ + const handleFormSubmit = useCallback( + async ( taskId, popoverId, config ) => { + try { + // Find the task + const task = tasks.find( + ( t ) => + t.slug === taskId || + t.prpl_provider?.slug === taskId || + `${ t.id }` === taskId + ); + + if ( ! task ) { + return; + } + + let submitPromise; + + switch ( config.type ) { + case 'siteSettings': + submitPromise = submitSiteSettings( { + settingAPIKey: config.settingAPIKey, + setting: config.setting, + popoverId, + settingCallbackValue: config.settingCallbackValue, + } ); + break; + + case 'pluginSettings': + submitPromise = submitPluginSettings( { + setting: config.setting, + settingPath: config.settingPath, + popoverId, + action: + config.action || 'prpl_interactive_task_submit', + settingCallbackValue: config.settingCallbackValue, + } ); + break; + + case 'customSubmit': + // Handle specific custom submits + submitPromise = handleCustomSubmit( taskId, popoverId ); + break; + + default: + return; + } + + await submitPromise; + + // Trigger task completion + await onComplete( task.id, task ); + + // Close the popover + closePopover( popoverId ); + } catch ( error ) { + // Error already shown by submit functions + // eslint-disable-next-line no-console + console.error( 'Popover form submission error:', error ); + } + }, + [ tasks, onComplete, handleCustomSubmit ] + ); + + /** + * Set up form listeners for all popovers. + */ + useEffect( () => { + const formHandlers = new Map(); + + // Set up listeners for each configured popover + Object.entries( POPOVER_CONFIG ).forEach( ( [ taskId, config ] ) => { + const popoverId = `prpl-popover-${ taskId }`; + const formElement = document.querySelector( + `#${ popoverId } form` + ); + + if ( ! formElement ) { + return; + } + + const handler = ( event ) => { + event.preventDefault(); + handleFormSubmit( taskId, popoverId, config ); + }; + + formElement.addEventListener( 'submit', handler ); + formHandlers.set( popoverId, { formElement, handler } ); + } ); + + // Set up input validation for blogdescription + const blogdescriptionInput = document.querySelector( + 'input#blogdescription' + ); + if ( blogdescriptionInput ) { + const submitButton = document.querySelector( + '#prpl-popover-core-blogdescription button[type="submit"]' + ); + if ( submitButton ) { + const inputHandler = ( e ) => { + submitButton.disabled = e.target.value.length === 0; + }; + blogdescriptionInput.addEventListener( 'input', inputHandler ); + } + } + + // Set up date format preview + setupDateFormatPreview(); + + // Set up permalink structure preview + setupPermalinkPreview(); + + // Set up media uploaders + setupMediaUploaders(); + + // Cleanup + return () => { + formHandlers.forEach( ( { formElement, handler } ) => { + formElement.removeEventListener( 'submit', handler ); + } ); + }; + }, [ tasks, handleFormSubmit ] ); + + // This component doesn't render anything + return null; +} + +/** + * Set up date format preview functionality. + */ +function setupDateFormatPreview() { + const radios = document.querySelectorAll( + '#prpl-popover-set-date-format input[name="date_format"]' + ); + const customInput = document.querySelector( + '#prpl-popover-set-date-format input[name="date_format_custom"]' + ); + + if ( ! radios.length || ! customInput ) { + return; + } + + // Handle radio change + radios.forEach( ( radio ) => { + radio.addEventListener( 'change', () => { + if ( radio.value === 'custom' ) { + customInput.disabled = false; + customInput.focus(); + } else { + customInput.disabled = true; + } + } ); + } ); + + // Handle custom input - update preview via AJAX + let debounceTimeout; + customInput.addEventListener( 'input', () => { + clearTimeout( debounceTimeout ); + debounceTimeout = setTimeout( async () => { + const format = customInput.value; + if ( ! format ) { + return; + } + + try { + const ajaxUrl = + window.prplSuggestedTasksConfig?.ajaxUrl || + window.progressPlanner?.ajaxUrl || + '/wp-admin/admin-ajax.php'; + const nonce = + window.prplSuggestedTasksConfig?.nonce || + window.progressPlanner?.nonce || + ''; + const response = await fetch( + `${ ajaxUrl }?action=prpl_date_format_preview&format=${ encodeURIComponent( + format + ) }&_ajax_nonce=${ nonce }`, + { credentials: 'same-origin' } + ); + const data = await response.json(); + if ( data.success && data.data ) { + // Update the custom preview + const customPreview = customInput + .closest( '.prpl-radio-wrapper' ) + ?.querySelector( '.date-time-text' ); + if ( customPreview ) { + customPreview.textContent = data.data; + } + } + } catch { + // Preview update failed, ignore + } + }, 300 ); + } ); +} + +/** + * Set up permalink structure preview functionality. + */ +function setupPermalinkPreview() { + const radios = document.querySelectorAll( + '#prpl-popover-core-permalink-structure input[name="permalink_structure"]' + ); + + if ( ! radios.length ) { + return; + } + + const customInput = document.querySelector( + '#prpl-popover-core-permalink-structure input[name="permalink_custom"]' + ); + + radios.forEach( ( radio ) => { + radio.addEventListener( 'change', () => { + if ( radio.value === 'custom' && customInput ) { + customInput.disabled = false; + customInput.focus(); + } else if ( customInput ) { + customInput.disabled = true; + } + } ); + } ); +} + +/** + * Set up media uploaders for site icon and organization logo. + */ +function setupMediaUploaders() { + // Site icon uploader + const siteIconButton = document.querySelector( + '#prpl-popover-core-siteicon .prpl-upload-site-icon' + ); + if ( siteIconButton && window.wp?.media ) { + let siteIconUploader; + + siteIconButton.addEventListener( 'click', ( e ) => { + e.preventDefault(); + + if ( ! siteIconUploader ) { + siteIconUploader = window.wp.media( { + title: siteIconButton.dataset.title || 'Select Site Icon', + button: { + text: + siteIconButton.dataset.button || 'Use as site icon', + }, + multiple: false, + library: { type: 'image' }, + } ); + + siteIconUploader.on( 'select', () => { + const attachment = siteIconUploader + .state() + .get( 'selection' ) + .first() + .toJSON(); + + // Update hidden field + const hiddenInput = document.querySelector( + '#prpl-popover-core-siteicon input[name="site_icon"]' + ); + if ( hiddenInput ) { + hiddenInput.value = attachment.id; + } + + // Update preview + const preview = document.querySelector( + '#prpl-popover-core-siteicon .prpl-site-icon-preview img' + ); + if ( preview ) { + preview.src = attachment.url; + } + + // Enable submit button + const submitButton = document.querySelector( + '#prpl-popover-core-siteicon button[type="submit"]' + ); + if ( submitButton ) { + submitButton.disabled = false; + } + } ); + } + + siteIconUploader.open(); + } ); + } + + // Yoast organization logo uploader + const logoButton = document.querySelector( + '#prpl-popover-yoast-organization-logo .prpl-upload-logo' + ); + if ( logoButton && window.wp?.media ) { + let logoUploader; + + logoButton.addEventListener( 'click', ( e ) => { + e.preventDefault(); + + if ( ! logoUploader ) { + logoUploader = window.wp.media( { + title: logoButton.dataset.title || 'Select Logo', + button: { + text: logoButton.dataset.button || 'Use as logo', + }, + multiple: false, + library: { type: 'image' }, + } ); + + logoUploader.on( 'select', () => { + const attachment = logoUploader + .state() + .get( 'selection' ) + .first() + .toJSON(); + + // Update hidden field + const hiddenInput = document.querySelector( + '#prpl-popover-yoast-organization-logo input[name="company_logo_id"]' + ); + if ( hiddenInput ) { + hiddenInput.value = attachment.id; + } + + // Update preview + const preview = document.querySelector( + '#prpl-popover-yoast-organization-logo .prpl-logo-preview img' + ); + if ( preview ) { + preview.src = attachment.url; + } + + // Enable submit button + const submitButton = document.querySelector( + '#prpl-popover-yoast-organization-logo button[type="submit"]' + ); + if ( submitButton ) { + submitButton.disabled = false; + } + } ); + } + + logoUploader.open(); + } ); + } +} diff --git a/assets/src/widgets/SuggestedTasks/TaskActions.js b/assets/src/widgets/SuggestedTasks/TaskActions.js new file mode 100644 index 0000000000..bc5635e311 --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/TaskActions.js @@ -0,0 +1,151 @@ +/** + * Task Actions Component. + * + * Renders action buttons for a task (complete, snooze, info, etc.). + * Uses the prpl_task_actions array from the API which contains pre-rendered HTML. + */ + +import { useEffect, useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Task Actions component. + * + * @param {Object} props Component props. + * @param {Object} props.task The task object. + * @param {boolean} props.isUserTask Whether this is a user task. + * @param {Function} props.onComplete Callback for completing a task. + * @param {Function} props.onSnooze Callback for snoozing a task. + * @param {Function} props.onDelete Callback for deleting a task. + * @return {JSX.Element} The task actions component. + */ +export default function TaskActions( { + task, + isUserTask, + onComplete, + onSnooze, + onDelete, +} ) { + const actionsRef = useRef( null ); + + /** + * Set up event handlers for the rendered HTML actions. + */ + useEffect( () => { + if ( ! actionsRef.current ) { + return; + } + + const container = actionsRef.current; + + // Handle complete button clicks. + const completeButtons = container.querySelectorAll( + '[data-action="complete"]' + ); + completeButtons.forEach( ( button ) => { + button.addEventListener( 'click', ( e ) => { + e.preventDefault(); + onComplete( task.id, task ); + } ); + } ); + + // Handle snooze radio changes. + const snoozeRadios = container.querySelectorAll( + '.prpl-snooze-duration-radio-group input[type="radio"]' + ); + snoozeRadios.forEach( ( radio ) => { + radio.addEventListener( 'change', () => { + onSnooze( task.id, radio.value ); + } ); + } ); + + // Handle popover triggers - intercept onclick and use showPopover. + const popoverLinks = container.querySelectorAll( + 'a[onclick*="showPopover"]' + ); + popoverLinks.forEach( ( link ) => { + // Extract popover ID from onclick attribute. + const onclickAttr = link.getAttribute( 'onclick' ); + const match = onclickAttr?.match( + /getElementById\(['"]([^'"]+)['"]\)/ + ); + if ( match ) { + const popoverId = match[ 1 ]; + link.removeAttribute( 'onclick' ); + link.addEventListener( 'click', ( e ) => { + e.preventDefault(); + const popover = document.getElementById( popoverId ); + if ( + popover && + typeof popover.showPopover === 'function' + ) { + popover.showPopover(); + } + } ); + } + } ); + + // Handle delete buttons for user tasks. + const deleteButtons = container.querySelectorAll( + '.prpl-suggested-task-button.trash' + ); + deleteButtons.forEach( ( button ) => { + button.addEventListener( 'click', ( e ) => { + e.preventDefault(); + onDelete( task.id ); + } ); + } ); + + // Cleanup event listeners on unmount. + return () => { + completeButtons.forEach( ( button ) => { + button.replaceWith( button.cloneNode( true ) ); + } ); + snoozeRadios.forEach( ( radio ) => { + radio.replaceWith( radio.cloneNode( true ) ); + } ); + }; + }, [ task, onComplete, onSnooze, onDelete ] ); + + // Get task actions from API response. + const taskActions = task.prpl_task_actions || []; + + // If no actions and not a user task, return empty. + if ( taskActions.length === 0 && ! isUserTask ) { + return
; + } + + return ( +
+ { /* Render pre-built HTML actions from the API */ } + { taskActions.map( ( actionHTML, index ) => ( + + ) ) } + + { /* Add delete button for user tasks */ } + { isUserTask && ( + + + + ) } +
+ ); +} diff --git a/assets/src/widgets/SuggestedTasks/TaskItem.js b/assets/src/widgets/SuggestedTasks/TaskItem.js new file mode 100644 index 0000000000..94b42b9adb --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/TaskItem.js @@ -0,0 +1,315 @@ +/** + * Task Item Component. + * + * Renders a single task item with its controls. + */ + +import { useRef, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import TaskActions from './TaskActions'; + +/** + * Arrow icon SVG for non-user tasks. + * + * @return {JSX.Element} The arrow SVG. + */ +function ArrowIcon() { + return ( + + + + ); +} + +/** + * Trash icon SVG. + * + * @return {JSX.Element} The trash SVG. + */ +function TrashIcon() { + return ( + + ); +} + +/** + * Task Item component. + * + * @param {Object} props Component props. + * @param {Object} props.task The task object. + * @param {boolean} props.isUserTask Whether this is a user task. + * @param {boolean} props.isCelebrating Whether the task is being celebrated. + * @param {Function} props.onComplete Callback for completing a task. + * @param {Function} props.onSnooze Callback for snoozing a task. + * @param {Function} props.onDelete Callback for deleting a task. + * @param {Function} props.onMove Callback for moving a task. + * @param {Function} props.onTitleChange Callback for changing the title. + * @return {JSX.Element} The task item component. + */ +export default function TaskItem( { + task, + isUserTask, + isCelebrating, + onComplete, + onSnooze, + onDelete, + onMove, + onTitleChange, +} ) { + const titleRef = useRef( null ); + const debounceTimeoutRef = useRef( null ); + + // Determine task action based on status. + const getTaskAction = () => { + if ( task.status === 'pending' ) { + return 'celebrate'; + } + if ( isCelebrating ) { + return 'celebrate'; + } + return ''; + }; + + // Check if task is completed (for user tasks). + const isCompleted = task.status === 'trash' || task.status === 'pending'; + + /** + * Handle checkbox change for user tasks. + */ + const handleCheckboxChange = useCallback( () => { + onComplete( task.id, task ); + }, [ task, onComplete ] ); + + /** + * Handle title keydown to prevent enter key. + * + * @param {KeyboardEvent} event The keyboard event. + */ + const handleTitleKeyDown = useCallback( ( event ) => { + if ( event.key === 'Enter' ) { + event.preventDefault(); + event.stopPropagation(); + event.target.blur(); + return false; + } + }, [] ); + + /** + * Handle title input with debounce. + */ + const handleTitleInput = useCallback( () => { + clearTimeout( debounceTimeoutRef.current ); + debounceTimeoutRef.current = setTimeout( () => { + if ( titleRef.current ) { + const newTitle = titleRef.current.textContent.replace( + /\n/g, + '' + ); + onTitleChange( task.id, newTitle ); + } + }, 300 ); + }, [ task.id, onTitleChange ] ); + + /** + * Handle move up. + */ + const handleMoveUp = useCallback( () => { + onMove( task.id, 'up' ); + }, [ task.id, onMove ] ); + + /** + * Handle move down. + */ + const handleMoveDown = useCallback( () => { + onMove( task.id, 'down' ); + }, [ task.id, onMove ] ); + + /** + * Handle trash click for user tasks. + */ + const handleTrash = useCallback( () => { + onDelete( task.id ); + }, [ task.id, onDelete ] ); + + // Get the task ID for the data attribute. + const taskId = task.slug || task.id; + + // Get the provider slug. + const providerSlug = task.prpl_provider?.slug || ''; + + // Build the class name. + const className = [ + 'prpl-suggested-task', + isCelebrating ? 'prpl-suggested-task-celebrated' : '', + ] + .filter( Boolean ) + .join( ' ' ); + + return ( +
  • +
    + { isUserTask ? ( + // eslint-disable-next-line jsx-a11y/label-has-associated-control -- Checkbox is nested inside label. + + ) : ( + + ) } +
    + +
    +

    + { isUserTask ? ( + + ) : ( + + ) } +

    +
    + +
    + { task.prpl_points > 0 && ( + + +{ task.prpl_points } + + ) } + + { isUserTask && ( + + ) } +
    + + { isUserTask && ( +
    + + + + +
    + ) } + +
    + +
    +
  • + ); +} diff --git a/assets/src/widgets/SuggestedTasks/hooks/useCelebration.js b/assets/src/widgets/SuggestedTasks/hooks/useCelebration.js new file mode 100644 index 0000000000..8cec398eb3 --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/hooks/useCelebration.js @@ -0,0 +1,72 @@ +/** + * Celebration Hook. + * + * Provides functions for triggering task completion celebrations. + */ + +import { useCallback } from '@wordpress/element'; + +/** + * Custom hook for task celebration functionality. + * + * @return {Object} Object containing celebration functions. + */ +export function useCelebration() { + /** + * Trigger celebration for a completed task. + * + * This dispatches the 'prpl/celebrateTasks' event which is handled + * by the existing celebrate.js script to render confetti. + * + * @param {HTMLElement} element The task element to celebrate. + */ + const celebrate = useCallback( ( element ) => { + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks', { + detail: { element }, + } ) + ); + }, [] ); + + /** + * Remove celebrated tasks from the DOM. + * + * This dispatches the 'prpl/removeCelebratedTasks' event which is handled + * by the existing celebrate.js script. + */ + const removeCelebratedTasks = useCallback( () => { + document.dispatchEvent( + new CustomEvent( 'prpl/removeCelebratedTasks' ) + ); + }, [] ); + + /** + * Update the Ravi gauge with earned points. + * + * @param {number} points The points to add to the gauge. + */ + const updateRaviGauge = useCallback( ( points ) => { + if ( typeof window.prplUpdateRaviGauge === 'function' ) { + window.prplUpdateRaviGauge( points ); + } + }, [] ); + + /** + * Trigger grid resize event. + * + * This dispatches the 'prpl/grid/resize' event which is handled + * by the grid masonry layout to recalculate item positions. + */ + const triggerGridResize = useCallback( () => { + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, [] ); + + return { + celebrate, + removeCelebratedTasks, + updateRaviGauge, + triggerGridResize, + }; +} + +export default useCelebration; diff --git a/assets/src/widgets/SuggestedTasks/hooks/usePopoverForms.js b/assets/src/widgets/SuggestedTasks/hooks/usePopoverForms.js new file mode 100644 index 0000000000..c484edff19 --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/hooks/usePopoverForms.js @@ -0,0 +1,285 @@ +/** + * Popover Forms Hook. + * + * Handles form submission logic for interactive task popovers. + * Replaces the vanilla JS prplInteractiveTaskFormListener. + */ + +import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; + +/** + * Show loading state on a form. + * + * @param {HTMLFormElement} formElement The form element. + */ +function showLoading( formElement ) { + let submitButton = formElement.querySelector( 'button[type="submit"]' ); + + if ( ! submitButton ) { + submitButton = formElement.querySelector( + 'button[data-action="completeTask"]' + ); + } + + if ( submitButton ) { + submitButton.disabled = true; + + // Add spinner. + const spinner = document.createElement( 'span' ); + spinner.classList.add( 'prpl-spinner' ); + spinner.innerHTML = + ''; + submitButton.after( spinner ); + } +} + +/** + * Hide loading state on a form. + * + * @param {HTMLFormElement} formElement The form element. + */ +function hideLoading( formElement ) { + let submitButton = formElement.querySelector( 'button[type="submit"]' ); + + if ( ! submitButton ) { + submitButton = formElement.querySelector( + 'button[data-action="completeTask"]' + ); + } + + if ( submitButton ) { + submitButton.disabled = false; + } + + const spinner = formElement.querySelector( 'span.prpl-spinner' ); + if ( spinner ) { + spinner.remove(); + } +} + +/** + * Show error message on a form. + * + * @param {string} popoverId The popover ID. + */ +function showError( popoverId ) { + const formElement = document.querySelector( `#${ popoverId } form` ); + + if ( ! formElement ) { + return; + } + + // Check if there's already an error message + const existingError = formElement.parentNode.querySelector( + 'p.prpl-interactive-task-error-message' + ); + + if ( ! existingError ) { + const errorParagraph = document.createElement( 'p' ); + errorParagraph.classList.add( + 'prpl-note', + 'prpl-note-error', + 'prpl-interactive-task-error-message' + ); + errorParagraph.textContent = __( + 'Something went wrong. Please try again.', + 'progress-planner' + ); + formElement.insertAdjacentElement( 'afterend', errorParagraph ); + } +} + +/** + * Clear error message from a form. + * + * @param {string} popoverId The popover ID. + */ +function clearError( popoverId ) { + const formElement = document.querySelector( `#${ popoverId } form` ); + if ( ! formElement ) { + return; + } + + const existingError = formElement.parentNode.querySelector( + 'p.prpl-interactive-task-error-message' + ); + if ( existingError ) { + existingError.remove(); + } +} + +/** + * Submit site settings via WordPress REST API. + * + * @param {Object} options Options object. + * @param {string} options.settingAPIKey The API key for the setting. + * @param {string} options.setting The form field name. + * @param {string} options.popoverId The popover ID. + * @param {Function} options.settingCallbackValue Optional callback to transform the value. + * @return {Promise} Promise resolving to the response. + */ +export async function submitSiteSettings( { + settingAPIKey, + setting, + popoverId, + settingCallbackValue = ( value ) => value, +} ) { + const formElement = document.querySelector( `#${ popoverId } form` ); + if ( ! formElement ) { + throw new Error( 'Form not found' ); + } + + showLoading( formElement ); + clearError( popoverId ); + + try { + const formData = new FormData( formElement ); + const settingValue = settingCallbackValue( formData.get( setting ) ); + + const response = await apiFetch( { + path: '/wp/v2/settings', + method: 'POST', + data: { [ settingAPIKey ]: settingValue }, + } ); + + hideLoading( formElement ); + return response; + } catch ( error ) { + hideLoading( formElement ); + showError( popoverId ); + throw error; + } +} + +/** + * Submit plugin settings via AJAX. + * + * @param {Object} options Options object. + * @param {string} options.setting The setting name. + * @param {string} options.settingPath The setting path (JSON string). + * @param {string} options.popoverId The popover ID. + * @param {string} options.action The AJAX action. + * @param {Function} options.settingCallbackValue Optional callback to transform the value. + * @return {Promise} Promise resolving to the response. + */ +export async function submitPluginSettings( { + setting, + settingPath = false, + popoverId, + action = 'prpl_interactive_task_submit', + settingCallbackValue = ( value ) => value, +} ) { + const formElement = document.querySelector( `#${ popoverId } form` ); + if ( ! formElement ) { + throw new Error( 'Form not found' ); + } + + showLoading( formElement ); + clearError( popoverId ); + + try { + const formData = new FormData( formElement ); + const settingValue = settingCallbackValue( formData.get( setting ) ); + + const ajaxUrl = + window.prplSuggestedTasksConfig?.ajaxUrl || + window.progressPlanner?.ajaxUrl || + '/wp-admin/admin-ajax.php'; + const nonce = + window.prplSuggestedTasksConfig?.nonce || + window.progressPlanner?.nonce || + ''; + + const body = new URLSearchParams( { + action, + _ajax_nonce: nonce, + setting, + value: settingValue, + } ); + + if ( settingPath ) { + body.append( 'setting_path', settingPath ); + } + + const response = await fetch( ajaxUrl, { + method: 'POST', + body, + credentials: 'same-origin', + } ); + + const data = await response.json(); + hideLoading( formElement ); + + if ( data.success !== true ) { + showError( popoverId ); + throw new Error( 'Settings update failed' ); + } + + return data; + } catch ( error ) { + hideLoading( formElement ); + showError( popoverId ); + throw error; + } +} + +/** + * Submit custom callback. + * + * @param {Object} options Options object. + * @param {string} options.popoverId The popover ID. + * @param {Function} options.callback The callback function returning a Promise. + * @return {Promise} Promise resolving to the response. + */ +export async function submitCustom( { popoverId, callback } ) { + const formElement = document.querySelector( `#${ popoverId } form` ); + if ( ! formElement ) { + throw new Error( 'Form not found' ); + } + + showLoading( formElement ); + clearError( popoverId ); + + try { + const response = await callback(); + hideLoading( formElement ); + + if ( response.success !== true ) { + showError( popoverId ); + throw new Error( 'Custom submit failed' ); + } + + return response; + } catch ( error ) { + hideLoading( formElement ); + showError( popoverId ); + throw error; + } +} + +/** + * Delete a WordPress post. + * + * @param {number} postId The post ID. + * @param {string} postType The post type (default: 'posts'). + * @return {Promise} Promise resolving to the response. + */ +export async function deletePost( postId, postType = 'posts' ) { + return apiFetch( { + path: `/wp/v2/${ postType }/${ postId }?force=true`, + method: 'DELETE', + } ); +} + +/** + * Close a popover by ID. + * + * @param {string} popoverId The popover ID. + */ +export function closePopover( popoverId ) { + const popover = document.getElementById( popoverId ); + if ( popover && typeof popover.hidePopover === 'function' ) { + popover.hidePopover(); + } +} diff --git a/assets/src/widgets/SuggestedTasks/hooks/useTasksApi.js b/assets/src/widgets/SuggestedTasks/hooks/useTasksApi.js new file mode 100644 index 0000000000..97fad62d90 --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/hooks/useTasksApi.js @@ -0,0 +1,245 @@ +/** + * Tasks API Hook. + * + * Provides functions for interacting with the tasks REST API. + */ + +import apiFetch from '@wordpress/api-fetch'; + +/** + * Snooze duration map (duration key to days). + */ +const SNOOZE_DURATION_DAYS = { + '1-week': 7, + '2-weeks': 14, + '1-month': 30, + '3-months': 90, + '6-months': 180, + '1-year': 365, + forever: 3650, +}; + +/** + * Build query string from parameters object. + * + * @param {Object} params The parameters object. + * @return {string} The query string. + */ +function buildQueryString( params ) { + const searchParams = new URLSearchParams(); + + Object.entries( params ).forEach( ( [ key, value ] ) => { + if ( Array.isArray( value ) ) { + value.forEach( ( v ) => searchParams.append( key, v ) ); + } else if ( value !== undefined && value !== null && value !== '' ) { + searchParams.append( key, value ); + } + } ); + + return searchParams.toString(); +} + +/** + * Fetch tasks from the API. + * + * @param {Object} options Fetch options. + * @param {string} options.status Task status (publish, pending, future, trash). + * @param {number} options.perPage Number of tasks to fetch. + * @param {string} options.excludeProvider Provider to exclude (e.g., 'user'). + * @param {string} options.provider Provider to include. + * @param {number[]} options.excludeIds Array of post IDs to exclude. + * @return {Promise} Promise resolving to array of tasks. + */ +export async function fetchTasks( { + status = 'publish', + perPage = 100, + excludeProvider, + provider, + excludeIds = [], +} = {} ) { + const params = { + status, + per_page: perPage, + _embed: true, + 'filter[orderby]': 'menu_order', + 'filter[order]': 'ASC', + }; + + if ( excludeProvider ) { + params.exclude_provider = excludeProvider; + } + + if ( provider ) { + params.provider = provider; + } + + if ( excludeIds.length > 0 ) { + params.exclude = excludeIds.join( ',' ); + } + + const query = buildQueryString( params ); + + try { + const response = await apiFetch( { + path: `/wp/v2/prpl_recommendations?${ query }`, + } ); + return response || []; + } catch ( error ) { + console.error( 'Error fetching tasks:', error ); + return []; + } +} + +/** + * Complete a task (change status to trash). + * + * @param {number} postId The post ID. + * @return {Promise} Promise resolving to the updated task. + */ +export async function completeTask( postId ) { + return apiFetch( { + path: `/wp/v2/prpl_recommendations/${ postId }`, + method: 'POST', + data: { + status: 'trash', + }, + } ); +} + +/** + * Snooze a task (change status to future with scheduled date). + * + * @param {number} postId The post ID. + * @param {string} duration The snooze duration key. + * @return {Promise} Promise resolving to the updated task. + */ +export async function snoozeTask( postId, duration ) { + const durationDays = SNOOZE_DURATION_DAYS[ duration ] || 7; + + // Calculate the future date. + const futureDate = new Date( + Date.now() + durationDays * 24 * 60 * 60 * 1000 + ); + const dateString = futureDate.toISOString().split( '.' )[ 0 ]; + + return apiFetch( { + path: `/wp/v2/prpl_recommendations/${ postId }`, + method: 'POST', + data: { + status: 'future', + date: dateString, + date_gmt: dateString, + }, + } ); +} + +/** + * Delete a task permanently. + * + * @param {number} postId The post ID. + * @return {Promise} Promise resolving to the deleted task. + */ +export async function deleteTask( postId ) { + return apiFetch( { + path: `/wp/v2/prpl_recommendations/${ postId }?force=true`, + method: 'DELETE', + } ); +} + +/** + * Update a task. + * + * @param {number} postId The post ID. + * @param {Object} data The data to update. + * @return {Promise} Promise resolving to the updated task. + */ +export async function updateTask( postId, data ) { + return apiFetch( { + path: `/wp/v2/prpl_recommendations/${ postId }`, + method: 'POST', + data, + } ); +} + +/** + * Send a task action for analytics. + * + * @param {number} postId The post ID. + * @param {string} actionType The action type (complete, delete, pending). + * @return {Promise} Promise resolving to the response. + */ +export async function sendTaskAction( postId, actionType ) { + const nonce = window.prplSuggestedTasksConfig?.nonce || ''; + const ajaxUrl = + window.prplSuggestedTasksConfig?.ajaxUrl || '/wp-admin/admin-ajax.php'; + + const formData = new FormData(); + formData.append( 'action', 'progress_planner_suggested_task_action' ); + formData.append( 'post_id', postId ); + formData.append( 'action_type', actionType ); + formData.append( 'nonce', nonce ); + + try { + const response = await fetch( ajaxUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin', + } ); + return response.json(); + } catch ( error ) { + console.error( 'Error sending task action:', error ); + return null; + } +} + +/** + * Submit an interactive task form. + * + * @param {Object} options Submit options. + * @param {string} options.setting The setting name. + * @param {string} options.value The value to set. + * @param {Array} options.settingPath The path to the setting (for nested values). + * @return {Promise} Promise resolving to the response. + */ +export async function submitInteractiveTask( { + setting, + value, + settingPath = [], +} ) { + const nonce = window.prplSuggestedTasksConfig?.nonce || ''; + const ajaxUrl = + window.prplSuggestedTasksConfig?.ajaxUrl || '/wp-admin/admin-ajax.php'; + + const formData = new FormData(); + formData.append( 'action', 'prpl_interactive_task_submit' ); + formData.append( 'setting', setting ); + formData.append( 'value', value ); + formData.append( 'setting_path', JSON.stringify( settingPath ) ); + formData.append( 'nonce', nonce ); + + try { + const response = await fetch( ajaxUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin', + } ); + return response.json(); + } catch ( error ) { + console.error( 'Error submitting interactive task:', error ); + throw error; + } +} + +/** + * Update WordPress site settings via REST API. + * + * @param {Object} settings Key-value pairs of settings to update. + * @return {Promise} Promise resolving to the updated settings. + */ +export async function updateSiteSettings( settings ) { + return apiFetch( { + path: '/wp/v2/settings', + method: 'POST', + data: settings, + } ); +} diff --git a/assets/src/widgets/SuggestedTasks/index.js b/assets/src/widgets/SuggestedTasks/index.js new file mode 100644 index 0000000000..db6dfb9e96 --- /dev/null +++ b/assets/src/widgets/SuggestedTasks/index.js @@ -0,0 +1,411 @@ +/** + * Suggested Tasks Widget Component. + * + * Displays a list of suggested tasks (recommendations) for improving the site. + */ + +import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import TaskItem from './TaskItem'; +import PopoverManager from './PopoverManager'; +import { + fetchTasks, + completeTask, + snoozeTask, + deleteTask, + updateTask, + sendTaskAction, +} from './hooks/useTasksApi'; + +/** + * Suggested Tasks widget component. + * + * @return {JSX.Element} The widget component. + */ +export default function SuggestedTasks() { + const [ tasks, setTasks ] = useState( [] ); + const [ isLoading, setIsLoading ] = useState( true ); + const [ showAll, setShowAll ] = useState( + window.prplSuggestedTasksConfig?.showAll || false + ); + const [ celebratingTaskIds, setCelebratingTaskIds ] = useState( new Set() ); + const listRef = useRef( null ); + const injectedTaskIdsRef = useRef( new Set() ); + + /** + * Load tasks on component mount. + */ + useEffect( () => { + const loadTasks = async () => { + try { + const perPage = showAll + ? 100 + : window.prplSuggestedTasksConfig?.perPage || 5; + + // Fetch published tasks (excluding user tasks). + const publishedTasks = await fetchTasks( { + status: 'publish', + perPage, + excludeProvider: 'user', + } ); + + // Track injected task IDs. + publishedTasks.forEach( ( task ) => { + injectedTaskIdsRef.current.add( task.id ); + } ); + + setTasks( publishedTasks ); + setIsLoading( false ); + + // Check for pending celebration tasks. + if ( ! window.prplSuggestedTasksConfig?.delayCelebration ) { + const pendingTasks = await fetchTasks( { + status: 'pending', + perPage, + excludeProvider: 'user', + } ); + + if ( pendingTasks.length > 0 ) { + // Add pending tasks to the list. + setTasks( ( prev ) => [ ...prev, ...pendingTasks ] ); + + // Track pending task IDs. + pendingTasks.forEach( ( task ) => { + injectedTaskIdsRef.current.add( task.id ); + } ); + + // Trash the pending tasks in the background. + pendingTasks.forEach( ( task ) => { + completeTask( task.id ).catch( () => {} ); + } ); + + // Trigger celebration after 3 seconds. + setTimeout( () => { + // Add celebrating class to pending tasks. + const pendingIds = new Set( + pendingTasks.map( ( t ) => t.id ) + ); + setCelebratingTaskIds( pendingIds ); + + // Dispatch celebration event. + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks' ) + ); + + // Remove celebrated tasks after animation. + setTimeout( () => { + setTasks( ( prev ) => + prev.filter( + ( t ) => ! pendingIds.has( t.id ) + ) + ); + setCelebratingTaskIds( new Set() ); + + // Trigger grid resize. + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + }, 2000 ); + }, 3000 ); + } + } + + // Trigger grid resize. + setTimeout( () => { + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + }, 100 ); + } catch { + setIsLoading( false ); + } + }; + + loadTasks(); + }, [ showAll ] ); + + /** + * Handle task completion. + * + * @param {number} postId The post ID. + * @param {Object} task The task object. + */ + const handleComplete = useCallback( async ( postId, task ) => { + try { + // Add to celebrating set. + setCelebratingTaskIds( ( prev ) => new Set( [ ...prev, postId ] ) ); + + // Update task status via API. + await completeTask( postId ); + + // Send analytics action. + sendTaskAction( postId, 'complete' ); + + // Get task points. + const eventPoints = parseInt( task.prpl_points ) || 0; + + // Update Ravi gauge if task has points. + if ( + eventPoints > 0 && + typeof window.prplUpdateRaviGauge === 'function' + ) { + window.prplUpdateRaviGauge( eventPoints ); + } + + // Dispatch celebration event for confetti. + if ( eventPoints > 0 && listRef.current ) { + const taskElement = listRef.current.querySelector( + `[data-post-id="${ postId }"]` + ); + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks', { + detail: { element: taskElement }, + } ) + ); + } + + // Remove task after animation delay. + setTimeout( async () => { + setTasks( ( prev ) => prev.filter( ( t ) => t.id !== postId ) ); + setCelebratingTaskIds( ( prev ) => { + const next = new Set( prev ); + next.delete( postId ); + return next; + } ); + + // Fetch replacement task. + const replacementTasks = await fetchTasks( { + status: 'publish', + perPage: 1, + excludeProvider: 'user', + excludeIds: Array.from( injectedTaskIdsRef.current ), + } ); + + if ( replacementTasks.length > 0 ) { + setTasks( ( prev ) => [ ...prev, replacementTasks[ 0 ] ] ); + injectedTaskIdsRef.current.add( replacementTasks[ 0 ].id ); + } + + // Trigger grid resize. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, 2000 ); + } catch { + // Remove from celebrating on error. + setCelebratingTaskIds( ( prev ) => { + const next = new Set( prev ); + next.delete( postId ); + return next; + } ); + } + }, [] ); + + /** + * Handle task snooze. + * + * @param {number} postId The post ID. + * @param {string} duration The snooze duration. + */ + const handleSnooze = useCallback( async ( postId, duration ) => { + try { + await snoozeTask( postId, duration ); + + // Remove task from list. + setTasks( ( prev ) => prev.filter( ( t ) => t.id !== postId ) ); + + // Fetch replacement task. + const replacementTasks = await fetchTasks( { + status: 'publish', + perPage: 1, + excludeProvider: 'user', + excludeIds: Array.from( injectedTaskIdsRef.current ), + } ); + + if ( replacementTasks.length > 0 ) { + setTasks( ( prev ) => [ ...prev, replacementTasks[ 0 ] ] ); + injectedTaskIdsRef.current.add( replacementTasks[ 0 ].id ); + } + + // Trigger grid resize. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + } catch { + // Error handled silently. + } + }, [] ); + + /** + * Handle task deletion. + * + * @param {number} postId The post ID. + */ + const handleDelete = useCallback( async ( postId ) => { + try { + await deleteTask( postId ); + + // Send analytics action. + sendTaskAction( postId, 'delete' ); + + // Remove task from list. + setTasks( ( prev ) => prev.filter( ( t ) => t.id !== postId ) ); + + // Fetch replacement task. + const replacementTasks = await fetchTasks( { + status: 'publish', + perPage: 1, + excludeProvider: 'user', + excludeIds: Array.from( injectedTaskIdsRef.current ), + } ); + + if ( replacementTasks.length > 0 ) { + setTasks( ( prev ) => [ ...prev, replacementTasks[ 0 ] ] ); + injectedTaskIdsRef.current.add( replacementTasks[ 0 ].id ); + } + + // Trigger grid resize. + setTimeout( () => { + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, 500 ); + } catch { + // Error handled silently. + } + }, [] ); + + /** + * Handle task title change (for user tasks). + * + * @param {number} postId The post ID. + * @param {string} newTitle The new title. + */ + const handleTitleChange = useCallback( async ( postId, newTitle ) => { + try { + await updateTask( postId, { title: newTitle } ); + } catch { + // Error handled silently. + } + }, [] ); + + /** + * Handle task move (for user tasks). + * + * @param {number} postId The post ID. + * @param {string} direction The direction ('up' or 'down'). + */ + const handleMove = useCallback( + async ( postId, direction ) => { + const currentIndex = tasks.findIndex( ( t ) => t.id === postId ); + if ( currentIndex === -1 ) { + return; + } + + const newIndex = + direction === 'up' ? currentIndex - 1 : currentIndex + 1; + if ( newIndex < 0 || newIndex >= tasks.length ) { + return; + } + + // Reorder tasks in state. + const newTasks = [ ...tasks ]; + const [ movedTask ] = newTasks.splice( currentIndex, 1 ); + newTasks.splice( newIndex, 0, movedTask ); + + setTasks( newTasks ); + + // Update menu_order for all affected tasks. + newTasks.forEach( ( task, index ) => { + updateTask( task.id, { menu_order: index } ).catch( () => {} ); + } ); + }, + [ tasks ] + ); + + /** + * Handle show all/fewer toggle. + */ + const handleToggleShowAll = useCallback( async () => { + const newShowAll = ! showAll; + setShowAll( newShowAll ); + setIsLoading( true ); + + // Clear tracking. + injectedTaskIdsRef.current.clear(); + + // Update URL. + const url = new URL( window.location ); + if ( newShowAll ) { + url.searchParams.set( 'prpl_show_all_recommendations', '' ); + } else { + url.searchParams.delete( 'prpl_show_all_recommendations' ); + } + window.history.pushState( {}, '', url ); + }, [ showAll ] ); + + // Show loading state. + if ( isLoading ) { + return ( +

    + { __( 'Loading tasks…', 'progress-planner' ) } +

    + ); + } + + // Show empty state. + if ( tasks.length === 0 ) { + return ( + <> +
      +

      + { __( + 'You have completed all recommended tasks.', + 'progress-planner' + ) } +
      + { __( + 'Check back later for new tasks!', + 'progress-planner' + ) } +

      + + ); + } + + return ( + <> + +
        +
          + { tasks.map( ( task ) => ( + + ) ) } +
        +

        + +

        + + ); +} diff --git a/assets/src/widgets/TodoWidget/index.js b/assets/src/widgets/TodoWidget/index.js new file mode 100644 index 0000000000..0f41e9902e --- /dev/null +++ b/assets/src/widgets/TodoWidget/index.js @@ -0,0 +1,734 @@ +/** + * Todo Widget Component. + * + * Displays a list of user-created todo tasks. + */ + +import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Fetch user tasks from API. + * + * @param {Object} options Fetch options. + * @param {Array} options.status Task status(es). + * @return {Promise} Promise resolving to tasks array. + */ +async function fetchUserTasks( { status = [ 'publish', 'trash' ] } = {} ) { + const statusParam = Array.isArray( status ) + ? status.map( ( s ) => `status[]=${ s }` ).join( '&' ) + : `status=${ status }`; + + return apiFetch( { + path: `/wp/v2/prpl_recommendations?${ statusParam }&provider=user&per_page=100&_embed=true&filter[orderby]=menu_order&filter[order]=ASC`, + } ); +} + +/** + * Create a new task. + * + * @param {Object} data Task data. + * @param {string} data.title Task title. + * @param {number} data.order Menu order. + * @return {Promise} Promise resolving to created task. + */ +async function createTask( { title, order } ) { + return apiFetch( { + path: '/wp/v2/prpl_recommendations', + method: 'POST', + data: { + title, + status: 'publish', + menu_order: order, + prpl_recommendations_provider: + window.prplTodoConfig?.userProviderId, + }, + } ); +} + +/** + * Update a task. + * + * @param {number} postId Task post ID. + * @param {Object} data Data to update. + * @return {Promise} Promise resolving to updated task. + */ +async function updateTask( postId, data ) { + return apiFetch( { + path: `/wp/v2/prpl_recommendations/${ postId }`, + method: 'POST', + data, + } ); +} + +/** + * Delete a task. + * + * @param {number} postId Task post ID. + * @return {Promise} Promise resolving to deletion response. + */ +async function deleteTask( postId ) { + return apiFetch( { + path: `/wp/v2/prpl_recommendations/${ postId }?force=true`, + method: 'DELETE', + } ); +} + +/** + * Todo Item Component. + * + * @param {Object} props Component props. + * @param {Object} props.task The task object. + * @param {boolean} props.isGolden Whether this is the golden task. + * @param {boolean} props.isCompleted Whether the task is completed. + * @param {Function} props.onToggle Callback for toggling completion. + * @param {Function} props.onDelete Callback for deleting task. + * @param {Function} props.onMove Callback for moving task. + * @param {Function} props.onTitleChange Callback for title change. + * @return {JSX.Element} The todo item component. + */ +function TodoItem( { + task, + isGolden, + isCompleted, + onToggle, + onDelete, + onMove, + onTitleChange, +} ) { + const titleRef = useRef( null ); + const debounceRef = useRef( null ); + + const handleTitleInput = useCallback( () => { + if ( debounceRef.current ) { + clearTimeout( debounceRef.current ); + } + debounceRef.current = setTimeout( () => { + if ( titleRef.current ) { + const newTitle = titleRef.current.textContent.replace( + /\n/g, + '' + ); + onTitleChange( task.id, newTitle ); + } + }, 300 ); + }, [ task.id, onTitleChange ] ); + + const handleKeyDown = useCallback( ( e ) => { + if ( e.key === 'Enter' ) { + e.preventDefault(); + e.target.blur(); + } + }, [] ); + + return ( +
      • +
        + { /* eslint-disable-next-line jsx-a11y/label-has-associated-control */ } + +
        + +
        +

        + + { task.title?.rendered || task.title } + +

        +
        + +
        + { ( task.prpl_points || 0 ) > 0 && ( + + +{ task.prpl_points } + + ) } + +
        + + { ! isCompleted && ( +
        + + +
        + ) } +
      • + ); +} + +/** + * Todo Widget main component. + * + * @return {JSX.Element} The widget component. + */ +export default function TodoWidget() { + const [ pendingTasks, setPendingTasks ] = useState( [] ); + const [ completedTasks, setCompletedTasks ] = useState( [] ); + const [ isLoading, setIsLoading ] = useState( true ); + const [ newTaskTitle, setNewTaskTitle ] = useState( '' ); + const [ showDeletePopover, setShowDeletePopover ] = useState( false ); + const inputRef = useRef( null ); + + /** + * Load tasks on mount. + */ + useEffect( () => { + const loadTasks = async () => { + try { + const tasks = await fetchUserTasks(); + const pending = tasks.filter( ( t ) => t.status === 'publish' ); + const completed = tasks.filter( ( t ) => t.status === 'trash' ); + + // Sort by menu_order + pending.sort( + ( a, b ) => ( a.menu_order || 0 ) - ( b.menu_order || 0 ) + ); + completed.sort( + ( a, b ) => ( a.menu_order || 0 ) - ( b.menu_order || 0 ) + ); + + setPendingTasks( pending ); + setCompletedTasks( completed ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error loading tasks:', error ); + } finally { + setIsLoading( false ); + // Trigger grid resize + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + } + }; + + loadTasks(); + }, [] ); + + /** + * Create a new task. + */ + const handleCreateTask = useCallback( + async ( e ) => { + e.preventDefault(); + + if ( ! newTaskTitle.trim() ) { + return; + } + + try { + const highestOrder = pendingTasks.reduce( + ( max, t ) => Math.max( max, t.menu_order || 0 ), + 0 + ); + + const newTask = await createTask( { + title: newTaskTitle, + order: highestOrder + 1, + } ); + + setPendingTasks( ( prev ) => [ ...prev, newTask ] ); + setNewTaskTitle( '' ); + + // Announce to screen readers + if ( window.wp?.a11y?.speak ) { + window.wp.a11y.speak( + __( 'Task added successfully', 'progress-planner' ), + 'polite' + ); + } + + // Focus input + inputRef.current?.focus(); + + // Trigger grid resize + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error creating task:', error ); + } + }, + [ newTaskTitle, pendingTasks ] + ); + + /** + * Toggle task completion. + */ + const handleToggle = useCallback( + async ( taskId ) => { + const task = + pendingTasks.find( ( t ) => t.id === taskId ) || + completedTasks.find( ( t ) => t.id === taskId ); + + if ( ! task ) { + return; + } + + const isCurrentlyCompleted = task.status === 'trash'; + const newStatus = isCurrentlyCompleted ? 'publish' : 'trash'; + + try { + await updateTask( taskId, { status: newStatus } ); + + if ( isCurrentlyCompleted ) { + // Move from completed to pending + setCompletedTasks( ( prev ) => + prev.filter( ( t ) => t.id !== taskId ) + ); + setPendingTasks( ( prev ) => [ + ...prev, + { ...task, status: 'publish' }, + ] ); + } else { + // Move from pending to completed + setPendingTasks( ( prev ) => + prev.filter( ( t ) => t.id !== taskId ) + ); + setCompletedTasks( ( prev ) => [ + ...prev, + { ...task, status: 'trash' }, + ] ); + + // Trigger celebration if has points + if ( task.prpl_points > 0 ) { + if ( + typeof window.prplUpdateRaviGauge === 'function' + ) { + window.prplUpdateRaviGauge( task.prpl_points ); + } + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks', { + detail: {}, + } ) + ); + } + } + + // Trigger grid resize + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error toggling task:', error ); + } + }, + [ pendingTasks, completedTasks ] + ); + + /** + * Delete a task. + */ + const handleDelete = useCallback( async ( taskId ) => { + try { + await deleteTask( taskId ); + setPendingTasks( ( prev ) => + prev.filter( ( t ) => t.id !== taskId ) + ); + setCompletedTasks( ( prev ) => + prev.filter( ( t ) => t.id !== taskId ) + ); + + // Trigger grid resize + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error deleting task:', error ); + } + }, [] ); + + /** + * Move a task up or down. + */ + const handleMove = useCallback( + async ( taskId, direction ) => { + const index = pendingTasks.findIndex( ( t ) => t.id === taskId ); + if ( index === -1 ) { + return; + } + + const newIndex = direction === 'up' ? index - 1 : index + 1; + if ( newIndex < 0 || newIndex >= pendingTasks.length ) { + return; + } + + // Swap tasks + const newTasks = [ ...pendingTasks ]; + [ newTasks[ index ], newTasks[ newIndex ] ] = [ + newTasks[ newIndex ], + newTasks[ index ], + ]; + + // Update menu_order for all tasks + const updates = newTasks.map( ( t, i ) => ( { + ...t, + menu_order: i, + } ) ); + + setPendingTasks( updates ); + + // Save order changes to server + try { + await Promise.all( + updates.map( ( t ) => + updateTask( t.id, { menu_order: t.menu_order } ) + ) + ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error saving task order:', error ); + } + + // Trigger grid resize + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, + [ pendingTasks ] + ); + + /** + * Update task title. + */ + const handleTitleChange = useCallback( async ( taskId, newTitle ) => { + try { + await updateTask( taskId, { title: newTitle } ); + + // Update local state + setPendingTasks( ( prev ) => + prev.map( ( t ) => + t.id === taskId + ? { ...t, title: { rendered: newTitle } } + : t + ) + ); + setCompletedTasks( ( prev ) => + prev.map( ( t ) => + t.id === taskId + ? { ...t, title: { rendered: newTitle } } + : t + ) + ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error updating task title:', error ); + } + }, [] ); + + /** + * Delete all completed tasks. + */ + const handleDeleteAllCompleted = useCallback( async () => { + try { + await Promise.all( + completedTasks.map( ( t ) => deleteTask( t.id ) ) + ); + setCompletedTasks( [] ); + setShowDeletePopover( false ); + + // Announce to screen readers + if ( window.wp?.a11y?.speak ) { + window.wp.a11y.speak( + __( 'All completed tasks deleted', 'progress-planner' ), + 'assertive' + ); + } + + // Trigger grid resize + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error deleting completed tasks:', error ); + } + }, [ completedTasks ] ); + + if ( isLoading ) { + return ( +

        + { __( 'Loading items…', 'progress-planner' ) } +

        + ); + } + + return ( + <> +
        + +
          + { pendingTasks.map( ( task, index ) => ( + 0 } + isCompleted={ false } + onToggle={ handleToggle } + onDelete={ handleDelete } + onMove={ handleMove } + onTitleChange={ handleTitleChange } + /> + ) ) } +
        + +
        + setNewTaskTitle( e.target.value ) } + /> + +
        + + { completedTasks.length > 0 && ( +
        + + { __( 'Completed tasks', 'progress-planner' ) } + + + + + + +
        + +
        +
          + { completedTasks.map( ( task ) => ( + + ) ) } +
        +
        + ) } + + { showDeletePopover && ( +
        +
        + + { __( + 'Are you sure you want to delete all completed tasks? This action cannot be undone.', + 'progress-planner' + ) } + +
        + +
        + + +
        +
        + ) } + { showDeletePopover && ( +
        setShowDeletePopover( false ) } + onKeyDown={ ( e ) => { + if ( e.key === 'Enter' || e.key === ' ' ) { + setShowDeletePopover( false ); + } + } } + /> + ) } + + ); +} diff --git a/build/suggested-tasks.asset.php b/build/suggested-tasks.asset.php new file mode 100644 index 0000000000..24cf00715a --- /dev/null +++ b/build/suggested-tasks.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'ba74c51066ae5072eacb'); diff --git a/build/suggested-tasks.js b/build/suggested-tasks.js new file mode 100644 index 0000000000..16865d1e64 --- /dev/null +++ b/build/suggested-tasks.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var n in s)e.o(s,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:s[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,s=window.wp.i18n,n=window.ReactJSXRuntime;function a({task:e,isUserTask:a,onComplete:r,onSnooze:o,onDelete:i}){const l=(0,t.useRef)(null);(0,t.useEffect)(()=>{if(!l.current)return;const t=l.current,s=t.querySelectorAll('[data-action="complete"]');s.forEach(t=>{t.addEventListener("click",t=>{t.preventDefault(),r(e.id,e)})});const n=t.querySelectorAll('.prpl-snooze-duration-radio-group input[type="radio"]');return n.forEach(t=>{t.addEventListener("change",()=>{o(e.id,t.value)})}),t.querySelectorAll('a[onclick*="showPopover"]').forEach(e=>{const t=e.getAttribute("onclick"),s=t?.match(/getElementById\(['"]([^'"]+)['"]\)/);if(s){const t=s[1];e.removeAttribute("onclick"),e.addEventListener("click",e=>{e.preventDefault();const s=document.getElementById(t);s&&"function"==typeof s.showPopover&&s.showPopover()})}}),t.querySelectorAll(".prpl-suggested-task-button.trash").forEach(t=>{t.addEventListener("click",t=>{t.preventDefault(),i(e.id)})}),()=>{s.forEach(e=>{e.replaceWith(e.cloneNode(!0))}),n.forEach(e=>{e.replaceWith(e.cloneNode(!0))})}},[e,r,o,i]);const c=e.prpl_task_actions||[];return 0!==c.length||a?(0,n.jsxs)("div",{className:"tooltip-actions",ref:l,children:[c.map((e,t)=>(0,n.jsx)("span",{className:"tooltip-action",dangerouslySetInnerHTML:{__html:e}},t)),a&&(0,n.jsx)("span",{className:"tooltip-action",children:(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button trash","data-post-id":e.id,title:(0,s.__)("Delete","progress-planner"),onClick:()=>i(e.id),children:[(0,n.jsx)("span",{className:"prpl-tooltip-action-text",children:(0,s.__)("Delete","progress-planner")}),(0,n.jsxs)("span",{className:"screen-reader-text",children:[(0,s.__)("Delete","progress-planner"),":"," ",e.title?.rendered||e.title]})]})})]}):(0,n.jsx)("div",{className:"tooltip-actions"})}function r(){return(0,n.jsx)("span",{style:{width:"0.75rem",height:"100%",display:"flex",alignItems:"center",justifyContent:"center"},children:(0,n.jsx)("svg",{role:"img","aria-hidden":"true",focusable:"false",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 17",children:(0,n.jsx)("path",{fill:"#6b7280",d:"M19.92 8.12c-.05-.12-.12-.23-.22-.33L12.21.29A.996.996 0 1 0 10.8 1.7l5.79 5.79H1c-.55 0-1 .45-1 1s.45 1 1 1h15.59l-5.79 5.79a.996.996 0 0 0 .71 1.7c.26 0 .51-.1.71-.29l7.5-7.5c.1-.1.17-.21.22-.33.05-.12.07-.24.08-.38 0-.14-.03-.27-.08-.38Z"})})})}function o(){return(0,n.jsx)("svg",{role:"img","aria-hidden":"true",focusable:"false",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",children:(0,n.jsx)("path",{fill:"#9ca3af",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Zm-17.98-3h17.97c1.9 0 3.51-1.48 3.65-3.38l2.34-30.46c-2.15-.3-4.33-.53-6.48-.7h-.03c-5.62-.43-11.32-.43-16.95 0h-.03c-2.15.17-4.33.4-6.48.7l2.34 30.46c.15 1.9 1.75 3.38 3.65 3.38ZM24 7.01c2.37 0 4.74.07 7.11.22v-.49c0-1.93-1.47-3.49-3.34-3.55-2.5-.08-5.03-.08-7.52 0-1.88.06-3.34 1.62-3.34 3.55v.49c2.36-.15 4.73-.22 7.11-.22Zm5.49 32.26h-.06c-.83-.03-1.47-.73-1.44-1.56l.79-20.65c.03-.83.75-1.45 1.56-1.44.83.03 1.47.73 1.44 1.56l-.79 20.65c-.03.81-.7 1.44-1.5 1.44Zm-10.98 0c-.8 0-1.47-.63-1.5-1.44l-.79-20.65c-.03-.83.61-1.52 1.44-1.56.84 0 1.52.61 1.56 1.44l.79 20.65c.03.83-.61 1.52-1.44 1.56h-.06Z"})})}function i({task:e,isUserTask:i,isCelebrating:l,onComplete:c,onSnooze:p,onDelete:d,onMove:u,onTitleChange:g}){const m=(0,t.useRef)(null),h=(0,t.useRef)(null),w="trash"===e.status||"pending"===e.status,y=(0,t.useCallback)(()=>{c(e.id,e)},[e,c]),f=(0,t.useCallback)(e=>{if("Enter"===e.key)return e.preventDefault(),e.stopPropagation(),e.target.blur(),!1},[]),v=(0,t.useCallback)(()=>{clearTimeout(h.current),h.current=setTimeout(()=>{if(m.current){const t=m.current.textContent.replace(/\n/g,"");g(e.id,t)}},300)},[e.id,g]),b=(0,t.useCallback)(()=>{u(e.id,"up")},[e.id,u]),_=(0,t.useCallback)(()=>{u(e.id,"down")},[e.id,u]),k=(0,t.useCallback)(()=>{d(e.id)},[e.id,d]),S=e.slug||e.id,x=e.prpl_provider?.slug||"",C=["prpl-suggested-task",l?"prpl-suggested-task-celebrated":""].filter(Boolean).join(" ");return(0,n.jsxs)("li",{className:C,"data-task-id":S,"data-post-id":e.id,"data-task-action":"pending"===e.status||l?"celebrate":"","data-task-provider-id":x,"data-task-points":e.prpl_points||0,"data-task-order":e.menu_order||0,children:[(0,n.jsx)("div",{className:"prpl-suggested-task-checkbox-wrapper",children:i?(0,n.jsxs)("label",{children:[(0,n.jsx)("input",{type:"checkbox",className:"prpl-suggested-task-checkbox",onChange:y,style:{margin:0},checked:w,disabled:l}),(0,n.jsxs)("span",{className:"screen-reader-text",children:[e.title?.rendered||e.title,":"," ",(0,s.__)("Mark as complete","progress-planner")]})]}):(0,n.jsx)(r,{})}),(0,n.jsx)("div",{className:"prpl-suggested-task-title-wrapper",children:(0,n.jsx)("h3",{className:"prpl-task-title",children:i?(0,n.jsx)("span",{ref:m,contentEditable:"plaintext-only",role:"textbox",tabIndex:0,"aria-label":(0,s.__)("Edit task title","progress-planner"),"aria-multiline":"false",onKeyDown:f,onInput:v,suppressContentEditableWarning:!0,dangerouslySetInnerHTML:{__html:e.title?.rendered||e.title}}):(0,n.jsx)("span",{dangerouslySetInnerHTML:{__html:e.title?.rendered||e.title}})})}),(0,n.jsxs)("div",{className:"prpl-suggested-task-points-wrapper",children:[e.prpl_points>0&&(0,n.jsxs)("span",{className:"prpl-suggested-task-points",children:["+",e.prpl_points]}),i&&(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button trash","data-post-id":e.id,title:(0,s.__)("Delete","progress-planner"),onClick:k,children:[(0,n.jsx)(o,{}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Delete","progress-planner")})]})]}),i&&(0,n.jsx)("div",{className:"tooltip-actions prpl-move-buttons-wrapper",children:(0,n.jsxs)("span",{className:"prpl-move-buttons",children:[(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button move-up","data-task-id":S,"data-task-title":e.title?.rendered||e.title,"data-action":"move-up","data-target":"move-up",title:(0,s.__)("Move up","progress-planner"),onClick:b,children:[(0,n.jsx)("span",{className:"dashicons dashicons-arrow-up-alt2"}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Move up","progress-planner")})]}),(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button move-down","data-task-id":S,"data-task-title":e.title?.rendered||e.title,"data-action":"move-down","data-target":"move-down",title:(0,s.__)("Move down","progress-planner"),onClick:_,children:[(0,n.jsx)("span",{className:"dashicons dashicons-arrow-down-alt2"}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Move down","progress-planner")})]})]})}),(0,n.jsx)("div",{className:"prpl-suggested-task-actions-wrapper",children:(0,n.jsx)(a,{task:e,isUserTask:i,onComplete:c,onSnooze:p,onDelete:d})})]})}const l=window.wp.apiFetch;var c=e.n(l);function p(e){let t=e.querySelector('button[type="submit"]');if(t||(t=e.querySelector('button[data-action="completeTask"]')),t){t.disabled=!0;const e=document.createElement("span");e.classList.add("prpl-spinner"),e.innerHTML='',t.after(e)}}function d(e){let t=e.querySelector('button[type="submit"]');t||(t=e.querySelector('button[data-action="completeTask"]')),t&&(t.disabled=!1);const s=e.querySelector("span.prpl-spinner");s&&s.remove()}function u(e){const t=document.querySelector(`#${e} form`);if(t&&!t.parentNode.querySelector("p.prpl-interactive-task-error-message")){const e=document.createElement("p");e.classList.add("prpl-note","prpl-note-error","prpl-interactive-task-error-message"),e.textContent=(0,s.__)("Something went wrong. Please try again.","progress-planner"),t.insertAdjacentElement("afterend",e)}}function g(e){const t=document.querySelector(`#${e} form`);if(!t)return;const s=t.parentNode.querySelector("p.prpl-interactive-task-error-message");s&&s.remove()}async function m({setting:e,settingPath:t=!1,popoverId:s,action:n="prpl_interactive_task_submit",settingCallbackValue:a=e=>e}){const r=document.querySelector(`#${s} form`);if(!r)throw new Error("Form not found");p(r),g(s);try{const o=a(new FormData(r).get(e)),i=window.prplSuggestedTasksConfig?.ajaxUrl||window.progressPlanner?.ajaxUrl||"/wp-admin/admin-ajax.php",l=window.prplSuggestedTasksConfig?.nonce||window.progressPlanner?.nonce||"",c=new URLSearchParams({action:n,_ajax_nonce:l,setting:e,value:o});t&&c.append("setting_path",t);const p=await fetch(i,{method:"POST",body:c,credentials:"same-origin"}),g=await p.json();if(d(r),!0!==g.success)throw u(s),new Error("Settings update failed");return g}catch(e){throw d(r),u(s),e}}async function h(e,t="posts"){return c()({path:`/wp/v2/${t}/${e}?force=true`,method:"DELETE"})}const w={"core-blogdescription":{type:"siteSettings",settingAPIKey:"description",setting:"blogdescription"},"disable-comments":{type:"siteSettings",settingAPIKey:"default_comment_status",setting:"default_comment_status",settingCallbackValue:()=>"closed"},"disable-comment-pagination":{type:"siteSettings",settingAPIKey:"page_comments",setting:"page_comments",settingCallbackValue:()=>!1},"select-locale":{type:"siteSettings",settingAPIKey:"WPLANG",setting:"WPLANG"},"select-timezone":{type:"siteSettings",settingAPIKey:"timezone_string",setting:"timezone_string"},"search-engine-visibility":{type:"pluginSettings",setting:"blog_public",action:"prpl_interactive_task_submit",settingCallbackValue:()=>"1"},"set-date-format":{type:"siteSettings",settingAPIKey:"date_format",setting:"date_format"},"core-permalink-structure":{type:"siteSettings",settingAPIKey:"permalink_structure",setting:"permalink_structure"},"rename-uncategorized-category":{type:"customSubmit"},"hello-world":{type:"customSubmit"},"sample-page":{type:"customSubmit"},"yoast-author-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-author"]),settingCallbackValue:()=>!0},"yoast-date-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-date"]),settingCallbackValue:()=>!0},"yoast-format-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-post_format"]),settingCallbackValue:()=>!0},"yoast-media-pages":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-attachment"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-emoji-scripts":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_emoji_scripts"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-feed-authors":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_feed_authors"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-feed-global-comments":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_feed_global_comments"]),settingCallbackValue:()=>!0},"yoast-organization-logo":{type:"customSubmit"},"aioseo-author-archive":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["archives","author","show"]),settingCallbackValue:()=>!1},"aioseo-date-archive":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["archives","date","show"]),settingCallbackValue:()=>!1},"aioseo-media-pages":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["postTypes","attachment","show"]),settingCallbackValue:()=>!1},"aioseo-crawl-settings-feed-authors":{type:"pluginSettings",setting:"aioseo_options_rss_content",settingPath:JSON.stringify(["authorFeed"]),settingCallbackValue:()=>!1},"aioseo-crawl-settings-feed-comments":{type:"pluginSettings",setting:"aioseo_options_rss_content",settingPath:JSON.stringify(["commentFeed"]),settingCallbackValue:()=>!1},"core-siteicon":{type:"customSubmit"},"update-term-description":{type:"customSubmit"},"remove-terms-without-posts":{type:"customSubmit"}};function y({tasks:e,onComplete:s}){const n=(0,t.useCallback)(async(e,t)=>{switch(e){case"hello-world":{const e=window.helloWorldData?.postId;return e&&await h(e,"posts"),{success:!0}}case"sample-page":{const e=window.samplePageData?.postId;return e&&await h(e,"pages"),{success:!0}}case"rename-uncategorized-category":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("category_name"),s=window.renameUncategorizedCategoryData?.termId;s&&t&&await c()({path:`/wp/v2/categories/${s}`,method:"POST",data:{name:t}})}return{success:!0}}case"core-siteicon":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("site_icon");t&&await c()({path:"/wp/v2/settings",method:"POST",data:{site_icon:parseInt(t)}})}return{success:!0}}case"yoast-organization-logo":{const e=document.querySelector(`#${t} form`);if(e){const s=new FormData(e).get("company_logo_id");s&&await m({setting:"wpseo",settingPath:JSON.stringify(["company_logo_id"]),popoverId:t,settingCallbackValue:()=>parseInt(s)})}return{success:!0}}case"update-term-description":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("description"),s=window.updateTermDescriptionData?.termId,n=window.updateTermDescriptionData?.taxonomy||"category";if(s&&t){const e="category"===n?"categories":n;await c()({path:`/wp/v2/${e}/${s}`,method:"POST",data:{description:t}})}}return{success:!0}}case"remove-terms-without-posts":if(document.querySelector(`#${t} form`)){const e=window.removeTermsWithoutPostsData?.termIds||[],t=window.removeTermsWithoutPostsData?.taxonomy||"category",s="category"===t?"categories":t;await Promise.all(e.map(e=>c()({path:`/wp/v2/${s}/${e}?force=true`,method:"DELETE"})))}return{success:!0};default:return{success:!0}}},[]),a=(0,t.useCallback)(async(t,a,r)=>{try{const o=e.find(e=>e.slug===t||e.prpl_provider?.slug===t||`${e.id}`===t);if(!o)return;let i;switch(r.type){case"siteSettings":i=async function({settingAPIKey:e,setting:t,popoverId:s,settingCallbackValue:n=e=>e}){const a=document.querySelector(`#${s} form`);if(!a)throw new Error("Form not found");p(a),g(s);try{const s=n(new FormData(a).get(t)),r=await c()({path:"/wp/v2/settings",method:"POST",data:{[e]:s}});return d(a),r}catch(e){throw d(a),u(s),e}}({settingAPIKey:r.settingAPIKey,setting:r.setting,popoverId:a,settingCallbackValue:r.settingCallbackValue});break;case"pluginSettings":i=m({setting:r.setting,settingPath:r.settingPath,popoverId:a,action:r.action||"prpl_interactive_task_submit",settingCallbackValue:r.settingCallbackValue});break;case"customSubmit":i=n(t,a);break;default:return}await i,await s(o.id,o),function(e){const t=document.getElementById(e);t&&"function"==typeof t.hidePopover&&t.hidePopover()}(a)}catch(e){console.error("Popover form submission error:",e)}},[e,s,n]);return(0,t.useEffect)(()=>{const e=new Map;Object.entries(w).forEach(([t,s])=>{const n=`prpl-popover-${t}`,r=document.querySelector(`#${n} form`);if(!r)return;const o=e=>{e.preventDefault(),a(t,n,s)};r.addEventListener("submit",o),e.set(n,{formElement:r,handler:o})});const t=document.querySelector("input#blogdescription");if(t){const e=document.querySelector('#prpl-popover-core-blogdescription button[type="submit"]');if(e){const s=t=>{e.disabled=0===t.target.value.length};t.addEventListener("input",s)}}return function(){const e=document.querySelectorAll('#prpl-popover-set-date-format input[name="date_format"]'),t=document.querySelector('#prpl-popover-set-date-format input[name="date_format_custom"]');if(!e.length||!t)return;let s;e.forEach(e=>{e.addEventListener("change",()=>{"custom"===e.value?(t.disabled=!1,t.focus()):t.disabled=!0})}),t.addEventListener("input",()=>{clearTimeout(s),s=setTimeout(async()=>{const e=t.value;if(e)try{const s=window.prplSuggestedTasksConfig?.ajaxUrl||window.progressPlanner?.ajaxUrl||"/wp-admin/admin-ajax.php",n=window.prplSuggestedTasksConfig?.nonce||window.progressPlanner?.nonce||"",a=await fetch(`${s}?action=prpl_date_format_preview&format=${encodeURIComponent(e)}&_ajax_nonce=${n}`,{credentials:"same-origin"}),r=await a.json();if(r.success&&r.data){const e=t.closest(".prpl-radio-wrapper")?.querySelector(".date-time-text");e&&(e.textContent=r.data)}}catch{}},300)})}(),function(){const e=document.querySelectorAll('#prpl-popover-core-permalink-structure input[name="permalink_structure"]');if(!e.length)return;const t=document.querySelector('#prpl-popover-core-permalink-structure input[name="permalink_custom"]');e.forEach(e=>{e.addEventListener("change",()=>{"custom"===e.value&&t?(t.disabled=!1,t.focus()):t&&(t.disabled=!0)})})}(),function(){const e=document.querySelector("#prpl-popover-core-siteicon .prpl-upload-site-icon");if(e&&window.wp?.media){let t;e.addEventListener("click",s=>{s.preventDefault(),t||(t=window.wp.media({title:e.dataset.title||"Select Site Icon",button:{text:e.dataset.button||"Use as site icon"},multiple:!1,library:{type:"image"}}),t.on("select",()=>{const e=t.state().get("selection").first().toJSON(),s=document.querySelector('#prpl-popover-core-siteicon input[name="site_icon"]');s&&(s.value=e.id);const n=document.querySelector("#prpl-popover-core-siteicon .prpl-site-icon-preview img");n&&(n.src=e.url);const a=document.querySelector('#prpl-popover-core-siteicon button[type="submit"]');a&&(a.disabled=!1)})),t.open()})}const t=document.querySelector("#prpl-popover-yoast-organization-logo .prpl-upload-logo");if(t&&window.wp?.media){let e;t.addEventListener("click",s=>{s.preventDefault(),e||(e=window.wp.media({title:t.dataset.title||"Select Logo",button:{text:t.dataset.button||"Use as logo"},multiple:!1,library:{type:"image"}}),e.on("select",()=>{const t=e.state().get("selection").first().toJSON(),s=document.querySelector('#prpl-popover-yoast-organization-logo input[name="company_logo_id"]');s&&(s.value=t.id);const n=document.querySelector("#prpl-popover-yoast-organization-logo .prpl-logo-preview img");n&&(n.src=t.url);const a=document.querySelector('#prpl-popover-yoast-organization-logo button[type="submit"]');a&&(a.disabled=!1)})),e.open()})}}(),()=>{e.forEach(({formElement:e,handler:t})=>{e.removeEventListener("submit",t)})}},[e,a]),null}const f={"1-week":7,"2-weeks":14,"1-month":30,"3-months":90,"6-months":180,"1-year":365,forever:3650};async function v({status:e="publish",perPage:t=100,excludeProvider:s,provider:n,excludeIds:a=[]}={}){const r={status:e,per_page:t,_embed:!0,"filter[orderby]":"menu_order","filter[order]":"ASC"};s&&(r.exclude_provider=s),n&&(r.provider=n),a.length>0&&(r.exclude=a.join(","));const o=function(e){const t=new URLSearchParams;return Object.entries(e).forEach(([e,s])=>{Array.isArray(s)?s.forEach(s=>t.append(e,s)):null!=s&&""!==s&&t.append(e,s)}),t.toString()}(r);try{return await c()({path:`/wp/v2/prpl_recommendations?${o}`})||[]}catch(e){return console.error("Error fetching tasks:",e),[]}}async function b(e){return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:{status:"trash"}})}async function _(e,t){return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:t})}async function k(e,t){const s=window.prplSuggestedTasksConfig?.nonce||"",n=window.prplSuggestedTasksConfig?.ajaxUrl||"/wp-admin/admin-ajax.php",a=new FormData;a.append("action","progress_planner_suggested_task_action"),a.append("post_id",e),a.append("action_type",t),a.append("nonce",s);try{return(await fetch(n,{method:"POST",body:a,credentials:"same-origin"})).json()}catch(e){return console.error("Error sending task action:",e),null}}function S(){const[e,a]=(0,t.useState)([]),[r,o]=(0,t.useState)(!0),[l,p]=(0,t.useState)(window.prplSuggestedTasksConfig?.showAll||!1),[d,u]=(0,t.useState)(new Set),g=(0,t.useRef)(null),m=(0,t.useRef)(new Set);(0,t.useEffect)(()=>{(async()=>{try{const e=l?100:window.prplSuggestedTasksConfig?.perPage||5,t=await v({status:"publish",perPage:e,excludeProvider:"user"});if(t.forEach(e=>{m.current.add(e.id)}),a(t),o(!1),!window.prplSuggestedTasksConfig?.delayCelebration){const t=await v({status:"pending",perPage:e,excludeProvider:"user"});t.length>0&&(a(e=>[...e,...t]),t.forEach(e=>{m.current.add(e.id)}),t.forEach(e=>{b(e.id).catch(()=>{})}),setTimeout(()=>{const e=new Set(t.map(e=>e.id));u(e),document.dispatchEvent(new CustomEvent("prpl/celebrateTasks")),setTimeout(()=>{a(t=>t.filter(t=>!e.has(t.id))),u(new Set),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},2e3)},3e3))}setTimeout(()=>{window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},100)}catch{o(!1)}})()},[l]);const h=(0,t.useCallback)(async(e,t)=>{try{u(t=>new Set([...t,e])),await b(e),k(e,"complete");const s=parseInt(t.prpl_points)||0;if(s>0&&"function"==typeof window.prplUpdateRaviGauge&&window.prplUpdateRaviGauge(s),s>0&&g.current){const t=g.current.querySelector(`[data-post-id="${e}"]`);document.dispatchEvent(new CustomEvent("prpl/celebrateTasks",{detail:{element:t}}))}setTimeout(async()=>{a(t=>t.filter(t=>t.id!==e)),u(t=>{const s=new Set(t);return s.delete(e),s});const t=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});t.length>0&&(a(e=>[...e,t[0]]),m.current.add(t[0].id)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},2e3)}catch{u(t=>{const s=new Set(t);return s.delete(e),s})}},[]),w=(0,t.useCallback)(async(e,t)=>{try{await async function(e,t){const s=f[t]||7,n=new Date(Date.now()+24*s*60*60*1e3).toISOString().split(".")[0];return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:{status:"future",date:n,date_gmt:n}})}(e,t),a(t=>t.filter(t=>t.id!==e));const s=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});s.length>0&&(a(e=>[...e,s[0]]),m.current.add(s[0].id)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch{}},[]),S=(0,t.useCallback)(async e=>{try{await async function(e){return c()({path:`/wp/v2/prpl_recommendations/${e}?force=true`,method:"DELETE"})}(e),k(e,"delete"),a(t=>t.filter(t=>t.id!==e));const t=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});t.length>0&&(a(e=>[...e,t[0]]),m.current.add(t[0].id)),setTimeout(()=>{window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},500)}catch{}},[]),x=(0,t.useCallback)(async(e,t)=>{try{await _(e,{title:t})}catch{}},[]),C=(0,t.useCallback)(async(t,s)=>{const n=e.findIndex(e=>e.id===t);if(-1===n)return;const r="up"===s?n-1:n+1;if(r<0||r>=e.length)return;const o=[...e],[i]=o.splice(n,1);o.splice(r,0,i),a(o),o.forEach((e,t)=>{_(e.id,{menu_order:t}).catch(()=>{})})},[e]),j=(0,t.useCallback)(async()=>{const e=!l;p(e),o(!0),m.current.clear();const t=new URL(window.location);e?t.searchParams.set("prpl_show_all_recommendations",""):t.searchParams.delete("prpl_show_all_recommendations"),window.history.pushState({},"",t)},[l]);return r?(0,n.jsx)("p",{className:"prpl-suggested-tasks-loading",children:(0,s.__)("Loading tasks…","progress-planner")}):0===e.length?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("ul",{id:"prpl-suggested-tasks-list",className:"prpl-suggested-tasks-list",ref:g}),(0,n.jsxs)("p",{className:"prpl-no-suggested-tasks",children:[(0,s.__)("You have completed all recommended tasks.","progress-planner"),(0,n.jsx)("br",{}),(0,s.__)("Check back later for new tasks!","progress-planner")]})]}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(y,{tasks:e,onComplete:h}),(0,n.jsx)("ul",{style:{display:"none"}}),(0,n.jsx)("ul",{id:"prpl-suggested-tasks-list",className:"prpl-suggested-tasks-list",ref:g,children:e.map(e=>(0,n.jsx)(i,{task:e,isUserTask:"user"===e.prpl_provider?.slug,isCelebrating:d.has(e.id),onComplete:h,onSnooze:w,onDelete:S,onMove:C,onTitleChange:x},e.id))}),(0,n.jsx)("p",{className:"prpl-show-all-tasks",children:(0,n.jsx)("button",{type:"button",id:"prpl-toggle-all-recommendations",className:"prpl-toggle-all-recommendations-button",onClick:j,children:l?(0,s.__)("Show fewer recommendations","progress-planner"):(0,s.__)("Show all recommendations","progress-planner")})})]})}function x(){const e=document.getElementById("prpl-suggested-tasks-root");e&&(0,t.createRoot)(e).render((0,n.jsx)(S,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",x):x()})(); \ No newline at end of file diff --git a/build/todo.asset.php b/build/todo.asset.php new file mode 100644 index 0000000000..4155a15e35 --- /dev/null +++ b/build/todo.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '57c33252d20fa7a21373'); diff --git a/build/todo.js b/build/todo.js new file mode 100644 index 0000000000..aefd781e2d --- /dev/null +++ b/build/todo.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,s=window.wp.i18n,r=window.wp.apiFetch;var n=e.n(r);const a=window.ReactJSXRuntime;async function l(e,t){return n()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:t})}async function o(e){return n()({path:`/wp/v2/prpl_recommendations/${e}?force=true`,method:"DELETE"})}function i({task:e,isGolden:r,isCompleted:n,onToggle:l,onDelete:o,onMove:i,onTitleChange:d}){const p=(0,t.useRef)(null),c=(0,t.useRef)(null),u=(0,t.useCallback)(()=>{c.current&&clearTimeout(c.current),c.current=setTimeout(()=>{if(p.current){const t=p.current.textContent.replace(/\n/g,"");d(e.id,t)}},300)},[e.id,d]),h=(0,t.useCallback)(e=>{"Enter"===e.key&&(e.preventDefault(),e.target.blur())},[]);return(0,a.jsxs)("li",{className:"prpl-suggested-task"+(r?" prpl-golden-task":""),"data-task-id":e.slug||e.id,"data-post-id":e.id,"data-task-action":n?"completed":"publish","data-task-provider-id":"user","data-task-points":e.prpl_points||0,"data-task-order":e.menu_order||0,children:[(0,a.jsx)("div",{className:"prpl-suggested-task-checkbox-wrapper",children:(0,a.jsxs)("label",{children:[(0,a.jsx)("input",{type:"checkbox",className:"prpl-suggested-task-checkbox",checked:n,onChange:()=>l(e.id)}),(0,a.jsxs)("span",{className:"screen-reader-text",children:[e.title?.rendered||e.title,":"," ",(0,s.__)("Mark as completed","progress-planner")]})]})}),(0,a.jsx)("div",{className:"prpl-suggested-task-title-wrapper",children:(0,a.jsx)("h3",{className:"prpl-task-title",children:(0,a.jsx)("span",{ref:p,contentEditable:!0,suppressContentEditableWarning:!0,onInput:u,onKeyDown:h,"data-post-id":e.id,tabIndex:0,role:"textbox","aria-label":(0,s.__)("Edit task title","progress-planner"),children:e.title?.rendered||e.title})})}),(0,a.jsxs)("div",{className:"prpl-suggested-task-points-wrapper",children:[(e.prpl_points||0)>0&&(0,a.jsxs)("span",{className:"prpl-suggested-task-points",children:["+",e.prpl_points]}),(0,a.jsx)("button",{type:"button",className:"prpl-suggested-task-delete",onClick:()=>o(e.id),"aria-label":(0,s.__)("Delete task","progress-planner"),children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",width:"16",height:"16","aria-hidden":"true",children:(0,a.jsx)("path",{fill:"currentColor",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Z"})})})]}),!n&&(0,a.jsxs)("div",{className:"tooltip-actions prpl-move-buttons-wrapper",children:[(0,a.jsx)("button",{type:"button",className:"prpl-move-up",onClick:()=>i(e.id,"up"),"aria-label":(0,s.__)("Move up","progress-planner"),children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",children:(0,a.jsx)("path",{fill:"currentColor",d:"M12 4l-8 8h6v8h4v-8h6z"})})}),(0,a.jsx)("button",{type:"button",className:"prpl-move-down",onClick:()=>i(e.id,"down"),"aria-label":(0,s.__)("Move down","progress-planner"),children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",children:(0,a.jsx)("path",{fill:"currentColor",d:"M12 20l8-8h-6v-8h-4v8h-6z"})})})]})]})}function d(){const[e,r]=(0,t.useState)([]),[d,p]=(0,t.useState)([]),[c,u]=(0,t.useState)(!0),[h,m]=(0,t.useState)(""),[g,w]=(0,t.useState)(!1),x=(0,t.useRef)(null);(0,t.useEffect)(()=>{(async()=>{try{const e=await async function({status:e=["publish","trash"]}={}){const t=Array.isArray(e)?e.map(e=>`status[]=${e}`).join("&"):`status=${e}`;return n()({path:`/wp/v2/prpl_recommendations?${t}&provider=user&per_page=100&_embed=true&filter[orderby]=menu_order&filter[order]=ASC`})}(),t=e.filter(e=>"publish"===e.status),s=e.filter(e=>"trash"===e.status);t.sort((e,t)=>(e.menu_order||0)-(t.menu_order||0)),s.sort((e,t)=>(e.menu_order||0)-(t.menu_order||0)),r(t),p(s)}catch(e){console.error("Error loading tasks:",e)}finally{u(!1),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}})()},[]);const v=(0,t.useCallback)(async t=>{if(t.preventDefault(),h.trim())try{const t=e.reduce((e,t)=>Math.max(e,t.menu_order||0),0),a=await async function({title:e,order:t}){return n()({path:"/wp/v2/prpl_recommendations",method:"POST",data:{title:e,status:"publish",menu_order:t,prpl_recommendations_provider:window.prplTodoConfig?.userProviderId}})}({title:h,order:t+1});r(e=>[...e,a]),m(""),window.wp?.a11y?.speak&&window.wp.a11y.speak((0,s.__)("Task added successfully","progress-planner"),"polite"),x.current?.focus(),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error creating task:",e)}},[h,e]),k=(0,t.useCallback)(async t=>{const s=e.find(e=>e.id===t)||d.find(e=>e.id===t);if(!s)return;const n="trash"===s.status,a=n?"publish":"trash";try{await l(t,{status:a}),n?(p(e=>e.filter(e=>e.id!==t)),r(e=>[...e,{...s,status:"publish"}])):(r(e=>e.filter(e=>e.id!==t)),p(e=>[...e,{...s,status:"trash"}]),s.prpl_points>0&&("function"==typeof window.prplUpdateRaviGauge&&window.prplUpdateRaviGauge(s.prpl_points),document.dispatchEvent(new CustomEvent("prpl/celebrateTasks",{detail:{}})))),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error toggling task:",e)}},[e,d]),_=(0,t.useCallback)(async e=>{try{await o(e),r(t=>t.filter(t=>t.id!==e)),p(t=>t.filter(t=>t.id!==e)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error deleting task:",e)}},[]),b=(0,t.useCallback)(async(t,s)=>{const n=e.findIndex(e=>e.id===t);if(-1===n)return;const a="up"===s?n-1:n+1;if(a<0||a>=e.length)return;const o=[...e];[o[n],o[a]]=[o[a],o[n]];const i=o.map((e,t)=>({...e,menu_order:t}));r(i);try{await Promise.all(i.map(e=>l(e.id,{menu_order:e.menu_order})))}catch(e){console.error("Error saving task order:",e)}window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},[e]),f=(0,t.useCallback)(async(e,t)=>{try{await l(e,{title:t}),r(s=>s.map(s=>s.id===e?{...s,title:{rendered:t}}:s)),p(s=>s.map(s=>s.id===e?{...s,title:{rendered:t}}:s))}catch(e){console.error("Error updating task title:",e)}},[]),j=(0,t.useCallback)(async()=>{try{await Promise.all(d.map(e=>o(e.id))),p([]),w(!1),window.wp?.a11y?.speak&&window.wp.a11y.speak((0,s.__)("All completed tasks deleted","progress-planner"),"assertive"),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error deleting completed tasks:",e)}},[d]);return c?(0,a.jsx)("p",{id:"prpl-todo-list-loading",children:(0,s.__)("Loading items…","progress-planner")}):(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("div",{id:"todo-aria-live-region","aria-live":"polite",style:{position:"absolute",left:"-9999px"}}),(0,a.jsx)("ul",{id:"todo-list",className:"prpl-todo-list prpl-suggested-tasks-list",children:e.map((e,t)=>(0,a.jsx)(i,{task:e,isGolden:0===t&&e.prpl_points>0,isCompleted:!1,onToggle:k,onDelete:_,onMove:b,onTitleChange:f},e.id))}),(0,a.jsxs)("form",{id:"create-todo-item",onSubmit:v,children:[(0,a.jsx)("input",{ref:x,type:"text",id:"new-todo-content",placeholder:(0,s.__)("Add a new task","progress-planner"),"aria-label":(0,s.__)("Add a new task","progress-planner"),required:!0,value:h,onChange:e=>m(e.target.value)}),(0,a.jsxs)("button",{type:"submit","aria-label":(0,s.__)("Add task","progress-planner"),children:[(0,a.jsx)("span",{className:"dashicons dashicons-plus-alt2","aria-hidden":"true"}),(0,a.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Add task","progress-planner")})]})]}),d.length>0&&(0,a.jsxs)("details",{id:"todo-list-completed-details",children:[(0,a.jsxs)("summary",{children:[(0,s.__)("Completed tasks","progress-planner"),(0,a.jsx)("span",{className:"prpl-todo-list-completed-summary-icon",children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:"1.5",stroke:"currentColor",children:(0,a.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m19.5 8.25-7.5 7.5-7.5-7.5"})})})]}),(0,a.jsx)("div",{id:"todo-list-completed-delete-all-wrapper",children:(0,a.jsxs)("button",{id:"todo-list-completed-delete-all",onClick:()=>w(!0),children:[(0,a.jsx)("span",{style:{display:"inline-block",width:"18px",height:"18px"},children:(0,a.jsx)("svg",{role:"img","aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",children:(0,a.jsx)("path",{fill:"#9ca3af",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Z"})})}),(0,s.__)("Delete all completed tasks","progress-planner")]})}),(0,a.jsx)("ul",{id:"todo-list-completed",className:"prpl-todo-list prpl-suggested-tasks-list",children:d.map(e=>(0,a.jsx)(i,{task:e,isGolden:!1,isCompleted:!0,onToggle:k,onDelete:_,onMove:b,onTitleChange:f},e.id))})]}),g&&(0,a.jsxs)("div",{id:"todo-list-completed-delete-all-popover",className:"prpl-popover",style:{position:"fixed",top:"50%",left:"50%",transform:"translate(-50%, -50%)",zIndex:1e4,background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 4px 20px rgba(0,0,0,0.15)"},children:[(0,a.jsx)("div",{className:"prpl-note",children:(0,a.jsx)("span",{className:"prpl-note-text",children:(0,s.__)("Are you sure you want to delete all completed tasks? This action cannot be undone.","progress-planner")})}),(0,a.jsxs)("div",{className:"prpl-buttons-wrapper",style:{display:"flex",gap:"10px",marginTop:"15px"},children:[(0,a.jsxs)("button",{id:"todo-list-completed-delete-all-cancel",onClick:()=>w(!1),children:[(0,a.jsx)("strong",{children:(0,s.__)("No","progress-planner")}),", ",(0,s.__)("keep this list","progress-planner")]}),(0,a.jsxs)("button",{id:"todo-list-completed-delete-all-confirm",onClick:j,children:[(0,a.jsx)("strong",{children:(0,s.__)("Yes","progress-planner")}),", ",(0,s.__)("delete all completed tasks","progress-planner")]})]})]}),g&&(0,a.jsx)("div",{role:"button",tabIndex:0,"aria-label":(0,s.__)("Close dialog","progress-planner"),style:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.3)",zIndex:9999},onClick:()=>w(!1),onKeyDown:e=>{"Enter"!==e.key&&" "!==e.key||w(!1)}})]})}function p(){const e=document.getElementById("prpl-todo-root");e&&(0,t.createRoot)(e).render((0,a.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/classes/admin/class-dashboard-widget-score.php b/classes/admin/class-dashboard-widget-score.php index 56aae5ec82..c53c9c7e40 100644 --- a/classes/admin/class-dashboard-widget-score.php +++ b/classes/admin/class-dashboard-widget-score.php @@ -50,17 +50,8 @@ public function render_widget() { \progress_planner()->get_admin__enqueue()->enqueue_script( 'external-link-accessibility-helper' ); - // Majoriry of the tasks are now interactive, we need a global object to handle the AJAX requests. - \progress_planner()->get_admin__enqueue()->enqueue_script( - 'recommendations/interactive-task', - [ - 'name' => 'progressPlanner', - 'data' => [ - 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), - 'nonce' => \wp_create_nonce( 'progress_planner' ), - ], - ] - ); + // Note: Interactive task form handling is now done by React PopoverManager. + // The progressPlanner global is provided by prplSuggestedTasksConfig in the React bundle. \progress_planner()->the_view( "dashboard-widgets/{$this->id}.php" ); } diff --git a/classes/admin/widgets/class-suggested-tasks.php b/classes/admin/widgets/class-suggested-tasks.php index 435e5f0616..aa7d801fb3 100644 --- a/classes/admin/widgets/class-suggested-tasks.php +++ b/classes/admin/widgets/class-suggested-tasks.php @@ -51,4 +51,45 @@ public function get_stylesheet_dependencies() { 'progress-planner-suggested-task', ]; } + + /** + * Enqueue scripts for the widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/suggested-tasks.asset.php'; + if ( ! \file_exists( $asset_file ) ) { + return; + } + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/suggested-tasks', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/suggested-tasks.js', + $asset['dependencies'], + $asset['version'], + true + ); + + // Check if the request URI contains the parameter 'prpl_show_all_recommendations'. + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + $show_all = false !== \strpos( $request_uri, 'prpl_show_all_recommendations' ); + + \wp_localize_script( + 'progress-planner/suggested-tasks', + 'prplSuggestedTasksConfig', + [ + 'perPage' => self::PER_PAGE_DEFAULT, + 'raviName' => \progress_planner()->get_ui__branding()->get_ravi_name(), + 'nonce' => \wp_create_nonce( 'progress_planner' ), + 'showAll' => $show_all, + 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), + 'delayCelebration' => ! \progress_planner()->is_on_progress_planner_dashboard_page(), + ] + ); + + // Enqueue celebrate.js for confetti effects (listens for prpl/celebrateTasks event). + \progress_planner()->get_admin__enqueue()->enqueue_script( 'celebrate' ); + } } diff --git a/classes/admin/widgets/class-todo.php b/classes/admin/widgets/class-todo.php index 171abb3d0a..1f84a18ab8 100644 --- a/classes/admin/widgets/class-todo.php +++ b/classes/admin/widgets/class-todo.php @@ -26,79 +26,63 @@ final class ToDo extends Widget { */ protected $width = 2; + /** + * Enqueue scripts for the widget. + * + * @return void + */ + public function enqueue_scripts() { + $asset_file = \PROGRESS_PLANNER_DIR . '/build/todo.asset.php'; + if ( ! \file_exists( $asset_file ) ) { + return; + } + $asset = include $asset_file; + + \wp_enqueue_script( + 'progress-planner/todo', + \constant( 'PROGRESS_PLANNER_URL' ) . '/build/todo.js', + $asset['dependencies'], + $asset['version'], + true + ); + + // Get the user provider term ID. + $user_provider_term = \get_term_by( 'slug', 'user', 'prpl_recommendations_provider' ); + $user_provider_id = $user_provider_term ? $user_provider_term->term_id : 0; + + \wp_localize_script( + 'progress-planner/todo', + 'prplTodoConfig', + [ + 'nonce' => \wp_create_nonce( 'wp_rest' ), + 'userProviderId' => $user_provider_id, + ] + ); + + // Enqueue celebrate.js for confetti effects. + \progress_planner()->get_admin__enqueue()->enqueue_script( 'celebrate' ); + } + /** * Print the widget content. * * @return void */ public function print_content() { - echo '

        ' . \esc_html__( 'Write down all the website maintenance tasks you want to get done!', 'progress-planner' ) . '

        '; - $this->the_todo_list(); + // The React component renders all content. + echo '
        '; } /** * The TODO list. * + * @deprecated 2.0.0 Now handled by React component. + * * @return void */ public function the_todo_list() { - ?> -

        -
        - -
          - -
          - - -
          -
          - - - - - - - -
          - -
          -
            -
            -
            -
            - - the_asset( 'images/icon_exclamation_triangle_solid.svg' ); ?> - - - - -
            - -
            - - -
            -
            -
            '; } /** diff --git a/classes/suggested-tasks/providers/class-tasks-interactive.php b/classes/suggested-tasks/providers/class-tasks-interactive.php index a82278f776..b59f3403e6 100644 --- a/classes/suggested-tasks/providers/class-tasks-interactive.php +++ b/classes/suggested-tasks/providers/class-tasks-interactive.php @@ -245,32 +245,16 @@ abstract public function print_popover_form_contents(); /** * Enqueue the scripts. * + * Form submission handling is now done by the React PopoverManager component + * in assets/src/widgets/SuggestedTasks/PopoverManager.js. + * * @param string $hook The current admin page. * * @return void */ public function enqueue_scripts( $hook ) { - - // Don't enqueue the script if the user is not at least an editor, since we dont want to enqueue scripts on WP Dashboard page. - if ( ! \current_user_can( 'edit_others_posts' ) ) { - return; - } - - // Enqueue the script only on Progress Planner and WP dashboard pages. - if ( 'toplevel_page_progress-planner' !== $hook && 'index.php' !== $hook ) { - return; - } - - // Don't enqueue the script if the task is not published. - if ( ! $this->is_task_published() ) { - return; - } - - // Enqueue the web component. - \progress_planner()->get_admin__enqueue()->enqueue_script( - 'progress-planner/recommendations/' . $this->get_provider_id(), - $this->get_enqueue_data() - ); + // Form submission handling migrated to React PopoverManager. + // Individual recommendation JS files are no longer needed. } /** diff --git a/views/page-widgets/suggested-tasks.php b/views/page-widgets/suggested-tasks.php index d0ca25a330..fae40b3b9e 100644 --- a/views/page-widgets/suggested-tasks.php +++ b/views/page-widgets/suggested-tasks.php @@ -27,39 +27,11 @@ get_ui__branding()->get_ravi_name() ) ); ?>

            -
              -
                -

                - -

                - - - -

                - -

                -

                - -
                - -

                +
                diff --git a/webpack.config.js b/webpack.config.js index ea6d96b526..eb3534dbb4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,6 +10,8 @@ module.exports = { 'streak-badges': './assets/src/streak-badges.js', 'activity-scores': './assets/src/activity-scores.js', 'whats-new': './assets/src/whats-new.js', + 'suggested-tasks': './assets/src/suggested-tasks.js', + todo: './assets/src/todo.js', }, output: { path: path.resolve( __dirname, 'build' ), From 0d58687dc85bc284c4aef9499aa953763aa7bd72 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 10 Dec 2025 22:43:56 +0200 Subject: [PATCH 008/275] cleanup & more migrations --- assets/js/grid-masonry.js | 80 --- .../recommendations/aioseo-author-archive.js | 32 - .../aioseo-crawl-settings-feed-authors.js | 32 - .../aioseo-crawl-settings-feed-comments.js | 32 - .../js/recommendations/aioseo-date-archive.js | 32 - .../js/recommendations/aioseo-media-pages.js | 32 - .../recommendations/core-blogdescription.js | 23 - .../core-permalink-structure.js | 70 -- assets/js/recommendations/core-siteicon.js | 195 ------ .../disable-comment-pagination.js | 15 - assets/js/recommendations/disable-comments.js | 15 - assets/js/recommendations/hello-world.js | 37 - assets/js/recommendations/interactive-task.js | 299 -------- .../remove-terms-without-posts.js | 214 ------ .../rename-uncategorized-category.js | 76 -- assets/js/recommendations/sample-page.js | 37 - .../search-engine-visibility.js | 14 - assets/js/recommendations/select-locale.js | 15 - assets/js/recommendations/select-timezone.js | 26 - assets/js/recommendations/set-date-format.js | 129 ---- .../update-term-description.js | 250 ------- .../recommendations/yoast-author-archive.js | 15 - .../yoast-crawl-settings-emoji-scripts.js | 15 - .../yoast-crawl-settings-feed-authors.js | 15 - ...ast-crawl-settings-feed-global-comments.js | 15 - .../js/recommendations/yoast-date-archive.js | 15 - .../recommendations/yoast-format-archive.js | 15 - .../js/recommendations/yoast-media-pages.js | 15 - .../yoast-organization-logo.js | 217 ------ assets/js/suggested-task.js | 653 ------------------ assets/js/widgets/suggested-tasks.js | 258 ------- assets/js/widgets/todo.js | 336 --------- assets/src/hooks/useGridMasonry.js | 96 +++ assets/src/widgets/SuggestedTasks/index.js | 4 + assets/src/widgets/TodoWidget/index.js | 4 + build/suggested-tasks.asset.php | 2 +- build/suggested-tasks.js | 2 +- build/todo.asset.php | 2 +- build/todo.js | 2 +- classes/admin/class-dashboard-widget-todo.php | 2 - classes/admin/class-enqueue.php | 68 -- classes/admin/class-page.php | 1 - classes/admin/widgets/class-todo.php | 12 - package.json | 4 +- views/admin-page.php | 2 - views/dashboard-widgets/todo.php | 2 +- views/js-templates/suggested-task.html | 70 -- views/page-widgets/todo.php | 2 +- 48 files changed, 112 insertions(+), 3387 deletions(-) delete mode 100644 assets/js/grid-masonry.js delete mode 100644 assets/js/recommendations/aioseo-author-archive.js delete mode 100644 assets/js/recommendations/aioseo-crawl-settings-feed-authors.js delete mode 100644 assets/js/recommendations/aioseo-crawl-settings-feed-comments.js delete mode 100644 assets/js/recommendations/aioseo-date-archive.js delete mode 100644 assets/js/recommendations/aioseo-media-pages.js delete mode 100644 assets/js/recommendations/core-blogdescription.js delete mode 100644 assets/js/recommendations/core-permalink-structure.js delete mode 100644 assets/js/recommendations/core-siteicon.js delete mode 100644 assets/js/recommendations/disable-comment-pagination.js delete mode 100644 assets/js/recommendations/disable-comments.js delete mode 100644 assets/js/recommendations/hello-world.js delete mode 100644 assets/js/recommendations/interactive-task.js delete mode 100644 assets/js/recommendations/remove-terms-without-posts.js delete mode 100644 assets/js/recommendations/rename-uncategorized-category.js delete mode 100644 assets/js/recommendations/sample-page.js delete mode 100644 assets/js/recommendations/search-engine-visibility.js delete mode 100644 assets/js/recommendations/select-locale.js delete mode 100644 assets/js/recommendations/select-timezone.js delete mode 100644 assets/js/recommendations/set-date-format.js delete mode 100644 assets/js/recommendations/update-term-description.js delete mode 100644 assets/js/recommendations/yoast-author-archive.js delete mode 100644 assets/js/recommendations/yoast-crawl-settings-emoji-scripts.js delete mode 100644 assets/js/recommendations/yoast-crawl-settings-feed-authors.js delete mode 100644 assets/js/recommendations/yoast-crawl-settings-feed-global-comments.js delete mode 100644 assets/js/recommendations/yoast-date-archive.js delete mode 100644 assets/js/recommendations/yoast-format-archive.js delete mode 100644 assets/js/recommendations/yoast-media-pages.js delete mode 100644 assets/js/recommendations/yoast-organization-logo.js delete mode 100644 assets/js/suggested-task.js delete mode 100644 assets/js/widgets/suggested-tasks.js delete mode 100644 assets/js/widgets/todo.js create mode 100644 assets/src/hooks/useGridMasonry.js delete mode 100644 views/js-templates/suggested-task.html diff --git a/assets/js/grid-masonry.js b/assets/js/grid-masonry.js deleted file mode 100644 index 67eb0aedfe..0000000000 --- a/assets/js/grid-masonry.js +++ /dev/null @@ -1,80 +0,0 @@ -/* global prplDocumentReady */ -/* - * Grid Masonry - * - * A script to allow a grid to behave like a masonry layout. - * Inspired by https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb - * - * Dependencies: progress-planner/document-ready - */ - -/** - * Trigger a resize event on the grid. - */ -const prplTriggerGridResize = () => { - setTimeout( () => { - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - } ); -}; - -prplDocumentReady( () => { - prplTriggerGridResize(); - setTimeout( prplTriggerGridResize, 1000 ); -} ); - -window.addEventListener( 'resize', prplTriggerGridResize ); - -// Fire event after all images are loaded. -window.addEventListener( 'load', prplTriggerGridResize ); - -window.addEventListener( - 'prpl/grid/resize', - () => { - /** - * Update the grid masonry items (row spans). - */ - document - .querySelectorAll( '.prpl-widget-wrapper' ) - .forEach( ( item ) => { - if ( ! item || item.classList.contains( 'in-popover' ) ) { - return; - } - - const innerContainer = item.querySelector( - '.widget-inner-container' - ); - if ( ! innerContainer ) { - return; - } - - const rowHeight = parseInt( - window - .getComputedStyle( - document.querySelector( '.prpl-widgets-container' ) - ) - .getPropertyValue( 'grid-auto-rows' ) - ); - - const paddingTop = parseInt( - window - .getComputedStyle( item ) - .getPropertyValue( 'padding-top' ) - ); - const paddingBottom = parseInt( - window - .getComputedStyle( item ) - .getPropertyValue( 'padding-bottom' ) - ); - - const rowSpan = Math.ceil( - ( innerContainer.getBoundingClientRect().height + - paddingTop + - paddingBottom ) / - rowHeight - ); - - item.style.gridRowEnd = 'span ' + ( rowSpan + 1 ); - } ); - }, - false -); diff --git a/assets/js/recommendations/aioseo-author-archive.js b/assets/js/recommendations/aioseo-author-archive.js deleted file mode 100644 index 5ddac8f983..0000000000 --- a/assets/js/recommendations/aioseo-author-archive.js +++ /dev/null @@ -1,32 +0,0 @@ -/* global prplInteractiveTaskFormListener, progressPlanner */ - -/* - * All in One SEO: noindex the author archive. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'aioseo-author-archive', - popoverId: 'prpl-popover-aioseo-author-archive', - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_aioseo-author-archive', - nonce: progressPlanner.nonce, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/aioseo-crawl-settings-feed-authors.js b/assets/js/recommendations/aioseo-crawl-settings-feed-authors.js deleted file mode 100644 index 4544f78dee..0000000000 --- a/assets/js/recommendations/aioseo-crawl-settings-feed-authors.js +++ /dev/null @@ -1,32 +0,0 @@ -/* global prplInteractiveTaskFormListener, progressPlanner */ - -/* - * All in One SEO: disable author RSS feeds. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'aioseo-crawl-settings-feed-authors', - popoverId: 'prpl-popover-aioseo-crawl-settings-feed-authors', - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_aioseo-crawl-settings-feed-authors', - nonce: progressPlanner.nonce, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/aioseo-crawl-settings-feed-comments.js b/assets/js/recommendations/aioseo-crawl-settings-feed-comments.js deleted file mode 100644 index c0a4777113..0000000000 --- a/assets/js/recommendations/aioseo-crawl-settings-feed-comments.js +++ /dev/null @@ -1,32 +0,0 @@ -/* global prplInteractiveTaskFormListener, progressPlanner */ - -/* - * All in One SEO: disable global comment RSS feeds. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'aioseo-crawl-settings-feed-comments', - popoverId: 'prpl-popover-aioseo-crawl-settings-feed-comments', - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_aioseo-crawl-settings-feed-comments', - nonce: progressPlanner.nonce, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/aioseo-date-archive.js b/assets/js/recommendations/aioseo-date-archive.js deleted file mode 100644 index d2a5600322..0000000000 --- a/assets/js/recommendations/aioseo-date-archive.js +++ /dev/null @@ -1,32 +0,0 @@ -/* global prplInteractiveTaskFormListener, progressPlanner */ - -/* - * All in One SEO: noindex the date archive. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'aioseo-date-archive', - popoverId: 'prpl-popover-aioseo-date-archive', - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_aioseo-date-archive', - nonce: progressPlanner.nonce, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/aioseo-media-pages.js b/assets/js/recommendations/aioseo-media-pages.js deleted file mode 100644 index 638b8aa3fe..0000000000 --- a/assets/js/recommendations/aioseo-media-pages.js +++ /dev/null @@ -1,32 +0,0 @@ -/* global prplInteractiveTaskFormListener, progressPlanner */ - -/* - * All in One SEO: redirect media pages. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'aioseo-media-pages', - popoverId: 'prpl-popover-aioseo-media-pages', - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_aioseo-media-pages', - nonce: progressPlanner.nonce, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/core-blogdescription.js b/assets/js/recommendations/core-blogdescription.js deleted file mode 100644 index c53c9047f5..0000000000 --- a/assets/js/recommendations/core-blogdescription.js +++ /dev/null @@ -1,23 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Core Blog Description recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.siteSettings( { - settingAPIKey: 'description', - setting: 'blogdescription', - taskId: 'core-blogdescription', - popoverId: 'prpl-popover-core-blogdescription', -} ); - -document - .querySelector( 'input#blogdescription' ) - ?.addEventListener( 'input', function ( e ) { - const button = document.querySelector( - '[popover-id="prpl-popover-core-blogdescription"] button[type="submit"]' - ); - button.disabled = e.target.value.length === 0; - } ); diff --git a/assets/js/recommendations/core-permalink-structure.js b/assets/js/recommendations/core-permalink-structure.js deleted file mode 100644 index f9e9849575..0000000000 --- a/assets/js/recommendations/core-permalink-structure.js +++ /dev/null @@ -1,70 +0,0 @@ -/* global prplInteractiveTaskFormListener, prplDocumentReady, progressPlanner */ - -/* - * Set the permalink structure. - * - * Dependencies: progress-planner/recommendations/interactive-task, progress-planner/document-ready - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'core-permalink-structure', - popoverId: 'prpl-popover-core-permalink-structure', - callback: () => { - const customPermalinkStructure = document.querySelector( - '#prpl-popover-core-permalink-structure input[name="prpl_custom_permalink_structure"]' - ); - - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_core-permalink-structure', - nonce: progressPlanner.nonce, - value: customPermalinkStructure.value, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); - -prplDocumentReady( () => { - // Handle custom date format input, this value is what is actually submitted to the server. - const customPermalinkStructureInput = document.querySelector( - '#prpl-popover-core-permalink-structure input[name="prpl_custom_permalink_structure"]' - ); - - // If there is no custom permalink structure input, return. - if ( ! customPermalinkStructureInput ) { - return; - } - - // Handle date format radio button clicks. - document - .querySelectorAll( - '#prpl-popover-core-permalink-structure input[name="prpl_permalink_structure"]' - ) - .forEach( function ( input ) { - input.addEventListener( 'click', function () { - // Dont update the custom permalink structure input if the custom radio button is checked. - if ( 'prpl_permalink_structure_custom_radio' !== this.id ) { - customPermalinkStructureInput.value = this.value; - } - } ); - } ); - - // If users clicks on the custom permalink structure input, check the custom radio button. - customPermalinkStructureInput.addEventListener( 'click', function () { - document.getElementById( - 'prpl_permalink_structure_custom_radio' - ).checked = true; - } ); -} ); diff --git a/assets/js/recommendations/core-siteicon.js b/assets/js/recommendations/core-siteicon.js deleted file mode 100644 index b91380f841..0000000000 --- a/assets/js/recommendations/core-siteicon.js +++ /dev/null @@ -1,195 +0,0 @@ -/* global prplInteractiveTaskFormListener, prplSiteIcon */ -/** - * Core Site Icon recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task, wp-api - */ -( function () { - /** - * Core Site Icon class. - */ - class CoreSiteIcon { - /** - * Constructor. - */ - constructor() { - this.mediaUploader = null; - this.elements = this.getElements(); - this.init(); - } - - /** - * Get all DOM elements. - * - * @return {Object} Object containing all DOM elements. - */ - getElements() { - return { - uploadButton: document.getElementById( - 'prpl-upload-site-icon-button' - ), - popover: document.getElementById( - 'prpl-popover-core-siteicon' - ), - hiddenField: document.getElementById( 'prpl-site-icon-id' ), - preview: document.getElementById( 'site-icon-preview' ), - submitButton: document.getElementById( - 'prpl-set-site-icon-button' - ), - }; - } - - /** - * Initialize the component. - */ - init() { - if ( this.elements.uploadButton ) { - this.bindEvents(); - } - this.initFormListener(); - } - - /** - * Bind event listeners. - */ - bindEvents() { - this.elements.uploadButton.addEventListener( 'click', ( e ) => { - this.handleUploadButtonClick( e ); - } ); - } - - /** - * Handle upload button click. - * - * @param {Event} e The click event. - */ - handleUploadButtonClick( e ) { - e.preventDefault(); - - // If the uploader object has already been created, reopen the dialog. - if ( this.mediaUploader ) { - this.mediaUploader.open(); - return; - } - - this.createMediaUploader(); - this.bindMediaUploaderEvents(); - this.mediaUploader.open(); - } - - /** - * Create the media uploader. - */ - createMediaUploader() { - this.mediaUploader = wp.media.frames.file_frame = wp.media( { - title: prplSiteIcon?.mediaTitle || 'Choose Site Icon', - button: { - text: prplSiteIcon?.mediaButtonText || 'Use as Site Icon', - }, - multiple: false, - library: { - type: 'image', - }, - } ); - } - - /** - * Bind media uploader events. - */ - bindMediaUploaderEvents() { - // Hide popover when media library opens. - this.mediaUploader.on( 'open', () => { - if ( this.elements.popover ) { - this.elements.popover.hidePopover(); - } - } ); - - // Show popover when media library closes. - this.mediaUploader.on( 'close', () => { - if ( this.elements.popover ) { - this.elements.popover.showPopover(); - } - } ); - - // Handle image selection. - this.mediaUploader.on( 'select', () => { - this.handleImageSelection(); - } ); - } - - /** - * Handle image selection. - */ - handleImageSelection() { - const attachment = this.mediaUploader - .state() - .get( 'selection' ) - .first() - .toJSON(); - - this.updateHiddenField( attachment ); - this.updatePreview( attachment ); - this.enableSubmitButton(); - } - - /** - * Update the hidden field with attachment ID. - * - * @param {Object} attachment The selected attachment. - */ - updateHiddenField( attachment ) { - if ( this.elements.hiddenField ) { - this.elements.hiddenField.value = attachment.id; - } - } - - /** - * Update the preview with the selected image. - * - * @param {Object} attachment The selected attachment. - */ - updatePreview( attachment ) { - if ( ! this.elements.preview ) { - return; - } - - // Use thumbnail size if available, otherwise use full size. - const imageUrl = - attachment.sizes && attachment.sizes.thumbnail - ? attachment.sizes.thumbnail.url - : attachment.url; - - this.elements.preview.innerHTML = - '' +
-				( attachment.alt || 'Site icon preview' ) +
-				''; - } - - /** - * Enable the submit button. - */ - enableSubmitButton() { - if ( this.elements.submitButton ) { - this.elements.submitButton.disabled = false; - } - } - - /** - * Initialize the form listener. - */ - initFormListener() { - prplInteractiveTaskFormListener.siteSettings( { - settingAPIKey: 'site_icon', - setting: 'site_icon', - taskId: 'core-siteicon', - popoverId: 'prpl-popover-core-siteicon', - settingCallbackValue: ( value ) => parseInt( value, 10 ), - } ); - } - } - - // Initialize the component. - new CoreSiteIcon(); -} )(); diff --git a/assets/js/recommendations/disable-comment-pagination.js b/assets/js/recommendations/disable-comment-pagination.js deleted file mode 100644 index c4aa7c8bb0..0000000000 --- a/assets/js/recommendations/disable-comment-pagination.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Disable Comment Pagination recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.settings( { - setting: 'page_comments', - settingPath: '{}', - taskId: 'disable-comment-pagination', - popoverId: 'prpl-popover-disable-comment-pagination', - settingCallbackValue: () => '', -} ); diff --git a/assets/js/recommendations/disable-comments.js b/assets/js/recommendations/disable-comments.js deleted file mode 100644 index 9eb191c436..0000000000 --- a/assets/js/recommendations/disable-comments.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Disable Comments recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task, progress-planner/web-components/prpl-install-plugin - */ - -prplInteractiveTaskFormListener.siteSettings( { - settingAPIKey: 'default_comment_status', - setting: 'default_comment_status', - taskId: 'disable-comments', - popoverId: 'prpl-popover-disable-comments', - settingCallbackValue: () => 'closed', -} ); diff --git a/assets/js/recommendations/hello-world.js b/assets/js/recommendations/hello-world.js deleted file mode 100644 index c76b04f06e..0000000000 --- a/assets/js/recommendations/hello-world.js +++ /dev/null @@ -1,37 +0,0 @@ -/* global prplInteractiveTaskFormListener, helloWorldData */ - -/* - * Core Blog Description recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'hello-world', - popoverId: 'prpl-popover-hello-world', - callback: () => { - return new Promise( ( resolve, reject ) => { - const post = new wp.api.models.Post( { - id: helloWorldData.postId, - } ); - post.fetch() - .then( () => { - // Handle the case when plain URL structure is used, it used to result in invalid URL (404): http://localhost:8080/index.php?rest_route=/wp/v2/prpl_recommendations/35?force=true - const url = post.url().includes( 'rest_route=' ) - ? post.url() + '&force=true' - : post.url() + '?force=true'; - - post.destroy( { url } ) - .then( () => { - resolve( { success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/interactive-task.js b/assets/js/recommendations/interactive-task.js deleted file mode 100644 index e7f511aa25..0000000000 --- a/assets/js/recommendations/interactive-task.js +++ /dev/null @@ -1,299 +0,0 @@ -/* global prplSuggestedTask, progressPlannerAjaxRequest, progressPlanner, prplL10n */ - -/* - * Core Blog Description recommendation. - * - * Dependencies: wp-api, progress-planner/suggested-task, progress-planner/web-components/prpl-interactive-task, progress-planner/ajax-request - */ - -// eslint-disable-next-line no-unused-vars -const prplInteractiveTaskFormListener = { - /** - * Add a form listener to an interactive task form. - * - * @param {Object} options - The options for the interactive task form listener. - * @param {string} options.settingAPIKey - The API key for the setting. - * @param {string} options.setting - The setting to update. - * @param {string} options.taskId - The ID of the task. - * @param {string} options.popoverId - The ID of the popover. - * @param {Function} options.settingCallbackValue - The callback function to get the value of the setting. - */ - siteSettings: ( { - settingAPIKey, - setting, - taskId, - popoverId, - settingCallbackValue = ( value ) => value, - } = {} ) => { - const formElement = document.querySelector( `#${ popoverId } form` ); - - if ( ! formElement ) { - return; - } - - // Add a form listener to the form. - formElement.addEventListener( 'submit', ( event ) => { - event.preventDefault(); - - prplInteractiveTaskFormListener.showLoading( formElement ); - - // Get the form data. - const formData = new FormData( formElement ); - const settingsToPass = {}; - settingsToPass[ settingAPIKey ] = settingCallbackValue( - formData.get( setting ) - ); - - const taskEl = document.querySelector( - `.prpl-suggested-task[data-task-id="${ taskId }"]` - ); - - // Update the blog description. - wp.api.loadPromise.done( () => { - const settings = new wp.api.models.Settings( settingsToPass ); - - settings.save().then( ( response ) => { - const postId = parseInt( taskEl.dataset.postId ); - if ( ! postId ) { - return response; - } - - prplInteractiveTaskFormListener.hideLoading( formElement ); - - // This will trigger the celebration event (confetti) as well. - prplSuggestedTask.maybeComplete( postId ).then( () => { - // Close popover. - document.getElementById( popoverId ).hidePopover(); - } ); - } ); - } ); - } ); - }, - - customSubmit: ( { taskId, popoverId, callback = () => {} } = {} ) => { - const formElement = document.querySelector( `#${ popoverId } form` ); - - if ( ! formElement ) { - return; - } - - const formSubmitHandler = ( event ) => { - event.preventDefault(); - - prplInteractiveTaskFormListener.showLoading( formElement ); - - callback() - .then( ( response ) => { - if ( true !== response.success ) { - // Show error to the user. - prplInteractiveTaskFormListener.showError( - response, - popoverId - ); - - return response; - } - - const taskEl = document.querySelector( - `.prpl-suggested-task[data-task-id="${ taskId }"]` - ); - const postId = parseInt( taskEl.dataset.postId ); - if ( ! postId ) { - return; - } - - // This will trigger the celebration event (confetti) as well. - prplSuggestedTask.maybeComplete( postId ).then( () => { - // Close popover. - document.getElementById( popoverId ).hidePopover(); - } ); - } ) - .catch( ( error ) => { - // Show error to the user. - prplInteractiveTaskFormListener.showError( - error, - popoverId - ); - } ) - .finally( () => { - // Hide loading state. - prplInteractiveTaskFormListener.hideLoading( formElement ); - - // Remove the form listener once the callback is executed. - formElement.removeEventListener( - 'submit', - formSubmitHandler - ); - } ); - }; - - // Add a form listener to the form. - formElement.addEventListener( 'submit', formSubmitHandler ); - }, - - settings: ( { - taskId, - setting, - settingPath = false, - popoverId, - settingCallbackValue = ( settingValue ) => settingValue, - action = 'prpl_interactive_task_submit', - } = {} ) => { - const formElement = document.querySelector( `#${ popoverId } form` ); - - if ( ! formElement ) { - return; - } - - formElement.addEventListener( 'submit', ( event ) => { - event.preventDefault(); - - prplInteractiveTaskFormListener.showLoading( formElement ); - - const formData = new FormData( formElement ); - const settingsToPass = {}; - settingsToPass[ setting ] = settingCallbackValue( - formData.get( setting ) - ); - - progressPlannerAjaxRequest( { - url: progressPlanner.ajaxUrl, - data: { - action, - _ajax_nonce: progressPlanner.nonce, - post_id: taskId, - setting, - value: settingsToPass[ setting ], - setting_path: settingPath, - }, - } ) - .then( ( response ) => { - if ( true !== response.success ) { - // Show error to the user. - prplInteractiveTaskFormListener.showError( - response, - popoverId - ); - - return response; - } - - const taskEl = document.querySelector( - `.prpl-suggested-task[data-task-id="${ taskId }"]` - ); - - if ( ! taskEl ) { - return response; - } - - const postId = parseInt( taskEl.dataset.postId ); - if ( ! postId ) { - return response; - } - - // This will trigger the celebration event (confetti) as well. - prplSuggestedTask.maybeComplete( postId ).then( () => { - // Close popover. - document.getElementById( popoverId ).hidePopover(); - } ); - } ) - .catch( ( error ) => { - // Show error to the user. - prplInteractiveTaskFormListener.showError( - error, - popoverId - ); - } ) - .finally( () => { - // Hide loading state. - prplInteractiveTaskFormListener.hideLoading( formElement ); - } ); - } ); - }, - - /** - * Helper which shows user an error message. - * For now the error message is generic. - * - * @param {Object} error - The error object. - * @param {string} popoverId - The ID of the popover. - * @return {void} - */ - showError: ( error, popoverId ) => { - const formElement = document.querySelector( `#${ popoverId } form` ); - - if ( ! formElement ) { - return; - } - - console.error( 'Error in interactive task callback:', error ); - - // Check if there's already an error message

                element right after the form - const existingErrorElement = formElement.parentNode.querySelector( - 'p.prpl-interactive-task-error-message' - ); - - if ( ! existingErrorElement ) { - // Add paragraph with error message. - const errorParagraph = document.createElement( 'p' ); - errorParagraph.classList.add( - 'prpl-note', - 'prpl-note-error', - 'prpl-interactive-task-error-message' - ); - errorParagraph.textContent = prplL10n( 'somethingWentWrong' ); - - // Append after the form element. - formElement.insertAdjacentElement( 'afterend', errorParagraph ); - } - }, - - /** - * Show loading state. - * - * @param {HTMLFormElement} formElement - The form element. - * @return {void} - */ - showLoading: ( formElement ) => { - let submitButton = formElement.querySelector( 'button[type="submit"]' ); - - if ( ! submitButton ) { - submitButton = formElement.querySelector( - 'button[data-action="completeTask"]' - ); - } - - submitButton.disabled = true; - - // Add spinner. - const spinner = document.createElement( 'span' ); - spinner.classList.add( 'prpl-spinner' ); - spinner.innerHTML = - ''; // WP spinner. - - // Append spinner after submit button. - submitButton.after( spinner ); - }, - - /** - * Hide loading state. - * - * @param {HTMLFormElement} formElement - The form element. - * @return {void} - */ - hideLoading: ( formElement ) => { - let submitButton = formElement.querySelector( 'button[type="submit"]' ); - - if ( ! submitButton ) { - submitButton = formElement.querySelector( - 'button[data-action="completeTask"]' - ); - } - - submitButton.disabled = false; - const spinner = formElement.querySelector( 'span.prpl-spinner' ); - if ( spinner ) { - spinner.remove(); - } - }, -}; diff --git a/assets/js/recommendations/remove-terms-without-posts.js b/assets/js/recommendations/remove-terms-without-posts.js deleted file mode 100644 index 2ed1fbbf67..0000000000 --- a/assets/js/recommendations/remove-terms-without-posts.js +++ /dev/null @@ -1,214 +0,0 @@ -/* global progressPlanner, prplInteractiveTaskFormListener */ -/** - * Remove Terms Without Posts recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task, progress-planner/ajax-request, progress-planner/suggested-task - */ -( function () { - /** - * Remove Terms Without Posts class. - */ - class RemoveTermsWithoutPosts { - /** - * Constructor. - */ - constructor() { - this.popoverId = 'prpl-popover-remove-terms-without-posts'; - - // Early return if the popover is not found. - if ( ! document.getElementById( this.popoverId ) ) { - return; - } - - this.currentTermData = null; - this.currentTaskElement = null; - this.elements = this.getElements(); - this.init(); - } - - /** - * Get all DOM elements. - * - * @return {Object} Object containing all DOM elements. - */ - getElements() { - const popover = document.getElementById( this.popoverId ); - return { - popover, - popoverTitle: popover.querySelector( '.prpl-popover-title' ), - - termNameElement: popover.querySelector( - '#prpl-delete-term-name' - ), - taxonomyElement: popover.querySelector( - '#prpl-delete-term-taxonomy' - ), - taxonomyNameElement: popover.querySelector( - '#prpl-delete-term-taxonomy-name' - ), - termIdField: popover.querySelector( '#prpl-delete-term-id' ), - taxonomyField: popover.querySelector( '#prpl-delete-taxonomy' ), - }; - } - - /** - * Initialize the component. - */ - init() { - this.bindEvents(); - } - - /** - * Bind event listeners. - */ - bindEvents() { - // Listen for the generic interactive task action event. - document.addEventListener( - 'prpl-interactive-task-action-remove-terms-without-posts', - ( event ) => { - this.handleInteractiveTaskAction( event ); - - // After the event is handled, initialize the form listener. - this.initFormListener(); - } - ); - } - - /** - * Handle interactive task action event. - * - * @param {CustomEvent} event The custom event with task context data. - */ - handleInteractiveTaskAction( event ) { - this.currentTermData = { - termId: this.decodeHtmlEntities( event.detail.target_term_id ), - taxonomy: this.decodeHtmlEntities( - event.detail.target_taxonomy - ), - taxonomyName: this.decodeHtmlEntities( - event.detail.target_taxonomy_name - ), - termName: this.decodeHtmlEntities( - event.detail.target_term_name - ), - }; - - // Store reference to the task element that triggered this. - this.currentTaskElement = event.target.closest( - '.prpl-suggested-task' - ); - - // Update the popover content with the term data. - this.updatePopoverContent( - this.currentTermData.termId, - this.currentTermData.taxonomy, - this.currentTermData.termName, - this.currentTermData.taxonomyName, - this.decodeHtmlEntities( event.detail.post_title ) - ); - } - - /** - * Update the popover content. - * - * @param {string} termId The term ID. - * @param {string} taxonomy The taxonomy. - * @param {string} termName The term name. - * @param {string} taxonomyName The taxonomy name. - * @param {string} postTitle The post title. - */ - updatePopoverContent( - termId, - taxonomy, - termName, - taxonomyName, - postTitle - ) { - if ( this.elements.popoverTitle ) { - this.elements.popoverTitle.textContent = postTitle; - } - - if ( this.elements.termNameElement ) { - this.elements.termNameElement.textContent = termName; - } - - if ( this.elements.taxonomyElement ) { - this.elements.taxonomyElement.textContent = taxonomy; - } - - if ( this.elements.taxonomyNameElement ) { - this.elements.taxonomyNameElement.textContent = taxonomyName; - } - - if ( this.elements.termIdField ) { - this.elements.termIdField.value = termId; - } - - if ( this.elements.taxonomyField ) { - this.elements.taxonomyField.value = taxonomy; - } - } - - /** - * Initialize the form listener. - */ - initFormListener() { - if ( ! this.currentTermData || ! this.currentTaskElement ) { - return; - } - - prplInteractiveTaskFormListener.customSubmit( { - taskId: this.currentTaskElement.dataset.taskId, - popoverId: this.popoverId, - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': - 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_remove-terms-without-posts', - nonce: progressPlanner.nonce, - term_id: this.elements.termIdField.value, - taxonomy: this.elements.taxonomyField.value, - } ), - } ) - .then( () => { - this.currentTaskElement = null; - this.currentTermData = null; - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, - } ); - } - - /** - * Decodes HTML entities in a string (like ", &, etc.) - * @param {string} str The string to decode. - * @return {string} The decoded string. - */ - decodeHtmlEntities( str ) { - if ( typeof str !== 'string' ) { - return str; - } - - return str - .replace( /"/g, '"' ) - .replace( /'/g, "'" ) - .replace( /</g, '<' ) - .replace( />/g, '>' ) - .replace( /&/g, '&' ); - } - } - - // Initialize the component. - new RemoveTermsWithoutPosts(); -} )(); diff --git a/assets/js/recommendations/rename-uncategorized-category.js b/assets/js/recommendations/rename-uncategorized-category.js deleted file mode 100644 index 0adc4b9128..0000000000 --- a/assets/js/recommendations/rename-uncategorized-category.js +++ /dev/null @@ -1,76 +0,0 @@ -/* global prplInteractiveTaskFormListener, progressPlanner, prplDocumentReady */ - -/* - * Rename the Uncategorized category. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'rename-uncategorized-category', - popoverId: 'prpl-popover-rename-uncategorized-category', - callback: () => { - const name = document.querySelector( - '#prpl-popover-rename-uncategorized-category input[name="prpl_uncategorized_category_name"]' - ); - const slug = document.querySelector( - '#prpl-popover-rename-uncategorized-category input[name="prpl_uncategorized_category_slug"]' - ); - - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_rename-uncategorized-category', - nonce: progressPlanner.nonce, - uncategorized_category_name: name.value, - uncategorized_category_slug: slug.value, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); - -prplDocumentReady( () => { - const name = document.querySelector( - '#prpl-popover-rename-uncategorized-category input[name="prpl_uncategorized_category_name"]' - ); - const slug = document.querySelector( - '#prpl-popover-rename-uncategorized-category input[name="prpl_uncategorized_category_slug"]' - ); - - if ( ! name || ! slug ) { - return; - } - - // Function to check if both fields are valid and toggle button state - const toggleSubmitButton = () => { - const submitButton = document.querySelector( - '#prpl-popover-rename-uncategorized-category button[type="submit"]' - ); - const isNameValid = - name.value && - name.value.toLowerCase() !== name.placeholder.toLowerCase(); - const isSlugValid = - slug.value && - slug.value.toLowerCase() !== slug.placeholder.toLowerCase(); - - submitButton.disabled = ! ( isNameValid && isSlugValid ); - }; - - // If there is no name or slug or it is the same as placeholder the submit button should be disabled. - toggleSubmitButton(); - - // Add event listeners to both fields - name.addEventListener( 'input', toggleSubmitButton ); - slug.addEventListener( 'input', toggleSubmitButton ); -} ); diff --git a/assets/js/recommendations/sample-page.js b/assets/js/recommendations/sample-page.js deleted file mode 100644 index 7b10ed1cb4..0000000000 --- a/assets/js/recommendations/sample-page.js +++ /dev/null @@ -1,37 +0,0 @@ -/* global prplInteractiveTaskFormListener, samplePageData */ - -/* - * Core Blog Description recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'sample-page', - popoverId: 'prpl-popover-sample-page', - callback: () => { - return new Promise( ( resolve, reject ) => { - const post = new wp.api.models.Page( { - id: samplePageData.postId, - } ); - post.fetch() - .then( () => { - // Handle the case when plain URL structure is used, it used to result in invalid URL (404): http://localhost:8080/index.php?rest_route=/wp/v2/prpl_recommendations/35?force=true - const url = post.url().includes( 'rest_route=' ) - ? post.url() + '&force=true' - : post.url() + '?force=true'; - - post.destroy( { url } ) - .then( () => { - resolve( { success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); diff --git a/assets/js/recommendations/search-engine-visibility.js b/assets/js/recommendations/search-engine-visibility.js deleted file mode 100644 index 9d4e13139a..0000000000 --- a/assets/js/recommendations/search-engine-visibility.js +++ /dev/null @@ -1,14 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Search Engine Visibility recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - settingAPIKey: 'blog_public', - setting: 'blog_public', - taskId: 'search-engine-visibility', - popoverId: 'prpl-popover-search-engine-visibility', - action: 'prpl_interactive_task_submit_search-engine-visibility', -} ); diff --git a/assets/js/recommendations/select-locale.js b/assets/js/recommendations/select-locale.js deleted file mode 100644 index 425fbf87da..0000000000 --- a/assets/js/recommendations/select-locale.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Select Locale recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.settings( { - settingAPIKey: 'language', - setting: 'language', - taskId: 'select-locale', - popoverId: 'prpl-popover-select-locale', - action: 'prpl_interactive_task_submit_select-locale', -} ); diff --git a/assets/js/recommendations/select-timezone.js b/assets/js/recommendations/select-timezone.js deleted file mode 100644 index 35f2607a3a..0000000000 --- a/assets/js/recommendations/select-timezone.js +++ /dev/null @@ -1,26 +0,0 @@ -/* global prplInteractiveTaskFormListener, prplDocumentReady */ - -/* - * Set the site timezone. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.settings( { - settingAPIKey: 'timezone', - setting: 'timezone', - taskId: 'select-timezone', - popoverId: 'prpl-popover-select-timezone', - action: 'prpl_interactive_task_submit_select-timezone', -} ); - -prplDocumentReady( () => { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const timezoneSelect = document.querySelector( 'select#timezone' ); - const timezoneSaved = timezoneSelect?.dataset?.timezoneSaved || 'false'; - - // Try to preselect the timezone. - if ( timezone && timezoneSelect && 'false' === timezoneSaved ) { - timezoneSelect.value = timezone; - } -} ); diff --git a/assets/js/recommendations/set-date-format.js b/assets/js/recommendations/set-date-format.js deleted file mode 100644 index 82a00f13f4..0000000000 --- a/assets/js/recommendations/set-date-format.js +++ /dev/null @@ -1,129 +0,0 @@ -/* global prplInteractiveTaskFormListener, prplDocumentReady, progressPlanner */ - -/* - * Set the site date format. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ - -prplInteractiveTaskFormListener.customSubmit( { - taskId: 'set-date-format', - popoverId: 'prpl-popover-set-date-format', - callback: () => { - return new Promise( ( resolve, reject ) => { - const format = document.querySelector( - '#prpl-popover-set-date-format input[name="date_format"]:checked' - ); - const customFormat = document.querySelector( - '#prpl-popover-set-date-format input[name="date_format_custom"]' - ); - - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_set-date-format', - nonce: progressPlanner.nonce, - date_format: format.value, - date_format_custom: customFormat.value, - } ), - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, -} ); - -prplDocumentReady( () => { - // Handle date format radio button clicks - document - .querySelectorAll( - '#prpl-popover-set-date-format input[name="date_format"]' - ) - .forEach( function ( input ) { - input.addEventListener( 'click', function () { - if ( 'date_format_custom_radio' !== this.id ) { - const customInput = document.querySelector( - '#prpl-popover-set-date-format input[name="date_format_custom"]' - ); - const fieldset = customInput.closest( 'fieldset' ); - const exampleElement = fieldset.querySelector( '.example' ); - const formatText = - this.parentElement.querySelector( - '.format-i18n' - ).textContent; - - customInput.value = this.value; - exampleElement.textContent = formatText; - } - } ); - } ); - - // Handle custom date format input - const customDateInput = document.querySelector( - 'input[name="date_format_custom"]' - ); - - if ( customDateInput ) { - customDateInput.addEventListener( 'click', function () { - document.getElementById( - 'date_format_custom_radio' - ).checked = true; - } ); - - customDateInput.addEventListener( 'input', function () { - document.getElementById( - 'date_format_custom_radio' - ).checked = true; - - const format = this; - const fieldset = format.closest( 'fieldset' ); - const example = fieldset.querySelector( '.example' ); - - // Debounce the event callback while users are typing. - clearTimeout( format.dataset.timer ); - format.dataset.timer = setTimeout( function () { - // If custom date is not empty. - if ( format.value ) { - // Find the spinner element within the fieldset - const spinner = fieldset.querySelector( '.spinner' ); - if ( spinner ) { - spinner.classList.add( 'is-active' ); - } - - // Use fetch instead of $.post - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'date_format', - date: format.value, - } ), - } ) - .then( function ( response ) { - return response.text(); - } ) - .then( function ( data ) { - example.textContent = data; - } ) - .catch( function ( error ) { - console.error( 'Error:', error ); - } ) - .finally( function () { - if ( spinner ) { - spinner.classList.remove( 'is-active' ); - } - } ); - } - }, 500 ); - } ); - } -} ); diff --git a/assets/js/recommendations/update-term-description.js b/assets/js/recommendations/update-term-description.js deleted file mode 100644 index 3eb925e176..0000000000 --- a/assets/js/recommendations/update-term-description.js +++ /dev/null @@ -1,250 +0,0 @@ -/* global progressPlanner, prplInteractiveTaskFormListener */ -/** - * Update Term Description recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task, progress-planner/ajax-request, progress-planner/suggested-task - */ -( function () { - /** - * Update Term Description class. - */ - class UpdateTermDescription { - /** - * Constructor. - */ - constructor() { - this.popoverId = 'prpl-popover-update-term-description'; - - // Early return if the popover is not found. - if ( ! document.getElementById( this.popoverId ) ) { - return; - } - - this.currentTermData = null; - this.currentTaskElement = null; - this.elements = this.getElements(); - this.init(); - } - - /** - * Get all DOM elements. - * - * @return {Object} Object containing all DOM elements. - */ - getElements() { - const popover = document.getElementById( this.popoverId ); - - return { - popover, - popoverTitle: popover.querySelector( '.prpl-popover-title' ), - - termNameElement: popover.querySelector( - '#prpl-update-term-name' - ), - taxonomyElement: popover.querySelector( - '#prpl-update-term-taxonomy' - ), - taxonomyNameElement: popover.querySelector( - '#prpl-update-term-taxonomy-name' - ), - termIdField: popover.querySelector( '#prpl-update-term-id' ), - taxonomyField: popover.querySelector( '#prpl-update-taxonomy' ), - descriptionField: popover.querySelector( - '#prpl-term-description' - ), - }; - } - - /** - * Initialize the component. - */ - init() { - this.bindEvents(); - } - - /** - * Bind event listeners. - */ - bindEvents() { - // Listen for the generic interactive task action event. - document.addEventListener( - 'prpl-interactive-task-action-update-term-description', - ( event ) => { - this.handleInteractiveTaskAction( event ); - - // After the event is handled, initialize the form listener. - this.initFormListener(); - } - ); - } - - /** - * Handle interactive task action event. - * - * @param {CustomEvent} event The custom event with task context data. - */ - handleInteractiveTaskAction( event ) { - this.currentTermData = { - termId: this.decodeHtmlEntities( event.detail.target_term_id ), - taxonomy: this.decodeHtmlEntities( - event.detail.target_taxonomy - ), - taxonomyName: this.decodeHtmlEntities( - event.detail.target_taxonomy_name - ), - termName: this.decodeHtmlEntities( - event.detail.target_term_name - ), - }; - - // Store reference to the task element that triggered this. - this.currentTaskElement = event.target.closest( - '.prpl-suggested-task' - ); - - // Update the popover content with the term data. - this.updatePopoverContent( - this.currentTermData.termId, - this.currentTermData.taxonomy, - this.currentTermData.termName, - this.currentTermData.taxonomyName, - this.decodeHtmlEntities( event.detail.post_title ) - ); - } - - /** - * Update the popover content. - * - * @param {string} termId The term ID. - * @param {string} taxonomy The taxonomy. - * @param {string} termName The term name. - * @param {string} taxonomyName The taxonomy name. - * @param {string} postTitle The post title. - */ - updatePopoverContent( - termId, - taxonomy, - termName, - taxonomyName, - postTitle - ) { - if ( this.elements.popoverTitle ) { - this.elements.popoverTitle.textContent = postTitle; - } - - if ( this.elements.termNameElement ) { - this.elements.termNameElement.textContent = termName; - } - - if ( this.elements.taxonomyElement ) { - this.elements.taxonomyElement.textContent = taxonomy; - } - - if ( this.elements.taxonomyNameElement ) { - this.elements.taxonomyNameElement.textContent = taxonomyName; - } - - if ( this.elements.termIdField ) { - this.elements.termIdField.value = termId; - } - - if ( this.elements.taxonomyField ) { - this.elements.taxonomyField.value = taxonomy; - } - - // Clear the description field. - if ( this.elements.descriptionField ) { - this.elements.descriptionField.value = ''; - } - } - - /** - * Initialize the form listener. - */ - initFormListener() { - if ( ! this.currentTermData || ! this.currentTaskElement ) { - return; - } - - const formElement = this.elements.popover.querySelector( 'form' ); - - if ( ! formElement ) { - return; - } - - // Submit button should be disabled if description is empty. - const submitButton = document.getElementById( - 'prpl-update-term-description-button' - ); - - if ( submitButton ) { - submitButton.disabled = true; - } - - // Add event listener to description field. - const descriptionField = formElement.querySelector( - '#prpl-term-description' - ); - if ( descriptionField ) { - descriptionField.addEventListener( 'input', () => { - submitButton.disabled = ! descriptionField.value.trim(); - } ); - } - - prplInteractiveTaskFormListener.customSubmit( { - taskId: this.currentTaskElement.dataset.taskId, - popoverId: this.popoverId, - callback: () => { - return new Promise( ( resolve, reject ) => { - fetch( progressPlanner.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': - 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - action: 'prpl_interactive_task_submit_update-term-description', - nonce: progressPlanner.nonce, - term_id: this.elements.termIdField.value, - taxonomy: this.elements.taxonomyField.value, - description: - this.elements.descriptionField.value, - } ), - } ) - .then( () => { - this.currentTaskElement = null; - this.currentTermData = null; - } ) - .then( ( response ) => { - resolve( { response, success: true } ); - } ) - .catch( ( error ) => { - reject( { success: false, error } ); - } ); - } ); - }, - } ); - } - - /** - * Decodes HTML entities in a string (like ", &, etc.) - * @param {string} str The string to decode. - * @return {string} The decoded string. - */ - decodeHtmlEntities( str ) { - if ( typeof str !== 'string' ) { - return str; - } - - return str - .replace( /"/g, '"' ) - .replace( /'/g, "'" ) - .replace( /</g, '<' ) - .replace( />/g, '>' ) - .replace( /&/g, '&' ); - } - } - - // Initialize the component. - new UpdateTermDescription(); -} )(); diff --git a/assets/js/recommendations/yoast-author-archive.js b/assets/js/recommendations/yoast-author-archive.js deleted file mode 100644 index 78cd23658b..0000000000 --- a/assets/js/recommendations/yoast-author-archive.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast author archive recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo_titles', - settingPath: JSON.stringify( [ 'disable-author' ] ), - taskId: 'yoast-author-archive', - popoverId: 'prpl-popover-yoast-author-archive', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-crawl-settings-emoji-scripts.js b/assets/js/recommendations/yoast-crawl-settings-emoji-scripts.js deleted file mode 100644 index db5ad25e04..0000000000 --- a/assets/js/recommendations/yoast-crawl-settings-emoji-scripts.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast remove emoji scripts recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo', - settingPath: JSON.stringify( [ 'remove_emoji_scripts' ] ), - taskId: 'yoast-crawl-settings-emoji-scripts', - popoverId: 'prpl-popover-yoast-crawl-settings-emoji-scripts', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-crawl-settings-feed-authors.js b/assets/js/recommendations/yoast-crawl-settings-feed-authors.js deleted file mode 100644 index e871be7f71..0000000000 --- a/assets/js/recommendations/yoast-crawl-settings-feed-authors.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast remove post authors feeds recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo', - settingPath: JSON.stringify( [ 'remove_feed_authors' ] ), - taskId: 'yoast-crawl-settings-feed-authors', - popoverId: 'prpl-popover-yoast-crawl-settings-feed-authors', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-crawl-settings-feed-global-comments.js b/assets/js/recommendations/yoast-crawl-settings-feed-global-comments.js deleted file mode 100644 index 4643dbdb6c..0000000000 --- a/assets/js/recommendations/yoast-crawl-settings-feed-global-comments.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast remove global comment feeds recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo', - settingPath: JSON.stringify( [ 'remove_feed_global_comments' ] ), - taskId: 'yoast-crawl-settings-feed-global-comments', - popoverId: 'prpl-popover-yoast-crawl-settings-feed-global-comments', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-date-archive.js b/assets/js/recommendations/yoast-date-archive.js deleted file mode 100644 index 5f9ed45bbd..0000000000 --- a/assets/js/recommendations/yoast-date-archive.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast date archive recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo_titles', - settingPath: JSON.stringify( [ 'disable-date' ] ), - taskId: 'yoast-date-archive', - popoverId: 'prpl-popover-yoast-date-archive', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-format-archive.js b/assets/js/recommendations/yoast-format-archive.js deleted file mode 100644 index 4beef5357b..0000000000 --- a/assets/js/recommendations/yoast-format-archive.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast format archive recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo_titles', - settingPath: JSON.stringify( [ 'disable-post_format' ] ), - taskId: 'yoast-format-archive', - popoverId: 'prpl-popover-yoast-format-archive', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-media-pages.js b/assets/js/recommendations/yoast-media-pages.js deleted file mode 100644 index 36fa395e42..0000000000 --- a/assets/js/recommendations/yoast-media-pages.js +++ /dev/null @@ -1,15 +0,0 @@ -/* global prplInteractiveTaskFormListener */ - -/* - * Yoast remove global comment feeds recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task - */ -prplInteractiveTaskFormListener.settings( { - setting: 'wpseo_titles', - settingPath: JSON.stringify( [ 'disable-attachment' ] ), - taskId: 'yoast-media-pages', - popoverId: 'prpl-popover-yoast-media-pages', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => true, -} ); diff --git a/assets/js/recommendations/yoast-organization-logo.js b/assets/js/recommendations/yoast-organization-logo.js deleted file mode 100644 index dc70f09c47..0000000000 --- a/assets/js/recommendations/yoast-organization-logo.js +++ /dev/null @@ -1,217 +0,0 @@ -/* global prplInteractiveTaskFormListener, prplYoastOrganizationLogo */ -/** - * Core Site Icon recommendation. - * - * Dependencies: progress-planner/recommendations/interactive-task, wp-api - */ -( function () { - /** - * Core Site Icon class. - */ - class CoreSiteIcon { - /** - * Constructor. - */ - constructor() { - this.mediaUploader = null; - this.elements = this.getElements(); - this.init(); - } - - /** - * Get all DOM elements. - * - * @return {Object} Object containing all DOM elements. - */ - getElements() { - return { - uploadButton: document.getElementById( - 'prpl-upload-organization-logo-button' - ), - popover: document.getElementById( - 'prpl-popover-yoast-organization-logo' - ), - hiddenField: document.getElementById( - 'prpl-yoast-organization-logo-id' - ), - preview: document.getElementById( 'organization-logo-preview' ), - submitButton: document.getElementById( - 'prpl-set-organization-logo-button' - ), - }; - } - - /** - * Initialize the component. - */ - init() { - if ( this.elements.uploadButton ) { - this.bindEvents(); - } - this.initFormListener(); - } - - /** - * Bind event listeners. - */ - bindEvents() { - this.elements.uploadButton.addEventListener( 'click', ( e ) => { - this.handleUploadButtonClick( e ); - } ); - } - - /** - * Handle upload button click. - * - * @param {Event} e The click event. - */ - handleUploadButtonClick( e ) { - e.preventDefault(); - - // If the uploader object has already been created, reopen the dialog. - if ( this.mediaUploader ) { - this.mediaUploader.open(); - return; - } - - this.createMediaUploader(); - this.bindMediaUploaderEvents(); - this.mediaUploader.open(); - } - - /** - * Create the media uploader. - */ - createMediaUploader() { - this.mediaUploader = wp.media.frames.file_frame = wp.media( { - title: - prplYoastOrganizationLogo?.mediaTitle || 'Choose Site Icon', - button: { - text: - prplYoastOrganizationLogo?.mediaButtonText || - 'Use as Site Icon', - }, - multiple: false, - library: { - type: 'image', - }, - } ); - } - - /** - * Bind media uploader events. - */ - bindMediaUploaderEvents() { - // Hide popover when media library opens. - this.mediaUploader.on( 'open', () => { - if ( this.elements.popover ) { - this.elements.popover.hidePopover(); - } - } ); - - // Show popover when media library closes. - this.mediaUploader.on( 'close', () => { - if ( this.elements.popover ) { - this.elements.popover.showPopover(); - } - } ); - - // Handle image selection. - this.mediaUploader.on( 'select', () => { - this.handleImageSelection(); - } ); - } - - /** - * Handle image selection. - */ - handleImageSelection() { - const attachment = this.mediaUploader - .state() - .get( 'selection' ) - .first() - .toJSON(); - - this.updateHiddenField( attachment ); - this.updatePreview( attachment ); - this.enableSubmitButton(); - } - - /** - * Update the hidden field with attachment ID. - * - * @param {Object} attachment The selected attachment. - */ - updateHiddenField( attachment ) { - if ( this.elements.hiddenField ) { - this.elements.hiddenField.value = attachment.id; - } - } - - /** - * Update the preview with the selected image. - * - * @param {Object} attachment The selected attachment. - */ - updatePreview( attachment ) { - if ( ! this.elements.preview ) { - return; - } - - // Use thumbnail size if available, otherwise use full size. - const imageUrl = - attachment.sizes && attachment.sizes.thumbnail - ? attachment.sizes.thumbnail.url - : attachment.url; - - this.elements.preview.innerHTML = - '' +
-				( attachment.alt || 'Site icon preview' ) +
-				''; - } - - /** - * Enable the submit button. - */ - enableSubmitButton() { - if ( this.elements.submitButton ) { - this.elements.submitButton.disabled = false; - } - } - - /** - * Initialize the form listener. - */ - initFormListener() { - prplInteractiveTaskFormListener.settings( { - setting: 'wpseo_titles', - settingPath: - 'company' === prplYoastOrganizationLogo.companyOrPerson - ? JSON.stringify( [ 'company_logo_id' ] ) - : JSON.stringify( [ 'person_logo_id' ] ), - taskId: 'yoast-organization-logo', - popoverId: 'prpl-popover-yoast-organization-logo', - action: 'prpl_interactive_task_submit', - settingCallbackValue: () => { - const popover = document.getElementById( - 'prpl-popover-yoast-organization-logo' - ); - - if ( ! popover ) { - return false; - } - - const organizationLogoId = popover.querySelector( - 'input[name="prpl_yoast_organization_logo_id"]' - ).value; - return parseInt( organizationLogoId, 10 ); - }, - } ); - } - } - - // Initialize the component. - new CoreSiteIcon(); -} )(); diff --git a/assets/js/suggested-task.js b/assets/js/suggested-task.js deleted file mode 100644 index 42bd863074..0000000000 --- a/assets/js/suggested-task.js +++ /dev/null @@ -1,653 +0,0 @@ -/* global HTMLElement, prplSuggestedTask, prplL10n, prplUpdateRaviGauge, prplTerms */ -/* - * Suggested Task scripts & helpers. - * - * Dependencies: wp-api, progress-planner/l10n, progress-planner/suggested-task-terms, progress-planner/web-components/prpl-gauge, progress-planner/widgets/suggested-tasks - */ -/* eslint-disable camelcase, jsdoc/require-param-type, jsdoc/require-param, jsdoc/check-param-names */ - -prplSuggestedTask = { - ...prplSuggestedTask, - injectedItemIds: [], - l10n: { - info: prplL10n( 'info' ), - moveUp: prplL10n( 'moveUp' ), - moveDown: prplL10n( 'moveDown' ), - snooze: prplL10n( 'snooze' ), - disabledRRCheckboxTooltip: prplL10n( 'disabledRRCheckboxTooltip' ), - markAsComplete: prplL10n( 'markAsComplete' ), - taskDelete: prplL10n( 'taskDelete' ), - delete: prplL10n( 'delete' ), - whyIsThisImportant: prplL10n( 'whyIsThisImportant' ), - }, - - /** - * Fetch items for arguments. - * - * @param {Object} args The arguments to pass to the injectItems method. - * @return {Promise} A promise that resolves with the collection of posts. - */ - fetchItems: ( args ) => { - console.info( - `Fetching recommendations with args: ${ JSON.stringify( args ) }...` - ); - - const fetchData = { - status: args.status, - per_page: args.per_page || 100, - _embed: true, - exclude: prplSuggestedTask.injectedItemIds, - filter: { - orderby: 'menu_order', - order: 'ASC', - }, - }; - - // Pass through provider and exclude_provider if provided. - if ( args.provider ) { - fetchData.provider = args.provider; - } - if ( args.exclude_provider ) { - fetchData.exclude_provider = args.exclude_provider; - } - - return prplSuggestedTask - .getPostsCollectionPromise( { data: fetchData } ) - .then( ( response ) => response.data ); - }, - - /** - * Inject items. - * - * @param {Object[]} items The items to inject. - */ - injectItems: ( items ) => { - if ( items.length ) { - // Inject the items into the DOM. - items.forEach( ( item ) => { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: { - item, - listId: 'prpl-suggested-tasks-list', - insertPosition: 'beforeend', - }, - } ) - ); - prplSuggestedTask.injectedItemIds.push( item.id ); - } ); - } - - // Trigger the grid resize event. - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - }, - - /** - * Get a collection of posts. - * - * @param {Object} fetchArgs The arguments to pass to the fetch method. - * @return {Promise} A promise that resolves with the collection of posts. - */ - getPostsCollectionPromise: ( fetchArgs ) => { - const collectionsPromise = new Promise( ( resolve ) => { - const postsCollection = - new wp.api.collections.Prpl_recommendations(); - postsCollection - .fetch( fetchArgs ) - .done( ( data ) => resolve( { data, postsCollection } ) ); - } ); - - return collectionsPromise; - }, - - /** - * Render a new item. - * - * @param {Object} post The post object. - */ - getNewItemTemplatePromise: ( { post = {}, listId = '' } ) => - new Promise( ( resolve ) => { - const { prpl_recommendations_provider } = post; - const terms = { prpl_recommendations_provider }; - - Object.values( prplTerms.get( 'provider' ) ).forEach( ( term ) => { - if ( term.id === terms[ prplTerms.provider ][ 0 ] ) { - terms[ prplTerms.provider ] = term; - } - } ); - - const template = wp.template( 'prpl-suggested-task' ); - const data = { - post, - terms, - listId, - assets: prplSuggestedTask.assets, - action: 'pending' === post.status ? 'celebrate' : '', - l10n: prplSuggestedTask.l10n, - }; - - resolve( template( data ) ); - } ), - - /** - * Run a task action. - * - * @param {number} postId The post ID. - * @param {string} actionType The action type. - * @return {Promise} A promise that resolves with the response from the server. - */ - runTaskAction: ( postId, actionType ) => - wp.ajax.post( 'progress_planner_suggested_task_action', { - post_id: postId, - nonce: prplSuggestedTask.nonce, - action_type: actionType, - } ), - - /** - * Trash (delete) a task. - * Only user tasks can be trashed. - * - * @param {number} postId The post ID. - */ - trash: ( postId ) => { - const post = new wp.api.models.Prpl_recommendations( { - id: postId, - } ); - post.fetch().then( () => { - // Handle the case when plain URL structure is used, it used to result in invalid URL (404): http://localhost:8080/index.php?rest_route=/wp/v2/prpl_recommendations/35?force=true - const url = post.url().includes( 'rest_route=' ) - ? post.url() + '&force=true' - : post.url() + '?force=true'; - - post.destroy( { url } ).then( () => { - // Remove the task from the todo list. - prplSuggestedTask.removeTaskElement( postId ); - - // Fetch and inject a replacement task - prplSuggestedTask.fetchAndInjectReplacementTask(); - - setTimeout( - () => - window.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ), - 500 - ); - - prplSuggestedTask.runTaskAction( postId, 'delete' ); - } ); - } ); - }, - - /** - * Maybe complete a task. - * - * @param {number} postId The post ID. - */ - maybeComplete: ( postId ) => { - // Return the promise chain so callers can wait for completion - return new Promise( ( resolve, reject ) => { - // Get the task. - const post = new wp.api.models.Prpl_recommendations( { - id: postId, - } ); - post.fetch() - .then( ( postData ) => { - const taskProviderId = prplTerms.getTerm( - postData?.[ prplTerms.provider ], - prplTerms.provider - ).slug; - - const el = prplSuggestedTask.getTaskElement( postId ); - - // Dismissable tasks don't have pending status, it's either publish or trash. - const newStatus = - 'publish' === postData.status ? 'trash' : 'publish'; - - // Disable the checkbox for RR tasks, to prevent multiple clicks. - el.querySelector( - '.prpl-suggested-task-checkbox' - )?.setAttribute( 'disabled', 'disabled' ); - - post.set( 'status', newStatus ) - .save() - .then( () => { - prplSuggestedTask.runTaskAction( - postId, - 'trash' === newStatus ? 'complete' : 'pending' - ); - const eventPoints = parseInt( - postData?.prpl_points - ); - - // Task is trashed, check if we need to celebrate. - if ( 'trash' === newStatus ) { - el.setAttribute( - 'data-task-action', - 'celebrate' - ); - if ( 'user' === taskProviderId ) { - // Set class to trigger strike through animation. - el.classList.add( - 'prpl-suggested-task-celebrated' - ); - - setTimeout( () => { - // Move task from published to trash. - document - .getElementById( - 'todo-list-completed' - ) - .insertAdjacentElement( - 'beforeend', - el - ); - - // Remove the class to trigger the strike through animation. - el.classList.remove( - 'prpl-suggested-task-celebrated' - ); - - window.dispatchEvent( - new CustomEvent( - 'prpl/grid/resize' - ) - ); - - // Remove the disabled attribute for user tasks, so they can be clicked again. - el.querySelector( - '.prpl-suggested-task-checkbox' - )?.removeAttribute( 'disabled' ); - - // Resolve the promise after the timeout completes - resolve( { - postId, - newStatus, - eventPoints, - } ); - }, 2000 ); - } else { - // Check the chekcbox, since completing task can be triggered in different ways ("Mark as done" button), without triggering the onchange event. - const checkbox = el.querySelector( - '.prpl-suggested-task-checkbox' - ); - if ( checkbox ) { - checkbox.checked = true; - } - - /** - * Strike completed tasks and remove them from the DOM. - */ - document.dispatchEvent( - new CustomEvent( - 'prpl/removeCelebratedTasks' - ) - ); - - // Fetch and inject a replacement task for non-user tasks - prplSuggestedTask.fetchAndInjectReplacementTask(); - - // Resolve immediately for non-user tasks - resolve( { - postId, - newStatus, - eventPoints, - } ); - } - - // We trigger celebration only if the task has points. - if ( 0 < eventPoints ) { - prplUpdateRaviGauge( eventPoints ); - - // Trigger the celebration event (confetti). - document.dispatchEvent( - new CustomEvent( - 'prpl/celebrateTasks', - { - detail: { element: el }, - } - ) - ); - } - } else if ( - 'publish' === newStatus && - 'user' === taskProviderId - ) { - // This is only possible for user tasks. - // Set the task action to publish. - el.setAttribute( - 'data-task-action', - 'publish' - ); - - // Update the Ravi gauge. - prplUpdateRaviGauge( 0 - eventPoints ); - - // Move task from trash to published, tasks with points go to the beginning of the list. - document - .getElementById( 'todo-list' ) - .insertAdjacentElement( - 0 < eventPoints - ? 'afterbegin' - : 'beforeend', - el - ); - - window.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ); - - // Remove the disabled attribute for user tasks, so they can be clicked again. - el.querySelector( - '.prpl-suggested-task-checkbox' - )?.removeAttribute( 'disabled' ); - - // Resolve immediately for publish actions - resolve( { postId, newStatus, eventPoints } ); - } - } ) - .catch( reject ); - } ) - .catch( reject ); - } ); - }, - - /** - * Snooze a task. - * - * @param {number} postId The post ID. - * @param {string} snoozeDuration The snooze duration. - */ - snooze: ( postId, snoozeDuration ) => { - const snoozeDurationMap = { - '1-week': 7, - '2-weeks': 14, - '1-month': 30, - '3-months': 90, - '6-months': 180, - '1-year': 365, - forever: 3650, - }; - - const snoozeDurationDays = snoozeDurationMap[ snoozeDuration ]; - const date = new Date( - Date.now() + snoozeDurationDays * 24 * 60 * 60 * 1000 - ) - .toISOString() - .split( '.' )[ 0 ]; - const postModelToSave = new wp.api.models.Prpl_recommendations( { - id: postId, - status: 'future', - date, - date_gmt: date, - } ); - postModelToSave.save().then( () => { - prplSuggestedTask.removeTaskElement( postId ); - - // Fetch and inject a replacement task - prplSuggestedTask.fetchAndInjectReplacementTask(); - } ); - }, - - /** - * Run a tooltip action. - * - * @param {HTMLElement} button The button that was clicked. - */ - runButtonAction: ( button ) => { - let action = button.getAttribute( 'data-action' ); - const target = button.getAttribute( 'data-target' ); - const item = button.closest( 'li.prpl-suggested-task' ); - const tooltipActions = item.querySelector( '.tooltip-actions' ); - const elClass = '.prpl-suggested-task-' + target; - - // If the tooltip was already open, close it. - if ( - !! tooltipActions.querySelector( - `${ elClass }[data-tooltip-visible]` - ) - ) { - action = 'close-' + target; - } else { - const closestTaskListVisible = item - .closest( '.prpl-suggested-tasks-list' ) - .querySelector( `[data-tooltip-visible]` ); - // Close the any opened radio group. - closestTaskListVisible?.classList.remove( - 'prpl-toggle-radio-group-open' - ); - // Remove any existing tooltip visible attribute, in the entire list. - closestTaskListVisible?.removeAttribute( 'data-tooltip-visible' ); - } - - switch ( action ) { - case 'move-up': - case 'move-down': - if ( 'move-up' === action && item.previousElementSibling ) { - item.parentNode.insertBefore( - item, - item.previousElementSibling - ); - } else if ( - 'move-down' === action && - item.nextElementSibling - ) { - item.parentNode.insertBefore( - item.nextElementSibling, - item - ); - } - // Trigger a custom event. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/move', { - detail: { node: item }, - } ) - ); - break; - } - }, - - /** - * Update the task title. - * - * @param {HTMLElement} el The element that was edited. - */ - updateTaskTitle: ( el ) => { - // Add debounce to the input event. - clearTimeout( this.debounceTimeout ); - this.debounceTimeout = setTimeout( () => { - // Update an existing post. - const title = el.textContent.replace( /\n/g, '' ); - const postModel = new wp.api.models.Prpl_recommendations( { - id: parseInt( el.getAttribute( 'data-post-id' ) ), - title, - } ); - postModel.save().then( () => - // Update the task title. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/update', { - detail: { - node: el.closest( 'li.prpl-suggested-task' ), - }, - } ) - ) - ); - el - .closest( 'li.prpl-suggested-task' ) - .querySelector( - 'label:has(.prpl-suggested-task-checkbox) .screen-reader-text' - ).innerHTML = `${ title }: ${ prplL10n( 'markAsComplete' ) }`; - }, 300 ); - }, - - /** - * Prevent Enter key in contenteditable elements. - * - * @param {Event} event The keydown event. - */ - preventEnterKey: ( event ) => { - if ( event.key === 'Enter' ) { - event.preventDefault(); - event.stopPropagation(); - event.target.blur(); - return false; - } - }, - - /** - * Get the task element. - * - * @param {number} postId The post ID. - * @return {HTMLElement} The task element. - */ - getTaskElement: ( postId ) => - document.querySelector( - `.prpl-suggested-task[data-post-id="${ postId }"]` - ), - - /** - * Remove the task element. - * - * @param {number} postId The post ID. - */ - removeTaskElement: ( postId ) => - prplSuggestedTask.getTaskElement( postId )?.remove(), - - /** - * Fetch and inject a replacement task after one is removed. - * - * Replacement tasks are always fetched for the suggested-tasks-list, - * which excludes user tasks (user tasks have their own todo list). - */ - fetchAndInjectReplacementTask: () => { - // Collect all currently visible task IDs from the DOM - const visibleTaskIds = Array.from( - document.querySelectorAll( '.prpl-suggested-task[data-post-id]' ) - ).map( ( el ) => parseInt( el.getAttribute( 'data-post-id' ) ) ); - - // Combine with injectedItemIds to ensure we have a complete exclusion list - const allTaskIds = [ - ...new Set( [ - ...prplSuggestedTask.injectedItemIds, - ...visibleTaskIds, - ] ), - ]; - - // Update injectedItemIds to include any tasks that might have been missed - prplSuggestedTask.injectedItemIds = allTaskIds; - - const fetchArgs = { - status: 'publish', - per_page: 1, - exclude_provider: 'user', // Always exclude user tasks from suggested-tasks-list - }; - - prplSuggestedTask.fetchItems( fetchArgs ).then( ( items ) => { - if ( items && items.length > 0 ) { - prplSuggestedTask.injectItems( items ); - } - } ); - }, -}; - -/** - * Inject an item. - */ -document.addEventListener( 'prpl/suggestedTask/injectItem', ( event ) => { - prplSuggestedTask - .getNewItemTemplatePromise( { - post: event.detail.item, - listId: event.detail.listId, - } ) - .then( ( itemHTML ) => { - /** - * @todo Implement the parent task functionality. - * Use this code: `const parent = event.detail.item.parent && '' !== event.detail.item.parent ? event.detail.item.parent : null; - */ - const parent = false; - - if ( ! parent ) { - // Inject the item into the list. - document - .getElementById( event.detail.listId ) - .insertAdjacentHTML( - event.detail.insertPosition, - itemHTML - ); - - // Trigger the grid resize event. - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - - return; - } - - // If we could not find the parent item, try again after 500ms. - window.prplRenderAttempts = window.prplRenderAttempts || 0; - if ( window.prplRenderAttempts > 20 ) { - return; - } - const parentItem = document.querySelector( - `.prpl-suggested-task[data-task-id="${ parent }"]` - ); - if ( ! parentItem ) { - setTimeout( () => { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: { - item: event.detail.item, - listId: event.detail.listId, - insertPosition: event.detail.insertPosition, - }, - } ) - ); - window.prplRenderAttempts++; - }, 100 ); - return; - } - - // If the child list does not exist, create it. - if ( - ! parentItem.querySelector( '.prpl-suggested-task-children' ) - ) { - const childListElement = document.createElement( 'ul' ); - childListElement.classList.add( - 'prpl-suggested-task-children' - ); - parentItem.appendChild( childListElement ); - } - - // Inject the item into the child list. - parentItem - .querySelector( '.prpl-suggested-task-children' ) - .insertAdjacentHTML( 'beforeend', itemHTML ); - } ); -} ); - -// When the 'prpl/suggestedTask/move' event is triggered, -// update the menu_order of the todo items. -document.addEventListener( 'prpl/suggestedTask/move', ( event ) => { - const listUl = event.detail.node.closest( 'ul' ); - const todoItemsIDs = []; - // Get all the todo items. - const todoItems = listUl.querySelectorAll( '.prpl-suggested-task' ); - let menuOrder = 0; - todoItems.forEach( ( todoItem ) => { - const itemID = parseInt( todoItem.getAttribute( 'data-post-id' ) ); - todoItemsIDs.push( itemID ); - todoItem.setAttribute( 'data-task-order', menuOrder ); - - listUl - .querySelector( `.prpl-suggested-task[data-post-id="${ itemID }"]` ) - .setAttribute( 'data-task-order', menuOrder ); - - // Update an existing post. - const post = new wp.api.models.Prpl_recommendations( { - id: itemID, - menu_order: menuOrder, - } ); - post.save(); - menuOrder++; - } ); -} ); - -/* eslint-enable camelcase, jsdoc/require-param-type, jsdoc/require-param, jsdoc/check-param-names */ diff --git a/assets/js/widgets/suggested-tasks.js b/assets/js/widgets/suggested-tasks.js deleted file mode 100644 index 102529bdea..0000000000 --- a/assets/js/widgets/suggested-tasks.js +++ /dev/null @@ -1,258 +0,0 @@ -/* global prplSuggestedTask, prplTerms, prplTodoWidget, prplL10nStrings, history, prplDocumentReady */ -/* - * Widget: Suggested Tasks - * - * A widget that displays a list of suggested tasks. - * - * Dependencies: wp-api, progress-planner/document-ready, progress-planner/suggested-task, progress-planner/widgets/todo, progress-planner/celebrate, progress-planner/web-components/prpl-tooltip, progress-planner/suggested-task-terms - */ -/* eslint-disable camelcase */ - -const prplSuggestedTasksWidget = { - /** - * Remove the "Loading..." text and resize the grid items. - */ - removeLoadingItems: () => { - document.querySelector( '.prpl-suggested-tasks-loading' )?.remove(); - setTimeout( - () => window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ), - 2000 - ); - }, - - /** - * Populate the suggested tasks list. - */ - populateList: () => { - // Do nothing if the list does not exist. - if ( ! document.querySelector( '.prpl-suggested-tasks-list' ) ) { - return; - } - - // If preloaded tasks are available, inject them. - if ( 'undefined' !== typeof prplSuggestedTask.tasks ) { - // Inject the pending tasks. - if ( - Array.isArray( prplSuggestedTask.tasks.pendingTasks ) && - prplSuggestedTask.tasks.pendingTasks.length - ) { - prplSuggestedTask.injectItems( - prplSuggestedTask.tasks.pendingTasks - ); - } - - // Inject the pending celebration tasks, but only on Progress Planner dashboard page. - if ( - ! prplSuggestedTask.delayCelebration && - Array.isArray( - prplSuggestedTask.tasks.pendingCelebrationTasks - ) && - prplSuggestedTask.tasks.pendingCelebrationTasks.length - ) { - prplSuggestedTask.injectItems( - prplSuggestedTask.tasks.pendingCelebrationTasks - ); - - // Set post status to trash. - prplSuggestedTask.tasks.pendingCelebrationTasks.forEach( - ( task ) => { - const post = new wp.api.models.Prpl_recommendations( { - id: task.id, - } ); - // Destroy the post, without the force parameter. - post.destroy( { url: post.url() } ); - } - ); - - // Trigger the celebration event (trigger confetti, strike through tasks, remove from DOM). - setTimeout( () => { - // Trigger the celebration event. - document.dispatchEvent( - new CustomEvent( 'prpl/celebrateTasks' ) - ); - - /** - * Strike completed tasks and remove them from the DOM. - */ - document.dispatchEvent( - new CustomEvent( 'prpl/removeCelebratedTasks' ) - ); - - // Trigger the grid resize event. - window.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ); - }, 3000 ); - } - - // Toggle the "Loading..." text. - prplSuggestedTasksWidget.removeLoadingItems(); - } else { - // Otherwise, inject tasks from the API. - // Inject published tasks (excluding user tasks). - const tasksPerPage = - 'undefined' !== typeof prplSuggestedTask.tasksPerPage && - -1 === prplSuggestedTask.tasksPerPage - ? 100 - : prplSuggestedTask.tasksPerPage || - prplSuggestedTask.perPageDefault; - - prplSuggestedTask - .fetchItems( { - status: [ 'publish' ], - per_page: tasksPerPage, - exclude_provider: 'user', - } ) - .then( ( data ) => { - if ( data.length ) { - prplSuggestedTask.injectItems( data ); - } - } ); - - // We trigger celebration only on Progress Planner dashboard page. - if ( ! prplSuggestedTask.delayCelebration ) { - // Inject pending celebration tasks. - prplSuggestedTask - .fetchItems( { - status: [ 'pending' ], - per_page: tasksPerPage, - exclude_provider: 'user', - } ) - .then( ( data ) => { - // If there were pending tasks. - if ( data.length ) { - prplSuggestedTask.injectItems( data ); - - // Set post status to trash. - data.forEach( ( task ) => { - const post = - new wp.api.models.Prpl_recommendations( { - id: task.id, - } ); - // Destroy the post, without the force parameter. - post.destroy( { url: post.url() } ); - } ); - - // Trigger the celebration event (trigger confetti, strike through tasks, remove from DOM). - setTimeout( () => { - // Trigger the celebration event. - document.dispatchEvent( - new CustomEvent( 'prpl/celebrateTasks' ) - ); - - /** - * Strike completed tasks and remove them from the DOM. - */ - document.dispatchEvent( - new CustomEvent( - 'prpl/removeCelebratedTasks' - ) - ); - - // Trigger the grid resize event. - window.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ); - }, 3000 ); - } - } ); - } - } - }, -}; - -/** - * Populate the suggested tasks list when the terms are loaded. - */ -prplTerms.getCollectionsPromises().then( () => { - prplSuggestedTasksWidget.populateList(); - prplTodoWidget.populateList(); -} ); - -/** - * Handle the "Show all recommendations" / "Show fewer recommendations" toggle. - */ -prplDocumentReady( () => { - const toggleButton = document.getElementById( - 'prpl-toggle-all-recommendations' - ); - if ( ! toggleButton ) { - return; - } - - toggleButton.addEventListener( 'click', () => { - const showAll = toggleButton.dataset.showAll === '1'; - const newPerPage = showAll ? prplSuggestedTask.perPageDefault : 100; - - // Update button text and state. - toggleButton.textContent = showAll - ? prplL10nStrings.showAllRecommendations - : prplL10nStrings.showFewerRecommendations; - toggleButton.dataset.showAll = showAll ? '0' : '1'; - toggleButton.disabled = true; - - // Clear existing tasks. - const tasksList = document.getElementById( - 'prpl-suggested-tasks-list' - ); - tasksList.innerHTML = ''; - - // Clear the injected items tracking array so tasks can be fetched again. - prplSuggestedTask.injectedItemIds = []; - - // Show loading message. - const loadingMessage = document.createElement( 'p' ); - loadingMessage.className = 'prpl-suggested-tasks-loading'; - loadingMessage.textContent = prplL10nStrings.loadingTasks; - tasksList.parentNode.insertBefore( - loadingMessage, - tasksList.nextSibling - ); - - // Fetch and inject new tasks. - prplSuggestedTask - .fetchItems( { - status: [ 'publish' ], - per_page: newPerPage, - exclude_provider: 'user', - } ) - .then( ( data ) => { - if ( data.length ) { - prplSuggestedTask.injectItems( data ); - } - - // Remove loading message. - loadingMessage?.remove(); - - // Re-enable button. - toggleButton.disabled = false; - - // Trigger grid resize. - setTimeout( () => { - window.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ); - }, 100 ); - } ) - .catch( () => { - // On error, restore button state. - toggleButton.textContent = showAll - ? prplL10nStrings.showFewerRecommendations - : prplL10nStrings.showAllRecommendations; - toggleButton.dataset.showAll = showAll ? '1' : '0'; - toggleButton.disabled = false; - loadingMessage?.remove(); - } ); - - // Update URL without reload. - const url = new URL( window.location ); - if ( showAll ) { - url.searchParams.delete( 'prpl_show_all_recommendations' ); - } else { - url.searchParams.set( 'prpl_show_all_recommendations', '' ); - } - history.pushState( {}, '', url ); - } ); -} ); - -/* eslint-enable camelcase */ diff --git a/assets/js/widgets/todo.js b/assets/js/widgets/todo.js deleted file mode 100644 index 64d55a433c..0000000000 --- a/assets/js/widgets/todo.js +++ /dev/null @@ -1,336 +0,0 @@ -/* global prplSuggestedTask, prplTerms, prplL10n */ -/* - * Widget: Todo - * - * A widget that displays a todo list. - * - * Dependencies: wp-api, progress-planner/suggested-task, wp-util, wp-a11y, progress-planner/celebrate, progress-planner/suggested-task-terms, progress-planner/l10n - */ - -const prplTodoWidget = { - /** - * Get the highest `order` value from the todo items. - * - * @return {number} The highest `order` value. - */ - getHighestItemOrder: () => { - const items = document.querySelectorAll( - '#todo-list .prpl-suggested-task' - ); - let highestOrder = 0; - items.forEach( ( item ) => { - highestOrder = Math.max( - parseInt( item.getAttribute( 'data-task-order' ) ), - highestOrder - ); - } ); - return highestOrder; - }, - - /** - * Remove the "Loading..." text and resize the grid items. - */ - removeLoadingItems: () => { - // Remove the "Loading..." text. - document.querySelector( '#prpl-todo-list-loading' )?.remove(); - - // Resize the grid items. - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - }, - - /** - * Populate the todo list. - */ - populateList: () => { - // If preloaded tasks are available, inject them. - if ( 'undefined' !== typeof prplSuggestedTask.tasks ) { - // Inject the tasks. - if ( - Array.isArray( prplSuggestedTask.tasks.userTasks ) && - prplSuggestedTask.tasks.userTasks.length - ) { - prplSuggestedTask.tasks.userTasks.forEach( ( item ) => { - // Inject the items into the DOM. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: { - item, - insertPosition: - 1 === item?.prpl_points - ? 'afterbegin' // Add golden task to the start of the list. - : 'beforeend', - listId: - item.status === 'publish' - ? 'todo-list' - : 'todo-list-completed', - }, - } ) - ); - prplSuggestedTask.injectedItemIds.push( item.id ); - } ); - } - prplTodoWidget.removeLoadingItems(); - } else { - // Otherwise, inject tasks from the API. - prplSuggestedTask - .fetchItems( { - provider: 'user', - status: [ 'publish', 'trash' ], - per_page: 100, - } ) - .then( ( data ) => { - if ( ! data.length ) { - return data; - } - - // Inject the items into the DOM. - data.forEach( ( item ) => { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: { - item, - insertPosition: - 1 === item?.prpl_points - ? 'afterbegin' // Add golden task to the start of the list. - : 'beforeend', - listId: - item.status === 'publish' - ? 'todo-list' - : 'todo-list-completed', - }, - } ) - ); - prplSuggestedTask.injectedItemIds.push( item.id ); - } ); - - return data; - } ) - .then( () => prplTodoWidget.removeLoadingItems() ); - } - - // When the '#create-todo-item' form is submitted, - // add a new todo item to the list - document - .getElementById( 'create-todo-item' ) - ?.addEventListener( 'submit', ( event ) => { - event.preventDefault(); - - // Add the loader. - prplTodoWidget.addLoader(); - - // Create a new post - const post = new wp.api.models.Prpl_recommendations( { - // Set the post title. - title: document.getElementById( 'new-todo-content' ).value, - status: 'publish', - // Set the `prpl_recommendations_provider` term. - prpl_recommendations_provider: - prplTerms.get( 'provider' ).user.id, - menu_order: prplTodoWidget.getHighestItemOrder() + 1, - } ); - post.save().then( ( response ) => { - if ( ! response.id ) { - return; - } - const newTask = { - ...response, - meta: { - prpl_url: '', - ...( response.meta || {} ), - }, - provider: 'user', - order: prplTodoWidget.getHighestItemOrder() + 1, - prpl_points: 0, - }; - - // Inject the new task into the DOM. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: { - item: newTask, - insertPosition: - 1 === newTask.points - ? 'afterbegin' - : 'beforeend', // Add golden task to the start of the list. - listId: 'todo-list', - }, - } ) - ); - - // Remove the loader. - prplTodoWidget.removeLoader(); - - // Announce to screen readers. - prplTodoWidget.announceToScreenReader( - prplL10n( 'taskAddedSuccessfully' ) - ); - - // Resize the grid items. - window.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ); - } ); - - // Clear the new task input element. - document.getElementById( 'new-todo-content' ).value = ''; - - // Focus the new task input element. - document.getElementById( 'new-todo-content' ).focus(); - } ); - }, - - /** - * Announce to screen readers. - * - * @param {string} message The message to announce. - * @param {string} priority The priority ('polite' or 'assertive'). - */ - announceToScreenReader: ( message, priority = 'polite' ) => { - // Use WordPress a11y speak if available. - if ( 'undefined' !== typeof wp && wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, priority ); - } else { - // Fallback to ARIA live region. - const liveRegion = document.getElementById( - 'todo-aria-live-region' - ); - if ( liveRegion ) { - liveRegion.textContent = message; - setTimeout( () => { - liveRegion.textContent = ''; - }, 1000 ); - } - } - }, - - /** - * Add the loader. - */ - addLoader: () => { - const loader = document.createElement( 'span' ); - const loadingTasksText = prplL10n( 'loadingTasks' ); - loader.className = 'prpl-loader'; - loader.setAttribute( 'role', 'status' ); - loader.setAttribute( 'aria-live', 'polite' ); - loader.innerHTML = `${ loadingTasksText }`; - document.getElementById( 'todo-list' ).appendChild( loader ); - }, - - /** - * Remove the loader. - */ - removeLoader: () => { - document.querySelector( '#todo-list .prpl-loader' )?.remove(); - }, - - /** - * Show the delete all popover. - */ - showDeleteAllPopover: () => { - document - .getElementById( 'todo-list-completed-delete-all-popover' ) - .showPopover(); - }, - - /** - * Close the delete all popover. - */ - closeDeleteAllPopover: () => { - document - .getElementById( 'todo-list-completed-delete-all-popover' ) - .hidePopover(); - }, - - /** - * Delete all completed tasks and close the popover. - */ - deleteAllCompletedTasksAndClosePopover: () => { - prplTodoWidget.deleteAllCompletedTasks(); - prplTodoWidget.closeDeleteAllPopover(); - }, - - /** - * Delete all completed tasks. - */ - deleteAllCompletedTasks: () => { - const items = document.querySelectorAll( - '#todo-list-completed .prpl-suggested-task' - ); - const itemCount = items.length; - - items.forEach( ( item ) => { - const postId = parseInt( item.getAttribute( 'data-post-id' ) ); - prplSuggestedTask.trash( postId ); - } ); - - // Announce to screen readers. - const tasksWord = - itemCount === 1 - ? prplL10n( 'taskDeleted' ) - : prplL10n( 'tasksDeleted' ); - prplTodoWidget.announceToScreenReader( - `${ itemCount } ${ tasksWord }`, - 'assertive' - ); - - // Resize event will be triggered by the trash function. - }, -}; - -document - .getElementById( 'todo-list-completed-details' ) - ?.addEventListener( 'toggle', () => { - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - } ); - -// Add event listener for delete all button. -document - .getElementById( 'todo-list-completed-delete-all' ) - ?.addEventListener( 'click', () => { - prplTodoWidget.showDeleteAllPopover(); - } ); - -// Add event listener for cancel button in delete all popover. -document - .getElementById( 'todo-list-completed-delete-all-cancel' ) - ?.addEventListener( 'click', () => { - prplTodoWidget.closeDeleteAllPopover(); - } ); - -// Add event listener for confirm button in delete all popover. -document - .getElementById( 'todo-list-completed-delete-all-confirm' ) - ?.addEventListener( 'click', () => { - prplTodoWidget.deleteAllCompletedTasksAndClosePopover(); - } ); - -document.addEventListener( 'prpl/suggestedTask/itemInjected', ( event ) => { - if ( 'todo-list' !== event.detail.listId ) { - return; - } - setTimeout( () => { - // Get all items in the list. - const items = document.querySelectorAll( - `#${ event.detail.listId } .prpl-suggested-task` - ); - - // Reorder items based on their `data-task-order` attribute. - const orderedItems = Array.from( items ).sort( ( a, b ) => { - return ( - parseInt( a.getAttribute( 'data-task-order' ) ) - - parseInt( b.getAttribute( 'data-task-order' ) ) - ); - } ); - - // Remove all items from the list. - items.forEach( ( item ) => item.remove() ); - - // Inject the ordered items back into the list. - orderedItems.forEach( ( item ) => - document.getElementById( event.detail.listId ).appendChild( item ) - ); - - // Resize the grid items. - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - } ); -} ); diff --git a/assets/src/hooks/useGridMasonry.js b/assets/src/hooks/useGridMasonry.js new file mode 100644 index 0000000000..0beba79ce2 --- /dev/null +++ b/assets/src/hooks/useGridMasonry.js @@ -0,0 +1,96 @@ +/** + * useGridMasonry Hook + * + * Handles CSS grid masonry layout for dashboard widgets. + * Listens for 'prpl/grid/resize' events and calculates grid-row-end spans. + */ + +import { useEffect } from '@wordpress/element'; + +/** + * Hook to handle grid masonry layout for widgets. + * + * This hook sets up event listeners for: + * - 'prpl/grid/resize' custom event (dispatched by widgets when content changes) + * - 'resize' window event (for responsive layout) + * - 'load' window event (for initial layout after all resources load) + */ +export function useGridMasonry() { + useEffect( () => { + /** + * Handle grid resize by calculating row spans for each widget. + */ + const handleGridResize = () => { + document + .querySelectorAll( '.prpl-widget-wrapper' ) + .forEach( ( item ) => { + if ( ! item || item.classList.contains( 'in-popover' ) ) { + return; + } + + const innerContainer = item.querySelector( + '.widget-inner-container' + ); + if ( ! innerContainer ) { + return; + } + + const container = document.querySelector( + '.prpl-widgets-container' + ); + if ( ! container ) { + return; + } + + const rowHeight = parseInt( + window + .getComputedStyle( container ) + .getPropertyValue( 'grid-auto-rows' ) + ); + + const paddingTop = parseInt( + window + .getComputedStyle( item ) + .getPropertyValue( 'padding-top' ) + ); + const paddingBottom = parseInt( + window + .getComputedStyle( item ) + .getPropertyValue( 'padding-bottom' ) + ); + + const rowSpan = Math.ceil( + ( innerContainer.getBoundingClientRect().height + + paddingTop + + paddingBottom ) / + rowHeight + ); + + item.style.gridRowEnd = 'span ' + ( rowSpan + 1 ); + } ); + }; + + /** + * Trigger resize with a small delay. + */ + const triggerResize = () => { + setTimeout( handleGridResize, 0 ); + }; + + // Listen for custom event from widgets. + window.addEventListener( 'prpl/grid/resize', handleGridResize ); + window.addEventListener( 'resize', triggerResize ); + window.addEventListener( 'load', triggerResize ); + + // Initial calls. + triggerResize(); + setTimeout( triggerResize, 1000 ); + + // Cleanup on unmount. + return () => { + window.removeEventListener( 'prpl/grid/resize', handleGridResize ); + window.removeEventListener( 'resize', triggerResize ); + window.removeEventListener( 'load', triggerResize ); + }; + }, [] ); +} diff --git a/assets/src/widgets/SuggestedTasks/index.js b/assets/src/widgets/SuggestedTasks/index.js index db6dfb9e96..0350de2f1b 100644 --- a/assets/src/widgets/SuggestedTasks/index.js +++ b/assets/src/widgets/SuggestedTasks/index.js @@ -16,6 +16,7 @@ import { updateTask, sendTaskAction, } from './hooks/useTasksApi'; +import { useGridMasonry } from '../../hooks/useGridMasonry'; /** * Suggested Tasks widget component. @@ -32,6 +33,9 @@ export default function SuggestedTasks() { const listRef = useRef( null ); const injectedTaskIdsRef = useRef( new Set() ); + // Initialize grid masonry layout. + useGridMasonry(); + /** * Load tasks on component mount. */ diff --git a/assets/src/widgets/TodoWidget/index.js b/assets/src/widgets/TodoWidget/index.js index 0f41e9902e..688279baa5 100644 --- a/assets/src/widgets/TodoWidget/index.js +++ b/assets/src/widgets/TodoWidget/index.js @@ -7,6 +7,7 @@ import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; +import { useGridMasonry } from '../../hooks/useGridMasonry'; /** * Fetch user tasks from API. @@ -257,6 +258,9 @@ export default function TodoWidget() { const [ showDeletePopover, setShowDeletePopover ] = useState( false ); const inputRef = useRef( null ); + // Initialize grid masonry layout. + useGridMasonry(); + /** * Load tasks on mount. */ diff --git a/build/suggested-tasks.asset.php b/build/suggested-tasks.asset.php index 24cf00715a..658ef26c03 100644 --- a/build/suggested-tasks.asset.php +++ b/build/suggested-tasks.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'ba74c51066ae5072eacb'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => 'b0b7c1f5b951f955ff02'); diff --git a/build/suggested-tasks.js b/build/suggested-tasks.js index 16865d1e64..cd2096408a 100644 --- a/build/suggested-tasks.js +++ b/build/suggested-tasks.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var n in s)e.o(s,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:s[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,s=window.wp.i18n,n=window.ReactJSXRuntime;function a({task:e,isUserTask:a,onComplete:r,onSnooze:o,onDelete:i}){const l=(0,t.useRef)(null);(0,t.useEffect)(()=>{if(!l.current)return;const t=l.current,s=t.querySelectorAll('[data-action="complete"]');s.forEach(t=>{t.addEventListener("click",t=>{t.preventDefault(),r(e.id,e)})});const n=t.querySelectorAll('.prpl-snooze-duration-radio-group input[type="radio"]');return n.forEach(t=>{t.addEventListener("change",()=>{o(e.id,t.value)})}),t.querySelectorAll('a[onclick*="showPopover"]').forEach(e=>{const t=e.getAttribute("onclick"),s=t?.match(/getElementById\(['"]([^'"]+)['"]\)/);if(s){const t=s[1];e.removeAttribute("onclick"),e.addEventListener("click",e=>{e.preventDefault();const s=document.getElementById(t);s&&"function"==typeof s.showPopover&&s.showPopover()})}}),t.querySelectorAll(".prpl-suggested-task-button.trash").forEach(t=>{t.addEventListener("click",t=>{t.preventDefault(),i(e.id)})}),()=>{s.forEach(e=>{e.replaceWith(e.cloneNode(!0))}),n.forEach(e=>{e.replaceWith(e.cloneNode(!0))})}},[e,r,o,i]);const c=e.prpl_task_actions||[];return 0!==c.length||a?(0,n.jsxs)("div",{className:"tooltip-actions",ref:l,children:[c.map((e,t)=>(0,n.jsx)("span",{className:"tooltip-action",dangerouslySetInnerHTML:{__html:e}},t)),a&&(0,n.jsx)("span",{className:"tooltip-action",children:(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button trash","data-post-id":e.id,title:(0,s.__)("Delete","progress-planner"),onClick:()=>i(e.id),children:[(0,n.jsx)("span",{className:"prpl-tooltip-action-text",children:(0,s.__)("Delete","progress-planner")}),(0,n.jsxs)("span",{className:"screen-reader-text",children:[(0,s.__)("Delete","progress-planner"),":"," ",e.title?.rendered||e.title]})]})})]}):(0,n.jsx)("div",{className:"tooltip-actions"})}function r(){return(0,n.jsx)("span",{style:{width:"0.75rem",height:"100%",display:"flex",alignItems:"center",justifyContent:"center"},children:(0,n.jsx)("svg",{role:"img","aria-hidden":"true",focusable:"false",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 17",children:(0,n.jsx)("path",{fill:"#6b7280",d:"M19.92 8.12c-.05-.12-.12-.23-.22-.33L12.21.29A.996.996 0 1 0 10.8 1.7l5.79 5.79H1c-.55 0-1 .45-1 1s.45 1 1 1h15.59l-5.79 5.79a.996.996 0 0 0 .71 1.7c.26 0 .51-.1.71-.29l7.5-7.5c.1-.1.17-.21.22-.33.05-.12.07-.24.08-.38 0-.14-.03-.27-.08-.38Z"})})})}function o(){return(0,n.jsx)("svg",{role:"img","aria-hidden":"true",focusable:"false",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",children:(0,n.jsx)("path",{fill:"#9ca3af",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Zm-17.98-3h17.97c1.9 0 3.51-1.48 3.65-3.38l2.34-30.46c-2.15-.3-4.33-.53-6.48-.7h-.03c-5.62-.43-11.32-.43-16.95 0h-.03c-2.15.17-4.33.4-6.48.7l2.34 30.46c.15 1.9 1.75 3.38 3.65 3.38ZM24 7.01c2.37 0 4.74.07 7.11.22v-.49c0-1.93-1.47-3.49-3.34-3.55-2.5-.08-5.03-.08-7.52 0-1.88.06-3.34 1.62-3.34 3.55v.49c2.36-.15 4.73-.22 7.11-.22Zm5.49 32.26h-.06c-.83-.03-1.47-.73-1.44-1.56l.79-20.65c.03-.83.75-1.45 1.56-1.44.83.03 1.47.73 1.44 1.56l-.79 20.65c-.03.81-.7 1.44-1.5 1.44Zm-10.98 0c-.8 0-1.47-.63-1.5-1.44l-.79-20.65c-.03-.83.61-1.52 1.44-1.56.84 0 1.52.61 1.56 1.44l.79 20.65c.03.83-.61 1.52-1.44 1.56h-.06Z"})})}function i({task:e,isUserTask:i,isCelebrating:l,onComplete:c,onSnooze:p,onDelete:d,onMove:u,onTitleChange:g}){const m=(0,t.useRef)(null),h=(0,t.useRef)(null),w="trash"===e.status||"pending"===e.status,y=(0,t.useCallback)(()=>{c(e.id,e)},[e,c]),f=(0,t.useCallback)(e=>{if("Enter"===e.key)return e.preventDefault(),e.stopPropagation(),e.target.blur(),!1},[]),v=(0,t.useCallback)(()=>{clearTimeout(h.current),h.current=setTimeout(()=>{if(m.current){const t=m.current.textContent.replace(/\n/g,"");g(e.id,t)}},300)},[e.id,g]),b=(0,t.useCallback)(()=>{u(e.id,"up")},[e.id,u]),_=(0,t.useCallback)(()=>{u(e.id,"down")},[e.id,u]),k=(0,t.useCallback)(()=>{d(e.id)},[e.id,d]),S=e.slug||e.id,x=e.prpl_provider?.slug||"",C=["prpl-suggested-task",l?"prpl-suggested-task-celebrated":""].filter(Boolean).join(" ");return(0,n.jsxs)("li",{className:C,"data-task-id":S,"data-post-id":e.id,"data-task-action":"pending"===e.status||l?"celebrate":"","data-task-provider-id":x,"data-task-points":e.prpl_points||0,"data-task-order":e.menu_order||0,children:[(0,n.jsx)("div",{className:"prpl-suggested-task-checkbox-wrapper",children:i?(0,n.jsxs)("label",{children:[(0,n.jsx)("input",{type:"checkbox",className:"prpl-suggested-task-checkbox",onChange:y,style:{margin:0},checked:w,disabled:l}),(0,n.jsxs)("span",{className:"screen-reader-text",children:[e.title?.rendered||e.title,":"," ",(0,s.__)("Mark as complete","progress-planner")]})]}):(0,n.jsx)(r,{})}),(0,n.jsx)("div",{className:"prpl-suggested-task-title-wrapper",children:(0,n.jsx)("h3",{className:"prpl-task-title",children:i?(0,n.jsx)("span",{ref:m,contentEditable:"plaintext-only",role:"textbox",tabIndex:0,"aria-label":(0,s.__)("Edit task title","progress-planner"),"aria-multiline":"false",onKeyDown:f,onInput:v,suppressContentEditableWarning:!0,dangerouslySetInnerHTML:{__html:e.title?.rendered||e.title}}):(0,n.jsx)("span",{dangerouslySetInnerHTML:{__html:e.title?.rendered||e.title}})})}),(0,n.jsxs)("div",{className:"prpl-suggested-task-points-wrapper",children:[e.prpl_points>0&&(0,n.jsxs)("span",{className:"prpl-suggested-task-points",children:["+",e.prpl_points]}),i&&(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button trash","data-post-id":e.id,title:(0,s.__)("Delete","progress-planner"),onClick:k,children:[(0,n.jsx)(o,{}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Delete","progress-planner")})]})]}),i&&(0,n.jsx)("div",{className:"tooltip-actions prpl-move-buttons-wrapper",children:(0,n.jsxs)("span",{className:"prpl-move-buttons",children:[(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button move-up","data-task-id":S,"data-task-title":e.title?.rendered||e.title,"data-action":"move-up","data-target":"move-up",title:(0,s.__)("Move up","progress-planner"),onClick:b,children:[(0,n.jsx)("span",{className:"dashicons dashicons-arrow-up-alt2"}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Move up","progress-planner")})]}),(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button move-down","data-task-id":S,"data-task-title":e.title?.rendered||e.title,"data-action":"move-down","data-target":"move-down",title:(0,s.__)("Move down","progress-planner"),onClick:_,children:[(0,n.jsx)("span",{className:"dashicons dashicons-arrow-down-alt2"}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Move down","progress-planner")})]})]})}),(0,n.jsx)("div",{className:"prpl-suggested-task-actions-wrapper",children:(0,n.jsx)(a,{task:e,isUserTask:i,onComplete:c,onSnooze:p,onDelete:d})})]})}const l=window.wp.apiFetch;var c=e.n(l);function p(e){let t=e.querySelector('button[type="submit"]');if(t||(t=e.querySelector('button[data-action="completeTask"]')),t){t.disabled=!0;const e=document.createElement("span");e.classList.add("prpl-spinner"),e.innerHTML='',t.after(e)}}function d(e){let t=e.querySelector('button[type="submit"]');t||(t=e.querySelector('button[data-action="completeTask"]')),t&&(t.disabled=!1);const s=e.querySelector("span.prpl-spinner");s&&s.remove()}function u(e){const t=document.querySelector(`#${e} form`);if(t&&!t.parentNode.querySelector("p.prpl-interactive-task-error-message")){const e=document.createElement("p");e.classList.add("prpl-note","prpl-note-error","prpl-interactive-task-error-message"),e.textContent=(0,s.__)("Something went wrong. Please try again.","progress-planner"),t.insertAdjacentElement("afterend",e)}}function g(e){const t=document.querySelector(`#${e} form`);if(!t)return;const s=t.parentNode.querySelector("p.prpl-interactive-task-error-message");s&&s.remove()}async function m({setting:e,settingPath:t=!1,popoverId:s,action:n="prpl_interactive_task_submit",settingCallbackValue:a=e=>e}){const r=document.querySelector(`#${s} form`);if(!r)throw new Error("Form not found");p(r),g(s);try{const o=a(new FormData(r).get(e)),i=window.prplSuggestedTasksConfig?.ajaxUrl||window.progressPlanner?.ajaxUrl||"/wp-admin/admin-ajax.php",l=window.prplSuggestedTasksConfig?.nonce||window.progressPlanner?.nonce||"",c=new URLSearchParams({action:n,_ajax_nonce:l,setting:e,value:o});t&&c.append("setting_path",t);const p=await fetch(i,{method:"POST",body:c,credentials:"same-origin"}),g=await p.json();if(d(r),!0!==g.success)throw u(s),new Error("Settings update failed");return g}catch(e){throw d(r),u(s),e}}async function h(e,t="posts"){return c()({path:`/wp/v2/${t}/${e}?force=true`,method:"DELETE"})}const w={"core-blogdescription":{type:"siteSettings",settingAPIKey:"description",setting:"blogdescription"},"disable-comments":{type:"siteSettings",settingAPIKey:"default_comment_status",setting:"default_comment_status",settingCallbackValue:()=>"closed"},"disable-comment-pagination":{type:"siteSettings",settingAPIKey:"page_comments",setting:"page_comments",settingCallbackValue:()=>!1},"select-locale":{type:"siteSettings",settingAPIKey:"WPLANG",setting:"WPLANG"},"select-timezone":{type:"siteSettings",settingAPIKey:"timezone_string",setting:"timezone_string"},"search-engine-visibility":{type:"pluginSettings",setting:"blog_public",action:"prpl_interactive_task_submit",settingCallbackValue:()=>"1"},"set-date-format":{type:"siteSettings",settingAPIKey:"date_format",setting:"date_format"},"core-permalink-structure":{type:"siteSettings",settingAPIKey:"permalink_structure",setting:"permalink_structure"},"rename-uncategorized-category":{type:"customSubmit"},"hello-world":{type:"customSubmit"},"sample-page":{type:"customSubmit"},"yoast-author-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-author"]),settingCallbackValue:()=>!0},"yoast-date-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-date"]),settingCallbackValue:()=>!0},"yoast-format-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-post_format"]),settingCallbackValue:()=>!0},"yoast-media-pages":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-attachment"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-emoji-scripts":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_emoji_scripts"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-feed-authors":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_feed_authors"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-feed-global-comments":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_feed_global_comments"]),settingCallbackValue:()=>!0},"yoast-organization-logo":{type:"customSubmit"},"aioseo-author-archive":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["archives","author","show"]),settingCallbackValue:()=>!1},"aioseo-date-archive":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["archives","date","show"]),settingCallbackValue:()=>!1},"aioseo-media-pages":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["postTypes","attachment","show"]),settingCallbackValue:()=>!1},"aioseo-crawl-settings-feed-authors":{type:"pluginSettings",setting:"aioseo_options_rss_content",settingPath:JSON.stringify(["authorFeed"]),settingCallbackValue:()=>!1},"aioseo-crawl-settings-feed-comments":{type:"pluginSettings",setting:"aioseo_options_rss_content",settingPath:JSON.stringify(["commentFeed"]),settingCallbackValue:()=>!1},"core-siteicon":{type:"customSubmit"},"update-term-description":{type:"customSubmit"},"remove-terms-without-posts":{type:"customSubmit"}};function y({tasks:e,onComplete:s}){const n=(0,t.useCallback)(async(e,t)=>{switch(e){case"hello-world":{const e=window.helloWorldData?.postId;return e&&await h(e,"posts"),{success:!0}}case"sample-page":{const e=window.samplePageData?.postId;return e&&await h(e,"pages"),{success:!0}}case"rename-uncategorized-category":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("category_name"),s=window.renameUncategorizedCategoryData?.termId;s&&t&&await c()({path:`/wp/v2/categories/${s}`,method:"POST",data:{name:t}})}return{success:!0}}case"core-siteicon":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("site_icon");t&&await c()({path:"/wp/v2/settings",method:"POST",data:{site_icon:parseInt(t)}})}return{success:!0}}case"yoast-organization-logo":{const e=document.querySelector(`#${t} form`);if(e){const s=new FormData(e).get("company_logo_id");s&&await m({setting:"wpseo",settingPath:JSON.stringify(["company_logo_id"]),popoverId:t,settingCallbackValue:()=>parseInt(s)})}return{success:!0}}case"update-term-description":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("description"),s=window.updateTermDescriptionData?.termId,n=window.updateTermDescriptionData?.taxonomy||"category";if(s&&t){const e="category"===n?"categories":n;await c()({path:`/wp/v2/${e}/${s}`,method:"POST",data:{description:t}})}}return{success:!0}}case"remove-terms-without-posts":if(document.querySelector(`#${t} form`)){const e=window.removeTermsWithoutPostsData?.termIds||[],t=window.removeTermsWithoutPostsData?.taxonomy||"category",s="category"===t?"categories":t;await Promise.all(e.map(e=>c()({path:`/wp/v2/${s}/${e}?force=true`,method:"DELETE"})))}return{success:!0};default:return{success:!0}}},[]),a=(0,t.useCallback)(async(t,a,r)=>{try{const o=e.find(e=>e.slug===t||e.prpl_provider?.slug===t||`${e.id}`===t);if(!o)return;let i;switch(r.type){case"siteSettings":i=async function({settingAPIKey:e,setting:t,popoverId:s,settingCallbackValue:n=e=>e}){const a=document.querySelector(`#${s} form`);if(!a)throw new Error("Form not found");p(a),g(s);try{const s=n(new FormData(a).get(t)),r=await c()({path:"/wp/v2/settings",method:"POST",data:{[e]:s}});return d(a),r}catch(e){throw d(a),u(s),e}}({settingAPIKey:r.settingAPIKey,setting:r.setting,popoverId:a,settingCallbackValue:r.settingCallbackValue});break;case"pluginSettings":i=m({setting:r.setting,settingPath:r.settingPath,popoverId:a,action:r.action||"prpl_interactive_task_submit",settingCallbackValue:r.settingCallbackValue});break;case"customSubmit":i=n(t,a);break;default:return}await i,await s(o.id,o),function(e){const t=document.getElementById(e);t&&"function"==typeof t.hidePopover&&t.hidePopover()}(a)}catch(e){console.error("Popover form submission error:",e)}},[e,s,n]);return(0,t.useEffect)(()=>{const e=new Map;Object.entries(w).forEach(([t,s])=>{const n=`prpl-popover-${t}`,r=document.querySelector(`#${n} form`);if(!r)return;const o=e=>{e.preventDefault(),a(t,n,s)};r.addEventListener("submit",o),e.set(n,{formElement:r,handler:o})});const t=document.querySelector("input#blogdescription");if(t){const e=document.querySelector('#prpl-popover-core-blogdescription button[type="submit"]');if(e){const s=t=>{e.disabled=0===t.target.value.length};t.addEventListener("input",s)}}return function(){const e=document.querySelectorAll('#prpl-popover-set-date-format input[name="date_format"]'),t=document.querySelector('#prpl-popover-set-date-format input[name="date_format_custom"]');if(!e.length||!t)return;let s;e.forEach(e=>{e.addEventListener("change",()=>{"custom"===e.value?(t.disabled=!1,t.focus()):t.disabled=!0})}),t.addEventListener("input",()=>{clearTimeout(s),s=setTimeout(async()=>{const e=t.value;if(e)try{const s=window.prplSuggestedTasksConfig?.ajaxUrl||window.progressPlanner?.ajaxUrl||"/wp-admin/admin-ajax.php",n=window.prplSuggestedTasksConfig?.nonce||window.progressPlanner?.nonce||"",a=await fetch(`${s}?action=prpl_date_format_preview&format=${encodeURIComponent(e)}&_ajax_nonce=${n}`,{credentials:"same-origin"}),r=await a.json();if(r.success&&r.data){const e=t.closest(".prpl-radio-wrapper")?.querySelector(".date-time-text");e&&(e.textContent=r.data)}}catch{}},300)})}(),function(){const e=document.querySelectorAll('#prpl-popover-core-permalink-structure input[name="permalink_structure"]');if(!e.length)return;const t=document.querySelector('#prpl-popover-core-permalink-structure input[name="permalink_custom"]');e.forEach(e=>{e.addEventListener("change",()=>{"custom"===e.value&&t?(t.disabled=!1,t.focus()):t&&(t.disabled=!0)})})}(),function(){const e=document.querySelector("#prpl-popover-core-siteicon .prpl-upload-site-icon");if(e&&window.wp?.media){let t;e.addEventListener("click",s=>{s.preventDefault(),t||(t=window.wp.media({title:e.dataset.title||"Select Site Icon",button:{text:e.dataset.button||"Use as site icon"},multiple:!1,library:{type:"image"}}),t.on("select",()=>{const e=t.state().get("selection").first().toJSON(),s=document.querySelector('#prpl-popover-core-siteicon input[name="site_icon"]');s&&(s.value=e.id);const n=document.querySelector("#prpl-popover-core-siteicon .prpl-site-icon-preview img");n&&(n.src=e.url);const a=document.querySelector('#prpl-popover-core-siteicon button[type="submit"]');a&&(a.disabled=!1)})),t.open()})}const t=document.querySelector("#prpl-popover-yoast-organization-logo .prpl-upload-logo");if(t&&window.wp?.media){let e;t.addEventListener("click",s=>{s.preventDefault(),e||(e=window.wp.media({title:t.dataset.title||"Select Logo",button:{text:t.dataset.button||"Use as logo"},multiple:!1,library:{type:"image"}}),e.on("select",()=>{const t=e.state().get("selection").first().toJSON(),s=document.querySelector('#prpl-popover-yoast-organization-logo input[name="company_logo_id"]');s&&(s.value=t.id);const n=document.querySelector("#prpl-popover-yoast-organization-logo .prpl-logo-preview img");n&&(n.src=t.url);const a=document.querySelector('#prpl-popover-yoast-organization-logo button[type="submit"]');a&&(a.disabled=!1)})),e.open()})}}(),()=>{e.forEach(({formElement:e,handler:t})=>{e.removeEventListener("submit",t)})}},[e,a]),null}const f={"1-week":7,"2-weeks":14,"1-month":30,"3-months":90,"6-months":180,"1-year":365,forever:3650};async function v({status:e="publish",perPage:t=100,excludeProvider:s,provider:n,excludeIds:a=[]}={}){const r={status:e,per_page:t,_embed:!0,"filter[orderby]":"menu_order","filter[order]":"ASC"};s&&(r.exclude_provider=s),n&&(r.provider=n),a.length>0&&(r.exclude=a.join(","));const o=function(e){const t=new URLSearchParams;return Object.entries(e).forEach(([e,s])=>{Array.isArray(s)?s.forEach(s=>t.append(e,s)):null!=s&&""!==s&&t.append(e,s)}),t.toString()}(r);try{return await c()({path:`/wp/v2/prpl_recommendations?${o}`})||[]}catch(e){return console.error("Error fetching tasks:",e),[]}}async function b(e){return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:{status:"trash"}})}async function _(e,t){return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:t})}async function k(e,t){const s=window.prplSuggestedTasksConfig?.nonce||"",n=window.prplSuggestedTasksConfig?.ajaxUrl||"/wp-admin/admin-ajax.php",a=new FormData;a.append("action","progress_planner_suggested_task_action"),a.append("post_id",e),a.append("action_type",t),a.append("nonce",s);try{return(await fetch(n,{method:"POST",body:a,credentials:"same-origin"})).json()}catch(e){return console.error("Error sending task action:",e),null}}function S(){const[e,a]=(0,t.useState)([]),[r,o]=(0,t.useState)(!0),[l,p]=(0,t.useState)(window.prplSuggestedTasksConfig?.showAll||!1),[d,u]=(0,t.useState)(new Set),g=(0,t.useRef)(null),m=(0,t.useRef)(new Set);(0,t.useEffect)(()=>{(async()=>{try{const e=l?100:window.prplSuggestedTasksConfig?.perPage||5,t=await v({status:"publish",perPage:e,excludeProvider:"user"});if(t.forEach(e=>{m.current.add(e.id)}),a(t),o(!1),!window.prplSuggestedTasksConfig?.delayCelebration){const t=await v({status:"pending",perPage:e,excludeProvider:"user"});t.length>0&&(a(e=>[...e,...t]),t.forEach(e=>{m.current.add(e.id)}),t.forEach(e=>{b(e.id).catch(()=>{})}),setTimeout(()=>{const e=new Set(t.map(e=>e.id));u(e),document.dispatchEvent(new CustomEvent("prpl/celebrateTasks")),setTimeout(()=>{a(t=>t.filter(t=>!e.has(t.id))),u(new Set),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},2e3)},3e3))}setTimeout(()=>{window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},100)}catch{o(!1)}})()},[l]);const h=(0,t.useCallback)(async(e,t)=>{try{u(t=>new Set([...t,e])),await b(e),k(e,"complete");const s=parseInt(t.prpl_points)||0;if(s>0&&"function"==typeof window.prplUpdateRaviGauge&&window.prplUpdateRaviGauge(s),s>0&&g.current){const t=g.current.querySelector(`[data-post-id="${e}"]`);document.dispatchEvent(new CustomEvent("prpl/celebrateTasks",{detail:{element:t}}))}setTimeout(async()=>{a(t=>t.filter(t=>t.id!==e)),u(t=>{const s=new Set(t);return s.delete(e),s});const t=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});t.length>0&&(a(e=>[...e,t[0]]),m.current.add(t[0].id)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},2e3)}catch{u(t=>{const s=new Set(t);return s.delete(e),s})}},[]),w=(0,t.useCallback)(async(e,t)=>{try{await async function(e,t){const s=f[t]||7,n=new Date(Date.now()+24*s*60*60*1e3).toISOString().split(".")[0];return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:{status:"future",date:n,date_gmt:n}})}(e,t),a(t=>t.filter(t=>t.id!==e));const s=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});s.length>0&&(a(e=>[...e,s[0]]),m.current.add(s[0].id)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch{}},[]),S=(0,t.useCallback)(async e=>{try{await async function(e){return c()({path:`/wp/v2/prpl_recommendations/${e}?force=true`,method:"DELETE"})}(e),k(e,"delete"),a(t=>t.filter(t=>t.id!==e));const t=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});t.length>0&&(a(e=>[...e,t[0]]),m.current.add(t[0].id)),setTimeout(()=>{window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},500)}catch{}},[]),x=(0,t.useCallback)(async(e,t)=>{try{await _(e,{title:t})}catch{}},[]),C=(0,t.useCallback)(async(t,s)=>{const n=e.findIndex(e=>e.id===t);if(-1===n)return;const r="up"===s?n-1:n+1;if(r<0||r>=e.length)return;const o=[...e],[i]=o.splice(n,1);o.splice(r,0,i),a(o),o.forEach((e,t)=>{_(e.id,{menu_order:t}).catch(()=>{})})},[e]),j=(0,t.useCallback)(async()=>{const e=!l;p(e),o(!0),m.current.clear();const t=new URL(window.location);e?t.searchParams.set("prpl_show_all_recommendations",""):t.searchParams.delete("prpl_show_all_recommendations"),window.history.pushState({},"",t)},[l]);return r?(0,n.jsx)("p",{className:"prpl-suggested-tasks-loading",children:(0,s.__)("Loading tasks…","progress-planner")}):0===e.length?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("ul",{id:"prpl-suggested-tasks-list",className:"prpl-suggested-tasks-list",ref:g}),(0,n.jsxs)("p",{className:"prpl-no-suggested-tasks",children:[(0,s.__)("You have completed all recommended tasks.","progress-planner"),(0,n.jsx)("br",{}),(0,s.__)("Check back later for new tasks!","progress-planner")]})]}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(y,{tasks:e,onComplete:h}),(0,n.jsx)("ul",{style:{display:"none"}}),(0,n.jsx)("ul",{id:"prpl-suggested-tasks-list",className:"prpl-suggested-tasks-list",ref:g,children:e.map(e=>(0,n.jsx)(i,{task:e,isUserTask:"user"===e.prpl_provider?.slug,isCelebrating:d.has(e.id),onComplete:h,onSnooze:w,onDelete:S,onMove:C,onTitleChange:x},e.id))}),(0,n.jsx)("p",{className:"prpl-show-all-tasks",children:(0,n.jsx)("button",{type:"button",id:"prpl-toggle-all-recommendations",className:"prpl-toggle-all-recommendations-button",onClick:j,children:l?(0,s.__)("Show fewer recommendations","progress-planner"):(0,s.__)("Show all recommendations","progress-planner")})})]})}function x(){const e=document.getElementById("prpl-suggested-tasks-root");e&&(0,t.createRoot)(e).render((0,n.jsx)(S,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",x):x()})(); \ No newline at end of file +(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var n in s)e.o(s,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:s[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,s=window.wp.i18n,n=window.ReactJSXRuntime;function a({task:e,isUserTask:a,onComplete:r,onSnooze:o,onDelete:i}){const l=(0,t.useRef)(null);(0,t.useEffect)(()=>{if(!l.current)return;const t=l.current,s=t.querySelectorAll('[data-action="complete"]');s.forEach(t=>{t.addEventListener("click",t=>{t.preventDefault(),r(e.id,e)})});const n=t.querySelectorAll('.prpl-snooze-duration-radio-group input[type="radio"]');return n.forEach(t=>{t.addEventListener("change",()=>{o(e.id,t.value)})}),t.querySelectorAll('a[onclick*="showPopover"]').forEach(e=>{const t=e.getAttribute("onclick"),s=t?.match(/getElementById\(['"]([^'"]+)['"]\)/);if(s){const t=s[1];e.removeAttribute("onclick"),e.addEventListener("click",e=>{e.preventDefault();const s=document.getElementById(t);s&&"function"==typeof s.showPopover&&s.showPopover()})}}),t.querySelectorAll(".prpl-suggested-task-button.trash").forEach(t=>{t.addEventListener("click",t=>{t.preventDefault(),i(e.id)})}),()=>{s.forEach(e=>{e.replaceWith(e.cloneNode(!0))}),n.forEach(e=>{e.replaceWith(e.cloneNode(!0))})}},[e,r,o,i]);const c=e.prpl_task_actions||[];return 0!==c.length||a?(0,n.jsxs)("div",{className:"tooltip-actions",ref:l,children:[c.map((e,t)=>(0,n.jsx)("span",{className:"tooltip-action",dangerouslySetInnerHTML:{__html:e}},t)),a&&(0,n.jsx)("span",{className:"tooltip-action",children:(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button trash","data-post-id":e.id,title:(0,s.__)("Delete","progress-planner"),onClick:()=>i(e.id),children:[(0,n.jsx)("span",{className:"prpl-tooltip-action-text",children:(0,s.__)("Delete","progress-planner")}),(0,n.jsxs)("span",{className:"screen-reader-text",children:[(0,s.__)("Delete","progress-planner"),":"," ",e.title?.rendered||e.title]})]})})]}):(0,n.jsx)("div",{className:"tooltip-actions"})}function r(){return(0,n.jsx)("span",{style:{width:"0.75rem",height:"100%",display:"flex",alignItems:"center",justifyContent:"center"},children:(0,n.jsx)("svg",{role:"img","aria-hidden":"true",focusable:"false",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 17",children:(0,n.jsx)("path",{fill:"#6b7280",d:"M19.92 8.12c-.05-.12-.12-.23-.22-.33L12.21.29A.996.996 0 1 0 10.8 1.7l5.79 5.79H1c-.55 0-1 .45-1 1s.45 1 1 1h15.59l-5.79 5.79a.996.996 0 0 0 .71 1.7c.26 0 .51-.1.71-.29l7.5-7.5c.1-.1.17-.21.22-.33.05-.12.07-.24.08-.38 0-.14-.03-.27-.08-.38Z"})})})}function o(){return(0,n.jsx)("svg",{role:"img","aria-hidden":"true",focusable:"false",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",children:(0,n.jsx)("path",{fill:"#9ca3af",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Zm-17.98-3h17.97c1.9 0 3.51-1.48 3.65-3.38l2.34-30.46c-2.15-.3-4.33-.53-6.48-.7h-.03c-5.62-.43-11.32-.43-16.95 0h-.03c-2.15.17-4.33.4-6.48.7l2.34 30.46c.15 1.9 1.75 3.38 3.65 3.38ZM24 7.01c2.37 0 4.74.07 7.11.22v-.49c0-1.93-1.47-3.49-3.34-3.55-2.5-.08-5.03-.08-7.52 0-1.88.06-3.34 1.62-3.34 3.55v.49c2.36-.15 4.73-.22 7.11-.22Zm5.49 32.26h-.06c-.83-.03-1.47-.73-1.44-1.56l.79-20.65c.03-.83.75-1.45 1.56-1.44.83.03 1.47.73 1.44 1.56l-.79 20.65c-.03.81-.7 1.44-1.5 1.44Zm-10.98 0c-.8 0-1.47-.63-1.5-1.44l-.79-20.65c-.03-.83.61-1.52 1.44-1.56.84 0 1.52.61 1.56 1.44l.79 20.65c.03.83-.61 1.52-1.44 1.56h-.06Z"})})}function i({task:e,isUserTask:i,isCelebrating:l,onComplete:c,onSnooze:p,onDelete:d,onMove:u,onTitleChange:g}){const m=(0,t.useRef)(null),w=(0,t.useRef)(null),h="trash"===e.status||"pending"===e.status,y=(0,t.useCallback)(()=>{c(e.id,e)},[e,c]),f=(0,t.useCallback)(e=>{if("Enter"===e.key)return e.preventDefault(),e.stopPropagation(),e.target.blur(),!1},[]),v=(0,t.useCallback)(()=>{clearTimeout(w.current),w.current=setTimeout(()=>{if(m.current){const t=m.current.textContent.replace(/\n/g,"");g(e.id,t)}},300)},[e.id,g]),b=(0,t.useCallback)(()=>{u(e.id,"up")},[e.id,u]),_=(0,t.useCallback)(()=>{u(e.id,"down")},[e.id,u]),S=(0,t.useCallback)(()=>{d(e.id)},[e.id,d]),k=e.slug||e.id,x=e.prpl_provider?.slug||"",C=["prpl-suggested-task",l?"prpl-suggested-task-celebrated":""].filter(Boolean).join(" ");return(0,n.jsxs)("li",{className:C,"data-task-id":k,"data-post-id":e.id,"data-task-action":"pending"===e.status||l?"celebrate":"","data-task-provider-id":x,"data-task-points":e.prpl_points||0,"data-task-order":e.menu_order||0,children:[(0,n.jsx)("div",{className:"prpl-suggested-task-checkbox-wrapper",children:i?(0,n.jsxs)("label",{children:[(0,n.jsx)("input",{type:"checkbox",className:"prpl-suggested-task-checkbox",onChange:y,style:{margin:0},checked:h,disabled:l}),(0,n.jsxs)("span",{className:"screen-reader-text",children:[e.title?.rendered||e.title,":"," ",(0,s.__)("Mark as complete","progress-planner")]})]}):(0,n.jsx)(r,{})}),(0,n.jsx)("div",{className:"prpl-suggested-task-title-wrapper",children:(0,n.jsx)("h3",{className:"prpl-task-title",children:i?(0,n.jsx)("span",{ref:m,contentEditable:"plaintext-only",role:"textbox",tabIndex:0,"aria-label":(0,s.__)("Edit task title","progress-planner"),"aria-multiline":"false",onKeyDown:f,onInput:v,suppressContentEditableWarning:!0,dangerouslySetInnerHTML:{__html:e.title?.rendered||e.title}}):(0,n.jsx)("span",{dangerouslySetInnerHTML:{__html:e.title?.rendered||e.title}})})}),(0,n.jsxs)("div",{className:"prpl-suggested-task-points-wrapper",children:[e.prpl_points>0&&(0,n.jsxs)("span",{className:"prpl-suggested-task-points",children:["+",e.prpl_points]}),i&&(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button trash","data-post-id":e.id,title:(0,s.__)("Delete","progress-planner"),onClick:S,children:[(0,n.jsx)(o,{}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Delete","progress-planner")})]})]}),i&&(0,n.jsx)("div",{className:"tooltip-actions prpl-move-buttons-wrapper",children:(0,n.jsxs)("span",{className:"prpl-move-buttons",children:[(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button move-up","data-task-id":k,"data-task-title":e.title?.rendered||e.title,"data-action":"move-up","data-target":"move-up",title:(0,s.__)("Move up","progress-planner"),onClick:b,children:[(0,n.jsx)("span",{className:"dashicons dashicons-arrow-up-alt2"}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Move up","progress-planner")})]}),(0,n.jsxs)("button",{type:"button",className:"prpl-suggested-task-button move-down","data-task-id":k,"data-task-title":e.title?.rendered||e.title,"data-action":"move-down","data-target":"move-down",title:(0,s.__)("Move down","progress-planner"),onClick:_,children:[(0,n.jsx)("span",{className:"dashicons dashicons-arrow-down-alt2"}),(0,n.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Move down","progress-planner")})]})]})}),(0,n.jsx)("div",{className:"prpl-suggested-task-actions-wrapper",children:(0,n.jsx)(a,{task:e,isUserTask:i,onComplete:c,onSnooze:p,onDelete:d})})]})}const l=window.wp.apiFetch;var c=e.n(l);function p(e){let t=e.querySelector('button[type="submit"]');if(t||(t=e.querySelector('button[data-action="completeTask"]')),t){t.disabled=!0;const e=document.createElement("span");e.classList.add("prpl-spinner"),e.innerHTML='',t.after(e)}}function d(e){let t=e.querySelector('button[type="submit"]');t||(t=e.querySelector('button[data-action="completeTask"]')),t&&(t.disabled=!1);const s=e.querySelector("span.prpl-spinner");s&&s.remove()}function u(e){const t=document.querySelector(`#${e} form`);if(t&&!t.parentNode.querySelector("p.prpl-interactive-task-error-message")){const e=document.createElement("p");e.classList.add("prpl-note","prpl-note-error","prpl-interactive-task-error-message"),e.textContent=(0,s.__)("Something went wrong. Please try again.","progress-planner"),t.insertAdjacentElement("afterend",e)}}function g(e){const t=document.querySelector(`#${e} form`);if(!t)return;const s=t.parentNode.querySelector("p.prpl-interactive-task-error-message");s&&s.remove()}async function m({setting:e,settingPath:t=!1,popoverId:s,action:n="prpl_interactive_task_submit",settingCallbackValue:a=e=>e}){const r=document.querySelector(`#${s} form`);if(!r)throw new Error("Form not found");p(r),g(s);try{const o=a(new FormData(r).get(e)),i=window.prplSuggestedTasksConfig?.ajaxUrl||window.progressPlanner?.ajaxUrl||"/wp-admin/admin-ajax.php",l=window.prplSuggestedTasksConfig?.nonce||window.progressPlanner?.nonce||"",c=new URLSearchParams({action:n,_ajax_nonce:l,setting:e,value:o});t&&c.append("setting_path",t);const p=await fetch(i,{method:"POST",body:c,credentials:"same-origin"}),g=await p.json();if(d(r),!0!==g.success)throw u(s),new Error("Settings update failed");return g}catch(e){throw d(r),u(s),e}}async function w(e,t="posts"){return c()({path:`/wp/v2/${t}/${e}?force=true`,method:"DELETE"})}const h={"core-blogdescription":{type:"siteSettings",settingAPIKey:"description",setting:"blogdescription"},"disable-comments":{type:"siteSettings",settingAPIKey:"default_comment_status",setting:"default_comment_status",settingCallbackValue:()=>"closed"},"disable-comment-pagination":{type:"siteSettings",settingAPIKey:"page_comments",setting:"page_comments",settingCallbackValue:()=>!1},"select-locale":{type:"siteSettings",settingAPIKey:"WPLANG",setting:"WPLANG"},"select-timezone":{type:"siteSettings",settingAPIKey:"timezone_string",setting:"timezone_string"},"search-engine-visibility":{type:"pluginSettings",setting:"blog_public",action:"prpl_interactive_task_submit",settingCallbackValue:()=>"1"},"set-date-format":{type:"siteSettings",settingAPIKey:"date_format",setting:"date_format"},"core-permalink-structure":{type:"siteSettings",settingAPIKey:"permalink_structure",setting:"permalink_structure"},"rename-uncategorized-category":{type:"customSubmit"},"hello-world":{type:"customSubmit"},"sample-page":{type:"customSubmit"},"yoast-author-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-author"]),settingCallbackValue:()=>!0},"yoast-date-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-date"]),settingCallbackValue:()=>!0},"yoast-format-archive":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-post_format"]),settingCallbackValue:()=>!0},"yoast-media-pages":{type:"pluginSettings",setting:"wpseo_titles",settingPath:JSON.stringify(["disable-attachment"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-emoji-scripts":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_emoji_scripts"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-feed-authors":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_feed_authors"]),settingCallbackValue:()=>!0},"yoast-crawl-settings-feed-global-comments":{type:"pluginSettings",setting:"wpseo",settingPath:JSON.stringify(["remove_feed_global_comments"]),settingCallbackValue:()=>!0},"yoast-organization-logo":{type:"customSubmit"},"aioseo-author-archive":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["archives","author","show"]),settingCallbackValue:()=>!1},"aioseo-date-archive":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["archives","date","show"]),settingCallbackValue:()=>!1},"aioseo-media-pages":{type:"pluginSettings",setting:"aioseo_options_search_appearance",settingPath:JSON.stringify(["postTypes","attachment","show"]),settingCallbackValue:()=>!1},"aioseo-crawl-settings-feed-authors":{type:"pluginSettings",setting:"aioseo_options_rss_content",settingPath:JSON.stringify(["authorFeed"]),settingCallbackValue:()=>!1},"aioseo-crawl-settings-feed-comments":{type:"pluginSettings",setting:"aioseo_options_rss_content",settingPath:JSON.stringify(["commentFeed"]),settingCallbackValue:()=>!1},"core-siteicon":{type:"customSubmit"},"update-term-description":{type:"customSubmit"},"remove-terms-without-posts":{type:"customSubmit"}};function y({tasks:e,onComplete:s}){const n=(0,t.useCallback)(async(e,t)=>{switch(e){case"hello-world":{const e=window.helloWorldData?.postId;return e&&await w(e,"posts"),{success:!0}}case"sample-page":{const e=window.samplePageData?.postId;return e&&await w(e,"pages"),{success:!0}}case"rename-uncategorized-category":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("category_name"),s=window.renameUncategorizedCategoryData?.termId;s&&t&&await c()({path:`/wp/v2/categories/${s}`,method:"POST",data:{name:t}})}return{success:!0}}case"core-siteicon":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("site_icon");t&&await c()({path:"/wp/v2/settings",method:"POST",data:{site_icon:parseInt(t)}})}return{success:!0}}case"yoast-organization-logo":{const e=document.querySelector(`#${t} form`);if(e){const s=new FormData(e).get("company_logo_id");s&&await m({setting:"wpseo",settingPath:JSON.stringify(["company_logo_id"]),popoverId:t,settingCallbackValue:()=>parseInt(s)})}return{success:!0}}case"update-term-description":{const e=document.querySelector(`#${t} form`);if(e){const t=new FormData(e).get("description"),s=window.updateTermDescriptionData?.termId,n=window.updateTermDescriptionData?.taxonomy||"category";if(s&&t){const e="category"===n?"categories":n;await c()({path:`/wp/v2/${e}/${s}`,method:"POST",data:{description:t}})}}return{success:!0}}case"remove-terms-without-posts":if(document.querySelector(`#${t} form`)){const e=window.removeTermsWithoutPostsData?.termIds||[],t=window.removeTermsWithoutPostsData?.taxonomy||"category",s="category"===t?"categories":t;await Promise.all(e.map(e=>c()({path:`/wp/v2/${s}/${e}?force=true`,method:"DELETE"})))}return{success:!0};default:return{success:!0}}},[]),a=(0,t.useCallback)(async(t,a,r)=>{try{const o=e.find(e=>e.slug===t||e.prpl_provider?.slug===t||`${e.id}`===t);if(!o)return;let i;switch(r.type){case"siteSettings":i=async function({settingAPIKey:e,setting:t,popoverId:s,settingCallbackValue:n=e=>e}){const a=document.querySelector(`#${s} form`);if(!a)throw new Error("Form not found");p(a),g(s);try{const s=n(new FormData(a).get(t)),r=await c()({path:"/wp/v2/settings",method:"POST",data:{[e]:s}});return d(a),r}catch(e){throw d(a),u(s),e}}({settingAPIKey:r.settingAPIKey,setting:r.setting,popoverId:a,settingCallbackValue:r.settingCallbackValue});break;case"pluginSettings":i=m({setting:r.setting,settingPath:r.settingPath,popoverId:a,action:r.action||"prpl_interactive_task_submit",settingCallbackValue:r.settingCallbackValue});break;case"customSubmit":i=n(t,a);break;default:return}await i,await s(o.id,o),function(e){const t=document.getElementById(e);t&&"function"==typeof t.hidePopover&&t.hidePopover()}(a)}catch(e){console.error("Popover form submission error:",e)}},[e,s,n]);return(0,t.useEffect)(()=>{const e=new Map;Object.entries(h).forEach(([t,s])=>{const n=`prpl-popover-${t}`,r=document.querySelector(`#${n} form`);if(!r)return;const o=e=>{e.preventDefault(),a(t,n,s)};r.addEventListener("submit",o),e.set(n,{formElement:r,handler:o})});const t=document.querySelector("input#blogdescription");if(t){const e=document.querySelector('#prpl-popover-core-blogdescription button[type="submit"]');if(e){const s=t=>{e.disabled=0===t.target.value.length};t.addEventListener("input",s)}}return function(){const e=document.querySelectorAll('#prpl-popover-set-date-format input[name="date_format"]'),t=document.querySelector('#prpl-popover-set-date-format input[name="date_format_custom"]');if(!e.length||!t)return;let s;e.forEach(e=>{e.addEventListener("change",()=>{"custom"===e.value?(t.disabled=!1,t.focus()):t.disabled=!0})}),t.addEventListener("input",()=>{clearTimeout(s),s=setTimeout(async()=>{const e=t.value;if(e)try{const s=window.prplSuggestedTasksConfig?.ajaxUrl||window.progressPlanner?.ajaxUrl||"/wp-admin/admin-ajax.php",n=window.prplSuggestedTasksConfig?.nonce||window.progressPlanner?.nonce||"",a=await fetch(`${s}?action=prpl_date_format_preview&format=${encodeURIComponent(e)}&_ajax_nonce=${n}`,{credentials:"same-origin"}),r=await a.json();if(r.success&&r.data){const e=t.closest(".prpl-radio-wrapper")?.querySelector(".date-time-text");e&&(e.textContent=r.data)}}catch{}},300)})}(),function(){const e=document.querySelectorAll('#prpl-popover-core-permalink-structure input[name="permalink_structure"]');if(!e.length)return;const t=document.querySelector('#prpl-popover-core-permalink-structure input[name="permalink_custom"]');e.forEach(e=>{e.addEventListener("change",()=>{"custom"===e.value&&t?(t.disabled=!1,t.focus()):t&&(t.disabled=!0)})})}(),function(){const e=document.querySelector("#prpl-popover-core-siteicon .prpl-upload-site-icon");if(e&&window.wp?.media){let t;e.addEventListener("click",s=>{s.preventDefault(),t||(t=window.wp.media({title:e.dataset.title||"Select Site Icon",button:{text:e.dataset.button||"Use as site icon"},multiple:!1,library:{type:"image"}}),t.on("select",()=>{const e=t.state().get("selection").first().toJSON(),s=document.querySelector('#prpl-popover-core-siteicon input[name="site_icon"]');s&&(s.value=e.id);const n=document.querySelector("#prpl-popover-core-siteicon .prpl-site-icon-preview img");n&&(n.src=e.url);const a=document.querySelector('#prpl-popover-core-siteicon button[type="submit"]');a&&(a.disabled=!1)})),t.open()})}const t=document.querySelector("#prpl-popover-yoast-organization-logo .prpl-upload-logo");if(t&&window.wp?.media){let e;t.addEventListener("click",s=>{s.preventDefault(),e||(e=window.wp.media({title:t.dataset.title||"Select Logo",button:{text:t.dataset.button||"Use as logo"},multiple:!1,library:{type:"image"}}),e.on("select",()=>{const t=e.state().get("selection").first().toJSON(),s=document.querySelector('#prpl-popover-yoast-organization-logo input[name="company_logo_id"]');s&&(s.value=t.id);const n=document.querySelector("#prpl-popover-yoast-organization-logo .prpl-logo-preview img");n&&(n.src=t.url);const a=document.querySelector('#prpl-popover-yoast-organization-logo button[type="submit"]');a&&(a.disabled=!1)})),e.open()})}}(),()=>{e.forEach(({formElement:e,handler:t})=>{e.removeEventListener("submit",t)})}},[e,a]),null}const f={"1-week":7,"2-weeks":14,"1-month":30,"3-months":90,"6-months":180,"1-year":365,forever:3650};async function v({status:e="publish",perPage:t=100,excludeProvider:s,provider:n,excludeIds:a=[]}={}){const r={status:e,per_page:t,_embed:!0,"filter[orderby]":"menu_order","filter[order]":"ASC"};s&&(r.exclude_provider=s),n&&(r.provider=n),a.length>0&&(r.exclude=a.join(","));const o=function(e){const t=new URLSearchParams;return Object.entries(e).forEach(([e,s])=>{Array.isArray(s)?s.forEach(s=>t.append(e,s)):null!=s&&""!==s&&t.append(e,s)}),t.toString()}(r);try{return await c()({path:`/wp/v2/prpl_recommendations?${o}`})||[]}catch(e){return console.error("Error fetching tasks:",e),[]}}async function b(e){return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:{status:"trash"}})}async function _(e,t){return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:t})}async function S(e,t){const s=window.prplSuggestedTasksConfig?.nonce||"",n=window.prplSuggestedTasksConfig?.ajaxUrl||"/wp-admin/admin-ajax.php",a=new FormData;a.append("action","progress_planner_suggested_task_action"),a.append("post_id",e),a.append("action_type",t),a.append("nonce",s);try{return(await fetch(n,{method:"POST",body:a,credentials:"same-origin"})).json()}catch(e){return console.error("Error sending task action:",e),null}}function k(){const[e,a]=(0,t.useState)([]),[r,o]=(0,t.useState)(!0),[l,p]=(0,t.useState)(window.prplSuggestedTasksConfig?.showAll||!1),[d,u]=(0,t.useState)(new Set),g=(0,t.useRef)(null),m=(0,t.useRef)(new Set);(0,t.useEffect)(()=>{const e=()=>{document.querySelectorAll(".prpl-widget-wrapper").forEach(e=>{if(!e||e.classList.contains("in-popover"))return;const t=e.querySelector(".widget-inner-container");if(!t)return;const s=document.querySelector(".prpl-widgets-container");if(!s)return;const n=parseInt(window.getComputedStyle(s).getPropertyValue("grid-auto-rows")),a=parseInt(window.getComputedStyle(e).getPropertyValue("padding-top")),r=parseInt(window.getComputedStyle(e).getPropertyValue("padding-bottom")),o=Math.ceil((t.getBoundingClientRect().height+a+r)/n);e.style.gridRowEnd="span "+(o+1)})},t=()=>{setTimeout(e,0)};return window.addEventListener("prpl/grid/resize",e),window.addEventListener("resize",t),window.addEventListener("load",t),t(),setTimeout(t,1e3),()=>{window.removeEventListener("prpl/grid/resize",e),window.removeEventListener("resize",t),window.removeEventListener("load",t)}},[]),(0,t.useEffect)(()=>{(async()=>{try{const e=l?100:window.prplSuggestedTasksConfig?.perPage||5,t=await v({status:"publish",perPage:e,excludeProvider:"user"});if(t.forEach(e=>{m.current.add(e.id)}),a(t),o(!1),!window.prplSuggestedTasksConfig?.delayCelebration){const t=await v({status:"pending",perPage:e,excludeProvider:"user"});t.length>0&&(a(e=>[...e,...t]),t.forEach(e=>{m.current.add(e.id)}),t.forEach(e=>{b(e.id).catch(()=>{})}),setTimeout(()=>{const e=new Set(t.map(e=>e.id));u(e),document.dispatchEvent(new CustomEvent("prpl/celebrateTasks")),setTimeout(()=>{a(t=>t.filter(t=>!e.has(t.id))),u(new Set),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},2e3)},3e3))}setTimeout(()=>{window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},100)}catch{o(!1)}})()},[l]);const w=(0,t.useCallback)(async(e,t)=>{try{u(t=>new Set([...t,e])),await b(e),S(e,"complete");const s=parseInt(t.prpl_points)||0;if(s>0&&"function"==typeof window.prplUpdateRaviGauge&&window.prplUpdateRaviGauge(s),s>0&&g.current){const t=g.current.querySelector(`[data-post-id="${e}"]`);document.dispatchEvent(new CustomEvent("prpl/celebrateTasks",{detail:{element:t}}))}setTimeout(async()=>{a(t=>t.filter(t=>t.id!==e)),u(t=>{const s=new Set(t);return s.delete(e),s});const t=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});t.length>0&&(a(e=>[...e,t[0]]),m.current.add(t[0].id)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},2e3)}catch{u(t=>{const s=new Set(t);return s.delete(e),s})}},[]),h=(0,t.useCallback)(async(e,t)=>{try{await async function(e,t){const s=f[t]||7,n=new Date(Date.now()+24*s*60*60*1e3).toISOString().split(".")[0];return c()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:{status:"future",date:n,date_gmt:n}})}(e,t),a(t=>t.filter(t=>t.id!==e));const s=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});s.length>0&&(a(e=>[...e,s[0]]),m.current.add(s[0].id)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch{}},[]),k=(0,t.useCallback)(async e=>{try{await async function(e){return c()({path:`/wp/v2/prpl_recommendations/${e}?force=true`,method:"DELETE"})}(e),S(e,"delete"),a(t=>t.filter(t=>t.id!==e));const t=await v({status:"publish",perPage:1,excludeProvider:"user",excludeIds:Array.from(m.current)});t.length>0&&(a(e=>[...e,t[0]]),m.current.add(t[0].id)),setTimeout(()=>{window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},500)}catch{}},[]),x=(0,t.useCallback)(async(e,t)=>{try{await _(e,{title:t})}catch{}},[]),C=(0,t.useCallback)(async(t,s)=>{const n=e.findIndex(e=>e.id===t);if(-1===n)return;const r="up"===s?n-1:n+1;if(r<0||r>=e.length)return;const o=[...e],[i]=o.splice(n,1);o.splice(r,0,i),a(o),o.forEach((e,t)=>{_(e.id,{menu_order:t}).catch(()=>{})})},[e]),E=(0,t.useCallback)(async()=>{const e=!l;p(e),o(!0),m.current.clear();const t=new URL(window.location);e?t.searchParams.set("prpl_show_all_recommendations",""):t.searchParams.delete("prpl_show_all_recommendations"),window.history.pushState({},"",t)},[l]);return r?(0,n.jsx)("p",{className:"prpl-suggested-tasks-loading",children:(0,s.__)("Loading tasks…","progress-planner")}):0===e.length?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("ul",{id:"prpl-suggested-tasks-list",className:"prpl-suggested-tasks-list",ref:g}),(0,n.jsxs)("p",{className:"prpl-no-suggested-tasks",children:[(0,s.__)("You have completed all recommended tasks.","progress-planner"),(0,n.jsx)("br",{}),(0,s.__)("Check back later for new tasks!","progress-planner")]})]}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(y,{tasks:e,onComplete:w}),(0,n.jsx)("ul",{style:{display:"none"}}),(0,n.jsx)("ul",{id:"prpl-suggested-tasks-list",className:"prpl-suggested-tasks-list",ref:g,children:e.map(e=>(0,n.jsx)(i,{task:e,isUserTask:"user"===e.prpl_provider?.slug,isCelebrating:d.has(e.id),onComplete:w,onSnooze:h,onDelete:k,onMove:C,onTitleChange:x},e.id))}),(0,n.jsx)("p",{className:"prpl-show-all-tasks",children:(0,n.jsx)("button",{type:"button",id:"prpl-toggle-all-recommendations",className:"prpl-toggle-all-recommendations-button",onClick:E,children:l?(0,s.__)("Show fewer recommendations","progress-planner"):(0,s.__)("Show all recommendations","progress-planner")})})]})}function x(){const e=document.getElementById("prpl-suggested-tasks-root");e&&(0,t.createRoot)(e).render((0,n.jsx)(k,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",x):x()})(); \ No newline at end of file diff --git a/build/todo.asset.php b/build/todo.asset.php index 4155a15e35..23ec0774d1 100644 --- a/build/todo.asset.php +++ b/build/todo.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '57c33252d20fa7a21373'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '947eebde62f548265720'); diff --git a/build/todo.js b/build/todo.js index aefd781e2d..a19337dd45 100644 --- a/build/todo.js +++ b/build/todo.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,s=window.wp.i18n,r=window.wp.apiFetch;var n=e.n(r);const a=window.ReactJSXRuntime;async function l(e,t){return n()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:t})}async function o(e){return n()({path:`/wp/v2/prpl_recommendations/${e}?force=true`,method:"DELETE"})}function i({task:e,isGolden:r,isCompleted:n,onToggle:l,onDelete:o,onMove:i,onTitleChange:d}){const p=(0,t.useRef)(null),c=(0,t.useRef)(null),u=(0,t.useCallback)(()=>{c.current&&clearTimeout(c.current),c.current=setTimeout(()=>{if(p.current){const t=p.current.textContent.replace(/\n/g,"");d(e.id,t)}},300)},[e.id,d]),h=(0,t.useCallback)(e=>{"Enter"===e.key&&(e.preventDefault(),e.target.blur())},[]);return(0,a.jsxs)("li",{className:"prpl-suggested-task"+(r?" prpl-golden-task":""),"data-task-id":e.slug||e.id,"data-post-id":e.id,"data-task-action":n?"completed":"publish","data-task-provider-id":"user","data-task-points":e.prpl_points||0,"data-task-order":e.menu_order||0,children:[(0,a.jsx)("div",{className:"prpl-suggested-task-checkbox-wrapper",children:(0,a.jsxs)("label",{children:[(0,a.jsx)("input",{type:"checkbox",className:"prpl-suggested-task-checkbox",checked:n,onChange:()=>l(e.id)}),(0,a.jsxs)("span",{className:"screen-reader-text",children:[e.title?.rendered||e.title,":"," ",(0,s.__)("Mark as completed","progress-planner")]})]})}),(0,a.jsx)("div",{className:"prpl-suggested-task-title-wrapper",children:(0,a.jsx)("h3",{className:"prpl-task-title",children:(0,a.jsx)("span",{ref:p,contentEditable:!0,suppressContentEditableWarning:!0,onInput:u,onKeyDown:h,"data-post-id":e.id,tabIndex:0,role:"textbox","aria-label":(0,s.__)("Edit task title","progress-planner"),children:e.title?.rendered||e.title})})}),(0,a.jsxs)("div",{className:"prpl-suggested-task-points-wrapper",children:[(e.prpl_points||0)>0&&(0,a.jsxs)("span",{className:"prpl-suggested-task-points",children:["+",e.prpl_points]}),(0,a.jsx)("button",{type:"button",className:"prpl-suggested-task-delete",onClick:()=>o(e.id),"aria-label":(0,s.__)("Delete task","progress-planner"),children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",width:"16",height:"16","aria-hidden":"true",children:(0,a.jsx)("path",{fill:"currentColor",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Z"})})})]}),!n&&(0,a.jsxs)("div",{className:"tooltip-actions prpl-move-buttons-wrapper",children:[(0,a.jsx)("button",{type:"button",className:"prpl-move-up",onClick:()=>i(e.id,"up"),"aria-label":(0,s.__)("Move up","progress-planner"),children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",children:(0,a.jsx)("path",{fill:"currentColor",d:"M12 4l-8 8h6v8h4v-8h6z"})})}),(0,a.jsx)("button",{type:"button",className:"prpl-move-down",onClick:()=>i(e.id,"down"),"aria-label":(0,s.__)("Move down","progress-planner"),children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",children:(0,a.jsx)("path",{fill:"currentColor",d:"M12 20l8-8h-6v-8h-4v8h-6z"})})})]})]})}function d(){const[e,r]=(0,t.useState)([]),[d,p]=(0,t.useState)([]),[c,u]=(0,t.useState)(!0),[h,m]=(0,t.useState)(""),[g,w]=(0,t.useState)(!1),x=(0,t.useRef)(null);(0,t.useEffect)(()=>{(async()=>{try{const e=await async function({status:e=["publish","trash"]}={}){const t=Array.isArray(e)?e.map(e=>`status[]=${e}`).join("&"):`status=${e}`;return n()({path:`/wp/v2/prpl_recommendations?${t}&provider=user&per_page=100&_embed=true&filter[orderby]=menu_order&filter[order]=ASC`})}(),t=e.filter(e=>"publish"===e.status),s=e.filter(e=>"trash"===e.status);t.sort((e,t)=>(e.menu_order||0)-(t.menu_order||0)),s.sort((e,t)=>(e.menu_order||0)-(t.menu_order||0)),r(t),p(s)}catch(e){console.error("Error loading tasks:",e)}finally{u(!1),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}})()},[]);const v=(0,t.useCallback)(async t=>{if(t.preventDefault(),h.trim())try{const t=e.reduce((e,t)=>Math.max(e,t.menu_order||0),0),a=await async function({title:e,order:t}){return n()({path:"/wp/v2/prpl_recommendations",method:"POST",data:{title:e,status:"publish",menu_order:t,prpl_recommendations_provider:window.prplTodoConfig?.userProviderId}})}({title:h,order:t+1});r(e=>[...e,a]),m(""),window.wp?.a11y?.speak&&window.wp.a11y.speak((0,s.__)("Task added successfully","progress-planner"),"polite"),x.current?.focus(),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error creating task:",e)}},[h,e]),k=(0,t.useCallback)(async t=>{const s=e.find(e=>e.id===t)||d.find(e=>e.id===t);if(!s)return;const n="trash"===s.status,a=n?"publish":"trash";try{await l(t,{status:a}),n?(p(e=>e.filter(e=>e.id!==t)),r(e=>[...e,{...s,status:"publish"}])):(r(e=>e.filter(e=>e.id!==t)),p(e=>[...e,{...s,status:"trash"}]),s.prpl_points>0&&("function"==typeof window.prplUpdateRaviGauge&&window.prplUpdateRaviGauge(s.prpl_points),document.dispatchEvent(new CustomEvent("prpl/celebrateTasks",{detail:{}})))),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error toggling task:",e)}},[e,d]),_=(0,t.useCallback)(async e=>{try{await o(e),r(t=>t.filter(t=>t.id!==e)),p(t=>t.filter(t=>t.id!==e)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error deleting task:",e)}},[]),b=(0,t.useCallback)(async(t,s)=>{const n=e.findIndex(e=>e.id===t);if(-1===n)return;const a="up"===s?n-1:n+1;if(a<0||a>=e.length)return;const o=[...e];[o[n],o[a]]=[o[a],o[n]];const i=o.map((e,t)=>({...e,menu_order:t}));r(i);try{await Promise.all(i.map(e=>l(e.id,{menu_order:e.menu_order})))}catch(e){console.error("Error saving task order:",e)}window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},[e]),f=(0,t.useCallback)(async(e,t)=>{try{await l(e,{title:t}),r(s=>s.map(s=>s.id===e?{...s,title:{rendered:t}}:s)),p(s=>s.map(s=>s.id===e?{...s,title:{rendered:t}}:s))}catch(e){console.error("Error updating task title:",e)}},[]),j=(0,t.useCallback)(async()=>{try{await Promise.all(d.map(e=>o(e.id))),p([]),w(!1),window.wp?.a11y?.speak&&window.wp.a11y.speak((0,s.__)("All completed tasks deleted","progress-planner"),"assertive"),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error deleting completed tasks:",e)}},[d]);return c?(0,a.jsx)("p",{id:"prpl-todo-list-loading",children:(0,s.__)("Loading items…","progress-planner")}):(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("div",{id:"todo-aria-live-region","aria-live":"polite",style:{position:"absolute",left:"-9999px"}}),(0,a.jsx)("ul",{id:"todo-list",className:"prpl-todo-list prpl-suggested-tasks-list",children:e.map((e,t)=>(0,a.jsx)(i,{task:e,isGolden:0===t&&e.prpl_points>0,isCompleted:!1,onToggle:k,onDelete:_,onMove:b,onTitleChange:f},e.id))}),(0,a.jsxs)("form",{id:"create-todo-item",onSubmit:v,children:[(0,a.jsx)("input",{ref:x,type:"text",id:"new-todo-content",placeholder:(0,s.__)("Add a new task","progress-planner"),"aria-label":(0,s.__)("Add a new task","progress-planner"),required:!0,value:h,onChange:e=>m(e.target.value)}),(0,a.jsxs)("button",{type:"submit","aria-label":(0,s.__)("Add task","progress-planner"),children:[(0,a.jsx)("span",{className:"dashicons dashicons-plus-alt2","aria-hidden":"true"}),(0,a.jsx)("span",{className:"screen-reader-text",children:(0,s.__)("Add task","progress-planner")})]})]}),d.length>0&&(0,a.jsxs)("details",{id:"todo-list-completed-details",children:[(0,a.jsxs)("summary",{children:[(0,s.__)("Completed tasks","progress-planner"),(0,a.jsx)("span",{className:"prpl-todo-list-completed-summary-icon",children:(0,a.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:"1.5",stroke:"currentColor",children:(0,a.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m19.5 8.25-7.5 7.5-7.5-7.5"})})})]}),(0,a.jsx)("div",{id:"todo-list-completed-delete-all-wrapper",children:(0,a.jsxs)("button",{id:"todo-list-completed-delete-all",onClick:()=>w(!0),children:[(0,a.jsx)("span",{style:{display:"inline-block",width:"18px",height:"18px"},children:(0,a.jsx)("svg",{role:"img","aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",children:(0,a.jsx)("path",{fill:"#9ca3af",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Z"})})}),(0,s.__)("Delete all completed tasks","progress-planner")]})}),(0,a.jsx)("ul",{id:"todo-list-completed",className:"prpl-todo-list prpl-suggested-tasks-list",children:d.map(e=>(0,a.jsx)(i,{task:e,isGolden:!1,isCompleted:!0,onToggle:k,onDelete:_,onMove:b,onTitleChange:f},e.id))})]}),g&&(0,a.jsxs)("div",{id:"todo-list-completed-delete-all-popover",className:"prpl-popover",style:{position:"fixed",top:"50%",left:"50%",transform:"translate(-50%, -50%)",zIndex:1e4,background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 4px 20px rgba(0,0,0,0.15)"},children:[(0,a.jsx)("div",{className:"prpl-note",children:(0,a.jsx)("span",{className:"prpl-note-text",children:(0,s.__)("Are you sure you want to delete all completed tasks? This action cannot be undone.","progress-planner")})}),(0,a.jsxs)("div",{className:"prpl-buttons-wrapper",style:{display:"flex",gap:"10px",marginTop:"15px"},children:[(0,a.jsxs)("button",{id:"todo-list-completed-delete-all-cancel",onClick:()=>w(!1),children:[(0,a.jsx)("strong",{children:(0,s.__)("No","progress-planner")}),", ",(0,s.__)("keep this list","progress-planner")]}),(0,a.jsxs)("button",{id:"todo-list-completed-delete-all-confirm",onClick:j,children:[(0,a.jsx)("strong",{children:(0,s.__)("Yes","progress-planner")}),", ",(0,s.__)("delete all completed tasks","progress-planner")]})]})]}),g&&(0,a.jsx)("div",{role:"button",tabIndex:0,"aria-label":(0,s.__)("Close dialog","progress-planner"),style:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.3)",zIndex:9999},onClick:()=>w(!1),onKeyDown:e=>{"Enter"!==e.key&&" "!==e.key||w(!1)}})]})}function p(){const e=document.getElementById("prpl-todo-root");e&&(0,t.createRoot)(e).render((0,a.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file +(()=>{"use strict";var e={n:t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return e.d(r,{a:r}),r},d:(t,r)=>{for(var s in r)e.o(r,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:r[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,r=window.wp.i18n,s=window.wp.apiFetch;var n=e.n(s);const o=window.ReactJSXRuntime;async function a(e,t){return n()({path:`/wp/v2/prpl_recommendations/${e}`,method:"POST",data:t})}async function l(e){return n()({path:`/wp/v2/prpl_recommendations/${e}?force=true`,method:"DELETE"})}function i({task:e,isGolden:s,isCompleted:n,onToggle:a,onDelete:l,onMove:i,onTitleChange:d}){const p=(0,t.useRef)(null),c=(0,t.useRef)(null),u=(0,t.useCallback)(()=>{c.current&&clearTimeout(c.current),c.current=setTimeout(()=>{if(p.current){const t=p.current.textContent.replace(/\n/g,"");d(e.id,t)}},300)},[e.id,d]),h=(0,t.useCallback)(e=>{"Enter"===e.key&&(e.preventDefault(),e.target.blur())},[]);return(0,o.jsxs)("li",{className:"prpl-suggested-task"+(s?" prpl-golden-task":""),"data-task-id":e.slug||e.id,"data-post-id":e.id,"data-task-action":n?"completed":"publish","data-task-provider-id":"user","data-task-points":e.prpl_points||0,"data-task-order":e.menu_order||0,children:[(0,o.jsx)("div",{className:"prpl-suggested-task-checkbox-wrapper",children:(0,o.jsxs)("label",{children:[(0,o.jsx)("input",{type:"checkbox",className:"prpl-suggested-task-checkbox",checked:n,onChange:()=>a(e.id)}),(0,o.jsxs)("span",{className:"screen-reader-text",children:[e.title?.rendered||e.title,":"," ",(0,r.__)("Mark as completed","progress-planner")]})]})}),(0,o.jsx)("div",{className:"prpl-suggested-task-title-wrapper",children:(0,o.jsx)("h3",{className:"prpl-task-title",children:(0,o.jsx)("span",{ref:p,contentEditable:!0,suppressContentEditableWarning:!0,onInput:u,onKeyDown:h,"data-post-id":e.id,tabIndex:0,role:"textbox","aria-label":(0,r.__)("Edit task title","progress-planner"),children:e.title?.rendered||e.title})})}),(0,o.jsxs)("div",{className:"prpl-suggested-task-points-wrapper",children:[(e.prpl_points||0)>0&&(0,o.jsxs)("span",{className:"prpl-suggested-task-points",children:["+",e.prpl_points]}),(0,o.jsx)("button",{type:"button",className:"prpl-suggested-task-delete",onClick:()=>l(e.id),"aria-label":(0,r.__)("Delete task","progress-planner"),children:(0,o.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",width:"16",height:"16","aria-hidden":"true",children:(0,o.jsx)("path",{fill:"currentColor",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Z"})})})]}),!n&&(0,o.jsxs)("div",{className:"tooltip-actions prpl-move-buttons-wrapper",children:[(0,o.jsx)("button",{type:"button",className:"prpl-move-up",onClick:()=>i(e.id,"up"),"aria-label":(0,r.__)("Move up","progress-planner"),children:(0,o.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",children:(0,o.jsx)("path",{fill:"currentColor",d:"M12 4l-8 8h6v8h4v-8h6z"})})}),(0,o.jsx)("button",{type:"button",className:"prpl-move-down",onClick:()=>i(e.id,"down"),"aria-label":(0,r.__)("Move down","progress-planner"),children:(0,o.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",children:(0,o.jsx)("path",{fill:"currentColor",d:"M12 20l8-8h-6v-8h-4v8h-6z"})})})]})]})}function d(){const[e,s]=(0,t.useState)([]),[d,p]=(0,t.useState)([]),[c,u]=(0,t.useState)(!0),[h,g]=(0,t.useState)(""),[w,m]=(0,t.useState)(!1),x=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=()=>{document.querySelectorAll(".prpl-widget-wrapper").forEach(e=>{if(!e||e.classList.contains("in-popover"))return;const t=e.querySelector(".widget-inner-container");if(!t)return;const r=document.querySelector(".prpl-widgets-container");if(!r)return;const s=parseInt(window.getComputedStyle(r).getPropertyValue("grid-auto-rows")),n=parseInt(window.getComputedStyle(e).getPropertyValue("padding-top")),o=parseInt(window.getComputedStyle(e).getPropertyValue("padding-bottom")),a=Math.ceil((t.getBoundingClientRect().height+n+o)/s);e.style.gridRowEnd="span "+(a+1)})},t=()=>{setTimeout(e,0)};return window.addEventListener("prpl/grid/resize",e),window.addEventListener("resize",t),window.addEventListener("load",t),t(),setTimeout(t,1e3),()=>{window.removeEventListener("prpl/grid/resize",e),window.removeEventListener("resize",t),window.removeEventListener("load",t)}},[]),(0,t.useEffect)(()=>{(async()=>{try{const e=await async function({status:e=["publish","trash"]}={}){const t=Array.isArray(e)?e.map(e=>`status[]=${e}`).join("&"):`status=${e}`;return n()({path:`/wp/v2/prpl_recommendations?${t}&provider=user&per_page=100&_embed=true&filter[orderby]=menu_order&filter[order]=ASC`})}(),t=e.filter(e=>"publish"===e.status),r=e.filter(e=>"trash"===e.status);t.sort((e,t)=>(e.menu_order||0)-(t.menu_order||0)),r.sort((e,t)=>(e.menu_order||0)-(t.menu_order||0)),s(t),p(r)}catch(e){console.error("Error loading tasks:",e)}finally{u(!1),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}})()},[]);const v=(0,t.useCallback)(async t=>{if(t.preventDefault(),h.trim())try{const t=e.reduce((e,t)=>Math.max(e,t.menu_order||0),0),o=await async function({title:e,order:t}){return n()({path:"/wp/v2/prpl_recommendations",method:"POST",data:{title:e,status:"publish",menu_order:t,prpl_recommendations_provider:window.prplTodoConfig?.userProviderId}})}({title:h,order:t+1});s(e=>[...e,o]),g(""),window.wp?.a11y?.speak&&window.wp.a11y.speak((0,r.__)("Task added successfully","progress-planner"),"polite"),x.current?.focus(),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error creating task:",e)}},[h,e]),k=(0,t.useCallback)(async t=>{const r=e.find(e=>e.id===t)||d.find(e=>e.id===t);if(!r)return;const n="trash"===r.status,o=n?"publish":"trash";try{await a(t,{status:o}),n?(p(e=>e.filter(e=>e.id!==t)),s(e=>[...e,{...r,status:"publish"}])):(s(e=>e.filter(e=>e.id!==t)),p(e=>[...e,{...r,status:"trash"}]),r.prpl_points>0&&("function"==typeof window.prplUpdateRaviGauge&&window.prplUpdateRaviGauge(r.prpl_points),document.dispatchEvent(new CustomEvent("prpl/celebrateTasks",{detail:{}})))),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error toggling task:",e)}},[e,d]),_=(0,t.useCallback)(async e=>{try{await l(e),s(t=>t.filter(t=>t.id!==e)),p(t=>t.filter(t=>t.id!==e)),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error deleting task:",e)}},[]),y=(0,t.useCallback)(async(t,r)=>{const n=e.findIndex(e=>e.id===t);if(-1===n)return;const o="up"===r?n-1:n+1;if(o<0||o>=e.length)return;const l=[...e];[l[n],l[o]]=[l[o],l[n]];const i=l.map((e,t)=>({...e,menu_order:t}));s(i);try{await Promise.all(i.map(e=>a(e.id,{menu_order:e.menu_order})))}catch(e){console.error("Error saving task order:",e)}window.dispatchEvent(new CustomEvent("prpl/grid/resize"))},[e]),b=(0,t.useCallback)(async(e,t)=>{try{await a(e,{title:t}),s(r=>r.map(r=>r.id===e?{...r,title:{rendered:t}}:r)),p(r=>r.map(r=>r.id===e?{...r,title:{rendered:t}}:r))}catch(e){console.error("Error updating task title:",e)}},[]),f=(0,t.useCallback)(async()=>{try{await Promise.all(d.map(e=>l(e.id))),p([]),m(!1),window.wp?.a11y?.speak&&window.wp.a11y.speak((0,r.__)("All completed tasks deleted","progress-planner"),"assertive"),window.dispatchEvent(new CustomEvent("prpl/grid/resize"))}catch(e){console.error("Error deleting completed tasks:",e)}},[d]);return c?(0,o.jsx)("p",{id:"prpl-todo-list-loading",children:(0,r.__)("Loading items…","progress-planner")}):(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("div",{id:"todo-aria-live-region","aria-live":"polite",style:{position:"absolute",left:"-9999px"}}),(0,o.jsx)("ul",{id:"todo-list",className:"prpl-todo-list prpl-suggested-tasks-list",children:e.map((e,t)=>(0,o.jsx)(i,{task:e,isGolden:0===t&&e.prpl_points>0,isCompleted:!1,onToggle:k,onDelete:_,onMove:y,onTitleChange:b},e.id))}),(0,o.jsxs)("form",{id:"create-todo-item",onSubmit:v,children:[(0,o.jsx)("input",{ref:x,type:"text",id:"new-todo-content",placeholder:(0,r.__)("Add a new task","progress-planner"),"aria-label":(0,r.__)("Add a new task","progress-planner"),required:!0,value:h,onChange:e=>g(e.target.value)}),(0,o.jsxs)("button",{type:"submit","aria-label":(0,r.__)("Add task","progress-planner"),children:[(0,o.jsx)("span",{className:"dashicons dashicons-plus-alt2","aria-hidden":"true"}),(0,o.jsx)("span",{className:"screen-reader-text",children:(0,r.__)("Add task","progress-planner")})]})]}),d.length>0&&(0,o.jsxs)("details",{id:"todo-list-completed-details",children:[(0,o.jsxs)("summary",{children:[(0,r.__)("Completed tasks","progress-planner"),(0,o.jsx)("span",{className:"prpl-todo-list-completed-summary-icon",children:(0,o.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:"1.5",stroke:"currentColor",children:(0,o.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m19.5 8.25-7.5 7.5-7.5-7.5"})})})]}),(0,o.jsx)("div",{id:"todo-list-completed-delete-all-wrapper",children:(0,o.jsxs)("button",{id:"todo-list-completed-delete-all",onClick:()=>m(!0),children:[(0,o.jsx)("span",{style:{display:"inline-block",width:"18px",height:"18px"},children:(0,o.jsx)("svg",{role:"img","aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 48 48",children:(0,o.jsx)("path",{fill:"#9ca3af",d:"M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Z"})})}),(0,r.__)("Delete all completed tasks","progress-planner")]})}),(0,o.jsx)("ul",{id:"todo-list-completed",className:"prpl-todo-list prpl-suggested-tasks-list",children:d.map(e=>(0,o.jsx)(i,{task:e,isGolden:!1,isCompleted:!0,onToggle:k,onDelete:_,onMove:y,onTitleChange:b},e.id))})]}),w&&(0,o.jsxs)("div",{id:"todo-list-completed-delete-all-popover",className:"prpl-popover",style:{position:"fixed",top:"50%",left:"50%",transform:"translate(-50%, -50%)",zIndex:1e4,background:"white",padding:"20px",borderRadius:"8px",boxShadow:"0 4px 20px rgba(0,0,0,0.15)"},children:[(0,o.jsx)("div",{className:"prpl-note",children:(0,o.jsx)("span",{className:"prpl-note-text",children:(0,r.__)("Are you sure you want to delete all completed tasks? This action cannot be undone.","progress-planner")})}),(0,o.jsxs)("div",{className:"prpl-buttons-wrapper",style:{display:"flex",gap:"10px",marginTop:"15px"},children:[(0,o.jsxs)("button",{id:"todo-list-completed-delete-all-cancel",onClick:()=>m(!1),children:[(0,o.jsx)("strong",{children:(0,r.__)("No","progress-planner")}),", ",(0,r.__)("keep this list","progress-planner")]}),(0,o.jsxs)("button",{id:"todo-list-completed-delete-all-confirm",onClick:f,children:[(0,o.jsx)("strong",{children:(0,r.__)("Yes","progress-planner")}),", ",(0,r.__)("delete all completed tasks","progress-planner")]})]})]}),w&&(0,o.jsx)("div",{role:"button",tabIndex:0,"aria-label":(0,r.__)("Close dialog","progress-planner"),style:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.3)",zIndex:9999},onClick:()=>m(!1),onKeyDown:e=>{"Enter"!==e.key&&" "!==e.key||m(!1)}})]})}function p(){const e=document.getElementById("prpl-todo-root");e&&(0,t.createRoot)(e).render((0,o.jsx)(d,{}))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()})(); \ No newline at end of file diff --git a/classes/admin/class-dashboard-widget-todo.php b/classes/admin/class-dashboard-widget-todo.php index 7f8c08c76c..7f9916c0b6 100644 --- a/classes/admin/class-dashboard-widget-todo.php +++ b/classes/admin/class-dashboard-widget-todo.php @@ -45,8 +45,6 @@ public function render_widget() { } \progress_planner()->the_view( "dashboard-widgets/{$this->id}.php" ); - - \progress_planner()->the_view( 'js-templates/suggested-task.html' ); } } // phpcs:enable Generic.Commenting.Todo diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index 9b48aeff19..b83d3b29f7 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -206,74 +206,6 @@ public function localize_script( $handle, $localize_data = [] ) { ]; break; - case 'progress-planner/suggested-task': - // Celebrate only on the Progress Planner Dashboard page. - $delay_celebration = true; - if ( \progress_planner()->is_on_progress_planner_dashboard_page() ) { - // should_show_upgrade_popover() also checks if we're on the Progress Planner Dashboard page - but let's be explicit since that method might change in the future. - $delay_celebration = \progress_planner()->get_plugin_upgrade_tasks()->should_show_upgrade_popover(); - } - - // Get the providers available for the user. - $include_providers = []; - $providers_available_for_user = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_providers_available_for_user(); - foreach ( $providers_available_for_user as $provider ) { - // Skip user provider. - if ( 'user' === $provider->get_provider_id() ) { - continue; - } - $include_providers[] = $provider->get_provider_id(); - } - - // Check if user wants to see all recommendations. - $show_all_recommendations = isset( $_GET['prpl_show_all_recommendations'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $tasks_per_page = $show_all_recommendations ? -1 : \Progress_Planner\Admin\Widgets\Suggested_Tasks::PER_PAGE_DEFAULT; - - // Get tasks from task providers (limited to 5 by default, or unlimited if showing all). - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_in_rest_format( - [ - 'post_status' => 'publish', - 'posts_per_page' => $tasks_per_page, - 'include_provider' => $include_providers, // User provider is already excluded. - ] - ); - // Get pending celebration tasks. - $pending_celebration_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_in_rest_format( - [ - 'post_status' => 'pending', - 'posts_per_page' => 100, - 'include_provider' => $include_providers, // User provider is already excluded. - ] - ); - - // Get user tasks. - $user_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_in_rest_format( - [ - 'post_status' => [ 'publish', 'trash' ], - 'include_provider' => [ 'user' ], - ] - ); - - $localize_data = [ - 'name' => 'prplSuggestedTask', - 'data' => [ - 'nonce' => \wp_create_nonce( 'progress_planner' ), - 'assets' => [ - 'infoIcon' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_info.svg', - 'snoozeIcon' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_snooze.svg', - ], - 'tasks' => [ - 'pendingTasks' => $tasks, - 'pendingCelebrationTasks' => $pending_celebration_tasks, - 'userTasks' => $user_tasks, - ], - 'delayCelebration' => $delay_celebration, - 'tasksPerPage' => $tasks_per_page, - 'perPageDefault' => \Progress_Planner\Admin\Widgets\Suggested_Tasks::PER_PAGE_DEFAULT, - ], - ]; - break; - case 'progress-planner/celebrate': // Check if current date is between Feb 12-16 to use hearts confetti. $confetti_options = []; diff --git a/classes/admin/class-page.php b/classes/admin/class-page.php index e2b662e7e2..0ebe2ac9ce 100644 --- a/classes/admin/class-page.php +++ b/classes/admin/class-page.php @@ -196,7 +196,6 @@ public function enqueue_scripts() { \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-tooltip' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'header-filters', $default_localization_data ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'settings', $default_localization_data ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'grid-masonry' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'upgrade-tasks' ); } else { \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboard', $default_localization_data ); diff --git a/classes/admin/widgets/class-todo.php b/classes/admin/widgets/class-todo.php index 1f84a18ab8..d7925989ca 100644 --- a/classes/admin/widgets/class-todo.php +++ b/classes/admin/widgets/class-todo.php @@ -73,18 +73,6 @@ public function print_content() { echo '

                '; } - /** - * The TODO list. - * - * @deprecated 2.0.0 Now handled by React component. - * - * @return void - */ - public function the_todo_list() { - // Legacy method - now handled by React. - echo '
                '; - } - /** * Get the stylesheet dependencies. * diff --git a/package.json b/package.json index 3a81b574d4..e7bbda67d2 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "format": "wp-scripts format ./assets", "lint:css": "wp-scripts lint-style \"**/*.css\"", "lint:css:fix": "npm run lint:css -- --fix", - "lint:js": "wp-scripts lint-js ./assets/js/*.js && wp-scripts lint-js ./assets/js/web-components/*.js && wp-scripts lint-js ./assets/js/widgets/*.js && wp-scripts lint-js ./assets/js/recommendations/*.js && wp-scripts lint-js ./assets/src/**/*.js && wp-scripts lint-js ./tests/**/*.js", - "lint:js:fix": "wp-scripts lint-js ./assets/js/*.js --fix && wp-scripts lint-js ./assets/js/web-components/*.js --fix && wp-scripts lint-js ./assets/js/widgets/*.js --fix && wp-scripts lint-js ./assets/js/recommendations/*.js --fix && wp-scripts lint-js ./assets/src/**/*.js --fix && wp-scripts lint-js ./tests/**/*.js --fix", + "lint:js": "wp-scripts lint-js ./assets/js/*.js && wp-scripts lint-js ./assets/js/web-components/*.js && wp-scripts lint-js ./assets/src/**/*.js && wp-scripts lint-js ./tests/**/*.js", + "lint:js:fix": "wp-scripts lint-js ./assets/js/*.js --fix && wp-scripts lint-js ./assets/js/web-components/*.js --fix && wp-scripts lint-js ./assets/src/**/*.js --fix && wp-scripts lint-js ./tests/**/*.js --fix", "prepare": "husky", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", diff --git a/views/admin-page.php b/views/admin-page.php index 26d1fe02a2..016c46ab83 100644 --- a/views/admin-page.php +++ b/views/admin-page.php @@ -56,5 +56,3 @@ } } ); - -the_view( 'js-templates/suggested-task.html' ); ?> diff --git a/views/dashboard-widgets/todo.php b/views/dashboard-widgets/todo.php index 0fa0ff1a78..447af13b35 100644 --- a/views/dashboard-widgets/todo.php +++ b/views/dashboard-widgets/todo.php @@ -12,4 +12,4 @@ get_admin__widgets__todo()->the_todo_list(); +\progress_planner()->get_admin__widgets__todo()->print_content(); diff --git a/views/js-templates/suggested-task.html b/views/js-templates/suggested-task.html deleted file mode 100644 index f4723088c2..0000000000 --- a/views/js-templates/suggested-task.html +++ /dev/null @@ -1,70 +0,0 @@ - diff --git a/views/page-widgets/todo.php b/views/page-widgets/todo.php index 33d33179e0..56216c7cc0 100644 --- a/views/page-widgets/todo.php +++ b/views/page-widgets/todo.php @@ -43,4 +43,4 @@

                -get_admin__widgets__todo()->the_todo_list(); ?> +get_admin__widgets__todo()->print_content(); ?> From 09f0bd6d337002e2ecb0bf885079f8f9670996b3 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 11 Dec 2025 07:54:44 +0200 Subject: [PATCH 009/275] CSS migration --- assets/css/page-widgets/activity-scores.css | 7 +- assets/css/page-widgets/badge-streak.css | 33 +- assets/css/page-widgets/content-activity.css | 58 +-- assets/css/page-widgets/monthly-badges.css | 20 +- assets/css/page-widgets/suggested-tasks.css | 85 +---- assets/css/page-widgets/todo.css | 125 +------ assets/css/page-widgets/whats-new.css | 36 +- assets/css/suggested-task.css | 338 ++++++------------ assets/src/widgets/ActivityScores/index.js | 2 +- .../widgets/ContentActivity/ActivityTable.js | 7 +- assets/src/widgets/ContentBadges/index.js | 34 +- assets/src/widgets/StreakBadges/index.js | 31 +- .../src/widgets/SuggestedTasks/TaskActions.js | 45 ++- assets/src/widgets/SuggestedTasks/TaskItem.js | 162 ++++++++- assets/src/widgets/SuggestedTasks/index.js | 40 ++- assets/src/widgets/TodoWidget/index.js | 226 +++++++++++- assets/src/widgets/WhatsNew/index.js | 75 +++- build/activity-scores.asset.php | 2 +- build/activity-scores.js | 2 +- build/content-activity.asset.php | 2 +- build/content-activity.js | 2 +- build/content-badges.asset.php | 2 +- build/content-badges.js | 6 +- build/streak-badges.asset.php | 2 +- build/streak-badges.js | 6 +- build/suggested-tasks.asset.php | 2 +- build/suggested-tasks.js | 2 +- build/todo.asset.php | 2 +- build/todo.js | 2 +- build/whats-new.asset.php | 2 +- build/whats-new.js | 2 +- 31 files changed, 737 insertions(+), 623 deletions(-) diff --git a/assets/css/page-widgets/activity-scores.css b/assets/css/page-widgets/activity-scores.css index 9c04c9235b..d3aab5c362 100644 --- a/assets/css/page-widgets/activity-scores.css +++ b/assets/css/page-widgets/activity-scores.css @@ -1,6 +1 @@ -.prpl-widget-wrapper.prpl-activity-scores { - - .prpl-graph-wrapper { - max-height: 300px; - } -} +/* Styles migrated to React inline styles in ActivityScores/index.js */ diff --git a/assets/css/page-widgets/badge-streak.css b/assets/css/page-widgets/badge-streak.css index 3fa64c4562..50592b899d 100644 --- a/assets/css/page-widgets/badge-streak.css +++ b/assets/css/page-widgets/badge-streak.css @@ -1,5 +1,6 @@ +/* React .progress-wrapper styles migrated to StreakBadges/index.js */ - +/* PHP-rendered popover styles - keep until migrated to React */ #popover-badge-streak-content, #popover-badge-streak-maintenance { display: grid; @@ -35,10 +36,6 @@ } } -/*------------------------------------*\ - Badges popover. -\*------------------------------------*/ - #prpl-popover-badge-streak { .indicators { @@ -77,6 +74,7 @@ max-width: 42em; } +/* Widget wrapper layout - keep for PHP wrapper */ .prpl-widget-wrapper.prpl-badge-streak, .prpl-widget-wrapper.prpl-badge-streak-content, .prpl-widget-wrapper.prpl-badge-streak-maintenance { @@ -88,31 +86,6 @@ display: inline-block; } - .progress-wrapper { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: calc(var(--prpl-gap) / 2); - - &:not(:first-child) { - margin-top: var(--prpl-padding); - } - - .prpl-badge { - display: flex; - flex-direction: column; - align-items: center; - flex-wrap: wrap; - min-width: 0; - } - - p { - margin: 0; - font-size: var(--prpl-font-size-small); - text-align: center; - line-height: 1.2; - } - } - .prpl-widget-content { margin-bottom: 1em; } diff --git a/assets/css/page-widgets/content-activity.css b/assets/css/page-widgets/content-activity.css index 4548124649..21b5ad2f85 100644 --- a/assets/css/page-widgets/content-activity.css +++ b/assets/css/page-widgets/content-activity.css @@ -1,57 +1 @@ -.prpl-widget-wrapper.prpl-content-activity { - - table { - width: 100%; - margin-bottom: 1em; - border-spacing: 6px 0; - } - - th, - td { - border: none; - padding: 0.5em; - - &:not(:first-child) { - text-align: center; - } - } - - th { - text-align: start; - } - - tbody { - - th { - font-weight: 400; - } - - tr { - - &:nth-child(odd) { - background-color: var(--prpl-background-table); - } - } - } - - thead { - - th, - td { - text-align: start; - } - } - - tfoot { - - th, - td { - text-align: start; - border-top: 1px solid var(--prpl-color-border); - } - } - - tr:last-child td { - border-bottom: none; - } -} +/* Styles migrated to React inline styles in ContentActivity/ActivityTable.js */ diff --git a/assets/css/page-widgets/monthly-badges.css b/assets/css/page-widgets/monthly-badges.css index 16a14b8c80..08a608abc6 100644 --- a/assets/css/page-widgets/monthly-badges.css +++ b/assets/css/page-widgets/monthly-badges.css @@ -1,9 +1,11 @@ /** - * Suggested tasks widget. + * Monthly badges widget. * + * React PointsCounter styles migrated to MonthlyBadges/PointsCounter.js * Dependencies: progress-planner/web-components/prpl-badge */ +/* Grid positioning - keep for layout */ @media all and (min-width: 1400px) { .prpl-widget-wrapper.prpl-monthly-badges { @@ -12,6 +14,7 @@ } } +/* PHP-rendered widget wrapper styles */ .prpl-widget-wrapper.prpl-monthly-badges { /* Remove styling from the widget wrapper (but not in popover view). */ @@ -47,21 +50,10 @@ margin-bottom: 0; } } - - .prpl-widget-content-points { - display: flex; - justify-content: space-between; - align-items: center; - - .prpl-widget-content-points-number { - font-size: var(--prpl-font-size-3xl); - font-weight: 600; - } - } } /*------------------------------------*\ - Popover styles. + Popover styles - PHP rendered. \*------------------------------------*/ #prpl-popover-monthly-badges { @@ -105,7 +97,7 @@ } } -/* This is the badge streak widget. */ +/* PHP-rendered badge streak widget in monthly-badges context */ .prpl-widget-wrapper.prpl-badge-streak { display: flex; flex-direction: column; diff --git a/assets/css/page-widgets/suggested-tasks.css b/assets/css/page-widgets/suggested-tasks.css index 4f83b716c1..9fd2094173 100644 --- a/assets/css/page-widgets/suggested-tasks.css +++ b/assets/css/page-widgets/suggested-tasks.css @@ -3,15 +3,18 @@ /** * Suggested tasks widget. * + * Core list/loading/empty styles migrated to React inline in SuggestedTasks/index.js * Dependencies: progress-planner/suggested-task, progress-planner/web-components/prpl-badge */ +/* Dashboard widget wrapper - PHP context */ .prpl-dashboard-widget-suggested-tasks { .prpl-suggested-tasks-widget-description { max-width: 40rem; } + /* CSS-only :has() features for conditional display */ &:not(:has(.prpl-suggested-tasks-loading)):not(:has(.prpl-suggested-tasks-list li)) { .prpl-no-suggested-tasks { @@ -28,26 +31,6 @@ .prpl-show-all-tasks { display: none; - - .prpl-toggle-all-recommendations-button { - background: none; - border: none; - padding: 0; - color: var(--wp-admin-theme-color, #2271b1); - text-decoration: underline; - cursor: pointer; - font-size: inherit; - font-family: inherit; - - &:hover { - color: var(--wp-admin-theme-color-darker-10, #135e96); - } - - &:disabled { - opacity: 0.6; - cursor: not-allowed; - } - } } &:has(.prpl-suggested-tasks-list li) { @@ -60,23 +43,10 @@ .prpl-widget-title { display: none; } - - .prpl-no-suggested-tasks, - .prpl-suggested-tasks-loading { - display: none; - background-color: var(--prpl-background-activity); - padding: calc(var(--prpl-padding) / 2); - } - - .prpl-suggested-tasks-loading { - display: block; - } } +/* Last item border removal - CSS-only feature */ .prpl-suggested-tasks-list { - list-style: none; - padding: 0; - margin: 0 0 var(--prpl-padding) 0; &:not(:has(+ .prpl-suggested-tasks-list)) .prpl-suggested-task:last-child { border-bottom: none; @@ -84,12 +54,10 @@ } /*------------------------------------*\ - Interactive tasks, popover. + Interactive tasks, popover - PHP rendered. \*------------------------------------*/ .prpl-popover.prpl-popover-interactive { padding: 24px 24px 14px 24px; - - /* 14px is needed for the "next" button hover state. */ box-sizing: border-box; * { @@ -103,8 +71,6 @@ overflow: hidden; padding-bottom: 10px; - /* Needed for the "next" button hover state. */ - >* { flex-grow: 1; flex-basis: 300px; @@ -136,7 +102,6 @@ .prpl-column { - /* Set margin for headings and paragraphs. */ h1, h2, h3, @@ -170,7 +135,6 @@ } } - /* Set padding and background color for content column (description text). */ &.prpl-column-content { padding: 20px; border-radius: var(--prpl-border-radius-big); @@ -207,8 +171,7 @@ color: var(--prpl-color-alert-error-text); background-color: var(--prpl-background-alert-error); margin-bottom: 0; - - order: 98; /* One less than the spinner. */ + order: 98; flex-grow: 1; .prpl-note-icon { @@ -218,14 +181,12 @@ } } - /* To align the buttons to the bottom of the column. */ &:not(.prpl-column-content) { display: flex; flex-direction: column; - padding-top: 3px; /* To prevent custom radio and checkbox from being cut off. */ + padding-top: 3px; } - /* Inputs. */ input[type="text"], input[type="email"], input[type="number"], @@ -234,12 +195,8 @@ input[type="search"] { height: 44px; padding: 1rem; - - /* WIP */ width: 100%; min-width: 300px; - - /* WIP */ border-radius: 6px; border: 1px solid var(--prpl-color-border); } @@ -254,8 +211,6 @@ border-radius: var(--prpl-border-radius); background-color: var(--prpl-background-banner); cursor: pointer; - - /* WIP: pick exact color */ transition: all 0.25s ease-in-out; position: relative; @@ -265,8 +220,6 @@ width: 100%; height: 100%; background: var(--prpl-background-banner) !important; - - /* WIP: pick exact color */ position: absolute; top: 0; left: 0; @@ -279,12 +232,8 @@ &:focus { background: var(--prpl-background-banner); - /* WIP: pick exact color */ - &::after { background: var(--prpl-background-banner); - - /* WIP: pick exact color */ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.15); width: calc(100% + 4px); height: calc(100% + 4px); @@ -301,11 +250,8 @@ border: 1px solid var(--prpl-color-border); } - /* Used for radio and checkbox inputs. */ .radios { padding-left: 3px; - - /* To prevent custom radio and checkbox from being cut off. */ display: flex; flex-direction: column; gap: 0.5rem; @@ -318,7 +264,6 @@ --prpl-input-green: #3bb3a6; --prpl-input-gray: #8b99a6; - /* Hide the default input, because WP has it's own styles (which include pseudo-elements). */ .prpl-custom-checkbox input[type="checkbox"], .prpl-custom-radio input[type="radio"] { position: absolute; @@ -327,7 +272,6 @@ height: 0; } - /* Shared styles for the custom control */ .prpl-custom-control { display: inline-block; vertical-align: middle; @@ -339,7 +283,6 @@ transition: border-color 0.2s, background 0.2s; } - /* Label text styling */ .prpl-custom-checkbox, .prpl-custom-radio { display: flex; @@ -349,7 +292,6 @@ user-select: none; } - /* Checkbox styles */ .prpl-custom-checkbox { .prpl-custom-control { @@ -360,12 +302,10 @@ input[type="checkbox"] { - /* Checkbox hover (off) */ &:hover + .prpl-custom-control { box-shadow: 0 0 0 2px #f7f8fa, 0 0 0 3px var(--prpl-input-green); } - /* Checkbox checked (on) */ &:checked + .prpl-custom-control { background: var(--prpl-input-green); border-color: var(--prpl-input-green); @@ -373,7 +313,6 @@ } } - /* Checkmark */ .prpl-custom-control::after { content: ""; position: absolute; @@ -394,7 +333,6 @@ } } - /* Radio styles */ .prpl-custom-radio { .prpl-custom-control { @@ -403,14 +341,12 @@ background: #fff; } - /* Radio hover (off) */ input[type="radio"] { &:hover + .prpl-custom-control { box-shadow: 0 0 0 2px #f7f8fa, 0 0 0 3px var(--prpl-input-green); } - /* Radio checked (on) */ &:checked + .prpl-custom-control { background: var(--prpl-input-green); border-color: var(--prpl-input-green); @@ -418,7 +354,6 @@ } } - /* Radio dot */ .prpl-custom-control::after { content: ""; position: absolute; @@ -439,7 +374,6 @@ } } - /* Used for next step button. */ .prpl-steps-nav-wrapper { margin-top: auto; padding-top: 1rem; @@ -450,7 +384,6 @@ align-self: flex-end; width: 100%; - /* If there are no other elements in the form, align the button to the left. */ &:only-child { padding-top: 0; } @@ -459,7 +392,6 @@ &.prpl-steps-nav-wrapper-align-left { justify-content: flex-start; - /* Display the spinner after the button. */ .prpl-spinner { order: 99; } @@ -469,14 +401,12 @@ cursor: pointer; margin: 0; - /* If the button has empty data-action attribute disable it. */ &[data-action=""] { pointer-events: none; opacity: 0.5; } } - /* Display the spinner before the button. */ .prpl-spinner { order: -1; } @@ -484,7 +414,6 @@ } } - /* Set the date format. */ &#prpl-popover-set-date-format { .prpl-radio-wrapper { diff --git a/assets/css/page-widgets/todo.css b/assets/css/page-widgets/todo.css index cdd6898d96..bf046ce68e 100644 --- a/assets/css/page-widgets/todo.css +++ b/assets/css/page-widgets/todo.css @@ -1,9 +1,11 @@ /** * TODOs widget. * + * Core list/form styles migrated to React inline in TodoWidget/index.js * Dependencies: progress-planner/suggested-task */ +/* Widget wrapper padding - PHP context */ .prpl-widget-wrapper.prpl-todo { padding-left: 0; @@ -16,7 +18,7 @@ display: none; } - /* Silver task */ + /* Silver task - CSS-only :has() feature */ &:not(:has(#todo-list li[data-task-points="1"])) { .prpl-todo-silver-task-description { @@ -47,7 +49,7 @@ } } - /* Golden task */ + /* Golden task - CSS-only :has() feature */ &:has(#todo-list li[data-task-points="1"]) { .prpl-todo-silver-task-description { @@ -68,56 +70,20 @@ } } -#create-todo-item { - display: flex; - align-items: center; - flex-direction: row-reverse; - gap: 1em; - - button { - border: 1.5px solid; - border-radius: 50%; - background: none; - box-shadow: none; - display: flex; - align-items: center; - justify-content: center; - padding: 0.2em; - margin-inline-start: 0.3rem; - color: var(--prpl-color-ui-icon); - - .dashicons { - font-size: 0.825em; - width: 1em; - height: 1em; - } - } -} - -#new-todo-content { - flex: 1; - min-width: 0; -} - +/* Hide first/last move buttons - CSS-only feature */ #todo-list, #todo-list-completed { - list-style: none; - padding: 0; - - /* max-height: 30em; */ - - /* overflow-y: auto; */ - - /* margin: 0 0 0.5em calc(var(--prpl-padding) * -1); */ > *:first-child .move-up, - > *:last-child .move-down { + > *:last-child .move-down, + > *:first-child .prpl-move-up, + > *:last-child .prpl-move-down { visibility: hidden; } } /*------------------------------------*\ - Progress Planner TODO Dashboard widget styles. + Dashboard widget styles - PHP context \*------------------------------------*/ #progress_planner_dashboard_widget_todo { @@ -185,14 +151,11 @@ } } +/* Completed tasks list specific styles */ #todo-list-completed { .prpl-suggested-task { - h3 { - text-decoration: line-through; - } - .prpl-suggested-task-actions-wrapper, .prpl-move-buttons-wrapper, button[data-action="complete"] { @@ -201,74 +164,14 @@ } } +/* Completed details section - CSS-only features */ #todo-list-completed-details { - margin-top: 1rem; - border: 1px solid var(--prpl-color-border); - border-radius: 0.5rem; - - summary { - padding: 0.5rem; - font-weight: 500; - display: flex; - - & > .prpl-todo-list-completed-summary-icon { - margin-inline-start: auto; - display: block; - width: 20px; - height: 20px; - - transition: transform 0.3s ease-in-out; - - svg { - stroke: var(--prpl-color-ui-icon); - } - } - } - - &[open] { - - summary > .prpl-todo-list-completed-summary-icon { - transform: rotate(180deg); - } - } &:not(:has(.prpl-suggested-task)) { display: none; } - #todo-list-completed-delete-all-wrapper { - margin: 0.25rem 0.5rem 0.75rem 0.5rem; - border-top: 1px solid var(--prpl-color-border); - display: none; - - #todo-list-completed-delete-all { - display: flex; - align-items: center; - gap: 0.5rem; - background-color: transparent; - border: none; - padding: 0; - margin: 0.5rem 0 0 0; - cursor: pointer; - color: var(--prpl-color-link); - font-size: var(--prpl-font-size-small); - - svg path { - fill: var(--prpl-color-ui-icon); - } - - &:hover { - text-decoration: underline; - - svg path { - fill: var(--prpl-color-ui-icon-hover-delete); - } - } - - } - } - - /* Show the delete all button if there are at least 3 completed tasks */ + /* Show delete all button when 3+ completed tasks */ &:has(.prpl-suggested-task:nth-of-type(3)) #todo-list-completed-delete-all-wrapper { display: block; } @@ -309,6 +212,7 @@ } } + /* Hover effects for completed tasks */ .prpl-suggested-task:hover { .prpl-suggested-task-points { @@ -323,6 +227,7 @@ } } +/* Loading state overlay */ #todo-list { &:has(.prpl-loader) { @@ -342,7 +247,7 @@ } } - +/* Delete all popover - keep as CSS for complex layout */ #todo-list-completed-delete-all-popover { max-width: 600px; diff --git a/assets/css/page-widgets/whats-new.css b/assets/css/page-widgets/whats-new.css index 1a71376098..058e96c6d2 100644 --- a/assets/css/page-widgets/whats-new.css +++ b/assets/css/page-widgets/whats-new.css @@ -1,25 +1,18 @@ +/** + * What's New widget. + * + * Core styles migrated to React inline in WhatsNew/index.js + * Only CSS-only features (hover states) remain here. + */ + +/* Hover states - CSS-only features */ .prpl-widget-wrapper.prpl-whats-new { - ul { - margin: 0; - - p { - margin: 0; - } - } - li { h3 { - margin-top: 0; - font-size: var(--prpl-font-size-lg); - line-height: 1.25; - font-weight: 600; - margin-bottom: 6px; > a { - color: var(--prpl-color-headings); - text-decoration: none; .prpl-external-link-icon { margin-inline-start: 0.15em; @@ -38,12 +31,8 @@ } .prpl-widget-footer { - display: flex; - justify-content: flex-end; a { - color: var(--prpl-color-link); - text-decoration: underline; &:hover { color: var(--prpl-color-link-hover); @@ -54,15 +43,6 @@ } .prpl-blog-post-image { - width: 100%; - min-height: 120px; - aspect-ratio: 3 / 2; - background-size: cover; - margin-bottom: 0.75rem; - border-radius: var(--prpl-border-radius-big); - border: 1px solid var(--prpl-color-border); - background-color: var(--prpl-color-gauge-remain); /* Fallback, if remote host image is not accessible */ - transition: transform 0.2s, box-shadow 0.2s; &:hover { transform: scale(1.01); diff --git a/assets/css/suggested-task.css b/assets/css/suggested-task.css index e1b28ee8e5..ea85857769 100644 --- a/assets/css/suggested-task.css +++ b/assets/css/suggested-task.css @@ -1,243 +1,22 @@ -.prpl-suggested-task { - margin: 0; - padding: 0.75rem 0.5rem 0.625rem 0.5rem; - display: grid; - grid-template-columns: 1.5rem 1fr 3.5rem; - gap: 0.25rem 0.5rem; - position: relative; - line-height: 1; - - &:nth-child(odd) { - background-color: var(--prpl-background-table); - } - - .prpl-suggested-task-title-wrapper { - display: flex; - align-items: center; - gap: 0.5rem; - justify-content: space-between; - - .prpl-task-title { - width: 100%; - color: var(--prpl-color-text); - } - } - - .prpl-suggested-task-actions-wrapper { - grid-column: 2 / span 1; - display: flex; - } - - .prpl-suggested-task-checkbox { - flex-shrink: 0; /* Prevent shrinking on mobile */ - } - - /* If task has disabled checkbox it's title should be italic. */ - &:has(.prpl-suggested-task-disabled-checkbox-tooltip) { - - h3 { - font-style: italic; - } - } - - h3 { - font-size: 1rem; - margin: 0; - font-weight: 500; - - span { - text-decoration: none; - background-image: linear-gradient(#000, #000); - background-repeat: no-repeat; - background-position: center left; - background-size: 0% 1px; - transition: background-size 500ms ease-in-out; - - /* Give the span a width so the user can edit the task title */ - &:empty { - display: inline-block; - width: 100%; - } - } - } +/* Core .prpl-suggested-task styles migrated to React inline in TaskItem.js */ +/* Keep class name for backward compatibility with PHP-rendered tasks */ +.prpl-suggested-task { + /* Input disabled styles - applies to checkboxes */ input[type="checkbox"][disabled] { opacity: 0.5; border-color: #0773bf; background-color: #effbfe; } - &.prpl-suggested-task-celebrated h3 span { - background-size: 100% 1px; - color: inherit; - - /* Accessibility */ - text-decoration: line-through; - text-decoration-color: transparent; - } - - .prpl-suggested-task-points-wrapper { - display: flex; - gap: 0.5rem; - align-items: center; - justify-content: flex-end; - grid-row-end: span 2; - } - - .prpl-suggested-task-points { - font-size: var(--prpl-font-size-xs); - font-weight: 700; - color: var(--prpl-text-point); - background-color: var(--prpl-background-point); - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - } - - .tooltip-actions { - visibility: hidden; - padding-top: 2px; - gap: 0.4rem; - align-items: baseline; - - /* Style for "hover" links. */ - .tooltip-action { - display: inline-flex; - position: relative; - text-decoration: none; - - &:not(:last-child) { - padding-right: 0.4rem; /* same as gap */ - - &::after { - position: absolute; - top: 50%; - transform: translateY(-50%); - right: 0; - - content: ""; - display: inline-block; - width: 1px; - background: var(--prpl-color-text); - height: 0.75rem; - } - } - - .prpl-tooltip-action-text { - line-height: 1; - font-size: var(--prpl-font-size-small); - color: var(--prpl-color-link); - } - - button, - a { - text-decoration: none; - padding: 0; - line-height: 1; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - } - } - - /* Close and toggle radio group buttons should not have a text decoration. */ - .prpl-tooltip-close, - .prpl-toggle-radio-group { - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - } - - } - } - - &:hover, - &:focus-within { - - .tooltip-actions { - visibility: visible; - } - } - - .tooltip-actions:has([data-tooltip-visible]) { - visibility: visible; - } - - .prpl-suggested-task-description { - font-size: 0.825rem; - color: var(--prpl-color-text); - margin: 0; - } - - button { - padding: 0.1rem; - line-height: 0; - margin: 0; - background: none; - border: none; - cursor: pointer; - } - + /* Icon base style */ .icon { width: 1rem; height: 1rem; display: inline-block; } - .trash, - .move-up, - .move-down { - padding: 0; - border: 0; - background: none; - color: var(--prpl-color-ui-icon); - cursor: pointer; - box-shadow: none; - margin-top: 1px; - } - - .prpl-move-buttons, - .prpl-suggested-task-checkbox-wrapper, - .prpl-suggested-task-checkbox-wrapper label { - display: flex; - width: 100%; - gap: 0; - flex-direction: column; - align-items: center; - justify-content: center; - } - - .prpl-move-buttons-wrapper { - position: absolute; - left: calc(-8px - 0.5rem); /* -7px is the half width of the arrow, -0.5rem is the padding of the widget */ - top: 50%; - transform: translateY(-50%); - padding: 10px 10px 10px 0; /* Padding is needed for arrows to be accessible on hover */ - } - - .move-up, - .move-down { - height: 0.75rem; - - .dashicons { - font-size: 0.875rem; - width: 1em; - height: 1em; - } - - &:hover { - color: var(--prpl-color-ui-icon-hover); - } - } - + /* Snooze dropdown - PHP rendered */ .prpl-suggested-task-snooze { &.prpl-toggle-radio-group-open { @@ -308,13 +87,7 @@ } } - &[data-task-action="celebrate"] { - - .tooltip-actions { - pointer-events: none; /* Prevent clicking on actions while celebrating */ - } - } - + /* Task info - PHP rendered */ .prpl-suggested-task-info { margin-left: -30px; @@ -327,7 +100,7 @@ } } - /* Disabled checkbox styles. */ + /* Disabled checkbox tooltip - PHP rendered */ .prpl-suggested-task-disabled-checkbox-tooltip, .tooltip-actions { @@ -346,3 +119,98 @@ } } } + +/* Tooltip actions - keep for hover visibility behavior via CSS */ +.prpl-suggested-task .tooltip-actions { + visibility: hidden; + padding-top: 2px; + gap: 0.4rem; + align-items: baseline; + + /* Style for "hover" links. */ + .tooltip-action { + display: inline-flex; + position: relative; + text-decoration: none; + + &:not(:last-child) { + padding-right: 0.4rem; + + &::after { + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 0; + + content: ""; + display: inline-block; + width: 1px; + background: var(--prpl-color-text); + height: 0.75rem; + } + } + + .prpl-tooltip-action-text { + line-height: 1; + font-size: var(--prpl-font-size-small); + color: var(--prpl-color-link); + } + + button, + a { + text-decoration: none; + padding: 0; + line-height: 1; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + /* Close and toggle radio group buttons should not have a text decoration. */ + .prpl-tooltip-close, + .prpl-toggle-radio-group { + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + } +} + +.prpl-suggested-task:hover .tooltip-actions, +.prpl-suggested-task:focus-within .tooltip-actions { + visibility: visible; +} + +.prpl-suggested-task .tooltip-actions:has([data-tooltip-visible]) { + visibility: visible; +} + +/* Celebrate action state */ +.prpl-suggested-task[data-task-action="celebrate"] { + + .tooltip-actions { + pointer-events: none; + } +} + +/* Disabled checkbox italic title - CSS-only feature */ +.prpl-suggested-task:has(.prpl-suggested-task-disabled-checkbox-tooltip) { + + h3 { + font-style: italic; + } +} + +/* Description text */ +.prpl-suggested-task .prpl-suggested-task-description { + font-size: 0.825rem; + color: var(--prpl-color-text); + margin: 0; +} diff --git a/assets/src/widgets/ActivityScores/index.js b/assets/src/widgets/ActivityScores/index.js index 48a16df90a..00cbf6d403 100644 --- a/assets/src/widgets/ActivityScores/index.js +++ b/assets/src/widgets/ActivityScores/index.js @@ -140,7 +140,7 @@ export default function ActivityScores() { 'progress-planner' ) }

                -
                +
                diff --git a/assets/src/widgets/ContentActivity/ActivityTable.js b/assets/src/widgets/ContentActivity/ActivityTable.js index e46fcee617..562d54118a 100644 --- a/assets/src/widgets/ContentActivity/ActivityTable.js +++ b/assets/src/widgets/ContentActivity/ActivityTable.js @@ -31,6 +31,11 @@ export default function ActivityTable( { padding: '0.5em', }; + const bodyCellStyle = { + ...cellStyle, + fontWeight: 400, + }; + const centeredCellStyle = { ...cellStyle, textAlign: 'center', @@ -81,7 +86,7 @@ export default function ActivityTable( { > { activityTypes[ key ].label } diff --git a/assets/src/widgets/ContentBadges/index.js b/assets/src/widgets/ContentBadges/index.js index 3a570ce1cc..50a90ea573 100644 --- a/assets/src/widgets/ContentBadges/index.js +++ b/assets/src/widgets/ContentBadges/index.js @@ -65,6 +65,32 @@ export default function ContentBadges() { return

                { __( 'No badge data available.', 'progress-planner' ) }

                ; } + // Inline styles. + const progressWrapperStyle = { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: 'calc(var(--prpl-gap) / 4)', + padding: 'calc(var(--prpl-padding) / 2)', + borderRadius: 'var(--prpl-border-radius-big)', + background: 'var(--prpl-background-content-badge)', + }; + + const badgeStyle = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'flex-start', + flexWrap: 'wrap', + minWidth: 0, + }; + + const badgeLabelStyle = { + margin: 0, + fontSize: 'var(--prpl-font-size-small)', + textAlign: 'center', + lineHeight: 1.2, + }; + return ( <>

                @@ -138,11 +164,15 @@ export default function ContentBadges() {


                -
                +
                { allBadges.map( ( badge ) => ( -

                { badge.name }

                +

                { badge.name }

                ) ) }
                diff --git a/assets/src/widgets/StreakBadges/index.js b/assets/src/widgets/StreakBadges/index.js index b44065eae0..748d7b416f 100644 --- a/assets/src/widgets/StreakBadges/index.js +++ b/assets/src/widgets/StreakBadges/index.js @@ -138,12 +138,30 @@ export default function StreakBadges() {
                -
                +
                { allBadges.map( ( badge ) => ( -

                { badge.name }

                +

                + { badge.name } +

                ) ) }
                diff --git a/assets/src/widgets/SuggestedTasks/TaskActions.js b/assets/src/widgets/SuggestedTasks/TaskActions.js index bc5635e311..ab6b254218 100644 --- a/assets/src/widgets/SuggestedTasks/TaskActions.js +++ b/assets/src/widgets/SuggestedTasks/TaskActions.js @@ -110,33 +110,70 @@ export default function TaskActions( { // Get task actions from API response. const taskActions = task.prpl_task_actions || []; + // Inline styles. + const actionsStyle = { + paddingTop: '2px', + gap: '0.4rem', + alignItems: 'baseline', + }; + + const actionStyle = { + display: 'inline-flex', + position: 'relative', + textDecoration: 'none', + }; + + const actionTextStyle = { + lineHeight: 1, + fontSize: 'var(--prpl-font-size-small)', + color: 'var(--prpl-color-link)', + }; + + const buttonStyle = { + textDecoration: 'none', + padding: 0, + lineHeight: 1, + background: 'none', + border: 'none', + cursor: 'pointer', + }; + // If no actions and not a user task, return empty. if ( taskActions.length === 0 && ! isUserTask ) { - return
                ; + return
                ; } return ( -
                +
                { /* Render pre-built HTML actions from the API */ } { taskActions.map( ( actionHTML, index ) => ( ) ) } { /* Add delete button for user tasks */ } { isUserTask && ( - +
                { ! isCompleted && ( -
                +