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());
|
||||
}
|
||||
140
apps-script/contact-form/README.md
Normal file
140
apps-script/contact-form/README.md
Normal file
@@ -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 <https://notify-bot.line.me/> — log in → My page → Generate token)
|
||||
|
||||
---
|
||||
|
||||
## Setup (5 minutes)
|
||||
|
||||
### Step 1: Create the Apps Script project
|
||||
|
||||
1. Go to <https://script.google.com/start>
|
||||
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) | <https://notify-bot.line.me/> → My page → Generate token → Copy |
|
||||
| `RECIPIENT_EMAIL` | `contact@moreminimore.com` | Just type your email |
|
||||
|
||||
### Step 3: Create the Google Sheet
|
||||
|
||||
1. Go to <https://sheets.google.com/create>
|
||||
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 <https://notify-bot.line.me/>
|
||||
|
||||
If LINE Notify is shut down, swap the LINE call for a Telegram bot or Discord webhook (same `UrlFetchApp.fetch` pattern).
|
||||
169
src/components/Contact.astro
Normal file
169
src/components/Contact.astro
Normal file
@@ -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: 'อื่นๆ / ไม่แน่ใจ' },
|
||||
];
|
||||
---
|
||||
|
||||
<div id={id} class="fx-contact fx-reveal">
|
||||
{variant === 'prompt' ? (
|
||||
<>
|
||||
<h2 class="fx-contact-title" set:html={title} />
|
||||
<p class="fx-contact-desc" set:html={desc} />
|
||||
<form class="fx-contact-form" data-contact-prompt>
|
||||
<input
|
||||
type="text"
|
||||
name="prompt"
|
||||
class="fx-contact-input"
|
||||
placeholder="name / phone / line"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="fx-contact-submit">ENTER</button>
|
||||
</form>
|
||||
<p class="fx-contact-hint">กด ENTER เพื่อส่ง — หรือ <a href="/contact">กรอกแบบฟอร์มเต็ม</a></p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 class="fx-contact-title" set:html={title} />
|
||||
<p class="fx-contact-desc" set:html={desc} />
|
||||
<form class="fx-contact-form-full" data-contact-full>
|
||||
<div class="fx-contact-row">
|
||||
<label class="fx-contact-field">
|
||||
<span>ชื่อ <em>*</em></span>
|
||||
<input type="text" name="name" required placeholder="สมชาย ใจดี" />
|
||||
</label>
|
||||
<label class="fx-contact-field">
|
||||
<span>เบอร์โทร <em>*</em></span>
|
||||
<input type="tel" name="phone" required placeholder="080-xxx-xxxx" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="fx-contact-field">
|
||||
<span>อีเมล</span>
|
||||
<input type="email" name="email" placeholder="you@example.com" />
|
||||
</label>
|
||||
|
||||
<label class="fx-contact-field">
|
||||
<span>สนใจบริการ</span>
|
||||
<select name="service">
|
||||
{services.map((s) => <option value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="fx-contact-field">
|
||||
<span>รายละเอียดเพิ่มเติม</span>
|
||||
<textarea name="message" rows="4" placeholder="เล่าปัญหาหรือเป้าหมายที่อยากให้ช่วย..."></textarea>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="fx-contact-submit">ส่งข้อความ →</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div class="fx-contact-toast" data-contact-toast hidden></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { submitContact, isDevMode } from '../lib/contact-submit';
|
||||
|
||||
function showToast(el: HTMLElement, msg: string, type: 'ok' | 'err') {
|
||||
el.textContent = msg;
|
||||
el.className = `fx-contact-toast fx-contact-toast-${type}`;
|
||||
el.hidden = false;
|
||||
setTimeout(() => { el.hidden = true; }, 5000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const isDev = isDevMode();
|
||||
if (isDev) {
|
||||
console.info('[contact] dev mode — submissions will log to console, not POST to server');
|
||||
}
|
||||
|
||||
// Prompt form (1 input)
|
||||
const promptForm = document.querySelector<HTMLFormElement>('[data-contact-prompt]');
|
||||
promptForm?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(promptForm);
|
||||
const prompt = String(formData.get('prompt') ?? '');
|
||||
// Smart-parse: detect if it's name, phone, or line
|
||||
const looksLikePhone = /[\d-]{8,}/.test(prompt);
|
||||
const data = {
|
||||
name: looksLikePhone ? '' : prompt,
|
||||
phone: looksLikePhone ? prompt : '',
|
||||
email: '',
|
||||
service: 'other',
|
||||
message: prompt,
|
||||
variant: 'prompt' as const,
|
||||
};
|
||||
const toast = document.querySelector<HTMLElement>('[data-contact-toast]');
|
||||
if (toast) showToast(toast, 'กำลังส่ง...', 'ok');
|
||||
const result = await submitContact(data);
|
||||
if (toast) {
|
||||
if (result.ok) {
|
||||
showToast(toast, isDev ? '✓ ส่งแล้ว (dev mode)' : '✓ ส่งแล้ว เราจะติดต่อกลับภายใน 24 ชม.', 'ok');
|
||||
promptForm.reset();
|
||||
} else {
|
||||
showToast(toast, '✗ ส่งไม่สำเร็จ กรุณาลองใหม่หรือทัก LINE: @moreminimore', 'err');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Full form (5 fields)
|
||||
const fullForm = document.querySelector<HTMLFormElement>('[data-contact-full]');
|
||||
fullForm?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(fullForm);
|
||||
const data = {
|
||||
name: String(formData.get('name') ?? ''),
|
||||
phone: String(formData.get('phone') ?? ''),
|
||||
email: String(formData.get('email') ?? ''),
|
||||
service: String(formData.get('service') ?? 'other'),
|
||||
message: String(formData.get('message') ?? ''),
|
||||
variant: 'full' as const,
|
||||
};
|
||||
const toast = document.querySelector<HTMLElement>('[data-contact-toast]');
|
||||
if (toast) showToast(toast, 'กำลังส่ง...', 'ok');
|
||||
const result = await submitContact(data);
|
||||
if (toast) {
|
||||
if (result.ok) {
|
||||
showToast(toast, isDev ? '✓ ส่งแล้ว (dev mode)' : '✓ ส่งแล้ว เราจะติดต่อกลับภายใน 24 ชม.', 'ok');
|
||||
fullForm.reset();
|
||||
} else {
|
||||
showToast(toast, '✗ ส่งไม่สำเร็จ กรุณาลองใหม่หรือทัก LINE: @moreminimore', 'err');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
75
src/lib/contact-submit.ts
Normal file
75
src/lib/contact-submit.ts
Normal file
@@ -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<SubmitResult> {
|
||||
// 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) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user