Skip to content

Commit 7fc82b4

Browse files
committed
Merge branch 'main' of github.com:efdevcon/monorepo
2 parents 68ae558 + ee0c605 commit 7fc82b4

5 files changed

Lines changed: 234 additions & 58 deletions

File tree

devconnect-app/src/app/api/data/README.md

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
`GET /api/data`
55

66
## Description
7-
Fetches supporter and POI (Point of Interest) data from a Notion database. The data is automatically categorized based on the `Is POI` checkbox field.
7+
Fetches supporter and POI (Point of Interest) data from a Notion database. The data is automatically categorized based on the `POI` select field (previously was boolean `Is POI`).
88

99
## Response Format
1010

@@ -15,29 +15,29 @@ Fetches supporter and POI (Point of Interest) data from a Notion database. The d
1515
"supporters": [
1616
{
1717
"name": "Espresso",
18-
"layerNaming": "l2s/espresso",
19-
"districtId": 1,
20-
"locationId": 1
18+
"layerName": "l2s/espresso",
19+
"districtId": "1",
20+
"locationId": "1"
2121
}
2222
],
2323
"pois": [
2424
{
2525
"name": "Help desk 1",
26-
"layerNaming": "arts/help-desk-1",
27-
"districtId": 3,
28-
"locationId": 3
26+
"layerName": "arts/help-desk-1",
27+
"districtId": "3",
28+
"locationId": "3"
2929
}
3030
],
31-
"districts": [
32-
{ "id": 1, "name": "L2s" },
33-
{ "id": 2, "name": "Social" },
34-
{ "id": 3, "name": "Hardware & Wallets" }
35-
],
36-
"locations": [
37-
{ "id": 1, "name": "Pista Central" },
38-
{ "id": 2, "name": "Green Pavilion" },
39-
{ "id": 3, "name": "Pavilion 9" }
40-
]
31+
"districts": {
32+
"1": { "name": "L2s" },
33+
"2": { "name": "Social" },
34+
"3": { "name": "Hardware & Wallets" }
35+
},
36+
"locations": {
37+
"1": { "name": "Pista Central" },
38+
"2": { "name": "Green Pavilion" },
39+
"3": { "name": "Pavilion 9" }
40+
}
4141
},
4242
"timestamp": "2025-09-09T17:59:05.480Z"
4343
}
@@ -46,24 +46,27 @@ Fetches supporter and POI (Point of Interest) data from a Notion database. The d
4646
## Data Structure
4747

4848
### Supporters
49-
Array of supporter objects where `Is POI` is `false`. Each supporter contains:
49+
Array of supporter objects where `POI` field is empty. Each supporter contains:
5050
- `name` (string): Supporter name
51-
- `layerNaming` (string): Layer naming convention
52-
- `districtId` (number|null): Reference to districts array ID
53-
- `locationId` (number|null): Reference to locations array ID
51+
- `layerName` (string): Layer naming convention
52+
- `districtId` (string|null): Reference to districts object key
53+
- `locationId` (string|null): Reference to locations object key
5454

5555
### POIs (Points of Interest)
56-
Array of POI objects where `Is POI` is `true`. Each POI contains the same fields as supporters.
56+
Array of POI objects where `POI` field has a value. Each POI contains the same fields as supporters.
5757

5858
### Districts
59-
Array of unique district objects with:
60-
- `id` (number): Sequential ID starting from 1
61-
- `name` (string): District name
59+
Object of unique district objects with numeric string keys:
60+
61+
- Key (string): Sequential ID starting from "1"
62+
- Value: Object with `name` (string): District name
6263

6364
### Locations
64-
Array of unique location objects with:
65-
- `id` (number): Sequential ID starting from 1
66-
- `name` (string): Location name
65+
66+
Object of unique location objects with numeric string keys:
67+
68+
- Key (string): Sequential ID starting from "1"
69+
- Value: Object with `name` (string): Location name
6770

6871
## Supported Property Types
6972
- `title` - Page titles

devconnect-app/src/app/api/data/route.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { Client } from '@notionhq/client';
44
/**
55
* API endpoint to fetch data
66
* GET /api/data
7-
*
7+
*
88
* Attempts to fetch from Notion database, falls back to sample data on error
9+
* POI field is now a select type indicating the type of POI (previously was boolean isPOI)
910
*/
1011
export async function GET(request: NextRequest) {
1112
try {
@@ -87,30 +88,39 @@ export async function GET(request: NextRequest) {
8788
name: getPropertyValue('Supporter Name'),
8889
district: getPropertyValue('District'),
8990
location: getPropertyValue('Location'),
90-
id: getPropertyValue('Layer naming'),
91-
isPOI: getPropertyValue('Is POI'),
91+
layerName: getPropertyValue('Layer name'),
92+
POI: getPropertyValue('POI'),
9293
};
9394
});
9495

