From c874029a4529bf8d06221d1e1ef682aac72d10d7 Mon Sep 17 00:00:00 2001 From: Nicholas Jitkoff Date: Tue, 18 Jan 2022 23:57:49 -0800 Subject: [PATCH] Better recipe support and opengraph descriptions --- docs/404.html | 33 ++++++ docs/index.html | 10 +- docs/index.src/index.js | 20 +++- docs/manifest.appcache | 2 +- docs/render/recipe.css | 154 ++++++++++++++++++++++++++++ docs/render/recipe.js | 222 +++++++++++++--------------------------- firebase.json | 27 +++-- functions/.gitignore | 1 + functions/index.js | 39 +++++++ functions/package.json | 23 +++++ 10 files changed, 355 insertions(+), 176 deletions(-) create mode 100644 docs/404.html create mode 100644 docs/render/recipe.css create mode 100644 functions/.gitignore create mode 100644 functions/index.js create mode 100644 functions/package.json diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..829eda8 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,33 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + diff --git a/docs/index.html b/docs/index.html index 16f22a0..05842a0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,8 +1,8 @@ - - - - - + + + + + diff --git a/docs/index.src/index.js b/docs/index.src/index.js index 4d70684..47147a5 100644 --- a/docs/index.src/index.js +++ b/docs/index.src/index.js @@ -9,10 +9,25 @@ function dismiss() { document.body.classList.remove("toasting") } -var validTypes = ["image/svg+xml", "application/ld+json"] +function setFavicon(favicon) { + document.getElementById("favicon").href = 'data:image/svg+xml,'+ favicon + '' +} -window.onhashchange = window.onload = function() { +var validTypes = ["image/svg+xml", "application/ld+json"] +window.addEventListener("message", (e) => { + if (e.origin == 'null') { + if (e.data.title) document.title = e.data.title; + if (e.data.favicon) setFavicon(e.data.favicon); + } +}, false); + +window.onhashchange = window.onload = function() { var hash = window.location.hash.substring(1); + if (window.location.search) { + console.log("window.location.search.substring(1)", window.location.search.substring(1)) + window.history.replaceState(null, null, window.location.search.substring(1) + "#" + hash) + } + if (hash.length < 3) { location.href = "/edit"; } else { @@ -56,7 +71,6 @@ window.onhashchange = window.onload = function() { if (validTypes.includes(type)) { let script = '' script = script + " ".repeat(3 - (script.length % 3)) - console.log("script", script.length) preamble = btoa(script); } else { console.log("unknown type, rendering as download") diff --git a/docs/manifest.appcache b/docs/manifest.appcache index 02fd817..36a229b 100644 --- a/docs/manifest.appcache +++ b/docs/manifest.appcache @@ -1,2 +1,2 @@ CACHE MANIFEST -# v25 \ No newline at end of file +# v26 \ No newline at end of file diff --git a/docs/render/recipe.css b/docs/render/recipe.css new file mode 100644 index 0000000..670317f --- /dev/null +++ b/docs/render/recipe.css @@ -0,0 +1,154 @@ +* { + font-family: -apple-system, BlinkMacSystemFont, sans-serif; +} + +body { + margin: 0; +} + +.thumbnail { + width: 100%; + padding-top: 56.25%; + background-color: gray; + background-size: cover; + background-position: center; +} + +img.publisher { + max-width: 12em; + float: right; + margin-top: -0.5em; +} + +.recipe { + max-width: 40em; + margin: auto; + line-height: 133%; +} + +.columns { + display: flex; + gap: 2em; + margin-bottom: 4em; + align-content: center; + justify-content: center; +} + +header { + gap: 2em; + margin-bottom: 2em; +} + +h1 { + margin-top: 2em; +} + +.metadata { + align: right; + display: flex; + flex-wrap: wrap; + gap: 0 1em; + margin-bottom: 1em; + line-height: 2em; +} + +.metadata div:after { + content: "-"; + margin-left: 1em; +} + +.ingredients { + font-weight: 600; + font-size: 100%; + padding-top: 1em; + flex: 0 1 35%; +} + +.instructions { + white-space: pre-wrap; + line-height: 125%; + font-size: 100%; + flex: 0 1 65%; +} + +.ingredient { + padding: 0.625em 1em; + margin: 0 -1em; + line-height: 1.25em; + opacity: 0.75; + cursor: pointer; +} + +.ingredient.complete, +.step.complete { + text-decoration: line-through; + opacity: 0.33; +} + +.ingredient:hover { + opacity: 1.0; + background-color: rgba(128, 128, 128, 0.1) +} + +.step { + padding-top: 2em; + max-width: 40em; + cursor: pointer; +} + +.step:before { + content: ""; + width: 100px; + float: left; + margin-top: -1em; + height: 0.5px; + background-color: currentColor; + opacity: 0.54; +} + +a.action { + border: none; + background: none; + text-transform: uppercase; + text-decoration: none; + color: currentColor; + cursor: pointer; + border-radius: 1em; + line-height: 2em; + padding: 0 1em; +} + +a.action:hover { + background-color: rgba(128, 128, 128, 0.1) +} + +@media screen and (max-width: 480px) { + /* some CSS here */ + .columns { + flex-direction: column; + } + .ingredient, + .instructions, + header { + padding: 0 1em; + } + .ingredient { + margin: 0; + } +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #222; + color: white; + } +} + +@media print { + body { + font-size: 14px; + } + .noprint { + display: none; + } +} \ No newline at end of file diff --git a/docs/render/recipe.js b/docs/render/recipe.js index c76dabf..3c41ae4 100644 --- a/docs/render/recipe.js +++ b/docs/render/recipe.js @@ -1,14 +1,31 @@ +let script = document.currentScript; + + FRACTION_MAP = { - '1/2': '\u00BD', '1/4': '\u00BC', '3/4': '\u00BE', '1/3': '\u2153', '2/3': '\u2154', '1/5': '\u2155', '2/5': '\u2156', '3/5': '\u2157', '4/5': '\u2158', '1/6': '\u2159', '5/6': '\u215A', '1/8': '\u215B', '3/8': '\u215C', '5/8': '\u215D', '7/8': '\u215E', + '1/2': '\u00BD', + '1/4': '\u00BC', + '3/4': '\u00BE', + '1/3': '\u2153', + '2/3': '\u2154', + '1/5': '\u2155', + '2/5': '\u2156', + '3/5': '\u2157', + '4/5': '\u2158', + '1/6': '\u2159', + '5/6': '\u215A', + '1/8': '\u215B', + '3/8': '\u215C', + '5/8': '\u215D', + '7/8': '\u215E', replace: function(string) { - return string.replace(/\d\/\d/g, function(a,b,c) { + return string.replace(/\d\/\d/g, function(a, b, c) { return FRACTION_MAP[a]; }) } } const m = (selector, ...args) => { - var attrs = (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]) && !(args[0] instanceof HTMLElement)) ? args.shift() : {}; + var attrs = (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]) && !(args[0] instanceof HTMLElement)) ? args.shift() : {}; let classes = selector.split("."); if (classes.length > 0) selector = classes.shift(); @@ -22,12 +39,12 @@ const m = (selector, ...args) => { for (let prop in attrs) { if (attrs.hasOwnProperty(prop) && attrs[prop] != undefined) { if (prop.indexOf("data-") == 0) { - let dataProp = prop.substring(5).replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); + let dataProp = prop.substring(5).replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); }); node.dataset[dataProp] = attrs[prop]; } else { node[prop] = attrs[prop]; } - } + } } const append = (child) => { @@ -42,11 +59,11 @@ const m = (selector, ...args) => { function formatTime(time) { const timeRE = /(?-)?P(?:(?[.,\d]+)Y)?(?:(?[.,\d]+)M)?(?:(?[.,\d]+)W)?(?:(?[.,\d]+)D)?T(?:(?[.,\d]+)H)?(?:(?[.,\d]+)M)?(?:(?[.,\d]+)S)?/ - let duration = time.match(timeRE)?.groups; + let duration = time.match(timeRE).groups; console.log("match", duration) if (duration) { time = []; - if (duration.hours > 0 ) time.push(duration.hours + "h"); + if (duration.hours > 0) time.push(duration.hours + "h"); if (duration.minutes > 0) time.push(duration.minutes + "m"); time = time.join(" "); } @@ -56,26 +73,31 @@ function formatTime(time) { function markIngredient(e) { e.target.classList.toggle("complete") } + function highlightStep(e) { -// console.log("e", e.target) -// e.target.parent.children.forEach((i,el) => { -// el.classList.toggle("complete", ) -// } + // console.log("e", e.target) + // e.target.parent.children.forEach((i,el) => { + // el.classList.toggle("complete", ) + // } e.target.classList.toggle("complete") } function render() { - let data = document.body.innerText; + let data = document.body.innerHTML; document.body.innerText = ""; delete document.documentElement.style.display; - let json = JSON.parse(data); - - if (json["@type"] != "Recipe") { - json = (json["@graph"] ?? json).find((item) => item["@type"] == "Recipe") + try { + var json = JSON.parse(data); + if (json["@type"] != "Recipe") { + json = (json["@graph"] ?? json).find((item) => item["@type"] == "Recipe") + } + console.log("Recipe", json); + } catch (e) { + console.log("Data", e, {data}); } - console.log("recipe", json) + if (!json) return; let image = json.image; if (Array.isArray(image)) image = image.shift(); image = image.url || image; @@ -83,161 +105,55 @@ function render() { if (Array.isArray(instructions)) { if (Array.isArray(instructions[0])) instructions = instructions.flat() } else { - instructions = [{text:instructions}]; + instructions = [{ text: instructions }]; } - console.log(instructions); + parent.postMessage({title:json.name, favicon:"🍳"}, "*"); + document.body.appendChild( - m("article.recipe",{}, - m("img.publisher", {src:json.publisher?.image[0]?.url}), - m("img.thumbnail.noprint", {src:image}), - m("header", + m("article.recipe", {}, + image ? m(".thumbnail.noprint", { style: "background-image:url(" + image + ");" }) : null, + + m("header", + m("img.publisher", { src: json.publisher?.image ?.[0]?.url ?? json.publisher ?.logo ?.url }), m("h1", json.name), m(".metadata", - json.nutrition?.calories ? m("div", (json.nutrition?.calories) + " calories") : null, - m("div", json.recipeYield), - json.totalTime ? m("div", formatTime(json.totalTime)) : undefined, - json.author?.name ? m(".author", (json.author?.name)) : null, - "\u2605".repeat(json.aggregateRating?.ratingValue) + " " + parseFloat(json.aggregateRating?.ratingValue).toFixed(1) + " (" + json.aggregateRating?.ratingCount + ")", - // m(".rating", (json.aggregateRating?.ratingValue), (json.aggregateRating?.ratingCount)), - m("button.noprint", {onclick:() => window.print()},"print"), - m("a", {href:json.mainEntityOfPage}, "LINK"), + // json.nutrition?.calories ? m("div", (json.nutrition?.calories) + (parseFloat(json.nutrition?.calories) != NaN ? " calories" : "")) : null, + json.totalTime ? m("div", formatTime(json.totalTime)) : undefined, + m("div", json.recipeYield), + json.author?.name ? m(".author", (json.author?.name)) : null, + (rating = json.aggregateRating) ? [ + parseFloat(rating.ratingValue).toFixed(1), + "\u2606".repeat(1), + rating.ratingCount ? " (" + rating.ratingCount + ")" : null + ].join(" ") : null, + // m(".rating", (json.aggregateRating?.ratingValue), (json.aggregateRating?.ratingCount)), + m("a.action.noprint", { onclick: () => window.print() }, "print"), + m("a.action.noprint", { href: json.mainEntityOfPage }, "link"), - ), - m(".description", json.description), + ), + m(".description", json.description), ), m(".columns", m(".ingredients", - json.recipeIngredient?.map(i => m("div.ingredient", {onclick:markIngredient}, FRACTION_MAP.replace(i))) + json.recipeIngredient?.map(i => m("div.ingredient", { onclick: markIngredient }, FRACTION_MAP.replace(i))) ), m(".instructions", - instructions.map(i => m("div.step", {onclick:highlightStep}, i.text)) + instructions.map(i => m("div.step", { onclick: highlightStep }, i.text)) ) ) ) ) - document.head.appendChild(m("style", {}, ` - * { - font-family:-apple-system, BlinkMacSystemFont, sans-serif; - } + var path = script.src.substring(0, script.src.lastIndexOf(".")); + var cssURL = path + ".css"; - img.thumbnail { - width:100%; - } - - img.publisher { - max-width: 12em; - margin-bottom:1em; - } - .recipe { - max-width:40em; - margin:auto; - line-height:133%; - } - - .columns { - display:flex; - gap:2em; - margin-bottom:4em; - align-content:center; - justify-content:center; - } - header { - gap:2em; - margin-bottom:2em; - } - h1 { - margin-top:2em; - - } - - .metadata { - align:right; - display:flex; - gap:1em; - margin-bottom:1em; - } - .metadata div:after { - content:"-"; - margin-left:1em; - } - .ingredients { - font-weight:600; - font-size:100%; - padding-top:1em; - flex:0 1 35%; - } - - .instructions { - white-space: pre-wrap; - line-height:125%; - font-size:100%; - flex: 0 1 65%; - } - - .ingredient { - padding: 0.625em 1em; - margin:0 -1em; - line-height:1.25em; - opacity:0.75; - cursor:pointer; - } - .ingredient.complete, - .step.complete { - text-decoration:line-through; - opacity:0.33; - } - .ingredient:hover { - opacity:1.0; - background-color:rgba(128,128,128,0.1) - } - - .step { - padding-top:2em; - max-width:40em; - - cursor:pointer; - } - .step:before { - content:""; - width:100px; - float:left; - margin-top:-1em; - height:0.5px; - background-color:currentColor; - opacity:0.54; - } - - button { - border:none; - background:none; - text-transform:uppercase; - } - - @media screen and (max-width: 480px) { - /* some CSS here */ - .columns { - flex-direction:column; - } - } - - @media (prefers-color-scheme: dark) { - body { - background-color:#222; - color:white; - } - } - @media print { - body { font-size:14px; } - .noprint { display:none; } - } - - - `)); + let style = m("link", { rel: "stylesheet", type: "text/css", href: cssURL }) + document.head.appendChild(style); } + window.addEventListener('load', (event) => { render(); -}); +}); \ No newline at end of file diff --git a/firebase.json b/firebase.json index b4e14b4..f6b60a7 100644 --- a/firebase.json +++ b/firebase.json @@ -2,24 +2,23 @@ "hosting": { "public": "docs", "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**", - "samples/**", - "src/**", - "LICENSE", "**.md", - "**/.git/**", "**.sh", - "index.src/**" + "**/.*", + "**/.git/**", + "**/node_modules/**", + "firebase.json", + "index.src/**", + "LICENSE", + "samples/**", + "src/**" ], "rewrites": [ { - "source": "**", - "destination": "/index.html" + "source": "/**/", + "function": "index" } - ], - "cleanUrls": true, - "trailingSlash": false + ] } -} \ No newline at end of file +} + diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 0000000..05e9050 --- /dev/null +++ b/functions/index.js @@ -0,0 +1,39 @@ +const functions = require("firebase-functions"); + +exports.index = functions.https.onRequest((request, response) => { + functions.logger.info("Hello logs!", ); + const agent = request.headers["user-agent"]; + let path = request.path; + if (agent.indexOf("Twitterbot") != -1 || agent.indexOf("facebookexternalhit") != -1) { + let components = path.split("/"); + components.shift(); + components.pop(); + let title = components.shift().replace("_", " "); + let desc = components.shift().replace("_", " "); + let image = components.join("/"); + console.log("components", title, desc, image) + + let content = ""; + if (title) content += `${title}`; + if (desc) content += ``; + if (image) { + if (image.startsWith("http")) { + content += ``; + } else { + image = decodeURIComponent(image) + console.log("image", image); + let codepoints = []; + for (const char of image) { + codepoints.push(char.codePointAt(0).toString(16)); + } + content += ``; + // https://fonts.gstatic.com/s/e/notoemoji/14.0/1f468_1f3fd_200d_1f91d_200d_1f468_1f3fc/72.png + // https://fonts.gstatic.com/s/e/notoemoji/14.0/1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc/72.png + } + } + response.send(content); + } else { + response.redirect("/?" + path); + } + +}); \ No newline at end of file diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 0000000..1dae836 --- /dev/null +++ b/functions/package.json @@ -0,0 +1,23 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "16" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^9.8.0", + "firebase-functions": "^3.14.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "private": true +}