diff --git a/apps/components/App.js b/apps/components/App.js index 7301d1166b..b02c24d533 100644 --- a/apps/components/App.js +++ b/apps/components/App.js @@ -17,6 +17,7 @@ import Wizard from "./WizardWrapper"; import { Loader } from "@yoast/components"; import FacebookPreviewExample from "./FacebookPreviewExample"; import LinkSuggestionsWrapper from "./LinkSuggestionsExample"; +import WordOccurrencesWrapper from "./WordOccurrencesWrapper"; // Setup empty translations to prevent Jed error. setLocaleData( { "": {} }, "yoast-components" ); @@ -97,6 +98,11 @@ const components = [ name: "FacebookPreview", component: , }, + { + id: "wordoccurrences-example", + name: "WordOccurrences", + component: , + }, ]; const LanguageDirectionContainer = styled.div` diff --git a/apps/components/WordOccurrencesWrapper.js b/apps/components/WordOccurrencesWrapper.js new file mode 100644 index 0000000000..0e10122d97 --- /dev/null +++ b/apps/components/WordOccurrencesWrapper.js @@ -0,0 +1,236 @@ +import React from "react"; + +import ExamplesContainer from "./ExamplesContainer"; +import { WordOccurrences } from "@yoast/components"; + +const initialRelevantWords = [ + { + _word: "davids", + _stem: "david", + _occurrences: 2, + }, + { + _word: "goliaths", + _stem: "goliath", + _occurrences: 6, + }, + { + _word: "word", + _stem: "word", + _occurrences: 3, + }, + { + _word: "yoast", + _stem: "yoast", + _occurrences: 8, + }, + { + _word: "test", + _stem: "test", + _occurrences: 10, + }, + { + _word: "apps", + _stem: "app", + _occurrences: 6, + }, + { + _word: "teletubbies", + _stem: "teletubby", + _occurrences: 11, + }, + { + _word: "strange", + _stem: "stange", + _occurrences: 4, + }, + { + _word: "improvisation", + _stem: "improvisation", + _occurrences: 4, + }, + { + _word: "ranking", + _stem: "rank", + _occurrences: 5, + }, + { + _word: "google", + _stem: "google", + _occurrences: 5, + }, + { + _word: "terms", + _stem: "term", + _occurrences: 8, + }, + { + _word: "wordpress", + _stem: "wordpress", + _occurrences: 9, + }, + { + _word: "inspiration", + _stem: "inspiration", + _occurrences: 2, + }, + { + _word: "internal", + _stem: "internal", + _occurrences: 2, + }, + { + _word: "linking", + _stem: "link", + _occurrences: 2, + }, + { + _word: "suggestions", + _stem: "suggestion", + _occurrences: 3, + }, + { + _word: "keyword", + _stem: "keyword", + _occurrences: 3, + }, + { + _word: "analysis", + _stem: "analysis", + _occurrences: 4, + }, + { + _word: "linguïns", + _stem: "linguïn", + _occurrences: 5, + }, +]; + +const RelevantWordInputList = ( props ) => { + return props.relevantWords.map( + ( relevantWord, index ) => { + return ; + } + ); +}; + +const RelevantWordInputRow = ( props ) => { + function changeWord( event ) { + props.onChange( event.target.value, "_word", props.index ); + } + function changeStem( event ) { + props.onChange( event.target.value, "_stem", props.index ); + } + function changeOccurrences( event ) { + props.onChange( event.target.value, "_occurrences", props.index ); + } + function onDeleteClick(){ + props.onDeleteClick( props.index ); + } + return ( +
+ + + + +
+ ); +}; + +class WordOccurrencesWrapper extends React.Component { + constructor( props ) { + super( props ); + + this.state = { + relevantWords: initialRelevantWords.map( initialWord => Object.assign( {}, initialWord ) ), + }; + + this.changeRelevantWord = this.changeRelevantWord.bind( this ); + this.removeRelevantWord = this.removeRelevantWord.bind( this ); + this.addRelevantWordRow = this.addRelevantWordRow.bind( this ); + this.resetRelevantWords = this.resetRelevantWords.bind( this ); + } + + changeRelevantWord( input, type, index ) { + this.setState( ( prevState ) => { + const nextState = Object.assign( {}, prevState ); + nextState.relevantWords = [ ...prevState.relevantWords ]; + nextState.relevantWords[ index ][ type ] = input; + return nextState; + } ); + } + + removeRelevantWord( index ) { + this.setState( ( prevState ) => { + const nextState = Object.assign( {}, prevState ); + nextState.relevantWords.splice( index, 1 ); + return { + relevantWords: nextState.relevantWords, + }; + } ); + } + + addRelevantWordRow() { + this.setState( prevState => { + prevState.relevantWords.push( + { + _word: "", + _stem: "", + _occurrence: "", + } + ); + return ( + { + relevantWords: prevState.relevantWords, + } + ); + } ); + } + + resetRelevantWords() { + this.setState( { + relevantWords: initialRelevantWords.map( initialWord => Object.assign( {}, initialWord ) ), + } ); + } + + render() { + return ( + + + + +
+ +
+
+ ); + } +} + +export default WordOccurrencesWrapper; diff --git a/packages/components/src/KeywordSuggestions.js b/packages/components/src/KeywordSuggestions.js index c0c14e1364..97e9d83101 100644 --- a/packages/components/src/KeywordSuggestions.js +++ b/packages/components/src/KeywordSuggestions.js @@ -7,6 +7,7 @@ import { isFeatureEnabled } from "@yoast/feature-flag"; // Yoast dependencies. import WordList from "./WordList"; +import WordOccurrences from "./WordOccurrences"; /** @@ -89,23 +90,25 @@ const getKeywordSuggestionExplanation = keywords => { * @returns {JSX.Element} Rendered WordList component. */ const KeywordSuggestions = ( { relevantWords, keywordLimit } ) => { - const keywords = relevantWords.slice( 0, keywordLimit ).map( word => { - return isFeatureEnabled( "improvedInternalLinking" ) ? word.getWord() : word.getCombination(); - } ); + if ( isFeatureEnabled( "improvedInternalLinking" ) ) { + return ; + } - return ( { - return (

