x
This commit is contained in:
1 parent
142d991816
commit
ebc8f2ffeb
7 files changed
+713
-32
No files matched your search
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
interface CategoryTranslationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
categoryName?: string;
|
||||
categoryId?: string | number;
|
||||
data: CategoryTranslationData;
|
||||
initialTab?: 'export' | 'import';
|
||||
onApplyTranslations: (updated: { name: Record<string, string> }) => void;
|
||||
}
|
||||
|
||||
function getLocalesObject(val: unknown): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
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<string, unknown>;
|
||||
result[code] = typeof record[code] === 'string' ? record[code] : '';
|
||||
} else {
|
||||
result[code] = '';
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractFieldTranslations(obj: unknown, fieldKey: string): Record<string, string> {
|
||||
const res: Record<string, string> = {};
|
||||
if (!obj || typeof obj !== 'object') return res;
|
||||
|
||||
const record = obj as Record<string, any>;
|
||||
|
||||
// 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<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(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<string, boolean> = {};
|
||||
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(
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-night-950/80 backdrop-blur-md animate-fade-in">
|
||||
<div className="bg-night-900 border border-white/10 rounded-3xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden text-slate-200 animate-pop-in">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-5 border-b border-white/5 flex items-center justify-between bg-gradient-to-r from-white/[0.04] to-transparent shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-2xl bg-cyan-glow/10 border border-cyan-glow/20 text-cyan-glow">
|
||||
<FolderTree className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold text-white">
|
||||
จัดการชื่อหมวดหมู่หลายภาษา (Category Multi-Language JSON)
|
||||
</h3>
|
||||
{categoryId && (
|
||||
<span className="px-2 py-0.5 text-[10px] font-mono font-bold bg-white/5 text-slate-400 rounded border border-white/10">
|
||||
ID: {categoryId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
ส่งออกชื่อหมวดหมู่สินค้าให้ LLM แปล 8 ภาษา แล้วนำ JSON มาวางเพื่ออัปเดตทุกภาษาพร้อมกัน
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-400 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="px-6 pt-3 border-b border-white/5 bg-night-950/40 flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('export')}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider border-b-2 transition-all cursor-pointer ${
|
||||
activeTab === 'export'
|
||||
? 'border-cyan-glow text-cyan-glow bg-cyan-glow/5 rounded-t-xl'
|
||||
: 'border-transparent text-slate-400 hover:text-white hover:bg-white/5 rounded-t-xl'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>1. ส่งออก & พร้อมท์ AI (Export)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('import')}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider border-b-2 transition-all cursor-pointer ${
|
||||
activeTab === 'import'
|
||||
? 'border-pop-pink text-pop-pink bg-pop-pink/5 rounded-t-xl'
|
||||
: 'border-transparent text-slate-400 hover:text-white hover:bg-white/5 rounded-t-xl'
|
||||
}`}
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>2. วางผลแปล & อัปเดต (Import)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{/* Error Message */}
|
||||
{parseError && (
|
||||
<div className="p-4 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-400 flex items-center gap-3 animate-fade-in text-sm">
|
||||
<AlertCircle className="w-5 h-5 shrink-0" />
|
||||
<span>{parseError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Notification */}
|
||||
{successMsg && (
|
||||
<div className="p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center gap-3 animate-fade-in">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0" />
|
||||
<span className="text-sm font-semibold">{successMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'export' ? (
|
||||
<div className="space-y-4">
|
||||
{/* Instructions Banner */}
|
||||
<div className="bg-night-950/60 p-4 rounded-2xl border border-white/5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-slate-200">
|
||||
<Sparkles className="w-4 h-4 text-cyan-glow" />
|
||||
<span>ขั้นตอนการแปลชื่อหมวดหมู่ด้วย AI:</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 leading-relaxed">
|
||||
1. กดปุ่ม <b className="text-cyan-glow">"คัดลอกพร้อมท์ AI"</b> → 2. นำไปวางใน ChatGPT / Claude / Gemini → 3. สลับไปแท็บ <b className="text-pop-pink">"วางผลแปล & อัปเดต"</b> แล้วนำผลลัพธ์มาวาง
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0 w-full sm:w-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyPrompt}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-bold transition-all shadow-md cursor-pointer ${
|
||||
copiedPrompt
|
||||
? 'bg-emerald-500 text-white shadow-emerald-500/20'
|
||||
: 'bg-cyan-glow hover:bg-cyan-glow/90 text-night-950 shadow-cyan-glow/20'
|
||||
}`}
|
||||
>
|
||||
{copiedPrompt ? <Check className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
|
||||
<span>{copiedPrompt ? 'คัดลอกพร้อมท์แล้ว!' : 'คัดลอกพร้อมท์ AI'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyJson}
|
||||
className="flex items-center justify-center gap-1.5 px-3.5 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-white text-xs font-medium border border-white/10 transition-all cursor-pointer"
|
||||
>
|
||||
{copiedJson ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
|
||||
<span>{copiedJson ? 'คัดลอก JSON แล้ว' : 'คัดลอก JSON'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadJson}
|
||||
className="p-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white border border-white/10 transition-all cursor-pointer"
|
||||
title="ดาวน์โหลดไฟล์ .json"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Supported Languages Status Badges */}
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">
|
||||
สถานะภาษาในหมวดหมู่นี้ (8 ภาษา):
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{SUPPORTED_LOCALES.map(({ code, label, full, flag }) => {
|
||||
const hasVal = Boolean(data.name?.[code]?.trim());
|
||||
return (
|
||||
<div
|
||||
key={code}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-mono border transition-all ${
|
||||
hasVal
|
||||
? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
|
||||
: 'bg-white/5 text-slate-500 border-white/5'
|
||||
}`}
|
||||
title={full}
|
||||
>
|
||||
<span>{flag}</span>
|
||||
<span className="font-bold">{label}</span>
|
||||
<span className="text-[10px] opacity-70">({code})</span>
|
||||
{hasVal ? <Check className="w-3 h-3 text-emerald-400" /> : <span className="text-[10px] text-slate-600">-</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JSON Display Area */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-slate-400 flex items-center gap-1.5">
|
||||
<FileJson className="w-3.5 h-3.5 text-cyan-glow" />
|
||||
<span>โครงสร้าง JSON ข้อมูลชื่อหมวดหมู่ปัจจุบัน:</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 font-mono">UTF-8 Encoded</span>
|
||||
</div>
|
||||
<pre className="p-4 rounded-2xl bg-night-950 border border-white/10 font-mono text-xs text-cyan-200 overflow-x-auto max-h-[360px] leading-relaxed shadow-inner">
|
||||
<code>{exportJsonString}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{/* Import Textarea */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-300 flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-pop-pink" />
|
||||
<span>วางข้อความ JSON ผลลัพธ์จาก AI (ChatGPT / Claude / Gemini):</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
setImportText(text);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
className="text-[11px] text-cyan-glow hover:underline cursor-pointer flex items-center gap-1 font-semibold"
|
||||
>
|
||||
<span>วางจากคลิปบอร์ด</span>
|
||||
</button>
|
||||
</label>
|
||||
<textarea
|
||||
value={importText}
|
||||
onChange={(e) => setImportText(e.target.value)}
|
||||
placeholder={`วางโค้ด JSON ที่ได้จาก AI ที่นี่ (รองรับทั้งแบบครอบ \`\`\`json หรือ Object ปกติ)\nตัวอย่าง:\n{\n "name": {\n "th": "เทียนหอมอโรมา",\n "en": "Aromatherapy Scented Candles",\n "ja": "アロマキャンドル",\n "zh": "香薰蜡烛",\n "vi": "Nến thơm thư giãn",\n "ko": "아로마 캔들",\n "lo": "ທຽນຫອມອາໂຣມາ",\n "it": "Candele aromatiche profumate"\n }\n}`}
|
||||
rows={8}
|
||||
className="w-full p-4 rounded-2xl bg-night-950 border border-white/10 focus:border-pop-pink/50 text-xs font-mono text-white placeholder:text-slate-600 outline-none transition-all shadow-inner leading-relaxed resize-y"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Parsing Feedback & Validation */}
|
||||
{parsedImport?.error ? (
|
||||
<div className="p-4 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs flex items-center gap-3">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
<span>{parsedImport.error}</span>
|
||||
</div>
|
||||
) : parsedImport ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3.5 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-bold">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 shrink-0" />
|
||||
<span>{parsedImport.statsText}</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-normal text-emerald-400/80">พร้อมอัปเดตลงฟอร์ม</span>
|
||||
</div>
|
||||
|
||||
{/* 8-Language Live Preview Grid */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
ตัวอย่างชื่อหมวดหมู่ที่จะนำเข้า:
|
||||
</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{SUPPORTED_LOCALES.map(({ code, label, full, flag }) => {
|
||||
const val = parsedImport.name?.[code] || '';
|
||||
return (
|
||||
<div
|
||||
key={code}
|
||||
className={`p-3 rounded-xl border transition-all ${
|
||||
val
|
||||
? 'bg-night-950 border-white/10'
|
||||
: 'bg-white/[0.02] border-white/5 opacity-40'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span>{flag}</span>
|
||||
<span className="text-xs font-bold text-white">{label}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">{full}</span>
|
||||
</div>
|
||||
<p className="text-xs text-cyan-glow font-medium truncate" title={val}>
|
||||
{val || <span className="text-slate-600 italic">ไม่มีข้อมูล</span>}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-white/5 bg-night-950/60 flex items-center justify-between shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 hover:text-white hover:bg-white/5 transition-all cursor-pointer"
|
||||
>
|
||||
ยกเลิก
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{activeTab === 'export' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('import')}
|
||||
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-pop-pink hover:bg-pop-pink/90 text-white font-bold text-xs transition-all shadow-lg shadow-pop-pink/20 cursor-pointer"
|
||||
>
|
||||
<span>ไปที่หน้าวางผลแปล (Import)</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleApplyImport}
|
||||
disabled={!parsedImport || Boolean(parsedImport.error) || parsedImport.filledCount === 0}
|
||||
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-emerald-500 hover:bg-emerald-400 disabled:opacity-50 disabled:cursor-not-allowed text-night-950 font-black text-xs transition-all shadow-lg shadow-emerald-500/20 cursor-pointer"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
<span>อัปเดตลงฟอร์มทันที (Apply to Form)</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -157,6 +157,10 @@ export const BLOCK_TRANSLATABLE_FIELDS: Record<string, TranslatableFieldDef[]> =
|
||||
{ key: 'title', label: 'Section Title' },
|
||||
{ key: 'subtitle', label: 'Section Subtitle' },
|
||||
],
|
||||
CATEGORY_HEADER: [
|
||||
{ key: 'title', label: 'Category Title / Heading' },
|
||||
{ key: 'subtitle', label: 'Category Subtitle / Description', multiline: true },
|
||||
],
|
||||
PRODUCT_CAROUSEL: [
|
||||
{ key: 'title', label: 'Carousel Title' },
|
||||
{ key: 'description', label: 'Carousel Subtitle / Description', multiline: true },
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
CheckCircle2,
|
||||
Download,
|
||||
Edit3,
|
||||
Eye,
|
||||
EyeOff,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
Shield,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from 'lucide-react'
|
||||
import { API_BASE, type Category, type CategorySection } from '@homaroma/types'
|
||||
import MediaLibraryModal from '../components/MediaLibraryModal'
|
||||
@@ -30,6 +32,7 @@ import CategoryHeroCarouselBlockForm from '../components/category/CategoryHeroCa
|
||||
import CategoryDividerBlockForm from '../components/category/CategoryDividerBlockForm'
|
||||
import { validateCategoryHeroCarouselPayload } from '../components/category/categoryHeroCarouselValidation'
|
||||
import { getAdminDisplayString } from '../utils/localization'
|
||||
import BlockTranslationModal, { isTranslatableBlock } from '../components/page-builder/BlockTranslationModal'
|
||||
|
||||
interface ApiValidationIssue {
|
||||
path?: string
|
||||
@@ -167,6 +170,8 @@ export default function CategoryCmsPage() {
|
||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
|
||||
const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null)
|
||||
const [isAddBlockModalOpen, setIsAddBlockModalOpen] = useState(false)
|
||||
const [isTranslationModalOpen, setIsTranslationModalOpen] = useState(false)
|
||||
const [translationModalTab, setTranslationModalTab] = useState<'export' | 'import'>('export')
|
||||
|
||||
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
|
||||
|
||||
@@ -671,12 +676,42 @@ export default function CategoryCmsPage() {
|
||||
{SECTION_HELP[sectionType] || 'Configure the section payload for this category detail page.'}
|
||||
</p>
|
||||
</div>
|
||||
{selectedSection?.isLocked && (
|
||||
<div className="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-night-950 px-3 py-2 text-xs font-bold uppercase tracking-widest text-slate-400">
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
Locked
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isTranslatableBlock(sectionType) && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTranslationModalTab('export')
|
||||
setIsTranslationModalOpen(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow border border-cyan-glow/20 text-xs font-bold transition-all shadow-sm cursor-pointer"
|
||||
title="ส่งออกข้อความทุกภาษาของบล็อกนี้เพื่อนำไปแปลด้วย LLM"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>Export ภาษา</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTranslationModalTab('import')
|
||||
setIsTranslationModalOpen(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-pop-pink/10 hover:bg-pop-pink/20 text-pop-pink border border-pop-pink/20 text-xs font-bold transition-all shadow-sm cursor-pointer"
|
||||
title="วาง JSON ผลลัพธ์จาก LLM เพื่ออัปเดตทุกภาษาพร้อมกัน"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>Import ภาษา</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{selectedSection?.isLocked && (
|
||||
<div className="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-night-950 px-3 py-2 text-xs font-bold uppercase tracking-widest text-slate-400">
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
Locked
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-[1fr_auto]">
|
||||
@@ -990,6 +1025,22 @@ export default function CategoryCmsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Multi-Language Translation Modal */}
|
||||
<BlockTranslationModal
|
||||
isOpen={isTranslationModalOpen}
|
||||
onClose={() => setIsTranslationModalOpen(false)}
|
||||
section={{
|
||||
id: typeof selectedSectionId === 'number' ? selectedSectionId : undefined,
|
||||
type: sectionType,
|
||||
payload: sectionPayload,
|
||||
}}
|
||||
initialTab={translationModalTab}
|
||||
onApplyTranslations={(updatedPayload) => {
|
||||
setSectionPayload(updatedPayload)
|
||||
setSectionJson(JSON.stringify(updatedPayload, null, 2))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -18,17 +18,19 @@ import {
|
||||
LayoutDashboard,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
FileJson,
|
||||
Check,
|
||||
Code2,
|
||||
Download,
|
||||
Sparkles,
|
||||
LayoutTemplate,
|
||||
Columns,
|
||||
Grid3X3,
|
||||
EyeOff
|
||||
EyeOff,
|
||||
Upload,
|
||||
} from 'lucide-react'
|
||||
import MediaLibraryModal from '../components/MediaLibraryModal'
|
||||
import CategoryMediaEditor from '../components/CategoryMediaEditor'
|
||||
import CategoryTranslationModal from '../components/category/CategoryTranslationModal'
|
||||
|
||||
const ADDITIONAL_LANGS = [
|
||||
{ code: 'ja', label: 'Japanese (日本語)', placeholder: '例: アロマキャンドル' },
|
||||
@@ -62,6 +64,9 @@ export default function CategoryEditPage() {
|
||||
const [showMoreLangs, setShowMoreLangs] = useState(false)
|
||||
const [successToast, setSuccessToast] = useState<string | null>(null)
|
||||
|
||||
const [isTranslationModalOpen, setIsTranslationModalOpen] = useState(false)
|
||||
const [translationModalTab, setTranslationModalTab] = useState<'export' | 'import'>('export')
|
||||
|
||||
// JSON Quick Import State
|
||||
const [isJsonBoxOpen, setIsJsonBoxOpen] = useState(false)
|
||||
const [jsonInput, setJsonInput] = useState('')
|
||||
@@ -473,6 +478,32 @@ export default function CategoryEditPage() {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* AI Multi-Language Translation Buttons */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTranslationModalTab('export')
|
||||
setIsTranslationModalOpen(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3.5 py-2.5 rounded-xl bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow border border-cyan-glow/20 text-xs font-bold transition-all shadow-sm cursor-pointer"
|
||||
title="ส่งออกชื่อหมวดหมู่ 8 ภาษาเพื่อนำไปแปลด้วย AI"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Export ภาษา</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTranslationModalTab('import')
|
||||
setIsTranslationModalOpen(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3.5 py-2.5 rounded-xl bg-pop-pink/10 hover:bg-pop-pink/20 text-pop-pink border border-pop-pink/20 text-xs font-bold transition-all shadow-sm cursor-pointer"
|
||||
title="วาง JSON ผลลัพธ์จาก AI เพื่ออัปเดตทุกภาษาพร้อมกัน"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span>Import ภาษา</span>
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to="/categories"
|
||||
className="px-5 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white text-sm font-semibold transition-all cursor-pointer"
|
||||
@@ -528,24 +559,50 @@ export default function CategoryEditPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JSON Paste Action Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsJsonBoxOpen(!isJsonBoxOpen)
|
||||
setJsonError(null)
|
||||
setJsonSuccess(null)
|
||||
}}
|
||||
className={`flex items-center gap-2 px-3.5 py-1.5 rounded-xl text-xs font-semibold transition-all border cursor-pointer ${
|
||||
isJsonBoxOpen
|
||||
? 'bg-cyan-glow text-night-950 border-cyan-glow font-bold shadow-md shadow-cyan-glow/20'
|
||||
: 'bg-white/5 hover:bg-cyan-glow/10 text-slate-300 hover:text-cyan-glow border-white/10 hover:border-cyan-glow/30'
|
||||
}`}
|
||||
title="Paste multilingual category names as JSON"
|
||||
>
|
||||
<FileJson className="w-4 h-4" />
|
||||
<span>{isJsonBoxOpen ? 'Hide JSON Box' : 'Paste Names JSON'}</span>
|
||||
</button>
|
||||
{/* Multilingual AI Translation Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTranslationModalTab('export')
|
||||
setIsTranslationModalOpen(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow border border-cyan-glow/20 text-xs font-bold transition-all cursor-pointer"
|
||||
title="ส่งออกชื่อหมวดหมู่ 8 ภาษาเพื่อนำไปแปลด้วย AI"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>Export ภาษา</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTranslationModalTab('import')
|
||||
setIsTranslationModalOpen(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-pop-pink/10 hover:bg-pop-pink/20 text-pop-pink border border-pop-pink/20 text-xs font-bold transition-all cursor-pointer"
|
||||
title="วาง JSON ผลลัพธ์จาก AI เพื่ออัปเดตทุกภาษาพร้อมกัน"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>Import ภาษา</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsJsonBoxOpen(!isJsonBoxOpen)
|
||||
setJsonError(null)
|
||||
setJsonSuccess(null)
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all border cursor-pointer ${
|
||||
isJsonBoxOpen
|
||||
? 'bg-white/10 text-white border-white/20'
|
||||
: 'bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white border-white/10'
|
||||
}`}
|
||||
title="Quick JSON Code Editor"
|
||||
>
|
||||
<Code2 className="w-3.5 h-3.5" />
|
||||
<span>{isJsonBoxOpen ? 'Hide Code' : 'JSON Code'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable JSON Input Box */}
|
||||
@@ -1038,6 +1095,22 @@ export default function CategoryEditPage() {
|
||||
setIsMediaModalOpen(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* AI Multi-Language Translation Modal */}
|
||||
<CategoryTranslationModal
|
||||
isOpen={isTranslationModalOpen}
|
||||
onClose={() => setIsTranslationModalOpen(false)}
|
||||
categoryName={name.th || name.en}
|
||||
categoryId={id}
|
||||
data={{ name }}
|
||||
initialTab={translationModalTab}
|
||||
onApplyTranslations={({ name: updatedName }) => {
|
||||
setName((prev) => ({ ...prev, ...updatedName }))
|
||||
setShowMoreLangs(true)
|
||||
setSuccessToast('อัปเดตชื่อหมวดหมู่ทุกภาษาเรียบร้อยแล้ว')
|
||||
setTimeout(() => setSuccessToast(null), 3000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user