Skip to content

Commit

Permalink
Level 4 exercises (#7382)
Browse files Browse the repository at this point in the history
* Level 4 exercises

* Fix syntax error

* Add raindrops

* Add leap

* Improve leap

* Fix OperandsMustBeNumbers (#7379)

* Fix OperandsMustBeNumbers

* Add error messages around functions being used as variables

* Add more error messages

* Green

* Fix typo

* Make deployable

* Allow pasting and deleting multiple lines when there are readonly ranges (#7383)

* WIP

* WIP

* Add triangle

* Improve tests

* WIP

* Concepts

* WIP functions

* Some sanity

* Fix more things

* Fix incorrect tests type

* Add extra maze execise

* Show boolean expects (#7393)

* Show Passing IO cases (#7392)

* Show passing cases

* Remove unused import

* Don't show errorHtml if tests are passing

* Show correct cursor position (#7394)

* Show correct cursor position

* Target only the wrapper p

* Fix error displays

* Further improvements

* Show preview view on resetting code (#7395)

* Update viewcontainer to be hidden if there's no illustration

* Get rid of fallback

* Mount image inside hook, dont show image if there is none (#7396)

* Fix things

* Add max repeat until game over iterations

* Readd poo

* Disable while in collatz

* Fix diffs

* Improvements

* Improve locking logic

* Improve further

* tweak collatz

* Green?

* Add favicon

* Add icons

* Add concepts

* Add tests for expected function errors

* More green?

* Improve title

---------

Co-authored-by: Aron Demeter <[email protected]>
  • Loading branch information
iHiD and dem4ron authored Jan 28, 2025
1 parent bdfaf11 commit 0ee26c0
Show file tree
Hide file tree
Showing 128 changed files with 2,949 additions and 879 deletions.
2 changes: 2 additions & 0 deletions app/commands/bootcamp/exercise/available_for_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ def call
# If the exercise is gloabally locked, it's locked
return false if exercise.locked?

return false if user.bootcamp_data.level_idx < exercise.level_idx

# Otherwise the previous solution must be completed
previous_exercises_completed?
end
Expand Down
11 changes: 9 additions & 2 deletions app/css/bootcamp/components/codemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@
.cm-icon-container-gutter .cm-gutterElement {
@apply px-4;
}
.cm-lockedGutter {
.cm-lock-marker {
background-image: url("bootcamp/readonly-lock.svg");
background-size: 80%;
@apply bg-no-repeat bg-center;
@apply opacity-[0.4];
}
}
.cm-line {
padding: 2px 2px 2px 6px;
}
.cm-lockedLine,
.cm-lockedGutter {
opacity: 0.6;
background: #eee;
background: #f6f6f6;
}
.ͼ1 .cm-underline {
text-decoration: none;
Expand Down
6 changes: 6 additions & 0 deletions app/css/bootcamp/components/scenario.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,11 @@
@apply py-16 px-20;
h3 {
}

.Typewriter__wrapper {
p {
display: inline;
}
}
}
}
15 changes: 14 additions & 1 deletion app/css/bootcamp/exercises/maze.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
background: white;
position: relative;
}

.io-image {
@apply absolute inset-[10px];
@apply bg-cover bg-no-repeat;
@apply rounded-5 border-1 border-borderColor8;
}
}

.exercise-maze {
Expand Down Expand Up @@ -97,13 +103,20 @@
);
background-blend-mode: multiply;
}
&.bomb {
&.fire {
@apply grid place-items-center;
&:before {
content: "🔥";
font-size: 140%;
}
}
&.poop {
@apply grid place-items-center;
&:before {
content: "💩";
font-size: 160%;
}
}
}

.character {
Expand Down
5 changes: 4 additions & 1 deletion app/helpers/react_components/bootcamp/solve_exercise_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ def data
exercise: {
id: exercise.id,
slug: exercise.slug,
title: exercise.title,
introduction_html: exercise.introduction_html,
tasks: exercise.tasks,
config: {
title: exercise.config[:title],
description: exercise.config[:description],
project_type: exercise.config[:project_type],
tests_type: exercise.config[:tests_type],
interpreter_options: exercise.config[:interpreter_options]
interpreter_options: exercise.config[:interpreter_options],
stdlib_functions: exercise.config[:stdlib_functions],
exercise_functions: exercise.config[:exercise_functions]
},
test_results: submission&.test_results
},
Expand Down
1 change: 1 addition & 0 deletions app/images/bootcamp/readonly-lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ export const CodeMirror = forwardRef(function _CodeMirror(
try {
editorDidMount({ setValue, getValue, focus: view.focus.bind(view) })
} catch (e: unknown) {
if (
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test'
) {
throw e
}

setHasUnhandledError(true)
setUnhandledErrorBase64(
JSON.stringify({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const colorScheme = createTheme({
styles: [
{
tag: t.comment,
color: '#B1BBC4',
color: '#818B94',
fontStyle: 'italic',
},
{
tag: [t.variableName, t.propertyName, t.attributeName, t.regexp],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ const baseTheme = EditorView.baseTheme({
class LockMarker extends GutterMarker {
toDOM() {
const lockContainer = document.createElement('div')
lockContainer.classList.add('cm-lock-marker')
Object.assign(lockContainer.style, {
height: '16px',
width: '16px',
padding: '2px',
display: 'grid',
placeContent: 'center',
})
lockContainer.innerHTML = `🔒`
return lockContainer
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ export function useEditorHandler({
try {
runCode(value, editorViewRef.current)
} catch (e: unknown) {
console.log(e)
if (
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test'
) {
throw e
}

setHasUnhandledError(true)
setUnhandledErrorBase64(
JSON.stringify({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import Typewriter from 'typewriter-effect/dist/core'
import { type Options } from 'typewriter-effect'

export function _Instructions({
exerciseTitle,
exerciseInstructions,
}: {
exerciseTitle: string
exerciseInstructions: string
}): JSX.Element {
const { activeTaskIndex, tasks, areAllTasksCompleted } = useTaskStore()
Expand Down Expand Up @@ -49,7 +51,7 @@ export function _Instructions({

return (
<div className="scenario-rhs c-prose c-prose-small">
<h3>Instructions</h3>
<h3>{exerciseTitle}</h3>

<div
dangerouslySetInnerHTML={{
Expand All @@ -68,7 +70,6 @@ export function _Instructions({
Task {activeTaskIndex + 1}: {currentTask?.name}
</h4>
{/* "inline" is required to keep the cursor on the same line */}
{/*<div className="[&_p]:inline" ref={typewriterRef} />*/}
<div ref={typewriterRef} />
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UnhandledErrorView } from './TestResultsView/UnhandledErrorView'
import useErrorStore from './store/errorStore'

export function ResultsPanel() {
const { hasSyntaxError } = useTestStore()
const { hasSyntaxError, testSuiteResult } = useTestStore()
const { hasUnhandledError } = useErrorStore()

if (hasUnhandledError) {
Expand All @@ -18,10 +18,5 @@ export function ResultsPanel() {
return <SyntaxErrorView />
}

return (
<>
<InspectedTestResultView />
<TaskPreview />
</>
)
return testSuiteResult ? <InspectedTestResultView /> : <TaskPreview />
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ export default function SolveExercisePage({
<Resizer direction="vertical" handleMouseDown={handleMouseDown} />
{/* RHS */}
<div className="page-body-rhs" style={{ width: RHSWidth }}>
<Instructions exerciseInstructions={exercise.introductionHtml} />
<Instructions
exerciseTitle={exercise.title}
exerciseInstructions={exercise.introductionHtml}
/>
{/* <Tasks /> */}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import { CodeRun } from '../TestResultsView/CodeRun'
import { generateCodeRunString } from '../hooks/useSetupStores'
import { formatLiteral } from '@/interpreter/helpers'
export function IOPreview({
inspectedPreviewTaskTest,
}: {
Expand All @@ -23,7 +24,7 @@ export function IOPreview({
/>
<tr>
<th>Expected:</th>
<td>{inspectedPreviewTaskTest.expected}</td>
<td>{formatLiteral(inspectedPreviewTaskTest.expected)}</td>
</tr>
</tbody>
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ export function StatePreview({
<strong>Scenario: </strong>
{inspectedPreviewTaskTest.name}
</h3>
<div className="description text-bootcamp-purple">
<strong>Task: </strong>
<span
dangerouslySetInnerHTML={{
__html: inspectedPreviewTaskTest.descriptionHtml ?? '',
}}
/>
</div>

{inspectedPreviewTaskTest.descriptionHtml &&
inspectedPreviewTaskTest.descriptionHtml.length > 0 && (
<div className="description text-bootcamp-purple">
<strong>Task: </strong>
<span
dangerouslySetInnerHTML={{
__html: inspectedPreviewTaskTest.descriptionHtml ?? '',
}}
/>
</div>
)}
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export function _TaskPreview() {
const viewContainerRef = useMountViewOrImage({
config: exercise.config,
taskTest: inspectedPreviewTaskTest,
testSuiteResult,
inspectedPreviewTaskTest,
})

if (testSuiteResult) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
import React from 'react'
import { useRef, useEffect, useMemo } from 'react'
import { getAndInitializeExerciseClass } from '../utils/exerciseMap'
import { useRef, useEffect } from 'react'
import projectsCache from '../utils/exerciseMap'
import { Exercise } from '../exercises/Exercise'

export function useMountViewOrImage({
config,
taskTest,
result,
testSuiteResult,
inspectedPreviewTaskTest,
}: {
config: Config
taskTest: TaskTest
result?: PreviousTestResult
testSuiteResult: TestSuiteResult<any> | null
inspectedPreviewTaskTest: TaskTest | null
}) {
if (!taskTest) return
const exercise = useMemo(
() => getAndInitializeExerciseClass(config),
[config]
)

;(taskTest.setupFunctions || []).forEach((functionData) => {
let [functionName, params] = functionData
if (!params) {
params = []
}
if (!exercise) return
exercise[functionName](...params)
})

const viewContainerRef = useRef<HTMLDivElement | null>(null)

useEffect(() => {
if (!viewContainerRef.current) return
if (exercise && exercise.getView()) {
const view = exercise.getView()
if (!viewContainerRef.current) {
return
}
const Project = projectsCache.get(config.projectType)
if (Project) {
const exercise: Exercise = new Project()

if (viewContainerRef.current.children.length > 0) {
const oldView = viewContainerRef.current.children[0] as HTMLElement
document.body.appendChild(oldView)
oldView.style.display = 'none'
}
const setupFns = taskTest.setupFunctions || []

// on each result change, clear out view-container
viewContainerRef.current.innerHTML = ''
viewContainerRef.current.appendChild(view)
view.style.display = 'block'
setupFns.forEach((functionData) => {
let [functionName, params] = functionData
if (!params) {
params = []
}
if (!exercise) {
return
}
if (typeof exercise[functionName] === 'function') {
;(exercise[functionName] as Function)(...params)
}
})
if (exercise && exercise.getView()) {
const view = exercise.getView()

if (viewContainerRef.current.children.length > 0) {
const oldView = viewContainerRef.current.children[0] as HTMLElement
document.body.appendChild(oldView)
oldView.style.display = 'none'
}

viewContainerRef.current.innerHTML = ''
viewContainerRef.current.appendChild(view)
view.style.display = 'block'
}
} else {
let img
if (viewContainerRef.current.children.length > 0) {
Expand All @@ -59,11 +68,12 @@ export function useMountViewOrImage({
})
viewContainerRef.current.appendChild(img)
}
img.style.backgroundImage = `url('/exercise-images/${
taskTest.imageSlug ?? 'rock-paper-scissors/paper-paper.png'
}')`
img.style.backgroundImage = `url('https://assets.exercism.org/bootcamp/scenarios/${taskTest.imageSlug}')`

const viewDisplay = taskTest.imageSlug === undefined ? 'none' : 'block'
viewContainerRef.current.style.display = viewDisplay
}
}, [result])
}, [testSuiteResult, inspectedPreviewTaskTest])

return viewContainerRef
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import type { Change } from 'diff'
import { formatLiteral } from '@/interpreter/helpers'

export function IOTestResultView({ diff }: { diff: Change[] }) {
return (
Expand Down
Loading

0 comments on commit 0ee26c0

Please sign in to comment.