Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add seniority survey modal #7105

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/helpers/react_components/modals/seniority_survey_modal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module ReactComponents
module Modals
class SenioritySurveyModal < ReactComponent
def to_s
# return if current_user.introducer_dismissed?(slug)

super(
"modals-seniority-survey-modal",
{
links: {
hide_modal_endpoint: Exercism::Routes.hide_api_settings_introducer_path(slug),
api_user_endpoint: Exercism::Routes.api_user_url
}
}
)
end

private
def slug
"seniority-survey-modal"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useContext } from 'react'
import { Icon } from '@/components/common'
import { FormButton } from '@/components/common/FormButton'
import { ErrorBoundary, ErrorMessage } from '@/components/ErrorBoundary'
import { SenioritySurveyModalContext } from './SenioritySurveyModal'

const DEFAULT_ERROR = new Error('Unable to dismiss modal')

export function BootcampAdvertismentView() {
const { patchCloseModal } = useContext(SenioritySurveyModalContext)
return (
<>
<div className="lhs">
<header>
<h1>Oooh, good timing! 👏</h1>

<p className="mb-8">
You've signed up at a great time! In January, we're running a
one-off{' '}
<strong className="font-semibold text-softEmphasis">
part-time, remote bootcamp for beginners!
</strong>
</p>
<p className="mb-8">
We're going to teach you all the fundamentals with a hands-on
project-based approach. No stuffy videos, no heavy theory, just lots
of fun coding!
</p>
<p className="mb-8">
It's super affordable, with discounts if you're a student,
unemployed, or live in a country with an emerging economy.
</p>
<p className="mb-20">Watch our intro video to learn more 👉</p>
</header>
<div className="flex gap-8">
<a
href="https://bootcamp.exercism.org"
className="btn-primary btn-l cursor-pointer"
>
Go to the Bootcamp ✨
</a>

<FormButton
status={patchCloseModal.status}
className="btn-secondary btn-l"
type="button"
onClick={patchCloseModal.mutate}
>
Skip &amp; Close
</FormButton>
</div>
<ErrorBoundary resetKeys={[patchCloseModal.status]}>
<ErrorMessage
error={patchCloseModal.error}
defaultError={DEFAULT_ERROR}
/>
</ErrorBoundary>
</div>
<div className="rhs pt-72">
<div className="flex flex-row gap-8 items-center justify-center text-16 text-textColor1 mb-16">
<Icon
icon="exercism-face"
className="filter-textColor1"
alt="exercism-face"
height={16}
width={16}
/>
<div>
<strong className="font-semibold"> Exercism </strong>
Bootcamp
</div>
</div>
<div
className="video relative rounded-8 overflow-hidden !mb-16"
style={{ padding: '56.25% 0 0 0', position: 'relative' }}
>
<iframe
src="https://player.vimeo.com/video/1024390839?h=c2b3bdce14&amp;badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479"
title="Introducing the Exercism Bootcamp"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
<div className="bubbles">
<div className="bubble">
<Icon category="bootcamp" alt="wave-icon" icon="wave" />
<div className="text">
<strong>Live</strong> teaching
</div>
</div>
<div className="bubble">
<Icon category="bootcamp" alt="fun-icon" icon="fun" />
<div className="text">
<strong>Fun</strong> projects
</div>
</div>
<div className="bubble">
<Icon category="bootcamp" alt="price-icon" icon="price" />
<div className="text">
Priced <strong>fairly</strong>{' '}
</div>
</div>
</div>
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useContext, useState, useCallback } from 'react'
import { useMutation } from '@tanstack/react-query'
import { sendRequest } from '@/utils/send-request'
import { assembleClassNames } from '@/utils/assemble-classnames'
import { FormButton } from '@/components/common/FormButton'
import { ErrorBoundary, ErrorMessage } from '@/components/ErrorBoundary'
import { SenioritySurveyModalContext } from './SenioritySurveyModal'
import type { SeniorityLevel } from '../welcome-modal/WelcomeModal'
import { ErrorFallback } from '@/components/common/ErrorFallback'

const DEFAULT_ERROR = new Error('Unable to save seniority level.')

const SENIORITIES: { label: string; value: SeniorityLevel }[] = [
{
label: 'Absolute Beginner',
value: 'absolute_beginner',
},
{
label: 'Beginner',
value: 'beginner',
},
{
label: 'Junior Developer',
value: 'junior',
},
{
label: 'Mid-level Developer',
value: 'mid',
},
{
label: 'Senior Developer',
value: 'senior',
},
]

export function InitialView() {
const { links, setCurrentView } = useContext(SenioritySurveyModalContext)
const [selected, setSelected] = useState<SeniorityLevel | ''>('')

const {
mutate: setSeniorityMutation,
status: setSeniorityMutationStatus,
error: setSeniorityMutationError,
} = useMutation(
(seniority: SeniorityLevel) => {
const { fetch } = sendRequest({
endpoint: links.apiUserEndpoint + `?user[seniority]=${seniority}`,
method: 'PATCH',
body: null,
})

return fetch
},
{
onSuccess: () => {
if (selected.includes('beginner')) {
setCurrentView('bootcamp-advertisment')
return
}
setCurrentView('thanks')
},
}
)

const handleSaveSeniorityLevel = useCallback(() => {
if (selected === '') return
setSeniorityMutation(selected)
}, [selected, setSeniorityMutation])

return (
<div className="lhs">
<header>
<h1>Hey there 👋</h1>
<p className="mb-16">
As Exercism grows, certain features are becoming more relevant than
others based on your experience coding. So we're starting to filter
what we show by your seniority.
</p>
<h2>How experienced a developer are you?</h2>
</header>
<div className="flex flex-col flex-wrap gap-8 mb-16 text-18">
{SENIORITIES.map((seniority) => (
<button
key={seniority.value}
className={assembleClassNames(
'btn-m btn-enhanced',
selected === seniority.value
? 'border-prominentLinkColor text-prominentLinkColor'
: 'border-borderColor1'
)}
onClick={() => setSelected(seniority.value)}
>
{seniority.label}
</button>
))}
</div>

<p className="!text-14 text-center mb-20">
(This can be updated at any time in your settings)
</p>
<FormButton
status={setSeniorityMutationStatus}
disabled={selected === ''}
className="btn-primary btn-l"
type="button"
onClick={handleSaveSeniorityLevel}
>
Save my choice
</FormButton>
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[setSeniorityMutationStatus]}
>
<ErrorMessage
error={setSeniorityMutationError}
defaultError={DEFAULT_ERROR}
/>
</ErrorBoundary>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, {
createContext,
Dispatch,
SetStateAction,
useContext,
useState,
} from 'react'
import { Modal, ModalProps } from '../Modal'
import { ThanksView } from './ThanksView'
import { InitialView } from './InitialView'
import { useMutation } from '@tanstack/react-query'
import { sendRequest } from '@/utils/send-request'
import { BootcampAdvertismentView } from './BootcampAdvertismentView'

type ViewVariant = 'initial' | 'thanks' | 'bootcamp-advertisment'

type Links = { hideModalEndpoint: string; apiUserEndpoint: string }

type SenioritySurveyModalContextProps = {
currentView: ViewVariant
setCurrentView: Dispatch<SetStateAction<ViewVariant>>
setOpen: Dispatch<SetStateAction<boolean>>
links: Links
patchCloseModal: {
mutate: () => void
} & Pick<ReturnType<typeof useMutation>, 'status' | 'error'>
}

const DEFAULT_VIEW = 'initial'

export const SenioritySurveyModalContext =
createContext<SenioritySurveyModalContextProps>({
currentView: DEFAULT_VIEW,
setCurrentView: () => {},
setOpen: () => {},
links: { apiUserEndpoint: '', hideModalEndpoint: '' },
patchCloseModal: {
mutate: () => null,
status: 'idle',
error: null,
},
})

export default function SenioritySurveyModal({
links,
...props
}: Omit<ModalProps, 'className' | 'open' | 'onClose'> & {
links: Links
}): JSX.Element {
const [open, setOpen] = useState(true)
const [currentView, setCurrentView] = useState<ViewVariant>(DEFAULT_VIEW)

const {
mutate: hideModalMutation,
status: hideModalMutationStatus,
error: hideModalMutationError,
} = useMutation(
() => {
const { fetch } = sendRequest({
endpoint: links.hideModalEndpoint,
method: 'PATCH',
body: null,
})

return fetch
},
{
onSuccess: () => {
setOpen(false)
},
}
)

return (
<SenioritySurveyModalContext.Provider
value={{
currentView,
setCurrentView,
setOpen,
links,
patchCloseModal: {
mutate: hideModalMutation,
status: hideModalMutationStatus,
error: hideModalMutationError,
},
}}
>
<Modal
cover={true}
open={open}
{...props}
style={{
content: {
maxWidth: currentView === 'bootcamp-advertisment' ? '' : '620px',
placeSelf: 'center',
},
}}
onClose={() => null}
className="m-welcome"
>
<Inner />
</Modal>
</SenioritySurveyModalContext.Provider>
)
}

function Inner() {
const { currentView } = useContext(SenioritySurveyModalContext)
switch (currentView) {
case 'initial':
return <InitialView />
case 'bootcamp-advertisment':
return <BootcampAdvertismentView />
case 'thanks':
return <ThanksView />
}
}
Loading
Loading