131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
import React from "react";
|
||
import { Panel } from "../UI/Panel";
|
||
import { Button } from "../UI/Button";
|
||
import { Skeleton } from "../UI/Loading";
|
||
import { supabase } from "../../supabaseClient";
|
||
|
||
export const StopWordsPanel = () => {
|
||
const [words, setWords] = React.useState([]);
|
||
const [newWord, setNewWord] = React.useState("");
|
||
const [isLoading, setIsLoading] = React.useState(true);
|
||
const [error, setError] = React.useState("");
|
||
const [deletingId, setDeletingId] = React.useState(null);
|
||
|
||
const loadWords = React.useCallback(async () => {
|
||
setIsLoading(true);
|
||
setError("");
|
||
const { data, error: fetchError } = await supabase
|
||
.from("stop_words")
|
||
.select("id, word, created_at")
|
||
.order("word", { ascending: true });
|
||
if (fetchError) {
|
||
setError(fetchError.message);
|
||
} else {
|
||
setWords(data || []);
|
||
}
|
||
setIsLoading(false);
|
||
}, []);
|
||
|
||
React.useEffect(() => { loadWords(); }, [loadWords]);
|
||
|
||
const handleAdd = async () => {
|
||
const trimmed = newWord.trim().toLowerCase();
|
||
if (!trimmed) return;
|
||
if (words.some((w) => w.word === trimmed)) {
|
||
setError("Такое слово уже есть");
|
||
return;
|
||
}
|
||
setError("");
|
||
const { error: insertError } = await supabase
|
||
.from("stop_words")
|
||
.insert({ word: trimmed });
|
||
if (insertError) {
|
||
setError(insertError.message);
|
||
return;
|
||
}
|
||
setNewWord("");
|
||
await loadWords();
|
||
};
|
||
|
||
const handleDelete = async (id) => {
|
||
setDeletingId(id);
|
||
const { error: deleteError } = await supabase
|
||
.from("stop_words")
|
||
.delete()
|
||
.eq("id", id);
|
||
if (deleteError) {
|
||
setError(deleteError.message);
|
||
} else {
|
||
await loadWords();
|
||
}
|
||
setDeletingId(null);
|
||
};
|
||
|
||
const handleKeyDown = (e) => {
|
||
if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
handleAdd();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Panel className="space-y-5 p-6">
|
||
<div>
|
||
<h2 className="text-lg font-semibold">Стоп-слова</h2>
|
||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||
Позиции с этими словами не показываются клиентам в карточке доставки.
|
||
Добавляйте слова-маркеры: «сверление», «обмер» и т.д.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<input
|
||
type="text"
|
||
value={newWord}
|
||
onChange={(e) => setNewWord(e.target.value)}
|
||
onKeyDown={handleKeyDown}
|
||
placeholder="Новое стоп-слово"
|
||
className="flex-1 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-2.5 text-sm !text-[var(--color-text)] outline-none transition focus:border-[var(--color-accent)]"
|
||
maxLength={100}
|
||
/>
|
||
<Button onClick={handleAdd} disabled={!newWord.trim()}>
|
||
Добавить
|
||
</Button>
|
||
</div>
|
||
|
||
{error && (
|
||
<p className="text-sm text-[var(--color-warning)]">{error}</p>
|
||
)}
|
||
|
||
{isLoading ? (
|
||
<div className="flex flex-wrap gap-2">
|
||
{Array.from({ length: 6 }).map((_, i) => (
|
||
<Skeleton key={i} className="w-20 h-8 rounded-full" />
|
||
))}
|
||
</div>
|
||
) : !words.length ? (
|
||
<p className="text-sm text-[var(--color-text-muted)]">Стоп-слов пока нет. Добавьте первое.</p>
|
||
) : (
|
||
<div className="flex flex-wrap gap-2">
|
||
{words.map((w) => (
|
||
<span
|
||
key={w.id}
|
||
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm !text-[var(--color-text)]"
|
||
>
|
||
{w.word}
|
||
<button
|
||
type="button"
|
||
disabled={deletingId === w.id}
|
||
onClick={() => handleDelete(w.id)}
|
||
className="ml-0.5 flex h-4 w-4 items-center justify-center rounded-full text-[var(--color-text-muted)] transition hover:bg-[var(--color-accent-soft)] hover:!text-[var(--color-danger)] disabled:opacity-40"
|
||
aria-label={`Удалить ${w.word}`}
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Panel>
|
||
);
|
||
}; |