Skip to content

Commit

Permalink
fix: add ISR support 60s revalidate
Browse files Browse the repository at this point in the history
Fix issue #59
ISR for people, projects, and single project pages.
  • Loading branch information
ZL-Asica committed Jan 30, 2025
1 parent 734e5ca commit 450fde3
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 113 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
"homepage": "https://github.com/NUDelta/dtr-web",
"scripts": {
"dev": "next dev --turbo",
"build": "NODE_NO_WARNINGS=1 next build",
"build": "pnpm lint:fix && NODE_NO_WARNINGS=1 next build",
"start": "NODE_NO_WARNINGS=1 next start -H 0.0.0.0 -p ${PORT:-8080}",
"lint": "next lint",
"lint:fix": "next lint --fix"
"lint:fix": "next lint --fix",
"prepare": "husky"
},
"dependencies": {
"airtable": "^0.12.2",
Expand Down
5 changes: 4 additions & 1 deletion src/app/people/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Metadata } from 'next';
import PeopleProfiles from '@/components/people/PeopleProfiles';
import { fetchPeople, sortPeople } from '@/lib/people';
import { fetchPeople } from '@/lib/people';
import { sortPeople } from '@/utils';

export const revalidate = 600;

export const metadata: Metadata = {
title: 'People | DTR',
Expand Down
14 changes: 12 additions & 2 deletions src/app/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ import TeamMembers from '@/components/people/TeamMembers';
import ProjectVideo from '@/components/projects/ProjectVideo';
import { getCachedRecords } from '@/lib/airtable';
import { getProject } from '@/lib/project';
import { notFound } from 'next/navigation';
import ReactMarkdown from 'react-markdown';

// Next.js will invalidate the cache when a
// request comes in, at most once every 60 seconds.
export const revalidate = 60;

// We'll prerender only the params from `generateStaticParams` at build time.
// If a request comes in for a path that hasn't been generated,
// Next.js will server-render the page on-demand.
export const dynamicParams = true; // or false, to 404 on unknown paths

export async function generateStaticParams() {
const projects = await getCachedRecords('Projects');
return projects.map(project => ({
params: { id: project.id },
id: project.id,
}));
}

Expand Down Expand Up @@ -46,7 +56,7 @@ export default async function IndividualProjectPage({

if (!project) {
console.warn(`Project ${id} not found`);
return <div>Project not found</div>;
notFound();
}

return (
Expand Down
2 changes: 2 additions & 0 deletions src/app/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { fetchSigs } from '@/lib/sig';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';

export const revalidate = 60;

export const metadata: Metadata = {
title: 'Projects | DTR',
alternates: { canonical: 'https://dtr.northwestern.edu/projects' },
Expand Down
39 changes: 15 additions & 24 deletions src/lib/airtable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Attachment } from 'airtable';
'use server';

import process from 'node:process';
import Airtable from 'airtable';
import { unstable_cache } from 'next/cache';
Expand All @@ -9,31 +10,21 @@ Airtable.configure({
apiKey: process.env.AIRTABLE_API_KEY ?? '',
});

export const base = Airtable.base(process.env.AIRTABLE_BASE_ID ?? '');
const base = Airtable.base(process.env.AIRTABLE_BASE_ID ?? '');

export async function fetchAirtableRecords(
tableName: string,
): Promise<{ id: string; fields: Record<string, unknown> }[]> {
// eslint-disable-next-line no-console
console.log(`[ISR TEST] Fetching data from Airtable: ${tableName} at ${new Date().toISOString()}`);

export async function fetchAirtableRecords(tableName: string) {
const records = await base(tableName).select().all();
return records.map(record => ({ id: record.id, fields: record.fields }));
}

export const getCachedRecords = unstable_cache(
async (tableName: string) => fetchAirtableRecords(tableName),
['airtable'],
{ revalidate: revalidateTime },
);

/**
* Extracts the first image URL from Airtable's attachment array.
*
* @param {Attachment[] | undefined} attachmentArr - Array of Airtable Attachments, or undefined.
* @returns {string | null} The first image URL if found, otherwise `null`.
*
* @example
* const imgUrl = getImgUrlFromAttachmentObj(record.fields.profile_photo);
* console.log(imgUrl); // "https://dl.airtable.com/..."
*/
export function getImgUrlFromAttachmentObj(attachmentArr?: Attachment[]): string | null {
// Ensure the array is not empty and has at least one image attachment
const targetImg = attachmentArr?.[0];
return targetImg?.type.includes('image') ? targetImg.url : null;
}
export const getCachedRecords = async (tableName: string) =>
unstable_cache(
async () => fetchAirtableRecords(tableName),
[`airtable-${tableName}`],
{ revalidate: revalidateTime },
)();
84 changes: 4 additions & 80 deletions src/lib/people.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use server';

import type { Attachment } from 'airtable';
import { getCachedRecords, getImgUrlFromAttachmentObj } from './airtable';
import { getImgUrlFromAttachmentObj } from '@/utils';
import { getCachedRecords } from './airtable';

/**
* Fetches all people data from the Airtable "People" table.
Expand Down Expand Up @@ -34,82 +37,3 @@ export async function fetchPeople(): Promise<Person[] | null> {
profile_photo: getImgUrlFromAttachmentObj(fields.profile_photo as Attachment[]),
}));
}

/**
* Sorts an array of people based on status and role.
* People are categorized into:
* - Active members
* - Alumni
* - Special case: "Stella"
*
* Within each category, people are sorted by:
* - Faculty (predefined order)
* - Ph.D. Candidates → Ph.D. Students (sorted alphabetically)
* - Master's students → Undergraduate students (sorted alphabetically)
*
* @param {Person[]} people - The array of people to sort.
* @returns {Person[]} The sorted array of people.
*
* @example
* const sortedPeople = sortPeople(peopleArray);
* console.log(sortedPeople);
*/
export function sortPeople(people: Person[]): Person[] {
// Predefined faculty sorting order
const facultyOrder = new Map([
['Haoqi Zhang', 1],
['Eleanor "Nell" O\'Rourke', 2],
['Matt Easterday', 3],
['Liz Gerber', 4],
]);

// Predefined Ph.D. role order
const phdOrder = new Map([
['Ph.D. Candidate', 1],
['Ph.D. Student', 2],
]);

// Categorize people
const categories = {
active: [] as Person[],
alumni: [] as Person[],
stella: [] as Person[],
};

people.forEach((person) => {
if (person.name === 'Stella') {
categories.stella.push(person);
}
else if (person.status === 'Active') {
categories.active.push(person);
}
else if (person.status === 'Alumni') {
categories.alumni.push(person);
}
});

// Sorting function by role
const sortByRole = (people: Person[]): Person[] => {
// Sort faculty
const faculty = people.filter(p => p.role === 'Faculty').sort(
(a, b) => (facultyOrder.get(a.name) ?? Infinity) - (facultyOrder.get(b.name) ?? Infinity),
);

// Sort Ph.D. candidates and students
const phd = people.filter(p => phdOrder.has(p.role)).sort(
(a, b) => (phdOrder.get(a.role) ?? Infinity) - (phdOrder.get(b.role) ?? Infinity)
|| a.name.localeCompare(b.name),
);

// Sort Master's and Undergraduate students
const masters = people.filter(p => p.role === 'Masters Student Researcher')
.sort((a, b) => a.name.localeCompare(b.name));

const ugrads = people.filter(p => p.role === 'Undergraduate Student Researcher')
.sort((a, b) => a.name.localeCompare(b.name));

return [...faculty, ...phd, ...masters, ...ugrads];
};

return [...sortByRole(categories.active), ...categories.stella, ...sortByRole(categories.alumni)];
}
7 changes: 5 additions & 2 deletions src/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use server';

import type { Attachment } from 'airtable';
import { getCachedRecords, getImgUrlFromAttachmentObj } from './airtable';
import { fetchPeople, sortPeople } from './people';
import { getImgUrlFromAttachmentObj, sortPeople } from '@/utils';
import { getCachedRecords } from './airtable';
import { fetchPeople } from './people';

/**
* Retrieves detailed project information from Airtable.
Expand Down
7 changes: 5 additions & 2 deletions src/lib/sig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use server';

import type { Attachment } from 'airtable';
import { getCachedRecords, getImgUrlFromAttachmentObj } from './airtable';
import { fetchPeople, sortPeople } from './people';
import { getImgUrlFromAttachmentObj, sortPeople } from '@/utils';
import { getCachedRecords } from './airtable';
import { fetchPeople } from './people';
import { getProject } from './project';

/**
Expand Down
17 changes: 17 additions & 0 deletions src/utils/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Attachment } from 'airtable';

/**
* Extracts the first image URL from Airtable's attachment array.
*
* @param {Attachment[] | undefined} attachmentArr - Array of Airtable Attachments, or undefined.
* @returns {string | null} The first image URL if found, otherwise `null`.
*
* @example
* const imgUrl = getImgUrlFromAttachmentObj(record.fields.profile_photo);
* console.log(imgUrl); // "https://dl.airtable.com/..."
*/
export function getImgUrlFromAttachmentObj(attachmentArr?: Attachment[]): string | null {
// Ensure the array is not empty and has at least one image attachment
const targetImg = attachmentArr?.[0];
return targetImg?.type.includes('image') ? targetImg.url : null;
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { getImgUrlFromAttachmentObj } from './image';
export { sortPeople } from './people';
78 changes: 78 additions & 0 deletions src/utils/people.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Sorts an array of people based on status and role.
* People are categorized into:
* - Active members
* - Alumni
* - Special case: "Stella"
*
* Within each category, people are sorted by:
* - Faculty (predefined order)
* - Ph.D. Candidates → Ph.D. Students (sorted alphabetically)
* - Master's students → Undergraduate students (sorted alphabetically)
*
* @param {Person[]} people - The array of people to sort.
* @returns {Person[]} The sorted array of people.
*
* @example
* const sortedPeople = sortPeople(peopleArray);
* console.log(sortedPeople);
*/
export function sortPeople(people: Person[]): Person[] {
// Predefined faculty sorting order
const facultyOrder = new Map([
['Haoqi Zhang', 1],
['Eleanor "Nell" O\'Rourke', 2],
['Matt Easterday', 3],
['Liz Gerber', 4],
]);

// Predefined Ph.D. role order
const phdOrder = new Map([
['Ph.D. Candidate', 1],
['Ph.D. Student', 2],
]);

// Categorize people
const categories = {
active: [] as Person[],
alumni: [] as Person[],
stella: [] as Person[],
};

people.forEach((person) => {
if (person.name === 'Stella') {
categories.stella.push(person);
}
else if (person.status === 'Active') {
categories.active.push(person);
}
else if (person.status === 'Alumni') {
categories.alumni.push(person);
}
});

// Sorting function by role
const sortByRole = (people: Person[]): Person[] => {
// Sort faculty
const faculty = people.filter(p => p.role === 'Faculty').sort(
(a, b) => (facultyOrder.get(a.name) ?? Infinity) - (facultyOrder.get(b.name) ?? Infinity),
);

// Sort Ph.D. candidates and students
const phd = people.filter(p => phdOrder.has(p.role)).sort(
(a, b) => (phdOrder.get(a.role) ?? Infinity) - (phdOrder.get(b.role) ?? Infinity)
|| a.name.localeCompare(b.name),
);

// Sort Master's and Undergraduate students
const masters = people.filter(p => p.role === 'Masters Student Researcher')
.sort((a, b) => a.name.localeCompare(b.name));

const ugrads = people.filter(p => p.role === 'Undergraduate Student Researcher')
.sort((a, b) => a.name.localeCompare(b.name));

return [...faculty, ...phd, ...masters, ...ugrads];
};

return [...sortByRole(categories.active), ...categories.stella, ...sortByRole(categories.alumni)];
}

0 comments on commit 450fde3

Please sign in to comment.