This commit is contained in:
Your Name committed 2026-06-15 05:17:39 -04:00
1 parent 8c0b5a2a34
commit 2c66025842
13 files changed
+1421 -1043

No files matched your search

@@ -0,0 +1,140 @@
import React from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
interface AccordionFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
getLocalizedAccordionValue: (item: any, field: string) => string;
updateAccordionCategory: (catIdx: number, field: string, value: string) => void;
updateAccordionQuestion: (catIdx: number, qIdx: number, field: string, value: string) => void;
}
export default function AccordionForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
getLocalizedAccordionValue,
updateAccordionCategory,
updateAccordionQuestion,
}: AccordionFormProps) {
return (
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Categories & Questions</label>
<button
type="button"
onClick={() => {
const categories = [...(editingSection.payload?.categories || []), { icon: '✨', name: { th: 'หมวดหมู่ใหม่' }, questions: [] }];
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
}}
className="text-xs text-cyan-glow hover:underline flex items-center gap-1"
>
<Plus className="w-3.5 h-3.5" /> Add Category
</button>
</div>
{(editingSection.payload?.categories || []).map((cat: any, cIdx: number) => (
<div key={cIdx} className="p-5 border border-white/10 bg-white/[0.02] rounded-2xl space-y-4">
<div className="flex items-center gap-3">
<input
type="text"
value={cat.icon || ''}
onChange={(e) => updateAccordionCategory(cIdx, 'icon', e.target.value)}
className="admin-input w-16 text-center text-xl"
placeholder="Emoji"
/>
<input
type="text"
value={getLocalizedAccordionValue(cat, 'name')}
onChange={(e) => updateAccordionCategory(cIdx, 'name', e.target.value)}
className="admin-input flex-1"
placeholder="Category Name"
/>
<button
type="button"
onClick={() => {
const categories = [...editingSection.payload.categories];
categories.splice(cIdx, 1);
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
}}
className="p-2.5 bg-rose-500/10 text-rose-500 rounded-xl hover:bg-rose-500 hover:text-white transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="pl-4 border-l-2 border-white/5 space-y-3">
{(cat.questions || []).map((q: any, qIdx: number) => (
<div key={qIdx} className="p-4 bg-night-950/40 rounded-xl border border-white/5 space-y-3">
<div className="flex items-start gap-3">
<div className="flex-1 space-y-3">
<div>
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 mb-1 block">Question (Q)</label>
<input
type="text"
value={getLocalizedAccordionValue(q, 'q')}
onChange={(e) => updateAccordionQuestion(cIdx, qIdx, 'q', e.target.value)}
className="admin-input w-full text-xs"
placeholder="What is..."
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 mb-1 block">Answer (A)</label>
<textarea
rows={2}
value={getLocalizedAccordionValue(q, 'a')}
onChange={(e) => updateAccordionQuestion(cIdx, qIdx, 'a', e.target.value)}
className="admin-input w-full text-xs resize-none"
placeholder="Provide the answer here..."
/>
</div>
</div>
<button
type="button"
onClick={() => {
const categories = [...editingSection.payload.categories];
categories[cIdx].questions.splice(qIdx, 1);
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
}}
className="mt-5 p-2 text-rose-500/50 hover:text-rose-500 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
<button
type="button"
onClick={() => {
const categories = [...editingSection.payload.categories];
categories[cIdx].questions = [...(categories[cIdx].questions || []), { q: { th: 'คำถามใหม่' }, a: { th: 'คำตอบใหม่' } }];
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
}}
className="text-[10px] text-cyan-glow/80 hover:text-cyan-glow uppercase tracking-widest font-bold flex items-center gap-1"
>
<Plus className="w-3 h-3" /> Add Question
</button>
</div>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,123 @@
import React from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
interface AnnouncementBarFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedAnnouncementSlide: (slide: any) => string;
updateLocalizedAnnouncementSlide: (idx: number, value: string) => void;
}
export default function AnnouncementBarForm({
editingSection,
setEditingSection,
getLocalizedAnnouncementSlide,
updateLocalizedAnnouncementSlide,
}: AnnouncementBarFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
<input
type="text"
value={editingSection.payload?.backgroundColor || '#1F3D2D'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
})
}
className="admin-input w-full"
placeholder="e.g. #1F3D2D or HSL value"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
<input
type="text"
value={editingSection.payload?.textColor || '#FDFDF9'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
})
}
className="admin-input w-full"
placeholder="e.g. #FDFDF9"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Slide Speed (ms)</label>
<input
type="number"
min={1000}
step={500}
value={editingSection.payload?.speed || 3000}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), speed: parseInt(e.target.value) }
})
}
className="admin-input w-full"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Announcement Slides</label>
<button
type="button"
onClick={() => {
const currentSlides = editingSection.payload?.slides || [];
setEditingSection({
...editingSection,
payload: {
...(editingSection.payload || {}),
slides: [...currentSlides, 'New announcement message']
}
});
}}
className="text-xs text-cyan-glow hover:underline flex items-center gap-1"
>
<Plus className="w-3.5 h-3.5" /> Add Slide
</button>
</div>
<div className="space-y-3">
{(editingSection.payload?.slides || []).map((slide: string, idx: number) => (
<div key={idx} className="flex items-center gap-3">
<input
type="text"
value={getLocalizedAnnouncementSlide(slide)}
onChange={(e) => updateLocalizedAnnouncementSlide(idx, e.target.value)}
className="admin-input flex-1"
placeholder="Slide message text..."
/>
<button
type="button"
onClick={() => {
const newSlides = (editingSection.payload?.slides || []).filter((_: any, i: number) => i !== idx);
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), slides: newSlides }
});
}}
className="p-3 bg-rose-500/10 border border-rose-500/10 rounded-xl text-rose-500 hover:bg-rose-500 hover:text-white transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{(editingSection.payload?.slides || []).length === 0 && (
<p className="text-xs text-slate-500 italic">No slides added yet. Click Add Slide above.</p>
)}
</div>
</div>
</div>
);
}
@@ -0,0 +1,89 @@
import { UploadCloud, Image as ImageIcon } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
interface BannerFormProps {
editingSection: Partial<PageSection>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void;
setIsMediaModalOpen: (open: boolean) => void;
}
export default function BannerForm({
editingSection,
getLocalizedValue,
updateLocalizedValue,
setUploadTarget,
setIsMediaModalOpen,
}: BannerFormProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Banner Image uploading */}
<div className="md:col-span-2 space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Banner Image</label>
<div
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-dashed border-white/10 bg-night-950/50 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all"
onClick={() => {
setUploadTarget({ type: 'main' });
setIsMediaModalOpen(true);
}}
>
{editingSection.payload?.imageUrl ? (
<>
<img src={editingSection.payload.imageUrl} className="w-full h-full object-cover" alt="Banner" />
<div className="absolute inset-0 bg-night-950/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
<UploadCloud className="w-10 h-10 text-white" />
</div>
</>
) : (
<div className="text-center space-y-2">
<ImageIcon className="w-10 h-10 text-slate-700 mx-auto" />
<p className="text-xs text-slate-500 font-bold uppercase tracking-widest">Select Image</p>
</div>
)}
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Banner Title</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Button Text</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'buttonText')}
onChange={(e) => updateLocalizedValue('buttonText', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Link URL</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'linkUrl')}
onChange={(e) => updateLocalizedValue('linkUrl', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Description</label>
<textarea
rows={2}
value={getLocalizedValue(editingSection.payload, 'description')}
onChange={(e) => updateLocalizedValue('description', e.target.value)}
className="admin-input w-full resize-none"
/>
</div>
</div>
);
}
@@ -0,0 +1,81 @@
import React from 'react';
import { type PageSection, type Category } from '@homaroma/types';
interface CategoriesFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
categories: Category[];
}
export default function CategoriesForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
categories,
}: CategoriesFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Subtitle</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'subtitle')}
onChange={(e) => updateLocalizedValue('subtitle', e.target.value)}
className="admin-input w-full"
/>
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400 block">Choose Categories to display (leave empty for all)</label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{categories.map((cat) => {
const ids = editingSection.payload?.categoryIds || [];
const checked = ids.includes(cat.id);
return (
<label
key={cat.id}
className={`flex items-center gap-3 p-3 border rounded-xl cursor-pointer select-none transition-all ${
checked
? 'bg-cyan-glow/10 border-cyan-glow/30 text-white'
: 'bg-white/5 border-white/5 text-slate-400 hover:bg-white/10'
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => {
const newIds = checked
? ids.filter((id: number) => id !== cat.id)
: [...ids, cat.id];
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), categoryIds: newIds }
});
}}
className="accent-cyan-glow"
/>
<span className="text-xs font-bold font-mono">{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.th || (cat.name as any)?.en || ''}</span>
</label>
);
})}
</div>
</div>
</div>
);
}
@@ -0,0 +1,40 @@
import { type PageSection } from '@homaroma/types';
interface ContactFormFormProps {
editingSection: Partial<PageSection>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
}
export default function ContactFormForm({
editingSection,
getLocalizedValue,
updateLocalizedValue,
}: ContactFormFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Subtitle</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'subtitle')}
onChange={(e) => updateLocalizedValue('subtitle', e.target.value)}
className="admin-input w-full"
/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,80 @@
import React from 'react';
import { type PageSection } from '@homaroma/types';
interface ContactInfoFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
}
export default function ContactInfoForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
}: ContactInfoFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Email</label>
<input
type="text"
value={editingSection.payload?.email || ''}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), email: e.target.value }
})
}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Line ID</label>
<input
type="text"
value={editingSection.payload?.line || ''}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), line: e.target.value }
})
}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Business Hours</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'hours')}
onChange={(e) => updateLocalizedValue('hours', e.target.value)}
className="admin-input w-full"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Address</label>
<textarea
rows={3}
value={getLocalizedValue(editingSection.payload, 'address')}
onChange={(e) => updateLocalizedValue('address', e.target.value)}
className="admin-input w-full resize-none"
/>
</div>
</div>
);
}
@@ -0,0 +1,186 @@
import React from 'react';
import { Plus, Trash2, UploadCloud, Image as ImageIcon } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
interface EditorialCarouselFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
getLocalizedSlideValue: (slide: any, field: string) => string;
updateLocalizedSlideValue: (idx: number, field: string, value: string) => void;
setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void;
setIsMediaModalOpen: (open: boolean) => void;
}
export default function EditorialCarouselForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
getLocalizedSlideValue,
updateLocalizedSlideValue,
setUploadTarget,
setIsMediaModalOpen,
}: EditorialCarouselFormProps) {
return (
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Carousel Section Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
<p className="text-[9px] text-slate-500 font-medium">Use [brackets] for golden accent serif text.</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-white/5 pb-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Editorial Slides</label>
<button
type="button"
onClick={() => {
const currentSlides = editingSection.payload?.slides || [];
setEditingSection({
...editingSection,
payload: {
...(editingSection.payload || {}),
slides: [
...currentSlides,
{
category: 'Category',
title: 'Slide Title',
description: 'Slide Description text...',
imageUrl: '/images/oil_ozone.png',
buttonText: '',
buttonLink: ''
}
]
}
});
}}
className="text-xs text-cyan-glow hover:underline flex items-center gap-1"
>
<Plus className="w-3.5 h-3.5" /> Add Story Slide
</button>
</div>
<div className="space-y-6 divide-y divide-white/5">
{(editingSection.payload?.slides || []).map((slide: any, idx: number) => (
<div key={idx} className="pt-6 first:pt-0 space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-bold font-mono text-cyan-glow">Slide #{idx + 1}</span>
<button
type="button"
onClick={() => {
const currentSlides = editingSection.payload?.slides || [];
const newSlides = currentSlides.filter((_: any, i: number) => i !== idx);
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), slides: newSlides }
});
}}
className="text-xs text-rose-500 hover:underline flex items-center gap-1"
>
<Trash2 className="w-3.5 h-3.5" /> Remove Slide
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Slide Image */}
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Slide Image</label>
<div
className="relative aspect-video rounded-xl overflow-hidden border border-white/10 bg-night-950/30 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all"
onClick={() => {
setUploadTarget({ type: 'slide', index: idx });
setIsMediaModalOpen(true);
}}
>
{slide.imageUrl ? (
<>
<img src={slide.imageUrl} className="w-full h-full object-cover" alt="Slide" />
<div className="absolute inset-0 bg-night-950/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
<UploadCloud className="w-8 h-8 text-white" />
</div>
</>
) : (
<div className="text-center space-y-1">
<ImageIcon className="w-8 h-8 text-slate-700 mx-auto" />
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">Select Image</p>
</div>
)}
</div>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Category Tag</label>
<input
type="text"
required
value={getLocalizedSlideValue(slide, 'category')}
onChange={(e) => updateLocalizedSlideValue(idx, 'category', e.target.value)}
className="admin-input w-full text-xs"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Slide Title</label>
<input
type="text"
required
value={getLocalizedSlideValue(slide, 'title')}
onChange={(e) => updateLocalizedSlideValue(idx, 'title', e.target.value)}
className="admin-input w-full text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Button Text</label>
<input
type="text"
value={getLocalizedSlideValue(slide, 'buttonText')}
onChange={(e) => updateLocalizedSlideValue(idx, 'buttonText', e.target.value)}
className="admin-input w-full text-xs"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Button Link</label>
<input
type="text"
value={getLocalizedSlideValue(slide, 'buttonLink')}
onChange={(e) => updateLocalizedSlideValue(idx, 'buttonLink', e.target.value)}
className="admin-input w-full text-xs"
/>
</div>
</div>
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Slide Description / Content</label>
<textarea
rows={2}
required
value={getLocalizedSlideValue(slide, 'description')}
onChange={(e) => updateLocalizedSlideValue(idx, 'description', e.target.value)}
className="admin-input w-full text-xs resize-none"
/>
</div>
</div>
))}
{(editingSection.payload?.slides || []).length === 0 && (
<p className="text-xs text-slate-500 italic text-center py-4">No slides added yet. Click Add Story Slide above.</p>
)}
</div>
</div>
</div>
);
}
@@ -0,0 +1,139 @@
import React from 'react';
import { UploadCloud, Image as ImageIcon } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
interface EditorialTileFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void;
setIsMediaModalOpen: (open: boolean) => void;
}
export default function EditorialTileForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
setUploadTarget,
setIsMediaModalOpen,
}: EditorialTileFormProps) {
return (
<div className="space-y-6">
{/* Image uploading */}
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Editorial Image</label>
<div
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-dashed border-white/10 bg-night-950/50 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all"
onClick={() => {
setUploadTarget({ type: 'main' });
setIsMediaModalOpen(true);
}}
>
{editingSection.payload?.imageUrl ? (
<>
<img src={editingSection.payload.imageUrl} className="w-full h-full object-cover" alt="Editorial" />
<div className="absolute inset-0 bg-night-950/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
<UploadCloud className="w-10 h-10 text-white" />
</div>
</>
) : (
<div className="text-center space-y-2">
<ImageIcon className="w-10 h-10 text-slate-700 mx-auto" />
<p className="text-xs text-slate-500 font-bold uppercase tracking-widest">Select Image</p>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
<p className="text-[9px] text-slate-500 font-medium">Use [brackets] for golden accent serif text.</p>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Image Position</label>
<select
value={editingSection.payload?.imagePosition || 'left'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), imagePosition: e.target.value }
})
}
className="admin-input w-full bg-slate-900 border-white/10"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
<input
type="text"
value={editingSection.payload?.backgroundColor || '#FDFDF9'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
})
}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
<input
type="text"
value={editingSection.payload?.textColor || '#1F3D2D'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
})
}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Button Text</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'buttonText')}
onChange={(e) => updateLocalizedValue('buttonText', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Button Link</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'buttonLink')}
onChange={(e) => updateLocalizedValue('buttonLink', e.target.value)}
className="admin-input w-full"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Description / Story text</label>
<textarea
rows={4}
required
value={getLocalizedValue(editingSection.payload, 'description')}
onChange={(e) => updateLocalizedValue('description', e.target.value)}
className="admin-input w-full resize-none"
/>
</div>
</div>
);
}
@@ -0,0 +1,105 @@
import { UploadCloud, Image as ImageIcon } from 'lucide-react';
import { type PageSection } from '@homaroma/types';
interface HeroFormProps {
editingSection: Partial<PageSection>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void;
setIsMediaModalOpen: (open: boolean) => void;
}
export default function HeroForm({
editingSection,
getLocalizedValue,
updateLocalizedValue,
setUploadTarget,
setIsMediaModalOpen,
}: HeroFormProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Hero image uploading */}
<div className="md:col-span-2 space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Image</label>
<div
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-dashed border-white/10 bg-night-950/50 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all"
onClick={() => {
setUploadTarget({ type: 'main' });
setIsMediaModalOpen(true);
}}
>
{editingSection.payload?.imageUrl ? (
<>
<img src={editingSection.payload.imageUrl} className="w-full h-full object-cover" alt="Hero Background" />
<div className="absolute inset-0 bg-night-950/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
<UploadCloud className="w-10 h-10 text-white" />
</div>
</>
) : (
<div className="text-center space-y-2">
<ImageIcon className="w-10 h-10 text-slate-700 mx-auto" />
<p className="text-xs text-slate-500 font-bold uppercase tracking-widest">Select Image</p>
</div>
)}
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Tagline</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'tagline')}
onChange={(e) => updateLocalizedValue('tagline', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
<p className="text-[9px] text-slate-500 font-medium">Use [brackets] for golden accent serif text (e.g. Inhale [Serenity]).</p>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Description</label>
<textarea
rows={3}
required
value={getLocalizedValue(editingSection.payload, 'description')}
onChange={(e) => updateLocalizedValue('description', e.target.value)}
className="admin-input w-full resize-none"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Button Text</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'buttonText')}
onChange={(e) => updateLocalizedValue('buttonText', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Button Link</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'buttonLink')}
onChange={(e) => updateLocalizedValue('buttonLink', e.target.value)}
className="admin-input w-full"
/>
</div>
</div>
);
}
@@ -0,0 +1,92 @@
import React from 'react';
import { type PageSection } from '@homaroma/types';
interface NewsletterFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
}
export default function NewsletterForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
}: NewsletterFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Button Text</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'buttonText')}
onChange={(e) => updateLocalizedValue('buttonText', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Placeholder Text</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'placeholderText')}
onChange={(e) => updateLocalizedValue('placeholderText', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2 flex flex-col justify-end">
<label className="flex items-center gap-3 p-3 border border-white/5 bg-white/5 rounded-xl cursor-pointer select-none">
<input
type="checkbox"
checked={editingSection.payload?.showPrivacyCheckbox ?? true}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), showPrivacyCheckbox: e.target.checked }
})
}
className="accent-cyan-glow"
/>
<span className="text-xs font-bold text-slate-300">Show Privacy Policy Checkbox</span>
</label>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Description</label>
<textarea
rows={3}
required
value={getLocalizedValue(editingSection.payload, 'description')}
onChange={(e) => updateLocalizedValue('description', e.target.value)}
className="admin-input w-full resize-none"
/>
</div>
{(editingSection.payload?.showPrivacyCheckbox ?? true) && (
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Privacy Checkbox Text</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'privacyText')}
onChange={(e) => updateLocalizedValue('privacyText', e.target.value)}
className="admin-input w-full"
/>
</div>
)}
</div>
);
}
@@ -0,0 +1,171 @@
import React from 'react';
import { type PageSection } from '@homaroma/types';
interface Scent {
id: number;
name: string;
}
interface ProductsFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
scents: Scent[];
}
export default function ProductsForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
scents,
}: ProductsFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
required
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Subtitle</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'subtitle')}
onChange={(e) => updateLocalizedValue('subtitle', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Products Source type</label>
<select
value={editingSection.payload?.type || 'featured'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: {
...(editingSection.payload || {}),
type: e.target.value,
// reset conditional properties
scentId: e.target.value === 'scent' ? scents[0]?.id : undefined,
productIds: e.target.value === 'custom' ? [] : undefined
}
})
}
className="admin-input w-full bg-slate-900 border-white/10"
>
<option value="featured">Featured Categories (ID=1)</option>
<option value="scent">By Scent</option>
<option value="custom">Custom ID List</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Max display limit</label>
<input
type="number"
min={1}
max={10}
required
value={editingSection.payload?.limit || 3}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), limit: parseInt(e.target.value) }
})
}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Layout Style</label>
<select
value={editingSection.payload?.layout || 'grid'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), layout: e.target.value }
})
}
className="admin-input w-full bg-slate-900 border-white/10"
>
<option value="grid">Grid Layout</option>
<option value="carousel">Carousel Slider</option>
</select>
</div>
<div className="space-y-2 flex flex-col justify-end">
<label className="flex items-center gap-3 p-3 border border-white/5 bg-white/5 rounded-xl cursor-pointer select-none">
<input
type="checkbox"
checked={editingSection.payload?.enableQuickShop ?? false}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), enableQuickShop: e.target.checked }
})
}
className="accent-cyan-glow"
/>
<span className="text-xs font-bold text-slate-300">Enable Quick Shop (Size Swatches & Cart Add)</span>
</label>
</div>
</div>
{editingSection.payload?.type === 'scent' && (
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Select Scent</label>
<select
value={editingSection.payload?.scentId || ''}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), scentId: parseInt(e.target.value) }
})
}
className="admin-input w-full bg-slate-900 border-white/10"
>
{scents.map((scent) => (
<option key={scent.id} value={scent.id}>
{scent.name}
</option>
))}
</select>
</div>
)}
{editingSection.payload?.type === 'custom' && (
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Custom Product IDs (comma separated)</label>
<input
type="text"
required
placeholder="e.g. 151, 152, 153"
value={Array.isArray(editingSection.payload?.productIds) ? editingSection.payload.productIds.join(', ') : ''}
onChange={(e) => {
const ids = e.target.value
.split(',')
.map((id) => parseInt(id.trim()))
.filter((id) => !isNaN(id));
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), productIds: ids }
});
}}
className="admin-input w-full"
/>
</div>
)}
</div>
);
}
@@ -0,0 +1,62 @@
import React from 'react';
import { type PageSection } from '@homaroma/types';
interface TextFormProps {
editingSection: Partial<PageSection>;
setEditingSection: React.Dispatch<React.SetStateAction<Partial<PageSection> | null>>;
editingLang: string;
getLocalizedValue: (payload: any, field: string) => string;
updateLocalizedValue: (field: string, value: string) => void;
}
export default function TextForm({
editingSection,
setEditingSection,
getLocalizedValue,
updateLocalizedValue,
}: TextFormProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Title</label>
<input
type="text"
value={getLocalizedValue(editingSection.payload, 'title')}
onChange={(e) => updateLocalizedValue('title', e.target.value)}
className="admin-input w-full"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Align</label>
<select
value={editingSection.payload?.textAlign || 'left'}
onChange={(e) =>
setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), textAlign: e.target.value }
})
}
className="admin-input w-full bg-slate-900 border-white/10"
>
<option value="left">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Content</label>
<textarea
rows={6}
required
value={getLocalizedValue(editingSection.payload, 'content')}
onChange={(e) => updateLocalizedValue('content', e.target.value)}
className="admin-input w-full resize-none"
placeholder="Enter details about this text section..."
/>
</div>
</div>
);
}
File diff suppressed because it is too large. Load diff