From 57022d46af31303efadadb4cb766a450a3e90581 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 16:13:02 +0700 Subject: [PATCH] x --- .../src/components/CategoryBulkJsonModal.tsx | 585 ++++++++++++++++++ .../src/components/page-builder/HeroForm.tsx | 112 +++- apps/admin/src/pages/CategoriesPage.tsx | 23 +- apps/backend/src/routes/categories.ts | 100 +++ apps/spa/src/components/DynamicHero.tsx | 38 +- .../dynamic/DynamicPageRenderer.tsx | 37 +- packages/types/index.ts | 2 + 7 files changed, 892 insertions(+), 5 deletions(-) create mode 100644 apps/admin/src/components/CategoryBulkJsonModal.tsx diff --git a/apps/admin/src/components/CategoryBulkJsonModal.tsx b/apps/admin/src/components/CategoryBulkJsonModal.tsx new file mode 100644 index 0000000..687a1ad --- /dev/null +++ b/apps/admin/src/components/CategoryBulkJsonModal.tsx @@ -0,0 +1,585 @@ +import React, { useState, useMemo, useRef } from 'react'; +import { + X, + Copy, + Check, + Download, + Upload, + FileJson, + AlertCircle, + CheckCircle2, + FileText, + ArrowRight, + Bot, + Loader2, + Package +} from 'lucide-react'; +import { API_BASE, type Category } from '@homaroma/types'; + +interface CategoryBulkJsonModalProps { + isOpen: boolean; + onClose: () => void; + categories: Category[]; + onSuccess: () => void; +} + +const SUPPORTED_LOCALES = [ + { code: 'th', label: 'TH', full: 'ภาษาไทย' }, + { code: 'en', label: 'EN', full: 'English' }, + { code: 'ja', label: 'JA', full: '日本語' }, + { code: 'zh', label: 'ZH', full: '中文' }, + { code: 'vi', label: 'VI', full: 'Tiếng Việt' }, + { code: 'ko', label: 'KO', full: '한국어' }, + { code: 'lo', label: 'LO', full: 'ພາສາລາວ' }, + { code: 'it', label: 'IT', full: 'Italiano' }, +]; + +export default function CategoryBulkJsonModal({ + isOpen, + onClose, + categories, + onSuccess, +}: CategoryBulkJsonModalProps) { + const [activeTab, setActiveTab] = useState<'import' | 'export'>('import'); + const [copiedJson, setCopiedJson] = useState(false); + const [copiedPrompt, setCopiedPrompt] = useState(false); + const [importJsonText, setImportJsonText] = useState(''); + const [parseError, setParseError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [resultMessage, setResultMessage] = useState(null); + const fileInputRef = useRef(null); + + // 1. Export Data Generation + const exportData = useMemo(() => { + return categories.map((cat) => { + const transMap: Record = {}; + const catNameObj = typeof cat.name === 'object' && cat.name !== null ? (cat.name as any) : null; + const catBaseName = typeof cat.name === 'string' ? cat.name : (catNameObj?.th || ''); + + for (const loc of SUPPORTED_LOCALES) { + if (catNameObj && catNameObj[loc.code]) { + transMap[loc.code] = catNameObj[loc.code]; + } else if (Array.isArray(cat.translations)) { + const t = cat.translations.find((tr: any) => tr.langCode === loc.code); + if (t?.name) { + transMap[loc.code] = t.name; + } + } + } + + if (!transMap['th'] && catBaseName) { + transMap['th'] = catBaseName; + } + + return { + id: cat.id, + name: catBaseName || transMap['th'] || 'Unnamed Category', + imageUrl: cat.imageUrl || null, + translations: transMap, + }; + }); + }, [categories]); + + const exportJsonString = useMemo(() => { + return JSON.stringify(exportData, null, 2); + }, [exportData]); + + // AI Translation Prompt + const aiTranslatePrompt = useMemo(() => { + const sampleItems = exportData.slice(0, 5); + return `Please translate and optimize our E-commerce Category Names into all 8 supported languages (th, en, ja, zh, vi, ko, lo, it). +Ensure the tone is elegant, accurate for luxury aromatherapy & lifestyle products. + +Return ONLY a valid JSON Array conforming to this structure: +\`\`\`json +${JSON.stringify( + sampleItems.length > 0 + ? sampleItems + : [ + { + name: 'น้ำมันหอมระเหย', + imageUrl: '/images/categories/essential-oils.jpg', + translations: { + th: 'น้ำมันหอมระเหย', + en: 'Essential Oils', + ja: 'エッセンシャルオイル', + zh: '精油', + vi: 'Tinh dầu', + ko: '에센셜 오일', + lo: 'ນ້ຳມັນຫອມລະເຫີຍ', + it: 'Oli Essenziali', + }, + }, + { + name: 'ก้านไม้หอมปรับอากาศ', + imageUrl: '/images/categories/reed-diffusers.jpg', + translations: { + th: 'ก้านไม้หอมปรับอากาศ', + en: 'Reed Diffusers', + ja: 'リードディフューザー', + zh: '无火藤条香薰', + vi: 'Que khuếch tán tinh dầu', + ko: '리드 디퓨저', + lo: 'ກ້ານໄມ້ຫອມປັບອາກາດ', + it: 'Diffusori a Bastoncini', + }, + }, + ], + null, + 2 +)} +\`\`\``; + }, [exportData]); + + // 2. Parsed Import Items + const parsedImportData = useMemo(() => { + if (!importJsonText.trim()) return null; + + try { + const raw = JSON.parse(importJsonText); + const list = Array.isArray(raw) ? raw : (Array.isArray(raw.categories) ? raw.categories : null); + + if (!list || !Array.isArray(list)) { + setParseError('JSON must be an Array of category objects e.g. [ { "name": "...", "translations": { ... } } ]'); + return null; + } + + const normalized = list.map((item: any) => { + const trans: Record = {}; + if (item.translations && typeof item.translations === 'object') { + for (const [k, v] of Object.entries(item.translations)) { + if (typeof v === 'string') { + trans[k.toLowerCase()] = v; + } else if (v && typeof v === 'object' && typeof (v as any).name === 'string') { + trans[k.toLowerCase()] = (v as any).name; + } + } + } + + const baseName = item.name || trans['th'] || trans['en'] || 'Unnamed Category'; + if (!trans['th'] && baseName) { + trans['th'] = baseName; + } + + return { + id: typeof item.id === 'number' ? item.id : undefined, + name: baseName, + imageUrl: item.imageUrl || null, + translations: trans, + }; + }); + + if (normalized.length === 0) { + setParseError('No category items found in the array.'); + return null; + } + + setParseError(null); + return normalized; + } catch (err: any) { + setParseError(`Invalid JSON Syntax: ${err.message}`); + return null; + } + }, [importJsonText]); + + if (!isOpen) return null; + + const handleCopyJson = async () => { + try { + await navigator.clipboard.writeText(exportJsonString); + setCopiedJson(true); + setTimeout(() => setCopiedJson(false), 2000); + } catch {} + }; + + const handleCopyPrompt = async () => { + try { + await navigator.clipboard.writeText(aiTranslatePrompt); + setCopiedPrompt(true); + setTimeout(() => setCopiedPrompt(false), 2000); + } catch {} + }; + + 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 = `categories_multilang_${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 handleImportSubmit = async () => { + if (!parsedImportData || parsedImportData.length === 0) return; + setSubmitting(true); + setResultMessage(null); + + try { + const res = await fetch(`${API_BASE}/categories/bulk-import`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(parsedImportData), + }); + + const data = await res.json(); + + if (res.ok && data.success) { + setResultMessage(`นำเข้าสำเร็จเรียบร้อย! (สร้างใหม่ ${data.createdCount || 0} หมวดหมู่, อัปเดต ${data.updatedCount || 0} หมวดหมู่)`); + onSuccess(); + setTimeout(() => { + onClose(); + }, 1500); + } else { + setParseError(data.message || 'Failed to bulk import categories'); + } + } catch (err: any) { + console.error('Import categories error:', err); + setParseError(err.message || 'Network error occurred'); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+ + {/* Modal Header */} +
+
+
+ +
+
+

+ Multi-Language Categories JSON + + Bulk JSON Array + +

+

+ Import & Export Category details with translations for all 8 languages at once. +

+
+
+ + +
+ + {/* Tab Navigation */} +
+ + + +
+ + {/* Tab Content Body */} +
+ + {/* IMPORT TAB */} + {activeTab === 'import' && ( +
+ + {/* Action Toolbar */} +
+
+ + + + +
+ +
+ รูปแบบ: + + [{`{"name":"...", "translations":{...}}`}] + +
+
+ + {/* JSON Editor Textarea */} +
+