s.n ? "done" : "todo")} />}
);
})}
);
}
/* =========================================================
v2: Step 1
========================================================= */
function StepOne({ state, set, onNext, errors }) {
const { server, bottle, plan } = state;
const sObj = SERVERS.find((s) => s.id === server);
const bObj = BOTTLES.find((b) => b.id === bottle);
const supportFee = getSupportMonthly(server, plan);
const bottleFee = getWaterPrice(bottle, plan);
const kosodate = plan === "子育てアクアプラン" ? 550 : 0;
const monthly = supportFee !== null && bottleFee !== null ? supportFee + bottleFee * 4 - kosodate : null;
const canProceed = server && bottle && plan;
// ヘラルボニー選択時はプランを限定
const allowedPlanIds = isHeralbony(server)
? ["2年割プラン", "子育てアクアプラン"]
: null;
const visiblePlans = allowedPlanIds
? PLANS.filter((p) => allowedPlanIds.includes(p.id))
: PLANS;
// 不可プランが選択中ならクリア
useEffect(() => {
if (allowedPlanIds && plan && !allowedPlanIds.includes(plan)) {
set({ plan: null });
}
}, [server]);
return (
{SERVERS.map((item) => (
set({ server: item.id })} />
))}
{errors.server && {errors.server}
}
{BOTTLES.map((item) => (
set({ bottle: item.id })} />
))}
{errors.bottle && {errors.bottle}
}
{allowedPlanIds && (
ヘラルボニーデザインサーバーは「2年割プラン」「子育てアクアプラン」よりお選びいただけます。
)}
{visiblePlans.map((item) => (
set({ plan: item.id })} />
))}
{errors.plan && {errors.plan}
}
あんしんサポート料
{supportFee !== null ? <>{yen(supportFee)}円 / 月> : —}
{(plan === "2年割プラン" || plan === "子育てアクアプラン") && supportFee !== null && (
2年割プラン適用
)}
お水料金(1本)
{bottleFee !== null ? <>{yen(bottleFee)}円 / 本> : —}
{(plan === "2年割プラン" || plan === "子育てアクアプラン") && bottleFee !== null && (
割引価格適用
)}
月額目安(4本/月想定)
{monthly !== null ? <>{yen(monthly)}円 / 月> : —}
{plan === "子育てアクアプラン" && monthly !== null && (
子育て割 −{yen(550)}円
)}
{plan === "子育てアクアプラン" && (
★
子育てアクアプラン適用中は、合計金額より毎月 550円(税込) 引きとなります。
)}
{!canProceed && (
サーバー・ボトル・プランをお選びいただくと単価が表示されます。
)}
{!canProceed &&
3項目すべてをご選択ください
}
);
}
/* =========================================================
v2: Step 2
========================================================= */
function StepTwo({ state, set, onBack, onSubmit, initialErrors = {} }) {
const [errors, setErrors] = useState(initialErrors);
const [submitting, setSubmitting] = useState(false);
const [zipLoading, setZipLoading] = useState(false);
const [termsOpen, setTermsOpen] = useState(false);
const sObj = SERVERS.find((s) => s.id === state.server);
const bObj = BOTTLES.find((b) => b.id === state.bottle);
const pObj = PLANS.find((p) => p.id === state.plan);
const supportFee = getSupportMonthly(state.server, state.plan);
const bottleFee = getWaterPrice(state.bottle, state.plan);
const kosodate = state.plan === "子育てアクアプラン" ? 550 : 0;
const fetchAddress = async (zip) => {
if (zip.length !== 7) return;
setZipLoading(true);
try {
const res = await fetch(`https://zipcloud.ibsnet.co.jp/api/search?zipcode=${zip}`);
const data = await res.json();
if (data.results) {
const r = data.results[0];
set({ address: `${r.address1}${r.address2}${r.address3}` });
}
} catch {}
finally { setZipLoading(false); }
};
const validate = () => {
const e = {};
if (!state.name) e.name = "お名前をご入力ください";
if (!state.kana) e.kana = "フリガナをご入力ください";
if (!state.tel) e.tel = "電話番号をご入力ください";
else if (!/^[0-9\-]{10,}$/.test(state.tel)) e.tel = "正しい電話番号をご入力ください";
if (!state.email) e.email = "メールアドレスをご入力ください";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(state.email)) e.email = "メール形式が正しくありません";
if (!state.contact_method) e.contact_method = "ご希望の連絡方法をお選びください";
if (!state.zipcode || state.zipcode.length !== 7) e.zipcode = "7桁でご入力ください";
if (!state.address) e.address = "住所をご入力ください";
else if (!/[0-90-9一二三四五六七八九十百千]/.test(state.address)) e.address = "番地(数字)までご入力ください";
if (!state.agree) e.agree = "規約にご同意ください";
return e;
};
const handleSubmit = async (ev) => {
ev.preventDefault();
const e = validate();
setErrors(e);
if (Object.keys(e).length) {
const firstKey = Object.keys(e)[0];
const el = document.querySelector(`[data-field="${firstKey}"]`);
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
return;
}
setSubmitting(true);
const form = document.createElement('form');
form.method = 'POST';
form.action = 'submit.php';
const fields = {
token: window.__CSRF_TOKEN || '',
server: state.server || '',
bottle_size: state.bottle || '',
plan: state.plan || '',
name: state.name || '',
kana: state.kana || '',
company: state.company || '',
tel: state.tel || '',
email: state.email || '',
contact_method: state.contact_method|| '',
zipcode: state.zipcode || '',
address: state.address || '',
building: state.building || '',
agree: state.agree ? '1' : '',
};
Object.entries(fields).forEach(([k, v]) => {
const inp = document.createElement('input');
inp.type = 'hidden'; inp.name = k; inp.value = v;
form.appendChild(inp);
});
document.body.appendChild(form);
sessionStorage.removeItem("apply_v2_state");
sessionStorage.removeItem("apply_v2_step");
form.submit();
};
return (
);
}
/* =========================================================
v2: Done
========================================================= */
function Done({ state, onReset }) {
const sObj = SERVERS.find((s) => s.id === state.server);
const bObj = BOTTLES.find((b) => b.id === state.bottle);
const pObj = PLANS.find((p) => p.id === state.plan);
return (
Application Received
お申込みありがとうございます
{state.name && <>{state.name} 様、>}
内容を確認のうえ、担当者よりご連絡いたします。
通常 1〜2 営業日以内にご連絡いたします。
お申込み内容
{sObj && (<>- サーバー
- {sObj.name}
>)}
{bObj && (<>- ボトル
- {bObj.name}
>)}
{pObj && (<>- プラン
- {pObj.name}
>)}
{state.email && (<>- メール
- {state.email}
>)}
お電話でのお問い合わせ
0120-074-032
平日 9:00 – 17:30
);
}
/* =========================================================
v2: App
========================================================= */
const INITIAL = {
server: null, bottle: null, plan: null,
name: "", kana: "", company: "",
tel: "", email: "", contact_method: "",
zipcode: "", address: "", building: "",
agree: false,
};
function App() {
const [step, setStep] = useState(1);
const [state, setState] = useState(INITIAL);
useEffect(() => {
// PHPバリデーションエラー後の復元(submit.php → index.php リダイレクト時)
const phpOld = window.__FORM_OLD || {};
if (Object.keys(phpOld).length > 0) {
setState((s) => ({
...s,
server: phpOld.server || s.server,
bottle: phpOld.bottle_size || s.bottle,
plan: phpOld.plan || s.plan,
name: phpOld.name || s.name,
kana: phpOld.kana || s.kana,
company: phpOld.company || s.company,
tel: phpOld.tel || s.tel,
email: phpOld.email || s.email,
contact_method: phpOld.contact_method || s.contact_method,
zipcode: phpOld.zipcode || s.zipcode,
address: phpOld.address || s.address,
building: phpOld.building || s.building,
agree: phpOld.agree === '1',
}));
setStep(window.__INIT_STEP || 1);
return; // localStorage 復元はスキップ
}
try {
const saved = JSON.parse(sessionStorage.getItem("apply_v2_state") || "null");
const savedStep = parseInt(sessionStorage.getItem("apply_v2_step") || "1", 10);
if (saved) setState((s) => ({ ...s, ...saved }));
if (savedStep) setStep(savedStep);
} catch {}
}, []);
useEffect(() => {
try {
sessionStorage.setItem("apply_v2_state", JSON.stringify(state));
sessionStorage.setItem("apply_v2_step", String(step));
} catch {}
}, [state, step]);
const set = (patch) => setState((s) => ({ ...s, ...patch }));
const gotoStep = (n) => {
setStep(n);
window.scrollTo({ top: 0, behavior: "smooth" });
};
const handleReset = () => { setState(INITIAL); gotoStep(1); };
return (
{step < 3 && }
{step < 3 && }
{step === 1 && gotoStep(2)} errors={{}} />}
{step === 2 && gotoStep(1)} onSubmit={() => gotoStep(3)} initialErrors={window.__FORM_ERRORS || {}} />}
{step === 3 && }
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
);