702 lines
26 KiB
TypeScript
702 lines
26 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useRouter, useParams } from "next/navigation"
|
||
import Link from "next/link"
|
||
import {
|
||
getCollection,
|
||
addHanziToCollection,
|
||
removeHanziFromCollection,
|
||
removeMultipleHanziFromCollection,
|
||
searchHanziForCollection,
|
||
parseHanziList,
|
||
deleteCollection,
|
||
} from "@/actions/collections"
|
||
|
||
type Hanzi = {
|
||
id: string
|
||
simplified: string
|
||
pinyin: string | null
|
||
meaning: string | null
|
||
orderIndex: number
|
||
}
|
||
|
||
type Collection = {
|
||
id: string
|
||
name: string
|
||
description: string | null
|
||
isPublic: boolean
|
||
isGlobal: boolean
|
||
createdBy: string | null
|
||
hanziCount: number
|
||
hanzi: Hanzi[]
|
||
}
|
||
|
||
export default function CollectionDetailPage() {
|
||
const router = useRouter()
|
||
const params = useParams()
|
||
const collectionId = params.id as string
|
||
const [collection, setCollection] = useState<Collection | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
// Selection mode
|
||
const [selectionMode, setSelectionMode] = useState(false)
|
||
const [selectedHanziIds, setSelectedHanziIds] = useState<Set<string>>(new Set())
|
||
|
||
// Add hanzi modal
|
||
const [showAddModal, setShowAddModal] = useState(false)
|
||
const [addTab, setAddTab] = useState<"search" | "paste">("search")
|
||
|
||
// Search state
|
||
const [searchQuery, setSearchQuery] = useState("")
|
||
const [searchResults, setSearchResults] = useState<
|
||
Array<{
|
||
id: string
|
||
simplified: string
|
||
pinyin: string | null
|
||
meaning: string | null
|
||
inCollection: boolean
|
||
}>
|
||
>([])
|
||
const [searchLoading, setSearchLoading] = useState(false)
|
||
const [searchSelectedIds, setSearchSelectedIds] = useState<Set<string>>(new Set())
|
||
|
||
// Paste state
|
||
const [pasteInput, setPasteInput] = useState("")
|
||
const [parseResult, setParseResult] = useState<{
|
||
valid: boolean
|
||
found: Array<{ id: string; simplified: string; pinyin: string | null }>
|
||
notFound: string[]
|
||
duplicates: string[]
|
||
} | null>(null)
|
||
|
||
const [actionLoading, setActionLoading] = useState(false)
|
||
const [actionError, setActionError] = useState<string | null>(null)
|
||
|
||
useEffect(() => {
|
||
loadCollection()
|
||
}, [collectionId])
|
||
|
||
const loadCollection = async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
|
||
try {
|
||
const result = await getCollection(collectionId)
|
||
if (result.success && result.data) {
|
||
setCollection(result.data)
|
||
} else {
|
||
setError(result.message || "Failed to load collection")
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "An error occurred")
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleSearch = async () => {
|
||
if (!searchQuery.trim()) return
|
||
|
||
setSearchLoading(true)
|
||
try {
|
||
const result = await searchHanziForCollection(searchQuery, collectionId, 50, 0)
|
||
if (result.success && result.data) {
|
||
setSearchResults(result.data.hanzi)
|
||
}
|
||
} catch (err) {
|
||
console.error("Search failed:", err)
|
||
} finally {
|
||
setSearchLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleParseList = async () => {
|
||
if (!pasteInput.trim()) {
|
||
setActionError("Please enter hanzi list")
|
||
return
|
||
}
|
||
|
||
setActionLoading(true)
|
||
setActionError(null)
|
||
|
||
try {
|
||
const result = await parseHanziList(pasteInput)
|
||
if (result.success && result.data) {
|
||
setParseResult(result.data)
|
||
if (!result.data.valid) {
|
||
setActionError(`${result.data.notFound.length} hanzi not found in database`)
|
||
}
|
||
} else {
|
||
setActionError(result.message || "Failed to parse hanzi list")
|
||
}
|
||
} catch (err) {
|
||
setActionError(err instanceof Error ? err.message : "An error occurred")
|
||
} finally {
|
||
setActionLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleAddSelectedFromSearch = async () => {
|
||
if (searchSelectedIds.size === 0) return
|
||
|
||
setActionLoading(true)
|
||
setActionError(null)
|
||
|
||
try {
|
||
const result = await addHanziToCollection(collectionId, Array.from(searchSelectedIds))
|
||
if (result.success) {
|
||
setShowAddModal(false)
|
||
setSearchQuery("")
|
||
setSearchResults([])
|
||
setSearchSelectedIds(new Set())
|
||
await loadCollection()
|
||
} else {
|
||
setActionError(result.message || "Failed to add hanzi")
|
||
}
|
||
} catch (err) {
|
||
setActionError(err instanceof Error ? err.message : "An error occurred")
|
||
} finally {
|
||
setActionLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleAddFromPaste = async () => {
|
||
if (!parseResult || !parseResult.valid) {
|
||
setActionError("Please preview and fix any errors first")
|
||
return
|
||
}
|
||
|
||
setActionLoading(true)
|
||
setActionError(null)
|
||
|
||
try {
|
||
const hanziIds = parseResult.found.map((h) => h.id)
|
||
const result = await addHanziToCollection(collectionId, hanziIds)
|
||
if (result.success) {
|
||
setShowAddModal(false)
|
||
setPasteInput("")
|
||
setParseResult(null)
|
||
await loadCollection()
|
||
} else {
|
||
setActionError(result.message || "Failed to add hanzi")
|
||
}
|
||
} catch (err) {
|
||
setActionError(err instanceof Error ? err.message : "An error occurred")
|
||
} finally {
|
||
setActionLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleRemoveSingle = async (hanziId: string) => {
|
||
if (!confirm("Remove this hanzi from the collection?")) return
|
||
|
||
setActionLoading(true)
|
||
try {
|
||
const result = await removeHanziFromCollection(collectionId, hanziId)
|
||
if (result.success) {
|
||
await loadCollection()
|
||
}
|
||
} catch (err) {
|
||
console.error("Remove failed:", err)
|
||
} finally {
|
||
setActionLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleRemoveSelected = async () => {
|
||
if (selectedHanziIds.size === 0) return
|
||
if (!confirm(`Remove ${selectedHanziIds.size} hanzi from the collection?`)) return
|
||
|
||
setActionLoading(true)
|
||
try {
|
||
const result = await removeMultipleHanziFromCollection(collectionId, Array.from(selectedHanziIds))
|
||
if (result.success) {
|
||
setSelectionMode(false)
|
||
setSelectedHanziIds(new Set())
|
||
await loadCollection()
|
||
}
|
||
} catch (err) {
|
||
console.error("Remove failed:", err)
|
||
} finally {
|
||
setActionLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleDeleteCollection = async () => {
|
||
if (!confirm("Delete this collection? This action cannot be undone.")) return
|
||
|
||
setActionLoading(true)
|
||
try {
|
||
const result = await deleteCollection(collectionId)
|
||
if (result.success) {
|
||
router.push("/collections")
|
||
} else {
|
||
setActionError(result.message || "Failed to delete collection")
|
||
}
|
||
} catch (err) {
|
||
setActionError(err instanceof Error ? err.message : "An error occurred")
|
||
} finally {
|
||
setActionLoading(false)
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (error || !collection) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<p className="text-red-600 dark:text-red-400 mb-4">{error || "Collection not found"}</p>
|
||
<Link href="/collections" className="text-blue-600 hover:underline">
|
||
Back to Collections
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const canModify = !collection.isGlobal
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||
<nav className="bg-white dark:bg-gray-800 shadow">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex justify-between h-16 items-center">
|
||
<div className="flex items-center space-x-8">
|
||
<Link href="/dashboard">
|
||
<h1 className="text-xl font-bold text-gray-900 dark:text-white cursor-pointer">
|
||
MemoHanzi <span className="text-sm font-normal text-gray-500">记汉字</span>
|
||
</h1>
|
||
</Link>
|
||
<Link
|
||
href="/collections"
|
||
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||
>
|
||
Collections
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<div className="flex justify-between items-start mb-8">
|
||
<div>
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||
{collection.name}
|
||
</h2>
|
||
{collection.isGlobal && (
|
||
<span className="text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded">
|
||
HSK
|
||
</span>
|
||
)}
|
||
{collection.isPublic && !collection.isGlobal && (
|
||
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
|
||
Public
|
||
</span>
|
||
)}
|
||
</div>
|
||
{collection.description && (
|
||
<p className="text-gray-600 dark:text-gray-400 mb-2">{collection.description}</p>
|
||
)}
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
{collection.hanziCount} hanzi
|
||
</p>
|
||
</div>
|
||
|
||
{canModify && (
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setShowAddModal(true)}
|
||
disabled={actionLoading}
|
||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
||
>
|
||
Add Hanzi
|
||
</button>
|
||
{collection.hanziCount > 0 && (
|
||
<button
|
||
onClick={() => setSelectionMode(!selectionMode)}
|
||
className="bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700"
|
||
>
|
||
{selectionMode ? "Cancel" : "Select"}
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={handleDeleteCollection}
|
||
disabled={actionLoading}
|
||
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 disabled:bg-gray-400"
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{selectionMode && selectedHanziIds.size > 0 && (
|
||
<div className="bg-blue-100 dark:bg-blue-900 p-4 rounded-lg mb-6 flex justify-between items-center">
|
||
<span className="text-blue-900 dark:text-blue-200 font-medium">
|
||
{selectedHanziIds.size} selected
|
||
</span>
|
||
<button
|
||
onClick={handleRemoveSelected}
|
||
disabled={actionLoading}
|
||
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 disabled:bg-gray-400"
|
||
>
|
||
Remove Selected
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{actionError && (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
|
||
{actionError}
|
||
</div>
|
||
)}
|
||
|
||
{/* Hanzi Grid */}
|
||
{collection.hanzi.length === 0 ? (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
|
||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||
This collection is empty. Add some hanzi to get started.
|
||
</p>
|
||
{canModify && (
|
||
<button
|
||
onClick={() => setShowAddModal(true)}
|
||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
||
>
|
||
Add Hanzi
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||
{collection.hanzi.map((hanzi) => (
|
||
<div
|
||
key={hanzi.id}
|
||
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-4 relative ${
|
||
selectedHanziIds.has(hanzi.id) ? "ring-2 ring-blue-600" : ""
|
||
}`}
|
||
>
|
||
{selectionMode && (
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedHanziIds.has(hanzi.id)}
|
||
onChange={(e) => {
|
||
const newSet = new Set(selectedHanziIds)
|
||
if (e.target.checked) {
|
||
newSet.add(hanzi.id)
|
||
} else {
|
||
newSet.delete(hanzi.id)
|
||
}
|
||
setSelectedHanziIds(newSet)
|
||
}}
|
||
className="absolute top-2 left-2"
|
||
/>
|
||
)}
|
||
{!selectionMode && canModify && (
|
||
<button
|
||
onClick={() => handleRemoveSingle(hanzi.id)}
|
||
disabled={actionLoading}
|
||
className="absolute top-2 right-2 text-red-600 hover:text-red-800 disabled:text-gray-400"
|
||
title="Remove"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
<div className="text-center">
|
||
<div className="text-4xl mb-2">{hanzi.simplified}</div>
|
||
{hanzi.pinyin && (
|
||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||
{hanzi.pinyin}
|
||
</div>
|
||
)}
|
||
{hanzi.meaning && (
|
||
<div className="text-xs text-gray-500 dark:text-gray-500 line-clamp-2">
|
||
{hanzi.meaning}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</main>
|
||
|
||
{/* Add Hanzi Modal */}
|
||
{showAddModal && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-hidden">
|
||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||
<div className="flex justify-between items-center p-4">
|
||
<h3 className="text-xl font-bold text-gray-900 dark:text-white">Add Hanzi</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowAddModal(false)
|
||
setSearchQuery("")
|
||
setSearchResults([])
|
||
setSearchSelectedIds(new Set())
|
||
setPasteInput("")
|
||
setParseResult(null)
|
||
setActionError(null)
|
||
}}
|
||
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="flex border-t border-gray-200 dark:border-gray-700">
|
||
<button
|
||
onClick={() => setAddTab("search")}
|
||
className={`flex-1 py-3 px-6 text-center font-medium ${
|
||
addTab === "search"
|
||
? "border-b-2 border-blue-600 text-blue-600 dark:text-blue-400"
|
||
: "text-gray-600 dark:text-gray-400"
|
||
}`}
|
||
>
|
||
Search
|
||
</button>
|
||
<button
|
||
onClick={() => setAddTab("paste")}
|
||
className={`flex-1 py-3 px-6 text-center font-medium ${
|
||
addTab === "paste"
|
||
? "border-b-2 border-blue-600 text-blue-600 dark:text-blue-400"
|
||
: "text-gray-600 dark:text-gray-400"
|
||
}`}
|
||
>
|
||
Paste
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
||
{actionError && (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||
{actionError}
|
||
</div>
|
||
)}
|
||
|
||
{addTab === "search" ? (
|
||
<div className="space-y-4">
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||
placeholder="Search by character, pinyin, or meaning..."
|
||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
<button
|
||
onClick={handleSearch}
|
||
disabled={searchLoading}
|
||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
||
>
|
||
{searchLoading ? "..." : "Search"}
|
||
</button>
|
||
</div>
|
||
|
||
{searchSelectedIds.size > 0 && (
|
||
<div className="flex justify-between items-center bg-blue-50 dark:bg-blue-900/20 p-3 rounded">
|
||
<span className="text-sm text-blue-900 dark:text-blue-200">
|
||
{searchSelectedIds.size} selected
|
||
</span>
|
||
<button
|
||
onClick={() => setSearchSelectedIds(new Set())}
|
||
className="text-sm text-blue-600 hover:underline"
|
||
>
|
||
Clear
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{searchResults.length > 0 && (
|
||
<div className="space-y-2">
|
||
{searchResults.map((hanzi) => (
|
||
<label
|
||
key={hanzi.id}
|
||
className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||
hanzi.inCollection
|
||
? "opacity-50 cursor-not-allowed"
|
||
: "border-gray-300 dark:border-gray-600"
|
||
}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={searchSelectedIds.has(hanzi.id)}
|
||
onChange={(e) => {
|
||
const newSet = new Set(searchSelectedIds)
|
||
if (e.target.checked) {
|
||
newSet.add(hanzi.id)
|
||
} else {
|
||
newSet.delete(hanzi.id)
|
||
}
|
||
setSearchSelectedIds(newSet)
|
||
}}
|
||
disabled={hanzi.inCollection}
|
||
/>
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-2xl">{hanzi.simplified}</span>
|
||
{hanzi.pinyin && (
|
||
<span className="text-gray-600 dark:text-gray-400">
|
||
{hanzi.pinyin}
|
||
</span>
|
||
)}
|
||
{hanzi.inCollection && (
|
||
<span className="text-xs bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded">
|
||
Already in collection
|
||
</span>
|
||
)}
|
||
</div>
|
||
{hanzi.meaning && (
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
{hanzi.meaning}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</label>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<textarea
|
||
value={pasteInput}
|
||
onChange={(e) => {
|
||
setPasteInput(e.target.value)
|
||
setParseResult(null)
|
||
setActionError(null)
|
||
}}
|
||
placeholder="Paste hanzi here (newline, comma, or space separated) Example: 好 爱 你 or: 好, 爱, 你"
|
||
rows={6}
|
||
className="block w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono text-sm"
|
||
/>
|
||
<button
|
||
onClick={handleParseList}
|
||
disabled={actionLoading || !pasteInput.trim()}
|
||
className="mt-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 disabled:bg-gray-400"
|
||
>
|
||
{actionLoading ? "Parsing..." : "Preview"}
|
||
</button>
|
||
</div>
|
||
|
||
{parseResult && (
|
||
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg p-4">
|
||
<h4 className="font-semibold mb-3 text-gray-900 dark:text-white">
|
||
Preview Results
|
||
</h4>
|
||
|
||
{parseResult.found.length > 0 && (
|
||
<div className="mb-3">
|
||
<p className="text-sm font-medium text-green-700 dark:text-green-400 mb-2">
|
||
✓ Found: {parseResult.found.length} hanzi
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{parseResult.found.slice(0, 30).map((h) => (
|
||
<span
|
||
key={h.id}
|
||
className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded text-sm"
|
||
>
|
||
{h.simplified} {h.pinyin ? `(${h.pinyin})` : ""}
|
||
</span>
|
||
))}
|
||
{parseResult.found.length > 30 && (
|
||
<span className="text-gray-600 dark:text-gray-400 text-sm">
|
||
... and {parseResult.found.length - 30} more
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{parseResult.notFound.length > 0 && (
|
||
<div className="mb-3">
|
||
<p className="text-sm font-medium text-red-700 dark:text-red-400 mb-2">
|
||
✗ Not found: {parseResult.notFound.length} hanzi
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{parseResult.notFound.map((char, i) => (
|
||
<span
|
||
key={i}
|
||
className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 px-2 py-1 rounded text-sm"
|
||
>
|
||
{char}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{parseResult.duplicates.length > 0 && (
|
||
<div>
|
||
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-400 mb-2">
|
||
⚠ Duplicates in input: {parseResult.duplicates.length}
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{parseResult.duplicates.map((char, i) => (
|
||
<span
|
||
key={i}
|
||
className="bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 px-2 py-1 rounded text-sm"
|
||
>
|
||
{char}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="border-t border-gray-200 dark:border-gray-700 p-4 flex justify-end gap-2">
|
||
<button
|
||
onClick={() => {
|
||
setShowAddModal(false)
|
||
setSearchQuery("")
|
||
setSearchResults([])
|
||
setSearchSelectedIds(new Set())
|
||
setPasteInput("")
|
||
setParseResult(null)
|
||
setActionError(null)
|
||
}}
|
||
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={addTab === "search" ? handleAddSelectedFromSearch : handleAddFromPaste}
|
||
disabled={
|
||
actionLoading ||
|
||
(addTab === "search"
|
||
? searchSelectedIds.size === 0
|
||
: !parseResult || !parseResult.valid)
|
||
}
|
||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
||
>
|
||
{actionLoading
|
||
? "Adding..."
|
||
: addTab === "search"
|
||
? `Add Selected (${searchSelectedIds.size})`
|
||
: `Add ${parseResult?.found.length || 0} Hanzi`}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|