diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e086a7246..f432c787e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -84,11 +84,12 @@ jobs: ONBOARDING_TESTING_ENDING_DATE: "${{ vars.ONBOARDING_TESTING_ENDING_DATE }}" QUALITATIVE_FEEDBACK_ACTIVE: "${{ vars.QUALITATIVE_FEEDBACK_ACTIVE }}" QUALITATIVE_FEEDBACK_STARTING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTING_DATE }}" - QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE }}" - QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE }}" - QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE }}" - QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE }}" + QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY }}" + QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY }}" + QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY }}" + QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY }}" QUALITATIVE_FEEDBACK_ENDING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_ENDING_DATE }}" + IDEAS_GENERATOR_SOURCE: "${{ vars.IDEAS_GENERATOR_SOURCE }}" # used by src/template-config.js GTM_ID: ${{ secrets.GTM_ID }} diff --git a/src/components/carousel/carousel.jsx b/src/components/carousel/carousel.jsx index 65bd4fe2f..2568a16b7 100644 --- a/src/components/carousel/carousel.jsx +++ b/src/components/carousel/carousel.jsx @@ -59,9 +59,11 @@ const Carousel = props => { href = `/studios/${item.id}/`; break; case 'project': - href = props.fromStarterProjectsPage ? - `/projects/${item.id}?fromStarterProjectsPage=${props.fromStarterProjectsPage}` : - `/projects/${item.id}/`; + href = `/projects/${item.id}${ + props.fromStarterProjectsPage ? + '?fromStarterProjectsPage=true' : + '' + }`; break; default: href = `/${item.type}/${item.id}/`; diff --git a/src/components/modal/feedback/debugging-feedback.jsx b/src/components/modal/feedback/debugging-feedback.jsx index d1660a98b..8f5089c62 100644 --- a/src/components/modal/feedback/debugging-feedback.jsx +++ b/src/components/modal/feedback/debugging-feedback.jsx @@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js'; import { QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_QUESTION_ID -} from './qualitative_feedback_data.js'; -import {QualitativeFeedback} from './qualitative_feedback.jsx'; +} from './qualitative-feedback-data.js'; +import {QualitativeFeedback} from './qualitative-feedback.jsx'; import {connect} from 'react-redux'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; diff --git a/src/components/modal/feedback/ideas-generator-feedback.jsx b/src/components/modal/feedback/ideas-generator-feedback.jsx index 6662ec73f..44bdff641 100644 --- a/src/components/modal/feedback/ideas-generator-feedback.jsx +++ b/src/components/modal/feedback/ideas-generator-feedback.jsx @@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js'; import { QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_QUESTION_ID -} from './qualitative_feedback_data.js'; -import {QualitativeFeedback} from './qualitative_feedback.jsx'; +} from './qualitative-feedback-data.js'; +import {QualitativeFeedback} from './qualitative-feedback.jsx'; import {connect} from 'react-redux'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; diff --git a/src/components/modal/feedback/qualitative_feedback_data.js b/src/components/modal/feedback/qualitative-feedback-data.js similarity index 100% rename from src/components/modal/feedback/qualitative_feedback_data.js rename to src/components/modal/feedback/qualitative-feedback-data.js diff --git a/src/components/modal/feedback/qualitative_feedback.jsx b/src/components/modal/feedback/qualitative-feedback.jsx similarity index 62% rename from src/components/modal/feedback/qualitative_feedback.jsx rename to src/components/modal/feedback/qualitative-feedback.jsx index b8baf45c4..e8d903704 100644 --- a/src/components/modal/feedback/qualitative_feedback.jsx +++ b/src/components/modal/feedback/qualitative-feedback.jsx @@ -10,7 +10,7 @@ import {Formik} from 'formik'; import classNames from 'classnames'; import FormikRadioButton from '../../formik-forms/formik-radio-button.jsx'; -import './qualitative_feedback.scss'; +import './qualitative-feedback.scss'; const FeedbackOption = ({ id, @@ -45,7 +45,12 @@ FeedbackOption.propTypes = { value: PropTypes.string }; -export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAEvent}) => { +export const QualitativeFeedback = ({ + feedbackData, + hideFeedback, + isOpen, + sendGAEvent +}) => { const [displayModal, setDisplayModal] = useState(false); const [_, setFilledFeedback] = useLocalStorage( feedbackData.questionId, @@ -54,23 +59,37 @@ export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAE const intl = useIntl(); const onClose = useCallback(() => { - sendGAEvent('closed'); + if (displayModal) { + setDisplayModal(false); + sendGAEvent('closed'); + } + setFilledFeedback(true); hideFeedback(); - setDisplayModal(false); - }, [setFilledFeedback, hideFeedback, setDisplayModal]); + }, [ + displayModal, + setFilledFeedback, + hideFeedback, + setDisplayModal, + sendGAEvent + ]); - // TBD: add logic for sending events to GA const onSubmit = useCallback( formData => { - if (formData.feedback) { + if (formData.feedback && displayModal) { + setDisplayModal(false); sendGAEvent(formData.feedback); setFilledFeedback(true); hideFeedback(); - setDisplayModal(false); } }, - [hideFeedback, setDisplayModal, setFilledFeedback] + [ + displayModal, + hideFeedback, + setDisplayModal, + setFilledFeedback, + sendGAEvent + ] ); useEffect(() => { @@ -78,7 +97,7 @@ export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAE const timer = setTimeout(() => { setDisplayModal(true); }, 5000); - + return () => clearTimeout(timer); } }, [isOpen]); @@ -102,33 +121,31 @@ export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAE }} onSubmit={onSubmit} > - {({handleSubmit, setFieldValue, values}) => - ( - <form - className="feedback-form" - onSubmit={handleSubmit} - > - <div className="feedback-question"> - <FormattedMessage id={feedbackData.questionId} /> - </div> - <div className="feedback-options"> - {feedbackData.options.map(option => ( - <FeedbackOption - key={option.value} - id={option.label} - label={intl.formatMessage({id: option.label})} - selectedValue={values.feedback} - value={option.value} - onSetFieldValue={setFieldValue} - /> - ))} - </div> - <Button className="feedback-submit"> - <FormattedMessage id="feedback.submit" /> - </Button> - </form> - ) - } + {({handleSubmit, setFieldValue, values}) => ( + <form + className="feedback-form" + onSubmit={handleSubmit} + > + <div className="feedback-question"> + <FormattedMessage id={feedbackData.questionId} /> + </div> + <div className="feedback-options"> + {feedbackData.options.map(option => ( + <FeedbackOption + key={option.value} + id={option.label} + label={intl.formatMessage({id: option.label})} + selectedValue={values.feedback} + value={option.value} + onSetFieldValue={setFieldValue} + /> + ))} + </div> + <Button className="feedback-submit"> + <FormattedMessage id="feedback.submit" /> + </Button> + </form> + )} </Formik> </ModalInnerContent> </Modal> diff --git a/src/components/modal/feedback/qualitative_feedback.scss b/src/components/modal/feedback/qualitative-feedback.scss similarity index 100% rename from src/components/modal/feedback/qualitative_feedback.scss rename to src/components/modal/feedback/qualitative-feedback.scss diff --git a/src/components/modal/feedback/starter-projects-feedback.jsx b/src/components/modal/feedback/starter-projects-feedback.jsx index 929de92d4..49ff16f26 100644 --- a/src/components/modal/feedback/starter-projects-feedback.jsx +++ b/src/components/modal/feedback/starter-projects-feedback.jsx @@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js'; import { QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_QUESTION_ID -} from './qualitative_feedback_data.js'; -import {QualitativeFeedback} from './qualitative_feedback.jsx'; +} from './qualitative-feedback-data.js'; +import {QualitativeFeedback} from './qualitative-feedback.jsx'; import {connect} from 'react-redux'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; @@ -23,7 +23,7 @@ const StarterProjectsFeedback = ({hideFeedback, isOpen, projectName}) => { projectName: projectName, feedbackResponse: data }), - [] + [projectName] ); return ( diff --git a/src/components/modal/feedback/tutorials-feedback.jsx b/src/components/modal/feedback/tutorials-feedback.jsx index fadf60af9..380162244 100644 --- a/src/components/modal/feedback/tutorials-feedback.jsx +++ b/src/components/modal/feedback/tutorials-feedback.jsx @@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js'; import { QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_QUESTION_ID -} from './qualitative_feedback_data.js'; -import {QualitativeFeedback} from './qualitative_feedback.jsx'; +} from './qualitative-feedback-data.js'; +import {QualitativeFeedback} from './qualitative-feedback.jsx'; import {connect} from 'react-redux'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; diff --git a/src/lib/feedback.js b/src/lib/feedback.js index 3e61e5de7..6df7cdb3d 100644 --- a/src/lib/feedback.js +++ b/src/lib/feedback.js @@ -16,10 +16,13 @@ const isCurrentDayInRange = () => { ); }; -const hasGivenFeedback = feedbackQuestionId => +const notRespondedToFeedback = feedbackQuestionId => localStorageAvailable && localStorage.getItem(feedbackQuestionId) !== 'true'; const canShowFeedbackWidget = feedbackUserRate => { + // randomNumber will be in the range [0, feedbackUserRate - 1] + // and we are assering against one number from the range + // this way we simulate picking one from n users const randomNum = Math.floor( Math.random() * feedbackUserRate ); @@ -35,11 +38,11 @@ const isUserEligibleForFeedback = (user, permissions, feedbackQuestionId, feedba JSON.parse(process.env.QUALITATIVE_FEEDBACK_ACTIVE) && canShowFeedbackWidget(feedbackUserRate) && isCurrentDayInRange() && - hasGivenFeedback(feedbackQuestionId) + notRespondedToFeedback(feedbackQuestionId) ); const isFeedbackDisplayed = feedbacksDisplayed => - !Object.entries(feedbacksDisplayed).some(feedback => + Object.entries(feedbacksDisplayed).some(feedback => feedback.value ); @@ -51,7 +54,7 @@ export const shouldDisplayFeedbackWidget = ( feedbacksDisplayed ) => ( isUserEligibleForFeedback(user, permissions, feedbackQuestionId, feedbackUserRate) && - isFeedbackDisplayed(feedbacksDisplayed) + !isFeedbackDisplayed(feedbacksDisplayed) ); export const sendUserPropertiesForFeedback = (user, permissions, shouldDisplayFeedback) => { diff --git a/src/lib/user-eligibility.js b/src/lib/user-eligibility.js index 93831525b..0d3aad68c 100644 --- a/src/lib/user-eligibility.js +++ b/src/lib/user-eligibility.js @@ -17,7 +17,7 @@ export const isDateInRange = (date, startDate, endDate) => { export const calculateAgeGroup = (birthYear, birthMonth) => { if (!birthMonth || !birthYear) { - return '[unset]'; + return '[no-data]'; } const today = new Date(); diff --git a/src/redux/qualitative-feedback.js b/src/redux/qualitative-feedback.js index 436d05c45..a452ff0be 100644 --- a/src/redux/qualitative-feedback.js +++ b/src/redux/qualitative-feedback.js @@ -1,4 +1,4 @@ -import {QUALITATIVE_FEEDBACK_QUESTION_ID} from '../components/modal/feedback/qualitative_feedback_data'; +import {QUALITATIVE_FEEDBACK_QUESTION_ID} from '../components/modal/feedback/qualitative-feedback-data'; const initialState = { [QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator]: false, diff --git a/src/views/ideas/ideas.jsx b/src/views/ideas/ideas.jsx index 90227a134..bab7edf13 100644 --- a/src/views/ideas/ideas.jsx +++ b/src/views/ideas/ideas.jsx @@ -29,7 +29,7 @@ const { const {useRef} = require('react'); const { QUALITATIVE_FEEDBACK_QUESTION_ID -} = require('../../components/modal/feedback/qualitative_feedback_data.js'); +} = require('../../components/modal/feedback/qualitative-feedback-data.js'); const {shouldDisplayFeedbackWidget, sendUserPropertiesForFeedback} = require('../../lib/feedback.js'); require('./ideas.scss'); @@ -165,25 +165,30 @@ const Ideas = ({ } }; - const shoulDisplayFeedback = shouldDisplayFeedbackWidget( + const shouldDisplayFeedback = shouldDisplayFeedbackWidget( user, permissions, QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator, - process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE, + process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY, feedback ); - if (iframe && shoulDisplayFeedback) { + if (iframe && shouldDisplayFeedback) { sendUserPropertiesForFeedback( user, permissions, - shoulDisplayFeedback + shouldDisplayFeedback ); iframe.addEventListener('load', onIframeLoad); } return () => { if (iframe) { + iframe.contentWindow.document + .querySelectorAll('[class*="green-flag"]') + .forEach(element => + element.removeEventListener('click', onGreenFlagClick) + ); iframe.removeEventListener('load', onIframeLoad); } }; @@ -197,7 +202,7 @@ const Ideas = ({ <div className="banner-wrapper"> <iframe ref={iframeRef} - src={`http://localhost:8333/projects/9999923/embed`} + src={`${process.env.IDEAS_GENERATOR_SOURCE}/embed`} width="485" height="402" allowfullscreen diff --git a/src/views/preview/project-view.jsx b/src/views/preview/project-view.jsx index 0918f7bf9..5bdc320b2 100644 --- a/src/views/preview/project-view.jsx +++ b/src/views/preview/project-view.jsx @@ -47,7 +47,7 @@ const TutorialsHighlight = require('../../components/journeys/tutorials-highligh const {sendUserPropertiesForOnboarding, shouldDisplayOnboarding} = require('../../lib/onboarding.js'); const {triggerAnalyticsEvent} = require('../../lib/google-analytics-utils.js'); const {StarterProjectsFeedback} = require('../../components/modal/feedback/starter-projects-feedback.jsx'); -const {QUALITATIVE_FEEDBACK_QUESTION_ID} = require('../../components/modal/feedback/qualitative_feedback_data.js'); +const {QUALITATIVE_FEEDBACK_QUESTION_ID} = require('../../components/modal/feedback/qualitative-feedback-data.js'); const {shouldDisplayFeedbackWidget, sendUserPropertiesForFeedback} = require('../../lib/feedback.js'); const {displayQualitativeFeedback} = require('../../redux/qualitative-feedback.js'); const {DebuggingFeedback} = require('../../components/modal/feedback/debugging-feedback.jsx'); @@ -73,7 +73,7 @@ const IntlGUIWithProjectHandler = ({...props}) => { }, [props.projectId, prevProjectId, props.user, props.permissions]); const displayGuiFeedback = useCallback((feedbackQuestionId, feedbackUserRate) => { - const shoulDisplayFeedback = shouldDisplayFeedbackWidget( + const shouldDisplayFeedback = shouldDisplayFeedbackWidget( props.user, props.permissions, feedbackQuestionId, @@ -81,11 +81,11 @@ const IntlGUIWithProjectHandler = ({...props}) => { props.feedback ); - if (shoulDisplayFeedback) { + if (shouldDisplayFeedback) { sendUserPropertiesForFeedback( props.user, props.permissions, - shoulDisplayFeedback + shouldDisplayFeedback ); props.displayFeedback(feedbackQuestionId); } @@ -95,14 +95,14 @@ const IntlGUIWithProjectHandler = ({...props}) => { <> <IntlGUI // eslint-disable-next-line react/jsx-no-bind - displayDebugFeedback={() => displayGuiFeedback( + onDebugModalClose={() => displayGuiFeedback( QUALITATIVE_FEEDBACK_QUESTION_ID.debugging, - process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE + process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY )} // eslint-disable-next-line react/jsx-no-bind - displayTutorialsFeedback={() => displayGuiFeedback( + onTutorialSelect={() => displayGuiFeedback( QUALITATIVE_FEEDBACK_QUESTION_ID.tutorials, - process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE + process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY )} {...props} /> @@ -305,18 +305,18 @@ class Preview extends React.Component { sendUserPropertiesForOnboarding(this.props.user, this.props.permissions); const fromStarterProjectsPage = queryString.parse(location.search).fromStarterProjectsPage === 'true'; - const shoulDisplayFeedback = shouldDisplayFeedbackWidget( + const shouldDisplayFeedback = shouldDisplayFeedbackWidget( this.props.user, this.props.permissions, QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects, - process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE, + process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY, this.props.feedback ); - if (fromStarterProjectsPage && shoulDisplayFeedback) { + if (fromStarterProjectsPage && shouldDisplayFeedback) { sendUserPropertiesForFeedback( this.props.user, this.props.permissions, - shoulDisplayFeedback + shouldDisplayFeedback ); this.props.displayFeedback( QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects diff --git a/webpack.config.js b/webpack.config.js index dec67e397..e833bb5f9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -308,19 +308,20 @@ module.exports = { process.env.QUALITATIVE_FEEDBACK_STARTING_DATE || '2024-01-20' }"`, 'process.env.QUALITATIVE_FEEDBACK_ENDING_DATE': `"${ - process.env.QUALITATIVE_FEEDBACK_ENDING_DATE || '2030-11-20' + process.env.QUALITATIVE_FEEDBACK_ENDING_DATE || '2024-11-20' }"`, - 'process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE': `"${ - process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE || 2 + // Given user frequency X, show qualitative feedback to 1 in X users + 'process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY': `"${ + process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY || 2 }"`, - 'process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE': `"${ - process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE || 2 + 'process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY': `"${ + process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY || 2 }"`, - 'process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE': `"${ - process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE || 2 + 'process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY': `"${ + process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY || 2 }"`, - 'process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE': `"${ - process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE || 2 + 'process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY': `"${ + process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY || 2 }"`, 'process.env.IDEAS_GENERATOR_SOURCE': `"${ process.env.IDEAS_GENERATOR_SOURCE || 'https://scratch.mit.edu/projects/1108790117'