From 0d0a213bca265e57aa2c5f3ddbc39f3e54d52ada Mon Sep 17 00:00:00 2001 From: Wei Gao Date: Sun, 1 Dec 2019 00:21:42 +0800 Subject: [PATCH 1/9] Revert "Revert "Feature/GitHub auth"" --- gatsby-browser.js | 6 + package.json | 2 +- prettier.config.js | 16 +- .../RSVP/GitHub-Mark-Light-64px.png | Bin 0 -> 2330 bytes src/components/RSVP/index.js | 313 ++++++++++++------ src/components/RSVP/s.module.scss | 15 +- src/context/auth.js | 25 ++ src/functions/airtable.js | 188 ++++++++--- src/functions/auth.js | 38 +++ src/functions/package.json | 3 + src/functions/rollup.config.js | 10 +- src/functions/yarn.lock | 32 ++ src/pages/LoginCallback/index.jsx | 36 ++ 13 files changed, 519 insertions(+), 165 deletions(-) create mode 100644 src/components/RSVP/GitHub-Mark-Light-64px.png create mode 100644 src/context/auth.js create mode 100644 src/functions/auth.js create mode 100644 src/pages/LoginCallback/index.jsx diff --git a/gatsby-browser.js b/gatsby-browser.js index 7332da0..a83b4ae 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,4 +1,10 @@ +import React from 'react'; import ReactDOM from 'react-dom'; +import { AuthProvider } from './src/context/auth'; + +export const wrapRootElement = ({ element }) => ( + {element} +); // https://twitter.com/EphemeralCircle/status/1190670453221842944?s=20 // our own @thchia actually also pointed this out as well diff --git a/package.json b/package.json index ae3dbfb..beb4b22 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "scripts": { "develop": "gatsby develop", "start": "npm run develop", - "build": "yarn run functions && gatsby build", + "build": "yarn run functions && GATSBY_URL=$DEPLOY_PRIME_URL gatsby build", "functions": "cd src/functions && yarn run build" }, "resolutions": { diff --git a/prettier.config.js b/prettier.config.js index 213ea50..d6b8d13 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,9 +1,9 @@ module.exports = { - singleQuote: true, - trailingComma: 'es5', - bracketSpacing: true, - jsxBracketSameLine: false, - printWidth: 80, - parser: 'babel-flow', - } - \ No newline at end of file + singleQuote: true, + trailingComma: 'es5', + bracketSpacing: true, + jsxBracketSameLine: false, + printWidth: 80, + parser: 'babel-flow', + semi: true, +} diff --git a/src/components/RSVP/GitHub-Mark-Light-64px.png b/src/components/RSVP/GitHub-Mark-Light-64px.png new file mode 100644 index 0000000000000000000000000000000000000000..73db1f61f3aa55fcaecbca896dbea067706bb7bd GIT binary patch literal 2330 zcmaJ@dpy&7A0KVQT+TU3iOLKmvh6n;HrL%O6)ohFIM()K!!EXAIjO}-B}W%^)GMS^ zbRj&Vi`ep%r%r`PE0rauRMJE)p`NWn=lP@Od|$8M_wsqY-=EL>bNS=9n;FC~MOYyq z5Qu5ON?Ne?^wWKYaP5AUm;6k7ER@nCq#?pMX&OrmLdYE9CJ-6GXT^iTAd9m(`4;E} zfy}Yzg+@pt0@sk(LOz-_L`>RbTB#+~b3MT|& z14UGj%UhW)21C+=LfPqwY$6Bc>x1-?k+cr@pp=D_@sk7+lFS?R)h|hV*O@UW6`Ai_H`049j)JozV)4d8r`BUo7M!zgCCD`;G!S0dV&q zp8LL%~Ba#6Eg$iKtZe$N47EeRXa5>+}e1_})f4LZ~7>us9|I_MOlXiY|*KdQX zeS8~zP@tW6v39VXfr*USwp~rS`=B$j`)nk?@}kq(ClF4d>YOz8hkr;=W~kkm zCW7a<(pcz5X#A0R<9Z{E*BfJ?`u2(tHC?l!+(h-PRUM^q=-1CbjbNI^81U7VyU;3( zT2f^?#ma%q8;EN&iz|Pk@=)pdaMq3d25mbDNrIP#bi|hqP`3*fT}kuvR+%ng#;F!o z!3#SBB2F;iNzfJ(L`*fNFT`NdGQiAH|Ej|Hc2f5bF>p2ufwkUZTFe}(dJ$`MX;C(0 z-V{r-E2m(c5BI}drL(~k6SEswne#fEUdvi&3FiZh%N)80&$C_2kh}Zs$!WSMT5)2L z->t!4_PuW(@0iMUf!tM3EXsxR=8aqQJS!tI77vxXD$Q$NZ1vCFp)TCJPPKgJ6$dq6 z6AvwIT>T_kTfhs9Ro&hblgcP`#)4Y!f6RfcKKz5}fFeLswhx__Ru z`f>qsFw=AMC$D2)!n`KC&FVyxLH=oq<|UuIL`hLk2jvOa!Qq_R3CqNXSp|NlZhBlh z@V0BxvHNAmtK__)^rv^SNw9j3M7}#zg*ToWtbKT~2{x#g2)k9TC|R>Dzhq!T-pA~R z>ZL3u2sdInHHePlU?n!;#-16SNWGJd=j+gQ4vI}N_YbJpa3kc$Q-$nCw8DusXXMl4 z*nJS|qzj=rNJpZI; z$vRbg>YS|lmxjj--{P0zyGBQ?yoS4%MX$EkOMqE$O+Pm%_1o-3WM-q=b4f2dQfTt> zOFR@zTq!4JBAQzKEZEFs#YOE+WUR@=e`!;~PCh-+ddeAuP%>I*Cd8Cb%!1YNZFKDIJTra-- zG#+}%TS;ZSZaqDK8^0CL=AU{B|LU>Hu0mU~}a9D5U= zlszxG=SokFC;8liaQHBlh z2BUO}iYG`REj!%$Xj-syeaVfwq3hF5*L>e!Jm^2?Jo?@}>G*`TrXZHmap&CnfJ4u^ zk68!bvsByruv+!zzl{#!R8}8y9^Ra-sENMbl68A9w_o}?z5eO*E&If08t#Rq+K#s{ zb}mq^_${jVjmeHhTXNWUoFMIe)lJ26`vXz zWvk~HR@whs?OF8cKV8YyEs1et|A`i*;GS`_+RH=ns17TKJg|lJv;Y(H<+2}hIXsat z5ANw;3-!HT?J@e?t>VkCV$sSaXTfoVh-!;h=kN@TPi^X~Dlo@yIP4fl2 z#pt2|1Jj>Ov$71^ei~h1H$uM|R6wY=ZG3Q99U>X|mV`BoS9`4vYw~J@(FazM_8hI9o-&aIo@yE| z6xi92RAt7!kM;6HVQV)mjXu!6Snn}L!YlYRm7CF)XL~-WsBd9Uk6GUDFm8S4@oR}s z!f_@&&0G2FHTy?nQd8F&@&}*SrC)xRgU!h6eepUXk}}=;?q@wX1aZ#AJc}avS@*XR MKo6o-Qerdz3wwRZj{pDw literal 0 HcmV?d00001 diff --git a/src/components/RSVP/index.js b/src/components/RSVP/index.js index b20b60d..1ccdb24 100644 --- a/src/components/RSVP/index.js +++ b/src/components/RSVP/index.js @@ -1,134 +1,237 @@ import React from 'react'; import axios from 'axios'; +import qs from 'query-string'; import s from './s.module.scss'; +import githubLogo from './GitHub-Mark-Light-64px.png'; +import AuthContext from '../../context/auth'; + +export default ({ eventId, calendarLink }) => { + const { token } = React.useContext(AuthContext); + const [state, dispatch] = React.useReducer(reducer, initialState); + const handleError = makeHandleError(dispatch); + React.useEffect(() => { + dispatch({ type: 'REQUEST_STATUS' }); + if (!token) { + dispatch({ + type: 'RECEIVE_AUTH', + payload: { + authStatus: false, + }, + }); + return; + } + dispatch({ + type: 'RECEIVE_AUTH', + payload: { + authStatus: true, + }, + }); + getRSVPStatus(eventId, token) + .then(rsvpName => { + dispatch({ + type: 'RECEIVE_RSVP', + payload: { rsvpStatus: !!rsvpName, rsvpName }, + }); + }) + .catch(err => { + handleError(err); + }); + }, []); + + async function sendRSVP(isGoing) { + try { + let rsvpName = ''; + dispatch({ type: 'SEND_RSVP', payload: { rsvpStatus: isGoing } }); + if (isGoing) { + const { + data: { name }, + } = await insertAttendee({ eventId, token }); + rsvpName = name; + } else { + await removeAttendee({ eventId, token }); + } + dispatch({ + type: 'RECEIVE_RSVP', + payload: { rsvpStatus: isGoing, rsvpName }, + }); + } catch (e) { + handleError(e); + } + } + + return taggedSum( + state, + { + isError: err =>

