Skip to content

Commit

Permalink
feat: rewrite quiz implementation
Browse files Browse the repository at this point in the history
- now uses a markdown file per question
- supports inline and block code fences
- supports proper markdown parsing
- add timed question option
- add difficulty option
- add optional intro and outro sections for questions
- display the selected and correct answer if the user chooses the incorrect answer
- add a share your score option to the completion and game over screens
  • Loading branch information
gmickel committed Jul 18, 2024
1 parent c1e067b commit 46e91ec
Show file tree
Hide file tree
Showing 13 changed files with 473 additions and 114 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@radix-ui/react-alert-dialog": "1.1.1",
"@radix-ui/react-collapsible": "1.1.0",
"@radix-ui/react-progress": "1.1.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
Expand Down
8 changes: 5 additions & 3 deletions src/components/CodeHighlight.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { codeToHtml } from 'shiki/bundle/web';
import { codeToHtml } from 'shiki';

interface CodeHighlightProps {
code: string;
Expand Down Expand Up @@ -39,13 +39,15 @@ const CodeHighlight: React.FC<CodeHighlightProps> = ({ code, language, variant =

if (variant === 'inline') {
// For inline, we'll strip the pre and code tags and just return the highlighted content

/*
const strippedHtml = highlightedCode
.replace(/<pre.*?>/g, '')
.replace(/<\/pre>/g, '')
.replace(/<code.*?>/g, '')
.replace(/<\/code>/g, '');

return <span dangerouslySetInnerHTML={{ __html: strippedHtml }} />;
*/
return <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
}

return (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Quiz/Answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const Answers: React.FC<AnswersProps> = ({
<h2 className="text-lg font-semibold">Answers</h2>
{answers.map((answer, index) => {
const isSelected = selectedAnswer === index;
const isCorrect = correctAnswer === index;
const isCorrect = correctAnswer - 1 === index;

let buttonClass = '';
if (answered) {
Expand All @@ -42,7 +42,7 @@ const Answers: React.FC<AnswersProps> = ({
key={`answer-${index}`}
onClick={() => onAnswer(index)}
className={cn(
'w-full justify-start text-left h-auto',
'w-full justify-start text-left h-auto whitespace-pre-wrap',
buttonClass,
)}
variant="outline"
Expand Down
78 changes: 47 additions & 31 deletions src/components/Quiz/Completion.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,62 @@
import React from 'react';
import { Share2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import ConfettiCelebration from '@/components/ConfettiCelebration';
import { config } from '@/config';

interface CompletionProps {
score: number;
badges: string[];
onReset: () => void;
}

const Completion: React.FC<CompletionProps> = ({ score, badges, onReset }) => (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<ConfettiCelebration />
<Card className="w-full max-w-md">
<CardHeader>
<h1 className="text-2xl font-bold text-center">🎉 Congratulations! 🎉</h1>
</CardHeader>
<CardContent>
<p className="text-center mb-4">You've become an OpenEHR Integration Master!</p>
<p className="text-center mb-4">
Final Score:&nbsp;
{score}
</p>
<div className="mt-4">
You've earned the following badges:&nbsp;
<div className="flex flex-wrap gap-2 mt-4">
{badges.map(badge => (
<Badge key={`badge-${badge}`} variant="secondary">
{badge}
</Badge>
))}
const Completion: React.FC<CompletionProps> = ({ score, badges, onReset }) => {
const shareResults = () => {
const badgeText = badges.length === 1 ? 'badge' : 'badges';
const text = `I just completed the ${config.title} with a score of ${score} and earned ${badges.length} ${badgeText}! Create your own quest here https://github.com/gmickel/codequest or try to beat my score?`;
const url = config.url;
const shareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&hashtags=CodeQuest`;
window.open(shareUrl, '_blank');
};
return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<ConfettiCelebration />
<Card className="w-full max-w-md">
<CardHeader>
<h1 className="text-2xl font-bold text-center">🎉 Congratulations! 🎉</h1>
</CardHeader>
<CardContent>
<p className="text-center mb-4">You've become an OpenEHR Integration Master!</p>
<p className="text-center mb-4">
Final Score:&nbsp;
{score}
</p>
<div className="mt-4">
You've earned the following badges:&nbsp;
<div className="flex flex-wrap gap-2 mt-4">
{badges.map(badge => (
<Badge key={`badge-${badge}`} variant="secondary">
{badge}
</Badge>
))}
</div>
</div>
</div>
</CardContent>
<CardFooter>
<Button onClick={onReset} className="w-full">
Play Again
</Button>
</CardFooter>
</Card>
</div>
);
</CardContent>
<CardFooter className="flex flex-col space-y-2">
<Button onClick={shareResults} className="w-full bg-blue-500 hover:bg-blue-600 text-white">
<Share2 className="mr-2 h-4 w-4" />
{' '}
Share Results
</Button>
<Button onClick={onReset} variant="outline" className="w-full">
Play Again
</Button>
</CardFooter>
</Card>
</div>
);
};

export default Completion;
46 changes: 38 additions & 8 deletions src/components/Quiz/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import React from 'react';
import type { ParsedQuestion } from '@/lib/questionParser';
import { ChevronDown, ChevronUp } from 'lucide-react';
import type { ParsedQuestion } from '@/lib/markdownParser';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { Button } from '@/components/ui/button';
import type { renderContent as renderContentType } from '@/components/RenderContent';

interface ContextProps {
question: ParsedQuestion;
renderContent: typeof renderContentType;
isAdditionalInformationOpen: boolean;
setIsAdditionalInformationOpen: (isOpen: boolean) => void;
}

const Context: React.FC<ContextProps> = ({ question, renderContent }) => {
const Context: React.FC<ContextProps> = ({ question, renderContent, isAdditionalInformationOpen, setIsAdditionalInformationOpen }) => {
return (
<div className="space-y-6">
{question.context.introduction && (
Expand All @@ -25,12 +34,33 @@ const Context: React.FC<ContextProps> = ({ question, renderContent }) => {
</div>
</div>
{question.context.outro && (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Additional Information:</h2>
<div className="text-sm text-muted-foreground">
{renderContent({ content: question.context.outro })}
</div>
</div>
<Collapsible open={isAdditionalInformationOpen} onOpenChange={setIsAdditionalInformationOpen}>
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full">
{isAdditionalInformationOpen
? (
<>
<ChevronUp className="mr-2 h-4 w-4" />
Hide Additional Information
</>
)
: (
<>
<ChevronDown className="mr-2 h-4 w-4" />
Show Additional Information
</>
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-4 mt-4">
<h2 className="text-lg font-semibold">Additional Information:</h2>
<div className="text-sm text-muted-foreground">
{renderContent({ content: question.context.outro })}
</div>
</div>
</CollapsibleContent>
</Collapsible>
)}
</div>
);
Expand Down
60 changes: 39 additions & 21 deletions src/components/Quiz/GameOver.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import React from 'react';
import { Share2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card';
import { config } from '@/config';

interface GameOverProps {
score: number;
badges: string[];
onReset: () => void;
}

const GameOver: React.FC<GameOverProps> = ({ score, onReset }) => (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<Card className="w-full max-w-md">
<CardHeader>
<h1 className="text-2xl font-bold text-center">Game Over</h1>
</CardHeader>
<CardContent>
<p className="text-center mb-4">Your OpenEHR journey has come to an end.</p>
<p className="text-center mb-4">
Final Score:
{score}
</p>
</CardContent>
<CardFooter>
<Button onClick={onReset} className="w-full">
Try Again
</Button>
</CardFooter>
</Card>
</div>
);
const GameOver: React.FC<GameOverProps> = ({ score, onReset, badges }) => {
const shareResults = () => {
const badgeText = badges.length === 1 ? 'badge' : 'badges';
const text = `I just completed the ${config.title} with a score of ${score} and earned ${badges.length} ${badgeText}! Create your own quest here https://github.com/gmickel/codequest or try to beat my score?`;
const url = config.url;
const shareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&hashtags=CodeQuest`;
window.open(shareUrl, '_blank');
};

return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<Card className="w-full max-w-md">
<CardHeader>
<h1 className="text-2xl font-bold text-center">Game Over</h1>
</CardHeader>
<CardContent>
<p className="text-center mb-4">Your OpenEHR journey has come to an end.</p>
<p className="text-center mb-4">
Final Score:
{score}
</p>
</CardContent>
<CardFooter className="flex flex-col space-y-2">
<Button onClick={shareResults} className="w-full bg-blue-500 hover:bg-blue-600 text-white">
<Share2 className="mr-2 h-4 w-4" />
{' '}
Share Results
</Button>
<Button onClick={onReset} variant="outline" className="w-full">
Try Again
</Button>
</CardFooter>
</Card>
</div>
);
};

export default GameOver;
Loading

0 comments on commit 46e91ec

Please sign in to comment.