x
This commit is contained in:
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
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user