thread-master/apps/web/src/components/radar/QuickWatchStart.tsx

147 lines
4.6 KiB
TypeScript
Raw Normal View History

2026-08-19 07:37:44 +00:00
import { useEffect, useState } from "react";
import { SERVICE_AREAS } from "./serviceAreas";
import { Button, Textarea } from "../ui";
import { useRepos } from "../../data/DataContext";
import type { Brand, BrandProduct } from "../../domain/types";
import { useI18n } from "../../i18n/I18nContext";
import { useFormatApiError } from "../../lib/apiErrors";
import { expandIncludeTerms } from "../../lib/threadsTerm";
import { ProductWatchForm } from "./ProductWatchForm";
function splitTerms(raw: string): string[] {
return raw
.split(/[\n,、]/)
.map((s) => s.trim())
.filter(Boolean);
}
type Props = {
/** firstSweepTriggered後端已排第一輪前端不該再叫使用者按「立即巡邏」。 */
onCreated: (result: { firstSweepTriggered: boolean }) => void;
};
/** 今日空狀態:關鍵字就能開工,品牌/產品是選項。 */
export function QuickWatchStart({ onCreated }: Props) {
const { t } = useI18n();
const repos = useRepos();
const formatError = useFormatApiError();
const [terms, setTerms] = useState("");
const [regions, setRegions] = useState<string[]>([]);
const [brandId, setBrandId] = useState("");
const [productId, setProductId] = useState("");
const [brands, setBrands] = useState<Brand[]>([]);
const [products, setProducts] = useState<BrandProduct[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const scout = (repos as { scout?: { listBrands: () => Promise<Brand[]>; listProducts: (id: string) => Promise<BrandProduct[]> } }).scout;
useEffect(() => {
if (!scout) return;
void scout.listBrands().then(setBrands).catch(() => setBrands([]));
}, [scout]);
useEffect(() => {
if (!scout || !brandId) {
setProducts([]);
return;
}
void scout.listProducts(brandId).then(setProducts).catch(() => setProducts([]));
}, [scout, brandId]);
async function submit() {
const nextTerms = expandIncludeTerms(splitTerms(terms));
if (!nextTerms.length) {
setError(t("radar.watches.threadsRequired"));
return;
}
if ((brandId && !productId) || (!brandId && productId)) {
setError(t("radar.watches.needBrandProductShort"));
return;
}
setBusy(true);
setError("");
try {
const created = await repos.radar.createWatch({
terms: nextTerms,
regions,
enabled: true,
brand_id: brandId || undefined,
product_id: productId || undefined,
});
onCreated({ firstSweepTriggered: Boolean(created.first_sweep_triggered) });
} catch (e) {
setError(formatError(e));
} finally {
setBusy(false);
}
}
return (
<form
className="hb-radar-form hb-stack"
onSubmit={(e) => {
e.preventDefault();
void submit();
}}
>
<p className="hb-radar-section__hint">{t("radar.start.hint")}</p>
<Textarea
name="radar-start-terms"
label={t("radar.start.terms")}
hint={t("radar.watches.termsHint")}
rows={3}
required
value={terms}
onChange={(e) => setTerms(e.target.value)}
placeholder={t("radar.start.termsPh")}
/>
{brands.length ? (
<ProductWatchForm
brands={brands}
products={products}
brandId={brandId}
productId={productId}
onBrandChange={(id) => {
setBrandId(id);
setProductId("");
}}
onProductChange={(id) => {
setProductId(id);
const product = products.find((p) => p.id === id);
if (!product || terms.trim()) return;
const seeded = [...product.pain_points, ...product.match_tags].filter(Boolean);
if (seeded.length) setTerms(seeded.join("\n"));
}}
/>
) : null}
<p className="hb-radar-section__hint">{t("radar.start.regions")}</p>
<div className="hb-radar-area-grid">
{SERVICE_AREAS.map((area) => {
const on = regions.includes(area.code);
return (
<button
key={area.code}
type="button"
aria-pressed={on}
className={`hb-radar-chip${on ? " is-active" : ""}`}
onClick={() =>
setRegions((cur) => (on ? cur.filter((c) => c !== area.code) : [...cur, area.code]))
}
>
{area.label}
</button>
);
})}
</div>
{error ? (
<p className="hb-form-error" role="alert">
{error}
</p>
) : null}
<Button type="submit" disabled={busy}>
{busy ? t("radar.start.saving") : t("radar.start.submit")}
</Button>
</form>
);
}