// ─── Contact page with quote simulator ────────────────────────── const RECAPTCHA_SITE_KEY = "6Le_-8csAAAAAOKyOETYx1ho4k-xyMzMpkercLgB"; const RECAPTCHA_SCRIPT_ID = "recaptcha-enterprise"; function Contact({ go, initialTab }) { const [tab, setTab] = React.useState(initialTab || "simulator"); const [simQuote, setSimQuote] = React.useState(null); React.useEffect(() => { if (!document.getElementById(RECAPTCHA_SCRIPT_ID)) { const s = document.createElement("script"); s.id = RECAPTCHA_SCRIPT_ID; s.src = `https://www.google.com/recaptcha/enterprise.js?render=${RECAPTCHA_SITE_KEY}`; s.async = true; document.head.appendChild(s); } document.body.classList.add("recaptcha-active"); return () => { document.body.classList.remove("recaptcha-active"); }; }, []); const handleConsult = (quoteData) => { setSimQuote(quoteData); setTab("form"); }; return (
まずは、お話から。} lead="見積りシミュレーターで概算を把握しても、そのままフォームで具体相談にお進みいただいても。お客様のペースで進められます。" />
{[ { k: "simulator", l: "見積りシミュレーター", sub: "概算を把握したい" }, { k: "form", l: "お問い合わせフォーム", sub: "じっくり相談したい" }, ].map(t => ( ))}
{tab === "simulator" && } {tab === "form" && }
); } // ── Quote simulator ───────────────────────────────────────── const SIM_SERVICES = [ { k: "newsite", l: "新規サイト制作", base: 40000, perPage: 6000 }, { k: "renewal", l: "既存サイトのリニューアル", base: 30000, perPage: 5000 }, { k: "maint", l: "WordPress 保守", base: 35000, unit: "月額", plans: [["Standard", 35000], ["Business", 75000], ["Enterprise", 120000]] }, { k: "system", l: "Webシステム開発", base: 80000, perPage: 8000 }, { k: "seo", l: "SEO対策", base: 110000, unit: "月額" }, ]; const SIM_OPTIONS = [ { k: "design", l: "UIデザインを伴う", add: 10000 }, { k: "cms", l: "CMS(WordPress)構築", add: 10000 }, { k: "auth", l: "会員認証・マイページ", add: 80000 }, { k: "payment", l: "決済 / サブスクリプション", add: 20000 }, { k: "multilingual", l: "多言語対応", add: 180000 }, { k: "migrate", l: "既存データ移行", add: 15000 }, ]; const SIM_INTEREST_MAP = { newsite: "新規サイト制作", renewal: "既存サイトのリニューアル", maint: "WordPress 保守・改修", system: "Webシステム開発", seo: "SEO対策", }; function budgetFromAmount(n) { if (n < 500000) return "〜 5万円"; if (n < 2000000) return "6〜50万円"; if (n < 5000000) return "51〜100万円"; if (n < 10000000) return "101〜300万円"; return "301万円以上"; } function QuoteSimulator({ go, onConsult }) { const [service, setService] = React.useState("renewal"); const [pages, setPages] = React.useState(12); const [opts, setOpts] = React.useState({}); const [plan, setPlan] = React.useState("Business"); const [timeline, setTimeline] = React.useState("3months"); const svc = SIM_SERVICES.find(s => s.k === service); const isMonthly = svc.unit === "月額"; const base = isMonthly ? (svc.plans ? svc.plans.find(p => p[0] === plan)[1] : svc.base) : svc.base + (svc.perPage || 0) * pages; const optsSum = Object.entries(opts).filter(([, v]) => v).reduce((sum, [k]) => sum + (SIM_OPTIONS.find(o => o.k === k)?.add || 0), 0); const total = isMonthly ? base : base + optsSum; const urgency = !isMonthly && timeline === "1month" ? 1.2 : 1; const final = Math.round(total * urgency); const fmt = (n) => "¥" + n.toLocaleString("ja-JP"); const handleConsult = () => { const selectedOpts = Object.keys(opts).filter(k => opts[k]) .map(k => SIM_OPTIONS.find(o => o.k === k)?.l).filter(Boolean); const timelineLabel = timeline === "1month" ? "1ヶ月以内" : timeline === "3months" ? "3ヶ月前後" : "相談して決める"; let quoteText = `種別: ${svc.l}\n`; if (isMonthly && svc.plans) quoteText += `プラン: ${plan}\n`; if (!isMonthly) quoteText += `ページ数: ${pages}ページ\n`; if (selectedOpts.length) quoteText += `オプション: ${selectedOpts.join("、")}\n`; if (!isMonthly) quoteText += `スケジュール: ${timelineLabel}\n`; quoteText += `概算: ${fmt(final)}${isMonthly ? "/月〜" : "〜"}`; onConsult({ interest: SIM_INTEREST_MAP[service] || "", budget: isMonthly ? "相談して決めたい" : budgetFromAmount(final), quoteText, }); }; return (
{SIM_SERVICES.map(s => ( ))}
{isMonthly && svc.plans && (
{svc.plans.map(([name, price]) => ( ))}
)} {!isMonthly && ( <> setPages(Number(e.target.value))} style={{ width: "100%", accentColor: "var(--accent)" }} />
3 pages80 pages
{SIM_OPTIONS.map(o => { const on = !!opts[o.k]; return ( ); })}
{[["1month", "1ヶ月以内 (+20%)"], ["3months", "3ヶ月前後"], ["flex", "相談して決める"]].map(([k, l]) => ( ))}
)}
); } function SimBlock({ label, note, children }) { return (
{label}
{note &&
{note}
}
{children}
); } function SimRow({ k, v }) { return (
{k} {v}
); } // ── Contact form ───────────────────────────────────────────── function ContactForm({ simQuote }) { const [submitted, setSubmitted] = React.useState(false); const [sending, setSending] = React.useState(false); const [error, setError] = React.useState(null); const [form, setForm] = React.useState({ company: "", name: "", email: "", phone: "", interest: simQuote?.interest || "", budget: simQuote?.budget || "", message: simQuote ? `【シミュレーター概算】\n${simQuote.quoteText}\n\n` : "", }); const set = (k) => (e) => setForm({ ...form, [k]: e.target.value }); const handleSubmit = async (e) => { e.preventDefault(); setSending(true); setError(null); try { const recaptchaToken = await new Promise((resolve, reject) => { const gr = window.grecaptcha?.enterprise; if (!gr) { reject(new Error("reCAPTCHA が読み込まれていません。ページを再読み込みしてください。")); return; } gr.ready(() => { gr.execute(RECAPTCHA_SITE_KEY, { action: "contact" }) .then(resolve).catch(reject); }); }); const res = await fetch("/wp-json/uiarb/v1/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...form, quote: simQuote?.quoteText || "", recaptchaToken }), }); const json = await res.json(); if (!res.ok) throw new Error(json.message || "送信に失敗しました"); setSubmitted(true); } catch (err) { setError(err.message); } finally { setSending(false); } }; if (submitted) { return (

ありがとうございました。

3営業日以内にご返信いたします。

); } return (