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 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="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: [] }];
|
||||
const categories = [...(editingSection.payload?.categories || []), { id: crypto.randomUUID(), icon: '✨', name: { th: 'หมวดหมู่ใหม่' }, questions: [] }];
|
||||
setEditingSection({ ...editingSection, payload: { ...(editingSection.payload || {}), categories } });
|
||||
}}
|
||||
className="text-xs text-cyan-glow hover:underline flex items-center gap-1"
|
||||
@@ -124,7 +196,7 @@ export default function AccordionForm({
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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 } });
|
||||
}}
|
||||
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="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 || '#0F172A'}
|
||||
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 || '#0F172A'}
|
||||
onChange={(e) => setEditingSection({
|
||||
...editingSection,
|
||||
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
||||
})}
|
||||
className="admin-input flex-1 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={editingSection.payload?.backgroundColor || 'bg'}
|
||||
onChange={(e) => setEditingSection({
|
||||
...editingSection,
|
||||
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
|
||||
})}
|
||||
className="admin-input w-full"
|
||||
>
|
||||
<option value="bg">Page Background (bg)</option>
|
||||
<option value="black">Main Text (black)</option>
|
||||
<option value="pink">Primary Brand (pink)</option>
|
||||
<option value="yellow">Accent Brand (yellow)</option>
|
||||
<option value="orange">Warm Brand (orange)</option>
|
||||
<option value="red">Hover Red (red)</option>
|
||||
<option value="cyan">Cool Brand (cyan)</option>
|
||||
<option value="green">Success Brand (green)</option>
|
||||
<option value="purple">Soft Brand (purple)</option>
|
||||
</select>
|
||||
</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 || '#FFFFFF'}
|
||||
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 || '#FFFFFF'}
|
||||
onChange={(e) => setEditingSection({
|
||||
...editingSection,
|
||||
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
||||
})}
|
||||
className="admin-input flex-1 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={editingSection.payload?.textColor || 'black'}
|
||||
onChange={(e) => setEditingSection({
|
||||
...editingSection,
|
||||
payload: { ...(editingSection.payload || {}), textColor: e.target.value }
|
||||
})}
|
||||
className="admin-input w-full"
|
||||
>
|
||||
<option value="black">Main Text (black)</option>
|
||||
<option value="bg">Page Background (bg)</option>
|
||||
<option value="pink">Primary Brand (pink)</option>
|
||||
<option value="yellow">Accent Brand (yellow)</option>
|
||||
<option value="orange">Warm Brand (orange)</option>
|
||||
<option value="red">Hover Red (red)</option>
|
||||
<option value="cyan">Cool Brand (cyan)</option>
|
||||
<option value="green">Success Brand (green)</option>
|
||||
<option value="purple">Soft Brand (purple)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import crypto from "crypto";
|
||||
import type { AxiosInstance } from "axios";
|
||||
import { PAGE_SECTION_TYPES } from "@homaroma/types";
|
||||
|
||||
@@ -116,9 +117,12 @@ const SECTION_TOOL_CONFIGS = [
|
||||
properties: {
|
||||
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||
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: {
|
||||
type: "array",
|
||||
description: "Accordion categories",
|
||||
description: "Accordion categories. IMPORTANT: Providing this will completely overwrite the existing categories array. Ensure you provide all categories.",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -141,7 +145,7 @@ const SECTION_TOOL_CONFIGS = [
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["sectionId", "categories"]
|
||||
required: ["sectionId"]
|
||||
},
|
||||
{
|
||||
name: "update_contact_info",
|
||||
@@ -298,6 +302,74 @@ export const SECTION_TOOL_DEFINITIONS = [
|
||||
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",
|
||||
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}`);
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { EditorialCarouselSectionPayload } from '@homaroma/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,6 +8,7 @@ import { getLocalizedField } from '../utils/localization';
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import 'swiper/css/navigation';
|
||||
|
||||
interface DynamicEditorialCarouselProps {
|
||||
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 (
|
||||
<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="space-y-3 text-center">
|
||||
<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">
|
||||
<Swiper
|
||||
modules={[Autoplay, Pagination]}
|
||||
modules={[Autoplay, Pagination, Navigation]}
|
||||
spaceBetween={30}
|
||||
slidesPerView={1}
|
||||
breakpoints={{
|
||||
@@ -65,6 +93,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
||||
1024: { slidesPerView: 3 }
|
||||
}}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
|
||||
navigation={true}
|
||||
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
||||
className="w-full"
|
||||
>
|
||||
|
||||
@@ -19,17 +19,22 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
};
|
||||
|
||||
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">
|
||||
{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}
|
||||
</h2>
|
||||
)}
|
||||
{payload.categories.map((category, catIndex) => (
|
||||
<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">
|
||||
{category.icon && <span className="text-pop-pink">{category.icon}</span>}
|
||||
<h3 className="mb-6 flex items-center gap-3 font-serif text-2xl">
|
||||
{category.icon && (
|
||||
<span style={{ color: payload.iconColor }}>{category.icon}</span>
|
||||
)}
|
||||
{getLocalizedField(category.name, lang)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -43,15 +48,15 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
>
|
||||
<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 ${
|
||||
isOpen ? 'bg-pop-black/5' : ''
|
||||
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-black/5' : ''
|
||||
}`}
|
||||
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)}
|
||||
</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} />}
|
||||
</span>
|
||||
</button>
|
||||
@@ -60,7 +65,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
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)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -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" />
|
||||
<div className="ui-banner-overlay" />
|
||||
<div className="ui-banner-content">
|
||||
{title && <h2 className="text-3xl font-semibold md:text-5xl">{title}</h2>}
|
||||
{description && <p className="text-surface-card">{description}</p>}
|
||||
{buttonText && <span className="inline-flex rounded-full bg-surface-card px-6 py-3 font-semibold text-text-main">{buttonText}</span>}
|
||||
{title && <h2 className="text-3xl font-semibold text-brand-primary drop-shadow-md md:text-5xl">{title}</h2>}
|
||||
{description && <p className="text-surface-page/90 drop-shadow-sm">{description}</p>}
|
||||
{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>
|
||||
)
|
||||
@@ -399,56 +399,82 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : []
|
||||
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : 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
|
||||
|
||||
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">
|
||||
{title && (
|
||||
<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>
|
||||
)}
|
||||
<div 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 : ''
|
||||
<div className="relative">
|
||||
<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 -left-5"
|
||||
aria-label="Scroll left"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6 text-brand-primary" />
|
||||
</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 (
|
||||
<article key={index} className="ui-section-dark-card group w-[85vw] md:w-[350px] flex-none snap-start">
|
||||
{imageUrl ? (
|
||||
<div className="relative aspect-[4/3] overflow-hidden">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={slideTitle || 'Editorial slide'}
|
||||
fill
|
||||
unoptimized
|
||||
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>
|
||||
return (
|
||||
<article key={index} className="ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start">
|
||||
{imageUrl ? (
|
||||
<div className="relative aspect-[4/3] overflow-hidden">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={slideTitle || 'Editorial slide'}
|
||||
fill
|
||||
unoptimized
|
||||
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
) : null}
|
||||
<div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
|
||||
<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>
|
||||
</section>
|
||||
@@ -560,9 +586,12 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
|
||||
const categories = Array.isArray(payload.categories) ? payload.categories.map(toRecord) : []
|
||||
if (!categories.length) return null
|
||||
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 (
|
||||
<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">
|
||||
{sectionTitle && <h2 className="ui-section-title">{sectionTitle}</h2>}
|
||||
{categories.map((category, categoryIndex) => {
|
||||
@@ -573,8 +602,8 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
|
||||
return (
|
||||
<div key={categoryIndex} className="space-y-5">
|
||||
{name && (
|
||||
<h2 className="flex items-center gap-3 text-2xl font-semibold text-text-main">
|
||||
{icon && <span className="text-brand-primary">{icon}</span>}
|
||||
<h2 className="flex items-center gap-3 text-2xl font-semibold">
|
||||
{icon && <span style={{ color: iconColor }}>{icon}</span>}
|
||||
{name}
|
||||
</h2>
|
||||
)}
|
||||
@@ -586,7 +615,7 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
|
||||
|
||||
return (
|
||||
<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}
|
||||
</summary>
|
||||
<p className="mt-4 whitespace-pre-wrap leading-relaxed text-muted-text">{answer}</p>
|
||||
|
||||
Reference in new issue
Block a user