This commit is contained in:
Your Name committed 2026-10-04 18:59:27 +07:00
1 parent b641acbeb7
commit e349ef5b84
14 files changed
+720 -558

No files matched your search

@@ -7,6 +7,7 @@ import {
Smartphone,
Trash2,
} from 'lucide-react'
import ContentPositionPicker from '../page-builder/ContentPositionPicker'
type CategoryHeroCarouselBlockFormProps = {
payload: Record<string, unknown>
@@ -32,6 +33,8 @@ const createSlide = (): SlidePayload => ({
subtitle: {},
buttonText: {},
focalPoint: { x: '50%', y: '50%' },
buttonPosition: 'bottom-left',
mobileButtonPosition: '',
})
const getSlides = (payload: Record<string, unknown>): SlidePayload[] =>
@@ -253,6 +256,17 @@ export default function CategoryHeroCarouselBlockForm({
/>
</label>
</div>
{/* Slide Button & Content Position */}
<ContentPositionPicker
compact
defaultDesktop="bottom-left"
desktopPosition={getString(slide.buttonPosition, 'bottom-left')}
mobilePosition={getString(slide.mobileButtonPosition) || undefined}
onChangeDesktop={(pos) => updateSlide(slideIndex, { buttonPosition: pos })}
onChangeMobile={(pos) => updateSlide(slideIndex, { mobileButtonPosition: pos || '' })}
title="ตำแหน่งปุ่ม & ข้อความของสไลด์นี้ (Slide Content Position)"
/>
</article>
)
})}
@@ -1,6 +1,7 @@
import { UploadCloud, Image as ImageIcon } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
import FocalPointPicker from './FocalPointPicker';
import ContentPositionPicker from './ContentPositionPicker';
interface BannerFormProps {
editingSection: Partial<PageSection>;
@@ -97,6 +98,16 @@ export default function BannerForm({
className="admin-input w-full resize-none"
/>
</div>
<div className="md:col-span-2">
<ContentPositionPicker
defaultDesktop="center-left"
desktopPosition={editingSection.payload?.buttonPosition || 'center-left'}
mobilePosition={editingSection.payload?.mobileButtonPosition}
onChangeDesktop={(pos) => updatePayloadValue('buttonPosition', pos)}
onChangeMobile={(pos) => updatePayloadValue('mobileButtonPosition', pos)}
/>
</div>
</div>
);
}
@@ -0,0 +1,153 @@
import { Monitor, Smartphone, RotateCcw } from 'lucide-react';
export const GRID_POSITIONS = [
{ id: 'top-left', label: 'Top Left (บนซ้าย)', short: 'TL', icon: '↖' },
{ id: 'top-center', label: 'Top Center (บนกลาง)', short: 'TC', icon: '↑' },
{ id: 'top-right', label: 'Top Right (บนขวา)', short: 'TR', icon: '↗' },
{ id: 'center-left', label: 'Center Left (กลางซ้าย)', short: 'CL', icon: '←' },
{ id: 'center', label: 'Center (กึ่งกลาง)', short: 'C', icon: '•' },
{ id: 'center-right', label: 'Center Right (กลางขวา)', short: 'CR', icon: '→' },
{ id: 'bottom-left', label: 'Bottom Left (ล่างซ้าย)', short: 'BL', icon: '↙' },
{ id: 'bottom-center', label: 'Bottom Center (ล่างกลาง)', short: 'BC', icon: '↓' },
{ id: 'bottom-right', label: 'Bottom Right (ล่างขวา)', short: 'BR', icon: '↘' },
];
export interface ContentPositionPickerProps {
desktopPosition?: string;
mobilePosition?: string;
defaultDesktop?: string;
onChangeDesktop: (position: string) => void;
onChangeMobile: (position: string | undefined) => void;
title?: string;
subtitle?: string;
compact?: boolean;
}
export default function ContentPositionPicker({
desktopPosition,
mobilePosition,
defaultDesktop = 'center',
onChangeDesktop,
onChangeMobile,
title = 'ตำแหน่งปุ่ม & เนื้อหา (Button & Content Position)',
subtitle = 'สามารถแยกตำแหน่งระหว่าง Desktop (PC) และ Mobile (มือถือ) ได้อิสระ',
compact = false,
}: ContentPositionPickerProps) {
const effectiveDesktop = desktopPosition || defaultDesktop;
const currentMobilePos = mobilePosition || effectiveDesktop;
return (
<div className={`space-y-3 ${compact ? 'p-3 bg-night-950/60 rounded-xl border border-white/5' : 'pt-2'}`}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-1 border-b border-white/5 pb-2">
<div>
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-300">
{title}
</label>
{subtitle && (
<p className="text-[11px] text-slate-400 mt-0.5">
{subtitle}
</p>
)}
</div>
</div>
<div className={`grid grid-cols-1 ${compact ? 'gap-3 sm:grid-cols-2' : 'lg:grid-cols-2 gap-4'}`}>
{/* Desktop (PC) */}
<div className="bg-night-950/40 border border-white/5 p-3.5 rounded-2xl space-y-2.5">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-xs font-bold text-white uppercase tracking-wider">
<Monitor className="w-4 h-4 text-cyan-glow" />
Desktop (PC)
</span>
<span className="px-2 py-0.5 rounded-md bg-cyan-glow/10 text-cyan-glow font-mono font-bold text-[10px] border border-cyan-glow/20">
{effectiveDesktop}
</span>
</div>
<div className="flex items-center gap-3">
<div className="grid grid-cols-3 gap-1.5 p-1.5 bg-night-900 border border-white/10 rounded-xl w-28 h-28 shrink-0 shadow-inner">
{GRID_POSITIONS.map((pos) => {
const isSelected = effectiveDesktop === pos.id;
return (
<button
key={pos.id}
type="button"
onClick={() => onChangeDesktop(pos.id)}
className={`flex flex-col items-center justify-center rounded-lg transition-all cursor-pointer ${
isSelected
? 'bg-cyan-glow text-night-950 font-black shadow-md shadow-cyan-glow/20 scale-105 ring-1 ring-white/30'
: 'bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white'
}`}
title={`Desktop: ${pos.label}`}
>
<span className="text-xs leading-none">{pos.icon}</span>
<span className="text-[7px] font-mono mt-0.5">{pos.short}</span>
</button>
);
})}
</div>
<p className="text-[10px] text-slate-400 leading-relaxed">
ตำแหน่งแสดงบนหน้าจอคอมพิวเตอร์ / จอกว้าง (&ge; 768px)
</p>
</div>
</div>
{/* Mobile (Smartphone) */}
<div className="bg-night-950/40 border border-white/5 p-3.5 rounded-2xl space-y-2.5">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-xs font-bold text-white uppercase tracking-wider">
<Smartphone className="w-4 h-4 text-pop-pink" />
Mobile (มือถือ)
</span>
<div className="flex items-center gap-1.5">
{mobilePosition && (
<button
type="button"
onClick={() => onChangeMobile(undefined)}
className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-white px-2 py-0.5 rounded bg-white/5 hover:bg-white/10 border border-white/10 transition-colors cursor-pointer"
title="รีเซ็ตให้เหมือน Desktop"
>
<RotateCcw className="w-3 h-3" />
ตาม PC
</button>
)}
<span className="px-2 py-0.5 rounded-md bg-pop-pink/10 text-pop-pink font-mono font-bold text-[10px] border border-pop-pink/20">
{mobilePosition || `${effectiveDesktop} (ตาม PC)`}
</span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="grid grid-cols-3 gap-1.5 p-1.5 bg-night-900 border border-white/10 rounded-xl w-28 h-28 shrink-0 shadow-inner">
{GRID_POSITIONS.map((pos) => {
const isSelected = currentMobilePos === pos.id;
const isExplicit = mobilePosition === pos.id;
return (
<button
key={pos.id}
type="button"
onClick={() => onChangeMobile(pos.id)}
className={`flex flex-col items-center justify-center rounded-lg transition-all cursor-pointer ${
isSelected
? isExplicit
? 'bg-pop-pink text-white font-black shadow-md shadow-pop-pink/20 scale-105 ring-1 ring-white/30'
: 'bg-pop-pink/40 text-white font-bold ring-1 ring-pop-pink/40'
: 'bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white'
}`}
title={`Mobile: ${pos.label}`}
>
<span className="text-xs leading-none">{pos.icon}</span>
<span className="text-[7px] font-mono mt-0.5">{pos.short}</span>
</button>
);
})}
</div>
<p className="text-[10px] text-slate-400 leading-relaxed">
ตำแหน่งแสดงบนโทรศัพท์มือถือ (หากไม่เลือก จะใช้ตาม Desktop)
</p>
</div>
</div>
</div>
</div>
);
}
@@ -1,6 +1,7 @@
import React from 'react';
import { Plus, Trash2, UploadCloud, Image as ImageIcon, Sliders } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
import ContentPositionPicker from './ContentPositionPicker';
interface HeroCarouselFormProps {
editingSection: Partial<PageSection>;
@@ -318,7 +319,7 @@ export default function HeroCarouselForm({
</div>
{/* Accessibility Alt & Optional Overlay Text */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-2 border-t border-white/5">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 pt-2 border-t border-white/5">
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">
Alt Text
@@ -357,7 +358,31 @@ export default function HeroCarouselForm({
className="admin-input w-full text-xs"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">
Button Text (Optional)
</label>
<input
type="text"
value={getLocalizedSlideValue(slide, 'buttonText')}
onChange={(e) => updateLocalizedSlideValue(idx, 'buttonText', e.target.value)}
placeholder="e.g. Shop Now"
className="admin-input w-full text-xs"
/>
</div>
</div>
{/* Slide Button & Content Position */}
<ContentPositionPicker
compact
defaultDesktop="bottom-left"
desktopPosition={slide.buttonPosition}
mobilePosition={slide.mobileButtonPosition}
onChangeDesktop={(pos) => updateSlidePayloadValue(idx, 'buttonPosition', pos)}
onChangeMobile={(pos) => updateSlidePayloadValue(idx, 'mobileButtonPosition', pos || '')}
title="ตำแหน่งปุ่ม & ข้อความของสไลด์นี้ (Slide Content Position)"
/>
</div>
))}
</div>
@@ -1,6 +1,7 @@
import { UploadCloud, Image as ImageIcon, Monitor, Smartphone, RotateCcw, Layers, Sliders } from 'lucide-react';
import { UploadCloud, Image as ImageIcon, Layers, Sliders } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
import FocalPointPicker from './FocalPointPicker';
import ContentPositionPicker from './ContentPositionPicker';
interface HeroFormProps {
editingSection: Partial<PageSection>;
@@ -12,18 +13,6 @@ interface HeroFormProps {
setIsMediaModalOpen: (open: boolean) => void;
}
const GRID_POSITIONS = [
{ id: 'top-left', label: 'Top Left (บนซ้าย)', short: 'TL', icon: '↖' },
{ id: 'top-center', label: 'Top Center (บนกลาง)', short: 'TC', icon: '↑' },
{ id: 'top-right', label: 'Top Right (บนขวา)', short: 'TR', icon: '↗' },
{ id: 'center-left', label: 'Center Left (กลางซ้าย)', short: 'CL', icon: '←' },
{ id: 'center', label: 'Center (กึ่งกลาง)', short: 'C', icon: '•' },
{ id: 'center-right', label: 'Center Right (กลางขวา)', short: 'CR', icon: '→' },
{ id: 'bottom-left', label: 'Bottom Left (ล่างซ้าย)', short: 'BL', icon: '↙' },
{ id: 'bottom-center', label: 'Bottom Center (ล่างกลาง)', short: 'BC', icon: '↓' },
{ id: 'bottom-right', label: 'Bottom Right (ล่างขวา)', short: 'BR', icon: '↘' },
];
const OVERLAY_PRESETS = [
{ label: 'None (0%)', value: 0 },
{ label: 'Light (25%)', value: 25 },
@@ -237,116 +226,13 @@ export default function HeroForm({
</div>
{/* 9-Box Grid Button Position Selector (Desktop & Mobile) */}
<div className="md:col-span-2 space-y-4 pt-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-white/5 pb-2">
<div>
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-300">
ตำแหน่งปุ่ม & เนื้อหา (Button & Content Position)
</label>
<p className="text-[11px] text-slate-400 mt-0.5">
สามารถแยกตำแหน่งระหว่าง <strong>Desktop (PC)</strong> และ <strong>Mobile (มือถือ)</strong> ได้อิสระ
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Desktop (PC) */}
<div className="bg-night-950/40 border border-white/5 p-4 rounded-2xl space-y-3">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-xs font-bold text-white uppercase tracking-wider">
<Monitor className="w-4 h-4 text-cyan-glow" />
Desktop (PC)
</span>
<span className="px-2 py-0.5 rounded-md bg-cyan-glow/10 text-cyan-glow font-mono font-bold text-[10px] border border-cyan-glow/20">
{editingSection.payload?.buttonPosition || 'center'}
</span>
</div>
<div className="flex items-center gap-4">
<div className="grid grid-cols-3 gap-1.5 p-1.5 bg-night-900 border border-white/10 rounded-xl w-32 h-32 shrink-0 shadow-inner">
{GRID_POSITIONS.map((pos) => {
const isSelected = (editingSection.payload?.buttonPosition || 'center') === pos.id;
return (
<button
key={pos.id}
type="button"
onClick={() => updatePayloadValue('buttonPosition', pos.id)}
className={`flex flex-col items-center justify-center rounded-lg transition-all cursor-pointer ${
isSelected
? 'bg-cyan-glow text-night-950 font-black shadow-md shadow-cyan-glow/20 scale-105 ring-1 ring-white/30'
: 'bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white'
}`}
title={`Desktop: ${pos.label}`}
>
<span className="text-xs leading-none">{pos.icon}</span>
<span className="text-[7px] font-mono mt-0.5">{pos.short}</span>
</button>
);
})}
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
ตำแหน่งที่จะแสดงเมื่อเปิดบนหน้าจอคอมพิวเตอร์ / จอกว้าง (ขนาด &ge; 768px)
</p>
</div>
</div>
{/* Mobile (Smartphone) */}
<div className="bg-night-950/40 border border-white/5 p-4 rounded-2xl space-y-3">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-xs font-bold text-white uppercase tracking-wider">
<Smartphone className="w-4 h-4 text-pop-pink" />
Mobile (มือถือ)
</span>
<div className="flex items-center gap-1.5">
{editingSection.payload?.mobileButtonPosition && (
<button
type="button"
onClick={() => updatePayloadValue('mobileButtonPosition', undefined)}
className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-white px-2 py-0.5 rounded bg-white/5 hover:bg-white/10 border border-white/10 transition-colors"
title="รีเซ็ตให้เหมือน Desktop"
>
<RotateCcw className="w-3 h-3" />
ตาม PC
</button>
)}
<span className="px-2 py-0.5 rounded-md bg-pop-pink/10 text-pop-pink font-mono font-bold text-[10px] border border-pop-pink/20">
{editingSection.payload?.mobileButtonPosition || `${editingSection.payload?.buttonPosition || 'center'} (ตาม PC)`}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="grid grid-cols-3 gap-1.5 p-1.5 bg-night-900 border border-white/10 rounded-xl w-32 h-32 shrink-0 shadow-inner">
{GRID_POSITIONS.map((pos) => {
const currentMobilePos = editingSection.payload?.mobileButtonPosition || editingSection.payload?.buttonPosition || 'center';
const isSelected = currentMobilePos === pos.id;
const isExplicit = editingSection.payload?.mobileButtonPosition === pos.id;
return (
<button
key={pos.id}
type="button"
onClick={() => updatePayloadValue('mobileButtonPosition', pos.id)}
className={`flex flex-col items-center justify-center rounded-lg transition-all cursor-pointer ${
isSelected
? isExplicit
? 'bg-pop-pink text-white font-black shadow-md shadow-pop-pink/20 scale-105 ring-1 ring-white/30'
: 'bg-pop-pink/40 text-white font-bold ring-1 ring-pop-pink/40'
: 'bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white'
}`}
title={`Mobile: ${pos.label}`}
>
<span className="text-xs leading-none">{pos.icon}</span>
<span className="text-[7px] font-mono mt-0.5">{pos.short}</span>
</button>
);
})}
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
ตำแหน่งที่จะแสดงเมื่อเปิดบนโทรศัพท์มือถือ (หากไม่เลือก จะใช้ตาม Desktop อัตโนมัติ)
</p>
</div>
</div>
</div>
<div className="md:col-span-2">
<ContentPositionPicker
desktopPosition={editingSection.payload?.buttonPosition}
mobilePosition={editingSection.payload?.mobileButtonPosition}
onChangeDesktop={(pos) => updatePayloadValue('buttonPosition', pos)}
onChangeMobile={(pos) => updatePayloadValue('mobileButtonPosition', pos)}
/>
</div>
</div>
);
@@ -1,6 +1,7 @@
import React from 'react';
import { UploadCloud, Image as ImageIcon, Video } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
import ContentPositionPicker from './ContentPositionPicker';
interface HeroVideoFormProps {
editingSection: Partial<PageSection>;
@@ -291,6 +292,14 @@ export default function HeroVideoForm({
</div>
</div>
</div>
{/* Button & Content Position (9-Box Grid for Desktop & Mobile) */}
<ContentPositionPicker
desktopPosition={payload.buttonPosition}
mobilePosition={payload.mobileButtonPosition}
onChangeDesktop={(pos) => updatePayloadValue('buttonPosition', pos)}
onChangeMobile={(pos) => updatePayloadValue('mobileButtonPosition', pos)}
/>
</div>
);
}
@@ -7,6 +7,7 @@ import {
Smartphone,
Trash2,
} from 'lucide-react'
import ContentPositionPicker from '../page-builder/ContentPositionPicker'
type ProductHeroCarouselBlockFormProps = {
payload: Record<string, unknown>
@@ -32,6 +33,8 @@ const createSlide = (): SlidePayload => ({
subtitle: {},
buttonText: {},
focalPoint: { x: '50%', y: '50%' },
buttonPosition: 'bottom-left',
mobileButtonPosition: '',
})
const getSlides = (payload: Record<string, unknown>): SlidePayload[] =>
@@ -253,6 +256,17 @@ export default function ProductHeroCarouselBlockForm({
/>
</label>
</div>
{/* Slide Button & Content Position */}
<ContentPositionPicker
compact
defaultDesktop="bottom-left"
desktopPosition={getString(slide.buttonPosition, 'bottom-left')}
mobilePosition={getString(slide.mobileButtonPosition) || undefined}
onChangeDesktop={(pos) => updateSlide(slideIndex, { buttonPosition: pos })}
onChangeMobile={(pos) => updateSlide(slideIndex, { mobileButtonPosition: pos || '' })}
title="ตำแหน่งปุ่ม & ข้อความของสไลด์นี้ (Slide Content Position)"
/>
</article>
)
})}