Public Access
feat: add Essay, Match, and OrderQuestion renderers
This commit is contained in:
1 parent
82ba53da0e
commit
e48e37ad87
3 files changed
+231
No files matched your search
@@ -0,0 +1,109 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import Tex from '../Tex'
|
||||
import type { Question } from '../../types'
|
||||
|
||||
interface Props {
|
||||
q: Question
|
||||
value: Record<string, string> | null | undefined
|
||||
onChange: (val: Record<string, string>) => void
|
||||
locked?: boolean
|
||||
graded?: boolean
|
||||
}
|
||||
|
||||
export default function Match({ q, value, onChange, locked, graded }: Props) {
|
||||
const current = value ?? {}
|
||||
const [activeLeft, setActiveLeft] = useState<string | null>(null)
|
||||
const answer = (q.answer as Record<string, string>) ?? {}
|
||||
|
||||
const reverseMap = useMemo(() => {
|
||||
const m: Record<string, string> = {}
|
||||
Object.entries(current).forEach(([l, r]) => { m[r] = l })
|
||||
return m
|
||||
}, [current])
|
||||
|
||||
const handleRight = (rightId: string) => {
|
||||
if (locked) return
|
||||
if (activeLeft) {
|
||||
const next = { ...current }
|
||||
Object.keys(next).forEach((k) => { if (k === activeLeft || next[k] === rightId) delete next[k] })
|
||||
next[activeLeft] = rightId
|
||||
onChange(next)
|
||||
setActiveLeft(null)
|
||||
} else if (reverseMap[rightId]) {
|
||||
const next = { ...current }
|
||||
delete next[reverseMap[rightId]]
|
||||
onChange(next)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeft = (leftId: string) => {
|
||||
if (locked) return
|
||||
if (current[leftId]) {
|
||||
const next = { ...current }
|
||||
delete next[leftId]
|
||||
onChange(next)
|
||||
return
|
||||
}
|
||||
setActiveLeft(activeLeft === leftId ? null : leftId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="match-grid">
|
||||
<div className="match-col">
|
||||
<div className="match-col-label">Items</div>
|
||||
{(q.left ?? []).map((item, i) => {
|
||||
let cls = 'match-item'
|
||||
if (activeLeft === item.id) cls += ' active'
|
||||
else if (current[item.id]) cls += ' paired'
|
||||
if (graded) {
|
||||
if (answer[item.id] && current[item.id] === answer[item.id]) cls += ' correct'
|
||||
else if (current[item.id]) cls += ' wrong'
|
||||
}
|
||||
const pairedRightIdx = current[item.id]
|
||||
? (q.right ?? []).findIndex((x) => x.id === current[item.id])
|
||||
: -1
|
||||
return (
|
||||
<button key={item.id} className={cls} onClick={() => handleLeft(item.id)} disabled={locked}>
|
||||
<span style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<span className="mono faint" style={{ fontSize: 11 }}>{i + 1}</span>
|
||||
<Tex>{item.text}</Tex>
|
||||
</span>
|
||||
{pairedRightIdx >= 0 && (
|
||||
<span className="match-pair-badge">{String.fromCharCode(65 + pairedRightIdx)}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="match-col">
|
||||
<div className="match-col-label">Matches</div>
|
||||
{(q.right ?? []).map((item, i) => {
|
||||
let cls = 'match-item'
|
||||
if (reverseMap[item.id]) cls += ' paired'
|
||||
if (graded && reverseMap[item.id]) {
|
||||
cls += answer[reverseMap[item.id]] === item.id ? ' correct' : ' wrong'
|
||||
}
|
||||
const pairedLeftIdx = reverseMap[item.id]
|
||||
? (q.left ?? []).findIndex((x) => x.id === reverseMap[item.id])
|
||||
: -1
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
className={cls}
|
||||
onClick={() => handleRight(item.id)}
|
||||
disabled={locked || (!activeLeft && !reverseMap[item.id])}
|
||||
>
|
||||
<span style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<span className="mono faint" style={{ fontSize: 11 }}>{String.fromCharCode(65 + i)}</span>
|
||||
<Tex>{item.text}</Tex>
|
||||
</span>
|
||||
{pairedLeftIdx >= 0 && (
|
||||
<span className="match-pair-badge">{pairedLeftIdx + 1}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user