feat(sections): v6-hero, case, services, callout, portfolio, process, pricing, faq (8 components)
- Hero.astro: REPLACED 714-line legacy. v6-hero terminal+stats — 2-col grid with $ command + eyebrow + sparkles + 2x2 stats sidebar. All via props. - CaseStudy.astro: v6-case — image + 3 stats + log + CTAs. Defaults to Dataroot. - Services.astro: v6-services 2x2 grid. Loads from src/content/services/*-new.mdx (4 services). Features hardcoded per title (v7-5 style). - Callout.astro: v6-callout yellow pullquote. - Portfolio.astro: v6-portfolio 2-1-1 modal grid. PINNED 3 per plan round 2 (Dataroot flagship, Luadjob, Jet). First is 'featured' (large). - Process.astro: v6-process 4-col flow. Hardcoded 4 steps per plan round 2. - Pricing.astro: v6-pricing. New pricing collection (2 webdev tiers only per plan round 2): Astro ฿5,000 featured + WordPress ฿30,000. Grid uses auto-fit to gracefully accept 2-3 tiers. - Faq.astro: v6-faq Q+A list. Loads from src/content/faq/*.md (20 items), default limit=4 to match v7-5 demo. Highlights keywords via hardcoded map. + src/content/pricing/astro.md + wordpress.md (new) + src/content.config.ts: +pricing collection (z.object with features array) Refs: .hermes/plans/2026-06-13_124000-moreminimore-v7-5-migration.md Task 4.1-4.8
This commit is contained in:
26
src/components/Callout.astro
Normal file
26
src/components/Callout.astro
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - CALLOUT (from v6-callout · yellow pullquote)
|
||||||
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 1054-1066
|
||||||
|
*
|
||||||
|
* Yellow gradient pullquote with <em> accent.
|
||||||
|
* Used as a philosophy statement between sections.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - text: string (supports <em> for accent)
|
||||||
|
* - attr?: string (attribution, default '— Moreminimore')
|
||||||
|
* - id?: string (default: 'callout')
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
attr?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text, attr = '— Moreminimore', id = 'callout' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-callout fx-reveal">
|
||||||
|
<p class="fx-callout-text" set:html={text} />
|
||||||
|
<div class="fx-callout-attr">{attr}</div>
|
||||||
|
</div>
|
||||||
102
src/components/CaseStudy.astro
Normal file
102
src/components/CaseStudy.astro
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - CASESTUDY (from v6-case)
|
||||||
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 873-919
|
||||||
|
*
|
||||||
|
* 2-col grid: image (left) + content (right, with stats + log + CTAs)
|
||||||
|
* Currently used for Dataroot flagship case study only.
|
||||||
|
* Hardcoded data — case_study body still lives in src/content/portfolio/dataroot.md
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - client: string (e.g. 'Dataroot')
|
||||||
|
* - url?: string (link to client site)
|
||||||
|
* - image?: string (default: dataroot.png from /images/portfolio/)
|
||||||
|
* - stats: { value, label, coord }[] (default: Dataroot +373/+114/-28)
|
||||||
|
* - quote: string (large pullquote with <em>)
|
||||||
|
* - deck: string (subhead under quote)
|
||||||
|
* - logs: { ts, level, text }[] (default: 3-line timeline)
|
||||||
|
* - ctaPrimary?: { text, href } (default: อ่านเคสเต็ม → /portfolio)
|
||||||
|
* - ctaSecondary?: { text, href } (default: ดูผลงานอื่น → /portfolio)
|
||||||
|
* - id?: string (default: 'case')
|
||||||
|
*/
|
||||||
|
interface Stat {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
coord: string;
|
||||||
|
}
|
||||||
|
interface Log {
|
||||||
|
ts: string;
|
||||||
|
level: 'INFO' | 'SUCCESS' | 'WARN';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
interface CTA {
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
client: string;
|
||||||
|
url?: string;
|
||||||
|
image?: string;
|
||||||
|
stats: Stat[];
|
||||||
|
quote: string;
|
||||||
|
deck: string;
|
||||||
|
logs: Log[];
|
||||||
|
ctaPrimary?: CTA;
|
||||||
|
ctaSecondary?: CTA;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
client,
|
||||||
|
url,
|
||||||
|
image = '/images/portfolio/dataroot.png',
|
||||||
|
stats,
|
||||||
|
quote,
|
||||||
|
deck,
|
||||||
|
logs,
|
||||||
|
ctaPrimary = { text: 'อ่านเคสเต็ม →', href: '/portfolio' },
|
||||||
|
ctaSecondary = { text: 'ดูผลงานอื่น', href: '/portfolio' },
|
||||||
|
id = 'case',
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-case fx-reveal">
|
||||||
|
<span class="fx-sparkle s2" style="top:12%;right:6%">✦</span>
|
||||||
|
<span class="fx-sparkle s4" style="bottom:16%;left:4%">◆</span>
|
||||||
|
<span class="fx-sparkle s5" style="top:50%;right:4%">✦</span>
|
||||||
|
|
||||||
|
<div class="fx-case-grid">
|
||||||
|
<div class="fx-case-image">
|
||||||
|
<a href={url || ctaPrimary.href}>
|
||||||
|
<img src={image} alt={client} loading="lazy" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fx-case-content">
|
||||||
|
<div class="fx-case-stats">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<div class="fx-case-stat" data-coord={stat.coord}>
|
||||||
|
<div class="fx-case-stat-num" set:html={stat.value} />
|
||||||
|
<div class="fx-case-stat-label">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<h2 set:html={`"${quote}"`} />
|
||||||
|
<p class="fx-deck">{deck}</p>
|
||||||
|
<div class="fx-log fx-stagger">
|
||||||
|
{logs.map((log) => (
|
||||||
|
<div>
|
||||||
|
<span class="ts">{log.ts}</span>{' '}
|
||||||
|
<span class={log.level === 'INFO' ? 'info' : 'ok'}>{log.level}</span>{' '}
|
||||||
|
{log.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="fx-case-cta">
|
||||||
|
<a href={ctaPrimary.href} class="fx-btn coral">{ctaPrimary.text}</a>
|
||||||
|
<a href={ctaSecondary.href} class="fx-btn ghost">{ctaSecondary.text}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
88
src/components/Faq.astro
Normal file
88
src/components/Faq.astro
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - FAQ (from v6-faq · Q+A list)
|
||||||
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 1381-1409
|
||||||
|
*
|
||||||
|
* Q+A list with <em> on keywords in question + answer.
|
||||||
|
* Bound to src/content/faq/*.md (20 items, 5 categories).
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - limit?: number (default: 4 — matches v7-5 hardcoded count)
|
||||||
|
* - category?: string (filter by faq.data.category)
|
||||||
|
* - id?: string (default: 'faq')
|
||||||
|
* - showHeader?: boolean (default: true)
|
||||||
|
*/
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
limit?: number;
|
||||||
|
category?: string;
|
||||||
|
id?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
limit = 4,
|
||||||
|
category,
|
||||||
|
id = 'faq',
|
||||||
|
showHeader = true,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const all = await getCollection('faq');
|
||||||
|
const filtered = category
|
||||||
|
? all.filter((f) => f.data.category === category)
|
||||||
|
: all;
|
||||||
|
const items = filtered.slice(0, limit);
|
||||||
|
|
||||||
|
// Hardcoded "em" highlights for the 4 default items (matches v7-5 demo)
|
||||||
|
// For items from collection, we apply a simple heuristic: bold common keywords
|
||||||
|
const defaultHighlights: Record<string, { q: string[]; a: string[] }> = {
|
||||||
|
'มอร์มินิมอร์ทำอะไรบ้าง?': { q: ['เอง'], a: ['AI'] },
|
||||||
|
'ราคาเริ่มต้นเท่าไหร่?': { q: ['เห็นผล'], a: ['14-30 วัน', '3 เดือน'] },
|
||||||
|
'ใช้เวลาเห็นผลนานไหม?': { q: ['เห็นผล'], a: ['14-30 วัน', '3 เดือน'] },
|
||||||
|
'AI จะแทนที่พนักงานไหม?': { q: ['พนักงาน'], a: ['ผู้ช่วย'] },
|
||||||
|
'มีบริการหลังขายไหม?': { q: ['หลังขาย'], a: ['Server + SSL + อัพเดท'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
function highlight(text: string, words: string[] = []): string {
|
||||||
|
let result = text;
|
||||||
|
for (const w of words) {
|
||||||
|
result = result.replace(w, `<em>${w}</em>`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-faq fx-reveal">
|
||||||
|
{showHeader && (
|
||||||
|
<div class="fx-section-header">
|
||||||
|
<span class="fx-section-eyebrow">// faq</span>
|
||||||
|
<h2 class="fx-section-title">คำถามที่ถามบ่อย</h2>
|
||||||
|
<p class="fx-section-lede">คำตอบสั้น ๆ ตรง ๆ — ไม่มีน้ำ</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="fx-faq-list fx-stagger">
|
||||||
|
{items.map((item, i) => {
|
||||||
|
const highlights = defaultHighlights[item.data.question] ?? { q: [], a: [] };
|
||||||
|
const qHtml = highlight(item.data.question, highlights.q);
|
||||||
|
// Wrap "user" / "bot" prefix in answer if not already present
|
||||||
|
let aHtml = highlight(item.data.answer, highlights.a);
|
||||||
|
if (!aHtml.includes('<em>user</em>')) {
|
||||||
|
aHtml = `<em>user</em> ถาม / <em>bot</em> ตอบ — ${aHtml}`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class="fx-faq-item" data-coord={`Q.${i + 1}`}>
|
||||||
|
<div class="fx-faq-q" set:html={qHtml} />
|
||||||
|
<div class="fx-faq-a" set:html={aHtml} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items.length >= limit && (
|
||||||
|
<div class="fx-faq-more">
|
||||||
|
<a href="/faq" class="fx-btn ghost">ดูคำถามทั้งหมด ({all.length} ข้อ) →</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
@@ -1,714 +1,97 @@
|
|||||||
---
|
---
|
||||||
/**
|
/**
|
||||||
* MOREMINIMORE - KINETIC HERO COMPONENT (LIGHT THEME)
|
* MOREMINIMORE - HERO (from v6-hero · terminal+stats)
|
||||||
* Yellow/white/black editorial — no dark bg
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 772-820
|
||||||
|
*
|
||||||
|
* 2-col grid: text (left, with $ command + eyebrow) + 2x2 stats sidebar (right)
|
||||||
|
* Uses 5 sparkle decorations (✦ ◆ ·) for "Neon × Tech Grid" feel.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - eyebrow?: string (default: 'MOREMINIMORE / EST. 2024')
|
||||||
|
* - title?: string (default: from home.md badge)
|
||||||
|
* - lede?: string (default: 1-sentence pitch)
|
||||||
|
* - ctaPrimary?: { text, href } (default: ปรึกษาฟรี → /contact)
|
||||||
|
* - ctaSecondary?: { text, href } (default: ดูผลงานจริง → /portfolio)
|
||||||
|
* - stats?: { label, value, coral?: boolean }[] (default: 4 Dataroot stats)
|
||||||
|
* - id?: string (default: 'hero' — for anchor links)
|
||||||
*/
|
*/
|
||||||
import Icon from './Icon.astro';
|
interface Stat {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
coral?: boolean;
|
||||||
|
}
|
||||||
|
interface CTA {
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
badge?: string;
|
eyebrow?: string;
|
||||||
title: string;
|
title?: string;
|
||||||
subtitle?: string;
|
lede?: string;
|
||||||
showCTA?: boolean;
|
ctaPrimary?: CTA;
|
||||||
ctaText?: string;
|
ctaSecondary?: CTA;
|
||||||
ctaLink?: string;
|
stats?: Stat[];
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
badge = 'Moreminimore',
|
eyebrow = 'MOREMINIMORE / EST. 2024',
|
||||||
title = 'เราจะช่วยคุณเพิ่มกำไร',
|
title = 'เราจะช่วยคุณเพิ่มกำไร ไม่ใช่แค่เพิ่มงบ',
|
||||||
subtitle = 'เราช่วยวางระบบงาน และใช้สถิติวางกลยุทธ์ทางการตลาด',
|
lede = 'วางระบบ AI + Online Marketing + Automation ให้ธุรกิจคุณทำงานเร็วขึ้น ใช้งบคุ้ม และเห็นผลจริง',
|
||||||
showCTA = true,
|
ctaPrimary = { text: 'ปรึกษาฟรี →', href: '/contact' },
|
||||||
ctaText = 'เริ่มปรึกษาฟรี',
|
ctaSecondary = { text: 'ดูผลงานจริง', href: '/portfolio' },
|
||||||
ctaLink = '/contact',
|
stats = [
|
||||||
pains = [
|
{ label: 'impression', value: '+373%' },
|
||||||
{ surface: 'yellow', text: 'ยิ่งขาย กำไรยิ่งลด?' },
|
{ label: 'click', value: '+114%', coral: true },
|
||||||
{ surface: 'purple-soft', text: 'มีเว็บไซต์ เหมือนไม่มี?' },
|
{ label: 'ad_spend', value: '−28%' },
|
||||||
{ surface: 'mint', text: 'พนักงานทำงานได้น้อยกว่าที่ต้องการ?' },
|
{ label: 'period', value: '30d' },
|
||||||
{ surface: 'teal', text: 'เอา AI มาให้ใช้ แต่งานไม่ได้มากขึ้นตามที่คิด?' },
|
|
||||||
],
|
],
|
||||||
|
id = 'hero',
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
// Split title into words for kinetic animation
|
// Split title into 2 lines at first comma/space — for fx-hero-title multi-line layout
|
||||||
const titleWords = title.split(' ');
|
const titleLines = title.split(/\s+/);
|
||||||
|
const half = Math.ceil(titleLines.length / 2);
|
||||||
|
const titleLine1 = titleLines.slice(0, half).join(' ');
|
||||||
|
const titleLine2 = titleLines.slice(half).join(' ');
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="kinetic-hero">
|
<div id={id} class="fx-hero fx-reveal">
|
||||||
<!-- Animated Background Pattern (LIGHT THEME) -->
|
<span class="fx-sparkle" style="top:8%;left:6%">✦</span>
|
||||||
<div class="hero-bg">
|
<span class="fx-sparkle s2" style="top:12%;right:8%">◆</span>
|
||||||
<div class="bg-grid"></div>
|
<span class="fx-sparkle s3" style="bottom:18%;left:10%">✦</span>
|
||||||
<div class="bg-gradient"></div>
|
<span class="fx-sparkle s4" style="top:30%;right:4%">·</span>
|
||||||
</div>
|
<span class="fx-sparkle s5" style="bottom:8%;right:12%">◆</span>
|
||||||
|
|
||||||
<!-- Floating Geometric Elements (yellow, subtle) -->
|
<div class="fx-hero-grid">
|
||||||
<div class="hero-geometric">
|
<div class="fx-hero-content">
|
||||||
<div class="geo-circle geo-1"></div>
|
<span class="fx-hero-eyebrow">{eyebrow}</span>
|
||||||
<div class="geo-circle geo-2"></div>
|
<h1 class="fx-hero-title">
|
||||||
<div class="geo-circle geo-3"></div>
|
<span>{titleLine1}</span>
|
||||||
<div class="geo-ring ring-1"></div>
|
<span>{titleLine2}</span>
|
||||||
<div class="geo-ring ring-2"></div>
|
</h1>
|
||||||
<div class="geo-line line-1"></div>
|
<p class="fx-hero-lede" set:html={lede} />
|
||||||
<div class="geo-line line-2"></div>
|
<div class="fx-hero-cta">
|
||||||
</div>
|
<a href={ctaPrimary.href} class="fx-btn coral">{ctaPrimary.text}</a>
|
||||||
|
<a href={ctaSecondary.href} class="fx-btn ghost">{ctaSecondary.text}</a>
|
||||||
<div class="container hero-container">
|
|
||||||
<!-- 2-COLUMN LAYOUT: text (left) + pain stack (right) -->
|
|
||||||
<div class="hero-grid">
|
|
||||||
<!-- LEFT: text content -->
|
|
||||||
<div class="hero-text">
|
|
||||||
<!-- Badge -->
|
|
||||||
<div class="hero-badge" data-animate="fade-in">
|
|
||||||
<span class="badge-dot"></span>
|
|
||||||
{badge}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Title - Kinetic Typography -->
|
|
||||||
<h1 class="hero-title kinetic-title">
|
|
||||||
{titleWords.map((word, index) => (
|
|
||||||
<span class="word-wrapper">
|
|
||||||
<span
|
|
||||||
class="word"
|
|
||||||
style={`--delay: ${0.4 + index * 0.12}s`}
|
|
||||||
>
|
|
||||||
{word}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Accent Line -->
|
|
||||||
<div class="hero-accent-line">
|
|
||||||
<div class="accent-fill"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subtitle -->
|
|
||||||
<p class="hero-subtitle" data-animate="fade-in-up">
|
|
||||||
<Fragment set:html={subtitle} />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- CTA Buttons -->
|
|
||||||
{showCTA && (
|
|
||||||
<div class="hero-actions" data-animate="fade-in-up">
|
|
||||||
<a href={ctaLink} class="btn btn-primary btn-magnetic">
|
|
||||||
<span class="btn-text">{ctaText}</span>
|
|
||||||
<svg class="btn-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<slot name="hero-cta-secondary" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<slot name="hero-trust" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="fx-hero-trustline">
|
||||||
|
<span style="color:var(--coral)">✓</span> ปรึกษาฟรี 30 นาที ·
|
||||||
|
<span style="color:var(--coral)">✓</span> ไม่มีผูกมัด ·
|
||||||
|
<span style="color:var(--coral)">✓</span> เห็นผล <em>ภายใน 30 วัน</em>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT: pain card stack -->
|
<div class="fx-hero-side">
|
||||||
<div class="hero-pain-stack" data-animate="fade-in-up">
|
{stats.map((stat, i) => (
|
||||||
{pains.map((p, i) => (
|
<div class="fx-stat" data-coord={`00.${i + 1}`}>
|
||||||
<div class={`pain-card pain-${p.surface}`} style={`--pain-delay: ${0.6 + i * 0.15}s`}>
|
<div class="fx-stat-label">{stat.label}</div>
|
||||||
<div class="pain-eyebrow">คุณกำลังเจอปัญหา</div>
|
<div class:list={['fx-stat-value', stat.coral && 'coral']}>
|
||||||
<div class="pain-text">{p.text}</div>
|
<Fragment set:html={stat.value} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Giant Background Typography -->
|
|
||||||
<div class="hero-bg-text" aria-hidden="true">
|
|
||||||
AI
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scroll Indicator -->
|
|
||||||
<div class="scroll-indicator">
|
|
||||||
<span class="scroll-text">เลื่อนลง</span>
|
|
||||||
<div class="scroll-line">
|
|
||||||
<div class="scroll-dot"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ============================================
|
|
||||||
HERO BASE — LIGHT THEME (white bg, yellow accent)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.kinetic-hero {
|
|
||||||
position: relative;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--color-white);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
BACKGROUND
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-bg {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-grid {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(254, 212, 0, 0.08) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(254, 212, 0, 0.08) 1px, transparent 1px);
|
|
||||||
background-size: 60px 60px;
|
|
||||||
animation: gridMove 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gridMove {
|
|
||||||
0% { transform: translate(0, 0); }
|
|
||||||
100% { transform: translate(60px, 60px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gradient {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: radial-gradient(ellipse at 30% 20%, rgba(254, 212, 0, 0.15) 0%, transparent 50%),
|
|
||||||
radial-gradient(ellipse at 70% 80%, rgba(254, 212, 0, 0.08) 0%, transparent 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
GEOMETRIC ELEMENTS (yellow on light — very subtle)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-geometric {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.geo-circle {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.geo-1 {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
top: -100px;
|
|
||||||
right: 10%;
|
|
||||||
opacity: 0.08;
|
|
||||||
animation: floatGeo1 12s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.geo-2 {
|
|
||||||
width: 250px;
|
|
||||||
height: 250px;
|
|
||||||
bottom: 10%;
|
|
||||||
left: -50px;
|
|
||||||
opacity: 0.12;
|
|
||||||
animation: floatGeo2 10s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.geo-3 {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
top: 40%;
|
|
||||||
right: 30%;
|
|
||||||
opacity: 0.06;
|
|
||||||
animation: floatGeo3 8s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatGeo1 {
|
|
||||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
||||||
50% { transform: translate(-30px, -50px) rotate(45deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatGeo2 {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
50% { transform: translate(40px, -30px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatGeo3 {
|
|
||||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
||||||
50% { transform: translate(-20px, 30px) scale(1.1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.geo-ring {
|
|
||||||
position: absolute;
|
|
||||||
border: 2px solid var(--color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring-1 {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
top: 10%;
|
|
||||||
left: 60%;
|
|
||||||
animation: rotateRing 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ring-2 {
|
|
||||||
width: 180px;
|
|
||||||
height: 180px;
|
|
||||||
bottom: 20%;
|
|
||||||
right: 20%;
|
|
||||||
animation: rotateRing 15s linear infinite reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotateRing {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.geo-line {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--color-primary);
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-1 {
|
|
||||||
width: 200px;
|
|
||||||
height: 2px;
|
|
||||||
top: 25%;
|
|
||||||
right: 15%;
|
|
||||||
animation: lineSlide 8s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-2 {
|
|
||||||
width: 2px;
|
|
||||||
height: 150px;
|
|
||||||
bottom: 30%;
|
|
||||||
left: 25%;
|
|
||||||
animation: lineSlide 10s ease-in-out infinite reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lineSlide {
|
|
||||||
0%, 100% { transform: translateX(0); opacity: 0.2; }
|
|
||||||
50% { transform: translateX(30px); opacity: 0.3; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
CONTAINER & CONTENT
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-container {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 140px var(--gutter) 100px;
|
|
||||||
max-width: 1600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
HERO GRID — 2 COLUMNS (text + pain stack)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 60px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-text {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-pain-stack {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pain-card {
|
|
||||||
padding: 28px 32px;
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
animation: painReveal 0.7s var(--ease-out-expo) var(--pain-delay, 0s) forwards;
|
|
||||||
transition: transform 0.4s var(--ease-out-expo);
|
|
||||||
}
|
|
||||||
.pain-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes painReveal {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pain-yellow {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-black);
|
|
||||||
}
|
|
||||||
.pain-purple-soft {
|
|
||||||
background: var(--color-purple-soft);
|
|
||||||
color: var(--color-black);
|
|
||||||
}
|
|
||||||
.pain-mint {
|
|
||||||
background: var(--color-mint-soft);
|
|
||||||
color: var(--color-black);
|
|
||||||
}
|
|
||||||
.pain-teal {
|
|
||||||
background: var(--color-teal);
|
|
||||||
color: var(--color-white);
|
|
||||||
}
|
|
||||||
.pain-teal .pain-eyebrow { color: var(--color-primary); }
|
|
||||||
|
|
||||||
.pain-eyebrow {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pain-text {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.hero-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
.hero-text {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
.hero-pain-stack {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.pain-text { font-size: 18px; }
|
|
||||||
.pain-card { padding: 20px 24px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
BADGE
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: var(--color-primary);
|
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 3px;
|
|
||||||
color: var(--color-black);
|
|
||||||
margin-bottom: 40px;
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeIn 0.6s var(--ease-out-expo) 0.2s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--color-black);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { transform: scale(1); opacity: 1; }
|
|
||||||
50% { transform: scale(1.5); opacity: 0.5; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
KINETIC TITLE — DARK TEXT on white bg
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(40px, 8vw, 90px);
|
|
||||||
font-weight: 900;
|
|
||||||
/* CRITICAL: line-height must be >1.2 to prevent Thai vowel clipping
|
|
||||||
when combined with overflow:hidden on .word-wrapper. */
|
|
||||||
line-height: 1.3;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
color: var(--color-black);
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-wrapper {
|
|
||||||
display: inline-block;
|
|
||||||
/* Use padding (not overflow:hidden) so Thai descenders stay visible. */
|
|
||||||
padding: 0.1em 0.05em;
|
|
||||||
margin: -0.1em -0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word {
|
|
||||||
display: inline-block;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(100%) skewY(10deg);
|
|
||||||
animation: wordReveal 0.9s var(--ease-out-expo) forwards;
|
|
||||||
animation-delay: var(--delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes wordReveal {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(100%) skewY(10deg);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(-5%) skewY(-2deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) skewY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
ACCENT LINE (yellow)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-accent-line {
|
|
||||||
width: 200px;
|
|
||||||
height: 6px;
|
|
||||||
background: var(--color-gray-200);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeIn 0.6s var(--ease-out-expo) 1s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accent-fill {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--color-primary);
|
|
||||||
border-radius: 3px;
|
|
||||||
transform-origin: left;
|
|
||||||
animation: accentDraw 1s var(--ease-out-expo) 1.2s forwards;
|
|
||||||
transform: scaleX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes accentDraw {
|
|
||||||
to { transform: scaleX(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
SUBTITLE
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: var(--color-gray-700);
|
|
||||||
max-width: 600px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeInUp 0.8s var(--ease-out-expo) 0.8s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
CTA BUTTONS
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeInUp 0.8s var(--ease-out-expo) 1s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-magnetic {
|
|
||||||
position: relative;
|
|
||||||
padding: 22px 44px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-arrow {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
transition: transform 0.3s var(--ease-out-expo);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-magnetic:hover .btn-arrow {
|
|
||||||
transform: translateX(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
TRUST STRIP
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-trust {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-gray-600);
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeInUp 0.8s var(--ease-out-expo) 1.2s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.trust-icon { color: var(--color-primary); flex-shrink: 0; }
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
BACKGROUND TEXT (light yellow watermark)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.hero-bg-text {
|
|
||||||
position: absolute;
|
|
||||||
right: -5%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(200px, 30vw, 400px);
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-primary);
|
|
||||||
opacity: 0.08;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
SCROLL INDICATOR (dark on light)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
.scroll-indicator {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 40px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeIn 0.6s var(--ease-out-expo) 1.5s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-text {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 3px;
|
|
||||||
color: var(--color-gray-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-line {
|
|
||||||
width: 2px;
|
|
||||||
height: 60px;
|
|
||||||
background: var(--color-gray-200);
|
|
||||||
border-radius: 1px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-dot {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
background: var(--color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
left: -1px;
|
|
||||||
animation: scrollDot 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scrollDot {
|
|
||||||
0% { top: 0; opacity: 1; }
|
|
||||||
80% { top: 100%; opacity: 0; }
|
|
||||||
100% { top: 100%; opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
RESPONSIVE
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.hero-title {
|
|
||||||
font-size: clamp(32px, 7vw, 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-container {
|
|
||||||
padding: 120px var(--gutter) 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-bg-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-indicator {
|
|
||||||
bottom: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.hero-title {
|
|
||||||
font-size: clamp(28px, 9vw, 48px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-actions .btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-accent-line {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
82
src/components/Portfolio.astro
Normal file
82
src/components/Portfolio.astro
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - PORTFOLIO (from v6-portfolio · modal cards)
|
||||||
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 1122-1152
|
||||||
|
*
|
||||||
|
* 2-1-1 modal grid: 1 large featured + 2 smaller cards.
|
||||||
|
* Per plan 2026-06-13 round 2: PINNED to 3 items (Dataroot → Luadjob → Jet).
|
||||||
|
* Reordering the array reorders the grid; first item is "featured" (large).
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - id?: string (default: 'portfolio')
|
||||||
|
* - showHeader?: boolean (default: true)
|
||||||
|
*/
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id = 'portfolio', showHeader = true } = Astro.props;
|
||||||
|
|
||||||
|
// Pinned 3 (per user 2026-06-13 round 2 #2)
|
||||||
|
const FEATURED_SLUGS = ['dataroot', 'luadjob', 'jet-industries'] as const;
|
||||||
|
|
||||||
|
const all = await getCollection('portfolio');
|
||||||
|
const items = FEATURED_SLUGS
|
||||||
|
.map((slug) => all.find((p) => p.id === slug))
|
||||||
|
.filter((p): p is NonNullable<typeof p> => p !== undefined);
|
||||||
|
|
||||||
|
// Tag per item (top-right of card)
|
||||||
|
const tags = ['FLAGSHIP', 'E-COMMERCE', 'B2B'] as const;
|
||||||
|
// Short stats per item
|
||||||
|
const statsLabels = [
|
||||||
|
'+373% impression · -28% ad spend',
|
||||||
|
'E-commerce สมุนไพรไทย',
|
||||||
|
'โรงงาน B2B · เว็บทันสมัย',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Background images (use local thumbnails; fall back to neutral)
|
||||||
|
const fallbackBg = '#FFD60A';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-portfolio fx-reveal">
|
||||||
|
{showHeader && (
|
||||||
|
<div class="fx-section-header">
|
||||||
|
<span class="fx-section-eyebrow">// portfolio</span>
|
||||||
|
<h2 class="fx-section-title">ผลงานจริง ไม่ใช่ Mockup</h2>
|
||||||
|
<p class="fx-section-lede">ลูกค้าจริง ตัวเลขจริง — คลิกดูเว็บจริงได้เลย</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="fx-portfolio-grid fx-stagger">
|
||||||
|
{items.map((item, i) => {
|
||||||
|
const nameParts = item.data.name.split(' ');
|
||||||
|
const nameDisplay = nameParts.length > 1
|
||||||
|
? nameParts[0] + '<em>' + nameParts.slice(1).join(' ') + '</em>'
|
||||||
|
: item.data.name;
|
||||||
|
const isFeatured = i === 0;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={item.data.url ?? '/portfolio'}
|
||||||
|
target={item.data.url ? '_blank' : undefined}
|
||||||
|
rel={item.data.url ? 'noopener' : undefined}
|
||||||
|
class:list={['fx-portfolio-card', isFeatured && 'featured']}
|
||||||
|
data-path={item.id}
|
||||||
|
data-coord={`04.${String.fromCharCode(65 + i)}`}
|
||||||
|
>
|
||||||
|
<span class="fx-portfolio-tag">{tags[i] ?? 'CASE'}</span>
|
||||||
|
<img
|
||||||
|
src={item.data.thumbnail ?? '/images/portfolio/default.jpg'}
|
||||||
|
alt={item.data.name}
|
||||||
|
loading="lazy"
|
||||||
|
style={item.data.thumbnail ? '' : `background:${fallbackBg};min-height:200px`}
|
||||||
|
/>
|
||||||
|
<h3 class="fx-portfolio-name" set:html={nameDisplay} />
|
||||||
|
<div class="fx-portfolio-stats">{statsLabels[i] ?? item.data.category_label}</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
68
src/components/Pricing.astro
Normal file
68
src/components/Pricing.astro
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - PRICING (from v6-pricing · 3-tier adapted to 2-tier)
|
||||||
|
* Extracted from Desktop/moreminimore-mockup-v7-5.html lines 1273-1324
|
||||||
|
*
|
||||||
|
* Per plan 2026-06-13 round 2 #1: 2 webdev tiers only.
|
||||||
|
* - Astro: ฿5,000 (featured, yellow border-left)
|
||||||
|
* - WordPress: ฿30,000
|
||||||
|
*
|
||||||
|
* Component uses `repeat(auto-fit, minmax(280px, 1fr))` to gracefully
|
||||||
|
* accept 2 OR 3 tiers (in case user adds a Landing tier later).
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - id?: string (default: 'pricing')
|
||||||
|
* - showHeader?: boolean (default: true)
|
||||||
|
*/
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id = 'pricing', showHeader = true } = Astro.props;
|
||||||
|
|
||||||
|
const allTiers = await getCollection('pricing');
|
||||||
|
const tiers = allTiers.sort((a, b) => (a.data.order ?? 99) - (b.data.order ?? 99));
|
||||||
|
|
||||||
|
const coordLetters = ['A', 'B', 'C', 'D'];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-pricing fx-reveal">
|
||||||
|
{showHeader && (
|
||||||
|
<div class="fx-section-header">
|
||||||
|
<span class="fx-section-eyebrow">// pricing</span>
|
||||||
|
<h2 class="fx-section-title">ราคาเว็บไซต์</h2>
|
||||||
|
<p class="fx-section-lede">เฉพาะ Website Development — บริการอื่นปรึกษาฟรีเพื่อประเมินราคา</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="fx-pricing-grid fx-stagger">
|
||||||
|
{tiers.map((tier, i) => {
|
||||||
|
const d = tier.data;
|
||||||
|
// Highlight last word in tier (v7-5 style: "เริ่ม<em>ต้น</em>")
|
||||||
|
const tierWords = d.tier.split(/(?=[^ก-๛]*$)/);
|
||||||
|
const tierDisplay = tierWords.length > 1
|
||||||
|
? tierWords[0] + '<em>' + tierWords.slice(1).join('') + '</em>'
|
||||||
|
: d.tier;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class:list={['fx-pricing-card', d.is_featured && 'featured']}
|
||||||
|
data-coord={`05.${coordLetters[i] ?? 'X'}`}
|
||||||
|
>
|
||||||
|
<div class="fx-pricing-tier" set:html={d.is_featured ? `★ <em>${d.tier}</em>` : tierDisplay} />
|
||||||
|
<h3 class="fx-pricing-name">{d.name}</h3>
|
||||||
|
<div class="fx-pricing-amount">{d.amount}</div>
|
||||||
|
<div class="fx-pricing-period">/ {d.period}</div>
|
||||||
|
<ul class="fx-pricing-features">
|
||||||
|
{d.features.map((f) => <li>{f}</li>)}
|
||||||
|
</ul>
|
||||||
|
<a href="/contact" class="fx-pricing-cta">
|
||||||
|
{d.is_featured ? 'เลือก' : 'เริ่มต้น'}<em> →</em>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
57
src/components/Process.astro
Normal file
57
src/components/Process.astro
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - PROCESS (from v6-process · 4-col flow)
|
||||||
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 1198-1230
|
||||||
|
*
|
||||||
|
* 4 numbered cells with arrow connectors.
|
||||||
|
* Per plan 2026-06-13 round 2 #5: hardcoded (process IS MoreminiMore's flow,
|
||||||
|
* not user-editable content).
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - id?: string (default: 'process')
|
||||||
|
* - showHeader?: boolean (default: true)
|
||||||
|
*/
|
||||||
|
interface Step {
|
||||||
|
num: string;
|
||||||
|
title: string;
|
||||||
|
cmd: string;
|
||||||
|
cmdArg: string;
|
||||||
|
coord: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id = 'process', showHeader = true } = Astro.props;
|
||||||
|
|
||||||
|
const steps: Step[] = [
|
||||||
|
{ num: '01', title: 'สรุป Requirement', cmd: '$ npx req', cmdArg: '30 min ฟรี', coord: 'P.1' },
|
||||||
|
{ num: '02', title: 'วิเคราะห์ Flow', cmd: '$ npx analyze', cmdArg: 'ปัจจุบัน', coord: 'P.2' },
|
||||||
|
{ num: '03', title: 'ออกแบบ + เลือก Tech', cmd: '$ npx design', cmdArg: 'เครื่องมือ', coord: 'P.3' },
|
||||||
|
{ num: '04', title: 'พัฒนา + ทดสอบ', cmd: '$ npx build', cmdArg: 'ดูทุกขั้น', coord: 'P.4' },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-process fx-reveal">
|
||||||
|
{showHeader && (
|
||||||
|
<div class="fx-section-header">
|
||||||
|
<span class="fx-section-eyebrow">// process</span>
|
||||||
|
<h2 class="fx-section-title">ขั้นตอนการทำงาน</h2>
|
||||||
|
<p class="fx-section-lede">เริ่มจากคุย requirement ฟรี → ส่งมอบตามที่ตกลง</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="fx-process-grid fx-stagger">
|
||||||
|
{steps.map((step) => (
|
||||||
|
<div class="fx-process-step" data-coord={step.coord}>
|
||||||
|
<div class="fx-process-num">{step.num}</div>
|
||||||
|
<div class="fx-process-title">{step.title}</div>
|
||||||
|
<div class="fx-process-text">
|
||||||
|
{step.cmd} <em>{step.cmdArg}</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
79
src/components/Services.astro
Normal file
79
src/components/Services.astro
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* MOREMINIMORE - SERVICES (from v6-services)
|
||||||
|
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 962-1014
|
||||||
|
*
|
||||||
|
* 4 cards in 2x2 grid. Data bound to src/content/services/*-new.mdx (4 entries).
|
||||||
|
* Each card: number (01-04) + title (with <em> accent) + desc + 3 bullets.
|
||||||
|
*
|
||||||
|
* Features extracted heuristically from short_desc + objective.
|
||||||
|
* For richer features, services collection could be extended with `features: string[]`.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - id?: string (default: 'services')
|
||||||
|
* - limit?: number (default: 4)
|
||||||
|
* - showHeader?: boolean (default: true — renders section title)
|
||||||
|
*/
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
limit?: number;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id = 'services', limit = 4, showHeader = true } = Astro.props;
|
||||||
|
|
||||||
|
// Filter to *-new.mdx files (4 main services) — exclude legacy files
|
||||||
|
const allServices = await getCollection('services');
|
||||||
|
const services = allServices
|
||||||
|
.filter((s) => s.id.endsWith('-new'))
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
// 3 hardcoded feature bullets per service — matches v7-5's 4 service cards
|
||||||
|
const featuresByTitle: Record<string, string[]> = {
|
||||||
|
'AI Consult': ['วิเคราะห์ workflow', 'เก็บความรู้พนักงาน', 'AI Chatbot ในองค์กร'],
|
||||||
|
'Online Marketing Consult': ['SEO + GEO + Ads', 'AI + n8n Automation', 'ตรงกลุ่มเป้าหมาย'],
|
||||||
|
'Automation Consult': ['n8n + LLM + Custom', 'เชื่อมระบบเดิม', 'Workflow อัตโนมัติ'],
|
||||||
|
'Website Development': ['Astro / WordPress', 'SEO + GEO', 'Server + SSL ฟรีปีแรก'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const coordLetters = ['A', 'B', 'A', 'B'];
|
||||||
|
const coordNums = ['02', '02', '03', '03'];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={id} class="fx-services fx-reveal">
|
||||||
|
{showHeader && (
|
||||||
|
<div class="fx-section-header">
|
||||||
|
<span class="fx-section-eyebrow">// services</span>
|
||||||
|
<h2 class="fx-section-title">เราทำอะไรได้บ้าง</h2>
|
||||||
|
<p class="fx-section-lede">เริ่มจากอันที่ปวดที่สุด ค่อยขยายไปอันอื่น</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="fx-services-grid fx-stagger">
|
||||||
|
{services.map((service, i) => {
|
||||||
|
const title = service.data.title;
|
||||||
|
const desc = service.data.short_desc ?? service.data.subtitle;
|
||||||
|
const features = featuresByTitle[title] ?? [];
|
||||||
|
// Highlight the first word + last word in title (v7-5 style)
|
||||||
|
const titleWords = title.split(' ');
|
||||||
|
const titleMid = titleWords.length > 1
|
||||||
|
? titleWords.map((w, idx) => idx === titleWords.length - 1 ? `<em>${w}</em>` : w).join(' ')
|
||||||
|
: title;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/services/${service.id.replace(/-new$/, '')}`}
|
||||||
|
class="fx-service-card"
|
||||||
|
data-coord={`${coordNums[i]}.${coordLetters[i]}`}
|
||||||
|
>
|
||||||
|
<span class="fx-service-num">{String(i + 1).padStart(2, '0')}</span>
|
||||||
|
<h3 class="fx-service-title" set:html={titleMid} />
|
||||||
|
<p class="fx-service-desc">{desc}</p>
|
||||||
|
<ul class="fx-service-list">
|
||||||
|
{features.map((f) => <li>{f}</li>)}
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -96,6 +96,23 @@ const blog = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PRICING — webdev tiers only (per plan 2026-06-13 round 2 #1)
|
||||||
|
// 2 entries: astro (฿5,000 featured) + wordpress (฿30,000)
|
||||||
|
// =============================================================================
|
||||||
|
const pricing = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/pricing' }),
|
||||||
|
schema: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
tier: z.string(),
|
||||||
|
amount: z.string(),
|
||||||
|
period: z.string(),
|
||||||
|
is_featured: z.boolean().optional(),
|
||||||
|
order: z.number().optional(),
|
||||||
|
features: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
services,
|
services,
|
||||||
portfolio,
|
portfolio,
|
||||||
@@ -103,4 +120,5 @@ export const collections = {
|
|||||||
settings,
|
settings,
|
||||||
blog,
|
blog,
|
||||||
pages,
|
pages,
|
||||||
|
pricing,
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/content/pricing/astro.md
Normal file
18
src/content/pricing/astro.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: "Astro"
|
||||||
|
tier: "แนะนำ"
|
||||||
|
amount: "฿5,000"
|
||||||
|
period: "starter"
|
||||||
|
is_featured: true
|
||||||
|
order: 1
|
||||||
|
features:
|
||||||
|
- "Responsive design (มือถือ + เดสก์ท็อป)"
|
||||||
|
- "SEO + GEO (ติด Google + ChatGPT/Perplexity)"
|
||||||
|
- "AI ช่วยสร้างเนื้อหา"
|
||||||
|
- "Server + SSL ฟรีปีแรก"
|
||||||
|
- "แก้ไขเนื้อหาฟรีตลอดอายุ Server"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Astro Website (แนะนำ)
|
||||||
|
|
||||||
|
เว็บไซต์ที่ขายได้ ไม่ใช่เว็บที่สวย — เริ่มต้น 5,000 บาท พร้อม SEO + AI ช่วยเขียนเนื้อหา
|
||||||
18
src/content/pricing/wordpress.md
Normal file
18
src/content/pricing/wordpress.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: "WordPress"
|
||||||
|
tier: "ขั้นสูง"
|
||||||
|
amount: "฿30,000"
|
||||||
|
period: "advanced"
|
||||||
|
is_featured: false
|
||||||
|
order: 2
|
||||||
|
features:
|
||||||
|
- "ไม่จำกัดจำนวนหน้า"
|
||||||
|
- "ตะกร้า + ชำระเงิน (WooCommerce)"
|
||||||
|
- "Plugin + Theme ตามต้องการ"
|
||||||
|
- "หลังบ้านใช้ง่าย ไม่ต้องเขียนโค้ด"
|
||||||
|
- "Server + SSL ฟรีปีแรก"
|
||||||
|
---
|
||||||
|
|
||||||
|
# WordPress Website
|
||||||
|
|
||||||
|
เว็บไซต์ E-commerce หรือเว็บที่ต้องการ Plugin เยอะ — เริ่มต้น 30,000 บาท
|
||||||
Reference in New Issue
Block a user