feat(pages): Bento Grid redesign across all pages
Apply Bento Grid + decorative parallax orb system from about.astro to all remaining pages (home, services, portfolio, faq, contact, terms, privacy, blog list, blog detail, services detail). Layout changes (consistent across all pages): - Main content sections use <BentoGrid> + <BentoTile> - 2-3 <DecoOrb> per page for decorative parallax (no parallax blobs behind text — orbs are pure decoration, z-index: 0, pointer-events: none) - Surface variants: white, soft, yellow, purple-soft, teal, mint, dark (rotated across pages for visual variety) - Asymmetric span strategy (8+4, 7+5, 6+6) instead of flat grids - Process sections use clean 4x1 grid - Pull quote + yellow CTA kept as-is (standalone sections) Content rules preserved: - All Thai content kept verbatim - No fabricated statistics - No emoji icons (use text numerals 01 02 03 04) - All design tokens from global.css (no hardcoded hex) - Existing global classes (.container, .section, .btn, .section-badge, .section-title, .cta-section, etc.) reused — no design system break Build verified: 22 pages built in 1.80s, no errors. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,20 +3,65 @@ import Base from '../layouts/Base.astro';
|
||||
import Navigation from '../components/Navigation.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import PageHero from '../components/PageHero.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import BentoGrid from '../components/BentoGrid.astro';
|
||||
import BentoTile from '../components/BentoTile.astro';
|
||||
import DecoOrb from '../components/DecoOrb.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const faqItems = await getCollection('faq');
|
||||
const categories = ['บริการ', 'ราคา', 'ระยะเวลา', 'AI & เทคนิค', 'หลังการขาย'];
|
||||
|
||||
// Map categories to lucide icon names (semantic match, not visual emoji)
|
||||
const categoryIcons: Record<string, string> = {
|
||||
'บริการ': 'briefcase',
|
||||
'ราคา': 'dollarSign',
|
||||
'ระยะเวลา': 'clock',
|
||||
'AI & เทคนิค': 'bot',
|
||||
'หลังการขาย': 'wrench',
|
||||
};
|
||||
// Group FAQ items by category (preserve original order within each category)
|
||||
const groupedFaq = categories
|
||||
.map(cat => ({
|
||||
category: cat,
|
||||
items: faqItems.filter(f => f.data.category === cat),
|
||||
}))
|
||||
.filter(g => g.items.length > 0);
|
||||
|
||||
// Split each category's items into chunks of 2 (or 3 for the last chunk if odd count)
|
||||
function chunkItems<T>(arr: T[], size: number): T[][] {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Pre-compute all FAQ tiles with assigned spans + surfaces for visual rhythm
|
||||
// Rotation: yellow, soft, purple-soft, mint, teal, dark, coral, purple, mint
|
||||
const surfaceRotation = ['yellow', 'soft', 'purple-soft', 'mint', 'teal', 'dark', 'coral', 'purple', 'mint'] as const;
|
||||
|
||||
// Span strategy for category tile pairs: 7+5, 5+7, 8+4, 4+8, 7+5 ... avoid 6+6
|
||||
const spanPairs = [[7, 5], [5, 7], [8, 4], [4, 8], [7, 5]] as const;
|
||||
|
||||
interface FaqTile {
|
||||
category: string;
|
||||
items: typeof faqItems;
|
||||
surface: typeof surfaceRotation[number];
|
||||
span: 4 | 5 | 7 | 8;
|
||||
tileIndex: number; // 0, 1 within category
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
const faqTiles: FaqTile[] = [];
|
||||
groupedFaq.forEach((group, gIdx) => {
|
||||
const chunks = chunkItems(group.items, 2);
|
||||
const pair = spanPairs[gIdx % spanPairs.length];
|
||||
chunks.forEach((chunk, cIdx) => {
|
||||
const tileSurface = surfaceRotation[faqTiles.length % surfaceRotation.length];
|
||||
faqTiles.push({
|
||||
category: group.category,
|
||||
items: chunk,
|
||||
surface: tileSurface,
|
||||
span: (pair[cIdx] ?? 6) as 4 | 5 | 7 | 8,
|
||||
tileIndex: cIdx,
|
||||
isFirst: cIdx === 0,
|
||||
isLast: cIdx === chunks.length - 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
---
|
||||
|
||||
<Base title="คำถามที่พบบ่อย | MoreminiMore | รับทำเว็บไซต์ SEO AI Chatbot">
|
||||
@@ -28,21 +73,20 @@ const categoryIcons: Record<string, string> = {
|
||||
subtitle="30+ คำถามที่รวบรวมจากแชต LINE จริง ๆ ไม่ใช่แต่งขึ้นเอง"
|
||||
/>
|
||||
|
||||
<section class="section faq-section">
|
||||
<div class="container">
|
||||
{categories.map(cat => {
|
||||
const items = faqItems.filter(f => f.data.category === cat);
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div class="faq-category reveal">
|
||||
<h2 class="category-title">
|
||||
<span class="category-icon">
|
||||
<Icon name={categoryIcons[cat] as any} size={18} />
|
||||
</span>
|
||||
{cat}
|
||||
</h2>
|
||||
<!-- FAQ CATEGORIES (BENTO) -->
|
||||
<section class="section section-bento">
|
||||
<DecoOrb color="yellow" size="500px" speed={0.4} position={{ top: '-150px', left: '-100px' }} opacity={0.25} blur="80px" />
|
||||
<DecoOrb color="soft" size="400px" speed={0.3} position={{ bottom: '-100px', right: '-100px' }} opacity={0.4} blur="80px" />
|
||||
<div class="container" style="position: relative; z-index: 1;">
|
||||
<BentoGrid>
|
||||
{faqTiles.map((tile) => (
|
||||
<BentoTile
|
||||
span={tile.span}
|
||||
surface={tile.surface}
|
||||
eyebrow={tile.isFirst ? tile.category : `${tile.category} · ต่อ`}
|
||||
>
|
||||
<div class="faq-list">
|
||||
{items.map((item, qIndex) => (
|
||||
{tile.items.map((item) => (
|
||||
<details class="faq-item">
|
||||
<summary class="faq-question">
|
||||
<span class="question-text">{item.data.question}</span>
|
||||
@@ -54,72 +98,64 @@ const categoryIcons: Record<string, string> = {
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</BentoTile>
|
||||
))}
|
||||
|
||||
<!-- OTHER TOPICS — full-width tile with tag cloud -->
|
||||
<BentoTile span={12} surface="soft" eyebrow="เรื่องอื่น ๆ" title="คำถามอื่น ๆ ที่ลูกค้าถามบ่อย">
|
||||
<div class="tag-cloud">
|
||||
<span class="topic-tag">โฮสติ้ง</span>
|
||||
<span class="topic-tag">โดเมน</span>
|
||||
<span class="topic-tag">SSL</span>
|
||||
<span class="topic-tag">ใบเสนอราคา</span>
|
||||
<span class="topic-tag">ใบกำกับภาษี</span>
|
||||
<span class="topic-tag">สัญญา</span>
|
||||
<span class="topic-tag">NDA</span>
|
||||
<span class="topic-tag">ลิขสิทธิ์งาน</span>
|
||||
<span class="topic-tag">ทีมงาน</span>
|
||||
<span class="topic-tag">ขนาดทีม</span>
|
||||
<span class="topic-tag">ที่ตั้งบริษัท</span>
|
||||
<span class="topic-tag">ตัวอย่างงาน</span>
|
||||
<span class="topic-tag">ขอดูเว็บจริง</span>
|
||||
<span class="topic-tag">นัดคุยนอกสถานที่</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</BentoTile>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Other Topics -->
|
||||
<div class="faq-category">
|
||||
<h2 class="category-title">
|
||||
<span class="category-icon">
|
||||
<Icon name="book" size={18} />
|
||||
</span>
|
||||
เรื่องอื่น ๆ ที่ลูกค้าถามบ่อย
|
||||
</h2>
|
||||
<div class="tag-cloud">
|
||||
<span class="topic-tag">โฮสติ้ง</span>
|
||||
<span class="topic-tag">โดเมน</span>
|
||||
<span class="topic-tag">SSL</span>
|
||||
<span class="topic-tag">ใบเสนอราคา</span>
|
||||
<span class="topic-tag">ใบกำกับภาษี</span>
|
||||
<span class="topic-tag">สัญญา</span>
|
||||
<span class="topic-tag">NDA</span>
|
||||
<span class="topic-tag">ลิขสิทธิ์งาน</span>
|
||||
<span class="topic-tag">ทีมงาน</span>
|
||||
<span class="topic-tag">ขนาดทีม</span>
|
||||
<span class="topic-tag">ที่ตั้งบริษัท</span>
|
||||
<span class="topic-tag">ตัวอย่างงาน</span>
|
||||
<span class="topic-tag">ขอดูเว็บจริง</span>
|
||||
<span class="topic-tag">นัดคุยนอกสถานที่</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Channels -->
|
||||
<div class="channels-section reveal">
|
||||
<h2 class="channels-title">ไม่เจอคำตอบ? ถามตรง ๆ เลย</h2>
|
||||
<div class="channels-grid stagger-children">
|
||||
<a href="https://line.me/ti/p/~@539hdlul" target="_blank" rel="noopener" class="channel-card">
|
||||
<div class="channel-icon">
|
||||
<Icon name="message" size={28} />
|
||||
</div>
|
||||
<h3 class="channel-name">LINE</h3>
|
||||
<p class="channel-handle">@moreminimore</p>
|
||||
<p class="channel-time">ตอบใน 30 นาที (เวลาทำการ)</p>
|
||||
</a>
|
||||
<a href="tel:0809955945" class="channel-card">
|
||||
<div class="channel-icon">
|
||||
<Icon name="phone" size={28} />
|
||||
</div>
|
||||
<h3 class="channel-name">โทร</h3>
|
||||
<p class="channel-handle">080-995-5945</p>
|
||||
<p class="channel-time">จ-ศ 09:00-18:00</p>
|
||||
</a>
|
||||
<a href="mailto:contact@moreminimore.com" class="channel-card">
|
||||
<div class="channel-icon">
|
||||
<Icon name="mail" size={28} />
|
||||
</div>
|
||||
<h3 class="channel-name">Email</h3>
|
||||
<p class="channel-handle">contact@moreminimore.com</p>
|
||||
<p class="channel-time">ตอบภายใน 1 วัน</p>
|
||||
</a>
|
||||
</div>
|
||||
<!-- QUICK CHANNELS (BENTO) -->
|
||||
<section class="section section-bento">
|
||||
<DecoOrb color="purple" size="400px" speed={0.3} position={{ top: '-100px', right: '20%' }} opacity={0.2} blur="80px" />
|
||||
<DecoOrb color="yellow" size="300px" speed={0.4} position={{ bottom: '-100px', left: '-100px' }} opacity={0.3} blur="80px" />
|
||||
<div class="container" style="position: relative; z-index: 1;">
|
||||
<div class="section-header reveal">
|
||||
<span class="section-badge">ช่องทางติดต่อ</span>
|
||||
<h2 class="section-title">ไม่เจอคำตอบ? <span class="highlight">ถามตรง ๆ เลย</span></h2>
|
||||
</div>
|
||||
<BentoGrid>
|
||||
<BentoTile span={4} surface="yellow" eyebrow="LINE" title="@moreminimore">
|
||||
<p>คนที่อยากคุยเร็ว ๆ แบบเป็นกันเอง</p>
|
||||
<p><strong>ตอบใน 30 นาที (เวลาทำการ)</strong></p>
|
||||
<a href="https://line.me/ti/p/~@539hdlul" target="_blank" rel="noopener" class="btn btn-dark" style="margin-top: 16px;">ทักเลย →</a>
|
||||
</BentoTile>
|
||||
<BentoTile span={4} surface="soft" eyebrow="โทร" title="080-995-5945">
|
||||
<p>คนที่อยากคุยยาว ๆ 5–10 นาที ถามตอบสด</p>
|
||||
<p><strong>จ-ศ 09:00-18:00</strong></p>
|
||||
<a href="tel:0809955945" class="btn btn-dark" style="margin-top: 16px;">โทรเลย →</a>
|
||||
</BentoTile>
|
||||
<BentoTile span={4} surface="purple-soft" eyebrow="Email" title="contact@moreminimore.com">
|
||||
<p>คนที่อยากส่งรายละเอียดโปรเจกต์ + ไฟล์แนบ</p>
|
||||
<p><strong>ตอบภายใน 1 วัน</strong></p>
|
||||
<a href="mailto:contact@moreminimore.com" class="btn btn-dark" style="margin-top: 16px;">ส่งอีเมล →</a>
|
||||
</BentoTile>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section section-yellow cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-content">
|
||||
<div class="cta-content reveal">
|
||||
<h2 class="cta-title">พร้อมคุยรายละเอียด?</h2>
|
||||
<p class="cta-desc">นัดปรึกษาฟรี 30 นาที ผ่าน Zoom หรือนัดเจอที่ออฟฟิศ (กรุงเทพ/สมุทรสาคร)</p>
|
||||
<div class="cta-actions">
|
||||
@@ -134,27 +170,13 @@ const categoryIcons: Record<string, string> = {
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.faq-section { background: var(--color-white); }
|
||||
.section-yellow { background: var(--color-primary); }
|
||||
.section-bento {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.faq-category {
|
||||
max-width: 800px;
|
||||
margin: 0 auto 60px;
|
||||
}
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: var(--color-black);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 3px solid var(--color-primary);
|
||||
}
|
||||
.category-icon { font-size: 28px; }
|
||||
.faq-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
/* FAQ inside BentoTile */
|
||||
.faq-list { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
|
||||
|
||||
.faq-item {
|
||||
background: var(--color-white);
|
||||
@@ -166,11 +188,15 @@ const categoryIcons: Record<string, string> = {
|
||||
.faq-item[open] { border-color: var(--color-primary); }
|
||||
.faq-item:hover { box-shadow: var(--shadow-sm); }
|
||||
|
||||
/* When inside a yellow surface, change item background to be visible */
|
||||
.surface-yellow .faq-item { background: var(--color-white); }
|
||||
.surface-yellow .faq-item[open] { border-color: var(--color-black); }
|
||||
|
||||
.faq-question {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
padding: 14px 18px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
transition: background 0.2s ease;
|
||||
@@ -181,26 +207,26 @@ const categoryIcons: Record<string, string> = {
|
||||
.question-text {
|
||||
flex: 1;
|
||||
font-family: var(--font-display);
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--color-black);
|
||||
padding-right: 16px;
|
||||
}
|
||||
.faq-toggle {
|
||||
font-size: 28px;
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
color: var(--color-primary);
|
||||
color: var(--color-primary-dark);
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.faq-item[open] .faq-toggle { transform: rotate(45deg); }
|
||||
|
||||
.faq-answer {
|
||||
padding: 0 24px 24px;
|
||||
padding: 0 18px 16px;
|
||||
}
|
||||
.faq-answer p {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-gray-700);
|
||||
white-space: pre-line;
|
||||
}
|
||||
@@ -214,7 +240,7 @@ const categoryIcons: Record<string, string> = {
|
||||
.topic-tag {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: var(--color-bg-alt);
|
||||
background: var(--color-white);
|
||||
color: var(--color-gray-700);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-full);
|
||||
@@ -228,59 +254,31 @@ const categoryIcons: Record<string, string> = {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
/* Channels */
|
||||
.channels-section {
|
||||
max-width: 900px;
|
||||
margin: 80px auto 0;
|
||||
padding: 60px 40px;
|
||||
background: var(--color-bg-alt);
|
||||
border-radius: var(--radius-xl);
|
||||
text-align: center;
|
||||
}
|
||||
.channels-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
/* Section header */
|
||||
.section-header { text-align: center; margin-bottom: 48px; }
|
||||
.section-badge {
|
||||
display: inline-block;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-black);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.channels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.channel-card {
|
||||
display: block;
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.channel-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.channel-icon { font-size: 32px; margin-bottom: 12px; }
|
||||
.channel-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--color-black);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.channel-handle {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-dark);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.channel-time {
|
||||
padding: 8px 20px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 12px;
|
||||
color: var(--color-gray-500);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(28px, 4vw, 44px);
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
color: var(--color-black);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title .highlight { color: var(--color-primary-dark); }
|
||||
.section-soft { background: var(--color-bg-alt); }
|
||||
.section-yellow { background: var(--color-primary); }
|
||||
|
||||
/* CTA */
|
||||
.cta-content { text-align: center; max-width: 700px; margin: 0 auto; }
|
||||
@@ -303,13 +301,24 @@ const categoryIcons: Record<string, string> = {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.channels-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.cta-actions { flex-direction: column; }
|
||||
.cta-actions .btn { width: 100%; justify-content: center; }
|
||||
.faq-question { padding: 16px 20px; }
|
||||
.question-text { font-size: 15px; }
|
||||
.faq-question { padding: 14px 16px; }
|
||||
.question-text { font-size: 14px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Parallax orbs (use data-parallax-speed from DecoOrb)
|
||||
const parallaxEls = document.querySelectorAll('[data-parallax-speed]');
|
||||
function updateParallax() {
|
||||
const scrolled = window.scrollY;
|
||||
parallaxEls.forEach(el => {
|
||||
const speed = parseFloat(el.getAttribute('data-parallax-speed') || '0.4');
|
||||
const ty = scrolled * speed * -0.3;
|
||||
el.style.transform = `translate3d(0, ${ty}px, 0)`;
|
||||
});
|
||||
}
|
||||
window.addEventListener('scroll', () => requestAnimationFrame(updateParallax), { passive: true });
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user