x
This commit is contained in:
1 parent
1a13b3624a
commit
4454b860c1
9 files changed
+381
-105
No files matched your search
@@ -34,13 +34,85 @@ export default function AccordionForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={editingSection.payload?.backgroundColor || '#FFFFFF'}
|
||||||
|
onChange={(e) => setEditingSection({
|
||||||
|
...editingSection,
|
||||||
|
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
||||||
|
})}
|
||||||
|
className="w-10 h-10 rounded cursor-pointer p-1 bg-night-900 border border-white/10"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editingSection.payload?.backgroundColor || '#FFFFFF'}
|
||||||
|
onChange={(e) => setEditingSection({
|
||||||
|
...editingSection,
|
||||||
|
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
||||||
|
})}
|
||||||
|
className="admin-input flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={editingSection.payload?.textColor || '#000000'}
|
||||||
|
onChange={(e) => setEditingSection({
|
||||||
|
...editingSection,
|
||||||
|
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
||||||
|
})}
|
||||||
|
className="w-10 h-10 rounded cursor-pointer p-1 bg-night-900 border border-white/10"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editingSection.payload?.textColor || '#000000'}
|
||||||
|
onChange={(e) => setEditingSection({
|
||||||
|
...editingSection,
|
||||||
|
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
||||||
|
})}
|
||||||
|
className="admin-input flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Icon Color</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={editingSection.payload?.iconColor || '#000000'}
|
||||||
|
onChange={(e) => setEditingSection({
|
||||||
|
...editingSection,
|
||||||
|
payload: { ...(editingSection.payload || {}), iconColor: e.target.value }
|
||||||
|
})}
|
||||||
|
className="w-10 h-10 rounded cursor-pointer p-1 bg-night-900 border border-white/10"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editingSection.payload?.iconColor || '#000000'}
|
||||||
|
onChange={(e) => setEditingSection({
|
||||||
|
...editingSection,
|
||||||
|
payload: { ...(editingSection.payload || {}), iconColor: e.target.value }
|
||||||
|
})}
|
||||||
|
className="admin-input flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Categories & Questions</label>
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Categories & Questions</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const categories = [...(editingSection.payload?.categories || []), { icon: '✨', name: { th: 'หมวดหมู่ใหม่' }, questions: [] }];
|
const categories = [...(editingSection.payload?.categories || []), { id: crypto.randomUUID(), icon: '✨', name: { th: 'หมวดหมู่ใหม่' }, questions: [] }];
|
||||||
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
|
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
|
||||||
}}
|
}}
|
||||||
className="text-xs text-cyan-glow hover:underline flex items-center gap-1"
|
className="text-xs text-cyan-glow hover:underline flex items-center gap-1"
|
||||||
@@ -124,7 +196,7 @@ export default function AccordionForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const categories = [...editingSection.payload.categories];
|
const categories = [...editingSection.payload.categories];
|
||||||
categories[cIdx].questions = [...(categories[cIdx].questions || []), { q: { th: 'คำถามใหม่' }, a: { th: 'คำตอบใหม่' } }];
|
categories[cIdx].questions = [...(categories[cIdx].questions || []), { id: crypto.randomUUID(), q: { th: 'คำถามใหม่' }, a: { th: 'คำตอบใหม่' } }];
|
||||||
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
|
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"
|
className="text-[10px] text-cyan-glow/80 hover:text-cyan-glow uppercase tracking-widest font-bold flex items-center gap-1"
|
||||||
|
|||||||
@@ -43,49 +43,45 @@ export default function EditorialCarouselForm({
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
|
||||||
<div className="flex gap-3">
|
<select
|
||||||
<input
|
value={editingSection.payload?.backgroundColor || 'bg'}
|
||||||
type="color"
|
onChange={(e) => setEditingSection({
|
||||||
value={editingSection.payload?.backgroundColor || '#0F172A'}
|
...editingSection,
|
||||||
onChange={(e) => setEditingSection({
|
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
||||||
...editingSection,
|
})}
|
||||||
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
className="admin-input w-full"
|
||||||
})}
|
>
|
||||||
className="w-10 h-10 rounded cursor-pointer p-1 bg-night-900 border border-white/10"
|
<option value="bg">Page Background (bg)</option>
|
||||||
/>
|
<option value="black">Main Text (black)</option>
|
||||||
<input
|
<option value="pink">Primary Brand (pink)</option>
|
||||||
type="text"
|
<option value="yellow">Accent Brand (yellow)</option>
|
||||||
value={editingSection.payload?.backgroundColor || '#0F172A'}
|
<option value="orange">Warm Brand (orange)</option>
|
||||||
onChange={(e) => setEditingSection({
|
<option value="red">Hover Red (red)</option>
|
||||||
...editingSection,
|
<option value="cyan">Cool Brand (cyan)</option>
|
||||||
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
<option value="green">Success Brand (green)</option>
|
||||||
})}
|
<option value="purple">Soft Brand (purple)</option>
|
||||||
className="admin-input flex-1 font-mono text-xs"
|
</select>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
|
||||||
<div className="flex gap-3">
|
<select
|
||||||
<input
|
value={editingSection.payload?.textColor || 'black'}
|
||||||
type="color"
|
onChange={(e) => setEditingSection({
|
||||||
value={editingSection.payload?.textColor || '#FFFFFF'}
|
...editingSection,
|
||||||
onChange={(e) => setEditingSection({
|
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
||||||
...editingSection,
|
})}
|
||||||
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
className="admin-input w-full"
|
||||||
})}
|
>
|
||||||
className="w-10 h-10 rounded cursor-pointer p-1 bg-night-900 border border-white/10"
|
<option value="black">Main Text (black)</option>
|
||||||
/>
|
<option value="bg">Page Background (bg)</option>
|
||||||
<input
|
<option value="pink">Primary Brand (pink)</option>
|
||||||
type="text"
|
<option value="yellow">Accent Brand (yellow)</option>
|
||||||
value={editingSection.payload?.textColor || '#FFFFFF'}
|
<option value="orange">Warm Brand (orange)</option>
|
||||||
onChange={(e) => setEditingSection({
|
<option value="red">Hover Red (red)</option>
|
||||||
...editingSection,
|
<option value="cyan">Cool Brand (cyan)</option>
|
||||||
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
<option value="green">Success Brand (green)</option>
|
||||||
})}
|
<option value="purple">Soft Brand (purple)</option>
|
||||||
className="admin-input flex-1 font-mono text-xs"
|
</select>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
import type { AxiosInstance } from "axios";
|
import type { AxiosInstance } from "axios";
|
||||||
import { PAGE_SECTION_TYPES } from "@homaroma/types";
|
import { PAGE_SECTION_TYPES } from "@homaroma/types";
|
||||||
|
|
||||||
@@ -116,9 +117,12 @@ const SECTION_TOOL_CONFIGS = [
|
|||||||
properties: {
|
properties: {
|
||||||
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||||
title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` },
|
title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` },
|
||||||
|
backgroundColor: { type: "string", description: "Plain string representing a valid CSS color for the background" },
|
||||||
|
textColor: { type: "string", description: "Plain string representing a valid CSS color for text" },
|
||||||
|
iconColor: { type: "string", description: "Plain string representing a valid CSS color for icons" },
|
||||||
categories: {
|
categories: {
|
||||||
type: "array",
|
type: "array",
|
||||||
description: "Accordion categories",
|
description: "Accordion categories. IMPORTANT: Providing this will completely overwrite the existing categories array. Ensure you provide all categories.",
|
||||||
items: {
|
items: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -141,7 +145,7 @@ const SECTION_TOOL_CONFIGS = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["sectionId", "categories"]
|
required: ["sectionId"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "update_contact_info",
|
name: "update_contact_info",
|
||||||
@@ -298,6 +302,74 @@ export const SECTION_TOOL_DEFINITIONS = [
|
|||||||
required: config.required
|
required: config.required
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
{
|
||||||
|
name: "add_accordion_category",
|
||||||
|
description: "Add a category to an ACCORDION section.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||||
|
name: { ...localizedStringSchema, description: "Localized category name" },
|
||||||
|
icon: { type: "string", description: "Optional plain string for icon/emoji" },
|
||||||
|
questions: {
|
||||||
|
type: "array",
|
||||||
|
description: "Optional array of questions to add",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
q: { ...localizedStringSchema, description: "Localized question text" },
|
||||||
|
a: { ...localizedStringSchema, description: "Localized answer text" }
|
||||||
|
},
|
||||||
|
required: ["q", "a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["sectionId", "name"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_accordion_category",
|
||||||
|
description: "Update a category in an ACCORDION section.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||||
|
categoryId: { type: "string", description: "ID of the category to update" },
|
||||||
|
name: { ...localizedStringSchema, description: "Optional localized category name" },
|
||||||
|
icon: { type: "string", description: "Optional plain string for icon/emoji" }
|
||||||
|
},
|
||||||
|
required: ["sectionId", "categoryId"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add_accordion_question",
|
||||||
|
description: "Add a question to a category in an ACCORDION section.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||||
|
categoryId: { type: "string", description: "ID of the category" },
|
||||||
|
question: { ...localizedStringSchema, description: "Localized question text" },
|
||||||
|
answer: { ...localizedStringSchema, description: "Localized answer text" }
|
||||||
|
},
|
||||||
|
required: ["sectionId", "categoryId", "question", "answer"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_accordion_question",
|
||||||
|
description: "Update a question in an ACCORDION section.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||||
|
categoryId: { type: "string", description: "ID of the category" },
|
||||||
|
questionId: { type: "string", description: "ID of the question item to update" },
|
||||||
|
question: { ...localizedStringSchema, description: "Optional localized question text" },
|
||||||
|
answer: { ...localizedStringSchema, description: "Optional localized answer text" }
|
||||||
|
},
|
||||||
|
required: ["sectionId", "categoryIndex", "itemIndex"]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "delete_page_section",
|
name: "delete_page_section",
|
||||||
description: "Delete a block section from a dynamic page layout",
|
description: "Delete a block section from a dynamic page layout",
|
||||||
@@ -395,6 +467,65 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
|
|||||||
const res = await api.delete(`/pages/sections/${params.sectionId}`);
|
const res = await api.delete(`/pages/sections/${params.sectionId}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||||
}
|
}
|
||||||
|
case "add_accordion_category":
|
||||||
|
case "update_accordion_category":
|
||||||
|
case "add_accordion_question":
|
||||||
|
case "update_accordion_question": {
|
||||||
|
const existingRes = await api.get(`/pages/sections/${params.sectionId}?lang=all`);
|
||||||
|
const payload = existingRes.data?.payload || {};
|
||||||
|
payload.categories = payload.categories || [];
|
||||||
|
|
||||||
|
if (name === "add_accordion_category") {
|
||||||
|
const questions = params.questions?.map((q: any) => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
q: q.q,
|
||||||
|
a: q.a
|
||||||
|
})) || [];
|
||||||
|
payload.categories.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: params.name,
|
||||||
|
icon: params.icon,
|
||||||
|
questions
|
||||||
|
});
|
||||||
|
} else if (name === "update_accordion_category") {
|
||||||
|
const cat = payload.categories.find((c: any) => c.id === params.categoryId);
|
||||||
|
if (!cat) throw new Error(`Category with id ${params.categoryId} not found`);
|
||||||
|
if (params.name) {
|
||||||
|
cat.name = typeof params.name === "object" && typeof cat.name === "object"
|
||||||
|
? { ...cat.name, ...params.name }
|
||||||
|
: params.name;
|
||||||
|
}
|
||||||
|
if (params.icon !== undefined) cat.icon = params.icon;
|
||||||
|
} else if (name === "add_accordion_question") {
|
||||||
|
const cat = payload.categories.find((c: any) => c.id === params.categoryId);
|
||||||
|
if (!cat) throw new Error(`Category with id ${params.categoryId} not found`);
|
||||||
|
cat.questions = cat.questions || [];
|
||||||
|
cat.questions.push({ id: crypto.randomUUID(), q: params.question, a: params.answer });
|
||||||
|
} else if (name === "update_accordion_question") {
|
||||||
|
const cat = payload.categories.find((c: any) => c.id === params.categoryId);
|
||||||
|
if (!cat) throw new Error(`Category with id ${params.categoryId} not found`);
|
||||||
|
cat.questions = cat.questions || [];
|
||||||
|
const item = cat.questions.find((q: any) => q.id === params.questionId);
|
||||||
|
if (!item) throw new Error(`Question with id ${params.questionId} not found`);
|
||||||
|
if (params.question) {
|
||||||
|
item.q = typeof params.question === "object" && typeof item.q === "object"
|
||||||
|
? { ...item.q, ...params.question }
|
||||||
|
: params.question;
|
||||||
|
}
|
||||||
|
if (params.answer) {
|
||||||
|
item.a = typeof params.answer === "object" && typeof item.a === "object"
|
||||||
|
? { ...item.a, ...params.answer }
|
||||||
|
: params.answer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.put(`/pages/sections/${params.sectionId}`, {
|
||||||
|
type: "ACCORDION",
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
|
||||||
|
return { content: [{ type: "text", text: "Success message" }] };
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { Autoplay, Pagination } from 'swiper/modules';
|
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { EditorialCarouselSectionPayload } from '@homaroma/types';
|
import type { EditorialCarouselSectionPayload } from '@homaroma/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -8,6 +8,7 @@ import { getLocalizedField } from '../utils/localization';
|
|||||||
// Import Swiper styles
|
// Import Swiper styles
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/pagination';
|
import 'swiper/css/pagination';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
interface DynamicEditorialCarouselProps {
|
interface DynamicEditorialCarouselProps {
|
||||||
payload: EditorialCarouselSectionPayload;
|
payload: EditorialCarouselSectionPayload;
|
||||||
@@ -45,8 +46,35 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bgMap: Record<string, string> = {
|
||||||
|
bg: 'bg-pop-bg',
|
||||||
|
black: 'bg-pop-black',
|
||||||
|
pink: 'bg-pop-pink',
|
||||||
|
yellow: 'bg-pop-yellow',
|
||||||
|
orange: 'bg-pop-orange',
|
||||||
|
red: 'bg-pop-red',
|
||||||
|
cyan: 'bg-pop-cyan',
|
||||||
|
green: 'bg-pop-green',
|
||||||
|
purple: 'bg-pop-purple',
|
||||||
|
};
|
||||||
|
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
bg: 'text-pop-bg',
|
||||||
|
black: 'text-pop-black',
|
||||||
|
pink: 'text-pop-pink',
|
||||||
|
yellow: 'text-pop-yellow',
|
||||||
|
orange: 'text-pop-orange',
|
||||||
|
red: 'text-pop-red',
|
||||||
|
cyan: 'text-pop-cyan',
|
||||||
|
green: 'text-pop-green',
|
||||||
|
purple: 'text-pop-purple',
|
||||||
|
};
|
||||||
|
|
||||||
|
const bgClass = payload.backgroundColor && bgMap[payload.backgroundColor] ? bgMap[payload.backgroundColor] : 'bg-pop-bg';
|
||||||
|
const textClass = payload.textColor && textMap[payload.textColor] ? textMap[payload.textColor] : 'text-pop-black';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="overflow-hidden py-16 md:py-24" style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}>
|
<section className={`overflow-hidden py-16 md:py-24 ${bgClass} ${textClass}`}>
|
||||||
<div className="ui-section-inner space-y-12">
|
<div className="ui-section-inner space-y-12">
|
||||||
<div className="space-y-3 text-center">
|
<div className="space-y-3 text-center">
|
||||||
<h2 className="font-serif text-3xl font-light tracking-wide text-pop-pink md:text-5xl">
|
<h2 className="font-serif text-3xl font-light tracking-wide text-pop-pink md:text-5xl">
|
||||||
@@ -57,7 +85,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
|||||||
|
|
||||||
<div className="relative select-none pb-12">
|
<div className="relative select-none pb-12">
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Autoplay, Pagination]}
|
modules={[Autoplay, Pagination, Navigation]}
|
||||||
spaceBetween={30}
|
spaceBetween={30}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
breakpoints={{
|
breakpoints={{
|
||||||
@@ -65,6 +93,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
|||||||
1024: { slidesPerView: 3 }
|
1024: { slidesPerView: 3 }
|
||||||
}}
|
}}
|
||||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
|
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
|
||||||
|
navigation={true}
|
||||||
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,17 +19,22 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="ui-section-band w-full bg-pop-bg px-4 md:px-8">
|
<section
|
||||||
|
className="ui-section-band w-full px-4 md:px-8"
|
||||||
|
style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}
|
||||||
|
>
|
||||||
<div className="ui-section-inner">
|
<div className="ui-section-inner">
|
||||||
{title && (
|
{title && (
|
||||||
<h2 className="mb-10 text-center font-serif text-3xl text-pop-black md:text-4xl">
|
<h2 className="mb-10 text-center font-serif text-3xl md:text-4xl">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
{payload.categories.map((category, catIndex) => (
|
{payload.categories.map((category, catIndex) => (
|
||||||
<div key={catIndex} className="mb-12 last:mb-0">
|
<div key={catIndex} className="mb-12 last:mb-0">
|
||||||
<h3 className="mb-6 flex items-center gap-3 font-serif text-2xl text-pop-black">
|
<h3 className="mb-6 flex items-center gap-3 font-serif text-2xl">
|
||||||
{category.icon && <span className="text-pop-pink">{category.icon}</span>}
|
{category.icon && (
|
||||||
|
<span style={{ color: payload.iconColor }}>{category.icon}</span>
|
||||||
|
)}
|
||||||
{getLocalizedField(category.name, lang)}
|
{getLocalizedField(category.name, lang)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -43,15 +48,15 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex w-full items-center justify-between p-5 text-left focus:outline-none focus:ring-2 focus:ring-pop-pink/50 hover:bg-pop-black/5 transition-colors ${
|
className={`flex w-full items-center justify-between p-5 text-left focus:outline-none focus:ring-2 focus:ring-pop-pink/50 hover:bg-black/5 transition-colors ${
|
||||||
isOpen ? 'bg-pop-black/5' : ''
|
isOpen ? 'bg-black/5' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleAccordion(indexKey)}
|
onClick={() => toggleAccordion(indexKey)}
|
||||||
>
|
>
|
||||||
<span className="pr-4 text-lg font-medium text-pop-black">
|
<span className="pr-4 text-lg font-medium">
|
||||||
{getLocalizedField(item.q, lang)}
|
{getLocalizedField(item.q, lang)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-shrink-0 text-pop-pink">
|
<span className="flex-shrink-0" style={{ color: payload.iconColor }}>
|
||||||
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -60,7 +65,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
|||||||
isOpen ? 'max-h-96 pb-5 opacity-100' : 'max-h-0 opacity-0'
|
isOpen ? 'max-h-96 pb-5 opacity-100' : 'max-h-0 opacity-0'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-pop-black/80 leading-relaxed whitespace-pre-wrap">
|
<p className="opacity-80 leading-relaxed whitespace-pre-wrap">
|
||||||
{getLocalizedField(item.a, lang)}
|
{getLocalizedField(item.a, lang)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ui-button-secondary {
|
.ui-button-secondary {
|
||||||
@apply inline-flex items-center justify-center rounded-full bg-pop-bg px-6 py-3 text-xs font-bold uppercase tracking-widest text-pop-black transition-colors hover:bg-pop-black/5;
|
@apply inline-flex items-center justify-center rounded-full bg-pop-bg px-6 py-3 text-xs font-bold uppercase tracking-widest text-pop-black transition-colors hover:bg-pop-pink hover:text-pop-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-input-shell {
|
.ui-input-shell {
|
||||||
|
|||||||
@@ -290,9 +290,9 @@ function BannerSection({ payload, lang }: { payload: Record<string, unknown>, la
|
|||||||
<Image src={imageUrl} alt={title || 'Banner'} fill unoptimized className="absolute inset-0 h-full w-full object-cover" />
|
<Image src={imageUrl} alt={title || 'Banner'} fill unoptimized className="absolute inset-0 h-full w-full object-cover" />
|
||||||
<div className="ui-banner-overlay" />
|
<div className="ui-banner-overlay" />
|
||||||
<div className="ui-banner-content">
|
<div className="ui-banner-content">
|
||||||
{title && <h2 className="text-3xl font-semibold md:text-5xl">{title}</h2>}
|
{title && <h2 className="text-3xl font-semibold text-brand-primary drop-shadow-md md:text-5xl">{title}</h2>}
|
||||||
{description && <p className="text-surface-card">{description}</p>}
|
{description && <p className="text-surface-page/90 drop-shadow-sm">{description}</p>}
|
||||||
{buttonText && <span className="inline-flex rounded-full bg-surface-card px-6 py-3 font-semibold text-text-main">{buttonText}</span>}
|
{buttonText && <span className="inline-flex cursor-pointer rounded-full bg-surface-card px-6 py-3 font-semibold text-text-main transition-colors hover:bg-brand-primary hover:text-surface-card">{buttonText}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -399,56 +399,82 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
|||||||
const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : []
|
const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : []
|
||||||
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
|
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
|
||||||
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
|
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
|
||||||
|
const bgClass = backgroundColor ? `bg-pop-${backgroundColor}` : 'bg-surface-page'
|
||||||
|
const textClass = textColor ? `text-pop-${textColor}` : 'text-text-main'
|
||||||
|
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const scrollBy = (offset: number) => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollBy({ left: offset, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!slides.length) return null
|
if (!slides.length) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="ui-section-dark py-16 md:py-24" style={{ backgroundColor, color: textColor }}>
|
<section className={`px-6 py-16 md:py-24 ${bgClass} ${textClass}`}>
|
||||||
<div className="mx-auto max-w-7xl space-y-12">
|
<div className="mx-auto max-w-7xl space-y-12">
|
||||||
{title && (
|
{title && (
|
||||||
<div className="mx-auto max-w-3xl space-y-4 text-center">
|
<div className="mx-auto max-w-3xl space-y-4 text-center">
|
||||||
<h2 className="text-4xl font-semibold text-surface-card md:text-5xl" style={{ color: textColor }}>{renderAccentTitle(title)}</h2>
|
<h2 className={`text-4xl font-semibold md:text-5xl ${textClass}`}>{renderAccentTitle(title)}</h2>
|
||||||
<div className="mx-auto h-px w-16 bg-brand-accent" />
|
<div className="mx-auto h-px w-16 bg-brand-accent" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar gap-6 pb-8">
|
<div className="relative">
|
||||||
{slides.map((slide, index) => {
|
<button
|
||||||
const slideTitle = localizedFromRecord(slide, 'title', resolvedLang)
|
onClick={() => scrollBy(-400)}
|
||||||
const description = localizedFromRecord(slide, 'description', resolvedLang)
|
className="hidden md:flex bg-surface-card rounded-full shadow-card hover:text-brand-primary h-10 w-10 items-center justify-center absolute z-10 top-1/2 -translate-y-1/2 -left-5"
|
||||||
const category = localizedFromRecord(slide, 'category', resolvedLang)
|
aria-label="Scroll left"
|
||||||
const buttonText = localizedFromRecord(slide, 'buttonText', resolvedLang)
|
>
|
||||||
const buttonLink = typeof slide.buttonLink === 'string' ? slide.buttonLink : ''
|
<ChevronLeft className="h-6 w-6 text-brand-primary" />
|
||||||
const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : ''
|
</button>
|
||||||
|
<div ref={scrollContainerRef} className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar gap-6 pb-8">
|
||||||
|
{slides.map((slide, index) => {
|
||||||
|
const slideTitle = localizedFromRecord(slide, 'title', resolvedLang)
|
||||||
|
const description = localizedFromRecord(slide, 'description', resolvedLang)
|
||||||
|
const category = localizedFromRecord(slide, 'category', resolvedLang)
|
||||||
|
const buttonText = localizedFromRecord(slide, 'buttonText', resolvedLang)
|
||||||
|
const buttonLink = typeof slide.buttonLink === 'string' ? slide.buttonLink : ''
|
||||||
|
const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={index} className="ui-section-dark-card group w-[85vw] md:w-[350px] flex-none snap-start">
|
<article key={index} className="ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start">
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
<div className="relative aspect-[4/3] overflow-hidden">
|
<div className="relative aspect-[4/3] overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={slideTitle || 'Editorial slide'}
|
alt={slideTitle || 'Editorial slide'}
|
||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
|
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
|
|
||||||
<div>
|
|
||||||
{category && <p className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-accent mb-2">{category}</p>}
|
|
||||||
{slideTitle && <h3 className="ui-section-dark-card-title text-xl mb-3">{renderAccentTitle(slideTitle)}</h3>}
|
|
||||||
{description && <p className="ui-section-dark-card-copy text-sm line-clamp-3">{description}</p>}
|
|
||||||
</div>
|
|
||||||
{buttonText && buttonLink && (
|
|
||||||
<div className="pt-2">
|
|
||||||
<Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
|
|
||||||
{buttonText} <span className="group-hover/link:translate-x-1 transition-transform">→</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
<div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
|
||||||
</article>
|
<div>
|
||||||
)
|
{category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
|
||||||
})}
|
{slideTitle && <h3 className="text-xl font-bold text-text-main mt-6 group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
|
||||||
|
{description && <p className="text-base text-muted-text mt-3 whitespace-pre-wrap">{description}</p>}
|
||||||
|
</div>
|
||||||
|
{buttonText && buttonLink && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
|
||||||
|
{buttonText} <span className="group-hover/link:translate-x-1 transition-transform">→</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => scrollBy(400)}
|
||||||
|
className="hidden md:flex bg-surface-card rounded-full shadow-card hover:text-brand-primary h-10 w-10 items-center justify-center absolute z-10 top-1/2 -translate-y-1/2 -right-5"
|
||||||
|
aria-label="Scroll right"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-6 w-6 text-brand-primary" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -560,9 +586,12 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
|
|||||||
const categories = Array.isArray(payload.categories) ? payload.categories.map(toRecord) : []
|
const categories = Array.isArray(payload.categories) ? payload.categories.map(toRecord) : []
|
||||||
if (!categories.length) return null
|
if (!categories.length) return null
|
||||||
const sectionTitle = localizedFromRecord(payload, 'title', resolvedLang)
|
const sectionTitle = localizedFromRecord(payload, 'title', resolvedLang)
|
||||||
|
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
|
||||||
|
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
|
||||||
|
const iconColor = typeof payload.iconColor === 'string' ? payload.iconColor : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-surface-page ui-section-band">
|
<section className="ui-section-band" style={{ backgroundColor, color: textColor }}>
|
||||||
<div className="mx-auto max-w-4xl space-y-10">
|
<div className="mx-auto max-w-4xl space-y-10">
|
||||||
{sectionTitle && <h2 className="ui-section-title">{sectionTitle}</h2>}
|
{sectionTitle && <h2 className="ui-section-title">{sectionTitle}</h2>}
|
||||||
{categories.map((category, categoryIndex) => {
|
{categories.map((category, categoryIndex) => {
|
||||||
@@ -573,8 +602,8 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
|
|||||||
return (
|
return (
|
||||||
<div key={categoryIndex} className="space-y-5">
|
<div key={categoryIndex} className="space-y-5">
|
||||||
{name && (
|
{name && (
|
||||||
<h2 className="flex items-center gap-3 text-2xl font-semibold text-text-main">
|
<h2 className="flex items-center gap-3 text-2xl font-semibold">
|
||||||
{icon && <span className="text-brand-primary">{icon}</span>}
|
{icon && <span style={{ color: iconColor }}>{icon}</span>}
|
||||||
{name}
|
{name}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
@@ -586,7 +615,7 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<details key={itemIndex} className="group rounded-2xl bg-surface-card p-5 shadow-card">
|
<details key={itemIndex} className="group rounded-2xl bg-surface-card p-5 shadow-card">
|
||||||
<summary className="cursor-pointer list-none text-lg font-semibold text-text-main marker:hidden">
|
<summary className="cursor-pointer list-none text-lg font-semibold marker:hidden">
|
||||||
{question}
|
{question}
|
||||||
</summary>
|
</summary>
|
||||||
<p className="mt-4 whitespace-pre-wrap leading-relaxed text-muted-text">{answer}</p>
|
<p className="mt-4 whitespace-pre-wrap leading-relaxed text-muted-text">{answer}</p>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = 'apps/website/src/components/dynamic/DynamicPageRenderer.tsx';
|
||||||
|
let content = fs.readFileSync(path, 'utf8');
|
||||||
|
|
||||||
|
content = content.replace(/className="ui-section-dark-card group/g, 'className="ui-card group');
|
||||||
|
content = content.replace(/className="ui-section-dark-card-title mt-6 text-xl"/g, 'className="text-xl font-bold text-text-main mt-6"');
|
||||||
|
content = content.replace(/className="ui-section-dark-card-copy mt-3 text-base"/g, 'className="text-base text-muted-text mt-3 whitespace-pre-wrap"');
|
||||||
|
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
@@ -462,18 +462,23 @@ export type ScentGuidePage = z.infer<typeof ScentGuidePageSchema>;
|
|||||||
|
|
||||||
export const AccordionSectionPayloadSchema = z.object({
|
export const AccordionSectionPayloadSchema = z.object({
|
||||||
title: LocalizedStringSchema.optional(),
|
title: LocalizedStringSchema.optional(),
|
||||||
|
backgroundColor: z.string().optional(),
|
||||||
|
textColor: z.string().optional(),
|
||||||
|
iconColor: z.string().optional(),
|
||||||
categories: z.array(
|
categories: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
name: LocalizedStringSchema,
|
name: LocalizedStringSchema,
|
||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
questions: z.array(
|
questions: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
q: LocalizedStringSchema,
|
q: LocalizedStringSchema,
|
||||||
a: LocalizedStringSchema,
|
a: LocalizedStringSchema,
|
||||||
})
|
})
|
||||||
).min(1),
|
),
|
||||||
})
|
})
|
||||||
).min(1),
|
).optional(),
|
||||||
});
|
});
|
||||||
export type AccordionSectionPayload = z.infer<typeof AccordionSectionPayloadSchema>;
|
export type AccordionSectionPayload = z.infer<typeof AccordionSectionPayloadSchema>;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user