/* components/FilterModal.jsx */ (() => { const METRIC_LABELS = { cpo: "CPO", ctr: "CTR", cvr: "CVR", cvCount: "CV数", cost: "広告費", click: "クリック", imp: "IMP", transition: "LP遷移率", cpc: "CPC", cpm: "CPM", }; const METRIC_UNITS = { cpo: "円", ctr: "%", cvr: "%", cvCount: "件", cost: "円", click: "", imp: "", transition: "%", cpc: "円", cpm: "円", }; const MetricRow = ({ k, v, onChange }) => { const label = METRIC_LABELS[k] || k; const unit = METRIC_UNITS[k] || ""; return (
{label}
範囲で絞り込み
最小
onChange(k, { ...v, min: e.target.value }) } inputMode="decimal" className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 focus:outline-none focus:ring-2 focus:ring-emerald-600 text-sm" placeholder="例:100" />
最大
onChange(k, { ...v, max: e.target.value }) } inputMode="decimal" className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 focus:outline-none focus:ring-2 focus:ring-emerald-600 text-sm" placeholder="例:500" />
{unit}
); }; // アワード(ラベル)用のツールチップ(body に portal) // アワード(ラベル)用のツールチップ(body に portal) const AwardTipLabel = ({ id, def }) => { const ref = React.useRef(null); const tipRef = React.useRef(null); const [open, setOpen] = React.useState(false); const [anchorRect, setAnchorRect] = React.useState(null); const [pos, setPos] = React.useState({ left: 0, top: 0 }); const computeAnchor = () => { const el = ref.current; if (!el) return null; return el.getBoundingClientRect(); }; const setInitialPos = (r) => { if (!r) return; const margin = 12; const vw = window.innerWidth || 1024; const tipW = 360; let left = r.left; if (left + tipW + margin > vw) left = Math.max(margin, vw - tipW - margin); if (left < margin) left = margin; setPos({ left, top: r.bottom + 10 }); }; const show = () => { const r = computeAnchor(); setAnchorRect(r); setInitialPos(r); setOpen(true); }; const hide = () => setOpen(false); React.useLayoutEffect(() => { if (!open) return; const r = anchorRect || computeAnchor(); const el = tipRef.current; if (!r || !el) return; const margin = 12; const vw = window.innerWidth || 1024; const vh = window.innerHeight || 768; const rect = el.getBoundingClientRect(); const tipW = rect.width || 360; const tipH = rect.height || 200; let left = r.left; if (left + tipW + margin > vw) left = Math.max(margin, vw - tipW - margin); if (left < margin) left = margin; let top = r.bottom + 10; if (top + tipH + margin > vh) top = r.top - tipH - 10; if (top < margin) top = margin; if (top + tipH + margin > vh) top = Math.max(margin, vh - tipH - margin); setPos({ left, top }); }, [open, anchorRect]); const Tip = () => (
setOpen(true)} onMouseLeave={hide} > {/* アイコン + タイトルのみセンタリング */}
{def.icon ? (
) : null}
{def.title || def.label || id}
{/* 本文・条件は左寄せ */}
{def.desc ? (
{def.desc}
) : null} {def.cond ? (
条件:{def.cond}
) : null}
); return ( {def.label || id} {open && ReactDOM && ReactDOM.createPortal ? ReactDOM.createPortal(, document.body) : null} ); }; function FilterModal(props) { const { open, onClose, filters, setFilters, awardFilter, setAwardFilter, awardDefs, matchedCount, totalCount, } = props; if (!open) return null; const onRowChange = (k, next) => { setFilters((prev) => ({ ...prev, [k]: next })); }; const clearAll = () => { const cleared = {}; Object.keys(filters || {}).forEach( (k) => (cleared[k] = { min: "", max: "" }) ); setFilters(cleared); if (setAwardFilter) { setAwardFilter({ click: false, conversion: false, market: false, efficiency: false, funnel: false, outlier: false, }); } }; // UX: クリエイティブ担当がよく使う「まずCVありに寄せる」ショートカット const setHasCV = () => { setFilters((prev) => ({ ...prev, cvCount: { ...(prev.cvCount || { min: "", max: "" }), min: "1", }, })); }; return (
フィルター
{totalCount}件中{" "} {matchedCount}件 {" "} を表示
{/* ラベルで絞り込み */}
ラベルで絞り込み
チェックしたラベルを 含む バナーのみ表示します。
{[ "click", "conversion", "market", "efficiency", "funnel", "outlier", ].map((id) => { const def = (awardDefs && awardDefs[id]) || { label: id, dotClass: "bg-white/40", }; const checked = !!( awardFilter && awardFilter[id] ); return ( ); })}
{/* よく使う項目を上に */} {[ "cost", "imp", "click", "cvCount", "ctr", "transition", "cvr", "cpo", "cpc", "cpm", ].map((k) => ( ))}
); } window.Cmp = window.Cmp || {}; window.Cmp.FilterModal = FilterModal; })();