DEV Community

Cover image for How I Built a Unified Calendar Dashboard with Next.js, Vercel Edge Functions & No Database
Ravgeet Dhillon
Ravgeet Dhillon

Posted on • Originally published at ravgeet.in

How I Built a Unified Calendar Dashboard with Next.js, Vercel Edge Functions & No Database

Problem

I was juggling tasks across:

  • Company ClickUp (for team collaboration)

  • Notion (for personal to-dos and planning)

  • Google Calendar (from both company & personal emails)

The chaos was real. I was missing due dates, spending too much time jumping between apps, and lacked a single place to glance at all my tasks.

The Solution

I built a read-only personal dashboard that:

  • Aggregates tasks/events from ClickUp, Notion, and Google Calendar

  • Groups tasks as Overdue, Due Today, Upcoming by Date, and No Due Date

  • Runs entirely on Next.js + Edge Functions

  • Uses no database, just live API reads

  • Stores my daily tasks that I need to do

  • Is password protected and deployed on a Vercel subdomain

Here’s how it looks after multiple polishes:

Tech Stack

  • Frontend: Next.js App Router + React Bootstrap + TanStack Query

  • API Layer: Vercel Edge Functions

  • Auth: Cookie-based with middleware protection

  • Hosting: Vercel subdomain

Core Features

Unified Task View

Each task is grouped into:

  • Overdue

  • Due Today / Tomorrow

  • Upcoming

  • No Due Date

It pulls data from these 3 APIs:

const [clickup, notion, calendar] = await Promise.all([
  fetchClickupTasks(),
  fetchNotionTasks(),
  fetchCalendarEvents(),
]);
Enter fullscreen mode Exit fullscreen mode

Auth with Middleware

I implemented simple cookie-based authentication to protect my dashboard. The middleware runs on every request and checks for a valid auth cookie before allowing access to protected routes.

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const auth = request.cookies.get("auth");
  const pathname = request.nextUrl.pathname;

  const publicPaths = ["/login", "/api/login", "/api/logout"];
  if (publicPaths.includes(pathname)) return NextResponse.next();
  if (auth?.value === "1") return NextResponse.next();

  if (pathname.startsWith("/api/")) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.redirect(new URL("/login", request.url));
}

export const config = {
  matcher: [
    "/",
    "/dashboard",
    "/api/events",
    "/api/clickup",
    "/api/notion",
    "/api/calendar",
  ],
};
Enter fullscreen mode Exit fullscreen mode

Modular API Fetchers

I kept the codebase clean by separating each API integration into its own module. This makes it easier to maintain and test each data source independently.

// lib/sources/clickup.ts
export async function fetchClickupTasks() {
  const res = await fetch("https://api.clickup.com/api/v2/...");
  return await res.json();
}
Enter fullscreen mode Exit fullscreen mode

The same goes for Notion & Google Calendar.

Server API Route Example

The backend API routes handle data aggregation from all sources. This route fetches tasks from all three platforms simultaneously and returns them as a unified JSON response.

// app/api/events/route.ts
import { getAllEvents } from "@/lib/getAllEvents";

export async function GET() {
  const { clickup, notion, calendar } = await getAllEvents();
  return Response.json({ clickup, notion, calendar });
}
Enter fullscreen mode Exit fullscreen mode

Frontend UI

The frontend uses TanStack Query for efficient data fetching with automatic caching and background updates. This ensures the dashboard stays responsive while keeping data fresh.

Using TanStack Query for live fetching and caching:

const { data, isLoading } = useQuery({
  queryKey: ["events"],
  queryFn: () => fetch("/api/events").then((res) => res.json()),
});
Enter fullscreen mode Exit fullscreen mode

Then we categorize tasks by due date:

const overdue = allTasks.filter(task => isBefore(task.dueDate, today));
const dueToday = allTasks.filter(task => isToday(task.dueDate));
const upcoming = groupByDate(allTasks.filter(...));
const noDueDate = allTasks.filter(task => !task.dueDate);
Enter fullscreen mode Exit fullscreen mode

The dashboard also includes a "Today's Work List" feature where I can curate specific tasks from across all platforms. This has become my morning ritual - selecting what I want to focus on for the day creates clarity and intention around my daily goals.

Deployment

I deployed the app to Vercel and created a subdomain via Hostinger by:

  1. Creating a subdomain DNS record

  2. Adding the domain to Vercel

  3. Setting env variables and password via the Vercel dashboard

No secrets or tasks are stored — it's 100% live.

What’s Next?

I'm happy with the current version, but I could add the following features in the future:

  • Month View toggle

  • Desktop notifications for overdue tasks

  • Auto-refresh every 10 mins

  • Tauri or Expo wrapper for mobile

Final Thoughts

This project helped me regain clarity over my weekly tasks. I have pinned this dashboard in my browser and open it every morning to immediately see what matters. It's fast, reliable, and mine.

If you're tired of hopping between 5 apps, build something simple that fits your brain.

Top comments (0)