form-to-google-sheet 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Code.gs +56 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/form-to-sheet.js +106 -0
- package/index.mjs +59 -0
- package/package.json +47 -0
package/Code.gs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ===========================================================
|
|
2
|
+
// Google Apps Script — paste this into your Sheet's script editor
|
|
3
|
+
// (Extensions > Apps Script), then deploy as a Web App.
|
|
4
|
+
// ===========================================================
|
|
5
|
+
|
|
6
|
+
var SHEET_NAME = "Sheet1"; // change if your tab has a different name
|
|
7
|
+
|
|
8
|
+
function doPost(e) {
|
|
9
|
+
try {
|
|
10
|
+
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
|
|
11
|
+
if (!sheet) {
|
|
12
|
+
return _jsonResponse(404, { error: "Sheet '" + SHEET_NAME + "' not found" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var data = JSON.parse(e.postData.contents);
|
|
16
|
+
var rows = data.rows || [data]; // accept a single object or { rows: [...] }
|
|
17
|
+
|
|
18
|
+
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].filter(String);
|
|
19
|
+
|
|
20
|
+
if (headers.length === 0) {
|
|
21
|
+
// Sheet is empty — use the keys from the first row as headers
|
|
22
|
+
headers = Object.keys(rows[0]);
|
|
23
|
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var output = [];
|
|
27
|
+
rows.forEach(function (row) {
|
|
28
|
+
var newRow = headers.map(function (header) {
|
|
29
|
+
return row[header] !== undefined ? row[header] : "";
|
|
30
|
+
});
|
|
31
|
+
sheet.appendRow(newRow);
|
|
32
|
+
output.push(newRow);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return _jsonResponse(200, {
|
|
36
|
+
status: "ok",
|
|
37
|
+
rowsAdded: output.length,
|
|
38
|
+
headers: headers,
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return _jsonResponse(500, { error: err.message });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function doGet() {
|
|
46
|
+
return _jsonResponse(200, {
|
|
47
|
+
status: "ok",
|
|
48
|
+
message: "FormToGoogleSheet endpoint is live. Send a POST to add rows.",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _jsonResponse(code, payload) {
|
|
53
|
+
return ContentService.createTextOutput(JSON.stringify(payload)).setMimeType(
|
|
54
|
+
ContentService.MimeType.JSON
|
|
55
|
+
);
|
|
56
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sahil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# FormToGoogleSheet
|
|
2
|
+
|
|
3
|
+
Submit HTML form data straight to a Google Sheet. No API keys, no server, no database — just a Google Sheet and a `<script>` tag.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install form-to-google-sheet
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use a CDN — no install needed:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://unpkg.com/form-to-google-sheet"></script>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or with jsdelivr:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/form-to-google-sheet"></script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How It Works
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Browser form ──POST──▶ Google Apps Script (free) ──▶ Google Sheet
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Google Apps Script acts as a tiny serverless endpoint that lives inside your spreadsheet. You deploy it once, get a URL, and POST to it from anywhere.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Setup (5 minutes)
|
|
34
|
+
|
|
35
|
+
### 1. Prepare your Google Sheet
|
|
36
|
+
|
|
37
|
+
Open your sheet and add **column headers** in Row 1.
|
|
38
|
+
The header names must match the keys you send from your form.
|
|
39
|
+
|
|
40
|
+
| name | email | message |
|
|
41
|
+
|------|-------|---------|
|
|
42
|
+
| | | |
|
|
43
|
+
|
|
44
|
+
> If the sheet is empty (no headers), the script will auto-create headers from the first submission's keys.
|
|
45
|
+
|
|
46
|
+
### 2. Add the Apps Script
|
|
47
|
+
|
|
48
|
+
1. In your Google Sheet go to **Extensions → Apps Script**.
|
|
49
|
+
2. Delete any code in the editor and paste the contents of [`Code.gs`](Code.gs).
|
|
50
|
+
3. If your tab is not called `Sheet1`, update the `SHEET_NAME` variable at the top.
|
|
51
|
+
4. Click **💾 Save**.
|
|
52
|
+
|
|
53
|
+
### 3. Deploy as a Web App
|
|
54
|
+
|
|
55
|
+
1. In the Apps Script editor click **Deploy → New deployment**.
|
|
56
|
+
2. Click the gear icon next to **Select type** and choose **Web app**.
|
|
57
|
+
3. Set:
|
|
58
|
+
- **Description**: anything you like
|
|
59
|
+
- **Execute as**: **Me**
|
|
60
|
+
- **Who has access**: **Anyone**
|
|
61
|
+
4. Click **Deploy**.
|
|
62
|
+
5. Authorize the script when prompted (it needs permission to edit *your* sheet).
|
|
63
|
+
6. Copy the **Web app URL** — it looks like:
|
|
64
|
+
`https://script.google.com/macros/s/AKfycb.../exec`
|
|
65
|
+
|
|
66
|
+
### 4. Add to Your Project
|
|
67
|
+
|
|
68
|
+
**With npm / a bundler (React, Next.js, Vite, etc.):**
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import FormToSheet from "form-to-google-sheet";
|
|
72
|
+
|
|
73
|
+
FormToSheet.init("https://script.google.com/macros/s/YOUR_ID/exec");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**With a CDN / script tag (plain HTML):**
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<script src="https://unpkg.com/form-to-google-sheet"></script>
|
|
80
|
+
<script>
|
|
81
|
+
FormToSheet.init("https://script.google.com/macros/s/YOUR_ID/exec");
|
|
82
|
+
</script>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Option A: Bind to a `<form>`
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<form id="contact-form">
|
|
93
|
+
<input name="name" required />
|
|
94
|
+
<input name="email" required />
|
|
95
|
+
<textarea name="message"></textarea>
|
|
96
|
+
<button type="submit">Send</button>
|
|
97
|
+
</form>
|
|
98
|
+
|
|
99
|
+
<script src="form-to-sheet.js"></script>
|
|
100
|
+
<script>
|
|
101
|
+
FormToSheet.init("YOUR_APPS_SCRIPT_URL");
|
|
102
|
+
FormToSheet.bind("#contact-form", {
|
|
103
|
+
onSuccess: () => alert("Saved!"),
|
|
104
|
+
onError: (err) => console.error(err),
|
|
105
|
+
resetOnSuccess: true, // default: true
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Option B: Submit Programmatically
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
FormToSheet.init("YOUR_APPS_SCRIPT_URL");
|
|
114
|
+
|
|
115
|
+
// single row
|
|
116
|
+
FormToSheet.submit({ name: "Ada", email: "ada@example.com" })
|
|
117
|
+
.then(console.log);
|
|
118
|
+
|
|
119
|
+
// multiple rows at once
|
|
120
|
+
FormToSheet.submit([
|
|
121
|
+
{ name: "Ada", email: "ada@example.com" },
|
|
122
|
+
{ name: "Grace", email: "grace@example.com" },
|
|
123
|
+
]);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Option C: Use in React
|
|
127
|
+
|
|
128
|
+
```jsx
|
|
129
|
+
import { useRef } from "react";
|
|
130
|
+
import FormToSheet from "form-to-google-sheet";
|
|
131
|
+
|
|
132
|
+
FormToSheet.init("YOUR_APPS_SCRIPT_URL");
|
|
133
|
+
|
|
134
|
+
export default function ContactForm() {
|
|
135
|
+
const formRef = useRef(null);
|
|
136
|
+
|
|
137
|
+
const handleSubmit = async (e) => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
const data = Object.fromEntries(new FormData(formRef.current));
|
|
140
|
+
await FormToSheet.submit(data);
|
|
141
|
+
alert("Done!");
|
|
142
|
+
formRef.current.reset();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<form ref={formRef} onSubmit={handleSubmit}>
|
|
147
|
+
<input name="name" required />
|
|
148
|
+
<input name="email" required />
|
|
149
|
+
<button type="submit">Send</button>
|
|
150
|
+
</form>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Tip: call `FormToSheet.init(url)` once at app startup (e.g. in `main.jsx`) instead of per-component if you prefer.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## API
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
|--------|-------------|
|
|
163
|
+
| `FormToSheet.init(url)` | Set the Apps Script deployment URL. Call once. |
|
|
164
|
+
| `FormToSheet.submit(data)` | Send a row (`{}`) or rows (`[{}, ...]`). Returns a `Promise`. |
|
|
165
|
+
| `FormToSheet.bind(selector, opts)` | Auto-wire a `<form>` element. Options: `onSuccess`, `onError`, `resetOnSuccess`. |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Notes
|
|
170
|
+
|
|
171
|
+
- **No API key required.** The Google Apps Script web app handles auth.
|
|
172
|
+
- **CORS**: The library uses `mode: "no-cors"` so it works from any origin. The trade-off is you can't read the response body — but the row still gets written. If the fetch doesn't throw, it worked.
|
|
173
|
+
- **Rate limits**: Google Apps Script allows ~20,000 calls/day on a free Google account. More than enough for contact forms and small apps.
|
|
174
|
+
- **Security**: Anyone with the URL can POST to your sheet. For production, consider adding a shared secret or honeypot field in the Apps Script.
|
package/form-to-sheet.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Sahil Bambulkar (https://www.linkedin.com/in/sahil-bambulkar/)
|
|
4
|
+
* FormToGoogleSheet — tiny client library (no dependencies, ~1 KB)
|
|
5
|
+
*
|
|
6
|
+
* Usage (script tag):
|
|
7
|
+
* <script src="form-to-sheet.js"></script>
|
|
8
|
+
* <script>
|
|
9
|
+
* FormToSheet.init("https://script.google.com/macros/s/YOUR_DEPLOY_ID/exec");
|
|
10
|
+
* FormToSheet.submit({ name: "Ada", email: "ada@example.com" });
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* Usage (ES module):
|
|
14
|
+
* import { init, submit } from "./form-to-sheet.js";
|
|
15
|
+
* init("https://script.google.com/macros/s/YOUR_DEPLOY_ID/exec");
|
|
16
|
+
* submit({ name: "Ada", email: "ada@example.com" });
|
|
17
|
+
*/
|
|
18
|
+
(function (root, factory) {
|
|
19
|
+
if (typeof module === "object" && module.exports) {
|
|
20
|
+
module.exports = factory();
|
|
21
|
+
} else if (typeof define === "function" && define.amd) {
|
|
22
|
+
define(factory);
|
|
23
|
+
} else {
|
|
24
|
+
root.FormToSheet = factory();
|
|
25
|
+
}
|
|
26
|
+
})(typeof self !== "undefined" ? self : this, function () {
|
|
27
|
+
"use strict";
|
|
28
|
+
|
|
29
|
+
var _url = "";
|
|
30
|
+
|
|
31
|
+
function init(scriptUrl) {
|
|
32
|
+
if (!scriptUrl) throw new Error("[FormToSheet] Missing Apps Script URL");
|
|
33
|
+
_url = scriptUrl.replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _ensureInit() {
|
|
37
|
+
if (!_url)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"[FormToSheet] Call FormToSheet.init(url) before submitting"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Submit a single row (object) or multiple rows (array of objects).
|
|
45
|
+
* Returns a Promise that resolves with the Apps Script JSON response.
|
|
46
|
+
*/
|
|
47
|
+
function submit(data) {
|
|
48
|
+
_ensureInit();
|
|
49
|
+
var body = Array.isArray(data) ? { rows: data } : data;
|
|
50
|
+
|
|
51
|
+
return fetch(_url, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
mode: "no-cors",
|
|
54
|
+
headers: { "Content-Type": "text/plain" },
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
}).then(function (res) {
|
|
57
|
+
// Google Apps Script redirects and returns opaque responses in no-cors
|
|
58
|
+
// mode, so we can't reliably read the body. If the fetch didn't throw,
|
|
59
|
+
// the request was accepted.
|
|
60
|
+
if (res.type === "opaque") {
|
|
61
|
+
return { status: "ok", note: "opaque response (no-cors mode)" };
|
|
62
|
+
}
|
|
63
|
+
return res.json();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convenience: bind to a <form> element. On submit it reads all named
|
|
69
|
+
* inputs, sends them to the sheet, and calls your callback.
|
|
70
|
+
*
|
|
71
|
+
* FormToSheet.bind("#my-form", {
|
|
72
|
+
* onSuccess: () => alert("Saved!"),
|
|
73
|
+
* onError: (err) => console.error(err),
|
|
74
|
+
* });
|
|
75
|
+
*/
|
|
76
|
+
function bind(selectorOrEl, opts) {
|
|
77
|
+
_ensureInit();
|
|
78
|
+
opts = opts || {};
|
|
79
|
+
var form =
|
|
80
|
+
typeof selectorOrEl === "string"
|
|
81
|
+
? document.querySelector(selectorOrEl)
|
|
82
|
+
: selectorOrEl;
|
|
83
|
+
|
|
84
|
+
if (!form) throw new Error("[FormToSheet] Form not found: " + selectorOrEl);
|
|
85
|
+
|
|
86
|
+
form.addEventListener("submit", function (e) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
var formData = new FormData(form);
|
|
89
|
+
var row = {};
|
|
90
|
+
formData.forEach(function (value, key) {
|
|
91
|
+
row[key] = value;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
submit(row)
|
|
95
|
+
.then(function (res) {
|
|
96
|
+
if (opts.onSuccess) opts.onSuccess(res);
|
|
97
|
+
if (opts.resetOnSuccess !== false) form.reset();
|
|
98
|
+
})
|
|
99
|
+
.catch(function (err) {
|
|
100
|
+
if (opts.onError) opts.onError(err);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { init: init, submit: submit, bind: bind };
|
|
106
|
+
});
|
package/index.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
let _url = "";
|
|
2
|
+
|
|
3
|
+
function init(scriptUrl) {
|
|
4
|
+
if (!scriptUrl) throw new Error("[FormToSheet] Missing Apps Script URL");
|
|
5
|
+
_url = scriptUrl.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function _ensureInit() {
|
|
9
|
+
if (!_url)
|
|
10
|
+
throw new Error(
|
|
11
|
+
"[FormToSheet] Call FormToSheet.init(url) before submitting"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function submit(data) {
|
|
16
|
+
_ensureInit();
|
|
17
|
+
const body = Array.isArray(data) ? { rows: data } : data;
|
|
18
|
+
|
|
19
|
+
return fetch(_url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
mode: "no-cors",
|
|
22
|
+
headers: { "Content-Type": "text/plain" },
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
}).then((res) => {
|
|
25
|
+
if (res.type === "opaque") {
|
|
26
|
+
return { status: "ok", note: "opaque response (no-cors mode)" };
|
|
27
|
+
}
|
|
28
|
+
return res.json();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function bind(selectorOrEl, opts = {}) {
|
|
33
|
+
_ensureInit();
|
|
34
|
+
const form =
|
|
35
|
+
typeof selectorOrEl === "string"
|
|
36
|
+
? document.querySelector(selectorOrEl)
|
|
37
|
+
: selectorOrEl;
|
|
38
|
+
|
|
39
|
+
if (!form)
|
|
40
|
+
throw new Error("[FormToSheet] Form not found: " + selectorOrEl);
|
|
41
|
+
|
|
42
|
+
form.addEventListener("submit", (e) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
const formData = new FormData(form);
|
|
45
|
+
const row = Object.fromEntries(formData);
|
|
46
|
+
|
|
47
|
+
submit(row)
|
|
48
|
+
.then((res) => {
|
|
49
|
+
if (opts.onSuccess) opts.onSuccess(res);
|
|
50
|
+
if (opts.resetOnSuccess !== false) form.reset();
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
if (opts.onError) opts.onError(err);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { init };
|
|
59
|
+
export default { init, submit, bind };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "form-to-google-sheet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Submit HTML form data straight to a Google Sheet. No API keys, no server, no database.",
|
|
5
|
+
"main": "form-to-sheet.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./index.mjs",
|
|
10
|
+
"require": "./form-to-sheet.js",
|
|
11
|
+
"default": "./form-to-sheet.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"browser": "form-to-sheet.js",
|
|
15
|
+
"unpkg": "form-to-sheet.js",
|
|
16
|
+
"jsdelivr": "form-to-sheet.js",
|
|
17
|
+
"files": [
|
|
18
|
+
"form-to-sheet.js",
|
|
19
|
+
"index.mjs",
|
|
20
|
+
"Code.gs",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"google-sheets",
|
|
29
|
+
"google-sheet",
|
|
30
|
+
"form",
|
|
31
|
+
"form-submit",
|
|
32
|
+
"spreadsheet",
|
|
33
|
+
"google-apps-script",
|
|
34
|
+
"no-server",
|
|
35
|
+
"serverless"
|
|
36
|
+
],
|
|
37
|
+
"author": "Sahil",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/sahil/form-to-google-sheet.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/sahil/form-to-google-sheet#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/sahil/form-to-google-sheet/issues"
|
|
46
|
+
}
|
|
47
|
+
}
|