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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user