- Footer.astro (v6-footer): REPLACED legacy 439-line version. 4-col sitemap bound to site settings (phone, email, line, facebook, linkedin) + servicesDropdown + company links. Logo uses /images/logo-long-black.png (local, was hardcoded to dataroot CDN in v7-5). - Base.astro: mount <UtilityBar /> + <Marquee /> + <Navigation /> + <Footer /> around <slot />. Nav receives currentPath for active link highlight. Animation init now runs BOTH initAnimations (legacy bento) and fxInit (v7-5). - SWEEP: removed duplicate <Navigation /> / <Footer /> + their imports from 11 page files. Idempotent script via execute_code. Verified: all 9 pages return 200, header/footer render exactly once each. Refs: .hermes/plans/2026-06-13_124000-moreminimore-v7-5-migration.md Task 3.1-3.2
245 lines
9.9 KiB
Plaintext
245 lines
9.9 KiB
Plaintext
---
|
|
import Base from '../layouts/Base.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.
|
|
// Service category filters (multi-category supported via comma-sep)
|
|
const serviceFilters = [
|
|
{ id: 'all', label: 'ทั้งหมด', icon: 'layers' },
|
|
{ id: 'consult', label: 'Consult', icon: 'briefcase' },
|
|
{ id: 'webdev', label: 'Website Development', icon: 'code' },
|
|
];
|
|
---
|
|
|
|
<Base title="ผลงาน | MoreminiMore | รับทำเว็บไซต์ SEO AI Chatbot">
|
|
<PageHero
|
|
badge="9 โปรเจกต์ · 5 อุตสาหกรรม · ผลงานจริงทุกชิ้น"
|
|
title="เราส่งมอบให้ใคร มาบ้างแล้วบ้าง"
|
|
subtitle="โปรเจกต์จริง ลูกค้าจริง เว็บไซต์จริงที่ใช้งานอยู่ทุกวันนี้ — คลิกเข้าไปดูได้เลย"
|
|
/>
|
|
|
|
<!-- Industry Filter Bar -->
|
|
<section class="filter-section">
|
|
<div class="container">
|
|
<div class="filter-bar">
|
|
{serviceFilters.map(f => (
|
|
<button
|
|
class="filter-btn"
|
|
class:list={[{ active: f.id === 'all' }]}
|
|
data-filter={f.id}
|
|
>
|
|
{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' ? `ทั้งหมด (${realPortfolio.length})` : f.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<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">
|
|
{realPortfolio.map(item => (
|
|
<PortfolioCard
|
|
name={item.data.name}
|
|
url={item.data.url || '#'}
|
|
category={item.data.category}
|
|
category_label={item.data.category_label}
|
|
industry={item.data.industry}
|
|
thumbnail={item.data.thumbnail}
|
|
description={item.data.description}
|
|
what_we_did={item.data.what_we_did}
|
|
result={item.data.result}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- "ดีลที่เราเลือก" 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">
|
|
เรา <span class="highlight">เลือก</span> โปรเจกต์ที่ทำ — ไม่ใช่ทุกงานที่มา เรารับ
|
|
</h2>
|
|
</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>
|
|
|
|
<section class="section section-yellow cta-section">
|
|
<div class="container">
|
|
<div class="cta-content reveal">
|
|
<h2 class="cta-title">อยากเป็น <span class="highlight">ผลงานชิ้นต่อไป</span> ของเรา?</h2>
|
|
<p class="cta-desc">ถ้าธุรกิจคุณพร้อม เราพร้อม — คุยกันก่อน 30 นาที แล้วตัดสินใจเอง</p>
|
|
<div class="cta-actions">
|
|
<a href="/contact" class="btn btn-dark btn-lg">เริ่มโปรเจกต์ของคุณ →</a>
|
|
<a href="/services" class="btn btn-outline-dark btn-lg">ดูบริการที่เราทำ</a>
|
|
</div>
|
|
<p class="cta-reassurance">ไม่มี script · ไม่มี pressure · ตรงไปตรงมา</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</Base>
|
|
|
|
<script>
|
|
// Industry filter
|
|
const filterBtns = document.querySelectorAll('.filter-btn');
|
|
const cards = document.querySelectorAll('.portfolio-card');
|
|
filterBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
filterBtns.forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
const filter = btn.getAttribute('data-filter');
|
|
cards.forEach(card => {
|
|
const categories = (card.getAttribute('data-category') || '').toLowerCase().split(/[,\s]+/).filter(Boolean);
|
|
if (filter === 'all' || categories.includes(filter)) {
|
|
(card as HTMLElement).style.display = '';
|
|
} else {
|
|
(card as HTMLElement).style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
border-top: 1px solid var(--color-gray-200);
|
|
border-bottom: 1px solid var(--color-gray-200);
|
|
position: sticky;
|
|
top: 70px;
|
|
z-index: 50;
|
|
}
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 8px;
|
|
overflow-x: auto;
|
|
padding: 4px 0;
|
|
}
|
|
.filter-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 20px;
|
|
background: var(--color-white);
|
|
color: var(--color-black);
|
|
border: 1px solid var(--color-gray-200);
|
|
border-radius: var(--radius-full);
|
|
font-family: var(--font-display);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s var(--ease-out-expo);
|
|
white-space: nowrap;
|
|
}
|
|
.filter-btn:hover {
|
|
border-color: var(--color-primary);
|
|
color: var(--color-black);
|
|
}
|
|
.filter-btn.active {
|
|
background: var(--color-primary);
|
|
color: var(--color-black);
|
|
border-color: var(--color-primary);
|
|
}
|
|
.filter-icon { color: currentColor; }
|
|
|
|
/* Portfolio grid (unchanged — uses PortfolioCard component) */
|
|
.portfolio-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 28px;
|
|
}
|
|
|
|
.section-header { text-align: center; margin-bottom: 48px; }
|
|
.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);
|
|
font-size: clamp(28px, 4vw, 44px);
|
|
font-weight: 900;
|
|
color: var(--color-black);
|
|
margin-bottom: 16px;
|
|
}
|
|
.cta-title .highlight { color: var(--color-black); text-decoration: underline; text-decoration-color: var(--color-black); text-underline-offset: 6px; text-decoration-thickness: 4px; }
|
|
.cta-desc {
|
|
font-size: 18px;
|
|
color: rgba(0, 0, 0, 0.7);
|
|
margin-bottom: 32px;
|
|
}
|
|
.cta-actions {
|
|
display: flex;
|
|
gap: 16px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 16px;
|
|
}
|
|
.cta-reassurance {
|
|
font-size: 14px;
|
|
color: rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.portfolio-grid { grid-template-columns: repeat(2, 1fr); }
|
|
}
|
|
@media (max-width: 640px) {
|
|
.portfolio-grid { grid-template-columns: 1fr; }
|
|
.cta-actions { flex-direction: column; }
|
|
.cta-actions .btn { width: 100%; justify-content: center; }
|
|
}
|
|
</style>
|