Skip to content

Commit

Permalink
Handle bonus tasks (#7463)
Browse files Browse the repository at this point in the history
* Remove outdated comment

* Sort out 1 bonus task

* Add different mazes

* Add bonus task field

* Update config

* Change fn name

* Group bonus tasks separately

* Format modal, pass down bonus tasks

* Add bonus class to bonus buttons

* Use `meta` instead of `getters`

* Change fn name

* Remove dead code

* Tweaks

* Add bonus status

* Send pass_bonus status

* Hide before basic tasks are compelted

* Don't show bonus tasks in the preview

* Handle showing logic inside taskStore

* Only show tasks after pressing button on modal

* Only change editor code to code stored on server if it is newer by at least a minute

* Add comment

* Remove console.logs

* Add tests, show bonus scenarios immediately when acing them in one go

* Break out modal views into separate files

* Add new modal for bonus tasks

* Handle showing bonus modal

* Adjust behaviour of bonus tasks modal

* Fix error

* Tweak states

* Handle saving bonus tasks in localStorage (#7490)

* Handle saving bonus tasks in localStorage

* Get rid of modal data inside exercise data

* Simplify

* Get rid of actual

* Sort out showing instructions (#7492)

* Sort out showing instructions

* Remove logging

* Use task instructionsHtml

* Fix migration production check

* Fix bad rebase

* Fix a bug, add tests

* Add fixed Bonus Challenges header

* Improve logic around showing bonus tasks

* Bonus FE tweaks (#7513)

* Select first failing bonus test if other tests are passing

* Merge TestResultsButtons components, manage pausing timeline, only play animation if all frames are SUCCESS

* Memo testResults in buttons

* Fix anime timeline not resetting

* Undo memoing results

* Fix screen darkening

* Fix migration in prod test failure

* WIP bonus improvements (#7499)

* WIP bonus improvemnets

* Get odd/even passing

* Fix

* Don't diff if it's passing

* Add multiple expects

* Make sure bonus modal is only shown if everything passes

---------

Co-authored-by: dem4ron <[email protected]>

* Fix CSS

* Improve checkers

* WIP

* Add task to alien detector

* Add space invaders checks

* Add task for leap

* Further improvements

* Tweak more

* WIP

---------

Co-authored-by: Jeremy Walker <[email protected]>
  • Loading branch information
dem4ron and iHiD authored Feb 19, 2025
1 parent 9029ea8 commit 306bb3b
Show file tree
Hide file tree
Showing 101 changed files with 1,846 additions and 519 deletions.
2 changes: 2 additions & 0 deletions app/commands/bootcamp/solution/complete.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class Bootcamp::Solution::Complete
initialize_with :solution

def call
return if solution.completed?

# It's essential that both of these lines are called
# inline to ensure next exercise selection is correct
solution.update!(completed_at: Time.current)
Expand Down
17 changes: 15 additions & 2 deletions app/commands/bootcamp/submission/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ class Bootcamp::Submission::Create

def call
create_submission.tap do |submission|
solution.update(code:)
fire_events!(submission)
update_solution!(submission)
end
end

Expand All @@ -20,6 +19,20 @@ def create_submission
)
end

def update_solution!(submission)
solution.update(code:)

case submission.status
when :pass
solution.update!(passed_basic_tests: true)
when :pass_bonus
solution.update!(
passed_basic_tests: true,
passed_bonus_tests: true
)
end
end

def fire_events!(_submission)
nil # TODO: Implement this
# if submission.passed?
Expand Down
18 changes: 9 additions & 9 deletions app/css/bootcamp/components/scenario.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@
.scenario-lhs {
@apply bg-bootcamp-fail-light;
}

span.added-part {
@apply bg-bootcamp-success-light;
@apply border-b-1 border-bootcamp-success-dark;
}
span.removed-part {
@apply bg-bootcamp-fail-light;
@apply border-b-1 border-bootcamp-fail-dark;
}
}
.io-test-result-info {
border-spacing: 6px;
Expand All @@ -100,15 +109,6 @@
td {
@apply rounded-r-5;
@apply font-mono text-15;

span.added-part {
@apply bg-bootcamp-success-light;
@apply border-b-1 border-bootcamp-success-dark;
}
span.removed-part {
@apply bg-bootcamp-fail-light;
@apply border-b-1 border-bootcamp-fail-dark;
}
}
}
}
Expand Down
19 changes: 18 additions & 1 deletion app/css/bootcamp/components/test-buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,27 @@
.test-button {
@apply flex-shrink-0;
}

&.bonus {
&:before {
content: "⋮";
vertical-align: middle;
text-align: center;
font-size: 30px;
color: rgba(112, 41, 245, 0.3);
line-height: 40px;
@apply mr-2;
}

.test-button {
letter-spacing: 2px;
}
}
}
.test-button {
@apply relative p-4 w-[40px] h-[40px] grid place-content-center;
transition: background-color 0.3s ease-out;
@apply font-medium;

@apply border-2 rounded-3;
@apply bg-no-repeat;
Expand All @@ -30,7 +47,7 @@
@apply text-bootcamp-success-dark;
background-image: url("icons/bootcamp-tick-green.svg");
background-size: 12px;
background-position: 23px 2px;
background-position: 24px 0px;
&.selected {
@apply border-bootcamp-success-dark;
}
Expand Down
7 changes: 6 additions & 1 deletion app/css/bootcamp/pages/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ body.namespace-bootcamp.controller-dashboard.action-index {
}
.level-content {
p,
ul {
ul,
ol {
@apply text-20 leading-150;
@apply mb-8;
}
Expand All @@ -41,6 +42,10 @@ body.namespace-bootcamp.controller-dashboard.action-index {
@apply list-disc;
@apply ml-24;
}
ol {
@apply list-decimal;
@apply ml-24;
}
.tag {
@apply flex items-center;
@apply border-1 rounded-100;
Expand Down
4 changes: 3 additions & 1 deletion app/helpers/react_components/bootcamp/solve_exercise_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def data
},
solution: {
uuid: solution.uuid,
status: solution.status
status: solution.status,
passed_basic_tests: solution.passed_basic_tests?,
passed_bonus_tests: solution.passed_bonus_tests?
},
test_results: submission&.test_results,
code: {
Expand Down
7 changes: 5 additions & 2 deletions app/helpers/view_components/bootcamp/exercise_widget.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ def to_s

private
def status
user_project&.exercise_status(exercise, solution) ||
(::Bootcamp::Exercise::AvailableForUser.(exercise, current_user) ? :available : :locked)
s = user_project&.exercise_status(exercise, solution) ||
(::Bootcamp::Exercise::AvailableForUser.(exercise, current_user) ? :available : :locked)

s = 'completed-bonus' if s == :completed && (solution.passed_bonus_tests? || !exercise.has_bonus_tasks?)
s
end

memoize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export const CodeMirror = forwardRef(function _CodeMirror(
const { setExerciseLocalStorageData } = useContext(SolveExercisePageContext)

const { setHasUnhandledError, setUnhandledErrorBase64 } = useErrorStore()
const { wasFinishLessonModalShown } = useTaskStore()

const [textarea, setTextarea] = useState<HTMLDivElement | null>(null)

Expand All @@ -115,14 +114,9 @@ export const CodeMirror = forwardRef(function _CodeMirror(
code: value,
storedAt: new Date().toISOString(),
readonlyRanges: readonlyRanges,
wasFinishLessonModalShown,
})
}, 500)
}, [
setExerciseLocalStorageData,
readOnlyRangesStateField,
wasFinishLessonModalShown,
])
}, [setExerciseLocalStorageData, readOnlyRangesStateField])

let value = defaultCode

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export function useEditorHandler({
const { setDefaultCode } = useEditorStore()
const { setHasUnhandledError, setUnhandledErrorBase64 } = useErrorStore()

const { setWasFinishLessonModalShown } = useTaskStore()

const [latestValueSnapshot, setLatestValueSnapshot] = useState<
string | undefined
>(undefined)
Expand All @@ -40,22 +38,21 @@ export function useEditorHandler({
exerciseLocalStorageData.storedAt &&
code.storedAt &&
// if the code on the server is newer than in localstorage, update the storage and load the code from the server
exerciseLocalStorageData.storedAt < code.storedAt
// ---
// code on the server must be newer by at least a minute
new Date(exerciseLocalStorageData.storedAt).getTime() <
new Date(code.storedAt).getTime() - 60000
) {
setExerciseLocalStorageData({
code: code.code,
storedAt: code.storedAt,
readonlyRanges: code.readonlyRanges,
wasFinishLessonModalShown: false,
})
setDefaultCode(code.code)
setupEditor(editorViewRef.current, code)
} else {
// otherwise we are using the code from the storage
setDefaultCode(exerciseLocalStorageData.code)
setWasFinishLessonModalShown(
!!exerciseLocalStorageData.wasFinishLessonModalShown
)
setupEditor(editorViewRef.current, exerciseLocalStorageData)
}

Expand All @@ -75,7 +72,6 @@ export function useEditorHandler({
code: code.stub,
storedAt: new Date().toISOString(),
readonlyRanges: code.readonlyRanges,
wasFinishLessonModalShown: false,
})
setupEditor(editorViewRef.current, { code: '', readonlyRanges: [] })
setupEditor(editorViewRef.current, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function _ControlButtons({ handleRunCode }: { handleRunCode: () => void }) {
<PreviewTestButtons />
{/* Just ran the tests */}
<TestResultsButtons />
<TestResultsButtons isBonus />
</div>
)
}
Expand All @@ -34,7 +35,7 @@ function PreviewTestButtons() {
if (testSuiteResult) return null

return (
<div className="test-selector-buttons ">
<div className="test-selector-buttons">
{flatPreviewTaskTests.map((taskTest, testIdx) => (
<button
data-ci="preview-scenario-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { SolveExercisePageContext } from '../SolveExercisePageContextWrapper'

import { GraphicalIcon } from '@/components/common/GraphicalIcon'
import { ResetButton } from './ResetButton'
import { CompletedBonusTasksModal } from '../../modals/CompletedBonusTasksModal/CompletedBonusTasksModal'

function _Header() {
const { areAllTasksCompleted } = useTaskStore()

const { solution, links } = useContext(SolveExercisePageContext)

const {
Expand All @@ -25,6 +25,8 @@ function _Header() {
completedLevelIdx,
nextLevelIdx,
hasRuntimeErrors,
isCompletedBonusTasksModalOpen,
setIsCompletedBonusTasksModalOpen,
} = useTasks()

return (
Expand All @@ -39,32 +41,35 @@ function _Header() {
<ResetButton />

{solution.status === 'in_progress' && (
<button
onClick={handleCompleteSolution}
disabled={!areAllTasksCompleted || hasRuntimeErrors}
className={assembleClassNames(
'btn-primary btn-xxs',
areAllTasksCompleted ? '' : 'disabled cursor-not-allowed'
)}
>
Complete Exercise
</button>
)}
{areAllTasksCompleted && (
<>
<button
onClick={handleCompleteSolution}
disabled={!areAllTasksCompleted || hasRuntimeErrors}
className={assembleClassNames(
'btn-primary btn-xxs',
areAllTasksCompleted ? '' : 'disabled cursor-not-allowed'
)}
<FinishLessonModalContextWrapper
value={{
isFinishLessonModalOpen: isFinishModalOpen,
setIsFinishLessonModalOpen: setIsFinishModalOpen,
isCompletedBonusTasksModalOpen,
setIsCompletedBonusTasksModalOpen,
completedLevelIdx,
nextLevelIdx,
handleCompleteSolution,
modalView,
nextExerciseData,
}}
>
Complete Exercise
</button>
{areAllTasksCompleted && (
<FinishLessonModalContextWrapper
value={{
isOpen: isFinishModalOpen,
completedLevelIdx,
nextLevelIdx,
setIsOpen: setIsFinishModalOpen,
handleCompleteSolution,
modalView,
nextExerciseData,
}}
>
<FinishLessonModal />
</FinishLessonModalContextWrapper>
)}
<FinishLessonModal />
<CompletedBonusTasksModal />
</FinishLessonModalContextWrapper>
</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ export function ResetButton() {
useState(false)
const { resetEditorToStub, exercise } = useContext(SolveExercisePageContext)
const { setTestSuiteResult, setInspectedTestResult } = useTestStore()
const { initializeTasks } = useTaskStore()
const { initializeTasks, setShouldShowBonusTasks } = useTaskStore()

const handleResetExercise = useCallback(() => {
resetEditorToStub()
setShouldOpenConfirmationModal(false)
setTestSuiteResult(null)
setInspectedTestResult(null)
setShouldShowBonusTasks(false)
initializeTasks(exercise.tasks, null)
}, [resetEditorToStub, setShouldOpenConfirmationModal])

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react'
import React, { useContext, useEffect, useMemo, useRef } from 'react'
import { wrapWithErrorBoundary } from '@/components/bootcamp/common/ErrorBoundary/wrapWithErrorBoundary'
import useTaskStore from '../store/taskStore/taskStore'
import { useEffect, useMemo, useRef } from 'react'
import Typewriter from 'typewriter-effect/dist/core'
import { type Options } from 'typewriter-effect'
import useTestStore from '../store/testStore'
import { SolveExercisePageContext } from '../SolveExercisePageContextWrapper'

export function _Instructions({
exerciseTitle,
Expand All @@ -12,7 +13,16 @@ export function _Instructions({
exerciseTitle: string
exerciseInstructions: string
}): JSX.Element {
const { activeTaskIndex, tasks, areAllTasksCompleted } = useTaskStore()
const {
activeTaskIndex,
tasks,
areAllTasksCompleted,
bonusTasks,
shouldShowBonusTasks,
} = useTaskStore()
const { remainingBonusTasksCount } = useTestStore()

const { solution } = useContext(SolveExercisePageContext)

const typewriterRef = useRef<HTMLDivElement>(null)
const isFirstRender = useRef(true)
Expand All @@ -24,6 +34,11 @@ export function _Instructions({
[activeTaskIndex, tasks]
)

const bonusTasksInstructions: string = useMemo(() => {
if (!bonusTasks) return ''
return bonusTasks.map((task) => task.instructionsHtml).join('')
}, [bonusTasks])

useEffect(() => {
if (!typewriterRef.current || !currentTask) return

Expand Down Expand Up @@ -62,7 +77,14 @@ export function _Instructions({
}}
/>

{areAllTasksCompleted ? (
{shouldShowBonusTasks &&
remainingBonusTasksCount > 0 &&
!solution.passedBonusTests ? (
<>
<h4>Bonus Challenges</h4>
<div dangerouslySetInnerHTML={{ __html: bonusTasksInstructions }} />
</>
) : areAllTasksCompleted || solution.passedBasicTests ? (
<>
<h4 className="mt-12">Congratulations!</h4>
<p>You have successfully completed all the tasks!</p>
Expand Down
Loading

0 comments on commit 306bb3b

Please sign in to comment.