This commit is contained in:
Your Name committed 2026-10-18 08:01:36 +07:00
1 parent dd79519bf7
commit 765cdd1fa9
11 files changed
+1007 -71

No files matched your search

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { API_BASE } from '@homaroma/types';
import { X, Upload, Loader2, Image as ImageIcon, Video } from 'lucide-react';
@@ -105,7 +106,7 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect, mediaType
if (!isOpen) return null;
return (
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-night-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden animate-fade-in relative">
{/* Header */}
@@ -196,6 +197,7 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect, mediaType
)}
</div>
</div>
</div>
</div>,
document.body
);
}
@@ -1,4 +1,5 @@
import { useState } from 'react'
import { createPortal } from 'react-dom'
import {
Sparkles,
Copy,
@@ -35,6 +36,8 @@ const GUIDE_OVERVIEW_MARKDOWN = `# คู่มือการใช้ AI/LLM
### 1. Root Items (เมนูแถวบนสุด - Level 1)
- **label**: ออบเจกต์ข้อความรองรับ 8 ภาษา \`{ th, en, ja, zh, vi, ko, lo, it }\`
- **url**: ลิงก์ปลายทาง เช่น \`/category/reed-diffuser\` หรือ \`/collections/all\`
- **layout**: (ไม่บังคับ) \`'columns'\` (Mega Menu คอลัมน์) | \`'tabbed'\` (Tabbed Catalog แท็บซ้าย) | \`'visual-cards'\` (การ์ดรูปภาพหรูหรา 4 ใบ) | \`'classic'\` (Dropdown แนวตั้งเรียบหรู กะทัดรัด)
- **layoutConfig**: (ไม่บังคับ) ออบเจกต์ตั้งค่าเพิ่มเติม \`{ columnCount: 2 | 3 | 4, showFeatured: boolean }\`
- **rootImageUrl**: URL รูปภาพหลักฝั่งซ้ายสุดของเมกะเมนู (อัตราส่วน 1:1 จัตุรัส ขนาดแนะนำ 800x800)
- **columns**: อาร์เรย์ของคอลัมน์เมนูย่อย (Level 2)
- **featured**: การ์ดสินค้า/ไฮไลต์โปรโมตพิเศษที่แสดงฝั่งขวาสุดของเมกะเมนู
@@ -304,7 +307,7 @@ export default function MenuJsonGuideModal({
}
}
return (
return createPortal(
<div className="fixed inset-0 z-50 bg-night-950/85 backdrop-blur-md flex items-center justify-center p-3 sm:p-5 animate-fade-in">
<div className="w-full max-w-5xl bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col max-h-[92vh] overflow-hidden animate-zoom-in">
{/* Header */}
@@ -491,6 +494,7 @@ export default function MenuJsonGuideModal({
</div>
</div>
</div>
</div>
</div>,
document.body
)
}
@@ -1,4 +1,5 @@
import { useState } from 'react'
import { createPortal } from 'react-dom'
import {
Sparkles,
Copy,
@@ -613,7 +614,7 @@ export default function ProductJsonGuideModal({ isOpen, onClose, onOpenImport }:
}
}
return (
return createPortal(
<div className="fixed inset-0 z-50 bg-night-950/85 backdrop-blur-md flex items-center justify-center p-3 sm:p-5 animate-fade-in">
<div className="w-full max-w-5xl bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col max-h-[92vh] overflow-hidden animate-zoom-in">
@@ -792,6 +793,7 @@ export default function ProductJsonGuideModal({ isOpen, onClose, onOpenImport }:
</div>
</div>
</div>
</div>,
document.body
)
}
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import {
X,
@@ -852,7 +853,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
return matchesCategory && matchesSearch;
});
return (
return createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md p-4 sm:p-6 animate-fade-in">
<div className="bg-night-900 border border-white/10 rounded-3xl shadow-2xl w-full max-w-6xl flex flex-col h-[90vh] max-h-[850px] overflow-hidden text-slate-200">
{/* Modal Header */}
@@ -978,6 +979,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
)}
</div>
</div>
</div>
</div>,
document.body
);
}
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { X, Search, Loader2 } from 'lucide-react';
import { API_BASE } from '@homaroma/types';
import { getAdminDisplayString } from '../../utils/localization';
@@ -63,7 +64,7 @@ export default function ProductPickerModal({
onClose();
};
return (
return createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-night-900/80 backdrop-blur-sm" onClick={onClose}></div>
<div className="relative bg-slate-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden flex flex-col h-[80vh]">
@@ -160,6 +161,7 @@ export default function ProductPickerModal({
</div>
</div>
</div>
</div>
</div>,
document.body
);
}
+4 -4
View File
@@ -116,18 +116,18 @@
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
to { opacity: 1; transform: none; }
}
@keyframes zoomIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
to { opacity: 1; transform: none; }
}
.animate-fade-in {
animation: fadeIn 0.4s ease-out forwards;
animation: fadeIn 0.4s ease-out;
}
.animate-zoom-in {
animation: zoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
animation: zoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@@ -1,9 +1,11 @@
import { useState, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { API_BASE, type Menu } from '@homaroma/types'
import { Save, Loader2, Plus, Trash2, ChevronUp, ChevronDown, Copy, Check, Download, Upload, Star, Pencil, Sparkles, Code2, X } from 'lucide-react'
import { Save, Loader2, Plus, Trash2, ChevronUp, ChevronDown, Copy, Check, Download, Upload, Star, Pencil, Sparkles, Code2, X, LayoutGrid, ListFilter, Layers, Image as ImageIcon } from 'lucide-react'
import { ICON_REGISTRY } from '../../utils/iconRegistry'
import FocalPointPicker from '../../components/page-builder/FocalPointPicker'
import MenuJsonGuideModal from '../../components/MenuJsonGuideModal'
import MediaLibraryModal from '../../components/MediaLibraryModal'
const IconSelect = ({ value, onChange }: { value?: string, onChange: (val: string) => void }) => {
return (
@@ -30,7 +32,7 @@ type MenuEditorPageProps = {
description: string;
}
const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
const FeaturedCardEditorFields = ({ featured, onChange, onOpenMediaPicker }: any) => {
const [activeTab, setActiveTab] = useState<'content' | 'media'>('content');
const [activeLang, setActiveLang] = useState('th');
@@ -54,8 +56,28 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
['ctaLabel', 'CTA label', 'View details'],
];
const isEnabled = featured.enabled !== false;
return (
<div className="space-y-4">
<div className="flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5">
<div>
<div className="text-xs font-bold text-white">Enable Featured Card</div>
<div className="text-[10px] text-slate-400">เปิดหรือปิดการแสดงการ์ดโปรโมตใน Mega Menu</div>
</div>
<button
type="button"
onClick={() => updateFeatured('enabled', !isEnabled)}
className={`px-3 py-1 text-xs font-bold rounded-lg border transition-colors ${
isEnabled
? 'border-amber-400/40 bg-amber-400/20 text-amber-300'
: 'border-white/10 bg-white/5 text-slate-400 hover:border-white/20'
}`}
>
{isEnabled ? 'Enabled (เปิด)' : 'Disabled (ปิด)'}
</button>
</div>
<div className="grid grid-cols-2 rounded-xl bg-white/5 p-1">
{[
['content', 'Content'],
@@ -136,22 +158,59 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
</div>
<div className="space-y-4 rounded-xl border border-white/10 bg-white/5 p-4">
<div className="aspect-[4/3] overflow-hidden rounded-xl border border-white/10 bg-white/5 flex items-center justify-center">
<div
onClick={() => onOpenMediaPicker && onOpenMediaPicker((url: string) => updateFeatured('imageUrl', url))}
className="aspect-[4/3] relative overflow-hidden rounded-xl border-2 border-dashed border-white/10 hover:border-cyan-glow/50 bg-night-950/40 flex items-center justify-center cursor-pointer group transition-all"
title="คลิกเพื่อเลือกรูปจากคลังภาพ (Media Library)"
>
{featured.imageUrl ? (
<img src={featured.imageUrl} alt="Featured preview" className="h-full w-full object-cover" />
<>
<img src={featured.imageUrl} alt="Featured preview" className="h-full w-full object-cover group-hover:scale-105 transition-transform duration-300" />
<div className="absolute inset-0 bg-night-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-1.5 backdrop-blur-xs">
<ImageIcon className="w-6 h-6 text-cyan-glow" />
<span className="text-xs font-bold text-white">เปลี่ยนรูปจากคลังภาพ</span>
</div>
</>
) : (
<span className="px-4 text-center text-xs text-slate-600">No featured image</span>
<div className="flex flex-col items-center gap-2 text-slate-500 group-hover:text-cyan-glow transition-colors py-4">
<ImageIcon className="w-8 h-8" />
<span className="text-xs font-bold">เลือกรูปจากคลังภาพ (Images Lib)</span>
<span className="text-[10px] text-slate-600">คลิกที่นี่เพื่อเปิดคลังสื่อของระบบ</span>
</div>
)}
</div>
<div className="space-y-3">
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold text-slate-500">Featured Image URL</label>
<input
className="admin-input w-full text-xs py-1 px-2"
value={featured.imageUrl || ''}
placeholder="https://cdn.example.com/product.webp"
onChange={e => updateFeatured('imageUrl', e.target.value || null)}
/>
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold text-slate-500">Featured Image URL</label>
{onOpenMediaPicker && (
<button
type="button"
onClick={() => onOpenMediaPicker((url: string) => updateFeatured('imageUrl', url))}
className="text-[10px] font-bold text-cyan-glow hover:underline flex items-center gap-1 cursor-pointer"
>
<ImageIcon className="w-3 h-3" /> เลือกจากคลังภาพ
</button>
)}
</div>
<div className="flex gap-2">
<input
className="admin-input w-full text-xs py-1 px-2"
value={featured.imageUrl || ''}
placeholder="https://cdn.example.com/product.webp"
onChange={e => updateFeatured('imageUrl', e.target.value || null)}
/>
{featured.imageUrl && (
<button
type="button"
onClick={() => updateFeatured('imageUrl', null)}
className="px-2.5 py-1 text-xs text-rose-400 hover:bg-rose-500/10 rounded border border-white/10 shrink-0 cursor-pointer"
title="ลบรูปภาพ"
>
Clear
</button>
)}
</div>
</div>
{featured.imageUrl && (
<div className="pt-2">
@@ -188,7 +247,7 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
);
}
const FeaturedCardSidePanel = ({ node, onChange }: any) => {
const FeaturedCardSidePanel = ({ node, onChange, onOpenMediaPicker }: any) => {
if (!node) {
return (
<div className="admin-card p-6 space-y-3">
@@ -223,12 +282,13 @@ const FeaturedCardSidePanel = ({ node, onChange }: any) => {
<FeaturedCardEditorFields
featured={featured}
onChange={(newFeatured: any) => onChange({ ...node, featured: newFeatured })}
onOpenMediaPicker={onOpenMediaPicker}
/>
</div>
);
}
const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured, isFeaturedSelected }: any) => {
const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured, isFeaturedSelected, onOpenMediaPicker }: any) => {
const childrenKey = type === 'root' ? 'columns' : 'links';
const labelKey = type === 'column' ? 'title' : 'label';
const hasUrl = type !== 'column';
@@ -300,21 +360,56 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
{/* Image URL field */}
<div className="flex items-start gap-3">
{/* Preview thumbnail */}
<div className="w-14 h-14 shrink-0 rounded-lg overflow-hidden border border-white/10 bg-white/5 flex items-center justify-center">
<div
onClick={() => onOpenMediaPicker && onOpenMediaPicker((url: string) => onChange({ ...node, [imageFieldKey]: url }))}
className="w-14 h-14 shrink-0 rounded-lg overflow-hidden border border-white/10 hover:border-cyan-glow/50 bg-white/5 flex items-center justify-center cursor-pointer group relative transition-colors"
title="คลิกเพื่อเลือกรูปจากคลังภาพ (Media Library)"
>
{imageValue ? (
<img src={imageValue} alt="preview" className="w-full h-full object-cover" />
<>
<img src={imageValue} alt="preview" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-night-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<ImageIcon className="w-4 h-4 text-cyan-glow" />
</div>
</>
) : (
<span className="text-[9px] text-slate-600 text-center leading-tight px-1">No Image</span>
<div className="flex flex-col items-center justify-center text-slate-600 group-hover:text-cyan-glow transition-colors">
<ImageIcon className="w-4 h-4 mb-0.5" />
<span className="text-[8px] text-center leading-tight">Library</span>
</div>
)}
</div>
<div className="flex-1 space-y-1">
<label className="text-[10px] uppercase font-bold text-slate-500">{imageFieldLabel}</label>
<input
className="admin-input w-full text-xs py-1 px-2"
value={imageValue}
placeholder="https://cdn.example.com/image.webp"
onChange={e => onChange({ ...node, [imageFieldKey]: e.target.value || null })}
/>
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold text-slate-500">{imageFieldLabel}</label>
{onOpenMediaPicker && (
<button
type="button"
onClick={() => onOpenMediaPicker((url: string) => onChange({ ...node, [imageFieldKey]: url }))}
className="text-[10px] font-bold text-cyan-glow hover:underline flex items-center gap-1 cursor-pointer"
>
<ImageIcon className="w-3 h-3" /> เลือกจากคลังภาพ
</button>
)}
</div>
<div className="flex gap-2">
<input
className="admin-input w-full text-xs py-1 px-2"
value={imageValue}
placeholder="https://cdn.example.com/image.webp"
onChange={e => onChange({ ...node, [imageFieldKey]: e.target.value || null })}
/>
{imageValue && (
<button
type="button"
onClick={() => onChange({ ...node, [imageFieldKey]: null })}
className="px-2 py-1 text-xs text-rose-400 hover:bg-rose-500/10 rounded border border-white/10 shrink-0 cursor-pointer"
title="ลบรูปภาพ"
>
Clear
</button>
)}
</div>
{type !== 'root' && (
<IconSelect
value={node.icon}
@@ -325,30 +420,193 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
</div>
{type === 'root' && (
<div className="rounded-xl border border-amber-400/20 bg-amber-400/5 p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-amber-400/10 text-amber-300">
<Star className="w-5 h-5" />
<div className="space-y-3">
{/* Layout Mode Selector */}
<div className="rounded-xl border border-white/10 bg-white/5 p-4 space-y-3">
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold text-slate-400 tracking-wider">
Sub-Menu Layout Preset
</label>
<span className="text-[10px] text-cyan-glow font-medium">
{node.layout === 'classic'
? 'Classic Dropdown'
: node.layout === 'tabbed'
? 'Tabbed Hierarchy'
: node.layout === 'visual-cards'
? 'Visual Cards'
: `Mega Menu (${node.layoutConfig?.columnCount || 2} Cols)`}
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<button
type="button"
onClick={() => onChange({ ...node, layout: 'columns' })}
className={`flex items-center gap-2 p-2.5 rounded-lg border text-left transition-colors ${
(!node.layout || node.layout === 'columns')
? 'border-cyan-500/50 bg-cyan-500/10 text-white shadow-sm'
: 'border-white/5 bg-night-900/40 text-slate-400 hover:border-white/20'
}`}
>
<LayoutGrid className="w-4 h-4 shrink-0 text-cyan-glow" />
<div className="min-w-0">
<div className="text-xs font-bold leading-tight truncate">Column Grid</div>
<div className="text-[10px] text-slate-500 leading-tight mt-0.5 truncate">2-4 คอลัมน์</div>
</div>
</button>
<button
type="button"
onClick={() => onChange({ ...node, layout: 'tabbed' })}
className={`flex items-center gap-2 p-2.5 rounded-lg border text-left transition-colors ${
node.layout === 'tabbed'
? 'border-cyan-500/50 bg-cyan-500/10 text-white shadow-sm'
: 'border-white/5 bg-night-900/40 text-slate-400 hover:border-white/20'
}`}
>
<Layers className="w-4 h-4 shrink-0 text-cyan-glow" />
<div className="min-w-0">
<div className="text-xs font-bold leading-tight truncate">Tabbed Catalog</div>
<div className="text-[10px] text-slate-500 leading-tight mt-0.5 truncate">แท็บซ้าย + ลิงก์ขวา</div>
</div>
</button>
<button
type="button"
onClick={() => onChange({ ...node, layout: 'visual-cards' })}
className={`flex items-center gap-2 p-2.5 rounded-lg border text-left transition-colors ${
node.layout === 'visual-cards'
? 'border-cyan-500/50 bg-cyan-500/10 text-white shadow-sm'
: 'border-white/5 bg-night-900/40 text-slate-400 hover:border-white/20'
}`}
>
<Sparkles className="w-4 h-4 shrink-0 text-cyan-glow" />
<div className="min-w-0">
<div className="text-xs font-bold leading-tight truncate">Visual Cards</div>
<div className="text-[10px] text-slate-500 leading-tight mt-0.5 truncate">การ์ดรูปถ่ายหรูหรา</div>
</div>
</button>
<button
type="button"
onClick={() => onChange({ ...node, layout: 'classic' })}
className={`flex items-center gap-2 p-2.5 rounded-lg border text-left transition-colors ${
node.layout === 'classic'
? 'border-cyan-500/50 bg-cyan-500/10 text-white shadow-sm'
: 'border-white/5 bg-night-900/40 text-slate-400 hover:border-white/20'
}`}
>
<ListFilter className="w-4 h-4 shrink-0 text-cyan-glow" />
<div className="min-w-0">
<div className="text-xs font-bold leading-tight truncate">Classic Dropdown</div>
<div className="text-[10px] text-slate-500 leading-tight mt-0.5 truncate">แนวตั้งมินิมอล</div>
</div>
</button>
</div>
{/* Submenu Layout Modifier / Description */}
{(!node.layout || node.layout === 'columns') && (
<div className="pt-3 border-t border-white/5 space-y-3">
{/* Column Count */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-300">จำนวนคอลัมน์ (Columns)</span>
<div className="flex rounded-lg bg-black/40 p-1 border border-white/5">
{[2, 3, 4].map((count) => {
const currentCols = node.layoutConfig?.columnCount || 2;
return (
<button
key={count}
type="button"
onClick={() => onChange({
...node,
layoutConfig: { ...(node.layoutConfig || {}), columnCount: count }
})}
className={`px-3 py-1 text-xs font-bold rounded-md transition-colors ${
currentCols === count
? 'bg-cyan-glow text-night-900'
: 'text-slate-400 hover:text-white'
}`}
>
{count} Cols
</button>
);
})}
</div>
</div>
{/* Toggle Featured Card */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-300">แสดง Featured Card</span>
<button
type="button"
onClick={() => {
const isCurrentlyShown = node.layoutConfig?.showFeatured !== false && featured.enabled !== false;
const nextState = !isCurrentlyShown;
onChange({
...node,
layoutConfig: { ...(node.layoutConfig || {}), showFeatured: nextState },
featured: { ...featured, enabled: nextState }
});
}}
className={`flex items-center gap-1.5 px-3 py-1 text-xs font-bold rounded-lg border transition-colors ${
(node.layoutConfig?.showFeatured !== false && featured.enabled !== false)
? 'border-amber-400/40 bg-amber-400/20 text-amber-300'
: 'border-white/10 bg-white/5 text-slate-400 hover:border-white/20'
}`}
>
{(node.layoutConfig?.showFeatured !== false && featured.enabled !== false) ? 'ON (เปิด)' : 'OFF (ปิด)'}
</button>
</div>
</div>
<div className="min-w-0">
<h3 className="text-xs font-bold uppercase tracking-widest text-amber-200">Featured card</h3>
<p className="truncate text-[11px] text-slate-500">
{featured.enabled === false ? 'Disabled' : featuredTitle}
</p>
)}
{node.layout === 'tabbed' && (
<div className="pt-3 border-t border-white/5 text-xs text-slate-400 leading-relaxed bg-white/2 p-2.5 rounded-lg">
💡 <strong className="text-slate-200">Tabbed Hierarchy:</strong> แต่ละ Column จะทำหน้าที่เป็นแท็บหมวดหมู่ฝั่งซ้าย เมื่อเอาเมาส์ Hover แท็บใด รายการ Links ย่อยจะปรากฏฝั่งขวาทันที เหมาะสำหรับร้านค้าที่มีหมวดหมู่แคตตาล็อกลึก (เช่น อะไหล่ยนต์, เครื่องใช้ไฟฟ้า, ไอที)
</div>
)}
{node.layout === 'visual-cards' && (
<div className="pt-3 border-t border-white/5 text-xs text-slate-400 leading-relaxed bg-white/2 p-2.5 rounded-lg">
✨ <strong className="text-slate-200">Visual Image Cards:</strong> แสดงเป็นการ์ดรูปถ่าย 4 ใบแนวนอน โดยดึงรูปภาพจาก Column Icon URL หรือ Hover Image ของลิงก์แรก เหมาะสำหรับสินค้าที่เน้นความงามและดีไซน์ (เช่น ก้านไม้หอม, เทียนหอม, คอลเลกชันแฟชั่น)
</div>
)}
{node.layout === 'classic' && (
<div className="pt-3 border-t border-white/5 text-xs text-slate-400 leading-relaxed bg-white/2 p-2.5 rounded-lg">
📄 <strong className="text-slate-200">Classic Dropdown:</strong> แสดงเป็นกล่องแนวตั้งกะทัดรัดใต้ปุ่มเมนูบน Navbar เหมาะสำหรับเมนูข้อมูลทั่วไปที่มี 3-7 ลิงก์ (เช่น เกี่ยวกับเรา, ศูนย์บริการ, ข่าวสาร)
</div>
)}
</div>
{/* Featured Card editor trigger (only shown when in columns mode and featured card is enabled) */}
{(!node.layout || node.layout === 'columns') && (node.layoutConfig?.showFeatured !== false && featured.enabled !== false) && (
<div className="rounded-xl border border-amber-400/20 bg-amber-400/5 p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-amber-400/10 text-amber-300">
<Star className="w-5 h-5" />
</div>
<div className="min-w-0">
<h3 className="text-xs font-bold uppercase tracking-widest text-amber-200">Featured card</h3>
<p className="truncate text-[11px] text-slate-500">
{featuredTitle}
</p>
</div>
</div>
<button
type="button"
onClick={onEditFeatured}
className={`flex shrink-0 items-center gap-2 rounded-lg px-3 py-2 text-xs font-bold transition-colors ${
isFeaturedSelected ? 'bg-amber-300 text-night-900' : 'bg-amber-400/10 text-amber-200 hover:bg-amber-400/20'
}`}
>
<Pencil className="w-3.5 h-3.5" />
{isFeaturedSelected ? 'Editing' : 'Edit'}
</button>
</div>
</div>
<button
type="button"
onClick={onEditFeatured}
className={`flex shrink-0 items-center gap-2 rounded-lg px-3 py-2 text-xs font-bold transition-colors ${
isFeaturedSelected ? 'bg-amber-300 text-night-900' : 'bg-amber-400/10 text-amber-200 hover:bg-amber-400/20'
}`}
>
<Pencil className="w-3.5 h-3.5" />
{isFeaturedSelected ? 'Editing' : 'Edit'}
</button>
</div>
)}
</div>
)}
</div>
@@ -383,6 +641,7 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
depth={depth + 1}
onChange={(newChild: any) => handleUpdateChild(i, newChild)}
onRemove={() => handleRemoveChild(i)}
onOpenMediaPicker={onOpenMediaPicker}
/>
))}
</div>
@@ -406,6 +665,13 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
const [pastedJsonText, setPastedJsonText] = useState('')
const [pasteError, setPasteError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
const [mediaSelectCallback, setMediaSelectCallback] = useState<((url: string) => void) | null>(null)
const handleOpenMediaPicker = (onSelectUrl: (url: string) => void) => {
setMediaSelectCallback(() => onSelectUrl)
setIsMediaModalOpen(true)
}
useEffect(() => {
Promise.all([
@@ -708,6 +974,7 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
}}
onEditFeatured={() => setSelectedFeaturedIndex(idx)}
isFeaturedSelected={safeFeaturedIndex === idx}
onOpenMediaPicker={handleOpenMediaPicker}
/>
))
)}
@@ -829,6 +1096,7 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
<FeaturedCardSidePanel
node={selectedFeaturedNode}
onChange={handleUpdateFeaturedNode}
onOpenMediaPicker={handleOpenMediaPicker}
/>
) : (
<div className="admin-card p-6 space-y-4">
@@ -884,7 +1152,7 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
)}
{/* Paste JSON Modal */}
{showPasteModal && (
{showPasteModal && createPortal(
<div className="fixed inset-0 z-50 bg-night-950/85 backdrop-blur-md flex items-center justify-center p-4 animate-fade-in">
<div className="w-full max-w-2xl bg-night-900 border border-white/10 rounded-2xl shadow-2xl p-6 space-y-4 animate-zoom-in">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
@@ -932,7 +1200,8 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
</button>
</div>
</div>
</div>
</div>,
document.body
)}
{/* LLM Mega Menu Guide Modal */}
@@ -942,6 +1211,23 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
onOpenPasteJson={() => setShowPasteModal(true)}
onOpenImportFile={() => fileInputRef.current?.click()}
/>
{/* Media Library Modal */}
<MediaLibraryModal
isOpen={isMediaModalOpen}
onClose={() => {
setIsMediaModalOpen(false)
setMediaSelectCallback(null)
}}
onSelect={(url) => {
if (mediaSelectCallback) {
mediaSelectCallback(url)
}
setIsMediaModalOpen(false)
setMediaSelectCallback(null)
}}
mediaType="image"
/>
</div>
)
}