I'll provide a detailed implementation for connecting Google Sheets with Next.js forms to save data. We'll use Next.js 15 with the App Router and TypeScript. Here's a step-by-step guide:
Set up Google Cloud Project and enable Google Sheets API:
Create service account credentials:
Install necessary dependencies: Run the following command in your project directory:
npm install google-auth-library googleapis
Create a new file app/actions/submitForm.ts
:
'use server'
import { google } from 'googleapis';
import { GoogleAuth } from 'google-auth-library';
// Function to append data to Google Sheets
async function appendToSheet(data: string[]) {
const auth = new GoogleAuth({
keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
const spreadsheetId = process.env.GOOGLE_SHEET_ID;
const range = 'Sheet1!A:Z'; // Adjust this range as needed
try {
const response = await sheets.spreadsheets.values.append({
spreadsheetId,
range,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [data],
},
});
console.log('Data appended successfully');
return response.data;
} catch (error) {
console.error('Error appending data to sheet:', error);
throw error;
}
}
export async function submitForm(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
if (!name || !email || !message) {
throw new Error('All fields are required');
}
const timestamp = new Date().toISOString();
const dataToAppend = [timestamp, name, email, message];
try {
await appendToSheet(dataToAppend);
return { success: true, message: 'Form submitted successfully' };
} catch (error) {
console.error('Error submitting form:', error);
return { success: false, message: 'Error submitting form' };
}
}
Create a new file app/components/ContactForm.tsx
:
'use client'
import { useState } from 'react';
import { useFormState } from 'react-dom';
import { submitForm } from '../actions/submitForm';
export default function ContactForm() {
const [state, formAction] = useFormState(submitForm, null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (formData: FormData) => {
setIsPending(true);
await formAction(formData);
setIsPending(false);
};
return (
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
id="email"
name="email"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</label>
<textarea
id="message"
name="message"
required
rows={4}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
></textarea>
</div>
<button
type="submit"
disabled={isPending}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{state && (
<div className={`mt-4 ${state.success ? 'text-green-600' : 'text-red-600'}`}>
{state.message}
</div>
)}
</form>
);
}
Create or update the file app/page.tsx
:
import ContactForm from './components/ContactForm';
export default function Home() {
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Contact Us</h1>
<ContactForm />
</main>
);
}
Create a .env.local
file in your project root and add the following:
GOOGLE_APPLICATION_CREDENTIALS=path/to/your/service-account-key.json
GOOGLE_SHEET_ID=your-google-sheet-id
Replace path/to/your/service-account-key.json
with the actual path to your downloaded service account key file, and your-google-sheet-id
with the ID of the Google Sheet you want to use (you can find this in the sheet's URL).
next.config.js
:/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
}
module.exports = nextConfig
This implementation provides a detailed setup for connecting a Google Sheet with a Next.js form. Here's what it does:
submitForm
server action handles form submission and interacts with the Google Sheets API.ContactForm
component renders the form and handles client-side state.To implement this manually:
submitForm.ts
, ContactForm.tsx
, page.tsx
).next.config.js
.Make sure to keep your service account key secure and never commit it to version control. You may want to add the key file path and .env.local
to your .gitignore
file.
This implementation uses Server Actions, which are a new feature in Next.js 13 and later 1. It also follows the App Router structure, placing components and server actions in the appropriate directories 2.
Remember to handle any necessary error cases and add additional form validation as needed for your specific use case.