- 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
291 lines
9.1 KiB
JavaScript
291 lines
9.1 KiB
JavaScript
/**
|
|
* 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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function jsonResponse(data) {
|
|
return ContentService
|
|
.createTextOutput(JSON.stringify(data))
|
|
.setMimeType(ContentService.MimeType.JSON);
|
|
}
|