From 35edaea8ac7bc3d56df99a094f799d9f4ba45abd Mon Sep 17 00:00:00 2001 From: Aron Demeter <66035744+dem4ron@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:04:52 +0100 Subject: [PATCH] Track welcome modal changes (#7183) * Adjust width of modal * Add WhoIsThisTrackForView, pass down seniority level * Set up logic around showing bootcamp recommendation view * Add Views, rearrange things * Add bootcamp landing path * Commit changes in track_welcome_modal * Add tests * Change HasLearningModeStep copy * Tweak copy * Adjust tests and copy --------- Co-authored-by: Jeremy Walker --- app/css/modals/track-welcome-modal.css | 2 +- .../modals/track_welcome_modal.rb | 6 +- app/images/bootcamp/certificate.svg | 1 + app/images/bootcamp/complete.svg | 1 + .../LHS/BootcampRecommendationView.tsx | 86 +++++++++++++++++++ .../LHS/TrackWelcomeModal.machine.ts | 2 +- .../LHS/steps/HasLearningModeStep.tsx | 4 +- .../steps/LearningEnvironmentSelectorStep.tsx | 10 ++- .../RHS/TrackWelcomeModalRHS.tsx | 18 ++++ .../VideoRHS.tsx} | 7 +- .../RHS/WhoIsThisTrackForRHS.tsx | 34 ++++++++ .../track-welcome-modal/TrackWelcomeModal.tsx | 26 +++++- .../TrackWelcomeModal.types.tsx | 5 +- .../useTrackWelcomeModal.ts | 33 +++++-- .../pages/tracks/track_welcome_modal_test.rb | 52 ++++++++++- 15 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 app/images/bootcamp/certificate.svg create mode 100644 app/images/bootcamp/complete.svg create mode 100644 app/javascript/components/modals/track-welcome-modal/LHS/BootcampRecommendationView.tsx create mode 100644 app/javascript/components/modals/track-welcome-modal/RHS/TrackWelcomeModalRHS.tsx rename app/javascript/components/modals/track-welcome-modal/{TrackWelcomeModalRHS.tsx => RHS/VideoRHS.tsx} (75%) create mode 100644 app/javascript/components/modals/track-welcome-modal/RHS/WhoIsThisTrackForRHS.tsx diff --git a/app/css/modals/track-welcome-modal.css b/app/css/modals/track-welcome-modal.css index 55d9bcc1db..3372592cad 100644 --- a/app/css/modals/track-welcome-modal.css +++ b/app/css/modals/track-welcome-modal.css @@ -22,7 +22,7 @@ @apply shadow-lgZ1 rounded-16; @apply overflow-hidden; width: 100%; - max-width: 1100px; + max-width: 1120px; } .lhs { diff --git a/app/helpers/react_components/modals/track_welcome_modal.rb b/app/helpers/react_components/modals/track_welcome_modal.rb index 278983f645..8055d7cb3f 100644 --- a/app/helpers/react_components/modals/track_welcome_modal.rb +++ b/app/helpers/react_components/modals/track_welcome_modal.rb @@ -20,9 +20,11 @@ def to_s cli_walkthrough: Exercism::Routes.cli_walkthrough_path, track_tooling: Exercism::Routes.track_doc_path(track, 'installation'), learning_resources: Exercism::Routes.track_doc_path(track, 'learning'), - download_cmd: Exercise.for(track.slug, 'hello-world').download_cmd + download_cmd: Exercise.for(track.slug, 'hello-world').download_cmd, + bootcamp_landing: Exercism::Routes.bootcamp_path }, - track: + track:, + user_seniority: current_user.seniority } ) end diff --git a/app/images/bootcamp/certificate.svg b/app/images/bootcamp/certificate.svg new file mode 100644 index 0000000000..a63a825666 --- /dev/null +++ b/app/images/bootcamp/certificate.svg @@ -0,0 +1 @@ +Document Certificate Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/app/images/bootcamp/complete.svg b/app/images/bootcamp/complete.svg new file mode 100644 index 0000000000..5b1401eb13 --- /dev/null +++ b/app/images/bootcamp/complete.svg @@ -0,0 +1 @@ +Task List Edit Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/app/javascript/components/modals/track-welcome-modal/LHS/BootcampRecommendationView.tsx b/app/javascript/components/modals/track-welcome-modal/LHS/BootcampRecommendationView.tsx new file mode 100644 index 0000000000..fba68ecbdf --- /dev/null +++ b/app/javascript/components/modals/track-welcome-modal/LHS/BootcampRecommendationView.tsx @@ -0,0 +1,86 @@ +import React, { useContext } from 'react' +import { TrackContext } from '../TrackWelcomeModal' +import { GraphicalIcon } from '@/components/common' + +export function BootcampRecommendationView() { + const { hideBootcampRecommendationView, links } = useContext(TrackContext) + return ( + <> +

+ Our Bootcamp might be better for you… +

+ +

+ Exercism's tracks are designed for people who{' '} + already know how to code and + are practicing or learning new languages. +

+

+ If you're just starting out on your coding journey,{' '} + + our Bootcamp might be a better fit for you. + {' '} + It offers: +

+ +

+ It's part time, remote, and priced affordably, with discounts available + for students, people who are unemployed, and those living in emerging + economies. +

+ +
+ + Check out the Bootcamp + + +
+ + ) +} diff --git a/app/javascript/components/modals/track-welcome-modal/LHS/TrackWelcomeModal.machine.ts b/app/javascript/components/modals/track-welcome-modal/LHS/TrackWelcomeModal.machine.ts index 890f41332c..bda4d09544 100644 --- a/app/javascript/components/modals/track-welcome-modal/LHS/TrackWelcomeModal.machine.ts +++ b/app/javascript/components/modals/track-welcome-modal/LHS/TrackWelcomeModal.machine.ts @@ -1,4 +1,4 @@ -import { assign, createMachine } from 'xstate' +import { createMachine } from 'xstate' export type StateEvent = | 'HAS_LEARNING_MODE' diff --git a/app/javascript/components/modals/track-welcome-modal/LHS/steps/HasLearningModeStep.tsx b/app/javascript/components/modals/track-welcome-modal/LHS/steps/HasLearningModeStep.tsx index 1aa6d13193..f1cfccc35a 100644 --- a/app/javascript/components/modals/track-welcome-modal/LHS/steps/HasLearningModeStep.tsx +++ b/app/javascript/components/modals/track-welcome-modal/LHS/steps/HasLearningModeStep.tsx @@ -12,9 +12,9 @@ export function HasLearningModeStep({ return ( <>

Here to learn or practice?

-

+

This track can be used for learning {track.title} (Learning Mode) or for - practicing your existing {track.title} knowledge (Practice Mode).{' '} + practicing your {track.title} skills (Practice Mode).{' '}

We recommend Learning Mode if you're new to {track.title}, and Practice diff --git a/app/javascript/components/modals/track-welcome-modal/LHS/steps/LearningEnvironmentSelectorStep.tsx b/app/javascript/components/modals/track-welcome-modal/LHS/steps/LearningEnvironmentSelectorStep.tsx index 504dbfba1f..b6635e5c8d 100644 --- a/app/javascript/components/modals/track-welcome-modal/LHS/steps/LearningEnvironmentSelectorStep.tsx +++ b/app/javascript/components/modals/track-welcome-modal/LHS/steps/LearningEnvironmentSelectorStep.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react' import { TrackContext } from '../../TrackWelcomeModal' import { StepButton } from './components/StepButton' -import { ButtonContainer } from './components/ButtonContainer' +import { BootcampRecommendationView } from '../BootcampRecommendationView' export function LearningEnvironmentSelectorStep({ onSelectLocalMachine, @@ -10,7 +10,13 @@ export function LearningEnvironmentSelectorStep({ 'onSelectLocalMachine' | 'onSelectOnlineEditor', () => void >): JSX.Element { - const { track } = useContext(TrackContext) + const { track, shouldShowBootcampRecommendationView } = + useContext(TrackContext) + + if (shouldShowBootcampRecommendationView) { + return + } + return ( <>

Online or on your computer?

diff --git a/app/javascript/components/modals/track-welcome-modal/RHS/TrackWelcomeModalRHS.tsx b/app/javascript/components/modals/track-welcome-modal/RHS/TrackWelcomeModalRHS.tsx new file mode 100644 index 0000000000..abba6b5ca1 --- /dev/null +++ b/app/javascript/components/modals/track-welcome-modal/RHS/TrackWelcomeModalRHS.tsx @@ -0,0 +1,18 @@ +import React, { useContext } from 'react' +import { TrackContext } from '../TrackWelcomeModal' +import { VideoRHS } from './VideoRHS' +import { WhoIsThisTrackForRHS } from './WhoIsThisTrackForRHS' + +export function TrackWelcomeModalRHS(): JSX.Element { + const { track, currentState, shouldShowBootcampRecommendationView } = + useContext(TrackContext) + + if ( + currentState.matches('learningEnvironmentSelector') && + shouldShowBootcampRecommendationView + ) { + return + } + + return +} diff --git a/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModalRHS.tsx b/app/javascript/components/modals/track-welcome-modal/RHS/VideoRHS.tsx similarity index 75% rename from app/javascript/components/modals/track-welcome-modal/TrackWelcomeModalRHS.tsx rename to app/javascript/components/modals/track-welcome-modal/RHS/VideoRHS.tsx index 072a16359e..91aaf1d0a1 100644 --- a/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModalRHS.tsx +++ b/app/javascript/components/modals/track-welcome-modal/RHS/VideoRHS.tsx @@ -1,9 +1,8 @@ -import React, { useContext } from 'react' +import React from 'react' import VimeoEmbed from '@/components/common/VimeoEmbed' -import { TrackContext } from './TrackWelcomeModal' +import { Track } from '@/components/types' -export function TrackWelcomeModalRHS(): JSX.Element { - const { track } = useContext(TrackContext) +export function VideoRHS({ track }: { track: Track }): JSX.Element { return (
diff --git a/app/javascript/components/modals/track-welcome-modal/RHS/WhoIsThisTrackForRHS.tsx b/app/javascript/components/modals/track-welcome-modal/RHS/WhoIsThisTrackForRHS.tsx new file mode 100644 index 0000000000..ff2ac3bf35 --- /dev/null +++ b/app/javascript/components/modals/track-welcome-modal/RHS/WhoIsThisTrackForRHS.tsx @@ -0,0 +1,34 @@ +import { GraphicalIcon, Icon } from '@/components/common' +import VimeoEmbed from '@/components/common/VimeoEmbed' +import { Track } from '@/components/types' +import React from 'react' + +export function WhoIsThisTrackForRHS({ track }: { track: Track }): JSX.Element { + return ( +
+
+
+ +
+ Exercism + Bootcamp +
+
+ + + + 🗓️ The Bootcamp starts in January.{' '} + + Check out our introduction video (☝️) to see how it will work and if + it's the right fit for you! + +
+
+ ) +} diff --git a/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.tsx b/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.tsx index 98ca5b9c38..7139e6c77a 100644 --- a/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.tsx +++ b/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.tsx @@ -2,7 +2,7 @@ import React, { createContext } from 'react' import { Track } from '@/components/types' import { Modal, ModalProps } from '../Modal' -import { TrackWelcomeModalRHS as RHS } from './TrackWelcomeModalRHS' +import { TrackWelcomeModalRHS as RHS } from './RHS/TrackWelcomeModalRHS' import { TrackWelcomeModalLHS as LHS } from './LHS/TrackWelcomeModalLHS' import { useTrackWelcomeModal } from './useTrackWelcomeModal' import { @@ -12,6 +12,7 @@ import { } from './TrackWelcomeModal.types' import { ErrorBoundary, ErrorMessage } from '@/components/ErrorBoundary' import { ErrorFallback } from '@/components/common/ErrorFallback' +import { SeniorityLevel } from '../welcome-modal/WelcomeModal' const DEFAULT_ERROR = new Error('Unable to dismiss modal') @@ -20,16 +21,23 @@ export const TrackContext = createContext<{ currentState: CurrentState send: any links: TrackWelcomeModalLinks + userSeniority: SeniorityLevel + shouldShowBootcampRecommendationView: boolean + hideBootcampRecommendationView: () => void }>({ track: {} as Track, currentState: {} as CurrentState, send: () => {}, links: {} as TrackWelcomeModalLinks, + userSeniority: '' as SeniorityLevel, + shouldShowBootcampRecommendationView: false, + hideBootcampRecommendationView: () => {}, }) export const TrackWelcomeModal = ({ links, track, + userSeniority, }: Omit & TrackWelcomeModalProps): JSX.Element => { const { @@ -37,7 +45,9 @@ export const TrackWelcomeModal = ({ currentState, send, error: modalDismissalError, - } = useTrackWelcomeModal(links) + shouldShowBootcampRecommendationView, + hideBootcampRecommendationView, + } = useTrackWelcomeModal(links, userSeniority) return ( null} className="m-track-welcome-modal" > - +
diff --git a/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.types.tsx b/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.types.tsx index 7eb57f8952..1e8e644d50 100644 --- a/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.types.tsx +++ b/app/javascript/components/modals/track-welcome-modal/TrackWelcomeModal.types.tsx @@ -2,10 +2,12 @@ import { State, ResolveTypegenMeta, BaseActionObject, ServiceMap } from 'xstate' import { StateEvent } from './LHS/TrackWelcomeModal.machine' import { Typegen0 } from './LHS/TrackWelcomeModal.machine.typegen' import { Track } from '@/components/types' +import { SeniorityLevel } from '../welcome-modal/WelcomeModal' export type TrackWelcomeModalProps = { track: Track links: TrackWelcomeModalLinks + userSeniority: SeniorityLevel } export type TrackWelcomeModalLinks = Record< @@ -15,7 +17,8 @@ export type TrackWelcomeModalLinks = Record< | 'editHelloWorld' | 'cliWalkthrough' | 'trackTooling' - | 'learningResources', + | 'learningResources' + | 'bootcampLanding', string > diff --git a/app/javascript/components/modals/track-welcome-modal/useTrackWelcomeModal.ts b/app/javascript/components/modals/track-welcome-modal/useTrackWelcomeModal.ts index 13fb898768..4a408916a9 100644 --- a/app/javascript/components/modals/track-welcome-modal/useTrackWelcomeModal.ts +++ b/app/javascript/components/modals/track-welcome-modal/useTrackWelcomeModal.ts @@ -1,18 +1,28 @@ import { redirectTo } from '@/utils' import { sendRequest } from '@/utils/send-request' import { useMachine } from '@xstate/react' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useMutation } from '@tanstack/react-query' import { machine } from './LHS/TrackWelcomeModal.machine' import { TrackWelcomeModalLinks } from './TrackWelcomeModal.types' +import { SeniorityLevel } from '../welcome-modal/WelcomeModal' -export function useTrackWelcomeModal(links: TrackWelcomeModalLinks) { +export function useTrackWelcomeModal( + links: TrackWelcomeModalLinks, + userSeniority: SeniorityLevel +) { const [open, setOpen] = useState(true) - const { - mutate: hideModal, - status, - error, - } = useMutation( + + const [ + shouldShowBootcampRecommendationView, + setShouldShowBootcampRecommendationView, + ] = useState(userSeniority.includes('beginner')) + + const hideBootcampRecommendationView = useCallback(() => { + setShouldShowBootcampRecommendationView(false) + }, []) + + const { mutate: hideModal, error } = useMutation( () => { const { fetch } = sendRequest({ endpoint: links.hideModal, @@ -67,5 +77,12 @@ export function useTrackWelcomeModal(links: TrackWelcomeModalLinks) { }, }) - return { open, currentState, send, error } + return { + open, + currentState, + send, + error, + shouldShowBootcampRecommendationView, + hideBootcampRecommendationView, + } } diff --git a/test/system/pages/tracks/track_welcome_modal_test.rb b/test/system/pages/tracks/track_welcome_modal_test.rb index d5bb658ad2..406e4211b3 100644 --- a/test/system/pages/tracks/track_welcome_modal_test.rb +++ b/test/system/pages/tracks/track_welcome_modal_test.rb @@ -21,7 +21,7 @@ class TrackWelcomeModalTest < ApplicationSystemTestCase visit track_path(@track) assert_text "Welcome to #{@track.title}!" - assert_text "#{@track.title} (Learning Mode) or for practicing your existing #{@track.title} knowledge (Practice Mode)." + assert_selector '[data-capy-element="welcome-modal-track-info"]' assert_text "Here to learn or practice?" assert_text "Learning Mode" assert_text "Practice Mode" @@ -91,6 +91,56 @@ class TrackWelcomeModalTest < ApplicationSystemTestCase end end + test "user sees bootcamp recommendation page if beginner" do + use_capybara_host do + @user.reload + @user.update!(seniority: :beginner) + + sign_in!(@user) + visit track_path(@track) + + assert_text "Here to learn or practice?" + click_on "Learning Mode" + assert_selector '[data-capy-element="bootcamp-recommendation-header"]' + # assert if rhs is rendered correctly + assert_selector '[data-capy-element="who-is-this-track-for-rhs"]' + end + end + + test "user can go to bootcamp landing" do + use_capybara_host do + @user.reload + @user.update!(seniority: :beginner) + + sign_in!(@user) + visit track_path(@track) + + assert_text "Here to learn or practice?" + click_on "Practice Mode" + assert_selector '[data-capy-element="bootcamp-recommendation-header"]' + find(:css, '[data-capy-element="go-to-bootcamp-button"]').click + assert_current_path Exercism::Routes.bootcamp_path + end + end + + test "user can dismiss bootcamp recommendation" do + use_capybara_host do + @user.reload + @user.update!(seniority: :beginner) + + sign_in!(@user) + visit track_path(@track) + + assert_text "Here to learn or practice?" + click_on "Practice Mode" + assert_selector '[data-capy-element="bootcamp-recommendation-header"]' + find(:css, '[data-capy-element="continue-anyway-button"]').click + refute_selector '[data-capy-element="bootcamp-recommendation-header"]' + assert_text "Online or on your computer?" + refute_selector '[data-capy-element="who-is-this-track-for-rhs"]' + end + end + test "pages contain the correct links" do @track.update(course: false) use_capybara_host do