// ──────────────────────────────────────────────────────────────── // site-common.jsx // Production site用の共通コンポーネント // - SiteNav : 実リンク付きナビゲーション(PHILOSOPHY / SERVICES / ...) // - SiteFooter: 実リンク付きフッター // - ContactForm: プレースホルダ送信付きの問い合わせフォーム // // 既存の DesignCanvas プレビューを壊さないよう、既存 PageFooter は残し、 // production 用の各ページ JSX から SiteNav / SiteFooter を参照する。 // ──────────────────────────────────────────────────────────────── // Base path for all internal links. Each HTML sets window.__ASSET_BASE__ ('./' or '../'). const __base = () => (typeof window !== 'undefined' && window.__ASSET_BASE__) || './'; // ── ビューポート検知 ──────────────────────────────────────────── // isMobile: 768未満 (スマホ), isTablet: 768-1023, それ以上は desktop const useViewport = () => { const [w, setW] = React.useState(() => typeof window !== 'undefined' ? window.innerWidth : 1200); React.useEffect(() => { const on = () => setW(window.innerWidth); window.addEventListener('resize', on); return () => window.removeEventListener('resize', on); }, []); return { width: w, isMobile: w < 768, isTablet: w >= 768 && w < 1024 }; }; // ── 言語切替 (JP / EN) ──────────────────────────────────────────── // 翻訳辞書。キー = 日本語原文、値 = 英語訳 // 辞書にない文字列は日本語のまま表示される(段階的に翻訳を追加できる) const TRANSLATIONS_EN = { // Nav 'PHILOSOPHY': 'PHILOSOPHY', 'SERVICES': 'SERVICES', 'COMPANY': 'COMPANY', 'NEWS': 'NEWS', 'CAREERS': 'CAREERS', 'CONTACT →': 'CONTACT →', // Top page hero 'Technology で 食': 'Technology, alongside food', '(職人)': '(and its makers)', 'に寄り添い': '', '四方': 'Shihou', '良し': 'Good', 'の': ' ', 'AX': 'AX', 'を。': '.', '事業を見る →': 'See Services →', '会社概要': 'About', '採用情報': 'Careers', 'SCROLL / 読み進める': 'SCROLL / READ ON', // Intro '食の現場には、': 'In the field of food,', '熟練の手': 'skilled hands', 'と、': ' and', '伝えきれない': 'untold', '知恵': 'wisdom', 'があります。': 'endure.', // Philosophy '三方から、': 'From three,', '四方': 'four.', 'へ。': '', '四方のうちどれか一つでも欠ければ、事業は続かない。': 'A business cannot endure if any one of the four is missing.', // Tech × Craft 'Technologyは、': 'Technology is', '道具': 'a tool.', '。': '.', '主役は、': 'The protagonist is', '人の手': 'the human hand', // Services section '八つの、': 'Eight', '具体的な': 'tangible', '伴走': 'partnerships', '詳しく見る →': 'Learn more →', // CTA 'お問い合わせ →': 'Contact us →', '会社資料 DL': 'Download deck', // Manifesto 'プロフェッショナルが、': 'Professionals', '創造だけ': 'dedicated solely to creation', 'に、': ',', '専念できる世界を。': 'in a world that lets them.', // Footer 'ABOUT': 'ABOUT', 'Philosophy': 'Philosophy', 'Company': 'Company', 'Careers': 'Careers', 'Curecipe': 'Curecipe', 'CuliNFC': 'CuliNFC', 'Curegi': 'Curegi', 'Consulting': 'Consulting', 'お問い合わせ': 'Contact', '資料請求': 'Request materials', '採用エントリー': 'Apply', 'PRIVACY / TERMS': 'PRIVACY / TERMS', 'TOKYO / JAPAN': 'TOKYO / JAPAN', // Contact form 'FORM / GENERAL INQUIRY': 'FORM / GENERAL INQUIRY', 'お名前': 'Name', '会社名': 'Company', 'メール': 'Email', '電話番号': 'Phone', 'お問い合わせの種類': 'Subject', 'ご相談内容': 'Message', '送信する →': 'Submit →', '送信中...': 'Sending...', 'プライバシーポリシーに同意します': 'I agree to the privacy policy', }; const LangContext = React.createContext({ lang: 'jp', setLang: () => {}, t: (x) => x }); const LangProvider = ({ children }) => { const [lang, setLang] = React.useState(() => { try { return localStorage.getItem('culina-lang') || 'jp'; } catch { return 'jp'; } }); React.useEffect(() => { try { localStorage.setItem('culina-lang', lang); } catch {} if (typeof document !== 'undefined') document.documentElement.lang = lang === 'en' ? 'en' : 'ja'; }, [lang]); const t = React.useCallback((s) => { if (lang !== 'en') return s; return Object.prototype.hasOwnProperty.call(TRANSLATIONS_EN, s) ? TRANSLATIONS_EN[s] : s; }, [lang]); return {children}; }; const useLang = () => React.useContext(LangContext); const NAV_ITEMS = [ { key: 'top', label: 'PHILOSOPHY', path: '' }, { key: 'services', label: 'SERVICES', path: 'services/' }, { key: 'company', label: 'COMPANY', path: 'company/' }, { key: 'news', label: 'NEWS', path: 'news/' }, { key: 'careers', label: 'CAREERS', path: 'careers/' }, ]; // メインナビ ─ 現在ページは accent 色でハイライト const SiteNav = ({ active }) => { const { theme, mode, toggle } = useTheme(); const { lang, setLang, t } = useLang(); const { isMobile } = useViewport(); const [menuOpen, setMenuOpen] = React.useState(false); const v1 = { paper: theme.paper, ink: theme.ink, sumi: theme.sumi, shu: theme.accent, muted: theme.muted, line: theme.line, serif: theme.fonts.serif, sans: theme.fonts.sans, mono: theme.fonts.mono, }; // Mobile: hamburger-expandable menu if (isMobile) { return ( ); } // Desktop nav (unchanged) return ( ); }; // 共通フッター ─ 実リンク化 const SiteFooter = () => { const { theme } = useTheme(); const { t } = useLang(); const { isMobile } = useViewport(); const v1 = { paper: theme.paper, ink: theme.ink, sumi: theme.sumi, shu: theme.accent, muted: theme.muted, line: theme.line, serif: theme.fonts.serif, sans: theme.fonts.sans, mono: theme.fonts.mono, }; const A = ({ href, children }) => ( e.currentTarget.style.color = v1.shu} onMouseLeave={(e) => e.currentTarget.style.color = v1.sumi}> {children} ); return ( ); }; // プレースホルダ問い合わせフォーム (後で実際の送信先を設定) const ContactForm = () => { const { theme } = useTheme(); const v1 = { paper: theme.paper, paperDeep: theme.paperDeep, ink: theme.ink, sumi: theme.sumi, shu: theme.accent, muted: theme.muted, line: theme.line, serif: theme.fonts.serif, sans: theme.fonts.sans, mono: theme.fonts.mono, }; const [form, setForm] = React.useState({ name: '', company: '', email: '', tel: '', subject: '', message: '', agree: false, }); const [status, setStatus] = React.useState('idle'); // 'idle' | 'sending' | 'success' | 'error' const upd = (k) => (e) => setForm(s => ({ ...s, [k]: e.target.type === 'checkbox' ? e.target.checked : e.target.value })); const onSubmit = (e) => { e.preventDefault(); if (!form.name || !form.email || !form.subject || !form.message) { setStatus('error'); return; } if (!form.agree) { setStatus('error'); return; } // ★ 本番実装: 送信先 (Formspree / API Route / mailto) を後で設定 // 現状はプレースホルダとして 800ms 後に success 表示 setStatus('sending'); setTimeout(() => setStatus('success'), 800); }; const Field = ({ label, required, type = 'text', placeholder, value, onChange, name, span }) => (