diff --git a/apps/admin/src/components/SeoBulkJsonModal.tsx b/apps/admin/src/components/SeoBulkJsonModal.tsx new file mode 100644 index 0000000..5fcc47a --- /dev/null +++ b/apps/admin/src/components/SeoBulkJsonModal.tsx @@ -0,0 +1,543 @@ +import React, { useState, useMemo, useRef } from 'react'; +import { + X, + Copy, + Check, + Download, + Upload, + FileJson, + AlertCircle, + CheckCircle2, + FileText, + ArrowRight, + Bot +} from 'lucide-react'; +import type { StoreSettingsTranslation } from '@homaroma/types'; + +interface SeoBulkJsonModalProps { + isOpen: boolean; + onClose: () => void; + defaultSeoTitle: string; + defaultSeoDescription: string; + translations: StoreSettingsTranslation[]; + supportedLocales: Array<{ code: string; label: string; full: string }>; + onApply: (data: { thTitle: string; thDesc: string; translations: StoreSettingsTranslation[] }) => void; +} + +export default function SeoBulkJsonModal({ + isOpen, + onClose, + defaultSeoTitle, + defaultSeoDescription, + translations, + supportedLocales, + onApply, +}: SeoBulkJsonModalProps) { + const [activeTab, setActiveTab] = useState<'export' | 'import'>('import'); + const [copiedJson, setCopiedJson] = useState(false); + const [copiedPrompt, setCopiedPrompt] = useState(false); + const [importJsonText, setImportJsonText] = useState(''); + const [parseError, setParseError] = useState(null); + const [applySuccess, setApplySuccess] = useState(false); + const fileInputRef = useRef(null); + + // 1. Build Export Data + const exportData = useMemo(() => { + const out: Record = {}; + + for (const locale of supportedLocales) { + if (locale.code === 'th') { + const thTrans = translations.find((t) => t.langCode === 'th'); + out['th'] = { + title: thTrans?.defaultSeoTitle ?? defaultSeoTitle ?? '', + description: thTrans?.defaultSeoDescription ?? defaultSeoDescription ?? '', + }; + } else { + const trans = translations.find((t) => t.langCode === locale.code); + out[locale.code] = { + title: trans?.defaultSeoTitle ?? '', + description: trans?.defaultSeoDescription ?? '', + }; + } + } + return out; + }, [defaultSeoTitle, defaultSeoDescription, translations, supportedLocales]); + + const exportJsonString = useMemo(() => { + return JSON.stringify(exportData, null, 2); + }, [exportData]); + + // AI Prompt Helper to hand off to ChatGPT/Claude + const aiTranslatePrompt = useMemo(() => { + const currentTh = exportData['th'] || { title: defaultSeoTitle, description: defaultSeoDescription }; + return `Please translate and optimize the following SEO Title Template and Meta Description for our luxury aromatherapy & lifestyle brand into all supported languages (th, en, ja, zh, vi, ko, lo, it). + +Return ONLY valid JSON matching this exact schema: +\`\`\`json +{ + "th": { + "title": "${currentTh.title || 'HomAroma | Pure Essential Oils & Botanical Aromatherapy'}", + "description": "${currentTh.description || 'Hand-blended botanical fragrances and quiet luxury essential oils.'}" + }, + "en": { + "title": "HomAroma | Pure Essential Oils & Botanical Aromatherapy", + "description": "Hand-blended botanical fragrances and quiet luxury essential oils crafted for serene modern living." + }, + "ja": { + "title": "HomAroma | ピュアエッセンシャルオイル&アロマセラピー", + "description": "心身を癒す天然抽出のエッセンシャルオイルとボタニカルフレグランス。" + }, + "zh": { + "title": "HomAroma | 纯精油与植物芳香疗法", + "description": "精选天然植物萃取精油与高品质香氛,为现代生活打造宁静身心庇护所。" + }, + "vi": { + "title": "HomAroma | Tinh dầu nguyên chất & Liệu pháp hương thơm", + "description": "Tinh dầu thiên nhiên và hương thơm cao cấp mang lại sự thư giãn cho không gian sống." + }, + "ko": { + "title": "HomAroma | 프리미엄 퓨어 에센셜 오일 & 아로마테라피", + "description": "자연에서 추출한 순수 에센셜 오일과 보태니컬 향기로 완성하는 힐링 라이프스타일." + }, + "lo": { + "title": "HomAroma | ນ້ຳມັນຫອມລະເຫີຍບໍລິສຸດ & ອາໂຣມາເທຣາພີ", + "description": "ສຳຜັດຄວາມຜ່ອນຄາຍກັບນ້ຳມັນຫອມລະເຫີຍທຳມະຊາດ ແລະ ກິ່ນຫອມລະດັບພຣີມຽມ." + }, + "it": { + "title": "HomAroma | Oli Essenziali Puri e Aromaterapia Botanica", + "description": "Fragranze botaniche artigianali e oli essenziali di lusso per uno stile di vita sereno." + } +} +\`\`\``; + }, [exportData, defaultSeoTitle, defaultSeoDescription]); + + // 2. Parse Import Data + const parsedImportData = useMemo(() => { + if (!importJsonText.trim()) { + return null; + } + try { + const raw = JSON.parse(importJsonText); + const normalized: Record = {}; + + if (Array.isArray(raw)) { + for (const item of raw) { + const lang = item.langCode || item.lang || item.code; + if (lang && typeof lang === 'string') { + normalized[lang.toLowerCase()] = { + title: item.title || item.defaultSeoTitle || '', + description: item.description || item.defaultSeoDescription || '', + }; + } + } + } else if (typeof raw === 'object' && raw !== null) { + // Check if nested inside "locales" or "translations" or directly keyed by lang code + const targetObj = raw.locales || raw.translations || raw; + for (const [key, val] of Object.entries(targetObj)) { + if (val && typeof val === 'object') { + const v = val as any; + normalized[key.toLowerCase()] = { + title: v.title ?? v.defaultSeoTitle ?? '', + description: v.description ?? v.defaultSeoDescription ?? '', + }; + } + } + } + + const detectedLangs = Object.keys(normalized); + if (detectedLangs.length === 0) { + setParseError('No recognized language keys (e.g. "th", "en", "ja") found in JSON object.'); + return null; + } + + setParseError(null); + return normalized; + } catch (e: any) { + setParseError(`Invalid JSON Syntax: ${e.message}`); + return null; + } + }, [importJsonText]); + + if (!isOpen) return null; + + const handleCopyJson = async () => { + try { + await navigator.clipboard.writeText(exportJsonString); + setCopiedJson(true); + setTimeout(() => setCopiedJson(false), 2000); + } catch { + // ignore + } + }; + + const handleCopyPrompt = async () => { + try { + await navigator.clipboard.writeText(aiTranslatePrompt); + setCopiedPrompt(true); + setTimeout(() => setCopiedPrompt(false), 2000); + } catch { + // ignore + } + }; + + const handleDownloadJson = () => { + const blob = new Blob([exportJsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `homaroma_seo_defaults_${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result as string; + if (content) { + setImportJsonText(content); + } + }; + reader.readAsText(file); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleLoadSample = () => { + setImportJsonText(exportJsonString); + }; + + const handleApply = () => { + if (!parsedImportData) return; + + let newThTitle = defaultSeoTitle; + let newThDesc = defaultSeoDescription; + + const nextTranslations = [...translations]; + + for (const [lang, val] of Object.entries(parsedImportData)) { + if (lang === 'th') { + newThTitle = val.title; + newThDesc = val.description; + } + + const idx = nextTranslations.findIndex((t) => t.langCode.toLowerCase() === lang); + if (idx >= 0) { + nextTranslations[idx] = { + ...nextTranslations[idx], + defaultSeoTitle: val.title || null, + defaultSeoDescription: val.description || null, + }; + } else { + nextTranslations.push({ + id: 0, + storeSettingsId: 0, + langCode: lang, + defaultSeoTitle: val.title || null, + defaultSeoDescription: val.description || null, + } as StoreSettingsTranslation); + } + } + + onApply({ + thTitle: newThTitle, + thDesc: newThDesc, + translations: nextTranslations, + }); + + setApplySuccess(true); + setTimeout(() => { + setApplySuccess(false); + onClose(); + }, 1200); + }; + + return ( +
+
+ + {/* Modal Header */} +
+
+
+ +
+
+

+ Multi-Language SEO Defaults JSON + + Bulk All Languages + +

+

+ Import & Export Default SEO Title and Meta Description across all 8 languages in one click. +

+
+
+ + +
+ + {/* Tab Navigation */} +
+ + + +
+ + {/* Tab Content Body */} +
+ + {/* IMPORT TAB */} + {activeTab === 'import' && ( +
+ + {/* Actions & Helpers */} +
+
+ + + + +
+ +
+ รองรับคีย์: + th + en + ja + zh + ... (title & description) +
+
+ + {/* JSON Editor Textarea */} +
+