diff --git a/.gitignore b/.gitignore index 1630576736b..593b4553020 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ /dist +/generated /log /node_modules -/rev-manifest.json -/site-data.json -/feed.json diff --git a/content/team.json b/content/team.json index 926b390a324..636af96e6e9 100644 --- a/content/team.json +++ b/content/team.json @@ -3,21 +3,21 @@ "name": "Markus", "twitter": "MarkusTurm", "github": "MarkusTurm", - "text": "Bester Mann.", + "text": "Bester Mann. Toxic ☣️ aber fair. Sorgt für die Bitcoin Mass-Adoption \"one [Currywurstbude](http://www.curry-alm.info/) at a time\" 🌭", "image": "/img/team/markus.jpg" }, { "name": "Gigi", "twitter": "dergigi", "github": "dergigi", - "text": "Der Gigi.", + "text": "Der Gigi leiht dir seine Taschenlampe 🔦 solltest du dich auf deinem Weg im [Kaninchenbau](https://21lessons.com/) mal verlaufen 🕳🐇", "image": "/img/team/gigi.jpg" }, { "name": "Fab", "twitter": "fabthefoxx", "url": "http://fabthefox.com", - "text": "The Fox 🦊", + "text": "The Fox 🦊 verbreitet mit seinem Verlag Aprycot das Bitcoin-Wissen und ist der Wirt hinter der [Media-Theke](https://aprycot.media/thek/) 📙", "image": "/img/team/fab.jpg" }, { @@ -25,13 +25,13 @@ "twitter": "dennisreimann", "github": "dennisreimann", "url": "https://d11n.net", - "text": "d11n", + "text": "Mag Open Source und [BTCPay Server](https://btcpayserver.org/) 💚 und schreibt lieber Software als Texte über sich 👨🏻‍💻", "image": "/img/team/dennis.png" }, { "name": "Daniel", "twitter": "danielwingen", - "text": "Value Of Bitcoin.", + "text": "Kennt den [Value Of Bitcoin](https://valueofbitcoin.com/) 🧊 und ist daher nicht nur Sound Money Maximalist, sondern auch Fiat Minimalist 💸", "image": "/img/team/daniel.jpg" } ] diff --git a/package.json b/package.json index 2e96c5f2f4b..fb5e42bd300 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "node": ">=14.0.0" }, "scripts": { - "clean": "rm -rf rev-manifest.json feed.json site-data.json dist/* && mkdir -p dist", + "clean": "rm -rf dist generated && mkdir -p dist generated", "fetch": "node tasks/fetch_feed.js", "start": "npm-run-all clean fetch -p start:*", - "start:pages": "onchange -i -k 'site-data.json' 'pug.config.js' 'markdown.js' 'src/**/*.pug' 'src/**/*.svg' 'tasks/generate_pages.js' -- npm run build:pages", + "start:pages": "onchange -i -k 'pug.config.js' 'markdown.js' 'content/**' 'generated/**' 'src/**/*.pug' 'src/**/*.svg' 'tasks/generate_pages.js' -- npm run build:pages", "start:styles": "onchange -i -k 'src/**/*.css' -- npm run build:styles", "start:data": "onchange -i -k 'content/**/*' -- npm run build:data", "start:serve": "browser-sync start --config browser-sync.config.js --watch", @@ -23,7 +23,7 @@ "build:styles": "postcss src/css/main.css --output dist/css/main.css", "optimize": "npm-run-all -p optimize:* -s rev", "optimize:styles": "csso dist/css/main.css --output dist/css/main.css", - "rev": "node-file-rev --root=dist dist/css/* dist/js/* dist/img/*.png dist/img/*.svg dist/img/team/*.jpg dist/img/team/*.png", + "rev": "node-file-rev --manifest=generated/rev.json --root=dist dist/css/* dist/js/* dist/img/*.png dist/img/*.svg dist/img/team/*.jpg dist/img/team/*.png", "prod": "npm-run-all build optimize -s build:pages", "images": "node tasks/optimize_images.js" }, diff --git a/pug.config.js b/pug.config.js index e1128addd36..c28bb4a0bf4 100644 --- a/pug.config.js +++ b/pug.config.js @@ -3,10 +3,12 @@ const renderMarkdown = require('./markdown') const slugify = str => str.toLowerCase().replace(/\W/, '-') const random = max => Math.floor(Math.random() * Math.floor(max)) +const shuffle = arr => { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * i); const temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }; return arr } +const formatDate = date => (new Date(date)).toISOString().replace(/T.*/, '').split('-').reverse().join('.') const linkTarget = url => url.startsWith('http') ? '_blank' : null const assetPath = path => { let revs - try { revs = require('./rev-manifest.json') } catch (error) { } + try { revs = require('./generated/rev.json') } catch (error) { } return `${(revs && revs[path]) || path}` } const assetUrl = (path, protocol = 'https') => { @@ -16,9 +18,11 @@ const assetUrl = (path, protocol = 'https') => { module.exports = { basedir: './src/includes', random, + shuffle, slugify, assetUrl, assetPath, + formatDate, linkTarget, renderMarkdown, } diff --git a/src/css/base/elements.css b/src/css/base/elements.css index b081ebfa5c3..d3748852424 100644 --- a/src/css/base/elements.css +++ b/src/css/base/elements.css @@ -38,46 +38,55 @@ h4, h5, h6 { font-family: var(--font-family-head); + letter-spacing: 0.04em; line-height: 1.05; & a { - color: currentColor; + color: inherit; text-decoration: none !important; } } h1 { - font-size: var(--font-size-xxxl); -} - -h2 { font-size: var(--font-size-xxl); } -h3 { +h2 { font-size: var(--font-size-xl); } +h3 { + font-size: var(--font-size-l); +} + h4, h5, h6 { font-size: var(--font-size-m); } a { outline: 0; - color: inherit; - text-decoration: underline; - transition-property: background, color; + color: var(--color-accent); + text-decoration: none; + transition-property: color; transition-duration: var(--transition-duration-fast); + &.plain { + color: inherit; + } + &:hover { @media not all and (hover: none) { - color: var(--color-accent); - text-decoration: underline; + color: var(--color-accent-highlight); + text-decoration: none; + + &.plain { + color: var(--color-accent); + } } } & svg { - transition-property: background, color; + transition-property: color; transition-duration: var(--transition-duration-fast); } } @@ -87,7 +96,7 @@ p { } ul { - margin-left: var(--space-m); + margin-left: 1rem; margin-bottom: var(--space-l); } @@ -112,3 +121,21 @@ img:-moz-loading { [aria-hidden="true"] { display: none; } + +.button { + display: inline-block; + text-align: center; + padding: var(--space-m) var(--space-l); + color: var(--color-neutral-0); + background-color: var(--color-accent); + text-decoration: none; + border-radius: var(--space-m); + + &:hover { + @media not all and (hover: none) { + color: var(--color-neutral-0); + background-color: var(--color-accent-highlight); + text-decoration: none; + } + } +} diff --git a/src/css/base/footer.css b/src/css/base/footer.css index 0fff7b214e3..a713845cfb2 100644 --- a/src/css/base/footer.css +++ b/src/css/base/footer.css @@ -1,4 +1,5 @@ .footer { text-align: center; font-size: var(--font-size-xs); + color: var(--color-secondary); } diff --git a/src/css/base/header.css b/src/css/base/header.css index f25f999bb1a..2413bd9519a 100644 --- a/src/css/base/header.css +++ b/src/css/base/header.css @@ -26,6 +26,17 @@ transition-duration: var(--transition-duration-fast); } + & a { + color: var(--color-body-text); + + &:hover { + @media not all and (hover: none) { + color: var(--color-accent); + text-decoration: none; + } + } + } + & .wrap { @media (--L_and_up) { display: flex; diff --git a/src/css/base/layout.css b/src/css/base/layout.css index cfb5851648b..1f1563c9679 100644 --- a/src/css/base/layout.css +++ b/src/css/base/layout.css @@ -21,4 +21,14 @@ flex: 1; padding-top: var(--space-xl); padding-bottom: var(--space-xl); + + & h1, + & h2 { + margin-bottom: var(--space-l); + color: var(--color-secondary); + } + + & .lead { + margin-bottom: var(--space-xxl); + } } diff --git a/src/css/base/variables.css b/src/css/base/variables.css index 2d7dae9494d..44607aace7a 100644 --- a/src/css/base/variables.css +++ b/src/css/base/variables.css @@ -6,14 +6,19 @@ :root { --color-neutral-0: #fff; - --color-neutral-10: #ddd; + --color-neutral-10: #f6f6f6; + --color-neutral-50: #888; --color-neutral-90: #222; + --color-neutral-95: #1B1B1B; --color-body-text: var(--color-neutral-90); --color-body-bg: var(--color-neutral-0); + --color-card-bg: var(--color-neutral-10); --color-accent: #f7931a; + --color-accent-highlight: #dd7901; --color-derweg: #00B4CF; --color-interview: #151515; + --color-secondary: var(--color-neutral-50); --space-xs: .125rem; --space-s: .25rem; @@ -26,16 +31,12 @@ --transition-duration-medium: 0.75s; --transition-duration-slow: 1.5s; - --border-radius: 16px; - --opacity-text: 0.7; - --font-family-base: sans-serif; --font-family-head: 'The Bold Font', var(--font-family-base); --font-weight-light: 300; --font-weight-normal: 400; --font-weight-medium: 500; - --font-weight-semibold: 600; --font-weight-bold: 700; --font-size-base: 18px; @@ -43,7 +44,7 @@ --font-size-s: .85rem; --font-size-m: 1rem; --font-size-l: 1.25rem; - --font-size-xl: 1.5rem; + --font-size-xl: 1.75rem; --font-size-xxl: 2.5rem; --font-size-xxxl: 4rem; } @@ -51,11 +52,13 @@ :root[data-theme="dark"] { --color-body-text: var(--color-neutral-0); --color-body-bg: var(--color-neutral-90); + --color-card-bg: var(--color-neutral-95); } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --color-body-text: var(--color-neutral-0); --color-body-bg: var(--color-neutral-90); + --color-card-bg: var(--color-neutral-95); } } diff --git a/src/css/sections/podcast.css b/src/css/sections/podcast.css index 0391e715696..e52dab2feed 100644 --- a/src/css/sections/podcast.css +++ b/src/css/sections/podcast.css @@ -1,104 +1,78 @@ -#updates { - position: relative; - padding-top: 120px; +#podcast { +} - @media (--M_and_up) { - padding-bottom: 180px; +.episodes { + display: grid; + grid-gap: var(--space-xl); + grid-template-columns: 1fr; + margin: 0; + list-style: none; + + @media (--up_to_L) { + grid-template-columns: 1fr; } - - & h3 { - @media (--up_to_M) { - font-size: 36px; - margin-bottom: var(--space-l); - } - @media (--M_and_up) { - font-size: 48px; - margin-bottom: var(--space-xl); - } - } - - & .update + .update { - margin-top: var(--space-xxl); - } - - &:after { - display: inline-block; - content: ''; - position: absolute; - left: -65px; - bottom: -80px; - z-index: -1; - background-image: url(../img/bg/updates.svg); - background-repeat: no-repeat; - background-position: 0 100%; - background-size: contain; - max-width: 55%; - width: 369px; - height: 344px; - - @media (--up_to_M) { - display: none; - } + @media (--L_and_up) { + grid-template-columns: 1fr 1fr; } } -.update { - display: block; - text-decoration: none !important; +.episodeItem { + margin: 0; + background-color: var(--color-card-bg); + border-radius: var(--space-l); + @media (--up_to_M) { + padding: var(--space-l); + } @media (--M_and_up) { - display: flex; - align-items: center; - max-width: 53em; - transition-property: background, border, transform; - transition-duration: var(--transition-duration-fast); + padding: var(--space-xl); } - & .image { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--color-neutral-90); - box-shadow: 0px 100px 80px rgba(12, 11, 24, 0.15), 0px 41.7776px 33.4221px rgba(12, 11, 24, 0.107828), 0px 22.3363px 17.869px rgba(12, 11, 24, 0.0894161), 0px 12.5216px 10.0172px rgba(12, 11, 24, 0.075), 0px 6.6501px 5.32008px rgba(12, 11, 24, 0.0605839), 0px 2.76726px 2.21381px rgba(12, 11, 24, 0.0421718); - border: 3px solid var(--color-neutral-90); - border-radius: var(--space-l); + & a { + display: flex; + } + + & .media { + margin-right: var(--space-l); + + & a, & img { - max-width: 80%; - max-height: 80%; - } + display: block; + border-radius: var(--space-s); - @media (--up_to_M) { - width: 100%; - height: 195px; - margin-bottom: 25px; - } - @media (--M_and_up) { - flex: 1 0 42.5%; - height: 275px; - margin-right: 25px; - } - } - - & h4 { - margin-bottom: 15px; - - @media (--up_to_M) { - font-size: var(--font-size-xl); - } - } - - & p { - opacity: var(--opacity-text); - color: var(--color-body-text) !important; - } - - &:hover { - & .image { - @media not all and (hover: none) { - border-color: var(--color-accent); - background-image: linear-gradient(45deg, #1A136E 29.26%, #0D0AB7 92.45%); + @media (--up_to_M) { + height: 60px; + width: 60px; + } + @media (--M_and_up) { + height: 100px; + width: 100px; } } } + + & .meta { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + font-family: var(--font-family-head); + /* color: var(--color-secondary); */ + } + + & .content { + flex: 1; + + & > *:last-child { + margin-bottom: 0; + } + } + + & h3 { + margin-top: var(--space-s); + font-family: var(--font-family-base); + font-weight: var(--font-weight-medium); + font-size: var(--font-size-m); + line-height: 1.2; + } } diff --git a/src/css/sections/team.css b/src/css/sections/team.css index 77f93d41123..5e9ae3247dc 100644 --- a/src/css/sections/team.css +++ b/src/css/sections/team.css @@ -1,7 +1,7 @@ #team { & .members { display: grid; - grid-gap: var(--space-xxl); + grid-gap: var(--space-xl); margin: 0; list-style: none; @@ -18,37 +18,26 @@ & .member { margin: 0; + text-align: center; + background-color: var(--color-card-bg); + border-radius: var(--space-l); + padding: var(--space-xl); & img { - display: block; + display: inline-block; border-radius: 50%; - - @media (--up_to_L) { - height: 87px; - width: 87px; - } - @media (--L_and_up) { - height: 112px; - width: 112px; - } + height: 100px; + width: 100px; } - & h4 { - margin-top: var(--space-m); - font-size: 21px; - } - - & a { - font-size: var(--font-size-xs); - text-decoration: none; - text-transform: uppercase; + & h2 { + margin-top: var(--space-l); + margin-bottom: var(--space-m); + font-size: var(--font-size-xl); } & p { - margin: var(--space-s) 0 var(--space-l); - font-size: var(--font-size-s); - opacity: var(--opacity-text); - max-width: 30em; + margin-bottom: var(--space-l); overflow-wrap: anywhere; } diff --git a/src/includes/mixins.pug b/src/includes/mixins.pug new file mode 100644 index 00000000000..c7a790bc550 --- /dev/null +++ b/src/includes/mixins.pug @@ -0,0 +1,24 @@ +mixin sprite(id) + svg(role="img" title=id)&attributes(attributes) + use(xlink:href=`${assetPath("/img/sprite.svg")}#${id}`) + +mixin episodeItem(e) + article.episodeItem&attributes(attributes) + a.plain(href=e.anchor) + .media + img(src=e.image alt=e.title loading="lazy") + .content + .meta + span= e.categoryName + (e.number ? ` #${e.number}` : '') + time(datetime=e.date)= e.block || formatDate(e.date) + h3=e.titlePlain + +mixin episodeDetails(e) + article.episodeDetails&attributes(attributes) + .media + a(href=e.anchor) + img(src=e.image alt=e.title loading="lazy") + .content + h3: a(href=e.anchor)=e.title + p=formatDate(e.date) + !=e.content diff --git a/src/includes/template.pug b/src/includes/template.pug index f5462870698..d760fe70a83 100644 --- a/src/includes/template.pug +++ b/src/includes/template.pug @@ -1,3 +1,5 @@ +include mixins + block vars - const pageTitle = title ? `${title} · ${site.title}` : site.meta.title @@ -6,10 +8,6 @@ block vars - const pageCard = cardImage || site.meta.cardImage - const themeColor = '#FFFFFF' -mixin sprite(id) - svg(role="img" title=id)&attributes(attributes) - use(xlink:href=`${assetPath("/img/sprite.svg")}#${id}`) - html(lang="en") head diff --git a/src/podcast.pug b/src/podcast.pug index 865088bfda0..ddf05a00655 100644 --- a/src/podcast.pug +++ b/src/podcast.pug @@ -1,13 +1,18 @@ extends /template.pug block main - section - :markdown-it(html linkify typographer) - # Podcast - Content + section#podcast + .lead + h1 Podcast + :markdown-it(html linkify typographer) + Du findest unsere Episoden auf den üblichen Plattformen wie + [Spotify](https://open.spotify.com/show/10408JFbE1n8MexfrBv33r), + [Apple Podcasts](https://podcasts.apple.com/de/podcast/einundzwanzig-der-bitcoin-podcast/id1488229907), + [Overcast](https://overcast.fm/itunes1488229907/einundzwanzig-der-bitcoin-podcast) und + [Anchor](https://anchor.fm/einundzwanzig). + a.button(href="https://anchor.fm/s/d8d3c38/podcast/rss") Abonnieren / RSS - - [Spotify](https://open.spotify.com/show/10408JFbE1n8MexfrBv33r) - - [Apple Podcasts](https://podcasts.apple.com/de/podcast/einundzwanzig-der-bitcoin-podcast/id1488229907) - - [Overcast](https://overcast.fm/itunes1488229907/einundzwanzig-der-bitcoin-podcast) - - [Anchor](https://anchor.fm/einundzwanzig) - - [RSS](https://anchor.fm/s/d8d3c38/podcast/rss) + h2 Alle Episoden + .episodes + each e in episodes + +episodeItem(e) diff --git a/src/team.pug b/src/team.pug index 12b71b607f4..ba0eead0d55 100644 --- a/src/team.pug +++ b/src/team.pug @@ -2,20 +2,21 @@ extends /template.pug block main section#team - h1 Team - ul.members - each m in team + .lead + h1 Team + ul.members(data-shuffle) + each m in shuffle(team) li.member img(src=(assetPath(m.image)) alt=m.name loading="lazy") - h4=m.name - p(style=(m.name.startsWith('Arik') ? 'word-break:break-all;' : null))=m.text + h2=m.name + !=renderMarkdown(m.text) .links if m.twitter - a(href=(m.twitter.startsWith('https://') ? m.twitter : `https://twitter.com/${m.twitter}`) target="_blank" title=`${m.name} on Twitter`) + a.plain(href=(m.twitter.startsWith('https://') ? m.twitter : `https://twitter.com/${m.twitter}`) target="_blank" title=`${m.name} on Twitter`) +sprite("twitter") if m.github - a(href=(m.github.startsWith('https://') ? m.github : `https://github.com/${m.github}`) target="_blank" title=`${m.name} on GitHub`) + a.plain(href=(m.github.startsWith('https://') ? m.github : `https://github.com/${m.github}`) target="_blank" title=`${m.name} on GitHub`) +sprite("github") if m.url - a(href=m.url target="_blank") + a.plain(href=m.url target="_blank") +sprite("url") diff --git a/static/js/main.js b/static/js/main.js index b7122dbad39..398808ef96c 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,3 +1,5 @@ +const shuffle = arr => { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * i); const temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }; return arr } + document.addEventListener("DOMContentLoaded", () => { const $body = document.body const $headerAnchor = document.getElementById('header-anchor') @@ -32,4 +34,12 @@ document.addEventListener("DOMContentLoaded", () => { headerObserver.observe($headerAnchor) } + + // List shuffling + const lists = document.querySelectorAll('[data-shuffle]') + lists.forEach(list => { + const items = Array.from(list.children) + list.innerHTML = "" + shuffle(items).forEach(item => list.appendChild(item)) + }) }) diff --git a/tasks/fetch_feed.js b/tasks/fetch_feed.js index 9b01536afdf..f777bc34623 100644 --- a/tasks/fetch_feed.js +++ b/tasks/fetch_feed.js @@ -2,12 +2,46 @@ const { writeFileSync } = require('fs') const { join, resolve } = require('path') const Parser = require('rss-parser') -const dir = resolve(__dirname, '..') -const dst = join(dir, 'feed.json') +const dir = resolve(__dirname, '..', 'generated') +const write = (name, data) => writeFileSync(join(dir, `${name}.json`), JSON.stringify(data, null, 2)) +const parseInfo = e => { + const titleMatch = e.title.match(/([\w\s]+?)?\s?#(\d+) - (.*)/) + const [, categoryName = 'News', number, titlePlain] = titleMatch ? titleMatch : [,,,e.title] + + const blockMatch = e.contentSnippet.match(/Blockzeit\s(\d+)/) + const block = blockMatch ? parseInt(blockMatch[1]) : null + const category = categoryName.toLowerCase().replace(/\W/, '-') + + return { block, category, categoryName, number, titlePlain } +} ;(async () => { const parser = new Parser() const feed = await parser.parseURL('https://anchor.fm/s/d8d3c38/podcast/rss') - writeFileSync(dst, JSON.stringify(feed, null, 2)) + // Original Anchor-Feed + write('feed', feed) + + // All episodes + const episodes = feed.items.map(e => ({ + title: e.title.trim(), + content: e.content.trim(), + anchor: e.link, + date: e.isoDate, + enclosure: e.enclosure, + duration: e.itunes.duration, + image: e.itunes.image, + season: e.itunes.season, + episode: e.itunes.episode, + guid: e.guid, + ...parseInfo(e) + })) + + write('episodes', episodes) + + // By category/season + write('news', episodes.filter(e => e.category === 'news')) + write('der-weg', episodes.filter(e => e.category === 'der-weg')) + write('interview', episodes.filter(e => e.category === 'interview')) + write('lesestunde', episodes.filter(e => e.category === 'lesestunde')) })() diff --git a/tasks/generate_pages.js b/tasks/generate_pages.js index bef89244473..88585b8618f 100644 --- a/tasks/generate_pages.js +++ b/tasks/generate_pages.js @@ -1,9 +1,10 @@ const pug = require('pug') const { mkdirSync, writeFileSync } = require('fs') const { dirname, resolve } = require('path') + const config = require('../pug.config') -const site = require('../site-data') -const feed = require('../feed.json') +const site = require('../generated/site-data.json') +const episodes = require('../generated/episodes.json') const team = require('../content/team.json') const renderPage = (name, out, data = {}) => { @@ -19,4 +20,4 @@ const renderPage = (name, out, data = {}) => { renderPage('index', 'index', { navCurrent: 'index' }) renderPage('team', 'team/index', { navCurrent: 'team', team }) -renderPage('podcast', 'podcast/index', { navCurrent: 'podcast', feed }) +renderPage('podcast', 'podcast/index', { navCurrent: 'podcast', episodes }) diff --git a/tasks/generate_site_data.js b/tasks/generate_site_data.js index ba9bf470617..35685d5657a 100644 --- a/tasks/generate_site_data.js +++ b/tasks/generate_site_data.js @@ -3,7 +3,7 @@ const { join, resolve } = require('path') const meta = require('../content/meta.json') -const dir = resolve(__dirname, '..') +const dir = resolve(__dirname, '..', 'generated') const dst = join(dir, 'site-data.json') const date = (new Date()).toJSON().split('T')[0]