{ getKeywordSuggestionExplanation( keywords ) }

); - } } - showAfterList={ - () => { - return getKeywordResearchArticleLink(); + const prominentWords = relevantWords.slice( 0, keywordLimit ).map( word => word.getWord() ); + return ( + { + return (

{ getKeywordSuggestionExplanation( prominentWords ) }

); + } } + showAfterList={ + () => { + return getKeywordResearchArticleLink(); + } } - } - /> + /> ); }; diff --git a/packages/components/src/WordOccurrences.js b/packages/components/src/WordOccurrences.js new file mode 100644 index 0000000000..8822314697 --- /dev/null +++ b/packages/components/src/WordOccurrences.js @@ -0,0 +1,123 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; + +const ProminentWordOccurrence = styled.p` + display: inline-block; + margin: 2px 8px; +`; + +const ProminentWord = styled( ProminentWordOccurrence )` + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; +`; + +const WordOccurrencesList = styled.ol` + list-style: none; + padding: 0; + width: 100%; +`; + +const WordBarContainer = styled.li` + margin: 8px 0; + height: calc( 1.375em + 4px ); + display: flex; + width: 100%; + justify-content: space-between; + background: + linear-gradient( + to right, + rgba(164, 40, 106, 0.2) ${ props => props.width }, + ${ props => props.width }, + transparent + ); +`; + +/** + * A list item containing a word and its occurrence, with a gradient background reflecting the relative occurrence. + * + * @constructor + * + * @param {Object} word The word. + * @param {number} occurrences The word's occurrence. + * @param {string} width A string indicating the percentage of the bar that should be coloured purple. + * @returns {JSX} The list item. + */ +const WordBar = ( { word, occurrence, width } ) => { + return ( + + { word } + { occurrence } + + ); +}; + +WordBar.propTypes = { + word: PropTypes.string.isRequired, + occurrence: PropTypes.number.isRequired, + width: PropTypes.string.isRequired, +}; + +/** + * The WordOccurrences list, that contains words, their occurrence, and bars reflecting their relative occurrence. + * + * @param {Array} words The array of objects containing words and occurrences. + * + * @returns {ReactElement} The list of words, their occurrences, and bars. + */ +class WordOccurrences extends React.Component { + constructor( props ) { + super( props ); + + this.state = { + words: [ ...props.words ] || [], + occurrences: { + min: 0, + max: 0, + }, + }; + } + + static getDerivedStateFromProps( props ) { + const words = [ ...props.words ]; + words.sort( ( a, b ) => { + return b._occurrences - a._occurrences; + } ); + const allOccurrences = words.map( wordObject => wordObject._occurrences ); + + return { + occurrences: { + min: Math.min( ...allOccurrences ), + max: Math.max( ...allOccurrences ), + }, + words: words, + }; + } + + render() { + return ( + + { + this.state.words.map( + ( wordObject, index ) => { + const width = `${ ( wordObject._occurrences / this.state.occurrences.max ) * 100 }%`; + return ; + } + ) + } + + ); + } +} + +WordOccurrences.propTypes = { + words: PropTypes.array.isRequired, +}; + +export default WordOccurrences; diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 0fc1e9e961..402c7368a3 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -70,6 +70,7 @@ export { default as Tabs } from "./Tabs"; export { default as Warning } from "./Warning"; export { default as YouTubeVideo } from "./YouTubeVideo"; export { default as WordList } from "./WordList"; +export { default as WordOccurrences } from "./WordOccurrences"; export { default as Checkbox } from "./Checkbox"; export { ListTable, ZebrafiedListTable } from "./table/ListTable";