// ────────────────────────────────────────────────────────────────
// 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 }) => (
);
if (status === 'success') {
return (
SUBMITTED / 受付完了
お問い合わせありがとうございました。
担当者より2〜3営業日以内にご連絡させていただきます。
);
}
return (
);
};
// 右下フロート ─ 言語切替 (ThemeToggle の上に積む)
const LangToggle = () => {
const { theme } = useTheme();
const { lang, setLang } = useLang();
const { isMobile } = useViewport();
const t = theme;
return (
);
};
// 右下 Back-to-top ボタン ─ 便利系
const BackToTop = () => {
const { theme } = useTheme();
const [show, setShow] = React.useState(false);
React.useEffect(() => {
const onScroll = () => setShow((window.scrollY || document.documentElement.scrollTop) > 400);
window.addEventListener('scroll', onScroll, { passive: true });
// Also listen for the page's inner scroll container if any
const inner = document.querySelector('[data-scroll-root]');
if (inner) inner.addEventListener('scroll', onScroll, { passive: true });
return () => {
window.removeEventListener('scroll', onScroll);
if (inner) inner.removeEventListener('scroll', onScroll);
};
}, []);
if (!show) return null;
return (
);
};
Object.assign(window, {
SiteNav, SiteFooter, ContactForm, BackToTop, NAV_ITEMS,
LangProvider, LangContext, useLang, LangToggle, TRANSLATIONS_EN,
useViewport,
});