FlashcardView.jsx
9102 Chars | 317 Lines | 9.11 kb | Text File
Parent Directory
import React, { useEffect, useState, useMemo } from 'react'
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import Modal from 'react-bootstrap/Modal'
import ProgressBar from 'react-bootstrap/ProgressBar'
import { useUser } from '@/stores/user'
import { saveStarred, usePack } from '@/stores/pack'
import { useShallow } from 'zustand/react/shallow'
import { Markup } from '@/lib/Markup'
import './FlashcardView.scss'
import MultipleChoice from './MultipleChoice'
import { shuffle as shuffleFunction } from '@/lib/utilities'
import { motion } from 'framer-motion'
function smallText(text) {
return text.length > 150 || text.split('<p>').length > 3 || text.includes('<h')
}
function Definition({ content, setProgress }) {
const [subcard, setSubcard] = useState(0)
function handleSubcardKey(e) {
if (e.key === 'd' && subcard + 1 < content.length) {
setSubcard(subcard + 1)
} else if (e.key === 'a' && subcard - 1 >= 0) {
setSubcard(subcard - 1)
}
}
useEffect(() => {
document.addEventListener('keydown', handleSubcardKey)
return () => document.removeEventListener('keydown', handleSubcardKey)
}, [subcard, content, setSubcard, handleSubcardKey])
useEffect(() => {
if (content.length > 1) {
setProgress(
<div className="w-75 mx-auto my-3 d-flex justify-content-between align-items-center">
<kbd className="mx-2 badge text-bg-secondary">A</kbd>
<ProgressBar
variant="secondary"
className="w-100"
now={((subcard + 1) / content.length) * 100}
/>
<kbd className="mx-2 badge text-bg-secondary">D</kbd>
</div>
)
}
}, [subcard, setProgress])
return (
<div className={smallText(content[subcard]) ? 'fcTextSmall' : 'fcTextBig'}>
<Markup dark content={content[subcard]} />
</div>
)
}
export function FlashcardView() {
const [user] = useUser(useShallow((state) => [state.user]))
const [pack, editing] = usePack(useShallow((state) => [state.pack, state.editing]))
const [currentCard, setCurrentCard] = useState(0)
const [switchedTerm, setSwitchedTerm] = useState(false)
const [shuffle, setShuffle] = useState(false)
const [starred, setStarred] = useState(false)
const content = useMemo(
() => (shuffle ? shuffleFunction(pack.content) : pack.content),
[pack, shuffle]
)
// Term/Definition switch click
const [showDefinition, setShowDefinition] = useState(false)
// Options
const [showOptions, setShowOptions] = useState(false)
const [mcqMode, setMcqMode] = useState(false)
const [seperation, setSeperation] = useState(true)
// This is messy but it works
const [progress, setProgress] = useState(<></>)
// Arrow Keys Navigation
function handleKey(e) {
if (e.key === 'ArrowRight' && currentCard + 1 < content.length) {
setShowDefinition(mcqMode)
setCurrentCard(currentCard + 1)
} else if (e.key === 'ArrowLeft' && currentCard - 1 >= 0) {
setShowDefinition(mcqMode)
setCurrentCard(currentCard - 1)
} else if (e.key === ' ' && !mcqMode) {
e.preventDefault()
setShowDefinition(!showDefinition)
}
}
// All state referenced must be in the dependency array
useEffect(() => {
if (!editing) {
document.addEventListener('keydown', handleKey)
}
return () => {
document.removeEventListener('keydown', handleKey)
}
}, [mcqMode, editing, currentCard, showDefinition])
// Reset stars indicator
useEffect(() => {
setStarred(
localStorage.getItem(`starred-${pack.uuid}-${content[currentCard].uuid}`) === 'true'
)
}, [pack, currentCard])
// Reset progress bar
useEffect(() => {
if (!showDefinition || !seperation) setProgress(<></>)
}, [currentCard, showDefinition, seperation])
if (content.length == 0) {
return (
<div className="container bg-dark text-light rounded-3 py-4 shadow-lg mt-3">
<div className="container">
<h3 className="text-center text-muted">No cards match your filters</h3>
</div>
</div>
)
}
return (
<>
<Modal show={showOptions} onHide={() => setShowOptions(false)}>
<Modal.Header closeButton>
<Modal.Title>Flashcard Options</Modal.Title>
</Modal.Header>
<Modal.Body>
<h5>Presentation and Order</h5>
<Form.Check
className="me-3"
type="switch"
label="Switch Term and Definition"
checked={switchedTerm}
onChange={(e) => setSwitchedTerm(e.target.checked)}
/>
<Form.Check
type="switch"
label="Shuffle"
checked={shuffle}
onChange={(e) => setShuffle(e.target.checked)}
/>
<div className="d-flex justify-content-between align-content-center">
<Form.Check
type="switch"
label="Card Seperation"
checked={seperation}
className="me-2"
inline
onChange={(e) => setSeperation(e.target.checked)}
/>
<span className="badge bg-secondary my-auto">
✨ Make flashcards with multiple sides!
</span>
</div>
<h5>Assesment</h5>
<Form.Check
className="me-3"
type="switch"
label="Multiple Choice Mode"
checked={mcqMode}
onChange={(e) => {
if (content.length >= 4) {
setShowDefinition(false)
setMcqMode(e.target.checked)
} else {
alert('Add at least 4 cards to enable Multiple Choice mode')
}
if (e.target.checked == true) setShowDefinition(true)
}}
/>
</Modal.Body>
</Modal>
<div className="container bg-dark text-light rounded-3 py-4 shadow-lg mt-3">
<div className="container">
{/* Mode Options */}
<div className="d-flex justify-content-between align-items-center mb-4 flex-wrap">
<div>
<Button variant="dark" onClick={() => setShowOptions(true)}>
⚙️
</Button>
</div>
<div>
<b className="text-secondary me-2 align-middle">TERM</b>
<Button
variant={starred ? 'secondary' : 'dark'}
onClick={() => {
setStarred(!starred)
saveStarred(
!starred,
pack.uuid,
content[currentCard].uuid,
user?.uid
)
}}
>
⭐
</Button>
</div>
</div>
{/* Flashcard */}
<motion.div
key={content[currentCard][showDefinition ? 'definition' : 'term']}
className="shadow rounded-3 bg-secondary mx-auto text-center d-flex justify-content-center px-5 align-items-center flashCard"
onClick={() => {
if (!mcqMode) setShowDefinition(!showDefinition)
}}
initial={{
x: showDefinition ? -50 : 0,
y: showDefinition ? 0 : 50,
opacity: showDefinition ? 1 : 0.75,
}}
animate={{ x: 0, y: 0, opacity: 1 }}
transition={{ duration: 1, type: 'spring', bounce: 0.5 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{(showDefinition && !switchedTerm) || (!showDefinition && switchedTerm) ? (
<Definition
content={
seperation
? content[currentCard]['definition']
.split(';')
.map((e) => e.trim())
: [content[currentCard]['definition']]
}
setProgress={setProgress}
/>
) : (
<div
className={
smallText(content[currentCard]['term'])
? 'fcTextSmall'
: 'fcTextBig'
}
>
{content[currentCard]?.picture !== undefined && (
<img
className="w-100 mb-5 rounded-3 shadow"
src={content[currentCard]['picture']}
></img>
)}
<Markup dark content={content[currentCard]['term']} />
</div>
)}
</motion.div>
{progress}
{/* Multiple Choice */}
{mcqMode && (
<MultipleChoice
content={content[currentCard]}
currentCard={currentCard}
setCurrentCard={setCurrentCard}
flipped={switchedTerm}
/>
)}
{/* Messy controls */}
<div
className="d-flex justify-content-evenly align-content-center mt-5"
key={currentCard}
>
{currentCard - 1 < 0 ? (
<Button variant="light" disabled>
Previous
</Button>
) : (
<Button
variant="light"
onClick={() => {
setShowDefinition(false)
setCurrentCard(currentCard - 1)
}}
>
Previous
</Button>
)}
{/* Current Card */}
<span className="my-auto">
{currentCard + 1}/{content.length}
</span>
{currentCard + 1 >= content.length ? (
<Button variant="light" disabled>
Next
</Button>
) : (
<Button
variant="light"
onClick={() => {
setShowDefinition(false)
setCurrentCard(currentCard + 1)
}}
>
Next
</Button>
)}
</div>
</div>
</div>
</>
)
}