feat: liquid glass UI, blob background, redesign home/portfolio/about pages

- Liquid glass effect on navbar/cards with backdrop-filter invert
- Animated blob gradient background (SVG-based)
- Portfolio section: scene-dark invert, show 5 items on home
- How Work section: step flow with numbers + connecting lines
- Hero: Decision snapshot replacing problem selector
- About page: inverted background with contrast fixes
- Fix parallax JS bundling via Astro
- Fix navbar fixed positioning after liquid glass CSS
- Submenu hover fix
- Clean up removed legacy files/assets
This commit is contained in:
Kunthawat Greethong
2026-06-23 11:40:37 +07:00
parent e279119f97
commit f827afb33f
188 changed files with 4577 additions and 15483 deletions

153
google-apps-script/SETUP.md Normal file
View File

@@ -0,0 +1,153 @@
# Google Apps Script Lead Form Setup
ใช้ไฟล์นี้เพื่อตั้งระบบรับ lead จากฟอร์ม MoreminiMore แบบง่ายก่อน โดยให้ Google Apps Script บันทึกข้อมูลลง Google Sheet และส่งอีเมลแจ้งเตือนเข้า Gmail/Google Workspace
อ้างอิง official docs:
- Apps Script Web App deployment: https://developers.google.com/apps-script/guides/web
- Apps Script MailApp: https://developers.google.com/apps-script/reference/mail/mail-app
## สิ่งที่ต้องมี
- Google account หรือ Google Workspace account ที่จะใช้รับ lead
- แนะนำให้ใช้บัญชีของโดเมนบริษัท เช่น `contact@moreminimore.com`
- Google Sheet ใหม่ 1 ไฟล์ สำหรับเก็บ lead
## ขั้นตอนติดตั้ง
### 1. สร้าง Google Sheet
1. เข้า Google Drive
2. สร้าง Google Sheet ใหม่
3. ตั้งชื่อเช่น `MoreminiMore Website Leads`
4. ไม่ต้องสร้าง column เอง script จะสร้าง header ให้ตอนมี lead แรก
### 2. เปิด Apps Script จาก Sheet
1. ใน Google Sheet ไปที่ `Extensions`
2. เลือก `Apps Script`
3. จะเปิดหน้า Apps Script editor
4. ลบโค้ดเดิมใน `Code.gs`
5. Copy โค้ดทั้งหมดจาก `google-apps-script/lead-form.gs`
6. Paste ลงใน `Code.gs`
### 3. แก้อีเมลผู้รับ
ในไฟล์ `Code.gs` หา:
```js
const CONFIG = {
RECIPIENT_EMAIL: 'contact@moreminimore.com',
```
เปลี่ยน `RECIPIENT_EMAIL` เป็นอีเมลที่จะรับแจ้งเตือน lead
ถ้าใช้ `contact@moreminimore.com` อยู่แล้ว ไม่ต้องแก้
### 4. Save project
1. กด Save
2. ตั้งชื่อ project เช่น `MoreminiMore Lead Form`
### 5. Deploy เป็น Web App
ตาม official docs ของ Google ให้ deploy web app โดย:
1. มุมขวาบน กด `Deploy`
2. เลือก `New deployment`
3. ตรง `Select type` กด icon ตั้งค่า แล้วเลือก `Web app`
4. ตั้งค่า:
- Description: `MoreminiMore lead form endpoint`
- Execute as: `Me`
- Who has access: `Anyone`
5. กด `Deploy`
6. Google จะขอ authorize permissions
7. เลือก account ของคุณ
8. อนุญาตสิทธิ์ที่เกี่ยวกับ Google Sheets และส่งอีเมล
9. Copy `Web app URL` เก็บไว้
URL จะหน้าตาประมาณ:
```text
https://script.google.com/macros/s/xxxxxxxxxxxxxxxx/exec
```
### 6. ทดสอบ endpoint
เปิด URL ที่ copy มาใน browser ถ้าระบบทำงาน จะเห็น JSON ประมาณ:
```json
{"ok":true,"service":"MoreminiMore lead form"}
```
### 7. เอา URL ไปใส่ในเว็บ
ตอน implement หน้าเว็บ ให้ตั้งค่า URL นี้เป็น endpoint ของฟอร์ม
ข้อควรระวัง: Apps Script web app มักไม่เหมาะกับ fetch ที่ต้องอ่าน JSON response ข้ามโดเมนแบบเต็ม ๆ เพราะอาจติด CORS ได้ วิธีที่เหมาะกับ static site คือส่งข้อมูลแบบ simple POST หรือ `fetch(..., { mode: "no-cors" })` แล้วให้หน้าเว็บแสดง success state หลัง request ถูกส่งออกไป
ตัวอย่าง payload ที่เว็บควรส่ง:
```json
{
"name": "คุณเอ",
"phone": "0800000000",
"email": "owner@example.com",
"problems": ["ads_not_worth_it", "wrong_leads"],
"message": "ยิงแอดอยู่ แต่ยอดขายไม่คุ้ม อยากรู้ว่าควรแก้อะไรก่อน",
"pageUrl": "https://moreminimore.com/",
"userAgent": "browser user agent"
}
```
## Problem Keys ที่ script รองรับ
| Key | ข้อความ |
| --- | --- |
| `website_no_leads` | เว็บมีอยู่แล้ว แต่ไม่ค่อยมีลูกค้าทัก |
| `ads_not_worth_it` | ยิงแอดอยู่ แต่ยอดขายไม่คุ้ม |
| `wrong_leads` | มีคนทักมา แต่ไม่ใช่ลูกค้าที่ใช่ |
| `slow_or_error_work` | ทีมงานทำงานเดิม ๆ แต่ทำงานช้า หรือผิดพลาดบ่อย |
| `ai_not_sure` | อยากใช้ AI แต่ไม่รู้เริ่มตรงไหน |
| `not_sure` | ยังไม่แน่ใจว่าควรแก้อะไรก่อน |
## วิธีลดโอกาสเมลเข้าขยะ
Apps Script จะส่งเมลจากบัญชี Google ที่ deploy script ดังนั้นควร:
- ใช้บัญชี Google Workspace ของบริษัท ถ้ามี
- ตั้งค่า SPF/DKIM/DMARC ของโดเมนให้ถูกต้อง
- ใช้ subject ปกติ ไม่ spammy เช่น `มีโจทย์ธุรกิจใหม่จากเว็บไซต์ MoreminiMore`
- อย่าใช้ email ลูกค้าเป็น `From`
- ให้ script ใช้ email ลูกค้าเป็น `Reply-To` แทน
- เนื้อหาอีเมลควรเป็นข้อความสะอาด ไม่ใส่คำขายหรือ link เยอะ
## เวลาแก้ script หลัง deploy
ถ้าแก้โค้ดหลังจาก deploy แล้ว:
1. กด `Deploy`
2. เลือก `Manage deployments`
3. เลือก deployment เดิม
4. กด edit
5. เลือก version ใหม่ หรือ new version
6. กด deploy/update
ถ้าสร้าง deployment ใหม่ URL อาจเปลี่ยน ต้องเอา URL ใหม่ไปใส่ในเว็บอีกครั้ง
## Debug เบื้องต้น
ถ้า submit แล้วไม่เข้า Sheet:
1. เปิด Apps Script
2. ดูเมนู `Executions`
3. เปิด execution ล่าสุดเพื่อดู error
4. ตรวจว่า deploy เป็น `Who has access: Anyone`
5. ตรวจว่าใช้ URL ที่ลงท้าย `/exec` ไม่ใช่ `/dev`
ถ้าเข้า Sheet แต่ไม่ส่งเมล:
1. ตรวจสิทธิ์ MailApp ตอน authorize
2. ตรวจ `RECIPIENT_EMAIL`
3. ตรวจ quota ของ Google account
4. ดู error ใน `Executions`

