x
This commit is contained in:
1 parent
af6047fb4d
commit
d9901fd9a6
3 files changed
+811
-3
No files matched your search
@@ -0,0 +1,694 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
FileJson,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
Sparkles,
|
||||||
|
Bot,
|
||||||
|
ArrowRight,
|
||||||
|
Package,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export 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' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export interface ProductTranslationData {
|
||||||
|
name: Record<string, string>;
|
||||||
|
description: Record<string, string>;
|
||||||
|
details: Record<string, string>;
|
||||||
|
options?: Array<{
|
||||||
|
name: Record<string, string>;
|
||||||
|
values: Array<Record<string, string> & { imageUrl?: string }>;
|
||||||
|
}>;
|
||||||
|
attributes?: Array<{
|
||||||
|
definitionId: number;
|
||||||
|
name: string;
|
||||||
|
translations: Record<string, string>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductTranslationApplyPayload {
|
||||||
|
name: Record<string, string>;
|
||||||
|
description: Record<string, string>;
|
||||||
|
details: Record<string, string>;
|
||||||
|
options?: Array<{
|
||||||
|
name: Record<string, string>;
|
||||||
|
values: Array<Record<string, string> & { imageUrl?: string }>;
|
||||||
|
}>;
|
||||||
|
attributes?: Array<{
|
||||||
|
definitionId: number;
|
||||||
|
translations: Record<string, string>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductTranslationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
productTitle?: string;
|
||||||
|
productId?: string | number;
|
||||||
|
data: ProductTranslationData;
|
||||||
|
initialTab?: 'export' | 'import';
|
||||||
|
onApplyTranslations: (updated: ProductTranslationApplyPayload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalesObject(val: any): 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) {
|
||||||
|
result[code] = val[code] || '';
|
||||||
|
} else {
|
||||||
|
result[code] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLocalesObject(existing: any, incoming: any): Record<string, string> {
|
||||||
|
const base: Record<string, string> =
|
||||||
|
typeof existing === 'object' && existing !== null
|
||||||
|
? { ...existing }
|
||||||
|
: typeof existing === 'string'
|
||||||
|
? { th: existing }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (typeof incoming === 'object' && incoming !== null) {
|
||||||
|
SUPPORTED_LOCALES.forEach(({ code }) => {
|
||||||
|
if (typeof incoming[code] === 'string' && incoming[code].trim()) {
|
||||||
|
base[code] = incoming[code].trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof incoming === 'string' && incoming.trim()) {
|
||||||
|
base.th = incoming.trim();
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFieldTranslations(obj: any, fieldKey: string): Record<string, string> {
|
||||||
|
const res: Record<string, string> = {};
|
||||||
|
if (!obj || typeof obj !== 'object') return res;
|
||||||
|
|
||||||
|
if (typeof obj[fieldKey] === 'object' && obj[fieldKey] !== null) {
|
||||||
|
SUPPORTED_LOCALES.forEach(({ code }) => {
|
||||||
|
res[code] = obj[fieldKey][code] || '';
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORTED_LOCALES.forEach(({ code }) => {
|
||||||
|
if (obj[code] && typeof obj[code] === 'object' && obj[code][fieldKey]) {
|
||||||
|
res[code] = obj[code][fieldKey] || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof obj[fieldKey] === 'string') {
|
||||||
|
res.th = obj[fieldKey];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductTranslationModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
productTitle,
|
||||||
|
productId,
|
||||||
|
data,
|
||||||
|
initialTab = 'export',
|
||||||
|
onApplyTranslations,
|
||||||
|
}: ProductTranslationModalProps) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}, [isOpen, initialTab]);
|
||||||
|
|
||||||
|
// Structured export object
|
||||||
|
const exportData = useMemo(() => {
|
||||||
|
const payload: Record<string, any> = {
|
||||||
|
name: getLocalesObject(data.name),
|
||||||
|
description: getLocalesObject(data.description),
|
||||||
|
details: getLocalesObject(data.details),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(data.options) && data.options.length > 0) {
|
||||||
|
payload.options = data.options.map((opt, idx) => ({
|
||||||
|
option: idx + 1,
|
||||||
|
name: getLocalesObject(opt.name),
|
||||||
|
values: opt.values.map((v, vIdx) => ({
|
||||||
|
valueIndex: vIdx + 1,
|
||||||
|
text: getLocalesObject(v),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data.attributes) && data.attributes.length > 0) {
|
||||||
|
payload.attributes = data.attributes.map((attr) => ({
|
||||||
|
definitionId: attr.definitionId,
|
||||||
|
name: attr.name,
|
||||||
|
translations: getLocalesObject(attr.translations),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const exportJsonString = useMemo(() => {
|
||||||
|
return JSON.stringify(exportData, null, 2);
|
||||||
|
}, [exportData]);
|
||||||
|
|
||||||
|
const aiPromptString = useMemo(() => {
|
||||||
|
return `คุณคือผู้เชี่ยวชาญด้านการแปลภาษาและเขียนคำบรรยายสินค้า (Copywriting & Localization) สำหรับแบรนด์เครื่องหอมและลักชัวรีไลฟ์สไตล์ (HomAroma)
|
||||||
|
กรุณาแปลข้อความของสินค้า "${productTitle || 'Product'}" ตามโครงสร้าง JSON ด้านล่างนี้ให้ครบทุกภาษาที่กำหนด ได้แก่:
|
||||||
|
- th: ภาษาไทย
|
||||||
|
- en: English
|
||||||
|
- ja: ภาษาญี่ปุ่น (日本語)
|
||||||
|
- zh: ภาษาจีนตัวย่อ (中文)
|
||||||
|
- vi: ภาษาเวียดนาม (Tiếng Việt)
|
||||||
|
- ko: ภาษาเกาหลี (한국어)
|
||||||
|
- lo: ภาษาลาว (ພາສາລາວ)
|
||||||
|
- it: ภาษาอิตาลี (Italiano)
|
||||||
|
|
||||||
|
ข้อกำหนดสำคัญ:
|
||||||
|
1. รักษาโครงสร้าง JSON และคีย์หลักทั้งหมด (name, description, details, options, attributes)
|
||||||
|
2. "name": ชื่อสินค้า แปลให้สละสลวย หรูหรา ไพเราะ
|
||||||
|
3. "description": คำบรรยายสรุปสั้น ๆ ของสินค้า ถ่ายทอดอารมณ์และเสน่ห์ของกลิ่นหอม
|
||||||
|
4. "details": รายละเอียดสินค้าแบบ Markdown (คงรูปแบบหัวข้อ ข้อย่อย และ Bullet points ไว้)
|
||||||
|
5. สไตล์การแปล: หรูหรา สง่างาม ผ่อนคลาย อบอุ่น (Quiet Luxury & Sensory Fragrance Tone)
|
||||||
|
6. ตอบกลับมาเฉพาะโค้ด JSON ที่ถูกต้องเท่านั้น (Valid JSON only) ห้ามใส่คำอธิบายเพิ่มเติม
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
${exportJsonString}
|
||||||
|
\`\`\``;
|
||||||
|
}, [productTitle, 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 = `product-${productId || '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 (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||||
|
return { error: 'รูปแบบ JSON ต้องเป็น Object' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = extractFieldTranslations(parsed, 'name');
|
||||||
|
const description = extractFieldTranslations(parsed, 'description');
|
||||||
|
const details = extractFieldTranslations(parsed, 'details');
|
||||||
|
|
||||||
|
// Parse options if present
|
||||||
|
let optionsResult: any = undefined;
|
||||||
|
if (Array.isArray(parsed.options)) {
|
||||||
|
optionsResult = parsed.options.map((opt: any) => ({
|
||||||
|
name: extractFieldTranslations(opt, 'name'),
|
||||||
|
values: Array.isArray(opt.values)
|
||||||
|
? opt.values.map((v: any) => extractFieldTranslations(v, 'text'))
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attributes if present
|
||||||
|
let attributesResult: any = undefined;
|
||||||
|
if (Array.isArray(parsed.attributes)) {
|
||||||
|
attributesResult = parsed.attributes.map((attr: any) => ({
|
||||||
|
definitionId: attr.definitionId,
|
||||||
|
name: attr.name,
|
||||||
|
translations: extractFieldTranslations(attr, 'translations'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language counts
|
||||||
|
const languageCounts: Record<string, number> = {};
|
||||||
|
SUPPORTED_LOCALES.forEach(({ code }) => {
|
||||||
|
let count = 0;
|
||||||
|
if (name[code]?.trim()) count++;
|
||||||
|
if (description[code]?.trim()) count++;
|
||||||
|
if (details[code]?.trim()) count++;
|
||||||
|
languageCounts[code] = count;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
details,
|
||||||
|
options: optionsResult,
|
||||||
|
attributes: attributesResult,
|
||||||
|
languageCounts,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return { error: err.message || 'JSON ไม่ถูกต้อง' };
|
||||||
|
}
|
||||||
|
}, [importText]);
|
||||||
|
|
||||||
|
const handleApplyImport = () => {
|
||||||
|
if (!parsedImport || parsedImport.error) {
|
||||||
|
setParseError('กรุณาวาง JSON ที่ถูกต้องก่อนกดอัปเดต');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedName = mergeLocalesObject(data.name, parsedImport.name);
|
||||||
|
const updatedDescription = mergeLocalesObject(data.description, parsedImport.description);
|
||||||
|
const updatedDetails = mergeLocalesObject(data.details, parsedImport.details);
|
||||||
|
|
||||||
|
let updatedOptions = data.options;
|
||||||
|
if (parsedImport.options && Array.isArray(data.options)) {
|
||||||
|
updatedOptions = data.options.map((opt, oIdx) => {
|
||||||
|
const transOpt = parsedImport.options[oIdx];
|
||||||
|
if (!transOpt) return opt;
|
||||||
|
const newName = mergeLocalesObject(opt.name, transOpt.name);
|
||||||
|
const newValues = opt.values.map((v, vIdx) => {
|
||||||
|
const transVal = transOpt.values?.[vIdx];
|
||||||
|
const merged = mergeLocalesObject(v, transVal);
|
||||||
|
return {
|
||||||
|
...merged,
|
||||||
|
...(v.imageUrl ? { imageUrl: v.imageUrl } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { name: newName, values: newValues };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedAttributes = data.attributes?.map((attr) => ({
|
||||||
|
definitionId: attr.definitionId,
|
||||||
|
translations: attr.translations,
|
||||||
|
}));
|
||||||
|
if (parsedImport.attributes && Array.isArray(data.attributes)) {
|
||||||
|
updatedAttributes = data.attributes.map((attr) => {
|
||||||
|
const transAttr = parsedImport.attributes.find((a: any) => a.definitionId === attr.definitionId);
|
||||||
|
if (!transAttr) return { definitionId: attr.definitionId, translations: attr.translations };
|
||||||
|
return {
|
||||||
|
definitionId: attr.definitionId,
|
||||||
|
translations: mergeLocalesObject(attr.translations, transAttr.translations),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onApplyTranslations({
|
||||||
|
name: updatedName,
|
||||||
|
description: updatedDescription,
|
||||||
|
details: updatedDetails,
|
||||||
|
options: updatedOptions,
|
||||||
|
attributes: updatedAttributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSuccessMsg('อัปเดตข้อมูลสินค้าทุกภาษาลงในฟอร์มสำเร็จแล้ว!');
|
||||||
|
setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 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">
|
||||||
|
<Package className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-lg font-bold text-white">
|
||||||
|
จัดการข้อมูลสินค้าหลายภาษา (Product Multi-Language JSON)
|
||||||
|
</h3>
|
||||||
|
{productId && (
|
||||||
|
<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: {productId}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
ส่งออกชื่อ, คำบรรยาย, รายละเอียด (Markdown), ตัวเลือกสินค้า ให้ 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>
|
||||||
|
{parsedImport && !parsedImport.error && (
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-5">
|
||||||
|
{successMsg && (
|
||||||
|
<div className="p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 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-400 hover:text-white border border-white/10 transition-all cursor-pointer"
|
||||||
|
title="ดาวน์โหลดไฟล์ .json"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Supported Languages Chips */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>ภาษาที่รองรับในระบบ (8 ภาษา):</span>
|
||||||
|
<span className="font-mono text-[10px] text-cyan-glow">
|
||||||
|
{productTitle || 'Product Catalog Item'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{SUPPORTED_LOCALES.map(({ code, label, full }) => (
|
||||||
|
<span
|
||||||
|
key={code}
|
||||||
|
className="px-2.5 py-1 rounded-lg bg-white/5 border border-white/10 text-[11px] font-medium text-slate-300 flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<span className="font-mono font-bold text-cyan-glow">{label}</span>
|
||||||
|
<span className="text-slate-500">({full})</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JSON Code Viewer */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-xs font-bold uppercase tracking-wider text-slate-400 flex items-center gap-1.5">
|
||||||
|
<FileJson className="w-3.5 h-3.5 text-cyan-glow" />
|
||||||
|
<span>โครงสร้าง JSON ปัจจุบันของสินค้านี้:</span>
|
||||||
|
</label>
|
||||||
|
<span className="text-[10px] text-slate-500 font-mono">
|
||||||
|
(UTF-8 Encoded)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative rounded-2xl bg-black/60 border border-white/10 overflow-hidden">
|
||||||
|
<pre className="p-4 text-xs font-mono text-cyan-200/90 overflow-x-auto max-h-72 leading-relaxed selection:bg-cyan-500/30">
|
||||||
|
{exportJsonString}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Import Instructions */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs font-bold text-slate-200 flex items-center gap-1.5">
|
||||||
|
<Upload className="w-4 h-4 text-pop-pink" />
|
||||||
|
<span>วางโค้ด JSON ที่ได้จาก AI (ChatGPT / Claude / Gemini):</span>
|
||||||
|
</label>
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
ระบบรองรับทั้งโค้ด JSON ธรรมดา และบล็อกโค้ดมาร์กดาวน์ (```json ... ```) พร้อมจับคู่ภาษาอัตโนมัติ
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Paste Textarea */}
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
value={importText}
|
||||||
|
onChange={(e) => {
|
||||||
|
setImportText(e.target.value);
|
||||||
|
setParseError(null);
|
||||||
|
setSuccessMsg(null);
|
||||||
|
}}
|
||||||
|
placeholder={`วางโค้ด JSON ข้อมูลสินค้าที่แปลแล้วที่นี่...\n\nตัวอย่าง:\n${exportJsonString.slice(0, 160)}...`}
|
||||||
|
rows={9}
|
||||||
|
className="w-full rounded-2xl bg-black/60 border border-white/10 focus:border-pop-pink/50 focus:ring-1 focus:ring-pop-pink/30 p-4 text-xs font-mono text-slate-200 placeholder:text-slate-600 transition-all outline-none resize-none leading-relaxed"
|
||||||
|
/>
|
||||||
|
{importText && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setImportText('');
|
||||||
|
setParseError(null);
|
||||||
|
}}
|
||||||
|
className="absolute top-3 right-3 p-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-slate-400 hover:text-white text-xs transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
ล้างข้อความ
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Status & Error Alert */}
|
||||||
|
{parseError && (
|
||||||
|
<div className="p-3.5 rounded-2xl bg-red-500/10 border border-red-500/30 text-red-300 flex items-center gap-2.5 text-xs animate-shake">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||||
|
<span>{parseError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parsedImport && parsedImport.error && (
|
||||||
|
<div className="p-3.5 rounded-2xl bg-amber-500/10 border border-amber-500/30 text-amber-300 flex items-center gap-2.5 text-xs animate-fade-in">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-400 shrink-0" />
|
||||||
|
<span>รูปแบบ JSON ไม่ถูกต้อง: {parsedImport.error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Live Preview & Stats */}
|
||||||
|
{parsedImport && !parsedImport.error && (
|
||||||
|
<div className="space-y-3 p-4 rounded-2xl bg-night-950/60 border border-emerald-500/20 animate-fade-in">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-xs font-bold text-slate-200">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>สรุปข้อมูลสินค้าที่จะอัปเดตลงฟอร์ม:</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
||||||
|
ตรวจพบชื่อ, คำบรรยาย, รายละเอียด
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Languages summary chips */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
|
{SUPPORTED_LOCALES.map(({ code, label }) => {
|
||||||
|
const count = parsedImport.languageCounts?.[code] || 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={code}
|
||||||
|
className={`p-2 rounded-xl border flex items-center justify-between text-xs ${
|
||||||
|
count > 0
|
||||||
|
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-300'
|
||||||
|
: 'bg-white/[0.02] border-white/5 text-slate-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-mono font-bold">{label}</span>
|
||||||
|
<span className="text-[10px]">
|
||||||
|
{count > 0 ? `${count}/3 ฟิลด์หลัก` : 'ไม่มี'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Preview Cards */}
|
||||||
|
<div className="mt-3 border-t border-white/5 pt-3 space-y-2">
|
||||||
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400 block mb-1">
|
||||||
|
ตัวอย่างข้อความที่ตรวจพบ (Preview):
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Name Preview */}
|
||||||
|
<div className="p-3 rounded-xl bg-white/[0.02] border border-white/5 text-xs space-y-1">
|
||||||
|
<span className="text-[10px] uppercase font-bold text-cyan-glow font-mono">
|
||||||
|
Product Name (ชื่อสินค้า)
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1 text-[11px] text-slate-300">
|
||||||
|
{parsedImport.name && SUPPORTED_LOCALES.filter((l) => parsedImport.name?.[l.code]).map((l) => (
|
||||||
|
<div key={l.code} className="truncate">
|
||||||
|
<span className="font-mono font-bold text-slate-500 mr-1.5 uppercase">
|
||||||
|
[{l.code}]
|
||||||
|
</span>
|
||||||
|
<span>{parsedImport.name?.[l.code]}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description Preview */}
|
||||||
|
<div className="p-3 rounded-xl bg-white/[0.02] border border-white/5 text-xs space-y-1">
|
||||||
|
<span className="text-[10px] uppercase font-bold text-pop-pink font-mono">
|
||||||
|
Short Description (คำบรรยายย่อ)
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1 text-[11px] text-slate-300">
|
||||||
|
{parsedImport.description && SUPPORTED_LOCALES.filter((l) => parsedImport.description?.[l.code]).map((l) => (
|
||||||
|
<div key={l.code} className="truncate">
|
||||||
|
<span className="font-mono font-bold text-slate-500 mr-1.5 uppercase">
|
||||||
|
[{l.code}]
|
||||||
|
</span>
|
||||||
|
<span>{parsedImport.description?.[l.code]}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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-medium text-slate-400 hover:text-white hover:bg-white/5 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{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)}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
Download,
|
||||||
Edit3,
|
Edit3,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
Star,
|
Star,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Upload,
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -36,6 +38,7 @@ import ProductHeroCarouselBlockForm from '../components/products/ProductHeroCaro
|
|||||||
import ProductDividerBlockForm from '../components/products/ProductDividerBlockForm'
|
import ProductDividerBlockForm from '../components/products/ProductDividerBlockForm'
|
||||||
import { validateProductHeroCarouselPayload } from '../components/products/productHeroCarouselValidation'
|
import { validateProductHeroCarouselPayload } from '../components/products/productHeroCarouselValidation'
|
||||||
import { getAdminDisplayString } from '../utils/localization'
|
import { getAdminDisplayString } from '../utils/localization'
|
||||||
|
import BlockTranslationModal, { isTranslatableBlock } from '../components/page-builder/BlockTranslationModal'
|
||||||
|
|
||||||
interface Brand {
|
interface Brand {
|
||||||
id: number
|
id: number
|
||||||
@@ -246,6 +249,8 @@ export default function ProductCmsPage() {
|
|||||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
|
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
|
||||||
const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null)
|
const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null)
|
||||||
const [isProductPickerOpen, setIsProductPickerOpen] = useState(false)
|
const [isProductPickerOpen, setIsProductPickerOpen] = useState(false)
|
||||||
|
const [isTranslationModalOpen, setIsTranslationModalOpen] = useState(false)
|
||||||
|
const [translationModalTab, setTranslationModalTab] = useState<'export' | 'import'>('export')
|
||||||
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
|
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
|
||||||
const selectedSection = selectedSectionId === 'new'
|
const selectedSection = selectedSectionId === 'new'
|
||||||
? null
|
? null
|
||||||
@@ -757,6 +762,35 @@ export default function ProductCmsPage() {
|
|||||||
{SECTION_HELP[sectionType] || 'Configure the section payload for this product detail page.'}
|
{SECTION_HELP[sectionType] || 'Configure the section payload for this product detail page.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 && (
|
{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">
|
<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" />
|
<Lock className="h-3.5 w-3.5" />
|
||||||
@@ -764,6 +798,7 @@ export default function ProductCmsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-[1fr_auto]">
|
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-[1fr_auto]">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -1369,6 +1404,21 @@ export default function ProductCmsPage() {
|
|||||||
onClose={() => setIsProductPickerOpen(false)}
|
onClose={() => setIsProductPickerOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -19,12 +19,15 @@ import {
|
|||||||
ArrowDown,
|
ArrowDown,
|
||||||
Monitor,
|
Monitor,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
RotateCcw
|
RotateCcw,
|
||||||
|
Download,
|
||||||
|
Upload
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { API_BASE, type AttributeDefinition, type Category, type Product, type ProductAttribute, type ProductImage, type Variant } from '@homaroma/types'
|
import { API_BASE, type AttributeDefinition, type Category, type Product, type ProductAttribute, type ProductImage, type Variant } from '@homaroma/types'
|
||||||
import MDEditor from '@uiw/react-md-editor'
|
import MDEditor from '@uiw/react-md-editor'
|
||||||
import MediaLibraryModal from '../components/MediaLibraryModal'
|
import MediaLibraryModal from '../components/MediaLibraryModal'
|
||||||
import ContentLanguageTabs from '../components/ContentLanguageTabs'
|
import ContentLanguageTabs from '../components/ContentLanguageTabs'
|
||||||
|
import ProductTranslationModal from '../components/products/ProductTranslationModal'
|
||||||
|
|
||||||
type EditableAttribute = {
|
type EditableAttribute = {
|
||||||
id?: number
|
id?: number
|
||||||
@@ -117,8 +120,10 @@ export default function ProductEditPage() {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
|
|
||||||
// Form State - 4 Core Languages
|
// Form State - Languages & Translations
|
||||||
const [activeLang, setActiveLang] = useState('th')
|
const [activeLang, setActiveLang] = useState('th')
|
||||||
|
const [isTranslationModalOpen, setIsTranslationModalOpen] = useState(false)
|
||||||
|
const [translationModalTab, setTranslationModalTab] = useState<'export' | 'import'>('export')
|
||||||
|
|
||||||
const [categoryId, setCategoryId] = useState<number>(0)
|
const [categoryId, setCategoryId] = useState<number>(0)
|
||||||
const [name, setName] = useState<Record<string, string>>({})
|
const [name, setName] = useState<Record<string, string>>({})
|
||||||
@@ -488,9 +493,33 @@ export default function ProductEditPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setTranslationModalTab('export')
|
||||||
|
setIsTranslationModalOpen(true)
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 px-4 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="ส่งออกข้อมูลสินค้าทุกภาษาเพื่อนำไปแปลด้วย AI (Export & Prompt)"
|
||||||
|
>
|
||||||
|
<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-4 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>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/products')}
|
onClick={() => navigate('/products')}
|
||||||
className="px-6 py-2.5 rounded-xl border border-white/5 bg-white/5 hover:bg-white/10 text-sm font-bold transition-all"
|
className="px-6 py-2.5 rounded-xl border border-white/5 bg-white/5 hover:bg-white/10 text-sm font-bold transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -1332,6 +1361,41 @@ export default function ProductEditPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ProductTranslationModal
|
||||||
|
isOpen={isTranslationModalOpen}
|
||||||
|
onClose={() => setIsTranslationModalOpen(false)}
|
||||||
|
productTitle={name[activeLang] || name.th || name.en || product?.brand?.name}
|
||||||
|
productId={id}
|
||||||
|
initialTab={translationModalTab}
|
||||||
|
data={{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
details,
|
||||||
|
options,
|
||||||
|
attributes: attributes.map((attr) => ({
|
||||||
|
definitionId: attr.attributeDefinitionId,
|
||||||
|
name: typeof attr.attributeDefinition?.label === 'string'
|
||||||
|
? attr.attributeDefinition.label
|
||||||
|
: (attr.attributeDefinition?.code || 'Attribute'),
|
||||||
|
translations: attr.translations || {},
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
onApplyTranslations={(updated) => {
|
||||||
|
if (updated.name) setName(updated.name)
|
||||||
|
if (updated.description) setDescription(updated.description)
|
||||||
|
if (updated.details) setDetails(updated.details)
|
||||||
|
if (updated.options) setOptions(updated.options)
|
||||||
|
if (updated.attributes) {
|
||||||
|
setAttributes((prev) =>
|
||||||
|
prev.map((attr) => {
|
||||||
|
const matched = updated.attributes?.find((a) => a.definitionId === attr.attributeDefinitionId)
|
||||||
|
return matched ? { ...attr, translations: matched.translations } : attr
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user