x
This commit is contained in:
1 parent
8c0b5a2a34
commit
2c66025842
13 files changed
+1409
-1031
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
Reference in new issue
Block a user