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)
|
||||
* Yellow/white/black editorial — no dark bg
|
||||
* MOREMINIMORE - HERO (from v6-hero · terminal+stats)
|
||||
* 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 {
|
||||
badge?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
showCTA?: boolean;
|
||||
ctaText?: string;
|
||||
ctaLink?: string;
|
||||
eyebrow?: string;
|
||||
title?: string;
|
||||
lede?: string;
|
||||
ctaPrimary?: CTA;
|
||||
ctaSecondary?: CTA;
|
||||
stats?: Stat[];
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
badge = 'Moreminimore',
|
||||
title = 'เราจะช่วยคุณเพิ่มกำไร',
|
||||
subtitle = 'เราช่วยวางระบบงาน และใช้สถิติวางกลยุทธ์ทางการตลาด',
|
||||
showCTA = true,
|
||||
ctaText = 'เริ่มปรึกษาฟรี',
|
||||
ctaLink = '/contact',
|
||||
pains = [
|
||||
{ surface: 'yellow', text: 'ยิ่งขาย กำไรยิ่งลด?' },
|
||||
{ surface: 'purple-soft', text: 'มีเว็บไซต์ เหมือนไม่มี?' },
|
||||
{ surface: 'mint', text: 'พนักงานทำงานได้น้อยกว่าที่ต้องการ?' },
|
||||
{ surface: 'teal', text: 'เอา AI มาให้ใช้ แต่งานไม่ได้มากขึ้นตามที่คิด?' },
|
||||
eyebrow = 'MOREMINIMORE / EST. 2024',
|
||||
title = 'เราจะช่วยคุณเพิ่มกำไร ไม่ใช่แค่เพิ่มงบ',
|
||||
lede = 'วางระบบ AI + Online Marketing + Automation ให้ธุรกิจคุณทำงานเร็วขึ้น ใช้งบคุ้ม และเห็นผลจริง',
|
||||
ctaPrimary = { text: 'ปรึกษาฟรี →', href: '/contact' },
|
||||
ctaSecondary = { text: 'ดูผลงานจริง', href: '/portfolio' },
|
||||
stats = [
|
||||
{ label: 'impression', value: '+373%' },
|
||||
{ label: 'click', value: '+114%', coral: true },
|
||||
{ label: 'ad_spend', value: '−28%' },
|
||||
{ label: 'period', value: '30d' },
|
||||
],
|
||||
id = 'hero',
|
||||
} = Astro.props;
|
||||
|
||||
// Split title into words for kinetic animation
|
||||
const titleWords = title.split(' ');
|
||||
// Split title into 2 lines at first comma/space — for fx-hero-title multi-line layout
|
||||
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">
|
||||
<!-- Animated Background Pattern (LIGHT THEME) -->
|
||||
<div class="hero-bg">
|
||||
<div class="bg-grid"></div>
|
||||
<div class="bg-gradient"></div>
|
||||
</div>
|
||||
<div id={id} class="fx-hero fx-reveal">
|
||||
<span class="fx-sparkle" style="top:8%;left:6%">✦</span>
|
||||
<span class="fx-sparkle s2" style="top:12%;right:8%">◆</span>
|
||||
<span class="fx-sparkle s3" style="bottom:18%;left:10%">✦</span>
|
||||
<span class="fx-sparkle s4" style="top:30%;right:4%">·</span>
|
||||
<span class="fx-sparkle s5" style="bottom:8%;right:12%">◆</span>
|
||||
|
||||
<!-- Floating Geometric Elements (yellow, subtle) -->
|
||||
<div class="hero-geometric">
|
||||
<div class="geo-circle geo-1"></div>
|
||||
<div class="geo-circle geo-2"></div>
|
||||
<div class="geo-circle geo-3"></div>
|
||||
<div class="geo-ring ring-1"></div>
|
||||
<div class="geo-ring ring-2"></div>
|
||||
<div class="geo-line line-1"></div>
|
||||
<div class="geo-line line-2"></div>
|
||||
</div>
|
||||
|
||||
<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 class="fx-hero-grid">
|
||||
<div class="fx-hero-content">
|
||||
<span class="fx-hero-eyebrow">{eyebrow}</span>
|
||||
<h1 class="fx-hero-title">
|
||||
<span>{titleLine1}</span>
|
||||
<span>{titleLine2}</span>
|
||||
</h1>
|
||||
<p class="fx-hero-lede" set:html={lede} />
|
||||
<div class="fx-hero-cta">
|
||||
<a href={ctaPrimary.href} class="fx-btn coral">{ctaPrimary.text}</a>
|
||||
<a href={ctaSecondary.href} class="fx-btn ghost">{ctaSecondary.text}</a>
|
||||
</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="hero-pain-stack" data-animate="fade-in-up">
|
||||
{pains.map((p, i) => (
|
||||
<div class={`pain-card pain-${p.surface}`} style={`--pain-delay: ${0.6 + i * 0.15}s`}>
|
||||
<div class="pain-eyebrow">คุณกำลังเจอปัญหา</div>
|
||||
<div class="pain-text">{p.text}</div>
|
||||
<div class="fx-hero-side">
|
||||
{stats.map((stat, i) => (
|
||||
<div class="fx-stat" data-coord={`00.${i + 1}`}>
|
||||
<div class="fx-stat-label">{stat.label}</div>
|
||||
<div class:list={['fx-stat-value', stat.coral && 'coral']}>
|
||||
<Fragment set:html={stat.value} />
|
||||
</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>
|
||||
</div>
|
||||
|
||||
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 = {
|
||||
services,
|
||||
portfolio,
|
||||
@@ -103,4 +120,5 @@ export const collections = {
|
||||
settings,
|
||||
blog,
|
||||
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