feat(contact): real working form with Apps Script backend (3 sub-tasks)
4.9a: Contact.astro (v6-contact enhanced)
- 2 variants: 'prompt' (1 input + ENTER, for home) + 'full' (5 fields, for /contact)
- Smart-parses prompt input (phone vs name detection)
- Toast feedback on submit (success/error, 5s auto-hide)
- Dev-mode aware (no endpoint = console.log + 'dev mode' toast)
4.9b: apps-script/contact-form/ (USER DEPLOYS)
- Code.gs: doPost() handler → 3 actions:
1. Append to Google Sheet (SHEET_ID from Script Properties)
2. Send email via MailApp to RECIPIENT_EMAIL
3. Send LINE Notify via UrlFetchApp
- Returns {ok, id} or {ok:false, error} as JSON
- Includes testDoPost() for testing in Apps Script editor
- README.md: 6-step deploy guide (Script Properties + Sheet + LINE token)
with security notes + cost breakdown
4.9c: src/lib/contact-submit.ts
- Reads PUBLIC_CONTACT_ENDPOINT from .env
- Empty/missing = dev mode (console.log + 300ms mock latency)
- Set = POST JSON to endpoint
- Exports submitContact() + isDevMode() for Contact.astro to use
Refs: .hermes/plans/2026-06-13_124000-moreminimore-v7-5-migration.md Task 4.9a-c
This commit is contained in:
166
apps-script/contact-form/Code.gs
Normal file
166
apps-script/contact-form/Code.gs
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* MOREMINIMORE - Contact Form Backend (Google Apps Script)
|
||||
* Per plan 2026-06-13 round 2 #4:
|
||||
* Form submit → Apps Script doPost() → Google Sheet (log)
|
||||
* → Email to contact@moreminimore.com
|
||||
* → LINE Notify
|
||||
*
|
||||
* USER DEPLOYS THIS THEMSELVES (see README.md in same folder).
|
||||
* 1. Create new Apps Script project at https://script.google.com
|
||||
* 2. Paste this entire file into Code.gs
|
||||
* 3. Set Script Properties (Project Settings → Script Properties):
|
||||
* SHEET_ID — Google Sheet ID (from Sheet URL)
|
||||
* LINE_NOTIFY_TOKEN — from https://notify-bot.line.me
|
||||
* RECIPIENT_EMAIL — contact@moreminimore.com (or any email)
|
||||
* 4. Create Google Sheet with these headers in row 1:
|
||||
* timestamp | name | phone | email | service | message | variant | userAgent
|
||||
* 5. Deploy → New deployment → Type: Web app
|
||||
* Execute as: Me
|
||||
* Who has access: Anyone
|
||||
* Copy the deployment URL.
|
||||
* 6. Add to moreminimore-astroreal/.env:
|
||||
* PUBLIC_CONTACT_ENDPOINT=<paste deployment URL>
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* CONFIG — read from Script Properties (set in Project Settings) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function getConfig() {
|
||||
const props = PropertiesService.getScriptProperties();
|
||||
return {
|
||||
SHEET_ID: props.getProperty('SHEET_ID'),
|
||||
LINE_NOTIFY_TOKEN: props.getProperty('LINE_NOTIFY_TOKEN'),
|
||||
RECIPIENT_EMAIL: props.getProperty('RECIPIENT_EMAIL') || 'contact@moreminimore.com',
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* HANDLER — POST /exec (or /dev) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Handle form submission. Expects JSON body:
|
||||
* {
|
||||
* name, phone, email, service, message, variant, userAgent, submittedAt
|
||||
* }
|
||||
*/
|
||||
function doPost(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.postData.contents);
|
||||
const config = getConfig();
|
||||
|
||||
// 1. Log to Google Sheet
|
||||
let rowId = null;
|
||||
if (config.SHEET_ID) {
|
||||
const sheet = SpreadsheetApp.openById(config.SHEET_ID).getActiveSheet();
|
||||
const row = sheet.appendRow([
|
||||
new Date(), // timestamp
|
||||
data.name || '',
|
||||
data.phone || '',
|
||||
data.email || '',
|
||||
data.service || '',
|
||||
data.message || '',
|
||||
data.variant || 'full',
|
||||
data.userAgent || '',
|
||||
]);
|
||||
rowId = row.getRange().getRow();
|
||||
}
|
||||
|
||||
// 2. Send email
|
||||
const subject = `[moreminimore contact] ${data.service || 'general'} — ${data.name || data.phone || 'unknown'}`;
|
||||
const body = formatEmailBody(data);
|
||||
MailApp.sendEmail({
|
||||
to: config.RECIPIENT_EMAIL,
|
||||
subject: subject,
|
||||
body: body,
|
||||
replyTo: data.email || undefined,
|
||||
});
|
||||
|
||||
// 3. Send LINE Notify
|
||||
if (config.LINE_NOTIFY_TOKEN) {
|
||||
const lineMessage = formatLineMessage(data);
|
||||
UrlFetchApp.fetch('https://notify-api.line.me/api/notify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + config.LINE_NOTIFY_TOKEN,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
payload: { message: lineMessage },
|
||||
muteHttpExceptions: true,
|
||||
});
|
||||
}
|
||||
|
||||
return ContentService
|
||||
.createTextOutput(JSON.stringify({ ok: true, id: rowId }))
|
||||
.setMimeType(ContentService.MimeType.JSON);
|
||||
|
||||
} catch (err) {
|
||||
return ContentService
|
||||
.createTextOutput(JSON.stringify({ ok: false, error: err.toString() }))
|
||||
.setMimeType(ContentService.MimeType.JSON);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* FORMATTERS */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function formatEmailBody(data) {
|
||||
return [
|
||||
'New contact form submission',
|
||||
'',
|
||||
'---',
|
||||
'Name: ' + (data.name || '-'),
|
||||
'Phone: ' + (data.phone || '-'),
|
||||
'Email: ' + (data.email || '-'),
|
||||
'Service: ' + (data.service || '-'),
|
||||
'Variant: ' + (data.variant || 'full'),
|
||||
'---',
|
||||
'',
|
||||
'Message:',
|
||||
data.message || '(empty)',
|
||||
'',
|
||||
'---',
|
||||
'Submitted: ' + (data.submittedAt || new Date().toISOString()),
|
||||
'UserAgent: ' + (data.userAgent || 'unknown'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function formatLineMessage(data) {
|
||||
const lines = [
|
||||
'🔔 moreminimore contact',
|
||||
'',
|
||||
'👤 ' + (data.name || data.phone || 'unknown'),
|
||||
'📞 ' + (data.phone || '-'),
|
||||
'✉ ' + (data.email || '-'),
|
||||
'🎯 ' + (data.service || 'general'),
|
||||
];
|
||||
if (data.message) {
|
||||
lines.push('');
|
||||
lines.push(data.message.length > 200 ? data.message.slice(0, 200) + '...' : data.message);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TEST (optional — call testDoPost() from Apps Script editor) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function testDoPost() {
|
||||
const mockEvent = {
|
||||
postData: {
|
||||
contents: JSON.stringify({
|
||||
name: 'Test User',
|
||||
phone: '080-123-4567',
|
||||
email: 'test@example.com',
|
||||
service: 'webdev',
|
||||
message: 'This is a test message',
|
||||
variant: 'full',
|
||||
userAgent: 'test',
|
||||
submittedAt: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
Logger.log(doPost(mockEvent).getContent());
|
||||
}
|
||||
Reference in New Issue
Block a user