Skip to content

Commit 7752849

Browse files
authored
Add grace threshold for CSS exercises (exercism#7858)
* Add grace threshold for css exercises * Remove unused import
1 parent 55ade4d commit 7752849

File tree

4 files changed

+110
-49
lines changed

4 files changed

+110
-49
lines changed

app/javascript/components/bootcamp/CSSExercisePage/Header/Header.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ function _Header() {
4646
{solution.status === 'in_progress' && (
4747
<button
4848
onClick={handleCompleteSolution}
49-
disabled={assertionStatus !== 'pass'}
49+
disabled={assertionStatus === 'fail'}
5050
className={assembleClassNames(
5151
'btn-primary btn-xxs',
52-
assertionStatus === 'pass' ? '' : 'disabled cursor-not-allowed'
52+
assertionStatus === 'fail' ? 'disabled cursor-not-allowed' : ''
5353
)}
5454
>
5555
Complete Exercise
5656
</button>
5757
)}
58-
{assertionStatus === 'pass' && (
58+
{assertionStatus !== 'fail' && (
5959
<>
6060
<FinishLessonModalContext.Provider
6161
value={{

app/javascript/components/bootcamp/CSSExercisePage/LHS/ControlButtons.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { useCallback, useContext } from 'react'
2+
import toast from 'react-hot-toast'
23
import { assembleClassNames } from '@/utils/assemble-classnames'
34
import {
5+
AssertionStatus,
6+
GRACE_THRESHOLD,
47
PASS_THRESHOLD,
58
useCSSExercisePageStore,
69
} from '../store/cssExercisePageStore'
@@ -13,7 +16,6 @@ import { readOnlyRangesStateField } from '../../JikiscriptExercisePage/CodeMirro
1316
import { runHtmlChecks } from '../checks/runHtmlChecks'
1417
import { CheckResult } from '../checks/runChecks'
1518
import { runCssChecks } from '../checks/runCssChecks'
16-
import toast from 'react-hot-toast'
1719
import { validateHtml5 } from '../../common/validateHtml5/validateHtml5'
1820

1921
export function ControlButtons({
@@ -48,7 +50,7 @@ export function ControlButtons({
4850

4951
const percentage = await handleCompare()
5052

51-
let status: 'pass' | 'fail' = 'fail'
53+
let status: AssertionStatus = 'fail'
5254
let firstFailingCheck: CheckResult | null = null
5355

5456
if (htmlValue.length > 0) {
@@ -69,8 +71,13 @@ export function ControlButtons({
6971
const allCssChecksPass =
7072
exercise.cssChecks.length === 0 || cssChecks.success
7173

72-
if (percentage >= PASS_THRESHOLD && allHtmlChecksPass && allCssChecksPass) {
73-
status = 'pass'
74+
if (allHtmlChecksPass && allCssChecksPass) {
75+
if (percentage >= GRACE_THRESHOLD) {
76+
status = 'grace'
77+
}
78+
if (percentage >= PASS_THRESHOLD) {
79+
status = 'pass'
80+
}
7481
} else {
7582
firstFailingCheck =
7683
htmlChecks.results.find((check) => !check.passes) ||
Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,122 @@
11
import React from 'react'
22
import toast from 'react-hot-toast'
33
import { assembleClassNames } from '@/utils/assemble-classnames'
4-
import { PASS_THRESHOLD } from '../store/cssExercisePageStore'
4+
import {
5+
AssertionStatus,
6+
GRACE_THRESHOLD,
7+
PASS_THRESHOLD,
8+
} from '../store/cssExercisePageStore'
59
import { CheckResult } from '../checks/runChecks'
10+
import GraphicalIcon from '@/components/common/GraphicalIcon'
11+
12+
const STATUS_COLORS: Record<AssertionStatus, { border: string; text: string }> =
13+
{
14+
pass: {
15+
border: 'var(--successColor)',
16+
text: '#2E8C70',
17+
},
18+
fail: {
19+
border: '#D85050',
20+
text: '#D85050',
21+
},
22+
grace: {
23+
border: '#DC8604',
24+
text: '#DC8604',
25+
},
26+
}
627

728
export function showResultToast(
8-
status: 'pass' | 'fail',
29+
status: AssertionStatus,
930
percentage: number,
1031
firstFailingCheck?: CheckResult | null
1132
) {
1233
toast.custom(
1334
(t) => (
1435
<div
36+
style={{ borderColor: STATUS_COLORS[status].border }}
1537
className={assembleClassNames(
1638
t.visible ? 'animate-slideIn' : 'animate-slideOut',
17-
status === 'pass' ? 'border-successColor' : 'border-danger',
18-
'border-2 flex bg-white shadow-base text-14 rounded-5 p-8 gap-8 items-center'
39+
'max-w-[500px] w-full',
40+
`border-${STATUS_COLORS[status].border}`,
41+
'border-1 flex justify-between bg-white shadow-base text-14 rounded-8 p-8 gap-8 items-center relative'
1942
)}
2043
>
21-
{/*
22-
We always only show one message.
23-
Pixel-matching is top priority, then other checks' error message.
24-
Otherwise show a success message.
25-
*/}
26-
{percentage < PASS_THRESHOLD ? (
27-
<TextBlock
28-
status={status}
29-
text={`Your output isn't exactly the same as the target yet (${percentage}%).`}
30-
/>
31-
) : firstFailingCheck ? (
32-
<TextBlock
33-
status={status}
34-
text={firstFailingCheck.error_html || ''}
35-
/>
36-
) : (
37-
<TextBlock status={status} text="Congrats! All checks are passing!" />
38-
)}
39-
44+
<ToastText
45+
percentage={percentage}
46+
status={status}
47+
firstFailingCheck={firstFailingCheck}
48+
/>
4049
<button
41-
className="btn-xs btn-enhanced"
50+
className="rounded-circle bg-bootcamp-light-purple p-4 self-start"
4251
onClick={() => toast.dismiss(t.id)}
4352
>
44-
Got it
53+
<GraphicalIcon icon="close" height={12} width={12} />
4554
</button>
4655
</div>
4756
),
4857
{ duration: 6000 }
4958
)
5059
}
5160

61+
function ToastText({
62+
status,
63+
percentage,
64+
firstFailingCheck,
65+
}: {
66+
status: AssertionStatus
67+
percentage: number
68+
firstFailingCheck?: CheckResult | null
69+
}) {
70+
if (firstFailingCheck) {
71+
return (
72+
<TextBlock
73+
status={status}
74+
textHtml={firstFailingCheck.error_html || ''}
75+
/>
76+
)
77+
}
78+
79+
if (percentage < GRACE_THRESHOLD) {
80+
return (
81+
<TextBlock
82+
status={status}
83+
textHtml={`Your output isn't exactly the same as the target yet (${percentage}%).`}
84+
/>
85+
)
86+
}
87+
88+
if (percentage < PASS_THRESHOLD) {
89+
return (
90+
<div
91+
style={{ color: STATUS_COLORS[status].text }}
92+
className={assembleClassNames('font-medium px-8 py-4')}
93+
>
94+
<p className="text-14 leading-160">
95+
You're currently at {percentage}%. You can <strong> complete </strong>{' '}
96+
the exercise if you have invested as much energy as you want to into
97+
it. But you might like to try to get to <strong>100%</strong> still!
98+
</p>
99+
</div>
100+
)
101+
}
102+
103+
return (
104+
<TextBlock status={status} textHtml="Congrats! All checks are passing!" />
105+
)
106+
}
107+
52108
function TextBlock({
53-
text,
109+
textHtml,
54110
status,
55111
}: {
56-
text: string
57-
status: 'pass' | 'fail'
112+
textHtml: string
113+
status: AssertionStatus
58114
}) {
59115
return (
60116
<div
61-
className={assembleClassNames(
62-
'font-semibold',
63-
status === 'pass' ? 'text-darkSuccessGreen' : 'text-danger'
64-
)}
65-
>
66-
{text}
67-
</div>
117+
style={{ color: STATUS_COLORS[status].text }}
118+
className={assembleClassNames('font-medium px-8 py-4')}
119+
dangerouslySetInnerHTML={{ __html: textHtml }}
120+
/>
68121
)
69122
}

app/javascript/components/bootcamp/CSSExercisePage/store/cssExercisePageStore.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { launchConfetti } from '../../JikiscriptExercisePage/Tasks/launchConfett
33
import { ChecksResult } from '../checks/runChecks'
44

55
export const PASS_THRESHOLD = 99
6+
export const GRACE_THRESHOLD = 96
7+
8+
export type AssertionStatus = 'pass' | 'fail' | 'grace'
69

710
type CSSExercisePageStoreState = {
811
isDiffModeOn: boolean
@@ -20,9 +23,9 @@ type CSSExercisePageStoreState = {
2023
setPanelSizes: (panelSizes: { LHSWidth: number; RHSWidth: number }) => void
2124
matchPercentage: number
2225
setMatchPercentage: (matchPercentage: number) => void
23-
assertionStatus: 'pass' | 'fail' | 'pending'
24-
setAssertionStatus: (assertionStatus: 'pass' | 'fail' | 'pending') => void
25-
updateAssertionStatus: (newStatus: 'pass' | 'fail') => void
26+
assertionStatus: AssertionStatus
27+
setAssertionStatus: (assertionStatus: AssertionStatus) => void
28+
updateAssertionStatus: (newStatus: AssertionStatus) => void
2629
isFinishLessonModalOpen: boolean
2730
setIsFinishLessonModalOpen: (value: boolean) => void
2831
wasFinishLessonModalShown: boolean
@@ -65,7 +68,7 @@ export const useCSSExercisePageStore = create<CSSExercisePageStoreState>(
6568
setPanelSizes: (panelSizes) => set({ panelSizes }),
6669
checksResult: [],
6770
setChecksResult: (checksResult) => set({ checksResult }),
68-
assertionStatus: 'pending',
71+
assertionStatus: 'fail',
6972
setAssertionStatus: (assertionStatus) => {
7073
set({ assertionStatus })
7174
},
@@ -91,9 +94,7 @@ export const useCSSExercisePageStore = create<CSSExercisePageStoreState>(
9194
}
9295
return newState
9396
})
94-
} else {
95-
set({ assertionStatus: 'fail' })
96-
}
97+
} else set({ assertionStatus: newStatus })
9798
},
9899
matchPercentage: 0,
99100
setMatchPercentage: (matchPercentage) => {

0 commit comments

Comments
 (0)