From 8c2bf3d303b8a5207733059c7b1ab2f0da30dc3b Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Sat, 13 Jun 2026 17:55:59 +0700 Subject: [PATCH] feat(contact): real working form with Apps Script backend (3 sub-tasks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps-script/contact-form/Code.gs | 166 ++++++++++++++++++++++++++++ apps-script/contact-form/README.md | 140 ++++++++++++++++++++++++ src/components/Contact.astro | 169 +++++++++++++++++++++++++++++ src/lib/contact-submit.ts | 75 +++++++++++++ 4 files changed, 550 insertions(+) create mode 100644 apps-script/contact-form/Code.gs create mode 100644 apps-script/contact-form/README.md create mode 100644 src/components/Contact.astro create mode 100644 src/lib/contact-submit.ts diff --git a/apps-script/contact-form/Code.gs b/apps-script/contact-form/Code.gs new file mode 100644 index 0000000..87c1ba6 --- /dev/null +++ b/apps-script/contact-form/Code.gs @@ -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= + */ + +/* ------------------------------------------------------------------ */ +/* 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()); +} diff --git a/apps-script/contact-form/README.md b/apps-script/contact-form/README.md new file mode 100644 index 0000000..638d0ab --- /dev/null +++ b/apps-script/contact-form/README.md @@ -0,0 +1,140 @@ +# Contact Form Backend — Google Apps Script + +This is the backend for the contact form on the MoreminiMore website. + +When someone submits the form, this Apps Script: +1. **Logs** the submission to a Google Sheet (audit trail) +2. **Emails** the submission to `contact@moreminimore.com` (your Google Workspace) +3. **Sends** a LINE Notify to your phone + +The user (you) deploys this script. Hermes Agent cannot deploy it for you — it requires access to your Google account. + +--- + +## Prerequisites + +- A Google account (use `contact@moreminimore.com` or any Google Workspace account) +- A LINE Notify token (get one at — log in → My page → Generate token) + +--- + +## Setup (5 minutes) + +### Step 1: Create the Apps Script project + +1. Go to +2. Click **New project** (or "โปรเจ็กต์ใหม่" in Thai) +3. Rename the project (top-left) to `moreminimore-contact-form` +4. Delete the default `function myFunction() {}` code +5. Open `Code.gs` from this folder in your text editor +6. **Copy the entire contents** and **paste** into the Apps Script editor +7. Click 💾 (Save) or Ctrl+S + +### Step 2: Set Script Properties + +1. Click **Project Settings** (⚙️ gear icon on the left) +2. Scroll down to **Script Properties** +3. Click **Add script property** and add these 3: + +| Property | Value | Where to get it | +|---|---|---| +| `SHEET_ID` | (Google Sheet ID) | See Step 3 below | +| `LINE_NOTIFY_TOKEN` | (LINE token) | → My page → Generate token → Copy | +| `RECIPIENT_EMAIL` | `contact@moreminimore.com` | Just type your email | + +### Step 3: Create the Google Sheet + +1. Go to +2. Rename to `moreminimore-contact-log` (or whatever you like) +3. In row 1, add these headers (one per cell, A1 through H1): + ``` + timestamp | name | phone | email | service | message | variant | userAgent + ``` +4. **Get the Sheet ID** from the URL: + - URL looks like: `https://docs.google.com/spreadsheets/d/1aBcD...XyZ/edit` + - The `1aBcD...XyZ` part is the **SHEET_ID** — copy it +5. Paste it into Script Properties → `SHEET_ID` value + +### Step 4: Test the script (optional but recommended) + +1. In the Apps Script editor, select function `testDoPost` from the dropdown (next to the debug ▶ button) +2. Click **Run** (▶) +3. If prompted, authorize the script (review permissions → Allow) +4. Check: + - The Google Sheet has a new row at the bottom + - The recipient email got a new message + - Your LINE got a notification +5. Check **Execution log** (View → Logs) for any errors + +### Step 5: Deploy as Web App + +1. Click **Deploy** (top-right) → **New deployment** +2. Click the ⚙️ gear icon → select **Web app** +3. Configure: + - **Description**: `moreminimore contact form v1` + - **Execute as**: `Me (your-email@gmail.com)` + - **Who has access**: `Anyone` ← important, otherwise form can't reach it +4. Click **Deploy** +5. You may be asked to authorize again — click **Review permissions** → choose your account → **Allow** +6. **Copy the Web app URL** — it looks like: + ``` + https://script.google.com/macros/s/AKfycbz.../exec + ``` + +### Step 6: Wire the URL into the website + +1. Open `moreminimore-astroreal/.env` (create it if it doesn't exist) +2. Add this line (paste your URL): + ``` + PUBLIC_CONTACT_ENDPOINT=https://script.google.com/macros/s/AKfycbz.../exec + ``` +3. Save the file +4. Rebuild the site: `npm run build` +5. Deploy (or push to your host) + +--- + +## Verifying it works + +After deploy, visit the website, fill the form, hit submit. + +You should see: +- ✅ Toast: "✓ ส่งแล้ว เราจะติดต่อกลับภายใน 24 ชม." +- ✅ New row in the Google Sheet +- ✅ Email in your inbox +- ✅ LINE notification on your phone + +If something fails, check: +- **Apps Script Execution Log** (Executions tab on the left) — shows errors +- **Sheet ID** is correct (no extra spaces, full ID) +- **LINE token** is active (revoke + regenerate if needed) +- **"Who has access"** is set to `Anyone` on the deployment + +--- + +## Updating the script later + +When you change `Code.gs`: +1. Save in the editor +2. **Deploy** → **Manage deployments** → ✏️ (edit) → **Version: New version** → **Deploy** +3. The Web app URL stays the same — no need to update `.env` + +--- + +## Security notes + +- The web app URL is not secret — anyone who knows it can submit +- But it has no destructive power (only appends rows + sends email/notify to you) +- If you receive spam, you can revoke the LINE token + change the deployment URL +- Consider adding reCAPTCHA later if spam becomes a problem + +--- + +## Cost + +- **Google Apps Script**: free (1-hour quota, plenty for contact forms) +- **Google Sheets**: free up to 10M cells +- **MailApp**: free for ~100 emails/day per user +- **LINE Notify**: free, unlimited messages, but discontinued March 2025 in some regions — check status at + +If LINE Notify is shut down, swap the LINE call for a Telegram bot or Discord webhook (same `UrlFetchApp.fetch` pattern). diff --git a/src/components/Contact.astro b/src/components/Contact.astro new file mode 100644 index 0000000..f20bf42 --- /dev/null +++ b/src/components/Contact.astro @@ -0,0 +1,169 @@ +--- +/** + * MOREMINIMORE - CONTACT FORM (from v6-contact · $ prompt form, enhanced) + * Extracted from Desktop/moreminomore-mockup-v7-5.html lines 1453-1469 + * + * Per plan 2026-06-13 round 2 #4: REAL working form with backend + * (Apps Script → Google Sheet + email + LINE Notify). + * + * Two variants: + * - 'prompt': 1 input + ENTER button (for home page) — terminal-style + * - 'full': 5 fields (name, phone, email, service, message) (for /contact) + * + * Submits to src/lib/contact-submit.ts → submitContact() + * which reads PUBLIC_CONTACT_ENDPOINT from .env (empty = dev mode). + */ +interface Props { + variant?: 'prompt' | 'full'; + title?: string; + desc?: string; + id?: string; +} + +const { + variant = 'full', + title = 'คุยกับเราก่อน 30 นาที ฟรี', + desc = 'เราจะแนะนำแนวทางเบื้องต้น — บอกตรง ๆ ว่าอะไรควรทำ ไม่ควรทำ', + id = 'contact', +} = Astro.props; + +// Service options for full variant +const services = [ + { value: 'webdev', label: 'Website Development (Astro / WordPress)' }, + { value: 'ai-consult', label: 'AI Consult' }, + { value: 'automation', label: 'AI Automation' }, + { value: 'marketing', label: 'Online Marketing' }, + { value: 'other', label: 'อื่นๆ / ไม่แน่ใจ' }, +]; +--- + +
+ {variant === 'prompt' ? ( + <> +

+

+

+ + +
+

กด ENTER เพื่อส่ง — หรือ กรอกแบบฟอร์มเต็ม

+ + ) : ( + <> +

+

+

+
+ + +
+ + + + + + + + +
+ + )} + + +

+ + diff --git a/src/lib/contact-submit.ts b/src/lib/contact-submit.ts new file mode 100644 index 0000000..1158b35 --- /dev/null +++ b/src/lib/contact-submit.ts @@ -0,0 +1,75 @@ +/** + * MOREMINIMORE - Contact form submit handler + * Per plan 2026-06-13 round 2 #4: Real working form with Apps Script backend. + * + * Reads PUBLIC_CONTACT_ENDPOINT from .env (Astro env var). + * - Empty / missing → DEV MODE: log to console, return success (mock) + * - Set → POST JSON to endpoint + * + * Apps Script template lives in apps-script/contact-form/Code.gs + * (user deploys it themselves — see apps-script/contact-form/README.md) + */ + +export interface ContactData { + name: string; + phone: string; + email: string; + service: string; + message: string; + variant: 'prompt' | 'full'; +} + +export interface SubmitResult { + ok: boolean; + error?: string; + id?: string; + devMode?: boolean; +} + +/** Check if running in dev mode (no endpoint configured) */ +export function isDevMode(): boolean { + const endpoint = import.meta.env.PUBLIC_CONTACT_ENDPOINT; + return !endpoint || endpoint.trim() === ''; +} + +/** + * Submit contact form data to Apps Script endpoint. + * In dev mode (no endpoint), logs to console and returns success. + */ +export async function submitContact(data: ContactData): Promise { + // Dev mode: mock success + if (isDevMode()) { + console.info('[contact-submit] DEV MODE — payload:', data); + // Simulate network latency + await new Promise((r) => setTimeout(r, 300)); + return { ok: true, devMode: true }; + } + + // Production: POST to Apps Script + const endpoint = import.meta.env.PUBLIC_CONTACT_ENDPOINT as string; + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...data, + userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown', + submittedAt: new Date().toISOString(), + }), + // Apps Script web apps don't support CORS preflight reliably + // mode: 'no-cors' would prevent reading the response, so we accept opaque + }); + + if (!response.ok) { + return { ok: false, error: `HTTP ${response.status}` }; + } + + const result = (await response.json()) as { ok?: boolean; id?: string; error?: string }; + if (result.ok) { + return { ok: true, id: result.id }; + } + return { ok: false, error: result.error ?? 'Unknown error from server' }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +}