x
This commit is contained in:
1 parent
2a9dd8ddec
commit
de09293051
6 files changed
+80
-16
No files matched your search
@@ -76,6 +76,7 @@ Renderer จะอ่าน `componentId` แล้วไปหา component จ
|
|||||||
"ko": "매일 구운 식빵, 사워도우, 부드러운 샌드위치 브레드."
|
"ko": "매일 구운 식빵, 사워도우, 부드러운 샌드위치 브레드."
|
||||||
},
|
},
|
||||||
"href": "/shop",
|
"href": "/shop",
|
||||||
|
"icon": "bakery",
|
||||||
"imageUrl": "https://example.com/bread.jpg"
|
"imageUrl": "https://example.com/bread.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -88,6 +89,7 @@ Renderer จะอ่าน `componentId` แล้วไปหา component จ
|
|||||||
- ใช้ language keys ชุดเดียวกับระบบ: `th`, `en`, `ja`, `zh`, `vi`, `ko`
|
- ใช้ language keys ชุดเดียวกับระบบ: `th`, `en`, `ja`, `zh`, `vi`, `ko`
|
||||||
- field ที่เป็นข้อความควรรองรับ localized object เสมอ
|
- field ที่เป็นข้อความควรรองรับ localized object เสมอ
|
||||||
- field ที่เป็น route หรือ URL เช่น `href`, `imageUrl` เป็น string ปกติ
|
- field ที่เป็น route หรือ URL เช่น `href`, `imageUrl` เป็น string ปกติ
|
||||||
|
- `icon` เป็น optional string สำหรับเลือก line icon ในวงกลม เช่น `dessert`, `bakery`, `cake`, `gift`, `cookie`, `bread`
|
||||||
- `imageUrl` เป็น optional ถ้าไม่ใส่ component ต้องมี fallback ที่หน้าไม่พัง
|
- `imageUrl` เป็น optional ถ้าไม่ใส่ component ต้องมี fallback ที่หน้าไม่พัง
|
||||||
|
|
||||||
## Step 2: เพิ่ม Shared Type
|
## Step 2: เพิ่ม Shared Type
|
||||||
@@ -353,6 +355,7 @@ type BakeryCategory = {
|
|||||||
description?: string | Record<string, string>
|
description?: string | Record<string, string>
|
||||||
href: string
|
href: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
icon?: string
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -381,7 +384,7 @@ apps/mcp-server/section-toolkit.ts
|
|||||||
```ts
|
```ts
|
||||||
CUSTOM: {
|
CUSTOM: {
|
||||||
required: ["componentId"],
|
required: ["componentId"],
|
||||||
example: `{ "componentId": "BakeryCategories", "props": { "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "layout": "horizontal", "items": [{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "href": "/shop" }] } }`
|
example: `{ "componentId": "BakeryCategories", "props": { "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "layout": "horizontal", "items": [{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "href": "/shop", "icon": "bakery" }] } }`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -512,4 +515,3 @@ Admin Page Builder
|
|||||||
-> BakeryCategories resolves language
|
-> BakeryCategories resolves language
|
||||||
-> UI renders cards
|
-> UI renders cards
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export default function CustomBlockForm({
|
|||||||
defaultValue={formatProps(editingSection.payload?.props)}
|
defaultValue={formatProps(editingSection.payload?.props)}
|
||||||
onChange={(e) => updateProps(e.target.value)}
|
onChange={(e) => updateProps(e.target.value)}
|
||||||
className="admin-input w-full resize-y font-mono text-sm"
|
className="admin-input w-full resize-y font-mono text-sm"
|
||||||
placeholder={'{\n "title": { "th": "หมวดหมู่เบเกอรี่", "en": "Bakery Categories" },\n "subtitle": { "th": "เลือกซื้อขนมอบตามหมวดหมู่ที่คุณชอบ", "en": "Explore fresh-baked favorites by collection." },\n "layout": "horizontal",\n "items": []\n}'}
|
placeholder={'{\n "title": { "th": "หมวดหมู่เบเกอรี่", "en": "Bakery Categories" },\n "subtitle": { "th": "เลือกซื้อขนมอบตามหมวดหมู่ที่คุณชอบ", "en": "Explore fresh-baked favorites by collection." },\n "layout": "horizontal",\n "items": [\n { "title": { "th": "ขนมไทย", "en": "Thai Dessert" }, "href": "/shop", "icon": "dessert" }\n ]\n}'}
|
||||||
/>
|
/>
|
||||||
{jsonError && (
|
{jsonError && (
|
||||||
<p className="text-xs font-semibold text-red-300">{jsonError}</p>
|
<p className="text-xs font-semibold text-red-300">{jsonError}</p>
|
||||||
|
|||||||
@@ -699,7 +699,8 @@ export default function PageBuilderPage() {
|
|||||||
th: 'ขนมปังอบสดใหม่ทุกวัน',
|
th: 'ขนมปังอบสดใหม่ทุกวัน',
|
||||||
en: 'Daily loaves, sourdough, and soft sandwich breads.'
|
en: 'Daily loaves, sourdough, and soft sandwich breads.'
|
||||||
},
|
},
|
||||||
href: '/shop'
|
href: '/shop',
|
||||||
|
icon: 'bakery'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: { th: 'พาสทรี', en: 'Pastries' },
|
title: { th: 'พาสทรี', en: 'Pastries' },
|
||||||
@@ -707,7 +708,8 @@ export default function PageBuilderPage() {
|
|||||||
th: 'พาสทรีเนยหอมกรอบสำหรับมื้อเช้าและของฝาก',
|
th: 'พาสทรีเนยหอมกรอบสำหรับมื้อเช้าและของฝาก',
|
||||||
en: 'Layered butter pastries for breakfast and gifting.'
|
en: 'Layered butter pastries for breakfast and gifting.'
|
||||||
},
|
},
|
||||||
href: '/shop'
|
href: '/shop',
|
||||||
|
icon: 'dessert'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: { th: 'เค้ก', en: 'Cakes' },
|
title: { th: 'เค้ก', en: 'Cakes' },
|
||||||
@@ -715,7 +717,8 @@ export default function PageBuilderPage() {
|
|||||||
th: 'เค้กสำหรับวันพิเศษและช่วงเวลาพักผ่อน',
|
th: 'เค้กสำหรับวันพิเศษและช่วงเวลาพักผ่อน',
|
||||||
en: 'Celebration cakes and slices for quiet afternoons.'
|
en: 'Celebration cakes and slices for quiet afternoons.'
|
||||||
},
|
},
|
||||||
href: '/shop'
|
href: '/shop',
|
||||||
|
icon: 'cake'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: { th: 'คุกกี้', en: 'Cookies' },
|
title: { th: 'คุกกี้', en: 'Cookies' },
|
||||||
@@ -723,7 +726,8 @@ export default function PageBuilderPage() {
|
|||||||
th: 'คุกกี้อบใหม่ กรอบนอกนุ่มใน',
|
th: 'คุกกี้อบใหม่ กรอบนอกนุ่มใน',
|
||||||
en: 'Small-batch cookies with crisp edges and soft centers.'
|
en: 'Small-batch cookies with crisp edges and soft centers.'
|
||||||
},
|
},
|
||||||
href: '/shop'
|
href: '/shop',
|
||||||
|
icon: 'cookie'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const ADD_PAGE_SECTION_GUIDANCE: Record<string, { required: string[]; example: s
|
|||||||
},
|
},
|
||||||
CUSTOM: {
|
CUSTOM: {
|
||||||
required: ["componentId"],
|
required: ["componentId"],
|
||||||
example: `{ "componentId": "BakeryCategories", "props": { "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "layout": "horizontal", "items": [{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "href": "/shop" }] } }`
|
example: `{ "componentId": "BakeryCategories", "props": { "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "layout": "horizontal", "items": [{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "href": "/shop", "icon": "bakery" }] } }`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ArrowRight, CakeSlice, Cookie, Croissant, Gift, type LucideIcon } from 'lucide-react';
|
||||||
import type { CustomBlockComponentProps } from '../../config/customRegistry';
|
import type { CustomBlockComponentProps } from '../../config/customRegistry';
|
||||||
import { getLocalizedField } from '../../utils/localization';
|
import { getLocalizedField } from '../../utils/localization';
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ type BakeryCategory = {
|
|||||||
description?: LocalizedValue;
|
description?: LocalizedValue;
|
||||||
href: string;
|
href: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResolvedBakeryCategory = {
|
type ResolvedBakeryCategory = {
|
||||||
@@ -17,6 +19,19 @@ type ResolvedBakeryCategory = {
|
|||||||
description: string;
|
description: string;
|
||||||
href: string;
|
href: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconMap: Record<string, LucideIcon> = {
|
||||||
|
bakery: Croissant,
|
||||||
|
bread: Croissant,
|
||||||
|
cake: CakeSlice,
|
||||||
|
cakes: CakeSlice,
|
||||||
|
dessert: Cookie,
|
||||||
|
cookie: Cookie,
|
||||||
|
cookies: Cookie,
|
||||||
|
gift: Gift,
|
||||||
|
gifts: Gift,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultItems: BakeryCategory[] = [
|
const defaultItems: BakeryCategory[] = [
|
||||||
@@ -24,21 +39,25 @@ const defaultItems: BakeryCategory[] = [
|
|||||||
title: 'Breads',
|
title: 'Breads',
|
||||||
description: 'Daily loaves, sourdough, and soft sandwich breads.',
|
description: 'Daily loaves, sourdough, and soft sandwich breads.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'bakery',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Pastries',
|
title: 'Pastries',
|
||||||
description: 'Layered butter pastries for breakfast and gifting.',
|
description: 'Layered butter pastries for breakfast and gifting.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'dessert',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cakes',
|
title: 'Cakes',
|
||||||
description: 'Celebration cakes and slices for quiet afternoons.',
|
description: 'Celebration cakes and slices for quiet afternoons.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'cake',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cookies',
|
title: 'Cookies',
|
||||||
description: 'Small-batch cookies with crisp edges and soft centers.',
|
description: 'Small-batch cookies with crisp edges and soft centers.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'cookie',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -66,6 +85,7 @@ function readItems(value: unknown, lang: string): ResolvedBakeryCategory[] {
|
|||||||
description: readLocalized(record.description, lang, ''),
|
description: readLocalized(record.description, lang, ''),
|
||||||
href: readString(record.href, '/shop'),
|
href: readString(record.href, '/shop'),
|
||||||
imageUrl: typeof record.imageUrl === 'string' ? record.imageUrl : undefined,
|
imageUrl: typeof record.imageUrl === 'string' ? record.imageUrl : undefined,
|
||||||
|
icon: readString(record.icon, 'bakery'),
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -101,7 +121,7 @@ export default function BakeryCategories({ props }: CustomBlockComponentProps) {
|
|||||||
to={item.href}
|
to={item.href}
|
||||||
className="ui-shell-card ui-shell-card-hover group overflow-hidden"
|
className="ui-shell-card ui-shell-card-hover group overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="relative flex aspect-[4/3] items-center justify-center overflow-hidden bg-pop-black/5">
|
<div className="relative flex aspect-[16/9] items-center justify-center overflow-hidden bg-pop-black/5">
|
||||||
{item.imageUrl ? (
|
{item.imageUrl ? (
|
||||||
<img
|
<img
|
||||||
src={item.imageUrl}
|
src={item.imageUrl}
|
||||||
@@ -114,9 +134,18 @@ export default function BakeryCategories({ props }: CustomBlockComponentProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 p-6">
|
<div className="relative flex min-h-20 items-end gap-4 px-5 pb-4 pt-7">
|
||||||
<h3 className="text-xl font-semibold text-pop-black">{item.title}</h3>
|
<div className="absolute -top-8 left-5 flex h-14 w-14 items-center justify-center rounded-full border border-pop-pink/30 bg-pop-bg text-pop-pink shadow-sm">
|
||||||
<p className="text-sm leading-relaxed text-pop-black/70">{item.description}</p>
|
{(() => {
|
||||||
|
const Icon = iconMap[item.icon] ?? Croissant;
|
||||||
|
return <Icon className="h-7 w-7" strokeWidth={1.7} />;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="truncate text-lg font-semibold leading-tight text-pop-black">{item.title}</h3>
|
||||||
|
<p className="truncate text-sm leading-tight text-pop-black/70">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="mb-2 h-4 w-4 shrink-0 text-pop-black transition group-hover:translate-x-1 group-hover:text-pop-pink" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { ArrowRight, CakeSlice, Cookie, Croissant, Gift, type LucideIcon } from 'lucide-react'
|
||||||
import Link from '../LocalizedLink'
|
import Link from '../LocalizedLink'
|
||||||
import type { CustomBlockComponentProps } from '../../config/customRegistry'
|
import type { CustomBlockComponentProps } from '../../config/customRegistry'
|
||||||
import { resolveLocalized } from '../../lib/localization'
|
import { resolveLocalized } from '../../lib/localization'
|
||||||
@@ -10,6 +11,7 @@ type BakeryCategory = {
|
|||||||
description?: LocalizedValue
|
description?: LocalizedValue
|
||||||
href: string
|
href: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
icon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResolvedBakeryCategory = {
|
type ResolvedBakeryCategory = {
|
||||||
@@ -17,6 +19,19 @@ type ResolvedBakeryCategory = {
|
|||||||
description: string
|
description: string
|
||||||
href: string
|
href: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMap: Record<string, LucideIcon> = {
|
||||||
|
bakery: Croissant,
|
||||||
|
bread: Croissant,
|
||||||
|
cake: CakeSlice,
|
||||||
|
cakes: CakeSlice,
|
||||||
|
dessert: Cookie,
|
||||||
|
cookie: Cookie,
|
||||||
|
cookies: Cookie,
|
||||||
|
gift: Gift,
|
||||||
|
gifts: Gift,
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultItems: BakeryCategory[] = [
|
const defaultItems: BakeryCategory[] = [
|
||||||
@@ -24,21 +39,25 @@ const defaultItems: BakeryCategory[] = [
|
|||||||
title: 'Breads',
|
title: 'Breads',
|
||||||
description: 'Daily loaves, sourdough, and soft sandwich breads.',
|
description: 'Daily loaves, sourdough, and soft sandwich breads.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'bakery',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Pastries',
|
title: 'Pastries',
|
||||||
description: 'Layered butter pastries for breakfast and gifting.',
|
description: 'Layered butter pastries for breakfast and gifting.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'dessert',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cakes',
|
title: 'Cakes',
|
||||||
description: 'Celebration cakes and slices for quiet afternoons.',
|
description: 'Celebration cakes and slices for quiet afternoons.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'cake',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cookies',
|
title: 'Cookies',
|
||||||
description: 'Small-batch cookies with crisp edges and soft centers.',
|
description: 'Small-batch cookies with crisp edges and soft centers.',
|
||||||
href: '/shop',
|
href: '/shop',
|
||||||
|
icon: 'cookie',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -66,6 +85,7 @@ function readItems(value: unknown, lang: string): ResolvedBakeryCategory[] {
|
|||||||
description: readLocalized(record.description, lang, ''),
|
description: readLocalized(record.description, lang, ''),
|
||||||
href: readString(record.href, '/shop'),
|
href: readString(record.href, '/shop'),
|
||||||
imageUrl: typeof record.imageUrl === 'string' ? record.imageUrl : undefined,
|
imageUrl: typeof record.imageUrl === 'string' ? record.imageUrl : undefined,
|
||||||
|
icon: readString(record.icon, 'bakery'),
|
||||||
})
|
})
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
@@ -97,7 +117,7 @@ export default function BakeryCategories({ props, lang }: CustomBlockComponentPr
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className="group overflow-hidden rounded-2xl border border-line-primary/20 bg-surface-card shadow-card transition hover:-translate-y-1 hover:shadow-card-hover"
|
className="group overflow-hidden rounded-2xl border border-line-primary/20 bg-surface-card shadow-card transition hover:-translate-y-1 hover:shadow-card-hover"
|
||||||
>
|
>
|
||||||
<div className="relative flex aspect-[4/3] items-center justify-center overflow-hidden bg-surface-muted">
|
<div className="relative flex aspect-[16/9] items-center justify-center overflow-hidden bg-surface-muted">
|
||||||
{item.imageUrl ? (
|
{item.imageUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={item.imageUrl}
|
src={item.imageUrl}
|
||||||
@@ -112,9 +132,18 @@ export default function BakeryCategories({ props, lang }: CustomBlockComponentPr
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 p-6">
|
<div className="relative flex min-h-20 items-end gap-4 px-5 pb-4 pt-7">
|
||||||
<h3 className="text-xl font-semibold text-text-main">{item.title}</h3>
|
<div className="absolute -top-8 left-5 flex h-14 w-14 items-center justify-center rounded-full border border-brand-primary/30 bg-surface-card text-brand-primary shadow-card">
|
||||||
<p className="text-sm leading-relaxed text-muted-text">{item.description}</p>
|
{(() => {
|
||||||
|
const Icon = iconMap[item.icon] ?? Croissant
|
||||||
|
return <Icon className="h-7 w-7" strokeWidth={1.7} />
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="truncate text-lg font-semibold leading-tight text-text-main">{item.title}</h3>
|
||||||
|
<p className="truncate text-sm leading-tight text-muted-text">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="mb-2 h-4 w-4 shrink-0 text-text-main transition group-hover:translate-x-1 group-hover:text-brand-primary" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in new issue
Block a user