136 lines
5.1 KiB
TypeScript
136 lines
5.1 KiB
TypeScript
'use client'
|
|
import { useState } from 'react'
|
|
import { DndContext, closestCenter, type DragEndEvent } from '@dnd-kit/core'
|
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
|
import KeywordRow from './KeywordRow'
|
|
import { useKeywords, useAddKeyword, useReorderKeywords } from '@/hooks/useKeywords'
|
|
|
|
export default function KeywordsTable() {
|
|
const { data: keywords, isLoading } = useKeywords()
|
|
const addKw = useAddKeyword()
|
|
const reorder = useReorderKeywords()
|
|
const [newTerm, setNewTerm] = useState('')
|
|
const [newWeight, setNewWeight] = useState('1')
|
|
const [batchText, setBatchText] = useState('')
|
|
const [showBatch, setShowBatch] = useState(false)
|
|
|
|
const handleDragEnd = ({ active, over }: DragEndEvent) => {
|
|
if (!over || active.id === over.id || !keywords) return
|
|
const ids = keywords.map((k) => k.id)
|
|
const from = ids.indexOf(Number(active.id))
|
|
const to = ids.indexOf(Number(over.id))
|
|
const newOrder = [...ids]
|
|
newOrder.splice(to, 0, newOrder.splice(from, 1)[0])
|
|
reorder.mutate(newOrder)
|
|
}
|
|
|
|
const handleBatchImport = () => {
|
|
const lines = batchText.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
lines.forEach((line) => {
|
|
const [term, weight] = line.split(':')
|
|
addKw.mutate({ term: term.trim(), weight: parseFloat(weight || '1') })
|
|
})
|
|
setBatchText('')
|
|
setShowBatch(false)
|
|
}
|
|
|
|
if (isLoading) return (
|
|
<div className="g-card p-5 space-y-3">
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
<div key={i} className="h-10 bg-g-raised rounded-lg animate-pulse" style={{ opacity: 1 - i * 0.2 }} />
|
|
))}
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Add new */}
|
|
<div className="g-card p-4">
|
|
<p className="text-xs text-g-faint mb-3 leading-relaxed">
|
|
Each target is a search label sent to auction sites.
|
|
Set an <span className="text-g-green">AI Description</span> on each target to filter lot titles semantically.
|
|
</p>
|
|
<div className="flex gap-2 flex-wrap">
|
|
<input
|
|
value={newTerm}
|
|
onChange={(e) => setNewTerm(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && newTerm.trim() && (addKw.mutate({ term: newTerm, weight: parseFloat(newWeight) }), setNewTerm(''), setNewWeight('1'))}
|
|
placeholder="Search term…"
|
|
className="g-input h-9 flex-1 min-w-44 text-sm"
|
|
/>
|
|
<input
|
|
value={newWeight}
|
|
onChange={(e) => setNewWeight(e.target.value)}
|
|
type="number" step="0.1"
|
|
placeholder="Weight"
|
|
className="g-input h-9 w-24 text-sm font-mono"
|
|
/>
|
|
<button
|
|
onClick={() => {
|
|
if (newTerm.trim()) {
|
|
addKw.mutate({ term: newTerm, weight: parseFloat(newWeight) })
|
|
setNewTerm('')
|
|
setNewWeight('1')
|
|
}
|
|
}}
|
|
className="g-btn-primary h-9 text-sm px-4"
|
|
>
|
|
+ Add
|
|
</button>
|
|
<button
|
|
onClick={() => setShowBatch(!showBatch)}
|
|
className="g-btn h-9 text-sm"
|
|
>
|
|
Batch
|
|
</button>
|
|
</div>
|
|
|
|
{showBatch && (
|
|
<div className="mt-3 space-y-2 pt-3 border-t border-g-border/40">
|
|
<p className="text-xs text-g-faint">One target per line. Format: <code className="text-g-muted">laptop:2</code></p>
|
|
<textarea
|
|
value={batchText}
|
|
onChange={(e) => setBatchText(e.target.value)}
|
|
placeholder={"laptop:2\nRTX 4090:3\niPhone 15"}
|
|
rows={4}
|
|
className="g-input text-sm font-mono resize-none"
|
|
/>
|
|
<button onClick={handleBatchImport} className="g-btn-primary text-sm">Import all</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="g-card overflow-hidden">
|
|
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
<SortableContext items={(keywords ?? []).map((k) => k.id)} strategy={verticalListSortingStrategy}>
|
|
<div className="overflow-x-auto">
|
|
<table className="g-table">
|
|
<thead>
|
|
<tr>
|
|
<th className="w-8"></th>
|
|
<th>Target label</th>
|
|
<th className="text-center">Weight</th>
|
|
<th>AI description</th>
|
|
<th>Price filter</th>
|
|
<th className="w-10"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(keywords ?? []).map((kw) => <KeywordRow key={kw.id} keyword={kw} />)}
|
|
</tbody>
|
|
</table>
|
|
{!(keywords ?? []).length && (
|
|
<div className="flex flex-col items-center justify-center py-12 text-g-faint gap-2">
|
|
<span className="text-3xl opacity-20">⌖</span>
|
|
<p className="text-sm">No targets yet — add one above</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|