1
0
Fork 0

move to public repo

main
Gustav Lindqvist 3 months ago
commit 47c352b77b

@ -0,0 +1,20 @@
module.exports = {
tagsBySize: (collection) => {
// callback that can return any arbitrary data (since v0.5.3)
// see https://www.11ty.dev/docs/collections/#getall()
const allPosts = collection.getAll();
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
const countPostsByTag = new Map();
allPosts.forEach((post) => {
// short circuit eval sets tags to an empty array if there are no tags set
const tags = post.data.tags || [];
tags.forEach((tag) => {
const count = countPostsByTag.get(tag) || 0;
countPostsByTag.set(tag, count + 1)
})
});
return [...countPostsByTag].sort((a, b) => b[1] - a[1])
}
};

@ -0,0 +1,66 @@
const Image = require('@11ty/eleventy-img');
module.exports = {
featuredImageFilter: (src, sizes, style, postData, callback) => {
if ((typeof src === 'undefined' || !src) && postData.outputPath) {
callback(null, ``);
return null;
}
const documentPath = postData.filePathStem;
src = src.replace(/^\//, '');
let outputPath = '';
try {
outputPath = postData.outputPath
.substring(0, postData.outputPath.lastIndexOf("/")) // Remove document from path
.replace(/^\//, ''); // remove first slash
// If the image is absolute path or external
} catch (error) {
outputPath = '';
}
const folderPath = documentPath
.substring(0, documentPath.lastIndexOf("/") + 1) // Remove document from path
.replace(/^\//, ''); // remove first slash
// If the image is absolute path or external
if (src.startsWith('assets') || src.startsWith('http')) {
} else { // Otherwise assume the file is relative to the document folder
src = folderPath + src;
}
const options = {
widths: [500, 900, 1500, 2500, null],
formats: [null],
outputDir: outputPath,
urlPath: postData.url
};
Image(src, options).then((metadata) => {
let format = '';
for (const key in metadata) {
format = key;
}
let lowsrc = (metadata[format].length > 1) ? metadata[format][1] : metadata[format][0];
let imageSrc = metadata[format][0];
console.log('[' + '\x1b[36m%s\x1b[0m', '11ty Image' + '\x1b[0m' + ']:', 'Created featured image ', imageSrc.url);
callback(null, `<figure class="image ${style}">
<picture>
${Object.values(metadata).map(imageFormat => {
return ` <source type="${imageFormat[0].sourceType}" srcset="${imageFormat.map(entry => entry.srcset).join(", ")}" sizes="${sizes}">`;
}).join("\n")}
<img
src="${lowsrc.url}"
width="${lowsrc.width}"
height="${lowsrc.height}"
alt=""
loading="lazy"
decoding="async">
</picture></figure>`);
}).catch(message => {
console.error('[' + '\x1b[36m%s\x1b[0m', '11ty Image' + '\x1b[0m' + ']:', message)
});
}
};

@ -0,0 +1,253 @@
const _ = require('lodash');
const slugify = require('slugify');
const {DateTime} = require('luxon');
const moment = require('moment/moment');
const markdown = require('./.eleventy.markdown');
const sanitizeHTML = require('sanitize-html');
const fs = require('fs');
const hashString = require('./.eleventy.functions').hashString;
module.exports = {
groupByYear: (collection) => {
return _.chain(collection)
.groupBy((post) => post.date.getFullYear())
.toPairs()
.reverse()
.value();
},
sortByOrder: (items) => {
items = [...items];
return items.sort((a, b) => Math.sign(a.data.order - b.data.order));
},
slug: (input) => {
const options = {
replacement: '-',
remove: /[&,+()$~%.'":*?<>{}←→↑↓↔↕↖↗↘↙°′]/g,
lower: true
};
return slugify(input, options);
},
hasTag: (arr, str) => {
return arr.includes(str);
},
getRecipe: (recipeId, recipes) => {
return recipes.find((recipe) => {
return recipe._id === recipeId;
});
},
getBatchesWithRecipeId: (recipeId, batches) => {
return batches.filter((batch) => {
return batch.recipe._id === recipeId;
});
},
utf8_xml: (inputStr) => {
return inputStr.replace(/[^\x09\x0A\x0D\x20-\xFF\x85\xA0-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm, '');
},
trailingZeros: (num, totalLength) => {
return String(num).padEnd(totalLength, '0');
},
htmlDateString: (dateObj) => {
return DateTime.fromJSDate(dateObj).setZone('Europe/Stockholm').toFormat('yyyy-LL-dd');
},
volumeToBottles: (volume) => {
return Math.round(volume * 3);
},
head: (array, n) => {
if (!Array.isArray(array) || array.length === 0) {
return [];
}
if (n < 0) {
return array.slice(n);
}
return array.slice(0, n);
},
removePostsNotInSeries: (collection, series) => {
return collection.filter((post) => {
return post.data.series.includes(series);
});
},
date: (date, format) => {
return moment(date).format(format);
},
dateFromString: (dateString) => {
return new Date(dateString);
},
dateFolder: (dateObj) => {
return DateTime.fromJSDate(dateObj).setZone('Europe/Stockholm').toFormat('yyyy/MM/dd');
},
shortISODate: (dateObj) => {
return DateTime.fromJSDate(dateObj).setZone('Europe/Stockholm').toFormat('yyyy-MM-dd');
},
longISODate: (dateObj) => {
return DateTime.fromJSDate(dateObj).setZone('Europe/Stockholm').toFormat('yyyy-MM-dd HH:mm');
},
fullISODate: (dateObj) => {
return DateTime.fromJSDate(dateObj).setZone('Europe/Stockholm').toISO();
},
readableDate: (dateObj) => {
return DateTime.fromJSDate(dateObj).setLocale('sv').toFormat('d MMMM');
},
readableLongDate: (dateObj) => {
return DateTime.fromJSDate(dateObj).setLocale('sv').toFormat('d MMMM, yyyy');
},
lowercase: (inputString) => {
return inputString.toLowerCase();
},
firstLetterUpper: (inputString) => {
if (typeof inputString !== 'undefined' && inputString.length) {
return inputString.charAt(0).toUpperCase() + inputString.slice(1);
}
return '';
},
encodeURL: (inputString) => {
return encodeURI(inputString);
},
parseImagePath: (imagePath, postPath) => {
if (imagePath) {
if (imagePath.indexOf('/assets/images/') !== -1) { // If image is in the content folder.
return imagePath;
} else { // If not, prefix the post path to the image path
return postPath + imagePath;
}
} else {
return undefined;
}
},
shuffle: (arr) => {
let m = arr.length,
t,
i;
while (m) {
i = Math.floor(Math.random() * m--);
t = arr[m];
arr[m] = arr[i];
arr[i] = t;
}
return arr;
},
renderUsingMarkdown: (rawString) => {
return markdown.render(rawString);
},
firstLetterUppercase: (rawString) => {
return rawString.charAt(0).toUpperCase() + rawString.slice(1);
},
hashString: hashString,
prettyDigits: (number) => {
return number.toString().replace(/(?!^)(?=(?:\d{3})+(?:\.|$))/gm, ' ');
},
isoString: (date = Date.now()) => new Date(date).toISOString(),
interactionsForPage: (webmentions, comments, url) => {
let interactions = webmentions.concat(comments); // Merge external and local interactions
const allowedTypes = {
likes: ['like-of'],
reposts: ['repost-of'],
replies: ['mention-of', 'in-reply-to']
};
// define which HTML tags you want to allow in the webmention body content
// https://github.com/apostrophecms/sanitize-html#what-are-the-default-options
const allowedHTML = {
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
allowedAttributes: {
a: ['href']
}
};
const clean = (entry) => {
const oldTwitterName = 'LindqvistGustav';
const newTwitterName = 'lindqvistus';
const replaceTwitterName = new RegExp(oldTwitterName, 'ig');
if (entry.content) {
if (typeof entry.content.html !== 'undefined') {
// really long html mentions, usually newsletters or compilations
entry.content.value =
entry.content.html.length > 2000
? `nämnde detta i <a href="${entry['wm-source']}">${entry['wm-source']}</a>`
: sanitizeHTML(entry.content.html, allowedHTML);
} else {
entry.content.value = sanitizeHTML(entry.content.text, allowedHTML);
}
entry.content.value = (typeof entry.content.value.replaceAll === 'function') ? entry.content.value.replaceAll(replaceTwitterName, newTwitterName) : (entry.content.value);
}
const myAccounts = [
'https://twitter.com/Reedyn',
'https://twitter.com/lindqvistus',
'https://mastodon.acc.sunet.se/@reedyn',
'https://jkpg.rocks/@gustav',
'https://mastodon.se/@gustav'
];
entry.author['is-me'] = (typeof entry.author['is-me'] !== 'undefined' && entry.author['is-me']) ? true : myAccounts.includes(entry.author.url);
const urlFields = ['wm-target', 'mention-of'];
if (entry['wm-source'].indexOf('twitter') !== -1) {
entry['action-type'] = 'tweet';
} else if (entry['wm-source'].indexOf('mastodon') !== -1) {
entry['action-type'] = 'toot';
} else {
entry['action-type'] = 'comment';
}
urlFields.forEach((urlField) => {
if (typeof entry[urlField] !== 'undefined') {
entry[urlField] = entry[urlField].replace('http://', 'https://');
entry[urlField] = entry[urlField].replace('https://gustavlindqvist.se/.../vi-ska-inte-lyssna-pa.../', 'https://gustavlindqvist.se/2014/09/15/vi-ska-inte-lyssna-pa-deras-politik-men-pa-deras-valjare/');
entry[urlField] = entry[urlField].replace('https://gustavlindqvist.se/2022/TODO/', 'https://gustavlindqvist.se/2022/todo/');
entry[urlField] = entry[urlField].split('#')[0];
}
});
return entry;
};
// sort webmentions by published timestamp chronologically.
// swap a.published and b.published to reverse order.
const orderByDate = (a, b) => new Date(a.published) - new Date(b.published);
// only allow webmentions that have an author name and a timestamp
const checkRequiredFields = (entry) => {
const {author} = entry;
return !!author && !!author.name;
};
const pageInteractions = interactions
.map(clean)
.filter((mention) => mention['wm-target'] === url)
.filter(checkRequiredFields)
.sort(orderByDate);
const likes = pageInteractions
.filter((mention) => allowedTypes.likes.includes(mention['wm-property']))
.filter((like) => like.author)
.map((like) => like.author);
const reposts = pageInteractions
.filter((mention) => allowedTypes.reposts.includes(mention['wm-property']))
.filter((repost) => repost.author);
const replies = pageInteractions
.filter((mention) => allowedTypes.replies.includes(mention['wm-property']))
.filter((reply) => {
const {author, published, content} = reply;
return author && author.name && published && content;
});
const repostsAndReplies = reposts.concat(replies)
.sort(orderByDate);
return {
likes,
reposts,
replies,
repostsAndReplies
};
}
};

@ -0,0 +1,18 @@
const hashString = (string) => {
const cyrb53 = function (str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
return cyrb53(string).toString(16); // Returns hash in hexadecimal
};
module.exports = {
hashString
};

@ -0,0 +1,104 @@
require('dotenv').config();
const pluginRss = require('@11ty/eleventy-plugin-rss');
const pluginPageAssets = require('eleventy-plugin-page-assets');
const pluginNavigation = require('@11ty/eleventy-navigation');
const pluginSchema = require('@quasibit/eleventy-plugin-schema');
const markdown = require('./.eleventy.markdown.js');
const filters = require('./.eleventy.filters.js');
const asyncFilters = require('./.eleventy.filters.async.js');
const shortcodes = require('./.eleventy.shortcodes.js');
const asyncShortcodes = require('./.eleventy.shortcodes.async.js');
const collections = require('./.eleventy.collections.js');
module.exports = function (eleventyConfig) {
const ELEVENTY_ENVIRONMENT = (typeof process.env.ELEVENTY_ENV !== 'undefined') ? process.env.ELEVENTY_ENV : 'production';
console.log('Building with', ELEVENTY_ENVIRONMENT, 'environment.');
console.log('');
eleventyConfig.setFrontMatterParsingOptions({
excerpt: true,
excerpt_alias: 'custom_excerpt'
});
eleventyConfig.setLiquidOptions({
dynamicPartials: false,
strictFilters: true
});
eleventyConfig.addPlugin(pluginRss, {
posthtmlRenderOptions: {
closingSingleTag: 'default' // opt-out of <img/>-style XHTML single tags
}
});
eleventyConfig.addPlugin(pluginNavigation);
eleventyConfig.addPlugin(pluginPageAssets, {
mode: 'directory',
postsMatching: 'posts/**/*.md|pages/**/*.md',
assetsMatching: '*.png|*.jpg|*.jpeg|*.gif|*.webp|*.gpx|*.fit',
recursive: false,
hashAssets: false
});
eleventyConfig.addPlugin(pluginSchema);
// Filters
Object.keys(filters).forEach((filterName) => {
eleventyConfig.addFilter(filterName, filters[filterName])
});
// Asynchronous filters
Object.keys(asyncFilters).forEach((filterName) => {
eleventyConfig.addNunjucksAsyncFilter(filterName, asyncFilters[filterName])
});
// Shortcodes
Object.keys(shortcodes).forEach((shortcodeName) => {
eleventyConfig.addShortcode(shortcodeName, shortcodes[shortcodeName])
});
// Asynchronous shortcodes
Object.keys(asyncShortcodes).forEach((shortcodeName) => {
eleventyConfig.addNunjucksAsyncShortcode(shortcodeName, asyncShortcodes[shortcodeName])
});
// Collections
Object.keys(collections).forEach((collectionName) => {
eleventyConfig.addCollection(collectionName, collections[collectionName])
});
eleventyConfig.addPassthroughCopy('assets');
eleventyConfig.addPassthroughCopy('CNAME');
eleventyConfig.addPassthroughCopy('robots.txt');
eleventyConfig.addPassthroughCopy({'webfinger.json': '/.well-known/webfinger'});
eleventyConfig.addPassthroughCopy({'favicon': '/'});
eleventyConfig.addPassthroughCopy('_redirects');
// Layouts
eleventyConfig.addLayoutAlias('base', 'base.njk');
eleventyConfig.addLayoutAlias('page', 'page.njk');
eleventyConfig.addLayoutAlias('post', 'post.njk');
eleventyConfig.addLayoutAlias('beer', 'beer.njk');
eleventyConfig.addLayoutAlias('brewery', 'brewery.njk');
eleventyConfig.setDataDeepMerge(true);
eleventyConfig.setLibrary('md', markdown);
return {
// Control which files Eleventy will process
// e.g.: *.md, *.njk, *.html, *.liquid
templateFormats: [
'md',
'hbs',
'njk',
'html',
],
// Opt-out of pre-processing global data JSON files: (default: `liquid`)
dataTemplateEngine: false,
// Pre-process *.md files with: (default: `liquid`)
markdownTemplateEngine: 'njk',
// Pre-process *.html files with: (default: `liquid`)
htmlTemplateEngine: 'njk',
};
};

@ -0,0 +1,81 @@
const slugify = require('slugify');
const markdownItAnchor = require('markdown-it-anchor');
const markdownIt = require('markdown-it');
const markdownItTasklist = require('markdown-it-task-lists');
const markdownItSup = require('markdown-it-sup');
const markdownItAbbr = require('markdown-it-abbr');
const markdownItKbd = require('markdown-it-kbd');
const markdownItFootnotes = require('markdown-it-footnote');
const markdownItTables = require('markdown-it-multimd-table');
const markdownItContainer = require('markdown-it-container');
const markdownItAttrs = require('markdown-it-attrs');
const markdownItImageFigures = require('markdown-it-image-figures');
const markdownItAnchorOptions = {
level: [1, 2, 3, 4],
slugify: (str) =>
slugify(str, {
lower: true,
strict: true,
remove: /[*+~.()'"!:@]/g,
}),
tabIndex: false,
permalink: markdownItAnchor.permalink.headerLink(),
};
const scrollBlock = {
validate: function (params) {
return params.trim().match(/^scroll-block\s*(.*)$/);
},
render: function (tokens, idx) {
const m = tokens[idx].info.trim().match(/^scroll-block\s*(.*)$/);
if (tokens[idx].nesting === 1) {
// opening tag
return '<section class="scroll-block ' + m[1] + '">\n';
} else {
// closing tag
return '</section>\n';
}
}
};
// Customize Markdown library and settings:
let markdown = markdownIt({
html: true,
breaks: true,
linkify: true
})
.use(markdownItTasklist, {
enabled: false,
label: true,
labelAfter: false
})
.use(markdownItSup)
.use(markdownItAbbr)
.use(markdownItKbd)
.use(markdownItFootnotes)
.use(markdownItTables, {
multiline: false,
rowspan: true,
headerless: true,
})
.use(markdownItAnchor, markdownItAnchorOptions)
.use(markdownItContainer, 'scroll-block', scrollBlock)
.use(markdownItAttrs)
.use(markdownItImageFigures, {
lazy: true,
async: true,
copyAttrs: "class",
dataType: true,
figcaption: true
});
markdown.renderer.rules.footnote_block_open = () => (
'<section class="footnotes" aria-labelledby="fotnoter">\n' +
'<h2 class="separator-heading" id="fotnoter"><span>Fotnoter</span></h2>\n' +
'<ol class="footnotes-list">\n'
);
module.exports = markdown;

@ -0,0 +1,59 @@
const EleventyImage = require('@11ty/eleventy-img');
const markdown = require('./.eleventy.markdown');
module.exports = {
image: async function (src, style, alt, caption = undefined) {
if (typeof this.page.outputPath !== 'undefined' && typeof this.page.outputPath.lastIndexOf !== 'undefined') {
const sizes = '100vw';
const documentPath = this.page.filePathStem;
const outputPath = this.page.outputPath
.substring(0, this.page.outputPath.lastIndexOf('/')) // Remove document from path
.replace(/^\//, ''); // remove first slash
// If the image is absolute path or external
const folderPath = documentPath
.substring(0, documentPath.lastIndexOf('/') + 1) // Remove document from path
.replace(/^\//, ''); // remove first slash
// If the image is absolute path or external
if (src.startsWith('assets') || src.startsWith('http')) {
} else { // Otherwise assume the file is relative to the document folder
src = folderPath + src;
}
const options = {
widths: [500, 900, 1500, 2500, null],
formats: [null],
outputDir: outputPath,
urlPath: ''
};
let metadata = await EleventyImage(src, options);
console.log('[' + '\x1b[36m%s\x1b[0m', '11ty Image' + '\x1b[0m' + ']:', 'Created responsive images for', src);
let format = '';
for (const key in metadata) {
format = key;
}
let lowsrc = (metadata[format].length > 1) ? metadata[format][1] : metadata[format][0];
let highsrc = metadata[format][metadata[format].length - 1];
let captionElement = (typeof caption !== 'undefined') ? `<figcaption>${markdown.render(caption)}</figcaption>` : '';
let inlineStyling = (style === '-inline') ? ` style="flex: ${highsrc.width / highsrc.height}"` : '';
return `<figure class="image ${style}"${inlineStyling}>
<picture>
${Object.values(metadata).map(imageFormat => {
return ` <source type="${imageFormat[0].sourceType}" srcset="${imageFormat.map(entry => entry.srcset).join(', ')}" sizes="${sizes}">`;
}).join('\n')}
<img
src="${lowsrc.url}"
width="${lowsrc.width}"
height="${lowsrc.height}"
alt="${alt}"
loading="lazy"
decoding="async">
</picture>${captionElement}</figure>`;
}
return false;
}
};

@ -0,0 +1,168 @@
const fs = require('fs');
const hashString = require('./.eleventy.functions').hashString;
module.exports = {
pack: (pack) => {
function prettyDigits (number) {
return number.toString().replace(/(?!^)(?=(?:\d{3})+(?:\.|$))/gm, ' ');
}
if (pack) {
let outputString = `<section class="pack">`;
outputString += `<ul class="pack__list">`;
pack.contents.forEach((category) => {
const color = (typeof category.color !== 'undefined' && category.color.length) ? category.color : '#808080';
outputString += `<li class="pack__list-item -overview small">
<span class="pack__list-item__left"><svg class="icon -large" role="presentation" style="color: ${color}" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24"><use xmlns:xlink="http://www.w3.org/1999/xlink" href="/assets/icons/${category.icon}.svg#icon"></use></svg></span><span class="pack__list-item__middle">${category.name}<span class="sr-only">:</span></span>
<span class="pack__list-item__right" style="padding-right: 0.3rem">${prettyDigits(category.total_weight)}g</span>
<div class="pack__list__bar" style="width: ${((category.total_weight / pack.total_weight) * 200).toFixed(3)}%; background: ${color};"></div>
</li>`;
});
outputString += `</ul>`;
outputString += `<ul class="statistics__list ${(pack.consumables_weight > 0) ? '-column-count-4' : ''}">
<li class="statistics__list-item">
<span class="statistics__list-item__label">Total vikt<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.total_weight)}g</span>
</li>
<li class="statistics__list-item">
<span class="statistics__list-item__label">Basvikt<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.base_weight)}g</span>
</li>
<li class="statistics__list-item">
<span class="statistics__list-item__label"> kroppen<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.worn_weight)}g</span>
</li>`;
if (pack.consumables_weight > 0) {
outputString += `<li class="statistics__list-item">
<span class="statistics__list-item__label">Förbrukningsvaror<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.consumables_weight)}g</span>
</li>`;
}
outputString += `</ul>`;
outputString += `<p><a href="https://www.packstack.io/pack/${pack.id}">Utrustningslistan ${pack.name} på Packstack</a></p>`;
let equipmentString = ``;
equipmentString += '<div id="equipment" class="pack__list-container -collapsed">';
pack.contents.forEach((item_category) => {
let packList = '';
const color = (typeof item_category.color !== 'undefined' && item_category.color.length) ? item_category.color : '#ffffff';
packList += `<ul class="pack__list">`;
item_category.items.forEach((item) => {
const quantity = (typeof item.quantity !== 'undefined') ? item.quantity : 1;
let metaString = '';
if (typeof item.worn !== 'undefined' && item.worn) {
metaString = ` <span class="pack__list-item__bottom-right secondary"><svg class="icon" role="presentation" style="color: #5E35B1" aria-label="Buren" width="12" height="12" viewBox="0 0 24 24"><use xmlns:xlink="http://www.w3.org/1999/xlink" href="/assets/icons/tshirt-crew.svg#icon"></use></svg></span>`;
} else if (typeof item.consumable !== 'undefined' && item.consumable) {
metaString = ` <span class="pack__list-item__bottom-right secondary">Förbrukningsvara</span>`;
}
packList += `<li class="pack__list-item"><span class="pack__list-item__left secondary">${quantity}st</span> <span class="pack__list-item__bottom secondary">${item.item.name}<span class="sr-only">.</span></span> <span class="pack__list-item__middle">${(typeof item.item.brand !== 'undefined' && item.item.brand !== null) ? item.item.brand.name : ''} <span class="bold">${(typeof item.item.product !== 'undefined' && item.item.product !== null) ? item.item.product.name : ''}</span><span class="sr-only">.</span></span> <span class="pack__list-item__right">${prettyDigits(item.item.weight * quantity)}g</span>${metaString}</li>`;
});
equipmentString += `<span class="pack__label">
<svg class="icon -large" role="presentation" style="color: ${color}" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24"><use xmlns:xlink="http://www.w3.org/1999/xlink" href="/assets/icons/${item_category.icon}.svg#icon"></use></svg>
<span class="uppercase semibold">${item_category.name}</span>
<span class="secondary">(${prettyDigits(item_category.total_weight)}g)</span>
</span>`;
equipmentString += packList;
equipmentString += `</ul>`;
});
equipmentString += '<button id="visa-utrustning" class="Button pack__list-button">Visa all utrustning</button></div>';
outputString += equipmentString;
outputString += `</section>`;
outputString += `<script>document.getElementById('visa-utrustning').addEventListener('click', (event) => { let container = document.getElementById('equipment'); container.classList.remove('-collapsed'); container.removeChild(event.target); });</script>`;
outputString += `<script>const pack = ${JSON.stringify(pack)};</script>`;
return outputString;
}
return '';
},
packStatistics: (pack) => {
function prettyDigits (number) {
return number.toString().replace(/(?!^)(?=(?:\d{3})+(?:\.|$))/gm, ' ');
}
if (pack) {
let outputString = `<section class="packStatistics">`;
outputString += `<ul class="pack__list">`;
pack.contents.forEach((category) => {
const color = (typeof category.color !== 'undefined' && category.color.length) ? category.color : 'var(--color__text)';
outputString += `<li class="pack__list-item -overview small">
<span class="pack__list-item__left"><svg class="icon -large" role="presentation" style="color: ${color}" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24"><use xmlns:xlink="http://www.w3.org/1999/xlink" href="/assets/icons/${category.icon}.svg#icon"></use></svg></span><span class="pack__list-item__middle">${category.name}<span class="sr-only">:</span></span>
<span class="pack__list-item__right" style="padding-right: 0.3rem">${prettyDigits(category.total_weight)}g</span>
<div class="pack__list__bar" style="width: ${((category.total_weight / pack.total_weight) * 200).toFixed(3)}%; background: ${color};"></div>
</li>`;
});
outputString += `</ul>`;
outputString += `<ul class="statistics__list ${(pack.consumables_weight > 0) ? '-column-count-4' : ''}">
<li class="statistics__list-item">
<span class="statistics__list-item__label">Total vikt<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.total_weight)}g</span>
</li>
<li class="statistics__list-item">
<span class="statistics__list-item__label">Basvikt<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.base_weight)}g</span>
</li>
<li class="statistics__list-item">
<span class="statistics__list-item__label"> kroppen<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.worn_weight)}g</span>
</li>`;
if (pack.consumables_weight > 0) {
outputString += `<li class="statistics__list-item">
<span class="statistics__list-item__label">Förbrukningsvaror<span class="sr-only">:</span></span>
<span class="statistics__list-item__value">${prettyDigits(pack.consumables_weight)}g</span>
</li>`;
}
outputString += `</ul>`;
outputString += `<p><a class="Button" href="https://www.packstack.io/pack/${pack.id}">Utrustningslistan ${pack.name} på Packstack</a></p>`;
outputString += `</section>`;
return outputString;
}
return '';
},
packInventory: (pack) => {
function prettyDigits (number) {
return number.toString().replace(/(?!^)(?=(?:\d{3})+(?:\.|$))/gm, ' ');
}
if (pack) {
let outputString = `<section class="packStatistics">`;
let equipmentString = ``;
equipmentString += '<div id="equipment" class="pack__list-container -collapsed">';
pack.contents.forEach((item_category) => {
let packList = '';
const color = (typeof item_category.color !== 'undefined' && item_category.color.length) ? item_category.color : 'var(--color__text)';
packList += `<ul class="pack__list">`;
item_category.items.forEach((item) => {
const quantity = (typeof item.quantity !== 'undefined') ? item.quantity : 1;
let metaString = '';
if (typeof item.worn !== 'undefined' && item.worn) {
metaString = ` <span class="pack__list-item__bottom-right secondary"><svg class="icon" role="presentation" style="color: #5E35B1" aria-label="Buren" width="12" height="12" viewBox="0 0 24 24"><use xmlns:xlink="http://www.w3.org/1999/xlink" href="/assets/icons/tshirt-crew.svg#icon"></use></svg></span>`;
} else if (typeof item.consumable !== 'undefined' && item.consumable) {
metaString = ` <span class="pack__list-item__bottom-right secondary">Förbrukningsvara</span>`;
}
if (typeof item.item.brand !== 'undefined' && item.item.brand !== null) {
packList += `<li class="pack__list-item"><span class="pack__list-item__left secondary">${quantity}st</span> <span class="pack__list-item__bottom secondary">${item.item.name}<span class="sr-only">.</span></span> <span class="pack__list-item__middle"><span class="light">${(typeof item.item.brand !== 'undefined' && item.item.brand !== null) ? item.item.brand.name : ''}</span> <span class="bold">${(typeof item.item.product !== 'undefined' && item.item.product !== null) ? item.item.product.name : ''}</span><span class="sr-only">.</span></span> <span class="pack__list-item__right">${prettyDigits(item.item.weight * quantity)}g</span>${metaString}</li>`;
} else {
packList += `<li class="pack__list-item"><span class="pack__list-item__left secondary">${quantity}st</span> <span class="pack__list-item__middle bold">${item.item.name}<span class="sr-only">.</span></span> <span class="pack__list-item__bottom secondary">${(item.item.notes) ? item.item.notes : ''}<span class="sr-only">.</span></span> <span class="pack__list-item__right">${prettyDigits(item.item.weight * quantity)}g</span>${metaString}</li>`;
}
});
equipmentString += `<span class="pack__label">
<svg class="icon -large" role="presentation" style="color: ${color}" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24"><use xmlns:xlink="http://www.w3.org/1999/xlink" href="/assets/icons/${item_category.icon}.svg#icon"></use></svg>
<span class="uppercase semibold">${item_category.name}</span>
<span class="secondary">(${prettyDigits(item_category.total_weight)}g)</span>
</span>`;
equipmentString += packList;
equipmentString += `</ul>`;
});
equipmentString += '<button id="visa-utrustning" class="Button pack__list-button hidden@no-js">Visa all utrustning</button></div>';
outputString += equipmentString;
outputString += `</section>`;
outputString += `<script>document.getElementById('visa-utrustning').addEventListener('click', (event) => { let container = document.getElementById('equipment'); container.classList.remove('-collapsed'); container.removeChild(event.target); });</script>`;
outputString += `<script>const pack = ${JSON.stringify(pack)};</script>`;
return outputString;
}
return '';
},
checksum: function (filename) {
const fileContent = fs.readFileSync(filename, 'utf8');
return hashString(fileContent);
}
};

@ -0,0 +1,7 @@
README.md
.nojekyll
_sass
.git
.cache
.github
.netlify

@ -0,0 +1,13 @@
# Example for a .env configuration file
# Replace with your values, then rename to `.env`
ELEVENTY_ENV=development
WEBMENTIONIO_TOKEN=YOUR_WEBMENTIONIO_TOKEN_HERE
STEAM_ID=YOUR_STEAM_ID_HERE
STEAM_APIKEY=YOUR_STEAM_APIKEY_HERE
THISDB_APIKEY=YOUR_THISDB_APIKEY_HERE
THISDB_BUCKETID=YOUR_THISDB_BUCKETID_HERE
STRAVA_CLIENTID=YOUR_STRAVA_CLIENTID_HERE
STRAVA_SECRET=YOUR_STRAVA_SECRET_HERE
BREWFATHER_ID=YOUR_BREWFATHER_ID_HERE
BREWFATHER_APIKEY=YOUR_BREWFATHER_APIKEY_HERE

@ -0,0 +1,52 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": [ "error", "single"],
"array-bracket-spacing": 2,
"brace-style": 2,
"comma-dangle": 2,
"comma-spacing": 1,
"comma-style": [1, "last", { "exceptions": { "VariableDeclaration": true } }],
"curly": 2,
"dot-notation": [1, {"allowKeywords": false }],
"eol-last": [2, "always"],
"eqeqeq": 2,
"indent": [1, 4],
"key-spacing": 1,
"keyword-spacing": 1,
"new-cap": 0,
"no-continue": 2,
"no-empty": [2, {"allowEmptyCatch": true}],
"no-multiple-empty-lines": [1, {"max": 2}],
"no-eval": 2,
"no-implied-eval": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-str": 1,
"no-new-func": 2,
"no-plusplus": 2,
"no-sequences": 2,
"no-trailing-spaces": [1, { "skipBlankLines": true }],
"no-undef": 2,
"no-underscore-dangle": 0,
"no-use-before-define": 2,
"no-console": 1,
"no-with": 2,
"one-var": [1, "never"],
"semi": 2,
"semi-spacing": 1,
"space-before-blocks": 1,
"space-before-function-paren": [1, {"anonymous": "always", "named": "never"}],
"space-in-parens": 1,
"space-infix-ops": 1,
"space-unary-ops": 1,
"vars-on-top": 0,
"wrap-iife": 2,
"no-unused-vars": [1, {"vars": "local"}],
"max-len": [1, {"code": 100, "tabWidth": 4, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true, "ignoreStrings": false, "ignoreComments": true}]
}
}

@ -0,0 +1,12 @@
name: Build 11ty on schedule (Netlify)
on:
schedule:
- cron: "0 * * * *"
jobs:
deploy:
runs-on: ubuntu-22.04
steps:
- name: Trigger build on Netlify
run: curl -X POST ${{ secrets.NETLIFY_BUILD_HOOK }}

5
.gitignore vendored

@ -0,0 +1,5 @@
/node_modules/
/_site/
/.idea/
/.cache/
.env

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "openring"]
path = openring
url = https://git.sr.ht/~sircmpwn/openring

@ -0,0 +1,61 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-idiomatic-order"],
"fix": true,
"plugins": [
"stylelint-scss"
],
"ignoreFiles": ["**/*.html"],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true,
"indentation": 4,
"string-quotes": ["double", {"severity": "warning"}],
"no-duplicate-selectors": true,
"color-hex-case": ["lower", {"severity": "warning"}],
"color-hex-length": "short",
"color-named": ["always-where-possible"],
"color-function-notation": ["legacy", {"severity": "warning"}],
"selector-combinator-space-after": "always",
"selector-attribute-quotes": "always",
"selector-attribute-operator-space-before": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-brackets-space-inside": "never",
"declaration-block-trailing-semicolon": "always",
"declaration-colon-space-before": "never",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-newline-after": "always-multi-line",
"number-leading-zero": "never",
"function-url-quotes": "never",
"font-weight-notation": "numeric",
"font-family-name-quotes": "always-where-recommended",
"comment-whitespace-inside": "always",
"comment-empty-line-before": "always",
"rule-empty-line-before": [
"always",
ignore: ["after-comment", "first-nested", "inside-block"]
],
"selector-pseudo-element-colon-notation": "single",
"selector-pseudo-class-parentheses-space-inside": "never",
"media-feature-range-operator-space-before": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-parentheses-space-inside": "never",
"media-feature-colon-space-before": "never",
"media-feature-colon-space-after": "always",
"no-descending-specificity": null,
"no-eol-whitespace": [
true,
{
"severity": "warning",
ignore: ["empty-lines"]
}
],
"block-no-empty": [
true,
{
"severity": "warning",
ignore: ["comments"]
}
]
},
"syntax": "scss"
}

@ -0,0 +1,16 @@
---
title: 404
layout: page.njk
templateClass: post-template
type: page
permalink: 404.html
---
<p class="lead">Sidan hittades inte.</p>
<p><a href="/">Gå till huvudsidan</a> eller <a href="/kontakta-mig">kontakta mig</a> om du har frågor.</p>
<p id="extra-url-info" class="hidden">Sidan kan eventuellt finnas arkiverad i <a class="url-not-found" data-url-prefix="https://web.archive.org/web/">Wayback Machine</a> eller så kan du <a class="url-not-found" data-url-prefix="https://duckduckgo.com/?q=">prova att söka efter sidan i en sökmotor</a>.</p>
<script>
[...document.querySelectorAll('.url-not-found')].forEach((element) => {
element.href = element.getAttribute('data-url-prefix') + window.location.href;
});
document.getElementById('extra-url-info').classList.remove('hidden');
</script>

@ -0,0 +1 @@
gustavlindqvist.se

@ -0,0 +1,5 @@
# [gustavlindqvist.se](https://gustavlindqvist.se)
[![Netlify Status](https://api.netlify.com/api/v1/badges/fa7d9bbb-3148-439a-8d0a-e2422d4d2eca/deploy-status)](https://app.netlify.com/sites/gustavlindqvist/deploys)
Source code for my personal website [gustavlindqvist.se](https://gustavlindqvist.se), built with [Eleventy](https://www.11ty.io).

@ -0,0 +1,198 @@
[
{
"title": "All over the map",
"url": "https://www.nationalgeographic.com/search?q=All%20over%20the%20map&location=srp&type=manual",
"feed": false,
"feature_image": "/content/images/ng_all-over-the-map.webp",
"description": "National Geographics artikelserie om kartor.",
"type": "blog",
"tags": [
"kartor"
]
},
{
"title": "Game Maker's Toolkit",
"url": "https://www.youtube.com/c/MarkBrownGMT",
"feed": false,
"description": "Mark Brown bryter ned och analyserar spel och hur de skapas i sin YouTubekanal.",
"feature_image": "/content/images/gmtk.jpg",
"type": "youtube",
"tags": [
"gaming"
]
},
{
"title": "Elna Dahlstrand",
"url": "https://elnadahlstrand.se/",
"feed": "https://elnadahlstrand.se/feed/",
"description": "Elna Dahlstrands blogg om stigcykling och vardagsliv i Jönköpingstrakten.",
"color": "#5c2855",
"type": "blog",
"tags": [
"mtb",
"jönköping"
]
},
{
"title": "Hemomkringvandring",
"url": "https://hemomkringvandring.se/",
"feed": "https://hemomkringvandring.se/feed/",
"description": "Vandringsblogg där Therése Freid vill visa att det finns vacker natur överallt \" i mitt hemomkring och i ditt\".",
"color": "#ae7012",
"type": "blog",
"tags": [
"friluftsliv",
"vandring",
"småland"
]
},
{
"title": "Brors & Elvis om Europa",
"url": "https://omeuropa.se/",
"feed": "https://omeuropa.se/feed",
"description": "Henrik Brors och Ylva Elvis Nilssons blogg om Europa. Skarpa analyser och fingret på pulsen.",
"feature_image": "/content/images/eu-flag.jpg",
"type": "blog",
"tags": [
"politik",
"eu"
]
},
{
"title": "Cycling Jönköping",
"url": "https://cyclingjonkoping.com/",
"feed": "https://cyclingjonkoping.com/feed/",
"description": "Webbplats och blogg om att cykla i Jönköpingstrakten. Bra tips på stigar och rutter.",
"color": "linear-gradient(0deg,rgba(0,62,145,.7),rgba(0,62,145,.7)),url(/content/images/cycling-jonkoping.jpg) no-repeat center",
"type": "blog",
"tags": [
"mtb",
"jönköping"
]
},
{
"title": "P3 Dystopia",
"url": "https://sverigesradio.se/dystopia",
"feed": "https://api.sr.se/api/rss/pod/26068",
"description": "En podd från Sveriges Radio om vad som kan hända om det värsta inträffar.",
"feature_image": "/content/images/p3dystopia.jpg",
"type": "podcast",
"tags": [
"samhälle"
]
},
{
"title": "Känsligt läge",
"url": "https://sverigesradio.se/kansligtlage",
"feed": "https://api.sr.se/api/rss/pod/22029",
"description": "En podd från Sveriges Radio om relationer oss människor emellan. Hela arkivet är väl värt att lyssna på!",
"feature_image": "/content/images/kansligt-lage.jpg",
"type": "podcast",
"tags": [
"samhälle",
"relationer"
]
},
{
"title": "Konflikt",
"url": "https://sverigesradio.se/konflikt",
"feed": "https://api.sr.se/api/rss/pod/3967",
"description": "Konflikt är en fördjupande podd från Sveriges Radio om världspolitik och svensk vardag.",
"feature_image": "/content/images/konflikt.jpg",
"type": "podcast",
"tags": [
"samhälle",
"politik"
]
},
{
"title": "The Anthropocene Reviewed",
"url": "https://www.wnycstudios.org/podcasts/anthropocene-reviewed",
"feed": "http://feeds.wnyc.org/TheAnthropoceneReviewed",
"description": "“We all know how loving ends. But I want to fall in love with the world anyway, to let it crack me open. I want to feel what there is to feel while I am here.” —John Green",
"color": "repeating-linear-gradient( -45deg, #850055, #850055 15px, #151c46 15px, #151c46 75px )",
"type": "podcast",
"tags": []
},
{
"title": "Mediepodden",
"url": "https://mediepodden.se",
"feed": "http://mediepodden.libsyn.com/rss",
"description": "Mediepodden handlar om det nya medielandskapet med allt som det innebär.",
"feature_image": "https://images.squarespace-cdn.com/content/v1/523d7509e4b0496cd9c5453f/1547815826584-PZQIM7055HYZZMYKV13C/Mediepodden_bw.jpg?format=500w",
"type": "podcast",
"tags": [
"samhälle"
]
},
{
"title": "Cykelpodd med Elna & Helena",
"url": "https://cykelpodd.se",
"feed": "https://feed.podbean.com/cykelpodd/feed.xml",
"description": "En podd om kärleken till stigcykling i alla former.",
"feature_image": "https://mcdn.podbean.com/mf/web/rkr4uc/cykelpodd-poddbean-1600x400.jpg",
"type": "podcast",
"tags": [
"mtb"
]
},
{
"title": "Speljuntan",
"url": "https://www.patreon.com/speljuntan",
"feed": "https://speljuntan.libsyn.com/rss",
"description": "Speljuntan är en podcast om allt som handlar om spel.",
"feature_image": "/content/images/syjuntan.jpg",
"type": "podcast",
"tags": [
"gaming"
]
},
{
"title": "CinemaWins",
"url": "https://www.youtube.com/c/CinemaWins",
"feed": false,
"description": "CinemaWins är YouTubekanalen för dig som vill lära dig att uppskatta filmer mer.",
"type": "youtube",
"tags": [
"film"
]
},
{
"title": "Sfären",
"url": "https://jennymarianilsson.substack.com/archive?sort=new",
"feed": "https://jennymarianilsson.substack.com/feed",
"description": "Jenny Maria Nilssons blogg om skola, samhälle, fotografiur ett utilitaristiskt perspektiv.",
"color": "#661504",
"type": "blog",
"tags": [
"politik",
"filosofi",
"samhälle"
]
},
{
"title": "Emanuel Karlsten",
"url": "http://emanuelkarlsten.se/",
"feed": "http://emanuelkarlsten.se/feed",
"description": "Journalisten Emanuel Karlstens blogg om det som inte funnit en annan plats och om det andra medier inte skriver om.",
"feature_image": "/content/images/emanuel-karlsten.png",
"type": "blog",
"tags": [
"politik",
"samhälle"
]
},
{
"title": "Strong Towns",
"url": "https://www.strongtowns.org/journal",
"feed": "https://www.strongtowns.org/journal?format=rss",
"description": "En webbplats och blogg för rörelsen Strong Towns om hur vi kan bygga bättre städer.",
"color": "repeating-linear-gradient( 0deg, #0c2c41, #0c2c41 1px, #2c5b78 1px, #2c5b78 4px )",
"type": "blog",
"tags": [
"politik",
"samhälle",
"stadsplanering"
]
}
]

@ -0,0 +1,150 @@
[
{
"title": "Silly sausages",
"description": "Brittisk sarkasm i sitt esse. En ledare som The Economist skrev när de europeiska kött- och mejeribranscherna ville begränsa vad som skulle få kallas kött och mjölk.",
"url": "https://web.archive.org/web/20211203033054/https://www.economist.com/leaders/2019/06/29/europe-heroically-defends-itself-against-veggie-burgers",
"date": "2019-06-29",
"feature_image": "/content/images/silly-sauseges.jpg",
"type": "article",
"tags": ["politik"]
},
{
"title": "Gärningsmannen är polis",
"description": "“…en läsvärd och omskakande påminnelse om en myndighet som inte till fullo förtjänat vår tillit.” —Myra Åhbeck Öhrman",
"url": "https://bazarforlag.se/bocker/283369/garningsmannen-ar-polis/",
"color": "#001740",
"date": "2021-03-03",
"type": "book",
"tags": ["politik", "övergrepp"]
},
{
"title": "Why city flags may be the worst-designed thing you've never noticed",
"description": "Roman Mars är besatt av flaggor — och efter att ha tittat på denna filmen kan du vara det du med.",
"feature_image": "/content/images/roman-mars-flags.jpg",
"url": "https://www.youtube.com/watch?v=pnv5iKB2hl4",
"date": "2015-05-14",
"type": "youtube",
"tags": ["flaggor"]
},
{
"title": "Awesome lists",
"description": "😎 Awesome lists about all kinds of interesting topics.",
"feature_image": "/content/images/awesome-lists.jpg",
"url": "https://github.com/sindresorhus/awesome",
"type": "website",
"tags": ["internet"]
},
{
"title": "Old CSS, new CSS",
"description": "“Im here to tell all of you to get off my lawn. Heres a history of CSS and web design, as I remember it.” —Eevee",
"color": "url(/content/images/concrete_seamless.png) #0d1521",
"url": "https://eev.ee/blog/2020/02/01/old-css-new-css/",
"date": "2020-02-01",
"type": "article",
"tags": ["internet", "webdev"]
},
{
"title": "A Journey to the End of the Universe",
"description": "Professor David Kipping beskriver de praktiska effekterna av relativitetsteorin med ett teoretiskt konstant accelererande rymdskepp.",
"feature_image": "/content/images/journey-to-the-end.jpg",
"url": "https://www.youtube.com/watch?v=b_TkFhj9mgk",
"date": "2019-06-07",
"type": "youtube",
"tags": ["vetenskap"]
},
{
"title": "Just The Recipe",
"description": "App som hämtar receptet och ingredienserna från vilken sida som helst och rensar ut livshistorier och annat onödigt.",
"feature_image": "/content/images/justtherecipe.jpg",
"url": "https://www.justtherecipe.com/",
"date": "2021-02-05",
"type": "website",
"tags": ["matlagning"]
},
{
"title": "Geographic projections",
"description": "En webbsida som visualiserar och beskriver många olika världskartprojektioner.",
"feature_image": "/content/images/geo-projections.jpg",
"url": "https://www.geo-projections.com",
"type": "website",
"tags": ["kartor"]
},
{
"title": "A short history of the OReilly animals",
"description": "How lions, tigers, and tarsiers went geek.",
"feature_image": "https://www.oreilly.com/content/wp-content/uploads/sites/2/2019/06/primates-slender-loris-in-waking-sleeping-antique-print-1893-51148-p-8e88ca04dae772621e0068327d53a9ea.jpg",
"url": "https://www.oreilly.com/content/a-short-history-of-the-oreilly-animals/",
"type": "article",
"tags": ["design"]
},
{
"title": "Lisas Menyer",
"description": "Kul projekt om att samla på restaurangmenyer. Webbsidan är designad i äkta 90-talsstil.",
"feature_image": "https://static.wixstatic.com/media/495278_b3a57a22a0594103be241d3872dc2a1a~mv2.gif",
"url": "https://www.lisasmenyer.com",
"type": "website"
},
{
"title": "Logging off",
"description": "“Its asking me if I know any edible plants in our area. Im not sure if I even know the difference between oregano and thyme at the grocery store.” —Celine Nguyen",
"feature_image": "/content/images/compost_thumbnail.png",
"url": "https://two.compost.digital/logging-off/001/",
"date": "2021-09-01",
"type": "article",
"tags": ["solarpunk"]
},
{
"title": "Person in Lotus Position",
"description": "En kort historia över emojis och föregångarna samt processen över hur en ny emoji kommer till.",
"feature_image": "https://99percentinvisible.org/app/uploads/2017/08/lotus-position.jpg",
"url": "https://99percentinvisible.org/episode/person-lotus-position/",
"date": "2017-08-17",
"type": "article",
"tags": ["internet"]
},
{
"title": "How to use undocumented web APIs",
"description": "a fun way to use “secret” undocumented APIs in tiny personal programs.",
"url": "https://jvns.ca/blog/2022/03/10/how-to-use-undocumented-web-apis/",
"color": "#b35222",
"date": "2022-03-10",
"type": "article",
"tags": ["webdev"]
},
{
"title": "How to Pack a Backpack - SMARTER!",
"description": "Matt Shafter visar ett lite annorlunda sätt att packa en ryggsäck.",
"feature_image": "/content/images/mattshafter-packbag.jpg",
"url": "https://www.youtube.com/watch?v=7Ut4R4JMHEE",
"date": "2020-05-20",
"type": "youtube",
"tags": ["friluftsliv"]
},
{
"title": "Are you still you?",
"description": "Capissen38 svarar på frågan om du skulle vara samma person om du ersatte din hjärna med ett chip som gjorde att du tänker 1 000 gånger snabbare..",
"url": "https://web.archive.org/web/20221123143353/https://www.reddit.com/r/Futurology/comments/144ksw/comment/c79tv4a/",
"color": "#ff4601",
"date": "2012-12-02",
"type": "article",
"tags": ["scifi"]
},
{
"title": "I wrote a story for a friend",
"description": "Julian Gough berättar en väldigt känslofylld historia om hur han skrev slutet på Minecraft och den juridiska konflikten som uppstod kring denna.",
"url": "https://theeggandtherock.substack.com/p/i-wrote-a-story-for-a-friend",
"feature_image": "https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72cacb4f-4842-45cd-9ebe-3b6f8468c8c1_2000x1500.png",
"date": "2022-12-07",
"type": "article",
"tags": ["gaming"]
},
{
"title": "My students cheated... A lot",
"description": "En ganska lång berättelse om hur professorn Matt Crumb hanterar en klass som fuskar.",
"url": "https://web.archive.org/web/20220531082301/https://crumplab.com/articles/blog/post_994_5_26_22_cheating/index.html",
"color": "#1e353d",
"date": "2022-05-26",
"type": "article",
"tags": ["lärande"]
}
]

@ -0,0 +1,386 @@
const fetch = require("@11ty/eleventy-fetch");
require('dotenv').config();
module.exports = async () => {
const brewfatherId = process.env.BREWFATHER_ID;
const apiKey = process.env.BREWFATHER_APIKEY;
const rawAuthorizationHeader = Buffer.from(`${brewfatherId}:${apiKey}`);
const authorizationHeader = rawAuthorizationHeader.toString('base64');
const asyncForEach = async function(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
};
const calculateBalanceValue = (og, fg, ibu) => {
og = og * 1000 - 1000;
fg = fg * 1000 - 1000;
const rte = (0.82 * fg) + (0.18 * og);
return (0.8 * ibu / rte).toFixed(2);
};
const convertSRMtoEBC = (srm) => {
return Math.round(srm * 1.97);
};
const setEBCColor = (ebc) => {
const mappingTable = [
{
'ebc': 8,
"hexColor": '#f9e06c'
},
{
'ebc': 12,
"hexColor": '#eaaf1e'
},
{
'ebc': 15,
"hexColor": '#d68019'
},
{
'ebc': 18,
"hexColor": '#b7521a'
},
{
'ebc': 20,
"hexColor": '#9d3414'
},
{
'ebc': 25,
"hexColor": '#892515'
},
{
'ebc': 30,
"hexColor": '#731c0b'
},
{
'ebc': 35,
"hexColor": '#5b0b0a'
},
{
'ebc': 40,
"hexColor": '#450b0a'
},
{
'ebc': 99999,
"hexColor": '#240a0b'
}
];
for (const mapping of mappingTable) {
if (ebc < mapping.ebc) {
return mapping.hexColor
}
}
return undefined;
};
const getBatches = async () => {
const getBatch = async (batchId) => {
try {
const batch = await fetch('https://api.brewfather.app/v1/batches/' + batchId, {
duration: "1h",
type: "json",
directory: ".cache",
fetchOptions: {
headers: {'Authorization': 'Basic ' + authorizationHeader}
}
});
if (typeof batch.measuredOg && typeof