From ebc8f2ffeb6731a9b49df43a199d04a8a1094934 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 5 Oct 2026 06:43:10 +0700 Subject: [PATCH] x --- .../category/CategoryTranslationModal.tsx | 534 ++++++++++++++++++ .../page-builder/BlockTranslationModal.tsx | 4 + apps/admin/src/pages/CategoryCmsPage.tsx | 63 ++- apps/admin/src/pages/CategoryEditPage.tsx | 113 +++- apps/website/src/app/globals.css | 2 + apps/website/src/components/Navbar.tsx | 27 +- .../dynamic/DynamicPageRenderer.tsx | 2 +- 7 files changed, 713 insertions(+), 32 deletions(-) create mode 100644 apps/admin/src/components/category/CategoryTranslationModal.tsx diff --git a/apps/admin/src/components/category/CategoryTranslationModal.tsx b/apps/admin/src/components/category/CategoryTranslationModal.tsx new file mode 100644 index 0000000..1070a8a --- /dev/null +++ b/apps/admin/src/components/category/CategoryTranslationModal.tsx @@ -0,0 +1,534 @@ +import React, { useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { + X, + Copy, + Check, + Download, + Upload, + FileJson, + AlertCircle, + CheckCircle2, + Sparkles, + Bot, + ArrowRight, + FolderTree, +} from 'lucide-react'; + +export const SUPPORTED_LOCALES = [ + { code: 'th', label: 'TH', full: 'ภาษาไทย', flag: '🇹🇭' }, + { code: 'en', label: 'EN', full: 'English', flag: '🇬🇧' }, + { code: 'ja', label: 'JA', full: '日本語', flag: '🇯🇵' }, + { code: 'zh', label: 'ZH', full: '中文', flag: '🇨🇳' }, + { code: 'vi', label: 'VI', full: 'Tiếng Việt', flag: '🇻🇳' }, + { code: 'ko', label: 'KO', full: '한국어', flag: '🇰🇷' }, + { code: 'lo', label: 'LO', full: 'ພາສາລາວ', flag: '🇱🇦' }, + { code: 'it', label: 'IT', full: 'Italiano', flag: '🇮🇹' }, +] as const; + +export interface CategoryTranslationData { + name: Record; +} + +interface CategoryTranslationModalProps { + isOpen: boolean; + onClose: () => void; + categoryName?: string; + categoryId?: string | number; + data: CategoryTranslationData; + initialTab?: 'export' | 'import'; + onApplyTranslations: (updated: { name: Record }) => void; +} + +function getLocalesObject(val: unknown): Record { + const result: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + if (typeof val === 'string') { + result[code] = code === 'th' ? val : ''; + } else if (typeof val === 'object' && val !== null) { + const record = val as Record; + result[code] = typeof record[code] === 'string' ? record[code] : ''; + } else { + result[code] = ''; + } + }); + return result; +} + +function extractFieldTranslations(obj: unknown, fieldKey: string): Record { + const res: Record = {}; + if (!obj || typeof obj !== 'object') return res; + + const record = obj as Record; + + // Format A: { [fieldKey]: { th: "...", en: "..." } } + if (typeof record[fieldKey] === 'object' && record[fieldKey] !== null) { + SUPPORTED_LOCALES.forEach(({ code }) => { + res[code] = typeof record[fieldKey][code] === 'string' ? record[fieldKey][code].trim() : ''; + }); + return res; + } + + // Format B: { th: { [fieldKey]: "..." }, en: { [fieldKey]: "..." } } + SUPPORTED_LOCALES.forEach(({ code }) => { + if (record[code] && typeof record[code] === 'object' && record[code][fieldKey]) { + res[code] = typeof record[code][fieldKey] === 'string' ? record[code][fieldKey].trim() : ''; + } + }); + + // Flat Format: { th: "...", en: "..." } + if (Object.keys(res).length === 0 || !Object.values(res).some(Boolean)) { + SUPPORTED_LOCALES.forEach(({ code }) => { + if (typeof record[code] === 'string') { + res[code] = record[code].trim(); + } + }); + } + + return res; +} + +export default function CategoryTranslationModal({ + isOpen, + onClose, + categoryName, + categoryId, + data, + initialTab = 'export', + onApplyTranslations, +}: CategoryTranslationModalProps) { + const [activeTab, setActiveTab] = useState<'export' | 'import'>(initialTab); + const [copiedJson, setCopiedJson] = useState(false); + const [copiedPrompt, setCopiedPrompt] = useState(false); + const [importText, setImportText] = useState(''); + const [parseError, setParseError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + React.useEffect(() => { + if (isOpen) { + setActiveTab(initialTab); + setParseError(null); + setSuccessMsg(null); + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prevOverflow; + }; + } + }, [isOpen, initialTab]); + + // Structured export object + const exportData = useMemo(() => { + return { + name: getLocalesObject(data.name), + }; + }, [data]); + + const exportJsonString = useMemo(() => { + return JSON.stringify(exportData, null, 2); + }, [exportData]); + + // AI Prompt tailored for category naming & sensory fragrance branding + const aiPromptString = useMemo(() => { + const title = categoryName || data.name?.th || data.name?.en || 'หมวดหมู่สินค้า'; + return `คุณคือ AI ผู้เชี่ยวชาญด้าน E-Commerce Localization, Luxury Fragrance & Home Aroma Brand Strategist (HomAroma Platform) + +ภารกิจ: แปลชื่อหมวดหมู่สินค้า (Category / Collection Name): "${title}" เป็น 8 ภาษาให้ครบถ้วน สละสลวย ไพเราะ และกระตุ้นความรู้สึกสัมผัสกลิ่นหอมและบรรยากาศ (Sensory & Ambience Appeal): +- th: ภาษาไทย +- en: ภาษาอังกฤษ (English) +- ja: ภาษาญี่ปุ่น (日本語) +- zh: ภาษาจีน (中文) +- vi: ภาษาเวียดนาม (Tiếng Việt) +- ko: ภาษาเกาหลี (한국어) +- lo: ภาษาลาว (ພາສາລາວ) +- it: ภาษาอิตาลี (Italiano) + +ข้อกำหนดสำคัญ: +1. รักษาโครงสร้าง JSON และคีย์ "name" ให้ถูกต้องสมบูรณ์ +2. "name": ชื่อหมวดหมู่สินค้า แปลให้สละสลวย เป็นทางการ และกระตุ้นความรู้สึกผ่อนคลาย สง่างาม น่าค้นหา +3. สไตล์การแปล: หรูหรา สง่างาม อบอุ่น (Quiet Luxury, High-End Spa & Artisanal Perfumery) +4. ตอบกลับมาเฉพาะโค้ด JSON ที่ถูกต้องเท่านั้น (Valid JSON only) ห้ามใส่คำอธิบายเพิ่มเติม + +\`\`\`json +${exportJsonString} +\`\`\``; + }, [categoryName, data.name, exportJsonString]); + + const handleCopyJson = async () => { + try { + await navigator.clipboard.writeText(exportJsonString); + setCopiedJson(true); + setTimeout(() => setCopiedJson(false), 2000); + } catch { + // fallback + } + }; + + const handleCopyPrompt = async () => { + try { + await navigator.clipboard.writeText(aiPromptString); + setCopiedPrompt(true); + setTimeout(() => setCopiedPrompt(false), 2000); + } catch { + // fallback + } + }; + + 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 = `category-${categoryId || 'item'}-translations.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + // Parser & validator for pasted import JSON + const parsedImport = useMemo(() => { + if (!importText.trim()) return null; + + let clean = importText.trim(); + if (clean.startsWith('```json')) clean = clean.slice(7); + else if (clean.startsWith('```')) clean = clean.slice(3); + if (clean.endsWith('```')) clean = clean.slice(0, -3); + clean = clean.trim(); + + try { + const parsed = JSON.parse(clean); + if (!parsed || typeof parsed !== 'object') { + return { error: 'รูปแบบ JSON ไม่ถูกต้อง (ต้องเป็น JSON Object)' }; + } + + const nameMap = extractFieldTranslations(parsed, 'name'); + + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + languageCounts[code] = Boolean(nameMap[code]?.trim()); + }); + + const filledCount = Object.values(languageCounts).filter(Boolean).length; + + return { + name: nameMap, + languageCounts, + filledCount, + statsText: `พบชื่อหมวดหมู่ ${filledCount} / 8 ภาษา`, + }; + } catch (err: any) { + return { error: `JSON Parse Error: ${err.message}` }; + } + }, [importText]); + + const handleApplyImport = () => { + if (!parsedImport || parsedImport.error || !parsedImport.name) return; + + onApplyTranslations({ + name: parsedImport.name, + }); + + setSuccessMsg('อัปเดตชื่อหมวดหมู่ทุกภาษาลงในฟอร์มสำเร็จแล้ว!'); + setTimeout(() => { + onClose(); + }, 1200); + }; + + if (!isOpen) return null; + + return createPortal( +
+
+ {/* Header */} +
+
+
+ +
+
+
+

+ จัดการชื่อหมวดหมู่หลายภาษา (Category Multi-Language JSON) +

+ {categoryId && ( + + ID: {categoryId} + + )} +
+

+ ส่งออกชื่อหมวดหมู่สินค้าให้ LLM แปล 8 ภาษา แล้วนำ JSON มาวางเพื่ออัปเดตทุกภาษาพร้อมกัน +

+
+
+ +
+ + {/* Navigation Tabs */} +
+ + + +
+ + {/* Modal Body */} +
+ {/* Error Message */} + {parseError && ( +
+ + {parseError} +
+ )} + + {/* Success Notification */} + {successMsg && ( +
+ + {successMsg} +
+ )} + + {activeTab === 'export' ? ( +
+ {/* Instructions Banner */} +
+
+
+ + ขั้นตอนการแปลชื่อหมวดหมู่ด้วย AI: +
+

+ 1. กดปุ่ม "คัดลอกพร้อมท์ AI" → 2. นำไปวางใน ChatGPT / Claude / Gemini → 3. สลับไปแท็บ "วางผลแปล & อัปเดต" แล้วนำผลลัพธ์มาวาง +

+
+ +
+ + + + + +
+
+ + {/* Supported Languages Status Badges */} +
+ + สถานะภาษาในหมวดหมู่นี้ (8 ภาษา): + +
+ {SUPPORTED_LOCALES.map(({ code, label, full, flag }) => { + const hasVal = Boolean(data.name?.[code]?.trim()); + return ( +
+ {flag} + {label} + ({code}) + {hasVal ? : -} +
+ ); + })} +
+
+ + {/* JSON Display Area */} +
+
+ + + โครงสร้าง JSON ข้อมูลชื่อหมวดหมู่ปัจจุบัน: + + UTF-8 Encoded +
+
+                  {exportJsonString}
+                
+
+
+ ) : ( +
+ {/* Import Textarea */} +
+ +