diff --git a/.eleventy.js b/.eleventy.js index 57a5f77..5ad0dfa 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -1,3 +1,4 @@ +const { EleventyServerlessBundlerPlugin } = require('@11ty/eleventy'); const markdownIt = require('markdown-it'); const emoji = require('markdown-it-emoji'); const eleventyPluginFilesMinifier = require('@sherby/eleventy-plugin-files-minifier'); @@ -17,6 +18,12 @@ const isValidEvent = (event) => isValidTitle(event.data.title); const byDate = comparators((event) => event.data.date); module.exports = (eleventyConfig) => { + eleventyConfig.addPlugin(EleventyServerlessBundlerPlugin, { + name: 'onrequest', + functionsDir: './netlify/functions/', + copy: ['src/utils/'], + }); + eleventyConfig.addCollection('events', (collectionApi) => { return collectionApi.getFilteredByGlob('./src/schedule/*.md'); }); diff --git a/.gitignore b/.gitignore index ccd37ad..dffd2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ package-lock.json _site/ .idea/ **/.DS_Store -.cache \ No newline at end of file +.cache +.env +netlify/functions/onrequest/** +!netlify/functions/onrequest/index.js \ No newline at end of file diff --git a/netlify.toml b/netlify.toml index c1017e0..aba5fe9 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,2 +1,9 @@ [build.environment] - TZ='America/Los_Angeles' \ No newline at end of file +TZ = "America/Los_Angeles" + +[[redirects]] +from = "/live/" +to = "/.netlify/functions/onrequest" +status = 200 +force = true +_generated_by_eleventy_serverless = "onrequest" diff --git a/netlify/functions/onrequest/index.js b/netlify/functions/onrequest/index.js new file mode 100644 index 0000000..629190b --- /dev/null +++ b/netlify/functions/onrequest/index.js @@ -0,0 +1,50 @@ +const { EleventyServerless } = require('@11ty/eleventy'); + +// Explicit dependencies for the bundler from config file and global data. +// The file is generated by the Eleventy Serverless Bundler Plugin. +require('./eleventy-bundler-modules.js'); + +async function handler(event) { + let elev = new EleventyServerless('onrequest', { + path: event.path, + query: event.queryStringParameters, + functionsDir: './netlify/functions/', + }); + + try { + return { + statusCode: 200, + headers: { + 'Content-Type': 'text/html; charset=UTF-8', + }, + body: await elev.render(), + }; + } catch (error) { + // Only console log for matching serverless paths + // (otherwise you’ll see a bunch of BrowserSync 404s for non-dynamic URLs during --serve) + if (elev.isServerlessUrl(event.path)) { + console.log('Serverless Error:', error); + } + + return { + statusCode: error.httpStatusCode || 500, + body: JSON.stringify( + { + error: error.message, + }, + null, + 2 + ), + }; + } +} + +// Choose one: +// * Runs on each request: AWS Lambda (or Netlify Function) +// * Runs on first request only: Netlify On-demand Builder +// (don’t forget to `npm install @netlify/functions`) + +exports.handler = handler; + +//const { builder } = require("@netlify/functions"); +//exports.handler = builder(handler); diff --git a/package.json b/package.json index 7e6b025..f048f35 100644 --- a/package.json +++ b/package.json @@ -12,20 +12,23 @@ "recognize": "all-contributors add" }, "dependencies": { - "@11ty/eleventy": "^0.11.1", + "@11ty/eleventy": "1.0.0-beta.3", "@11ty/eleventy-cache-assets": "^2.3.0", + "axios": "^0.24.0", "calendar-link": "^2.0.8", "cross-env": "^7.0.3", "date-fns": "^2.16.1", "generate-comparators": "^1.0.3", "markdown-it": "^12.0.4", - "markdown-it-emoji": "^2.0.0" + "markdown-it-emoji": "^2.0.0", + "querystring": "^0.2.1" }, "devDependencies": { "@sherby/eleventy-plugin-files-minifier": "^1.1.1", "all-contributors-cli": "^6.19.0", "chalk": "^4.1.0", "commander": "^6.2.1", + "dotenv": "^10.0.0", "husky": "^4.3.6", "inquirer": "^7.3.3", "node-emoji": "^1.10.0", diff --git a/src/_data/streamers.json b/src/_data/streamers.json new file mode 100644 index 0000000..1f39dae --- /dev/null +++ b/src/_data/streamers.json @@ -0,0 +1,10 @@ +[ + "5t3phDev", + "BuildingBedrockLayout", + "lunchdev", + "marbiano", + "SomeAnticsDev", + "stepzen_dev", + "ThoriumSim", + "TrostCodes" +] diff --git a/src/_includes/layouts/event.html b/src/_includes/layouts/event.html index 2064850..29f62f6 100644 --- a/src/_includes/layouts/event.html +++ b/src/_includes/layouts/event.html @@ -3,7 +3,7 @@ - {% include partials/favicon.html %} + {% include 'partials/favicon.html' %} @@ -30,10 +30,10 @@ - {% include partials/analytics.html %} + {% include 'partials/analytics.html' %} - {% include partials/header.html %} + {% include 'partials/header.html' %}

{{ title }}

@@ -43,7 +43,7 @@

{{ title }}

{% if calendarLinks.isPastEvent %} -
{% include partials/calendarLinks.html %}{% include partials/join-discord-button.html %}
+
{% include 'partials/calendarLinks.html' %}{% include 'partials/join-discord-button.html' %}
{% endif %} {{ content }} {% case speakers.size %} {% when undefined %} {% when 1 %}

Speaker

{% else %} @@ -101,8 +101,8 @@

Speakers

- {% include partials/event-sesh-command.html %} + {% include 'partials/event-sesh-command.html' %}
- {% include partials/footer.html %} + {% include 'partials/footer.html' %} diff --git a/src/_includes/layouts/homepage.html b/src/_includes/layouts/homepage.html index d4fa4c4..a1b4579 100644 --- a/src/_includes/layouts/homepage.html +++ b/src/_includes/layouts/homepage.html @@ -3,7 +3,7 @@ - {% include partials/favicon.html %} + {% include 'partials/favicon.html' %} @@ -17,10 +17,10 @@ /> Lunch.dev Community Calendar - {% include partials/analytics.html %} + {% include 'partials/analytics.html' %} - {% include partials/header.html %} + {% include 'partials/header.html' %}

Next event

@@ -113,6 +113,6 @@

{{ event.data.title }}

{% endfor %}
- {% include partials/footer.html %} + {% include 'partials/footer.html' %} diff --git a/src/_includes/layouts/page.html b/src/_includes/layouts/page.html index 392051a..27a32e8 100644 --- a/src/_includes/layouts/page.html +++ b/src/_includes/layouts/page.html @@ -3,7 +3,7 @@ - {% include partials/favicon.html %} + {% include 'partials/favicon.html' %} @@ -14,16 +14,16 @@ rel="stylesheet" /> {{ title }} | Lunch.dev Community Calendar - {% include partials/analytics.html %} + {% include 'partials/analytics.html' %} - {% include partials/header.html %} + {% include 'partials/header.html' %}

{{ title }}

{{ content }}
- {% include partials/footer.html %} + {% include 'partials/footer.html' %} diff --git a/src/_includes/layouts/post.html b/src/_includes/layouts/post.html index 652df84..73f1cbb 100644 --- a/src/_includes/layouts/post.html +++ b/src/_includes/layouts/post.html @@ -13,10 +13,10 @@ rel="stylesheet" /> {{ title }} | Lunch.dev Community Calendar - {% include partials/analytics.html %} + {% include 'partials/analytics.html' %} - {% include partials/header.html %} + {% include 'partials/header.html' %}

{{ title }}

diff --git a/src/_includes/partials/calendarLinks.html b/src/_includes/partials/calendarLinks.html index 511eba3..1840529 100644 --- a/src/_includes/partials/calendarLinks.html +++ b/src/_includes/partials/calendarLinks.html @@ -3,21 +3,21 @@
diff --git a/src/live/index.liquid b/src/live/index.liquid new file mode 100644 index 0000000..0f69160 --- /dev/null +++ b/src/live/index.liquid @@ -0,0 +1,38 @@ +--- +title: Community Streamers +layout: layouts/page +permalink: + onrequest: /live/ +--- +{% if online.size == 0 %} +

No one is streaming right now!

+

But check out some channels from around the community.

+ +{% elsif online.size == 1 %} +{% assign featuredChannel = online[0] %} +

{{ featuredChannel }} is streaming right now!

+{% twitch featuredChannel %} + +{% else %} +

{{ online.size }} community members are streaming right now!

+ +{% endif %} + +

Streamer List

+ diff --git a/src/live/live.11tydata.js b/src/live/live.11tydata.js new file mode 100644 index 0000000..f22ed09 --- /dev/null +++ b/src/live/live.11tydata.js @@ -0,0 +1,54 @@ +// require('dotenv').config(); +const axios = require('axios'); +const qs = require('querystring'); + +let accessToken; + +/** + * Determines whether a given channel is streaming right now + * @param {string} channel + * @return {boolean} whether that channel is streaming + */ +async function isStreaming(channel) { + const { + data: { data: streams }, + } = await axios.get(`https://api.twitch.tv/helix/streams?user_login=${channel}`, { + headers: { + 'Client-ID': process.env.TWITCH_CLIENT_ID, + Authorization: `Bearer ${accessToken}`, + }, + }); + + return streams.length > 0; +} + +module.exports = { + eleventyComputed: { + online: async ({ streamers }) => { + if (!process.env.TWITCH_CLIENT_ID) { + return []; + } + + const opts = { + client_id: process.env.TWITCH_CLIENT_ID, + client_secret: process.env.TWITCH_CLIENT_SECRET, + grant_type: 'client_credentials', + scopes: '', + }; + const params = qs.stringify(opts); + + const { data } = await axios.post(`https://id.twitch.tv/oauth2/token?${params}`); + accessToken = data.access_token; + + let activeStreamers = []; + for (let streamer of streamers) { + const live = await isStreaming(streamer); + if (live) { + activeStreamers.push(streamer); + } + } + + return activeStreamers; + }, + }, +};