95-
const supporters = data.filter(item => item.isPOI === 'false');
96-
const pois = data.filter(item => item.isPOI === 'true');
96+
// Filter out entries with empty Supporter Name
97+
const filteredData = data.filter(item => item.name && item.name.trim() !== '');
9798

98-
// remove isPOI property from result
99-
const cleanSupporters = supporters.map(item => { delete item.isPOI; return item; });
100-
const cleanPois = pois.map(item => { delete item.isPOI; return item; });
99+
const supporters = filteredData.filter(item => !item.POI || item.POI.trim() === '');
100+
const pois = filteredData.filter(item => item.POI && item.POI.trim() !== '');
101101

102-
// Get unique districts and locations with IDs
103-
const uniqueDistricts = [...new Set(data.map(item => item.district).filter(Boolean))]
104-
.map((district, index) => ({ id: index + 1, name: district }));
105-
106-
const uniqueLocations = [...new Set(data.map(item => item.location).filter(Boolean))]
107-
.map((location, index) => ({ id: index + 1, name: location }));
102+
// remove POI property from result (was previously isPOI boolean)
103+
const cleanSupporters = supporters.map(item => { delete item.POI; return item; });
104+
const cleanPois = pois.map(item => { delete item.POI; return item; });
105+
106+
// Get unique districts and locations as objects with numeric ID as key
107+
const uniqueDistrictsArray = [...new Set(filteredData.map(item => item.district).filter(Boolean))];
108+
const uniqueDistricts = uniqueDistrictsArray.reduce((acc, district, index) => {
109+
acc[(index + 1).toString()] = { name: district };
110+
return acc;
111+
}, {} as Record<string, { name: string }>);
112+
113+
const uniqueLocationsArray = [...new Set(filteredData.map(item => item.location).filter(Boolean))];
114+
const uniqueLocations = uniqueLocationsArray.reduce((acc, location, index) => {
115+
acc[(index + 1).toString()] = { name: location };
116+
return acc;
117+
}, {} as Record<string, { name: string }>);
108118

109-
// Create lookup maps for district and location IDs
110-
const districtMap = new Map(uniqueDistricts.map(d => [d.name, d.id]));
111-
const locationMap = new Map(uniqueLocations.map(l => [l.name, l.id]));
119+
// Create lookup maps for district and location (name -> numeric ID)
120+
const districtMap = new Map(uniqueDistrictsArray.map((name, index) => [name, (index + 1).toString()]));
121+
const locationMap = new Map(uniqueLocationsArray.map((name, index) => [name, (index + 1).toString()]));
112122

