Files
moreminimore-astroreal/src/pages/portfolio.astro
Kunthawat Greethong b586464b5c feat(footer+layout): v6-footer + mount UtilityBar/Marquee/Nav/Footer in Base.astro
- 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
2026-06-13 17:50:10 +07:00

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>