/* 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 (
);
};
// アワード(ラベル)用のツールチップ(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}件
{" "}
を表示
{/* ラベルで絞り込み */}
ラベルで絞り込み
チェックしたラベルを
含む
バナーのみ表示します。
{/* よく使う項目を上に */}
{[
"cost",
"imp",
"click",
"cvCount",
"ctr",
"transition",
"cvr",
"cpo",
"cpc",
"cpm",
].map((k) => (
))}
);
}
window.Cmp = window.Cmp || {};
window.Cmp.FilterModal = FilterModal;
})();