113-
// Replace district and location names with IDs in supporters and POIs
123+
// Replace district and location names with their numeric IDs in supporters and POIs
114124
const supportersWithIds = cleanSupporters.map(item => {
115125
const { district, location, ...rest } = item;
116126
return {

devconnect/src/pages/api/notion/[...id].ts

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function extractFieldName(propertyName: string): { name: string | null; mode: 'e
3838
}
3939

4040
// Helper function to determine field type from Notion property
41-
function getFieldType(property: any): 'text' | 'email' | 'file' | 'url' | 'title' | 'select' | 'status' | 'checkbox' | 'formula' | null {
41+
function getFieldType(property: any): 'text' | 'email' | 'file' | 'url' | 'title' | 'select' | 'status' | 'checkbox' | 'formula' | 'rollup' | null {
4242
switch (property.type) {
4343
case 'rich_text':
4444
return 'text';
@@ -58,8 +58,9 @@ function getFieldType(property: any): 'text' | 'email' | 'file' | 'url' | 'title
5858
return 'checkbox';
5959
case 'formula':
6060
return 'formula';
61+
case 'rollup':
62+
return 'rollup';
6163
default:
62-
console.log(`Unsupported property type: ${property.type}`);
6364
return null;
6465
}
6566
}
@@ -84,6 +85,39 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
8485
}
8586
}
8687

88+
89+
// Helper function to fetch supporter data from rollup
90+
async function fetchSupporterFromRollup(pageProperties: any, notion: Client): Promise<string> {
91+
// Look for "Parent item" relation in the current page properties
92+
const parentRelation = pageProperties["Parent item"];
93+
if (parentRelation && parentRelation.type === 'relation' && parentRelation.relation && parentRelation.relation.length > 0) {
94+
const parentPageId = parentRelation.relation[0].id;
95+
96+
try {
97+
const parentPage = await notion.pages.retrieve({ page_id: parentPageId });
98+
const parentProperties = (parentPage as any).properties;
99+
100+
// Look for "Supporters Tracker" in parent page
101+
const supportersTracker = parentProperties["Supporters Tracker"];
102+
if (supportersTracker && supportersTracker.relation && supportersTracker.relation.length > 0) {
103+
const supporterPageId = supportersTracker.relation[0].id;
104+
const supporterPage = await notion.pages.retrieve({ page_id: supporterPageId });
105+
const supporterData = supporterPage as any;
106+
107+
// Extract supporter name/title like in organization API
108+
return supporterData.properties?.['Supporter Name']?.title?.[0]?.plain_text ||
109+
supporterData.properties?.Name?.title?.[0]?.plain_text ||
110+
supporterData.properties?.Title?.title?.[0]?.plain_text ||
111+
'Unknown Supporter';
112+
}
113+
} catch (error) {
114+
console.error('Failed to fetch supporter data from rollup:', error);
115+
}
116+
}
117+
118+
return '';
119+
}
120+
87121
// GET: Fetch page data for the given id
88122
async function handleGet(req: NextApiRequest, res: NextApiResponse, pageId: string) {
89123
const notion = new Client({ auth: process.env.NOTION_SECRET });
@@ -94,7 +128,8 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse, pageId: stri
94128
if (page.object !== 'page' || !('properties' in page)) {
95129
return res.status(400).json({ error: 'Invalid page object' });
96130
}
97-
131+
132+
98133
// Get database schema to access field descriptions
99134
let databaseSchema: any = null;
100135
if (page.parent && page.parent.type === 'database_id') {
@@ -109,7 +144,7 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse, pageId: stri
109144
const fields: Array<{
110145
name: string;
111146
value: string;
112-
type: 'text' | 'email' | 'file' | 'url' | 'title' | 'select' | 'status' | 'checkbox' | 'formula';
147+
type: 'text' | 'email' | 'file' | 'url' | 'title' | 'select' | 'status' | 'checkbox' | 'formula' | 'rollup';
113148
mode: 'edit' | 'read';
114149
order: number;
115150
description?: string;
@@ -172,6 +207,15 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse, pageId: stri
172207
}
173208
}
174209
break;
210+
case 'rollup':
211+
if (propertyAny.type === 'rollup') {
212+
// Handle rollup fields - they are read-only calculated fields
213+
fieldValue = '[Calculated Field]';
214+
}
215+
break;
216+
default:
217+
fieldValue = '[UNSUPPORTED TYPE]';
218+
break;
175219
}
176220

177221
configFields.push({ name: fieldName, value: fieldValue, order });
@@ -256,6 +300,63 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse, pageId: stri
256300
}
257301
}
258302
break;
303+
case 'rollup':
304+
if (propertyAny.type === 'rollup') {
305+
// Handle rollup fields - fetch supporter data if rollup is empty
306+
const rollupResult = propertyAny.rollup;
307+
308+
if (rollupResult && rollupResult.type === 'array') {
309+
const arrayValues = rollupResult.array || [];
310+
311+
// Check if rollup is empty but we have a parent with Supporters Tracker
312+
if (arrayValues.length === 0 || (arrayValues.length === 1 && arrayValues[0].type === 'relation' && arrayValues[0].relation.length === 0)) {
313+
// Try to fetch supporter data from parent page
314+
const supporterName = await fetchSupporterFromRollup(page.properties, notion);
315+
fieldValue = supporterName || '[Calculated Field]';
316+
} else {
317+
// Process normal rollup array
318+
const processedValues = arrayValues.map((item: any) => {
319+
if (item.type === 'select' && item.select) {
320+
return item.select.name;
321+
} else if (item.type === 'status' && item.status) {
322+
return item.status.name;
323+
} else if (item.type === 'title' && item.title) {
324+
return item.title[0]?.plain_text || '';
325+
} else if (item.type === 'rich_text' && item.rich_text) {
326+
return item.rich_text[0]?.plain_text || '';
327+
} else if (item.type === 'number' && item.number !== undefined) {
328+
return item.number.toString();
329+
} else if (item.type === 'checkbox') {
330+
return item.checkbox ? 'true' : 'false';
331+
} else if (item.type === 'date' && item.date) {
332+
return item.date.start || '';
333+
}
334+
return '';
335+
}).filter(Boolean);
336+
337+
fieldValue = processedValues.join(', ');
338+
}
339+
} else if (rollupResult) {
340+
// Handle other rollup types
341+
if (rollupResult.type === 'string') {
342+
fieldValue = rollupResult.string || '';
343+
} else if (rollupResult.type === 'number') {
344+
fieldValue = rollupResult.number?.toString() || '';
345+
} else if (rollupResult.type === 'boolean') {
346+
fieldValue = rollupResult.boolean ? 'true' : 'false';
347+
} else if (rollupResult.type === 'date') {
348+
fieldValue = rollupResult.date?.start || '';
349+
} else {
350+
fieldValue = '[Calculated Field]';
351+
}
352+
} else {
353+
fieldValue = '[Calculated Field]';
354+
}
355+
}
356+
break;
357+
default:
358+
fieldValue = '[UNSUPPORTED TYPE]';
359+
break;
259360
}
260361

261362
// Extract description and options from database schema if available
@@ -299,9 +400,10 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse, pageId: stri
299400
});
300401
}
301402

403+
302404
// Sort fields by order
303405
fields.sort((a, b) => a.order - b.order);
304-
406+
305407
// Return flat array of fields with config information
306408
return res.status(200).json({
307409
fields,

0 commit comments

Comments
 (0)