From be1f45af249a41de2bc43d1a37c40e4899e021d1 Mon Sep 17 00:00:00 2001 From: Juan M Date: Sat, 6 Apr 2024 06:59:43 -0300 Subject: [PATCH 1/5] Rename functions --- functions/{renderEvent.js => event.js} | 0 functions/{renderEvents.js => events.js} | 0 public/_redirects | 10 +++++----- 3 files changed, 5 insertions(+), 5 deletions(-) rename functions/{renderEvent.js => event.js} (100%) rename functions/{renderEvents.js => events.js} (100%) diff --git a/functions/renderEvent.js b/functions/event.js similarity index 100% rename from functions/renderEvent.js rename to functions/event.js diff --git a/functions/renderEvents.js b/functions/events.js similarity index 100% rename from functions/renderEvents.js rename to functions/events.js diff --git a/public/_redirects b/public/_redirects index 1d3b858..4d5d5a6 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,5 +1,5 @@ -/event/* /.netlify/functions/renderEvent/:route 200! -/events/* /.netlify/functions/renderEvents/:route 200! -/faq https://poap.notion.site/POAP-Family-FAQ-cef29bc0bb8c4f8f936164d988a944cc 301 -/help https://poap.zendesk.com/hc/en-us/articles/24008770288909-How-to-navigate-POAP-Family 301 -/* /index.html 200 +/event/* /.netlify/functions/event/:route 200! +/events/* /.netlify/functions/events/:route 200! +/faq https://poap.notion.site/POAP-Family-FAQ-cef29bc0bb8c4f8f936164d988a944cc 301 +/help https://poap.zendesk.com/hc/en-us/articles/24008770288909-How-to-navigate-POAP-Family 301 +/* /index.html 200 From 96bcb62def314cc7cc71563f5953ba598a85b9df Mon Sep 17 00:00:00 2001 From: Juan M Date: Sat, 6 Apr 2024 07:00:08 -0300 Subject: [PATCH 2/5] Make them Netlify functions instead of Lambda compatible --- functions/event.js | 32 ++++++++++++++++---------------- functions/events.js | 31 +++++++++++++++---------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/functions/event.js b/functions/event.js index a732454..cdf19e2 100644 --- a/functions/event.js +++ b/functions/event.js @@ -25,16 +25,16 @@ async function getEventInfo(eventId) { } } -exports.handler = async function(request, context, callback) { +exports.defaults = async function(request, context) { const eventId = request.path.split('/').pop() if (!IS_BOT.test(request.headers['user-agent'])) { - return { - statusCode: 301, + return new Response(null, { + status: 301, headers: { location: `${FAMILY_URL}/r/event/${eventId}`, }, - } + }) } let event, supply, emailReservations @@ -45,15 +45,16 @@ exports.handler = async function(request, context, callback) { emailReservations = eventInfo.emailReservations } catch (err) { if (err?.response?.status === 404) { - return { statusCode: 404 } + return new Response(null, { status: 404 }) } console.error('Fetch event info failed', err) - return { statusCode: 503 } + return new Response(null, { status: 503 }) } - return { - statusCode: 200, - body: ` + const description = `[ ${supply} + ${emailReservations} ] ${event.start_date}` + + `${event.city && event.country ? ` ${event.city}, ${event.country}` : ''}` + + return new Response(` @@ -64,19 +65,19 @@ exports.handler = async function(request, context, callback) { - + - + -
+

${event.name}

Supply
@@ -84,9 +85,8 @@ exports.handler = async function(request, context, callback) {
Email reservations
${emailReservations}
-

${event.start_date}

${event.city && event.country ? `

${event.city}, ${event.country}

` : ''} -
+

${event.start_date}

${event.city && event.country ? `\n

${event.city}, ${event.country}

` : ''} + -`, - } +`) } diff --git a/functions/events.js b/functions/events.js index 0f9c4e4..fce244c 100644 --- a/functions/events.js +++ b/functions/events.js @@ -62,17 +62,17 @@ async function getMetrics(eventIds) { return metrics } -exports.handler = async function(request, context, callback) { +exports.defaults = async function(request, context) { const rawIds = request.path.split('/').pop() const eventIds = parseEventIds(rawIds) if (!IS_BOT.test(request.headers['user-agent'])) { - return { - statusCode: 301, + return new Response(null, { + status: 301, headers: { location: `${FAMILY_URL}/r/events/${eventIds.join(',')}`, }, - } + }) } let eventsInfo @@ -80,10 +80,10 @@ exports.handler = async function(request, context, callback) { eventsInfo = await getEventsInfo(eventIds) } catch (err) { if (err?.response?.status === 404) { - return { statusCode: 404 } + return new Response(null, { status: 404 }) } console.error('Fetch events info failed', err) - return { statusCode: 503 } + return new Response(null, { status: 503 }) } let totalSupply = 0, totalEmailReservations = 0 @@ -95,9 +95,9 @@ exports.handler = async function(request, context, callback) { trs.push(`

${event.name}

${event.start_date}

${event.city && event.country ? `

${event.city}, ${event.country}

` : ''}${supply}${emailReservations}`) } - return { - statusCode: 200, - body: ` + const description = `[ ${totalSupply} + ${totalEmailReservations} ]` + + return new Response(` @@ -108,19 +108,19 @@ exports.handler = async function(request, context, callback) { - + - + -
+

${titles.join(', ')}

Total supply
@@ -130,10 +130,9 @@ exports.handler = async function(request, context, callback) {
- ${trs.join('')} + ${trs.join('\n')}
EventSupplyEmail reservations
-
+ -`, - } +`) } From 39e8f1a634e836663eef2203855b1752ffa52c60 Mon Sep 17 00:00:00 2001 From: Juan M Date: Sat, 6 Apr 2024 23:58:19 -0300 Subject: [PATCH 3/5] Move to egde functions --- functions/event.js | 92 ----------------- functions/events.js | 138 ------------------------- netlify/edge-functions/event.js | 116 +++++++++++++++++++++ netlify/edge-functions/events.js | 172 +++++++++++++++++++++++++++++++ public/_redirects | 4 +- public/index.html | 24 +++-- src/app/App.js | 10 +- src/loaders/event.js | 49 --------- 8 files changed, 305 insertions(+), 300 deletions(-) delete mode 100644 functions/event.js delete mode 100644 functions/events.js create mode 100644 netlify/edge-functions/event.js create mode 100644 netlify/edge-functions/events.js diff --git a/functions/event.js b/functions/event.js deleted file mode 100644 index cdf19e2..0000000 --- a/functions/event.js +++ /dev/null @@ -1,92 +0,0 @@ -const axios = require('axios') - -const FAMILY_URL = 'https://poap.family' -const FAMILY_API_URL = 'https://api.poap.family' -const IS_BOT = /(bot|check|cloud|crawler|download|monitor|preview|scan|spider|google|qwantify|yahoo|facebookexternalhit|flipboard|tumblr|vkshare|whatsapp|curl|perl|python|wget|heritrix|ia_archiver)/i - -async function getEventInfo(eventId) { - const response = await axios.get(`${FAMILY_API_URL}/event/${eventId}?metrics=true&fresh=true`) - const event = response.data - if ( - typeof event !== 'object' || - !('event' in event) || typeof event.event !== 'object' || - !('owners' in event) || !Array.isArray(event.owners) || - !('ts' in event) || typeof event.ts !== 'number' || - !('metrics' in event) || !event.metrics || - !('emailReservations' in event.metrics) || typeof event.metrics.emailReservations !== 'number' || - !('ts' in event.metrics) || (typeof event.metrics.ts !== 'number' && event.metrics.ts !== null) - ) { - throw new Error(`Event ${eventId} invalid response`) - } - return { - event: event.event, - supply: event.owners.length, - emailReservations: event.metrics.emailReservations, - } -} - -exports.defaults = async function(request, context) { - const eventId = request.path.split('/').pop() - - if (!IS_BOT.test(request.headers['user-agent'])) { - return new Response(null, { - status: 301, - headers: { - location: `${FAMILY_URL}/r/event/${eventId}`, - }, - }) - } - - let event, supply, emailReservations - try { - const eventInfo = await getEventInfo(eventId) - event = eventInfo.event - supply = eventInfo.supply - emailReservations = eventInfo.emailReservations - } catch (err) { - if (err?.response?.status === 404) { - return new Response(null, { status: 404 }) - } - console.error('Fetch event info failed', err) - return new Response(null, { status: 503 }) - } - - const description = `[ ${supply} + ${emailReservations} ] ${event.start_date}` + - `${event.city && event.country ? ` ${event.city}, ${event.country}` : ''}` - - return new Response(` - - - - - POAP Family: ${event.name} - - - - - - - - - - - - - - - - - -
-

${event.name}

-
-
Supply
-
${supply}
-
Email reservations
-
${emailReservations}
-
-

${event.start_date}

${event.city && event.country ? `\n

${event.city}, ${event.country}

` : ''} -
- -`) -} diff --git a/functions/events.js b/functions/events.js deleted file mode 100644 index fce244c..0000000 --- a/functions/events.js +++ /dev/null @@ -1,138 +0,0 @@ -const axios = require('axios') - -const FAMILY_URL = 'https://poap.family' -const FAMILY_API_URL = 'https://api.poap.family' -const IS_BOT = /(bot|check|cloud|crawler|download|monitor|preview|scan|spider|google|qwantify|yahoo|facebookexternalhit|flipboard|tumblr|vkshare|whatsapp|curl|perl|python|wget|heritrix|ia_archiver)/i - -function parseEventIds(rawIds) { - let eventIds = rawIds.split(',') - .filter((value, index, all) => all.indexOf(value) === index) - .map((value) => parseInt(value.trim())) - .filter((eventId) => !isNaN(eventId)) - eventIds.sort((a, b) => a - b) - return eventIds -} - -async function getEventsInfo(eventIds) { - const [events, owners, metrics] = await Promise.all([ - getEvents(eventIds), - getOwners(eventIds), - getMetrics(eventIds), - ]) - if (!events) { - return null - } - const eventsInfo = {} - for (const eventId of eventIds) { - if (eventId in events) { - eventsInfo[eventId] = { - event: events[eventId], - supply: eventId in owners ? owners[eventId].length : 0, - emailReservations: eventId in metrics ? metrics[eventId].emailReservations : 0, - } - } - } - return eventsInfo -} - -async function getEvents(eventIds) { - const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}?fresh=true`) - const events = response.data - if (typeof events !== 'object') { - throw new Error(`Events invalid response (type ${typeof events} expected object)`) - } - return events -} - -async function getOwners(eventIds) { - const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}/owners?fresh=true`) - const owners = response.data - if (typeof owners !== 'object') { - throw new Error(`Events owners invalid response (type ${typeof owners} expected object)`) - } - return owners -} - -async function getMetrics(eventIds) { - const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}/metrics`) - const metrics = response.data - if (typeof metrics !== 'object') { - throw new Error(`Events metrics invalid response (type ${typeof metrics} expected object)`) - } - return metrics -} - -exports.defaults = async function(request, context) { - const rawIds = request.path.split('/').pop() - const eventIds = parseEventIds(rawIds) - - if (!IS_BOT.test(request.headers['user-agent'])) { - return new Response(null, { - status: 301, - headers: { - location: `${FAMILY_URL}/r/events/${eventIds.join(',')}`, - }, - }) - } - - let eventsInfo - try { - eventsInfo = await getEventsInfo(eventIds) - } catch (err) { - if (err?.response?.status === 404) { - return new Response(null, { status: 404 }) - } - console.error('Fetch events info failed', err) - return new Response(null, { status: 503 }) - } - - let totalSupply = 0, totalEmailReservations = 0 - let titles = [], trs = [] - for (const { event, supply, emailReservations } of Object.values(eventsInfo)) { - totalSupply += supply - totalEmailReservations += emailReservations - titles.push(event.name) - trs.push(`

${event.name}

${event.start_date}

${event.city && event.country ? `

${event.city}, ${event.country}

` : ''}${supply}${emailReservations}`) - } - - const description = `[ ${totalSupply} + ${totalEmailReservations} ]` - - return new Response(` - - - - - POAP Family: ${titles.join(', ')} - - - - - - - - - - - - - - - - - -
-

${titles.join(', ')}

-
-
Total supply
-
${totalSupply}
-
Total email reservations
-
${totalEmailReservations}
-
- - - ${trs.join('\n')} -
EventSupplyEmail reservations
-
- -`) -} diff --git a/netlify/edge-functions/event.js b/netlify/edge-functions/event.js new file mode 100644 index 0000000..7715b65 --- /dev/null +++ b/netlify/edge-functions/event.js @@ -0,0 +1,116 @@ +import axios from 'https://esm.sh/axios' + +const FAMILY_URL = 'https://poap.family' +const FAMILY_API_URL = 'https://api.poap.family' + +function getQueryString(requestUrl) { + const searchParams = new URL(requestUrl).searchParams.toString() + return searchParams ? `?${searchParams}` : '' +} + +function getEventId(requestUrl) { + const url = new URL(requestUrl) + const [, rawEventId] = url.pathname.match(/event\/([^/]+)/) + const eventId = parseInt(rawEventId) + if (isNaN(eventId)) { + throw new Error(`Event invalid Id param`) + } + return eventId +} + +async function getEventInfo(eventId) { + const response = await axios.get(`${FAMILY_API_URL}/event/${eventId}?metrics=true&fresh=true`) + const event = response.data + if ( + typeof event !== 'object' || + !('event' in event) || + typeof event.event !== 'object' || + !('owners' in event) || + !Array.isArray(event.owners) || + !('metrics' in event) || + !('emailReservations' in event.metrics) || + typeof event.metrics.emailReservations !== 'number' + ) { + throw new Error(`Event ${eventId} invalid response`) + } + return { + event: event.event, + supply: event.owners.length, + emailReservations: event.metrics.emailReservations, + } +} + +function replaceMeta(html, title, description, image, url) { + return html + .replace(/([^<]+)<\/title>/, `<title>$1: ${title}`) + .replace(//g, ``) + .replace(//, ``) + .replace(//g, ``) + .replace(//g, ``) + .replace(//g, ``) +} + +function escapeHtml(str) { + return str + .replace(/(\r\n|\n|\r)/gm, '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +export default async function handler(request, context) { + const eventId = getEventId(request.url) + const queryString = getQueryString(request.url) + + const response = await context.next() + const html = await response.text() + + let eventInfo + try { + eventInfo = await getEventInfo(eventId) + } catch (err) { + if (err?.response?.status === 404) { + return new Response(html, { + status: 404, + headers: { + 'content-type': 'text/html', + }, + }) + } + return new Response(html, { + status: 503, + headers: { + 'content-type': 'text/html', + }, + }) + } + + const title = escapeHtml(eventInfo.event.name) + const description = escapeHtml( + `[ ${eventInfo.supply} + ${eventInfo.emailReservations} ] ` + + `${eventInfo.event.start_date}` + + `${eventInfo.event.city && eventInfo.event.country + ? ` ${eventInfo.event.city}, ${eventInfo.event.country}` + : '' + }` + ) + const image = eventInfo.event.image_url + const url = `${FAMILY_URL}/event/${eventId}${queryString}` + + return new Response( + replaceMeta( + html, + title, + description, + image, + url + ), + response + ) +} + +export const config = { + path: '/event/*', +} diff --git a/netlify/edge-functions/events.js b/netlify/edge-functions/events.js new file mode 100644 index 0000000..747e40a --- /dev/null +++ b/netlify/edge-functions/events.js @@ -0,0 +1,172 @@ +import axios from 'https://esm.sh/axios' + +const FAMILY_URL = 'https://poap.family' +const FAMILY_API_URL = 'https://api.poap.family' + +function getQueryString(requestUrl) { + const searchParams = new URL(requestUrl).searchParams.toString() + return searchParams ? `?${searchParams}` : '' +} + +function parseEventIds(rawIds) { + let eventIds = rawIds.split(',') + .filter((value, index, all) => all.indexOf(value) === index) + .map((value) => parseInt(value.trim())) + .filter((eventId) => !isNaN(eventId)) + eventIds.sort((a, b) => a - b) + return eventIds +} + +function getRawEventIds(requestUrl) { + const url = new URL(requestUrl) + const [, rawEventIds] = url.pathname.match(/events\/([^/]+)/) + return rawEventIds +} + +async function getEventsInfo(eventIds) { + const [events, owners, metrics] = await Promise.all([ + getEvents(eventIds), + getOwners(eventIds), + getMetrics(eventIds), + ]) + const eventsInfo = {} + for (const eventId of eventIds) { + if (eventId in events) { + eventsInfo[eventId] = { + event: events[eventId], + supply: eventId in owners ? owners[eventId].length : 0, + emailReservations: eventId in metrics ? metrics[eventId].emailReservations : 0, + } + } + } + return eventsInfo +} + +async function getEvents(eventIds) { + const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}?fresh=true`) + const events = response.data + if (typeof events !== 'object') { + throw new Error(`Events invalid response (type ${typeof events} expected object)`) + } + return events +} + +async function getOwners(eventIds) { + const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}/owners?fresh=true`) + const owners = response.data + if (typeof owners !== 'object') { + throw new Error(`Events owners invalid response (type ${typeof owners} expected object)`) + } + return owners +} + +async function getMetrics(eventIds) { + const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}/metrics`) + const metrics = response.data + if (typeof metrics !== 'object') { + throw new Error(`Events metrics invalid response (type ${typeof metrics} expected object)`) + } + return metrics +} + +function replaceMeta(html, title, description, image, url) { + return html + .replace(/([^<]+)<\/title>/, `<title>$1: ${title}`) + .replace(//g, ``) + .replace(//, ``) + .replace(//g, ``) + .replace(//g, ``) + .replace(//g, ``) +} + +function escapeHtml(str) { + return str + .replace(/(\r\n|\n|\r)/gm, '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +export default async function handler(request, context) { + const rawEventIds = getRawEventIds(request.url) + const queryString = getQueryString(request.url) + + const eventIds = parseEventIds(rawEventIds) + + const response = await context.next() + const html = await response.text() + + if (eventIds.length === 0) { + return new Response(html, { + status: 404, + headers: { + 'content-type': 'text/html', + }, + }) + } + + if (rawEventIds !== eventIds.join(',')) { + return Response.redirect( + new URL(`/events/${eventIds.join(',')}${queryString}`, request.url) + ) + } + + if (eventIds.length === 1) { + return Response.redirect( + new URL(`/event/${eventIds[0]}${queryString}`, request.url) + ) + } + + let eventsInfo + try { + eventsInfo = await getEventsInfo(eventIds) + } catch (err) { + if (err?.response?.status === 404) { + return new Response(html, { + status: 404, + headers: { + 'content-type': 'text/html', + }, + }) + } + return new Response(html, { + status: 503, + headers: { + 'content-type': 'text/html', + }, + }) + } + + let totalSupply = 0 + let totalEmailReservations = 0 + let names = [] + for (const eventInfo of Object.values(eventsInfo)) { + totalSupply += eventInfo.supply + totalEmailReservations += eventInfo.emailReservations + names = [...names, eventInfo.event.name] + } + + const title = escapeHtml(names.join(', ')) + const description = escapeHtml( + `[ ${totalSupply} + ${totalEmailReservations} ]` + ) + const image = `${FAMILY_URL}/poap-family.png` // TODO + const url = `${FAMILY_URL}/events/${eventIds.join(',')}${queryString}` + + return new Response( + replaceMeta( + html, + title, + description, + image, + url + ), + response + ) +} + +export const config = { + path: '/events/*', +} diff --git a/public/_redirects b/public/_redirects index 4d5d5a6..f9efe6d 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,5 +1,3 @@ -/event/* /.netlify/functions/event/:route 200! -/events/* /.netlify/functions/events/:route 200! /faq https://poap.notion.site/POAP-Family-FAQ-cef29bc0bb8c4f8f936164d988a944cc 301 /help https://poap.zendesk.com/hc/en-us/articles/24008770288909-How-to-navigate-POAP-Family 301 -/* /index.html 200 +/* /index.html 200 diff --git a/public/index.html b/public/index.html index e767cc0..5d40a62 100644 --- a/public/index.html +++ b/public/index.html @@ -4,15 +4,21 @@ POAP Family - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/app/App.js b/src/app/App.js index 73ef13c..f8c85b2 100644 --- a/src/app/App.js +++ b/src/app/App.js @@ -7,7 +7,7 @@ import Event from '../pages/Event' import Events from '../pages/Events' import PageError from '../components/PageError' import EventsPageError from '../components/EventsPageError' -import { eventLoader, eventRedirect, eventsLoader, eventsRedirect } from '../loaders/event' +import { eventLoader, eventsLoader } from '../loaders/event' import Last from '../pages/Last' import Settings from '../pages/Settings' import FeedbackList from '../pages/FeedbackList' @@ -40,20 +40,12 @@ function App() { index: true, element: , }, - { - path: '/r/event/:eventId', - loader: eventRedirect, - }, { path: '/event/:eventId', loader: eventLoader, element: , errorElement: , }, - { - path: '/r/events/:eventIds', - loader: eventsRedirect, - }, { path: '/events/:eventIds', loader: eventsLoader, diff --git a/src/loaders/event.js b/src/loaders/event.js index 1f279e1..68d1114 100644 --- a/src/loaders/event.js +++ b/src/loaders/event.js @@ -307,59 +307,10 @@ async function eventsLoader({ params, request }) { return events } -function eventRedirect({ params, request }) { - const searchParams = new URL(request.url).searchParams.toString() - return new Response('', { - status: 301, - statusText: 'Redirect to event', - headers: { - location: `/event/${params.eventId}${searchParams ? `?${searchParams}` : ''}`, - }, - }) -} - -function eventsRedirect({ params, request }) { - const searchParams = new URL(request.url).searchParams.toString() - const eventIds = parseEventIds(params.eventIds) - if (eventIds.length === 0) { - throw new Response('', { - status: 404, - statusText: 'Events not found', - }) - } - if (params.eventIds !== eventIds.join(',')) { - throw new Response('', { - status: 301, - statusText: 'Events given unordered', - headers: { - location: `/events/${eventIds.join(',')}${searchParams ? `?${searchParams}` : ''}`, - }, - }) - } - if (eventIds.length === 1) { - throw new Response('', { - status: 301, - statusText: 'One event', - headers: { - location: `/event/${eventIds[0]}${searchParams ? `?${searchParams}` : ''}`, - }, - }) - } - return new Response('', { - status: 301, - statusText: 'Redirect to events', - headers: { - location: `/events/${params.eventIds}${searchParams ? `?${searchParams}` : ''}`, - }, - }) -} - export { searchEvents, fetchEvent, fetchEventsOrErrors, eventLoader, eventsLoader, - eventRedirect, - eventsRedirect, } From de0b230225e882cd54cc56b72671160adcda835b Mon Sep 17 00:00:00 2001 From: Juan M Date: Sun, 7 Apr 2024 04:57:28 -0300 Subject: [PATCH 4/5] Images function --- netlify/edge-functions/events.js | 2 +- netlify/edge-functions/images.jsx | 121 ++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 netlify/edge-functions/images.jsx diff --git a/netlify/edge-functions/events.js b/netlify/edge-functions/events.js index 747e40a..9d60888 100644 --- a/netlify/edge-functions/events.js +++ b/netlify/edge-functions/events.js @@ -152,7 +152,7 @@ export default async function handler(request, context) { const description = escapeHtml( `[ ${totalSupply} + ${totalEmailReservations} ]` ) - const image = `${FAMILY_URL}/poap-family.png` // TODO + const image = `${FAMILY_URL}/images/${eventIds.join(',')}` const url = `${FAMILY_URL}/events/${eventIds.join(',')}${queryString}` return new Response( diff --git a/netlify/edge-functions/images.jsx b/netlify/edge-functions/images.jsx new file mode 100644 index 0000000..ce05d80 --- /dev/null +++ b/netlify/edge-functions/images.jsx @@ -0,0 +1,121 @@ +/** @jsxImportSource https://esm.sh/react */ +import axios from 'https://esm.sh/axios' +import { ImageResponse } from 'https://deno.land/x/og_edge/mod.ts' + +const FAMILY_API_URL = 'https://api.poap.family' + +function parseEventIds(rawIds) { + let eventIds = rawIds.split(',') + .filter((value, index, all) => all.indexOf(value) === index) + .map((value) => parseInt(value.trim())) + .filter((eventId) => !isNaN(eventId)) + eventIds.sort((a, b) => a - b) + return eventIds +} + +function getEventIds(requestUrl) { + const url = new URL(requestUrl) + const [, rawEventIds] = url.pathname.match(/images\/([^/]+)/) + return parseEventIds(rawEventIds) +} + +async function getEvents(eventIds) { + const response = await axios.get(`${FAMILY_API_URL}/events/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}?fresh=true`) + const events = response.data + if (typeof events !== 'object') { + throw new Error(`Events invalid response (type ${typeof events} expected object)`) + } + return events +} + +function renderEventsImages(events, canvas, size, pos) { + if (events.length === 0) { + return [] + } + + const angle = 360 / events.length + const offset = size / 2 + const radius = (canvas - size) / 2 + const center = pos + offset + radius + + const images = [] + + events.forEach((event, i) => { + const x = center + radius * Math.cos(angle * i * Math.PI / 180) + const y = center + radius * Math.sin(angle * i * Math.PI / 180) + + images.push( + {event.name} + ) + }) + + return images +} + +export default async function handler(request, context) { + const eventIds = getEventIds(request.url) + + let events + try { + events = await getEvents(eventIds) + } catch (err) { + if (err?.response?.status === 404) { + return new Response(null, { + status: 404, + }) + } + return new Response(null, { + status: 503, + }) + } + + const eventsLeft = Object.values(events) + .map((event) => ({ event, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ event }) => event) + + const images = [ + ...renderEventsImages(eventsLeft.splice(0, 12), 512, 128, 0), + ...renderEventsImages(eventsLeft.splice(0, 12), 256, 64, 128), + ...renderEventsImages(eventsLeft.splice(0, 12), 128, 32, 192), + ] + + return new ImageResponse( + ( +
+ {images} +
+ ), + { + width: 512, + height: 512, + } + ) +} + +export const config = { + path: '/images/*', +} From 7def451730c3daef0998e1fc31a925b51dc21a13 Mon Sep 17 00:00:00 2001 From: Juan M Date: Sun, 7 Apr 2024 04:59:12 -0300 Subject: [PATCH 5/5] Remove FAQ from README --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f3fb19..f546bf2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # POAP Family -[POAP.family](https://poap.family) is a tool to discover the POAPs that different collectors have in common. Read the [FAQ](https://poap.notion.site/poap/POAP-Family-FAQ-cef29bc0bb8c4f8f936164d988a944cc). +[POAP.family](https://poap.family) is a tool to discover the POAPs that different collectors have in common. diff --git a/package.json b/package.json index 80c9245..fe5d256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@poap-xyz/poap-family", - "version": "1.9.3", + "version": "1.10.0", "author": { "name": "POAP", "url": "https://poap.xyz"