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

147 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}