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 free-drawing page #7266

Merged
merged 19 commits into from
Jan 9, 2025
Merged
Prev Previous commit
Next Next commit
Add scrubber copy
  • Loading branch information
dem4ron committed Jan 9, 2025
commit 4628b838a444e4d5f884a8b5018ab37a86ad6208
149 changes: 149 additions & 0 deletions app/javascript/components/bootcamp/DrawingPage/Scrubber/Scrubber.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react'
import { useEffect, useState } from 'react'
import { calculateMaxInputValue, useScrubber } from './useScrubber'
import useEditorStore from '@/components/bootcamp/SolveExercisePage/store/editorStore'
import { TooltipInformation } from './ScrubberTooltipInformation'
import { InformationWidgetToggleButton } from './InformationWidgetToggleButton'
import { Icon } from '@/components/common'

function Scrubber({ testResult }: { testResult: NewTestResult }) {
const [isPlaying, setIsPlaying] = useState(false)

const { hasCodeBeenEdited, setShouldShowInformationWidget } = useEditorStore()

const {
value,
handleChange,
handleOnMouseUp,
handleOnKeyUp,
handleOnKeyDown,
handleMouseDown,
updateInputBackground,
rangeRef,
handleGoToNextFrame,
handleGoToPreviousFrame,
handleScrubToCurrentTime,
} = useScrubber({
setIsPlaying,
testResult,
})

useEffect(() => {
if (isPlaying || hasCodeBeenEdited) {
setShouldShowInformationWidget(false)
}
}, [isPlaying, hasCodeBeenEdited])

// when user switches between test results, scrub to animation timeline's persisted currentTime
useEffect(() => {
if (!testResult.animationTimeline) {
return
}
handleScrubToCurrentTime(testResult.animationTimeline)
}, [testResult.animationTimeline?.timeline.currentTime])

return (
<div
data-cy="scrubber"
id="scrubber"
onClick={() => {
// we wanna focus the range input, so keyboard shortcuts work
rangeRef.current?.focus()
}}
tabIndex={-1}
className="relative group"
>
{testResult.animationTimeline && (
<PlayButton
disabled={shouldScrubberBeDisabled(hasCodeBeenEdited, testResult)}
onClick={() => {
testResult.animationTimeline?.play(() =>
setShouldShowInformationWidget(false)
)
}}
/>
)}
<input
data-cy="scrubber-range-input"
disabled={shouldScrubberBeDisabled(hasCodeBeenEdited, testResult)}
type="range"
onKeyUp={(event) => handleOnKeyUp(event, testResult)}
onKeyDown={(event) => handleOnKeyDown(event, testResult)}
min={0}
ref={rangeRef}
max={calculateMaxInputValue(testResult)}
onInput={updateInputBackground}
value={value}
onMouseDown={(event) => handleMouseDown(event, testResult)}
onChange={(event) => {
handleChange(event, testResult)
updateInputBackground()
}}
onMouseUp={() => handleOnMouseUp(testResult)}
/>
<FrameStepperButtons
onNext={() => handleGoToNextFrame(testResult)}
onPrev={() => handleGoToPreviousFrame(testResult)}
disabled={shouldScrubberBeDisabled(hasCodeBeenEdited, testResult)}
/>
<InformationWidgetToggleButton disabled={hasCodeBeenEdited} />
<TooltipInformation
hasCodeBeenEdited={hasCodeBeenEdited}
notEnoughFrames={testResult.frames.length === 1}
/>
</div>
)
}

export default Scrubber

function PlayButton({
disabled,
onClick,
}: {
disabled: boolean
onClick: () => void
}) {
return (
<button disabled={disabled} className="play-button" onClick={onClick}>
<Icon icon="bootcamp-play" alt="Play" width={32} height={32} />
</button>
)
}

function FrameStepperButtons({
onNext,
onPrev,
disabled,
}: {
onNext: () => void
onPrev: () => void
disabled: boolean
}) {
return (
<div className="frame-stepper-buttons">
<button disabled={disabled} onClick={onPrev}>
<Icon
icon="bootcamp-chevron-right"
alt="Previous"
className="rotate-180"
/>
</button>
<button disabled={disabled} onClick={onNext}>
<Icon icon="bootcamp-chevron-right" alt="Next" />
</button>
</div>
)
}

function shouldScrubberBeDisabled(
hasCodeBeenEdited: boolean,
testResult: NewTestResult
) {
// if the code has been edited, the scrubber should be disabled
// if there is no animation timeline and there is only one frame, the scrubber should be disabled
return (
hasCodeBeenEdited ||
(!testResult.animationTimeline && testResult.frames.length === 1)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
export function TooltipInformation({
hasCodeBeenEdited,
notEnoughFrames,
}: Record<string, boolean>) {
// editing code removes frames anyway, so this has to be higher precedence
if (hasCodeBeenEdited) {
return (
<StaticTooltip text="Scrubber is disabled because the code has been edited" />
)
}

if (notEnoughFrames) {
return (
<StaticTooltip text="There is only one frame. You can inspect that by toggling the information widget." />
)
}
}

function StaticTooltip({ text }: { text: string }) {
return (
<div
className="absolute left-1/2 -top-10 py-4 px-8 -translate-x-1/2 -translate-y-[100%] hidden group-hover:block bg-gray-800 text-[#fafaff] text-sm rounded shadow-lg"
role="tooltip"
>
{text}
</div>
)
}
Loading