mango-v4-ui/components/quiz/Quiz.tsx

401 lines
16 KiB
TypeScript

import { Quiz as QuizType, QuizQuestion } from 'utils/quiz'
import { useState } from 'react'
import Button, { IconButton } from '@components/shared/Button'
import { useRouter } from 'next/router'
import {
ArrowLeftIcon,
CheckCircleIcon,
ChevronDownIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/20/solid'
import { Disclosure } from '@headlessui/react'
import Image from 'next/image'
import { useWallet } from '@solana/wallet-adapter-react'
import useMangoAccount from 'hooks/useMangoAccount'
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { useQueryClient } from '@tanstack/react-query'
import { useCompletedQuizzes } from 'hooks/useQuiz'
type RESULT = {
correctAnswers: number
wrongAnswers: QuizQuestion[]
}
const DEFAULT_RESULT = {
correctAnswers: 0,
wrongAnswers: [],
}
const Quiz = ({ quiz }: { quiz: QuizType }) => {
const router = useRouter()
const queryClient = useQueryClient()
const { connected, publicKey, signMessage } = useWallet()
const { mangoAccountAddress } = useMangoAccount()
const { data: solved } = useCompletedQuizzes(publicKey?.toBase58())
const [currentQuestion, setCurrentQuestion] = useState(0)
const [answerIndex, setAnswerIndex] = useState<number | null>(null)
const [isCorrectAnswer, setIsCorrectAnswer] = useState<boolean | null>(null)
const [result, setResult] = useState<RESULT>(DEFAULT_RESULT)
const [showIntro, setShowIntro] = useState(true)
const [showResult, setShowResult] = useState(false)
const { questions, intro } = quiz
const { question, choices, description, correctAnswer } =
questions[currentQuestion]
const handleAnswer = (answer: string, index: number) => {
setAnswerIndex(index)
if (answer === correctAnswer) {
setIsCorrectAnswer(true)
} else {
setIsCorrectAnswer(false)
}
}
const handleNext = () => {
setAnswerIndex(null)
setResult((prev) =>
isCorrectAnswer
? {
...prev,
correctAnswers: prev.correctAnswers + 1,
}
: {
...prev,
wrongAnswers: [...prev.wrongAnswers, questions[currentQuestion]],
},
)
if (currentQuestion !== questions.length - 1) {
setCurrentQuestion((prev) => prev + 1)
} else {
setCurrentQuestion(0)
setShowResult(true)
}
}
const handleTryAgain = () => {
setResult(DEFAULT_RESULT)
setShowResult(false)
}
const getResultsHeadingText = (score: number) => {
if (!score) {
return 'Whoops 😲'
} else if (score < 50) {
return 'Try Again'
} else if (score < 100) {
return 'Almost There...'
} else return 'Congratulations 🎉'
}
const completeQuiz = async () => {
const message = new TextEncoder().encode(mangoAccountAddress)
const signature = await signMessage!(message)
const rawResponse = await fetch(
'https://api.mngo.cloud/data/v4/user-data/complete-quiz',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
wallet_pk: publicKey?.toBase58(),
quiz_id: quiz.id,
mango_account: mangoAccountAddress,
signature: bs58.encode(signature),
}),
},
)
await rawResponse.json()
queryClient.invalidateQueries(['completed-quizzes', publicKey?.toBase58()])
router.push('/learn', undefined, { shallow: true })
}
const canClaimPoints = connected && mangoAccountAddress
return (
<>
<div className="flex h-12 w-full items-center justify-between bg-th-bkg-2 px-4 md:px-6">
<div className="flex items-center">
<IconButton
className="text-th-fgd-3"
hideBg
size="medium"
onClick={() => router.push('/learn', undefined, { shallow: true })}
>
<ArrowLeftIcon className="h-5 w-5" />
</IconButton>
<p className="text-th-fgd-2">{quiz.name} Quiz</p>
</div>
{showIntro || showResult ? null : (
<div className="rounded-full bg-th-bkg-1 px-3 py-1 font-mono">
<span>{currentQuestion + 1}</span>
<span>/{quiz.questions.length}</span>
</div>
)}
</div>
<div className="flex h-0.5 w-full grow bg-th-bkg-1">
<div
style={{
width:
showIntro || showResult
? '0%'
: `${((currentQuestion + 1) / quiz.questions.length) * 100}%`,
opacity: showResult ? 0 : 100,
}}
className="flex bg-th-active transition-all duration-700 ease-out"
/>
</div>
<div className="w-full px-4">
<div className="mx-auto mt-12 w-full max-w-xl rounded-xl bg-th-bkg-2 p-8">
{showIntro ? (
<>
{quiz.imagePath ? (
<Image
className="mx-auto mb-3"
src={quiz.imagePath}
height={48}
width={48}
alt="Quiz Image"
/>
) : null}
<h2 className="mb-2">{intro.title}</h2>
<p className="text-base">{intro.description}</p>
{intro?.docs ? (
<a className="mt-2 block text-base" href={intro.docs.url}>
{intro.docs.linkText}
</a>
) : null}
<Button
className="mt-6"
onClick={() => setShowIntro(false)}
size="large"
>
Let&apos;s Go
</Button>
<div className="mx-auto mt-6 w-max rounded-full border border-th-fgd-4 px-3 py-1">
<p className="text-th-fgd-2">
{!connected
? 'Connect wallet to earn rewards points'
: solved?.find((x) => x.quiz_id === quiz.id)
? 'Rewards Points Claimed'
: mangoAccountAddress
? `Score ${quiz.questions.length}/${quiz.questions.length} to earn rewards points`
: 'Create a Mango Account to earn rewards points'}
</p>
</div>
</>
) : !showResult ? (
<>
<h2 className="leading-tight">{question}</h2>
{description ? <p className="mt-2"></p> : null}
<div className="space-y-1 pt-6">
{choices.map((choice, index) => (
<button
className={`flex w-full items-center justify-between rounded-md bg-th-bkg-3 p-3 text-th-fgd-1 ${
answerIndex === index
? 'border border-th-active'
: 'border border-transparent md:hover:bg-th-bkg-4'
}`}
key={choice}
onClick={() => handleAnswer(choice, index)}
>
<div
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs ${
answerIndex === index
? 'bg-th-active text-th-bkg-1'
: 'bg-th-bkg-1 text-th-fgd-3'
}`}
>
<span className="font-bold">
{String.fromCharCode(97 + index).toUpperCase()}
</span>
</div>
<span className="mx-6 text-base">{choice}</span>
<div className="h-6 w-6" />
</button>
))}
</div>
<div className="mt-8 flex justify-end">
<Button disabled={answerIndex === null} onClick={handleNext}>
{currentQuestion === questions.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</>
) : (
<>
<h2 className="mb-4">
{getResultsHeadingText(
(result.correctAnswers / questions.length) * 100,
)}
</h2>
<p>You scored</p>
<span className="font-display text-5xl text-th-fgd-1">
{((result.correctAnswers / questions.length) * 100).toFixed()}%
</span>
{result.correctAnswers !== questions.length ? (
<div className="mx-auto mt-2 w-max rounded-full border border-th-fgd-4 px-3 py-1">
<p className="text-th-fgd-2">
Try again to earn rewards points.
</p>
</div>
) : null}
<div className="my-6 border-b border-th-bkg-4">
<div className="flex justify-between border-t border-th-bkg-4 p-4">
<div className="flex items-center">
<CheckCircleIcon className="mr-1 h-4 w-4 text-th-success" />
<p>Correct Answers</p>
</div>
<p className="font-mono text-th-fgd-1">
{result.correctAnswers}
</p>
</div>
{result.wrongAnswers?.length ? (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`w-full border-t border-th-bkg-4 p-4 text-left focus:outline-none`}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<XCircleIcon className="mr-1 h-4 w-4 text-th-error" />
<p>Wrong Answers</p>
</div>
<div className="flex items-center space-x-2">
<p className="font-mono text-th-fgd-1">
{result.wrongAnswers.length}
</p>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-0'
} h-6 w-6 shrink-0 text-th-fgd-3`}
/>
</div>
</div>
</Disclosure.Button>
<Disclosure.Panel className="pb-2">
{result.wrongAnswers.map((answer) => (
<div className="mb-2" key={answer.question}>
<Disclosure>
{({ open }) => (
<>
<div
className={`flex items-center justify-between rounded-lg bg-th-bkg-1 p-4 text-left ${
open ? 'rounded-b-none' : ''
}`}
>
<div className="flex items-start">
<XCircleIcon className="mr-1 mt-0.5 h-4 w-4 shrink-0 text-th-error" />
<p className="font-bold text-th-fgd-1">
{answer.question}
</p>
</div>
<Disclosure.Button className="ml-4">
<span className="whitespace-nowrap text-xs">
{open
? 'Hide Answer'
: 'Reveal Answer'}
</span>
</Disclosure.Button>
</div>
<Disclosure.Panel className="rounded-b-lg bg-th-bkg-1 p-4">
{answer.explanation ? (
<div className="mb-4 rounded-lg bg-th-up-muted p-4 text-left">
<div className="flex items-start">
<InformationCircleIcon className="mr-1 mt-0.5 h-4 w-4 shrink-0 text-th-fgd-1" />
<p className="text-th-fgd-1">
{answer.explanation}
</p>
</div>
</div>
) : null}
{answer.choices.map((choice, index) => (
<div
key={choice}
className={`mb-2 flex w-full items-center justify-between rounded-md p-3 text-th-fgd-1 ${
answer.correctAnswer === choice
? 'border border-th-success'
: 'border border-th-bkg-4'
}`}
>
<div
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs ${
answer.correctAnswer === choice
? 'bg-th-success text-th-bkg-1'
: 'bg-th-bkg-2 text-th-fgd-3'
}`}
>
<span className="font-bold">
{String.fromCharCode(
97 + index,
).toUpperCase()}
</span>
</div>
<span className="mx-6">{choice}</span>
<div className="h-6 w-6" />
</div>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
) : (
<div className="flex justify-between border-t border-th-bkg-4 p-4">
<div className="flex items-center">
<XCircleIcon className="mr-1 h-4 w-4 text-th-error" />
<p>Wrong Answers</p>
</div>
<p className="font-mono text-th-fgd-1">0</p>
</div>
)}
</div>
<div className="flex justify-center space-x-3">
{solved?.find((x) => x.quiz_id === quiz.id) ||
!canClaimPoints ? (
<Button
onClick={() =>
router.push('/learn', undefined, { shallow: true })
}
size="large"
>
Exit
</Button>
) : result.correctAnswers === questions.length ? (
<Button onClick={completeQuiz} size="large">
Claim Rewards Points
</Button>
) : (
<>
<Button onClick={handleTryAgain} secondary size="large">
Try Again
</Button>
<Button
onClick={() =>
router.push('/learn', undefined, { shallow: true })
}
size="large"
>
Exit
</Button>
</>
)}
</div>
</>
)}
</div>
</div>
</>
)
}
export default Quiz