Skip to content

Commit 0da498d

Browse files
feat: fetch calendar events from backend API instead of Google Sheets (#169)
* feat: fetch calendar events from backend API instead of Google Sheets * feat: consolidate to single BACKEND_URL env var and add push-based calendar revalidation * switch to BACKEND_URL
1 parent 58ce9f7 commit 0da498d

5 files changed

Lines changed: 54 additions & 95 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ Create a `.env.local` file in the root directory:
6868

6969
```env
7070
NEXT_PUBLIC_SITE_URL=http://localhost:3000
71+
BACKEND_URL=http://localhost:8080
72+
BACKEND_SECRET_KEY=replace-me
73+
REVALIDATION_SECRET=replace-me
7174
7275
# PostHog OpenTelemetry logs
7376
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxx

contributing.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ Create a `.env.local` file in the root directory:
6868

6969
```env
7070
NEXT_PUBLIC_SITE_URL=http://localhost:3000
71-
# Add other environment variables as needed
71+
BACKEND_URL=http://localhost:8080
72+
BACKEND_SECRET_KEY=replace-me
73+
REVALIDATION_SECRET=replace-me
7274
```
7375

7476
---

src/app/api/revalidate/route.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { revalidateTag } from "next/cache";
2+
import { NextResponse } from "next/server";
3+
4+
export async function POST(request: Request) {
5+
try {
6+
const body = (await request.json()) as { secret?: string };
7+
const secret = process.env.REVALIDATION_SECRET;
8+
9+
if (!secret || body.secret !== secret) {
10+
return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
11+
}
12+
13+
revalidateTag("calendar");
14+
15+
return NextResponse.json({ revalidated: true });
16+
} catch (error) {
17+
console.error("Revalidation error:", error);
18+
return NextResponse.json(
19+
{ error: "Failed to revalidate" },
20+
{ status: 500 },
21+
);
22+
}
23+
}

src/lib/form-submission.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
"use server";
2+
import { SeverityNumber } from "@opentelemetry/api-logs";
23
import type {
34
CollegeFormValues,
45
EventSubmissionFormValues,
56
IndividualFormValues,
67
} from "@/lib/forms-config";
7-
import { SeverityNumber } from "@opentelemetry/api-logs";
88
import { emitServerLog, flushOtelLogs } from "@/lib/otel-logger";
99

10-
const WEBHOOK_URL = process.env.WEBHOOK_URL;
10+
const BACKEND_URL = process.env.BACKEND_URL;
1111

1212
export async function submitFormData(
1313
name: "individual" | "community" | "event",
1414
data: IndividualFormValues | CollegeFormValues | EventSubmissionFormValues,
1515
) {
1616
try {
17-
if (!WEBHOOK_URL || !process.env.WEBHOOK_SECRET_KEY)
18-
throw new Error("WEBHOOK_URL not set");
17+
if (!BACKEND_URL || !process.env.BACKEND_SECRET_KEY)
18+
throw new Error("BACKEND_URL not set");
1919
// if (!doc) throw new Error("Doc not found!");
2020
// await doc.loadInfo();
2121

@@ -44,11 +44,11 @@ export async function submitFormData(
4444
// "──────────────────────────────",
4545
// };
4646

47-
const res = await fetch(`${WEBHOOK_URL}new-applicant`, {
47+
const res = await fetch(`${BACKEND_URL}/api/new-applicant`, {
4848
method: "POST",
4949
headers: {
5050
"Content-Type": "application/json",
51-
"X-Secret-Key": process.env.WEBHOOK_SECRET_KEY,
51+
"X-Secret-Key": process.env.BACKEND_SECRET_KEY,
5252
},
5353
body: JSON.stringify(data),
5454
});
@@ -73,11 +73,11 @@ export async function submitFormData(
7373
// "──────────────────────────────",
7474
// };
7575

76-
const res = await fetch(`${WEBHOOK_URL}new-college-applicant`, {
76+
const res = await fetch(`${BACKEND_URL}/api/new-college-applicant`, {
7777
method: "POST",
7878
headers: {
7979
"Content-Type": "application/json",
80-
"X-Secret-Key": process.env.WEBHOOK_SECRET_KEY,
80+
"X-Secret-Key": process.env.BACKEND_SECRET_KEY,
8181
},
8282
body: JSON.stringify(data),
8383
});
@@ -86,11 +86,11 @@ export async function submitFormData(
8686
throw new Error("Error submitting to webhook");
8787
}
8888
} else if (name === "event") {
89-
const res = await fetch(`${WEBHOOK_URL}new-event`, {
89+
const res = await fetch(`${BACKEND_URL}/api/new-event`, {
9090
method: "POST",
9191
headers: {
9292
"Content-Type": "application/json",
93-
"X-Secret-Key": process.env.WEBHOOK_SECRET_KEY,
93+
"X-Secret-Key": process.env.BACKEND_SECRET_KEY,
9494
},
9595
body: JSON.stringify(data),
9696
});

src/lib/get-events.ts

Lines changed: 15 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,10 @@
33
import { promises as fs } from "node:fs";
44
import path from "node:path";
55
import { endOfMonth } from "date-fns";
6-
import { JWT } from "google-auth-library";
7-
import { GoogleSpreadsheet } from "google-spreadsheet";
8-
import type { iconsMap } from "@/constants";
9-
import type { EventSubmissionFormValues } from "@/lib/forms-config";
10-
import type { IEvent, TEventColor } from "@/types";
11-
12-
const serviceAccountAuth = new JWT({
13-
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
14-
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
15-
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
16-
});
17-
18-
const doc = new GoogleSpreadsheet(
19-
process.env.GOOGLE_SHEET_ID ?? "",
20-
serviceAccountAuth,
21-
);
6+
import type { IEvent } from "@/types";
227

238
const localEventsPath = path.join(process.cwd(), "src/data/local-events.json");
9+
const backendUrl = process.env.BACKEND_URL;
2410

2511
async function readLocalEvents(): Promise<IEvent[]> {
2612
try {
@@ -63,80 +49,25 @@ export async function getEvents(
6349
return filterEventsByRange(localEvents, startDate, endDate);
6450
}
6551

66-
if (
67-
!process.env.GOOGLE_SHEET_ID ||
68-
!process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL ||
69-
!process.env.GOOGLE_PRIVATE_KEY
70-
) {
71-
console.warn("getEvents: Missing Google Sheets configuration.");
52+
if (!backendUrl) {
53+
console.warn("getEvents: Missing BACKEND_URL configuration.");
7254
return [];
7355
}
7456

7557
try {
76-
await doc.loadInfo();
77-
const sheet = doc.sheetsByIndex[3];
78-
if (!sheet) throw new Error("Sheet not found");
79-
80-
const rows = await sheet.getRows();
81-
const res: IEvent[] = rows
82-
.map((row): IEvent | null => {
83-
const eventData =
84-
row.toObject() as unknown as EventSubmissionFormValues & {
85-
ID: string;
86-
};
87-
const isDK24 = eventData.organizationName === "DK24";
88-
89-
const startDateTime = new Date(eventData.startDateTime);
90-
const endDateTime = new Date(eventData.endDateTime);
91-
92-
if (
93-
Number.isNaN(startDateTime.getTime()) ||
94-
Number.isNaN(endDateTime.getTime())
95-
) {
96-
return null;
97-
}
98-
99-
return {
100-
id: eventData.ID,
101-
title: eventData.eventName,
102-
startDateTime: startDateTime.toISOString(),
103-
endDateTime: endDateTime.toISOString(),
104-
time: eventData.startDateTime.split("T")[1]?.substring(0, 5) || "",
105-
location: eventData.eventLocation,
106-
description: eventData.eventDescription,
107-
registrationLink: eventData.registrationLink || "",
108-
joinLink: eventData.eventWebsite || "",
109-
icon: "calendar" as keyof typeof iconsMap,
110-
highlight: isDK24,
111-
youtubeLink: "",
112-
color: (isDK24 ? "green" : "gray") as TEventColor,
113-
organizationName: eventData.organizationName,
114-
posterUrl: eventData.eventPosterUrl,
115-
tags: (() => {
116-
const raw = eventData.eventTags as unknown;
117-
if (Array.isArray(raw)) return raw as string[];
118-
if (typeof raw === "string") {
119-
let s = (raw as string).trim();
120-
if (s.startsWith("[") && s.endsWith("]")) {
121-
s = s.slice(1, -1);
122-
}
123-
return s
124-
.split(",")
125-
.map((t: string) => t.replace(/^['"]|['"]$/g, "").trim())
126-
.filter(Boolean) as string[];
127-
}
128-
return [];
129-
})(),
130-
};
131-
})
132-
.filter((event): event is IEvent => event !== null);
58+
const response = await fetch(`${backendUrl}/api/events`, {
59+
next: { revalidate: 300, tags: ["calendar"] },
60+
});
61+
62+
if (!response.ok) {
63+
throw new Error(`Calendar API request failed with ${response.status}`);
64+
}
13365

134-
return filterEventsByRange(res, startDate, endDate);
66+
const data = (await response.json()) as { events?: IEvent[] };
67+
const events = Array.isArray(data.events) ? data.events : [];
68+
return filterEventsByRange(events, startDate, endDate);
13569
} catch (error) {
136-
console.error(
137-
"getEvents: Failed to load events from Google Sheets.",
138-
error,
139-
);
70+
console.error("getEvents: Failed to load events from Calendar API.", error);
14071
return [];
14172
}
14273
}

0 commit comments

Comments
 (0)