View File

@@ -0,0 +1,290 @@
/**
* MoreminiMore lead form endpoint.
*
* Recommended setup:
* 1. Create a Google Sheet for leads.
* 2. Open Extensions > Apps Script.
* 3. Paste this entire file into Code.gs.
* 4. Update CONFIG.RECIPIENT_EMAIL.
* 5. Deploy as Web app.
*/
const CONFIG = {
RECIPIENT_EMAIL: 'contact@moreminimore.com',
SHEET_NAME: 'Leads',
TIMEZONE: 'Asia/Bangkok',
EMAIL_SUBJECT: 'มีโจทย์ธุรกิจใหม่จากเว็บไซต์ MoreminiMore',
};
const PROBLEM_LABELS = {
website_no_leads: 'เว็บมีอยู่แล้ว แต่ไม่ค่อยมีลูกค้าทัก',
ads_not_worth_it: 'ยิงแอดอยู่ แต่ยอดขายไม่คุ้ม',
wrong_leads: 'มีคนทักมา แต่ไม่ใช่ลูกค้าที่ใช่',
slow_or_error_work: 'ทีมงานทำงานเดิม ๆ แต่ทำงานช้า หรือผิดพลาดบ่อย',
ai_not_sure: 'อยากใช้ AI แต่ไม่รู้เริ่มตรงไหน',
not_sure: 'ยังไม่แน่ใจว่าควรแก้อะไรก่อน',
};
function doGet() {
return jsonResponse({
ok: true,
service: 'MoreminiMore lead form',
});
}
function doPost(e) {
try {
const data = parseRequest(e);
if (isSpam(data)) {
return jsonResponse({ ok: true, skipped: true });
}
const lead = normalizeLead(data);
const validation = validateLead(lead);
if (!validation.ok) {
return jsonResponse({
ok: false,
error: validation.error,
});
}
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
appendLead(lead);
} finally {
lock.releaseLock();
}
sendLeadEmail(lead);
return jsonResponse({
ok: true,
message: 'Lead received',
diagnosis: buildLightDiagnosis(lead.problems),
});
} catch (error) {
console.error(error);
return jsonResponse({
ok: false,
error: 'ระบบรับข้อมูลมีปัญหา กรุณาลองใหม่อีกครั้ง',
});
}
}
function parseRequest(e) {
if (!e) return {};
const contentType = String(e.postData && e.postData.type || '').toLowerCase();
const raw = e.postData && e.postData.contents;
if (raw && contentType.indexOf('application/json') !== -1) {
return JSON.parse(raw);
}
if (raw && contentType.indexOf('text/plain') !== -1) {
try {
return JSON.parse(raw);
} catch (error) {
return e.parameter || {};
}
}
return e.parameter || {};
}
function normalizeLead(data) {
const problems = normalizeProblems(data.problems || data.problem || data.problemKeys);
return {
createdAt: Utilities.formatDate(new Date(), CONFIG.TIMEZONE, 'yyyy-MM-dd HH:mm:ss'),
name: cleanText(data.name),
phone: cleanText(data.phone),
email: cleanText(data.email).toLowerCase(),
message: cleanText(data.message || data.details || data.note),
problems,
pageUrl: cleanText(data.pageUrl || data.url),
userAgent: cleanText(data.userAgent),
};
}
function normalizeProblems(value) {
if (!value) return [];
if (Array.isArray(value)) {
return value.map(String).map(cleanText).filter(Boolean);
}
return String(value)
.split(',')
.map(cleanText)
.filter(Boolean);
}
function validateLead(lead) {
if (!lead.name) {
return { ok: false, error: 'กรุณาใส่ชื่อ' };
}
if (!lead.phone && !lead.email) {
return { ok: false, error: 'ใส่เบอร์โทรหรืออีเมลอย่างใดอย่างหนึ่งก็ได้' };
}
if (lead.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(lead.email)) {
return { ok: false, error: 'รูปแบบอีเมลไม่ถูกต้อง' };
}
if (lead.message.length > 2000) {
return { ok: false, error: 'รายละเอียดโจทย์ยาวเกินไป' };
}
return { ok: true };
}
function appendLead(lead) {
const sheet = getLeadSheet();
sheet.appendRow([
lead.createdAt,
lead.name,
lead.phone,
lead.email,
problemLabels(lead.problems).join(', '),
lead.message,
buildLightDiagnosis(lead.problems),
lead.pageUrl,
lead.userAgent,
]);
}
function getLeadSheet() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
sheet = spreadsheet.insertSheet(CONFIG.SHEET_NAME);
}
if (sheet.getLastRow() === 0) {
sheet.appendRow([
'วันที่ส่ง',
'ชื่อ',
'เบอร์โทร',
'อีเมล',
'ปัญหาที่เลือก',
'รายละเอียด',
'แนวทางเริ่มต้น',
'Page URL',
'User Agent',
]);
sheet.setFrozenRows(1);
}
return sheet;
}
function sendLeadEmail(lead) {
const labels = problemLabels(lead.problems);
const diagnosis = buildLightDiagnosis(lead.problems);
const plainBody = [
'มีโจทย์ธุรกิจใหม่จากเว็บไซต์ MoreminiMore',
'',
`ชื่อ: ${lead.name}`,
`เบอร์โทร: ${lead.phone || '-'}`,
`อีเมล: ${lead.email || '-'}`,
`ปัญหาที่เลือก: ${labels.length ? labels.join(', ') : '-'}`,
'',
'รายละเอียด:',
lead.message || '-',
'',
`แนวทางเริ่มต้น: ${diagnosis}`,
'',
`Page URL: ${lead.pageUrl || '-'}`,
`เวลาที่ส่ง: ${lead.createdAt}`,
].join('\n');
const htmlBody = `
<div style="font-family:Arial,sans-serif;line-height:1.6;color:#17120a">
<h2 style="margin:0 0 12px">มีโจทย์ธุรกิจใหม่จากเว็บไซต์ MoreminiMore</h2>
<p><strong>ชื่อ:</strong> ${escapeHtml(lead.name)}</p>
<p><strong>เบอร์โทร:</strong> ${escapeHtml(lead.phone || '-')}</p>
<p><strong>อีเมล:</strong> ${escapeHtml(lead.email || '-')}</p>
<p><strong>ปัญหาที่เลือก:</strong> ${escapeHtml(labels.length ? labels.join(', ') : '-')}</p>
<p><strong>รายละเอียด:</strong><br>${escapeHtml(lead.message || '-').replace(/\n/g, '<br>')}</p>
<p><strong>แนวทางเริ่มต้น:</strong> ${escapeHtml(diagnosis)}</p>
<hr>
<p style="color:#666;font-size:13px">
Page URL: ${escapeHtml(lead.pageUrl || '-')}<br>
เวลาที่ส่ง: ${escapeHtml(lead.createdAt)}
</p>
</div>
`;
const options = {
name: 'MoreminiMore Website',
htmlBody,
};
if (lead.email) {
options.replyTo = lead.email;
}
MailApp.sendEmail(CONFIG.RECIPIENT_EMAIL, CONFIG.EMAIL_SUBJECT, plainBody, options);
}
function buildLightDiagnosis(problemKeys) {
const keys = problemKeys || [];
if (keys.indexOf('ads_not_worth_it') !== -1 || keys.indexOf('wrong_leads') !== -1) {
return 'น่าจะเริ่มจากการดูข้อมูลแอด กลุ่มเป้าหมาย และคุณภาพลูกค้าที่ทักเข้ามาก่อน';
}
if (keys.indexOf('website_no_leads') !== -1) {
return 'น่าจะเริ่มจากการดูเว็บ เส้นทางลูกค้า และจุดที่ควรชวนให้ติดต่อก่อน';
}
if (keys.indexOf('slow_or_error_work') !== -1) {
return 'น่าจะเริ่มจากการดูขั้นตอนทำงานซ้ำ จุดที่ช้า และจุดที่ผิดพลาดบ่อยก่อน';
}
if (keys.indexOf('ai_not_sure') !== -1) {
return 'น่าจะเริ่มจากการดูงานจริงของทีมก่อน แล้วค่อยเลือกจุดที่ AI ช่วยได้อย่างเหมาะสม';
}
return 'เราจะเริ่มจากการทำความเข้าใจธุรกิจและข้อมูลที่มีอยู่ก่อน แล้วค่อยแนะนำทางที่คุ้มที่สุด';
}
function problemLabels(problemKeys) {
return (problemKeys || []).map(function (key) {
return PROBLEM_LABELS[key] || key;
});
}
function isSpam(data) {
return Boolean(data.website || data.company_url || data.url2);
}
function cleanText(value) {
return String(value || '')
.replace(/\r/g, '')
.trim()
.slice(0, 2000);
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function jsonResponse(data) {
return ContentService
.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}