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:
Kunthawat Greethong
2026-06-08 23:30:48 +07:00
parent 789473271e
commit b5be45bcd6
11 changed files with 1767 additions and 1984 deletions

View File

@@ -5,9 +5,14 @@ import Footer from '../components/Footer.astro';
import PageHero from '../components/PageHero.astro';
import PortfolioCard from '../components/PortfolioCard.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 portfolio = await getCollection('portfolio');
// Filter: only show real cases (have url) and not drafts
const realPortfolio = portfolio.filter(p => p.data.url && p.data.url !== '');
// Industry filter metadata: id -> { label, icon }
// Icons are lucide-style SVGs; emoji-free.
@@ -44,17 +49,19 @@ const industryFilters = [
>
{f.id !== 'all' && <Icon name={f.icon as any} size={16} class="filter-icon" />}
{f.id === 'all' && <Icon name={f.icon as any} size={16} class="filter-icon" />}
<span>{f.id === 'all' ? `ทั้งหมด (${portfolio.length})` : f.label}</span>
<span>{f.id === 'all' ? `ทั้งหมด (${realPortfolio.length})` : f.label}</span>
</button>
))}
</div>
</div>
</section>
<section class="section portfolio-section">
<div class="container">
<section class="section section-bento">
<DecoOrb color="yellow" size="500px" speed={0.4} position={{ top: '-150px', right: '-150px' }} opacity={0.2} blur="80px" />
<DecoOrb color="soft" size="400px" speed={0.3} position={{ bottom: '-100px', left: '-100px' }} opacity={0.35} blur="80px" />
<div class="container" style="position: relative; z-index: 1;">
<div class="portfolio-grid stagger-children">
{portfolio.map(item => (
{realPortfolio.map(item => (
<PortfolioCard
name={item.data.name}
url={item.data.url || '#'}
@@ -71,9 +78,11 @@ const industryFilters = [
</div>
</section>
<!-- "ดีลที่เราเลือก" Section -->
<section class="section section-soft">
<div class="container">
<!-- "ดีลที่เราเลือก" Section — Bento tiles -->
<section class="section section-soft section-bento">
<DecoOrb color="purple" size="500px" speed={0.4} position={{ top: '-200px', left: '20%' }} opacity={0.18} blur="100px" />
<DecoOrb color="yellow" size="350px" speed={0.3} position={{ bottom: '-100px', right: '5%' }} opacity={0.25} 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">
@@ -81,23 +90,19 @@ const industryFilters = [
</h2>
</div>
<div class="reasons-grid stagger-children">
<div class="reason-card">
<div class="reason-num">1</div>
<h3 class="reason-title">ธุรกิจที่พร้อมจริง ๆ</h3>
<p class="reason-desc">เราคุยกับเจ้าของธุรกิจก่อน ถ้าเป้าหมายยังไม่ชัด เราจะแนะนำให้รอก่อน ดีกว่าเสียเงินแล้วไม่ได้ผล</p>
</div>
<div class="reason-card">
<div class="reason-num">2</div>
<h3 class="reason-title">งบประมาณที่สมเหตุสมผล</h3>
<p class="reason-desc">เราไม่ได้ถูกที่สุด แต่ก็ไม่ได้แพงที่สุด ถ้าใครบอก "งบ 5,000 ทำเว็บได้ไหม" — เราแนะนำให้ไปฟรีแลนซ์ก่อน</p>
</div>
<div class="reason-card">
<div class="reason-num">3</div>
<h3 class="reason-title">ลูกค้าที่ฟัง</h3>
<p class="reason-desc">เราทำงานกับลูกค้าที่พร้อมฟังคำแนะนำ ไม่ใช่ลูกค้าที่บอก "ทำตามนี้เป๊ะ ๆ" แล้วผิดคาดทุกที</p>
</div>
</div>
<BentoGrid>
<BentoTile span={4} surface="yellow" eyebrow="ข้อ 01" title="ธุรกิจที่พร้อมจริง ๆ">
<p>เราคุยกับเจ้าของธุรกิจก่อน ถ้าเป้าหมายยังไม่ชัด เราจะแนะนำให้รอก่อน ดีกว่าเสียเงินแล้วไม่ได้ผล</p>
</BentoTile>
<BentoTile span={4} surface="purple-soft" eyebrow="ข้อ 02" title="งบประมาณที่สมเหตุสมผล">
<p>เราไม่ได้ถูกที่สุด แต่ก็ไม่ได้แพงที่สุด ถ้าใครบอก "งบ 5,000 ทำเว็บได้ไหม" — เราแนะนำให้ไปฟรีแลนซ์ก่อน</p>
</BentoTile>
<BentoTile span={4} surface="mint" eyebrow="ข้อ 03" title="ลูกค้าที่ฟัง">
<p>เราทำงานกับลูกค้าที่พร้อมฟังคำแนะนำ ไม่ใช่ลูกค้าที่บอก "ทำตามนี้เป๊ะ ๆ" แล้วผิดคาดทุกที</p>
</BentoTile>
</BentoGrid>
</div>
</section>
@@ -137,9 +142,27 @@ const industryFilters = [
});
});
});
// Parallax orbs
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>
<style>
.section-bento {
position: relative;
overflow: hidden;
}
/* Filter bar (unchanged) */
.filter-section {
background: var(--color-bg-alt);
padding: 20px 0;
@@ -182,10 +205,7 @@ const industryFilters = [
}
.filter-icon { color: currentColor; }
.portfolio-section { background: var(--color-white); }
.section-soft { background: var(--color-bg-alt); }
.section-yellow { background: var(--color-primary); }
/* Portfolio grid (unchanged — uses PortfolioCard component) */
.portfolio-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -193,65 +213,10 @@ const industryFilters = [
}
.section-header { text-align: center; margin-bottom: 48px; }
.section-badge {
display: inline-block;
background: var(--color-primary);
color: var(--color-black);
padding: 8px 20px;
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 16px;
}
.section-title {
font-family: var(--font-display);
font-size: clamp(28px, 4vw, 40px);
font-weight: 900;
line-height: 1.2;
color: var(--color-black);
}
.section-title .highlight { color: var(--color-primary-dark); }
.reasons-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.reason-card {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
padding: 32px;
}
.reason-num {
display: inline-block;
width: 40px;
height: 40px;
background: var(--color-primary);
color: var(--color-black);
border-radius: 50%;
text-align: center;
line-height: 40px;
font-family: var(--font-display);
font-size: 18px;
font-weight: 900;
margin-bottom: 16px;
}
.reason-title {
font-family: var(--font-display);
font-size: 18px;
font-weight: 800;
color: var(--color-black);
margin-bottom: 8px;
}
.reason-desc {
font-size: 15px;
color: var(--color-gray-600);
line-height: 1.6;
}
.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; }
.cta-title {
font-family: var(--font-display);
@@ -280,7 +245,6 @@ const industryFilters = [
@media (max-width: 1024px) {
.portfolio-grid { grid-template-columns: repeat(2, 1fr); }
.reasons-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.portfolio-grid { grid-template-columns: 1fr; }