Spaces:
Running
Running
Ig0tU commited on
Commit ·
33644aa
1
Parent(s): ee831a5
Chore: environment variable support and security updates
Browse files- .gitignore +2 -0
- credentials.json +0 -13
- public/live-site-integration.js +23 -1
- scrape_forms.js +119 -0
- server.js +15 -79
.gitignore
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
node_modules/
|
|
|
|
|
|
|
|
|
| 1 |
node_modules/
|
| 2 |
+
credentials.json
|
| 3 |
+
.env
|
credentials.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"type": "service_account",
|
| 3 |
-
"project_id": "gen-lang-client-0963727327",
|
| 4 |
-
"private_key_id": "9bd1ab7c0de460b7e1e99f7cbb9ee0423f0581e6",
|
| 5 |
-
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9vhCyLjXvka79\n4siXVml6tRMBjKu686XPjcZpj9DplKu4nuJbnkW9HbdKxz2qaA3K2/0Eb5+vV86U\ngK/QxArvJ3SwbF3Uoo8S6YOgtZ4/WZLKthIEN0o8t6emqifPbDydsvJ88hNmm7r0\n7Jy2+xCdQhnbkNnfPLBjEornTc86Pqq3lDh24XE8YK8bPB1gpsiOMKh6rzBV80qM\noPSYwkNttdvoEdMSVEr6bvWUtt7XJiXitR/ZZbs2oPl3jeVDZzxRPb4TTrPpvWRK\nXAXq8ktkj8ZiReapgahEcse3h49rAxU8Z93YBVdMKrNf3TxlL+VAuUafVknVBdg5\nCYd0vqgNAgMBAAECggEAAbYDFER1nrwvpyzp9SYj/YPvtBIOzGFHLwwVed7Kkz6J\n4cXdpDSl4BSDYyrlQHxC7rxx/+6wK8+8+BI3bBKyLIul4UfQCl+WronK5UDPseVx\nRyK7VH2c72u0Oqiy7tWPAMtBETYoddbOCAUcaFkN6toqFV60CzMc38JcLYIMZ2nN\nG/5OOCgopdX3pEjKLZqdp1pKCB1m4+Tkf+MXJigCnUqVtcOtaDMq4LjMxj1uwM4l\nQXd1fanQU4dEdPzCrgjNe0pFEfnCYsMSByMAw8ny99J4GTyYCRQaQxmf27rHWX31\n4XSDwQoUgI3KueqpAhchYujwGqy5nN10To3guNWccQKBgQDtBD889ltKvdx5vHts\n0Jfhhm1zzUYwKpsZqNJoR521MkdJZ5/scrrgFiOQ+TEmBkq2dKoO185kWf6saKeE\nuWdmbnEIuRC1hm1yS/NcDW9u66/Z02VS88ZOkPkzVxtfdLfrJzA5qQbji2kRKFoO\n0W56CrcdMUpNyFDdIp6txfmRBQKBgQDM8IQeYNFQAJqeS/NGQDa2TKbqohiqbivw\nNodXmeIAbYDyoZYuCbJq1kx0bJpwG+YXGmFJ0HPVi5C3wwKQcAzV4UQK5CAVk6pX\n+cpAk+/a6xdiER0a0pe9Iae+OCJaLsrWb2v9HGebRjKbffFI6EqrELSU6cWX17cs\nw18RpHUJaQKBgBCU8HuqXJ6xA8C8kAH/4fBUQEoOvW9XO7yi0/2ZrQ3lM5mOF2Eq\ncaqFwf09gdPAMu/q347kSDs7FJcpRzcA5ZwD9AKBOAsLGZMafy8cfYRMFuRtZrRT\n+7a5a8XMvUyDVO6tsjEGg0XeFf6uTQamXk1JfKAdN66TahzedC2CIUClAoGBAL0e\nnu1hrwbC5+x4bjFABL1KEyanG1f7fzSXPWJLsVFvu/UrxGkLrcgFplwx9HrMZBKh\n13HEmYBQ4OWTYgRkQpQE40OhrTH8KNAyxL+/RTKii9uFq3QbLsfsDN3u81SBpdEo\n1WCoG7wglYcEO+tp4a3nJp3c9OjgujrmuA6R0ycxAoGAU9KF9GfAJDXKMjXBQiH+\n6ch1awgNF8jQ+fmvp5Va62FgdK9aMSQTnJJDVMYg6nTg2cvhbTL1rXr/bYjWHglV\nJx0SIzUAJjMRoOLxeG9bbX3JAUcuxcRdN97f0JTTpG9y2FoxwwLuTPeG7GhAUi60\njNfEUUkqWEFAjnjK8eujifU=\n-----END PRIVATE KEY-----\n",
|
| 6 |
-
"client_email": "wall-form-api@gen-lang-client-0963727327.iam.gserviceaccount.com",
|
| 7 |
-
"client_id": "101446101542617249984",
|
| 8 |
-
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
-
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
-
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
-
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/wall-form-api%40gen-lang-client-0963727327.iam.gserviceaccount.com",
|
| 12 |
-
"universe_domain": "googleapis.com"
|
| 13 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public/live-site-integration.js
CHANGED
|
@@ -73,7 +73,7 @@
|
|
| 73 |
}
|
| 74 |
});
|
| 75 |
|
| 76 |
-
// Read file inputs as base64 DataURLs
|
| 77 |
const filePromises = [];
|
| 78 |
Array.from(form.elements).forEach(el => {
|
| 79 |
if (el.type !== 'file' || !el.files || !el.files.length) return;
|
|
@@ -88,6 +88,28 @@
|
|
| 88 |
});
|
| 89 |
await Promise.all(filePromises);
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
// Set fallbacks for strict checking on the backend if this was just a generic form
|
| 92 |
if (!formData.name && !formData.Name && !formData.contactName && !formData.vetFirstName && !formData.submitterName) {
|
| 93 |
formData._generic_name = "User";
|
|
|
|
| 73 |
}
|
| 74 |
});
|
| 75 |
|
| 76 |
+
// Read file inputs as base64 DataURLs
|
| 77 |
const filePromises = [];
|
| 78 |
Array.from(form.elements).forEach(el => {
|
| 79 |
if (el.type !== 'file' || !el.files || !el.files.length) return;
|
|
|
|
| 88 |
});
|
| 89 |
await Promise.all(filePromises);
|
| 90 |
|
| 91 |
+
// Upload photo directly to live server PHP — avoids HF ephemeral storage
|
| 92 |
+
if (PHOTO_UPLOAD_URL && formData.vetPhoto && formData.vetPhoto.startsWith('data:image')) {
|
| 93 |
+
try {
|
| 94 |
+
const idRes = await fetch(RESERVE_ID_URL + '?tab=Honor');
|
| 95 |
+
const idData = await idRes.json();
|
| 96 |
+
const uploadRes = await fetch(PHOTO_UPLOAD_URL, {
|
| 97 |
+
method: 'POST',
|
| 98 |
+
headers: { 'Content-Type': 'application/json' },
|
| 99 |
+
body: JSON.stringify({ image: formData.vetPhoto, id: idData.id })
|
| 100 |
+
});
|
| 101 |
+
if (uploadRes.ok) {
|
| 102 |
+
const uploadData = await uploadRes.json();
|
| 103 |
+
if (uploadData.url) {
|
| 104 |
+
formData.vetPhoto = uploadData.url;
|
| 105 |
+
formData._reservedPhotoId = idData.id;
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
} catch (e) {
|
| 109 |
+
console.warn('[WallAPI] Direct photo upload failed, API will handle it:', e.message);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
// Set fallbacks for strict checking on the backend if this was just a generic form
|
| 114 |
if (!formData.name && !formData.Name && !formData.contactName && !formData.vetFirstName && !formData.submitterName) {
|
| 115 |
formData._generic_name = "User";
|
scrape_forms.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require('fs');
|
| 2 |
+
const path = require('path');
|
| 3 |
+
const cheerio = require('cheerio');
|
| 4 |
+
|
| 5 |
+
const urls = [
|
| 6 |
+
{ name: 'tours', url: 'https://www.thewallthathealsreynolds.org/tours.html' },
|
| 7 |
+
{ name: 'escort', url: 'https://www.thewallthathealsreynolds.org/escort.html' },
|
| 8 |
+
{ name: 'honor', url: 'https://www.thewallthathealsreynolds.org/honor.html' },
|
| 9 |
+
{ name: 'contact', url: 'https://www.thewallthathealsreynolds.org/contact.html' }
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
const { execSync } = require('child_process');
|
| 13 |
+
|
| 14 |
+
async function scrapeForms() {
|
| 15 |
+
for (const item of urls) {
|
| 16 |
+
console.log(`Fetching ${item.url}...`);
|
| 17 |
+
try {
|
| 18 |
+
const html = execSync(`curl -s -k "${item.url}"`).toString();
|
| 19 |
+
const $ = cheerio.load(html);
|
| 20 |
+
|
| 21 |
+
// Find the form container
|
| 22 |
+
let formHtml = '';
|
| 23 |
+
|
| 24 |
+
const formContainer = $('.contact-form');
|
| 25 |
+
if (formContainer.length > 0) {
|
| 26 |
+
formHtml = $.html(formContainer);
|
| 27 |
+
} else {
|
| 28 |
+
const justForm = $('form[data-validate]');
|
| 29 |
+
if (justForm.length > 0) {
|
| 30 |
+
formHtml = $.html(justForm);
|
| 31 |
+
} else {
|
| 32 |
+
const fallback = $('form');
|
| 33 |
+
if (fallback.length > 0) {
|
| 34 |
+
formHtml = $.html(fallback.first());
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
if (formHtml) {
|
| 40 |
+
const finalHtml = `<!DOCTYPE html>
|
| 41 |
+
<html lang="en">
|
| 42 |
+
<head>
|
| 43 |
+
<meta charset="UTF-8">
|
| 44 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 45 |
+
<title>${item.name.charAt(0).toUpperCase() + item.name.slice(1)} Form</title>
|
| 46 |
+
<style>
|
| 47 |
+
body {
|
| 48 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 49 |
+
background-color: transparent;
|
| 50 |
+
margin: 0;
|
| 51 |
+
padding: 20px;
|
| 52 |
+
color: #333;
|
| 53 |
+
}
|
| 54 |
+
.contact-form {
|
| 55 |
+
max-width: 600px;
|
| 56 |
+
margin: 0 auto;
|
| 57 |
+
background: #fff;
|
| 58 |
+
padding: 30px;
|
| 59 |
+
border-radius: 8px;
|
| 60 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 61 |
+
}
|
| 62 |
+
.form-group {
|
| 63 |
+
margin-bottom: 20px;
|
| 64 |
+
}
|
| 65 |
+
label {
|
| 66 |
+
display: block;
|
| 67 |
+
margin-bottom: 8px;
|
| 68 |
+
font-weight: 500;
|
| 69 |
+
color: #1a365d;
|
| 70 |
+
}
|
| 71 |
+
input[type="text"],
|
| 72 |
+
input[type="email"],
|
| 73 |
+
input[type="tel"],
|
| 74 |
+
select,
|
| 75 |
+
textarea {
|
| 76 |
+
width: 100%;
|
| 77 |
+
padding: 10px;
|
| 78 |
+
border: 1px solid #ccc;
|
| 79 |
+
border-radius: 4px;
|
| 80 |
+
font-size: 16px;
|
| 81 |
+
box-sizing: border-box;
|
| 82 |
+
}
|
| 83 |
+
button[type="submit"], input[type="submit"] {
|
| 84 |
+
background-color: #1a365d;
|
| 85 |
+
color: white;
|
| 86 |
+
padding: 12px 24px;
|
| 87 |
+
border: none;
|
| 88 |
+
border-radius: 4px;
|
| 89 |
+
font-size: 16px;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
width: 100%;
|
| 92 |
+
transition: background-color 0.2s;
|
| 93 |
+
}
|
| 94 |
+
button[type="submit"]:hover, input[type="submit"]:hover {
|
| 95 |
+
background-color: #2c5282;
|
| 96 |
+
}
|
| 97 |
+
</style>
|
| 98 |
+
</head>
|
| 99 |
+
<body>
|
| 100 |
+
${formHtml.replace(/action="[^"]*"/g, '').replace(/method="[^"]*"/g, '')}
|
| 101 |
+
|
| 102 |
+
<!-- Integration Script to automatically wire this form up to the Hugging Face API -->
|
| 103 |
+
<script src="/live-site-integration.js"></script>
|
| 104 |
+
</body>
|
| 105 |
+
</html>`;
|
| 106 |
+
|
| 107 |
+
const outputPath = path.join(__dirname, 'public', `${item.name}.html`);
|
| 108 |
+
fs.writeFileSync(outputPath, finalHtml);
|
| 109 |
+
console.log(`Saved form to ${outputPath}`);
|
| 110 |
+
} else {
|
| 111 |
+
console.log(`No form found on ${item.url}`);
|
| 112 |
+
}
|
| 113 |
+
} catch (e) {
|
| 114 |
+
console.error(`Failed to fetch ${item.url}:`, e);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
scrapeForms();
|
server.js
CHANGED
|
@@ -82,27 +82,6 @@ async function getAuth() {
|
|
| 82 |
// Ensure the Google Sheet ID is configured
|
| 83 |
const spreadsheetId = process.env.GOOGLE_SHEET_ID;
|
| 84 |
|
| 85 |
-
// Reserve next ID for a tab (used by client before photo upload)
|
| 86 |
-
app.get('/api/reserve-id', async (req, res) => {
|
| 87 |
-
try {
|
| 88 |
-
const tab = req.query.tab || 'Sheet1';
|
| 89 |
-
const auth = await getAuth();
|
| 90 |
-
const sheets = google.sheets({ version: 'v4', auth });
|
| 91 |
-
const idCheck = await sheets.spreadsheets.values.get({
|
| 92 |
-
spreadsheetId,
|
| 93 |
-
range: `${tab}!A:A`,
|
| 94 |
-
});
|
| 95 |
-
const numericIds = (idCheck.data.values || [])
|
| 96 |
-
.flat()
|
| 97 |
-
.map(v => parseInt(v))
|
| 98 |
-
.filter(v => !isNaN(v));
|
| 99 |
-
const nextId = numericIds.length > 0 ? Math.max(...numericIds) + 1 : 1;
|
| 100 |
-
res.json({ id: nextId });
|
| 101 |
-
} catch (e) {
|
| 102 |
-
res.status(500).json({ error: e.message });
|
| 103 |
-
}
|
| 104 |
-
});
|
| 105 |
-
|
| 106 |
// API Endpoint to receive form submissions
|
| 107 |
app.post('/api/submit', async (req, res) => {
|
| 108 |
try {
|
|
@@ -127,31 +106,9 @@ app.post('/api/submit', async (req, res) => {
|
|
| 127 |
// We are going to append to Columns A to G (Timestamp, Name, Email, Message, Phone, Subject, Origin).
|
| 128 |
const TIMESTAMP = new Date().toISOString();
|
| 129 |
|
| 130 |
-
//
|
|
|
|
| 131 |
let range = 'Sheet1';
|
| 132 |
-
if (origin) {
|
| 133 |
-
const lo = origin.toLowerCase();
|
| 134 |
-
if (lo.includes('contact')) range = 'Contact';
|
| 135 |
-
else if (lo.includes('tours')) range = 'Tours';
|
| 136 |
-
else if (lo.includes('escort')) range = 'Escort';
|
| 137 |
-
else if (lo.includes('honor')) range = 'Honor';
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
let nextId = 1;
|
| 141 |
-
try {
|
| 142 |
-
const idCheck = await sheets.spreadsheets.values.get({
|
| 143 |
-
spreadsheetId,
|
| 144 |
-
range: `${range}!A:A`,
|
| 145 |
-
});
|
| 146 |
-
const numericIds = (idCheck.data.values || [])
|
| 147 |
-
.flat()
|
| 148 |
-
.filter(v => /^\d+$/.test(String(v).trim()))
|
| 149 |
-
.map(v => parseInt(v, 10));
|
| 150 |
-
nextId = numericIds.length > 0 ? Math.max(...numericIds) + 1 : 1;
|
| 151 |
-
} catch (e) {
|
| 152 |
-
console.error('ID fetch failed:', e.message);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
let rowData = [];
|
| 156 |
|
| 157 |
// Fallbacks if generic integration script used
|
|
@@ -164,8 +121,8 @@ app.post('/api/submit', async (req, res) => {
|
|
| 164 |
const lowerOrigin = origin.toLowerCase();
|
| 165 |
if (lowerOrigin.includes('contact')) {
|
| 166 |
range = 'Contact';
|
|
|
|
| 167 |
rowData = [
|
| 168 |
-
nextId,
|
| 169 |
TIMESTAMP,
|
| 170 |
safeName,
|
| 171 |
safeEmail,
|
|
@@ -176,8 +133,8 @@ app.post('/api/submit', async (req, res) => {
|
|
| 176 |
];
|
| 177 |
} else if (lowerOrigin.includes('tours')) {
|
| 178 |
range = 'Tours';
|
|
|
|
| 179 |
rowData = [
|
| 180 |
-
nextId,
|
| 181 |
TIMESTAMP,
|
| 182 |
req.body.groupName || '',
|
| 183 |
req.body.tourType || '',
|
|
@@ -194,13 +151,14 @@ app.post('/api/submit', async (req, res) => {
|
|
| 194 |
];
|
| 195 |
} else if (lowerOrigin.includes('escort')) {
|
| 196 |
range = 'Escort';
|
|
|
|
| 197 |
rowData = [
|
| 198 |
-
nextId,
|
| 199 |
TIMESTAMP,
|
| 200 |
req.body.name || '',
|
| 201 |
req.body.email || '',
|
| 202 |
req.body.phone || '',
|
| 203 |
req.body.vehicle || '',
|
|
|
|
| 204 |
origin
|
| 205 |
];
|
| 206 |
} else if (lowerOrigin.includes('honor')) {
|
|
@@ -210,35 +168,13 @@ app.post('/api/submit', async (req, res) => {
|
|
| 210 |
const photoData = req.body.vetPhoto;
|
| 211 |
if (photoData && photoData.startsWith('data:image')) {
|
| 212 |
try {
|
| 213 |
-
const matches = photoData.match(/^data:image\/(
|
| 214 |
if (matches) {
|
| 215 |
-
const
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
const phpRes = await fetch(process.env.PHOTO_UPLOAD_ENDPOINT, {
|
| 221 |
-
method: 'POST',
|
| 222 |
-
headers: { 'Content-Type': 'application/json' },
|
| 223 |
-
body: JSON.stringify({ image: photoData, id: nextId })
|
| 224 |
-
});
|
| 225 |
-
if (phpRes.ok) {
|
| 226 |
-
const phpData = await phpRes.json();
|
| 227 |
-
if (phpData.url) vetPhotoUrl = phpData.url;
|
| 228 |
-
}
|
| 229 |
-
} catch (phpErr) {
|
| 230 |
-
console.error('PHP upload failed, falling back to local:', phpErr.message);
|
| 231 |
-
}
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
// Fallback: save on HF space, but store the permanent thewall URL
|
| 235 |
-
if (!vetPhotoUrl) {
|
| 236 |
-
const uploadDir = path.join(__dirname, 'public', 'uploads');
|
| 237 |
-
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
| 238 |
-
const fileName = `${nextId}.${ext}`;
|
| 239 |
-
fs.writeFileSync(path.join(uploadDir, fileName), Buffer.from(matches[2], 'base64'));
|
| 240 |
-
vetPhotoUrl = `https://www.thewallthathealsreynolds.org/images/uploads/tributes/${fileName}`;
|
| 241 |
-
}
|
| 242 |
}
|
| 243 |
} catch (photoErr) {
|
| 244 |
console.error('Failed to save photo:', photoErr);
|
|
@@ -246,7 +182,6 @@ app.post('/api/submit', async (req, res) => {
|
|
| 246 |
}
|
| 247 |
|
| 248 |
rowData = [
|
| 249 |
-
nextId,
|
| 250 |
TIMESTAMP,
|
| 251 |
req.body.vetFirstName || '',
|
| 252 |
req.body.vetMiddleName || '',
|
|
@@ -268,10 +203,11 @@ app.post('/api/submit', async (req, res) => {
|
|
| 268 |
vetPhotoUrl
|
| 269 |
];
|
| 270 |
} else {
|
| 271 |
-
|
|
|
|
| 272 |
}
|
| 273 |
} else {
|
| 274 |
-
rowData = [
|
| 275 |
}
|
| 276 |
|
| 277 |
const response = await sheets.spreadsheets.values.append({
|
|
|
|
| 82 |
// Ensure the Google Sheet ID is configured
|
| 83 |
const spreadsheetId = process.env.GOOGLE_SHEET_ID;
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
// API Endpoint to receive form submissions
|
| 86 |
app.post('/api/submit', async (req, res) => {
|
| 87 |
try {
|
|
|
|
| 106 |
// We are going to append to Columns A to G (Timestamp, Name, Email, Message, Phone, Subject, Origin).
|
| 107 |
const TIMESTAMP = new Date().toISOString();
|
| 108 |
|
| 109 |
+
// The range specifies where to append.
|
| 110 |
+
// Dynamically sort into different tabs based on the origin URL!
|
| 111 |
let range = 'Sheet1';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
let rowData = [];
|
| 113 |
|
| 114 |
// Fallbacks if generic integration script used
|
|
|
|
| 121 |
const lowerOrigin = origin.toLowerCase();
|
| 122 |
if (lowerOrigin.includes('contact')) {
|
| 123 |
range = 'Contact';
|
| 124 |
+
// Match Contact Form: Date, Name, Email, Phone, Subject, Message, Origin
|
| 125 |
rowData = [
|
|
|
|
| 126 |
TIMESTAMP,
|
| 127 |
safeName,
|
| 128 |
safeEmail,
|
|
|
|
| 133 |
];
|
| 134 |
} else if (lowerOrigin.includes('tours')) {
|
| 135 |
range = 'Tours';
|
| 136 |
+
// Map exactly to: Date, Group Name, Tour Type, Contact Name, Contact Title, Email, Phone, Group Size, Date, Time, Special Needs, Additional Info, Origin
|
| 137 |
rowData = [
|
|
|
|
| 138 |
TIMESTAMP,
|
| 139 |
req.body.groupName || '',
|
| 140 |
req.body.tourType || '',
|
|
|
|
| 151 |
];
|
| 152 |
} else if (lowerOrigin.includes('escort')) {
|
| 153 |
range = 'Escort';
|
| 154 |
+
// Map exactly to: Date, Name, Email, Phone, Vehicle, Group Affiliation, Origin
|
| 155 |
rowData = [
|
|
|
|
| 156 |
TIMESTAMP,
|
| 157 |
req.body.name || '',
|
| 158 |
req.body.email || '',
|
| 159 |
req.body.phone || '',
|
| 160 |
req.body.vehicle || '',
|
| 161 |
+
req.body.groupAffiliation || '',
|
| 162 |
origin
|
| 163 |
];
|
| 164 |
} else if (lowerOrigin.includes('honor')) {
|
|
|
|
| 168 |
const photoData = req.body.vetPhoto;
|
| 169 |
if (photoData && photoData.startsWith('data:image')) {
|
| 170 |
try {
|
| 171 |
+
const matches = photoData.match(/^data:image\/(\w+);base64,(.+)$/s);
|
| 172 |
if (matches) {
|
| 173 |
+
const uploadDir = path.join(__dirname, 'public', 'uploads');
|
| 174 |
+
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
| 175 |
+
const fileName = `${Date.now()}.${matches[1]}`;
|
| 176 |
+
fs.writeFileSync(path.join(uploadDir, fileName), Buffer.from(matches[2], 'base64'));
|
| 177 |
+
vetPhotoUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
} catch (photoErr) {
|
| 180 |
console.error('Failed to save photo:', photoErr);
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
rowData = [
|
|
|
|
| 185 |
TIMESTAMP,
|
| 186 |
req.body.vetFirstName || '',
|
| 187 |
req.body.vetMiddleName || '',
|
|
|
|
| 203 |
vetPhotoUrl
|
| 204 |
];
|
| 205 |
} else {
|
| 206 |
+
// Fallback catch-all
|
| 207 |
+
rowData = [TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin];
|
| 208 |
}
|
| 209 |
} else {
|
| 210 |
+
rowData = [TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin || 'Unknown'];
|
| 211 |
}
|
| 212 |
|
| 213 |
const response = await sheets.spreadsheets.values.append({
|