Pack.jsx
7080 Chars | 273 Lines | 7.10 kb | Text File
Parent Directory
import React, { useContext, useEffect, useState } from 'react'
import Cards from './Cards'
import { Metadata } from './Metadata'
import { useParams, useNavigate } from 'react-router-dom'
import Button from 'react-bootstrap/Button'
import ButtonGroup from 'react-bootstrap/ButtonGroup'
import Spinner from 'react-bootstrap/Spinner'
import Form from 'react-bootstrap/Form'
import { Navigation } from './Navigation'
import { FlashcardView } from './Views/FlashcardView'
import { usePack } from '@/stores/pack'
import { useUser } from '@/stores/user'
import { useShallow } from 'zustand/react/shallow'
import { v4 as uuidv4 } from 'uuid'
import { ThemeContext } from '@/lib/context'
function Pack() {
const { displayName, packId } = useParams()
// We wrap useState around this so we can modify it (ex. adding new cards)
//const { pack: ogPack, mutate, isLoading, error } = usePack(packId)
//const { pack, setPack } = useState(ogPack)
const [user, newPack] = useUser(useShallow((state) => [state.user, state.newPack]))
const [
pack,
error,
loading,
filterPackCategory,
filterPackStarred,
loadPack,
addCards,
canEdit,
letMeEdit,
] = usePack(
useShallow((state) => [
// Data
state.pack,
state.error,
state.loading,
state.filterPackCategory,
state.filterPackStarred,
state.loadPack,
state.addCards,
// Editing
state.canEdit,
state.letMeEdit,
])
)
const theme = useContext(ThemeContext)
// For a saving progress spinner
const [saving, startSaving] = useState(false)
// Category filter
const [categoryFilter, setCategoryFilter] = useState(0)
// const [filteredPack, setFilteredPack] = useState(pack)
// Starred filter
const [starredFilter, setStarredFilter] = useState(0)
const navigate = useNavigate()
useEffect(() => {
loadPack(displayName, packId)
}, [])
useEffect(() => {
if (pack !== undefined) {
// Calculate editing access
if (
(user?.uid === pack.uid || (pack?.uid == 'me' && !user?.uid)) &&
categoryFilter === 0 &&
starredFilter === 0
) {
letMeEdit(true)
} else {
letMeEdit(false)
}
}
}, [user, pack])
// useEffect(() => {
// if (categoryFilter == 0) {
// setFilteredPack(pack)
// } else {
// setFilteredPack({
// ...pack,
// content: pack.content.filter((cards) => cards.category == categoryFilter),
// })
// }
// }, [categoryFilter, pack])
// The === is very important, since it must be a boolean, not an error object
if (error === true) {
return (
<div className="text-center">
<h1>404</h1>
<p>Sorry, this pack doesn't exist.</p>
</div>
)
}
if (error) {
return (
<div className="text-center">
<h1>403</h1>
<p>This pack exists, but hasn't been published.</p>
<code>{error.toString()}</code>
</div>
)
}
// Loading logic
// Also wait for the filteredPack to load
if (loading || pack?.uuid != packId) {
return (
<div className="text-center">
<h1>📎</h1>
<Spinner animation="grow" size="sm" />
</div>
)
}
function newCards() {
// Add new terms
addCards({
term: '',
definition: '',
category: 'default',
uuid: uuidv4(),
})
}
async function saveCards(editMode = false) {
startSaving(true)
await newPack(packId, pack)
if (editMode) {
navigate(`/edit/${pack.author}/${pack.uuid}`)
} else {
// This isn't a stopgap for async, it just shows the saving spinner,
// providing feedback to the user
// setTimeout(() => {
// startSaving(false)
// }, '200')
// Do you want to know what is a stopgap?
// Allows filters to work and not be weird
window.location.reload()
}
}
return (
<div id="packRoot" className="mx-auto">
<Navigation baseURL={`${pack.author}/${pack.uuid}`} currentPage="view" />
<div className="pack border border-2 p-4 rounded-3">
<Metadata />
<FlashcardView category={categoryFilter} />
<div className="d-flex justify-content-between align-items-center">
<div>
<Button
variant={theme.color}
size="sm"
className="me-3"
inline="true"
onClick={() => {
setStarredFilter(0)
setCategoryFilter(0)
filterPackStarred(0)
filterPackCategory(0)
}}
>
🔄
</Button>
<Form.Check
className="mt-3"
label={
starredFilter === 0 ? 'Starred Filter (Disabled)' : 'Starred Filter'
}
type="checkbox"
inline
onChange={(e) => {
filterPackStarred(e.target.checked)
// TODO: very very very complicated
// allow editing in filtered packs
// will require us to add uuids for each card
setStarredFilter(e.target.checked)
}}
checked={starredFilter}
/>
</div>
<Form.Select
className="w-25 mt-3"
onChange={(e) => {
let val = e.target.value
if (val === '0') val = 0
filterPackCategory(val)
// TODO: very very very complicated
// allow editing in filtered packs
// will require us to add uuids for each card
setCategoryFilter(val)
}}
value={categoryFilter}
>
<option value="0">All</option>
{Object.entries(pack.categories).map(([uuid, content]) => (
<option
key={uuid}
value={uuid}
style={{ backgroundColor: content.colors[1] }}
>
{content.name}
</option>
))}
</Form.Select>
</div>
<div id="packContentContainer" className="mt-3 border border-2 rounded-3">
{/* <AnimatePresence> */}
{pack.content.map((cards, index) => (
<Cards index={index} edit={false} key={cards.uuid + index} />
))}
{/* </AnimatePresence> */}
</div>
{canEdit && (
<div id="parentToolbar" className="d-flex justify-content-between fixed-bottom">
<ButtonGroup className="mx-auto fw-bold" id="bottomToolbar">
{/* <Button
variant="light"
onClick={() => {
setEditingAll(!editingAll)
}}
className="p-3"
>
{editingAll ? '✔️ Done' : '✏️ Edit All'}
</Button> */}
<Button
variant="light"
onClick={() => saveCards(false)}
className="p-3"
>
{saving ? (
<Spinner animation="border" variant="dark" size="sm" />
) : (
'💾 Save'
)}
</Button>
<Button variant="light" onClick={newCards} className="p-3">
➕ New
</Button>
{/* <LinkContainer to={`/edit/${pack.author}/${pack.uuid}`}> */}
<Button variant="light" onClick={() => saveCards(true)} className="p-3">
{saving ? 'Saving...' : '✏️ Edit'}
</Button>
{/* </LinkContainer> */}
</ButtonGroup>
</div>
)}
</div>
</div>
)
}
export default Pack