sto-k-odnomu/admin/src/components/PackCardsManager.tsx
2026-01-06 23:12:36 +03:00

209 lines
No EOL
7.5 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { cardsApi } from '@/api/cards'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'
import { Search, Check } from 'lucide-react'
interface PackCardsManagerProps {
currentCardIds: string[]
onCardsChange: (addIds: string[], removeIds: string[]) => void
disabled?: boolean
}
export function PackCardsManager({
currentCardIds,
onCardsChange,
disabled = false,
}: PackCardsManagerProps) {
const [search, setSearch] = useState('')
const [selectedCards, setSelectedCards] = useState<Set<string>>(new Set())
const [removedCards, setRemovedCards] = useState<Set<string>>(new Set())
// Load all cards with search
const { data: cardsData, isLoading } = useQuery({
queryKey: ['cards', 1, 100, search], // Limit to 100 (backend max)
queryFn: () => cardsApi.getCards({ page: 1, limit: 100, search }),
enabled: !disabled,
})
// Initialize selected cards from currentCardIds when it changes
useEffect(() => {
const initialSelected = new Set<string>(
currentCardIds.filter((id) => !removedCards.has(id.toString()))
)
setSelectedCards(initialSelected)
// Reset removed cards when currentCardIds changes (e.g., when opening dialog)
setRemovedCards(new Set())
}, [currentCardIds.join(',')]) // Use join to detect array changes
// Calculate which cards to add/remove when selection changes
useEffect(() => {
const currentlySelected = Array.from(selectedCards)
const removed = Array.from(removedCards)
// Cards to add: selected but not in currentCardIds and not removed
const toAdd = currentlySelected.filter(
(id) => !currentCardIds.includes(id) && !removed.includes(id)
)
// Cards to remove: in removedCards
const toRemove = removed.filter((id) => currentCardIds.includes(id))
if (toAdd.length > 0 || toRemove.length > 0) {
onCardsChange(toAdd, toRemove)
} else if (selectedCards.size > 0 || removedCards.size > 0) {
// Also notify if selection was cleared
onCardsChange([], [])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedCards.size, removedCards.size, currentCardIds.join(',')])
const handleToggleCard = (cardId: string | number) => {
if (disabled) return
const cardIdStr = String(cardId)
const isCurrentlySelected = selectedCards.has(cardIdStr) && !removedCards.has(cardIdStr)
const isInCurrentPack = currentCardIds.includes(cardIdStr)
if (isCurrentlySelected) {
// Deselect card
const newSelected = new Set(selectedCards)
newSelected.delete(cardIdStr)
setSelectedCards(newSelected)
// If it was in current pack, mark as removed
if (isInCurrentPack) {
setRemovedCards((prev) => new Set([...prev, cardIdStr]))
}
} else {
// Select card
const newSelected = new Set(selectedCards)
newSelected.add(cardIdStr)
setSelectedCards(newSelected)
// If it was marked as removed, unmark it
if (removedCards.has(cardIdStr)) {
setRemovedCards((prev) => {
const newRemoved = new Set(prev)
newRemoved.delete(cardIdStr)
return newRemoved
})
}
}
}
const isCardSelected = (cardId: string | number) => {
const cardIdStr = String(cardId)
return selectedCards.has(cardIdStr) && !removedCards.has(cardIdStr)
}
const isCardInCurrentPack = (cardId: string | number) => {
return currentCardIds.includes(String(cardId))
}
const allCards = cardsData?.items || []
const filteredCards = search
? allCards.filter(
(card) =>
card.original?.toLowerCase().includes(search.toLowerCase()) ||
card.translation?.toLowerCase().includes(search.toLowerCase()) ||
card.mnemo?.toLowerCase().includes(search.toLowerCase())
)
: allCards
return (
<div className="space-y-4">
<div className="space-y-2">
<Label>Manage Cards in Pack</Label>
<div className="flex items-center space-x-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search cards by original, translation, or mnemo..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-sm"
disabled={disabled}
/>
</div>
<p className="text-sm text-muted-foreground">
Selected cards: {selectedCards.size - removedCards.size} / {allCards.length}
</p>
</div>
{isLoading ? (
<div className="text-center py-4">Loading cards...</div>
) : (
<div className="border rounded-lg max-h-[400px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12"></TableHead>
<TableHead>ID</TableHead>
<TableHead>Original</TableHead>
<TableHead>Translation</TableHead>
<TableHead>Mnemo</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCards.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No cards found
</TableCell>
</TableRow>
) : (
filteredCards.map((card) => {
const cardIdStr = card.id ? String(card.id) : `temp-${card.original}`
const selected = isCardSelected(cardIdStr)
const inCurrentPack = isCardInCurrentPack(cardIdStr)
const newlyRemoved = removedCards.has(cardIdStr)
return (
<TableRow
key={card.id || `temp-${card.original}`}
className={selected ? 'bg-muted/50' : ''}
onClick={() => handleToggleCard(cardIdStr)}
>
<TableCell>
{selected ? (
<Check className="h-4 w-4 text-primary" />
) : (
<div className="h-4 w-4 border rounded" />
)}
</TableCell>
<TableCell className="font-mono text-sm">{card.id || '-'}</TableCell>
<TableCell className="font-medium">{card.original}</TableCell>
<TableCell>{card.translation}</TableCell>
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell>
<TableCell>
{newlyRemoved ? (
<Badge variant="destructive">Removed</Badge>
) : selected && inCurrentPack ? (
<Badge variant="default">In Pack</Badge>
) : selected ? (
<Badge variant="secondary">Selected</Badge>
) : inCurrentPack ? (
<Badge variant="outline">In Pack</Badge>
) : null}
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</div>
)}
</div>
)
}