{err}

, + isWorking: () =>

Hard at work...

, + isGoing: name => ( + +

+ See you there {name} :) Would you like to{' '} + + add this to your calendar + + ? +

+ +
+ ), + isAuthed: () => ( + + +

+ We will record your attendance using your Github details +

+
+ ), + }, + () => ( + + github-logo + Login to RSVP + + ) + ); +}; + +function insertAttendee({ eventId, token }) { + return axios({ + method: 'post', + url: `/.netlify/functions/airtable`, + data: { + eventId, + }, + headers: { + Authorization: `token ${token}`, + }, + }); +} + +function removeAttendee({ eventId, token }) { + return axios({ + method: 'delete', + url: `/.netlify/functions/airtable`, + data: { + eventId, + }, + headers: { + Authorization: `token ${token}`, + }, + }); +} + +function getGithubURL() { + const base = process.env.GATSBY_URL || 'http://localhost:8000'; + const from = typeof window !== 'undefined' ? window.location.pathname : '/'; + return ( + 'https://github.com/login/oauth/authorize?' + + qs.stringify({ + client_id: 'e3a62ea68aca5801ec9b', + state: 'home', + redirect_uri: `${base}/LoginCallback?from=${from}`, + scope: 'read:user', + }) + ); +} + +function getRSVPStatus(eventId, token) { + return axios({ + method: 'GET', + url: 'https://api.github.com/user', + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: `token ${token}`, + }, + }) + .then(({ data: { login } }) => { + return axios({ + method: 'get', + url: `/.netlify/functions/airtable?eventId=${eventId}&username=${login}`, + }); + }) + .then(({ data }) => { + if (data) return data.Name; + }); +} + +function taggedSum(state, pattern, def) { + if (state.error) return pattern.isError(state.error); + if (state.isWorking) return pattern.isWorking(); + if (state.isGoing) return pattern.isGoing(state.rsvpName); + if (state.isAuthed) return pattern.isAuthed(); + return def(); +} const initialState = { - name: '', - username: '', - submissionError: '', - submitting: false, - submissionSuccess: false, + isWorking: false, + isAuthed: false, + isGoing: false, + rsvpName: '', + error: '', }; + function reducer(state = initialState, action = { type: '' }) { switch (action.type) { - case 'input': + case 'REQUEST_STATUS': + return { + ...state, + isWorking: true, + error: '', + }; + case 'RECEIVE_AUTH': return { ...state, - [action.payload.name]: action.payload.value, - submission_error: '', - submissionSuccess: false, + isWorking: false, + isAuthed: action.payload.authStatus, + // if not authed, clear out any stale rsvp status + isGoing: action.payload.authStatus === false ? false : state.isGoing, + rsvpName: action.payload.authStatus === false ? '' : state.rsvpName, }; - case 'submit': + case 'RECEIVE_RSVP': return { ...state, - submissionError: '', - submissionSuccess: false, - submitting: true, + isWorking: false, + isAuthed: true, + isGoing: action.payload.rsvpStatus, + rsvpName: action.payload.rsvpName, }; - case 'submission_error': + case 'SEND_RSVP': return { ...state, - submissionError: action.payload.error, - submissionSuccess: false, - submitting: false, + isWorking: true, + isGoing: action.payload.rsvpStatus, }; - case 'submission_success': + case 'RECEIVE_ERROR': return { ...state, - name: '', - username: '', - submissionSuccess: true, - submissionError: '', - submitting: false, + isWorking: false, + error: action.payload.error, }; default: return state; } } -export default ({ eventId, calendarLink }) => { - const [formVisible, setFormVisible] = React.useState(false); - const [nameError, setNameError] = React.useState(''); - const [state, dispatch] = React.useReducer(reducer, initialState); - const handleSubmit = e => { - e.preventDefault(); - if (!state.name) { - return setNameError('Required'); +function makeHandleError(dispatch) { + return function handleError(err) { + const { response } = err; + if (response) { + const { data } = response; + const errorMessage = + typeof data === 'string' ? data : 'Something went wrong :('; + dispatch({ + type: 'RECEIVE_ERROR', + payload: { error: errorMessage }, + }); } else { - dispatch({ type: 'submit' }); - insertAttendee({ name: state.name, username: state.username, eventId }) - .then(() => { - dispatch({ type: 'submission_success' }); - setFormVisible(false); - }) - .catch(() => { - dispatch({ - type: 'submission_error', - payload: { - error: "Oops, we couldn't register you, please try again.", - }, - }); - }); + dispatch({ + type: 'RECEIVE_ERROR', + payload: { error: 'Something went wrong :(' }, + }); } }; - return ( - - {!state.submissionSuccess && ( - - )} -
- - - {state.submissionError && ( -

{state.submissionError}

- )} - {/* Ask if they want their name to be shown? */} - -
- {state.submissionSuccess && ( -

- See you there :) Would you like to{' '} - - add this to your calendar - - ? -

- )} -
- ); -}; - -function insertAttendee({ eventId, username, name }) { - return axios.post(`/.netlify/functions/airtable`, { - name, - username, - eventId, - }); } diff --git a/src/components/RSVP/s.module.scss b/src/components/RSVP/s.module.scss index 568f493..a5051e6 100644 --- a/src/components/RSVP/s.module.scss +++ b/src/components/RSVP/s.module.scss @@ -21,6 +21,19 @@ } } +.link { + @extend .btn; + background-color: black; + border-color: white; + color: white; + display: inline-flex; + align-items: center; + margin-bottom: 0.5em; + img { + margin: 0 1em 0 0; + } +} + .formField { display: block; margin: 0.5em 0em; @@ -38,7 +51,7 @@ .fieldCaption { color: #777; font-size: .75em; - margin-bottom: .5em; + margin: .5em 0; } .fieldError { diff --git a/src/context/auth.js b/src/context/auth.js new file mode 100644 index 0000000..b265fda --- /dev/null +++ b/src/context/auth.js @@ -0,0 +1,25 @@ +import React from 'react'; + +const defaultContext = { + token: '', + setToken: () => {}, +}; + +const AuthContext = React.createContext(defaultContext); + +function AuthProvider({ children }) { + const [token, setToken] = React.useState(''); + const contextValue = React.useMemo( + () => ({ + token, + setToken, + }), + [token, setToken] + ); + return ( + {children} + ); +} + +export { AuthProvider }; +export default AuthContext; diff --git a/src/functions/airtable.js b/src/functions/airtable.js index b4e6967..30ae95a 100644 --- a/src/functions/airtable.js +++ b/src/functions/airtable.js @@ -1,81 +1,179 @@ -import Airtable from 'airtable' +import Airtable from 'airtable'; +import axios from 'axios'; const ERROR_MSGS = { UNSUPPORTED_METHOD: 'Unsupported method', UNKNOWN_ERROR: 'Server Error', -} +}; export const handler = async (event, _, callback) => { try { - const atClient = _configureAirtable() + const atClient = _configureAirtable(); + const atService = AirtableService(atClient); switch (event.httpMethod) { - case 'POST': - await insertAttendee(atClient, event, callback) - break case 'GET': - await retrieveAttendees(atClient, event, callback) - break + await retrieveAttendees(atService, event, callback); + break; + case 'POST': + await insertAttendee(atService, event, callback); + break; + case 'DELETE': + await removeAttendee(atService, event, callback); + break; default: callback(Error({ message: ERROR_MSGS.UNSUPPORTED_METHOD }), { - status: 405, + statusCode: 405, body: ERROR_MSGS.UNSUPPORTED_METHOD, - }) + }); } } catch (e) { callback(Error(e), { - status: 500, + statusCode: 500, body: ERROR_MSGS.UNKNOWN_ERROR, - }) + }); } -} +}; async function retrieveAttendees(Client, event, callback) { - let attendees = [] - let selectOpts = {} - const { eventId } = event.queryStringParameters - if (eventId) { - selectOpts = { filterByFormula: `SEARCH("${eventId}",{Event ID})` } + let attendees; + const { eventId, username } = event.queryStringParameters; + if (eventId && username) { + attendees = await Client.getSingleAttendee({ eventId, username }); + } else if (eventId) { + attendees = await Client.listAttendees({ eventId }); + } else { + throw new Error('Missing parameters'); } - await Client('Attendees') - .select(selectOpts) - .eachPage((records, fetchNextPage) => { - records.forEach(function(record) { - attendees.push(record.fields) - }) - fetchNextPage() - }) callback(null, { - status: 200, + statusCode: 200, body: JSON.stringify(attendees), - }) + }); } async function insertAttendee(Client, event, callback) { if (event.httpMethod !== 'POST') { return callback(Error({ message: ERROR_MSGS.UNSUPPORTED_METHOD }), { - status: 405, + statusCode: 405, body: ERROR_MSGS.UNSUPPORTED_METHOD, - }) + }); } - const { eventId, name, username } = JSON.parse(event.body) - await Client('Attendees').create([ - { - fields: { - Name: name, - 'Github Username': username, - 'Event ID': eventId, - Type: 'Attendee', - 'Created Date': new Date().toISOString(), - }, + const userDetails = await axios({ + method: 'GET', + url: 'https://api.github.com/user', + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: event.headers.authorization, }, - ]) + }); + const { + data: { login, name }, + } = userDetails; + const { eventId } = JSON.parse(event.body); + const userRecord = await Client.getSingleAttendee({ + eventId, + username: login, + }); + if (userRecord && userRecord.id) { + return callback(null, { + statusCode: 409, + body: `You are already signed up!`, + }); + } + await Client.insertAttendee({ eventId, name, login }); callback(null, { - status: 200, + statusCode: 200, body: JSON.stringify({ name, eventId }), - }) + }); } +async function removeAttendee(Client, event, callback) { + const { eventId } = JSON.parse(event.body); + if (!eventId) { + throw new Error('Missing Parameters: eventId'); + } + const userDetails = await axios({ + method: 'GET', + url: 'https://api.github.com/user', + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: event.headers.authorization, + }, + }); + const { + data: { login }, + } = userDetails; + const userRecord = await Client.getSingleAttendee({ + eventId, + username: login, + }); + if (!userRecord) { + return callback(Error('User Not Found'), { + statusCode: 404, + body: 'User not found', + }); + } + const { id } = userRecord; + await Client.removeAttendee({ id }); + callback(null, { + statusCode: 200, + }); +} + +/****** UTILS ******/ + function _configureAirtable() { - Airtable.configure({ apiKey: '__AIRTABLE_API_KEY__' }) - return Airtable.base('__AIRTABLE_BASE_ID__') + Airtable.configure({ apiKey: '__AIRTABLE_API_KEY__' }); + return Airtable.base('__AIRTABLE_BASE_ID__')('Attendees'); +} + +function AirtableService(client) { + return { + async getSingleAttendee({ eventId, username }) { + let attendees = []; + await client + .select({ + filterByFormula: `AND( + SEARCH("${eventId}",{Event ID}), + SEARCH("${username}",{Github Username}) + )`, + }) + .eachPage((records, fetchNextPage) => { + records.forEach(function(record) { + attendees.push({ ...record.fields, id: record.id }); + }); + fetchNextPage(); + }); + return attendees[0]; // there should only be one + }, + async listAttendees({ eventId }) { + let attendees = []; + await client + .select({ + filterByFormula: `SEARCH("${eventId}",{Event ID})`, + }) + .eachPage((records, fetchNextPage) => { + records.forEach(function(record) { + attendees.push(record.fields); + }); + fetchNextPage(); + }); + return attendees; + }, + insertAttendee({ name, login, eventId }) { + return client.create([ + { + fields: { + Name: name, + 'Github Username': login, + 'Event ID': eventId, + Type: 'Attendee', + 'Created Date': new Date().toISOString(), + }, + }, + ]); + }, + removeAttendee({ id }) { + return client.destroy([id]); + }, + }; } diff --git a/src/functions/auth.js b/src/functions/auth.js new file mode 100644 index 0000000..ad07d65 --- /dev/null +++ b/src/functions/auth.js @@ -0,0 +1,38 @@ +import axios from 'axios'; +import qs from 'query-string'; + +export const handler = async (event, _, callback) => { + try { + await _retrieveToken(event, callback); + } catch (e) { + callback(e, { + statusCode: 500, + body: 'Server Error', + }); + } +}; + +async function _retrieveToken(event, callback) { + const { code, state } = event.queryStringParameters; + if (!code || !state) { + throw new Error('Missing parameters'); + } + const parameters = qs.stringify({ + code, + client_id: '__RK_RSVP_CLIENT_ID__', + client_secret: '__RK_RSVP_CLIENT_SECRET__', + state, + }); + const res = await axios({ + method: 'post', + url: `https://github.com/login/oauth/access_token?${parameters}`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); + callback(null, { + statusCode: 200, + body: JSON.stringify(res.data), + }); +} diff --git a/src/functions/package.json b/src/functions/package.json index 22332e4..a06b124 100644 --- a/src/functions/package.json +++ b/src/functions/package.json @@ -12,5 +12,8 @@ "@rollup/plugin-replace": "^2.2.0", "airtable": "^0.7.2", "rollup": "^1.26.3" + }, + "dependencies": { + "axios": "^0.19.0" } } diff --git a/src/functions/rollup.config.js b/src/functions/rollup.config.js index 5a1a004..817c9d4 100644 --- a/src/functions/rollup.config.js +++ b/src/functions/rollup.config.js @@ -20,9 +20,11 @@ export default () => { replace({ __AIRTABLE_API_KEY__: process.env.AIRTABLE_API_KEY, __AIRTABLE_BASE_ID__: process.env.AIRTABLE_BASE_ID, + __RK_RSVP_CLIENT_ID__: process.env.RK_RSVP_CLIENT_ID, + __RK_RSVP_CLIENT_SECRET__: process.env.RK_RSVP_CLIENT_SECRET, }), ], - external: ['airtable'] + external: ['airtable'], })) ); }); @@ -38,10 +40,8 @@ async function _getFunctionPaths() { 'node_modules', 'package.json', 'yarn.lock', - 'rollup.config.js' - ].includes( - filePath - ); + 'rollup.config.js', + ].includes(filePath); }) .map(filePath => ({ input: path.resolve(functionSrc, filePath), diff --git a/src/functions/yarn.lock b/src/functions/yarn.lock index 1ad72d7..bc76136 100644 --- a/src/functions/yarn.lock +++ b/src/functions/yarn.lock @@ -72,6 +72,14 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -103,6 +111,13 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -183,6 +198,13 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -258,6 +280,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +is-buffer@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + is-callable@^1.1.3, is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" @@ -358,6 +385,11 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" diff --git a/src/pages/LoginCallback/index.jsx b/src/pages/LoginCallback/index.jsx new file mode 100644 index 0000000..e3cf603 --- /dev/null +++ b/src/pages/LoginCallback/index.jsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import qs from 'query-string'; +import axios from 'axios'; +import { navigate } from '@reach/router'; +import AuthContext from '../../context/auth'; + +const LoginCallback = () => { + const { setToken } = React.useContext(AuthContext); + React.useEffect(() => { + const { code, state, from } = qs.parse(window.location.search); + const nextPage = from || '/'; + axios({ + method: 'get', + url: `/.netlify/functions/auth?code=${code}&state=${state}`, + headers: { + Accept: 'application/json', + }, + }) + .then(res => { + const { + data: { access_token }, + } = res; + if (!access_token || typeof access_token !== 'string') { + throw new Error(); + } + setToken(access_token); + navigate(nextPage); + }) + .catch(() => { + navigate(nextPage); + }); + }, []); + return
logging in...
; +}; + +export default LoginCallback; From 7bc7a49003bce19730009696a8eb7ff1a8a4ba6d Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Wed, 4 Dec 2019 20:52:22 +0800 Subject: [PATCH 2/9] Separate effects --- src/components/RSVP/index.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/RSVP/index.js b/src/components/RSVP/index.js index 1ccdb24..78d2fdc 100644 --- a/src/components/RSVP/index.js +++ b/src/components/RSVP/index.js @@ -9,7 +9,7 @@ export default ({ eventId, calendarLink }) => { const { token } = React.useContext(AuthContext); const [state, dispatch] = React.useReducer(reducer, initialState); const handleError = makeHandleError(dispatch); - React.useEffect(() => { + React.useLayoutEffect(() => { dispatch({ type: 'REQUEST_STATUS' }); if (!token) { dispatch({ @@ -26,17 +26,21 @@ export default ({ eventId, calendarLink }) => { authStatus: true, }, }); - getRSVPStatus(eventId, token) - .then(rsvpName => { - dispatch({ - type: 'RECEIVE_RSVP', - payload: { rsvpStatus: !!rsvpName, rsvpName }, - }); - }) - .catch(err => { - handleError(err); - }); }, []); + React.useEffect(() => { + if (state.isAuthed && token) { + getRSVPStatus(eventId, token) + .then(rsvpName => { + dispatch({ + type: 'RECEIVE_RSVP', + payload: { rsvpStatus: !!rsvpName, rsvpName }, + }); + }) + .catch(err => { + handleError(err); + }); + } + }, [state.isAuthed]); async function sendRSVP(isGoing) { try { From eff0b159c06842571b707e2e7e7c025f41b6121c Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 15:15:35 +0800 Subject: [PATCH 3/9] Checkpoint --- gatsby-node.js | 43 ++++++++++++++++++++---------------- src/components/RSVP/index.js | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/gatsby-node.js b/gatsby-node.js index 4f036d0..cc60a34 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,28 +1,33 @@ const path = require('path'); const { createFilePath } = require('gatsby-source-filesystem'); -const axios = require('axios') +const axios = require('axios'); // exports.sourceNodes = async ({ actions, reporter, createContentDigest }) => { -// const { createNode } = actions -// const host = process.env.NODE_ENV === 'production' ? `https://reactknowledgeable.org` : `` -// const data = await axios.get(`${host}/.netlify/functions/airtable`) +// const { createNode } = actions; +// const host = +// process.env.NODE_ENV === 'production' +// ? `https://reactknowledgeable.org` +// : ``; +// const data = await axios.get(`${host}/.netlify/functions/airtable`); // if (data.status >= 200 && data.status < 300) { -// data.data.forEach(datum => createNode({ -// ...datum, -// id: `${datum.Name}-${datum["Created Date"]}`, -// parent: null, -// children: [], -// internal: { -// type: "RKAttendee", -// contentDigest: createContentDigest(datum) -// } -// })) -// reporter.success("Retrieved attendee data") +// data.data.forEach(datum => +// createNode({ +// ...datum, +// id: `${datum.Name}-${datum['Created Date']}`, +// parent: null, +// children: [], +// internal: { +// type: 'RKAttendee', +// contentDigest: createContentDigest(datum), +// }, +// }) +// ); +// reporter.success('Retrieved attendee data'); // } else { -// reporter.error("Error encountered retrieving attendees") +// reporter.error('Error encountered retrieving attendees'); // } -// return -// } +// return; +// }; exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions; @@ -102,7 +107,7 @@ exports.createPages = ({ graphql, actions }) => { }); }); meetups.forEach(meetup => { - const id = meetup.fields.slug.replace(/[^\d]+/g, "") // to remove everything except the numbers + const id = meetup.fields.slug.replace(/[^\d]+/g, ''); // to remove everything except the numbers createPage({ path: `${meetup.fields.slug}`, component: meetupTemplate, diff --git a/src/components/RSVP/index.js b/src/components/RSVP/index.js index 78d2fdc..e920ee7 100644 --- a/src/components/RSVP/index.js +++ b/src/components/RSVP/index.js @@ -40,7 +40,7 @@ export default ({ eventId, calendarLink }) => { handleError(err); }); } - }, [state.isAuthed]); + }, [state.isAuthed, token]); async function sendRSVP(isGoing) { try { From 06851241bc9c537a418be91874bb85e940557a51 Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 16:02:57 +0800 Subject: [PATCH 4/9] Moved function code to top level directory since we can let it get bundled by Netlify. Use function to source attendee list, instead of airtable plugin so we can run the site part without needing an Airtable API key. --- .gitignore | 3 +- {src/functions => functions}/airtable.js | 49 +- functions/auth.js | 46 ++ gatsby-config.js | 12 - gatsby-node.js | 53 +- netlify.toml | 4 + package.json | 4 +- src/components/Participants/index.jsx | 9 +- src/functions/auth.js | 38 -- src/functions/package.json | 19 - src/functions/rollup.config.js | 50 -- src/functions/yarn.lock | 602 ----------------------- src/templates/Meetup/index.jsx | 22 +- yarn.lock | 15 + 14 files changed, 136 insertions(+), 790 deletions(-) rename {src/functions => functions}/airtable.js (82%) create mode 100644 functions/auth.js create mode 100644 netlify.toml delete mode 100644 src/functions/auth.js delete mode 100644 src/functions/package.json delete mode 100644 src/functions/rollup.config.js delete mode 100644 src/functions/yarn.lock diff --git a/.gitignore b/.gitignore index c3de1f6..b33c49a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .cache/ public .env -functions/* \ No newline at end of file +# Local Netlify folder +.netlify \ No newline at end of file diff --git a/src/functions/airtable.js b/functions/airtable.js similarity index 82% rename from src/functions/airtable.js rename to functions/airtable.js index 30ae95a..d61960b 100644 --- a/src/functions/airtable.js +++ b/functions/airtable.js @@ -1,25 +1,22 @@ -import Airtable from 'airtable'; -import axios from 'axios'; +const Airtable = require('airtable'); +const axios = require('axios'); const ERROR_MSGS = { UNSUPPORTED_METHOD: 'Unsupported method', UNKNOWN_ERROR: 'Server Error', }; -export const handler = async (event, _, callback) => { +exports.handler = async event => { try { const atClient = _configureAirtable(); const atService = AirtableService(atClient); switch (event.httpMethod) { case 'GET': - await retrieveAttendees(atService, event, callback); - break; + return await retrieveAttendees(atService, event); case 'POST': - await insertAttendee(atService, event, callback); - break; + return await insertAttendee(atService, event); case 'DELETE': - await removeAttendee(atService, event, callback); - break; + return await removeAttendee(atService, event); default: callback(Error({ message: ERROR_MSGS.UNSUPPORTED_METHOD }), { statusCode: 405, @@ -34,7 +31,7 @@ export const handler = async (event, _, callback) => { } }; -async function retrieveAttendees(Client, event, callback) { +async function retrieveAttendees(Client, event) { let attendees; const { eventId, username } = event.queryStringParameters; if (eventId && username) { @@ -44,13 +41,13 @@ async function retrieveAttendees(Client, event, callback) { } else { throw new Error('Missing parameters'); } - callback(null, { + return { statusCode: 200, body: JSON.stringify(attendees), - }); + }; } -async function insertAttendee(Client, event, callback) { +async function insertAttendee(Client, event) { if (event.httpMethod !== 'POST') { return callback(Error({ message: ERROR_MSGS.UNSUPPORTED_METHOD }), { statusCode: 405, @@ -74,19 +71,19 @@ async function insertAttendee(Client, event, callback) { username: login, }); if (userRecord && userRecord.id) { - return callback(null, { + return { statusCode: 409, body: `You are already signed up!`, - }); + }; } await Client.insertAttendee({ eventId, name, login }); - callback(null, { + return { statusCode: 200, body: JSON.stringify({ name, eventId }), - }); + }; } -async function removeAttendee(Client, event, callback) { +async function removeAttendee(Client, event) { const { eventId } = JSON.parse(event.body); if (!eventId) { throw new Error('Missing Parameters: eventId'); @@ -107,23 +104,25 @@ async function removeAttendee(Client, event, callback) { username: login, }); if (!userRecord) { - return callback(Error('User Not Found'), { + return { statusCode: 404, body: 'User not found', - }); + }; } const { id } = userRecord; await Client.removeAttendee({ id }); - callback(null, { - statusCode: 200, - }); + return { statusCode: 200 }; } /****** UTILS ******/ function _configureAirtable() { - Airtable.configure({ apiKey: '__AIRTABLE_API_KEY__' }); - return Airtable.base('__AIRTABLE_BASE_ID__')('Attendees'); + if (!process.env.AIRTABLE_BASE_ID) + throw new Error('must set process.env.AIRTABLE_BASE_ID'); + if (!process.env.AIRTABLE_API_KEY) + throw new Error('must set process.env.AIRTABLE_API_KEY'); + Airtable.configure({ apiKey: process.env.AIRTABLE_API_KEY }); + return Airtable.base(process.env.AIRTABLE_BASE_ID)('Attendees'); } function AirtableService(client) { diff --git a/functions/auth.js b/functions/auth.js new file mode 100644 index 0000000..05f0ffb --- /dev/null +++ b/functions/auth.js @@ -0,0 +1,46 @@ +const axios = require('axios'); +const qs = require('query-string'); + +exports.handler = async event => { + try { + return await _retrieveToken(event, callback); + } catch (e) { + let body = `Server Error`; + if (e.message) { + body = `${body} - ${e.message}`; + } + return { + statusCode: 500, + body, + }; + } +}; + +async function _retrieveToken(event) { + if (!process.env.RK_RSVP_CLIENT_ID) + throw new Error('OAuth Client ID is not set'); + if (!process.env.RK_RSVP_CLIENT_SECRET) + throw new Error('OAuth Client Secret is not set'); + const { code, state } = event.queryStringParameters; + if (!code || !state) { + throw new Error('Missing parameters'); + } + const parameters = qs.stringify({ + code, + client_id: process.env.RK_RSVP_CLIENT_ID, + client_secret: process.env.RK_RSVP_CLIENT_SECRET, + state, + }); + const res = await axios({ + method: 'post', + url: `https://github.com/login/oauth/access_token?${parameters}`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); + return { + statusCode: 200, + body: JSON.stringify(res.data), + }; +} diff --git a/gatsby-config.js b/gatsby-config.js index bd33c0b..e337e85 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -129,17 +129,5 @@ module.exports = { fetchOptions: {}, }, }, - { - resolve: `gatsby-source-airtable`, - options: { - apiKey: process.env.AIRTABLE_API_KEY, - tables: [ - { - baseId: process.env.AIRTABLE_BASE_ID, - tableName: 'Attendees', - }, - ], - }, - }, ], }; diff --git a/gatsby-node.js b/gatsby-node.js index cc60a34..fbb14ab 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -2,32 +2,33 @@ const path = require('path'); const { createFilePath } = require('gatsby-source-filesystem'); const axios = require('axios'); -// exports.sourceNodes = async ({ actions, reporter, createContentDigest }) => { -// const { createNode } = actions; -// const host = -// process.env.NODE_ENV === 'production' -// ? `https://reactknowledgeable.org` -// : ``; -// const data = await axios.get(`${host}/.netlify/functions/airtable`); -// if (data.status >= 200 && data.status < 300) { -// data.data.forEach(datum => -// createNode({ -// ...datum, -// id: `${datum.Name}-${datum['Created Date']}`, -// parent: null, -// children: [], -// internal: { -// type: 'RKAttendee', -// contentDigest: createContentDigest(datum), -// }, -// }) -// ); -// reporter.success('Retrieved attendee data'); -// } else { -// reporter.error('Error encountered retrieving attendees'); -// } -// return; -// }; +exports.sourceNodes = async ({ actions, reporter, createContentDigest }) => { + const { createNode } = actions; + const host = `https://reactknowledgeable.org`; + // const host = + // process.env.NODE_ENV === 'production' + // ? `https://reactknowledgeable.org` + // : ``; + const data = await axios.get(`${host}/.netlify/functions/airtable`); + if (data.status >= 200 && data.status < 300) { + data.data.forEach(datum => + createNode({ + ...datum, + id: `${datum.Name}-${datum['Created Date']}`, + parent: null, + children: [], + internal: { + type: 'RKAttendee', + contentDigest: createContentDigest(datum), + }, + }) + ); + reporter.success('Retrieved attendee data'); + } else { + reporter.error('Error encountered retrieving attendees'); + } + return; +}; exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..d35aace --- /dev/null +++ b/netlify.toml @@ -0,0 +1,4 @@ +[build] + publish = "public" + command = "yarn build" + functions = "functions" \ No newline at end of file diff --git a/package.json b/package.json index beb4b22..3444666 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dependencies": { "@raae/gatsby-remark-oembed": "^0.1.1", "@reach/router": "^1.2.1", + "airtable": "^0.8.1", "axios": "^0.19.0", "classnames": "^2.2.6", "gatsby": "^2.17.4", @@ -40,8 +41,7 @@ "scripts": { "develop": "gatsby develop", "start": "npm run develop", - "build": "yarn run functions && GATSBY_URL=$DEPLOY_PRIME_URL gatsby build", - "functions": "cd src/functions && yarn run build" + "build": "GATSBY_URL=$DEPLOY_PRIME_URL gatsby build" }, "resolutions": { "gatsby-plugin-favicon/favicons-webpack-plugin/favicons/sharp": "^0.23.1" diff --git a/src/components/Participants/index.jsx b/src/components/Participants/index.jsx index 569ae8d..db7b6da 100644 --- a/src/components/Participants/index.jsx +++ b/src/components/Participants/index.jsx @@ -9,16 +9,13 @@ const getAvatarProps = username => ({ const Participants = ({ rawParticipants }) => { const participants = new Set(); - rawParticipants.map(({ node: { data: { Github_Username: username } } }) => { + rawParticipants.map(({ node: { Github_Username: username } }) => { // dedupe participants.add(username ? username.toLowerCase() : 'react-knowledgeable'); }); const numberOfSecretParticipants = rawParticipants.filter( - ({ - node: { - data: { Github_Username: username }, - }, - }) => !username || username === 'react-knowledgeable' + ({ node: { Github_Username: username } }) => + !username || username === 'react-knowledgeable' ).length; return participants.size > 0 ? ( diff --git a/src/functions/auth.js b/src/functions/auth.js deleted file mode 100644 index ad07d65..0000000 --- a/src/functions/auth.js +++ /dev/null @@ -1,38 +0,0 @@ -import axios from 'axios'; -import qs from 'query-string'; - -export const handler = async (event, _, callback) => { - try { - await _retrieveToken(event, callback); - } catch (e) { - callback(e, { - statusCode: 500, - body: 'Server Error', - }); - } -}; - -async function _retrieveToken(event, callback) { - const { code, state } = event.queryStringParameters; - if (!code || !state) { - throw new Error('Missing parameters'); - } - const parameters = qs.stringify({ - code, - client_id: '__RK_RSVP_CLIENT_ID__', - client_secret: '__RK_RSVP_CLIENT_SECRET__', - state, - }); - const res = await axios({ - method: 'post', - url: `https://github.com/login/oauth/access_token?${parameters}`, - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }); - callback(null, { - statusCode: 200, - body: JSON.stringify(res.data), - }); -} diff --git a/src/functions/package.json b/src/functions/package.json deleted file mode 100644 index a06b124..0000000 --- a/src/functions/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "rk-functions", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "build": "yarn && rollup --config && cp -r node_modules ../../functions" - }, - "author": "", - "license": "ISC", - "devDependencies": { - "@rollup/plugin-replace": "^2.2.0", - "airtable": "^0.7.2", - "rollup": "^1.26.3" - }, - "dependencies": { - "axios": "^0.19.0" - } -} diff --git a/src/functions/rollup.config.js b/src/functions/rollup.config.js deleted file mode 100644 index 817c9d4..0000000 --- a/src/functions/rollup.config.js +++ /dev/null @@ -1,50 +0,0 @@ -/* This is for building the Netlify functions (lambdas) only! */ - -const fs = require('fs'); -const path = require('path'); -const promisify = require('util').promisify; -const readdir = promisify(fs.readdir); -const replace = require('@rollup/plugin-replace'); - -export default () => { - return new Promise(res => { - _getFunctionPaths().then(paths => { - res( - paths.map(func => ({ - input: func.input, - output: { - file: path.resolve(__dirname, '../../functions', func.filePath), - format: 'cjs', - }, - plugins: [ - replace({ - __AIRTABLE_API_KEY__: process.env.AIRTABLE_API_KEY, - __AIRTABLE_BASE_ID__: process.env.AIRTABLE_BASE_ID, - __RK_RSVP_CLIENT_ID__: process.env.RK_RSVP_CLIENT_ID, - __RK_RSVP_CLIENT_SECRET__: process.env.RK_RSVP_CLIENT_SECRET, - }), - ], - external: ['airtable'], - })) - ); - }); - }); -}; - -async function _getFunctionPaths() { - const functionSrc = path.resolve(__dirname); - const functionPaths = await readdir(functionSrc); - return functionPaths - .filter(filePath => { - return ![ - 'node_modules', - 'package.json', - 'yarn.lock', - 'rollup.config.js', - ].includes(filePath); - }) - .map(filePath => ({ - input: path.resolve(functionSrc, filePath), - filePath, - })); -} diff --git a/src/functions/yarn.lock b/src/functions/yarn.lock deleted file mode 100644 index bc76136..0000000 --- a/src/functions/yarn.lock +++ /dev/null @@ -1,602 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@rollup/plugin-replace@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.2.0.tgz#e3b11881fb1ab1be7570e1ccd175b8249e448c52" - integrity sha512-DnALjyAdfAyQ4i6tJdi4v6rJwx5tzV9/+M7G8qA3cHpPOYQOgM4U8bU4lSM24h9hFIbycxRTHtE+TkENLqRlEA== - dependencies: - magic-string "^0.25.2" - rollup-pluginutils "^2.6.0" - typescript "^3.4.3" - -"@types/estree@*": - version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - -"@types/node@*": - version "12.12.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.5.tgz#66103d2eddc543d44a04394abb7be52506d7f290" - integrity sha512-KEjODidV4XYUlJBF3XdjSH5FWoMCtO0utnhtdLf1AgeuZLOrRbvmU/gaRCVg7ZaQDjVf3l84egiY0mRNe5xE4A== - -acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== - -airtable@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/airtable/-/airtable-0.7.2.tgz#106e87b7139f6a2c9f1bd856583e83626e2f3c04" - integrity sha512-BwHIJyXmtUJ78EpnNzs+aYmPK9r0xNeFyKkmSn9I6WvG6QYcXlbDe6rhoEwqEdrjPVL6ZCoNwimJN4l6y5TUJg== - dependencies: - lodash "4.17.15" - request "2.88.0" - xhr "2.3.3" - -ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -axios@^0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" - integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== - dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -dom-walk@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" - integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg= - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -es-abstract@^1.13.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" - integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.0" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-inspect "^1.6.0" - object-keys "^1.1.1" - string.prototype.trimleft "^2.1.0" - string.prototype.trimright "^2.1.0" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -estree-walker@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" - integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -global@~4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" - integrity sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8= - dependencies: - min-document "^2.19.0" - process "~0.5.1" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -is-buffer@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== - -is-callable@^1.1.3, is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-function@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" - integrity sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU= - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -lodash@4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -magic-string@^0.25.2: - version "0.25.4" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.4.tgz#325b8a0a79fc423db109b77fd5a19183b7ba5143" - integrity sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw== - dependencies: - sourcemap-codec "^1.4.4" - -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= - dependencies: - dom-walk "^0.1.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-inspect@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" - integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -parse-headers@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.2.tgz#9545e8a4c1ae5eaea7d24992bca890281ed26e34" - integrity sha512-/LypJhzFmyBIDYP9aDVgeyEb5sQfbfY5mnDq4hVhlQ69js87wXfmEI5V3xI6vvXasqebp0oCytYFLxsBVfCzSg== - dependencies: - for-each "^0.3.3" - string.prototype.trim "^1.1.2" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -process@~0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" - integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8= - -psl@^1.1.24: - version "1.4.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" - integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -request@2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -rollup-pluginutils@^2.6.0: - version "2.8.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - -rollup@^1.26.3: - version "1.26.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.26.3.tgz#3e71b8120a4ccc745a856e926cab0efbe0eead90" - integrity sha512-8MhY/M8gnv3Q/pQQSWYWzbeJ5J1C5anCNY5BK1kV8Yzw9RFS0FF4lbLt+uyPO3wLKWXSXrhAL5pWL85TZAh+Sw== - dependencies: - "@types/estree" "*" - "@types/node" "*" - acorn "^7.1.0" - -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sourcemap-codec@^1.4.4: - version "1.4.6" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" - integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -string.prototype.trim@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz#75a729b10cfc1be439543dae442129459ce61e3d" - integrity sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.13.0" - function-bind "^1.1.1" - -string.prototype.trimleft@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" - integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimright@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" - integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -typescript@^3.4.3: - version "3.6.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" - integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -uuid@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -xhr@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.3.3.tgz#ad6b810e0917ce72b5ec704f5d41f1503b8e7524" - integrity sha1-rWuBDgkXznK17HBPXUHxUDuOdSQ= - dependencies: - global "~4.3.0" - is-function "^1.0.1" - parse-headers "^2.0.0" - xtend "^4.0.0" - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== diff --git a/src/templates/Meetup/index.jsx b/src/templates/Meetup/index.jsx index 11a0469..0b2aef1 100644 --- a/src/templates/Meetup/index.jsx +++ b/src/templates/Meetup/index.jsx @@ -40,7 +40,7 @@ export default ({ location, pageContext: { id }, data: { - allAirtable: { edges: rawParticipants }, + allRkAttendee: { edges: rawParticipants }, site: { siteMetadata: { description, @@ -62,7 +62,6 @@ export default ({ venueAddressLink, sponsors, talks: talkIssueIds, - issueLink, calendarLink, }, htmlAst, @@ -294,19 +293,24 @@ export default ({ export const pageQuery = graphql` query MeetupQuery($slug: String!, $id: String!) { - # allRkAttendee(filter: { Event_ID: { eq: $id } }) { - # totalCount - # } - allAirtable(filter: { data: { Event_ID: { eq: $id } } }) { + allRkAttendee(filter: { Event_ID: { eq: $id } }) { totalCount edges { node { - data { - Github_Username - } + Github_Username } } } + # allAirtable(filter: { data: { Event_ID: { eq: $id } } }) { + # totalCount + # edges { + # node { + # data { + # Github_Username + # } + # } + # } + # } site { siteMetadata { title diff --git a/yarn.lock b/yarn.lock index 99db21d..97b904a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1799,6 +1799,16 @@ airtable@^0.7.2: request "2.88.0" xhr "2.3.3" +airtable@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/airtable/-/airtable-0.8.1.tgz#8d10f04f41673b86ed75fa054f54470357e36ae0" + integrity sha512-Cxw55ta1olDwDERz++HFJOBX6LONtg+d7+wOcYguqI4PR4P5RHmgjTbY8tPKgLHb8U3FVOyAbpb7NpLRSnLGgg== + dependencies: + es6-promise "4.2.8" + lodash "4.17.15" + request "2.88.0" + xhr "2.3.3" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -4717,6 +4727,11 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-promise@4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + es6-promise@^3.0.2: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" From 177eb20e35be55697f04a1d4c9cfaabcf261ef8e Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 16:07:37 +0800 Subject: [PATCH 5/9] Appease the linter --- src/components/Participants/index.jsx | 2 +- src/components/RSVP/index.js | 10 ++++++---- src/pages/LoginCallback/index.jsx | 2 +- src/templates/Meetup/index.jsx | 6 +++++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/Participants/index.jsx b/src/components/Participants/index.jsx index db7b6da..150c440 100644 --- a/src/components/Participants/index.jsx +++ b/src/components/Participants/index.jsx @@ -9,7 +9,7 @@ const getAvatarProps = username => ({ const Participants = ({ rawParticipants }) => { const participants = new Set(); - rawParticipants.map(({ node: { Github_Username: username } }) => { + rawParticipants.forEach(({ node: { Github_Username: username } }) => { // dedupe participants.add(username ? username.toLowerCase() : 'react-knowledgeable'); }); diff --git a/src/components/RSVP/index.js b/src/components/RSVP/index.js index e920ee7..f322d2a 100644 --- a/src/components/RSVP/index.js +++ b/src/components/RSVP/index.js @@ -8,7 +8,9 @@ import AuthContext from '../../context/auth'; export default ({ eventId, calendarLink }) => { const { token } = React.useContext(AuthContext); const [state, dispatch] = React.useReducer(reducer, initialState); - const handleError = makeHandleError(dispatch); + const handleError = React.useCallback(() => makeHandleError(dispatch), [ + dispatch, + ]); React.useLayoutEffect(() => { dispatch({ type: 'REQUEST_STATUS' }); if (!token) { @@ -26,7 +28,7 @@ export default ({ eventId, calendarLink }) => { authStatus: true, }, }); - }, []); + }, [token]); React.useEffect(() => { if (state.isAuthed && token) { getRSVPStatus(eventId, token) @@ -40,7 +42,7 @@ export default ({ eventId, calendarLink }) => { handleError(err); }); } - }, [state.isAuthed, token]); + }, [state.isAuthed, token, handleError, eventId]); async function sendRSVP(isGoing) { try { @@ -72,7 +74,7 @@ export default ({ eventId, calendarLink }) => {

See you there {name} :) Would you like to{' '} - + add this to your calendar ? diff --git a/src/pages/LoginCallback/index.jsx b/src/pages/LoginCallback/index.jsx index e3cf603..85e9482 100644 --- a/src/pages/LoginCallback/index.jsx +++ b/src/pages/LoginCallback/index.jsx @@ -29,7 +29,7 @@ const LoginCallback = () => { .catch(() => { navigate(nextPage); }); - }, []); + }, [setToken]); return

logging in...
; }; diff --git a/src/templates/Meetup/index.jsx b/src/templates/Meetup/index.jsx index 0b2aef1..3d5b37e 100644 --- a/src/templates/Meetup/index.jsx +++ b/src/templates/Meetup/index.jsx @@ -177,7 +177,11 @@ export default ({ {venueAddressLink && ( <> ,{' '} - + Google Map From 79082f453514e7d334dcc9eb4f29805d06baea5d Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 16:21:29 +0800 Subject: [PATCH 6/9] Fix stale reference --- functions/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/auth.js b/functions/auth.js index 05f0ffb..8e3b6cf 100644 --- a/functions/auth.js +++ b/functions/auth.js @@ -3,7 +3,7 @@ const qs = require('query-string'); exports.handler = async event => { try { - return await _retrieveToken(event, callback); + return await _retrieveToken(event); } catch (e) { let body = `Server Error`; if (e.message) { From 7e22831d3a9ab54a806c831c19b2b02565d0f030 Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 16:39:12 +0800 Subject: [PATCH 7/9] Check to see if everything works when importing a shared axios instance --- functions/airtable.js | 2 +- functions/auth.js | 2 +- gatsby-node.js | 6 +----- src/utils/axios.js | 7 +++++++ 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 src/utils/axios.js diff --git a/functions/airtable.js b/functions/airtable.js index d61960b..a20b3ad 100644 --- a/functions/airtable.js +++ b/functions/airtable.js @@ -1,5 +1,5 @@ const Airtable = require('airtable'); -const axios = require('axios'); +const axios = require('../src/utils/axios'); const ERROR_MSGS = { UNSUPPORTED_METHOD: 'Unsupported method', diff --git a/functions/auth.js b/functions/auth.js index 8e3b6cf..753eb9d 100644 --- a/functions/auth.js +++ b/functions/auth.js @@ -1,4 +1,4 @@ -const axios = require('axios'); +const axios = require('../src/utils/axios'); const qs = require('query-string'); exports.handler = async event => { diff --git a/gatsby-node.js b/gatsby-node.js index fbb14ab..746b324 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,14 +1,10 @@ const path = require('path'); const { createFilePath } = require('gatsby-source-filesystem'); -const axios = require('axios'); +const axios = require('./src/utils/axios'); exports.sourceNodes = async ({ actions, reporter, createContentDigest }) => { const { createNode } = actions; const host = `https://reactknowledgeable.org`; - // const host = - // process.env.NODE_ENV === 'production' - // ? `https://reactknowledgeable.org` - // : ``; const data = await axios.get(`${host}/.netlify/functions/airtable`); if (data.status >= 200 && data.status < 300) { data.data.forEach(datum => diff --git a/src/utils/axios.js b/src/utils/axios.js new file mode 100644 index 0000000..c3ac807 --- /dev/null +++ b/src/utils/axios.js @@ -0,0 +1,7 @@ +import axios from 'axios'; + +export default axios.create({ + validateStatus: function(status) { + return status >= 200 && status < 300; + }, +}); From e7ca48008de2aa52ccdaec5c3c19d30ced93e408 Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 16:44:38 +0800 Subject: [PATCH 8/9] Remove import syntax --- src/utils/axios.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/axios.js b/src/utils/axios.js index c3ac807..9211254 100644 --- a/src/utils/axios.js +++ b/src/utils/axios.js @@ -1,6 +1,6 @@ -import axios from 'axios'; +const axios = require('axios'); -export default axios.create({ +module.exports = axios.create({ validateStatus: function(status) { return status >= 200 && status < 300; }, From 76f2f0e8aada50eec99fd7b8ba29d16327ebc6e1 Mon Sep 17 00:00:00 2001 From: Thomas Chia Date: Fri, 6 Dec 2019 16:54:06 +0800 Subject: [PATCH 9/9] Revert "Check to see if everything works when importing a shared axios instance" This reverts commit 7e22831d3a9ab54a806c831c19b2b02565d0f030. --- functions/airtable.js | 2 +- functions/auth.js | 2 +- gatsby-node.js | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/functions/airtable.js b/functions/airtable.js index a20b3ad..d61960b 100644 --- a/functions/airtable.js +++ b/functions/airtable.js @@ -1,5 +1,5 @@ const Airtable = require('airtable'); -const axios = require('../src/utils/axios'); +const axios = require('axios'); const ERROR_MSGS = { UNSUPPORTED_METHOD: 'Unsupported method', diff --git a/functions/auth.js b/functions/auth.js index 753eb9d..8e3b6cf 100644 --- a/functions/auth.js +++ b/functions/auth.js @@ -1,4 +1,4 @@ -const axios = require('../src/utils/axios'); +const axios = require('axios'); const qs = require('query-string'); exports.handler = async event => { diff --git a/gatsby-node.js b/gatsby-node.js index 746b324..fbb14ab 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,10 +1,14 @@ const path = require('path'); const { createFilePath } = require('gatsby-source-filesystem'); -const axios = require('./src/utils/axios'); +const axios = require('axios'); exports.sourceNodes = async ({ actions, reporter, createContentDigest }) => { const { createNode } = actions; const host = `https://reactknowledgeable.org`; + // const host = + // process.env.NODE_ENV === 'production' + // ? `https://reactknowledgeable.org` + // : ``; const data = await axios.get(`${host}/.netlify/functions/airtable`); if (data.status >= 200 && data.status < 300) { data.data.forEach(datum =>