From sheets to iCal

From Spreadsheet to Calendar: Building a Google Sheets to iCal App with Next.js

Hey there, fellow Next.js enthusiasts! ๐Ÿ‘‹ Today, I'm excited to walk you through a fun little project I've been working on. We're going to build a simple app that reads events from a Google Sheet and turns them into an iCal feed, complete with a snazzy landing page. This is perfect for beginners looking to dip their toes into Next.js and API integration. Let's dive in!

The Big Idea ๐Ÿ’ก

Have you ever found yourself maintaining a list of events in a Google Sheet and wished you could easily sync it with your calendar? Well, that's exactly what we're going to solve! Our app will:

  1. Read event data from a Google Sheet
  2. Generate an iCal feed from this data
  3. Display the events on a simple, responsive landing page

Sounds cool, right? Let's break it down step by step.

Setting Up Our Next.js Project ๐Ÿš€

First things first, let's set up our Next.js project. If you haven't already, install Node.js and create a new Next.js app:

npx create-next-app@latest google-sheets-ical
cd google-sheets-ical

Choose the following options:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes

The Layout: Our App's Foundation ๐Ÿ—๏ธ

Let's start with our layout.tsx file. This sets up the basic structure for our app:

import "@/styles/globals.css";
import { GeistSans } from "geist/font/sans";
import { type Metadata } from "next";

export const metadata: Metadata = {
  title: "Google Sheet to iCal",
  description: "Take a sheet with date/title and make all day event ical link",
  icons: [{ rel: "icon", url: "/favicon.ico" }],
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${GeistSans.variable}`}>
      <body>{children}</body>
    </html>
  );
}

This layout uses the Geist Sans font and sets up some basic metadata for our app. Nothing too fancy, but it's a solid foundation!

The Home Page: Where the Magic Happens โœจ

Now, let's create our main page in page.tsx:

import { getEvents } from "@/lib/google-sheets";
import EventListCard from "@/components/EventList";

export default async function Home() {
  const events = await getEvents();

  return (
    <main className="container mx-auto p-4">
      <h1 className="mb-4 text-2xl font-bold">Google Sheets Calendar</h1>
      <EventListCard events={events} />
      <div className="mt-4">
        <p>Subscribe to the iCal feed:</p>
        <code className="rounded bg-gray-100 p-2">
          {`${process.env.NEXT_PUBLIC_BASE_URL}/api/ical`}
        </code>
      </div>
    </main>
  );
}

This page does a few key things:

  1. It fetches events using our getEvents function (we'll create this soon!)
  2. Displays the events using an EventListCard component
  3. Shows a subscription link for the iCal feed

[Screenshot: Home page layout]

Displaying Events: The EventList Component ๐Ÿ“…

Let's create a component to display our events. Create a new file called EventList.tsx:

import type { Event } from "@/types";
import { Card } from "@/components/ui/card";
import { cn, isLessThanWeekAway, isPast } from "@/lib/utils";

interface EventListProps {
  events: Event[];
}

export default function EventListCards({ events }: EventListProps) {
  return (
    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
      {events.map((event, index) => (
        <Card
          key={index}
          className={cn(
            isLessThanWeekAway(event.date) && "bg-green-300",
            isPast(event.date) && "hidden",
            "border-muted rounded-md border p-4",
          )}
        >
          <div className="flex flex-col gap-2">
            <h3 className="text-lg font-semibold">{event.title}</h3>
            <p className="text-muted-foreground text-sm">
              {event.date.toLocaleDateString()}
            </p>
          </div>
        </Card>
      ))}
    </div>
  );
}

This component creates a responsive grid of event cards. It also applies some conditional styling:

  • Events less than a week away get a green background
  • Past events are hidden

Pretty neat, huh?

[Screenshot: EventList component showing event cards]

The iCal Feed: Sharing is Caring ๐Ÿค

Now for the piรจce de rรฉsistance - our iCal feed! Create a new file called route.ts in the app/api/ical directory:

import { NextResponse } from "next/server";
import { getEvents } from "@/lib/google-sheets";
import { generateICalFeed } from "@/lib/ical-generator";

export async function GET() {
  try {
    const events = await getEvents();
    const icalFeed = generateICalFeed(events);

    return new NextResponse(icalFeed, {
      status: 200,
      headers: {
        "Content-Type": "text/calendar",
        "Content-Disposition": 'attachment; filename="calendar.ics"',
      },
    });
  } catch (error) {
    console.error("Error generating iCal feed:", error);
    return new NextResponse("Error generating iCal feed", { status: 500 });
  }
}

This creates an API route that:

  1. Fetches events from our Google Sheet
  2. Generates an iCal feed
  3. Returns the feed as a downloadable file

The Missing Pieces ๐Ÿงฉ

Of course, we've glossed over a few important parts:

  • The getEvents function that fetches data from Google Sheets
  • The generateICalFeed function that creates our iCal feed
  • Utility functions like isLessThanWeekAway and isPast

These would be great topics for follow-up posts! (Wink, wink ๐Ÿ˜‰)

Wrapping Up ๐ŸŽ

And there you have it! We've built a simple yet powerful app that turns a Google Sheet into an iCal feed and displays events on a slick landing page. Here's what we've learned:

  1. Setting up a Next.js project with TypeScript and Tailwind CSS
  2. Creating a responsive layout with Next.js
  3. Fetching data from an external source (Google Sheets)
  4. Generating an iCal feed
  5. Creating API routes in Next.js

[Screenshot: Final app in action]

This project is just the tip of the iceberg. You could extend it by adding authentication, allowing users to add events through the UI, or even integrating with other calendar services.

I hope you found this tutorial helpful! Feel free to reach out if you have any questions or ideas for improvements. Happy coding! ๐Ÿš€๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘ฉโ€๐Ÿ’ป