diff --git a/package-lock.json b/package-lock.json index 631debdb803..05db1ddc18e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,6 +130,11 @@ } } }, + "amplitudejs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/amplitudejs/-/amplitudejs-5.2.0.tgz", + "integrity": "sha512-1Bfl62RfOQElS+5mHyS/ubD8WmgFPzLNMVQmGjQv/mIHGG2KLehic7ayikNkua5Hw3fq0HDJlLVo6/bfdlqGOg==" + }, "ansi-font": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/ansi-font/-/ansi-font-0.0.2.tgz", diff --git a/package.json b/package.json index fb5e42bd300..f3c8e59c0b5 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,13 @@ "scripts": { "clean": "rm -rf dist generated && mkdir -p dist generated", "fetch": "node tasks/fetch_feed.js", - "start": "npm-run-all clean fetch -p start:*", + "start": "npm-run-all clean fetch -p build:static start:*", "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", "build": "npm-run-all clean fetch -p build:*", - "build:static": "cp -r static/* dist", + "build:static": "cp -r static/* dist && cp node_modules/amplitudejs/dist/amplitude.min.js dist/js/amplitude.js", "build:data": "node tasks/generate_site_data.js", "build:pages": "node tasks/generate_pages.js", "build:styles": "postcss src/css/main.css --output dist/css/main.css", @@ -27,6 +27,9 @@ "prod": "npm-run-all build optimize -s build:pages", "images": "node tasks/optimize_images.js" }, + "dependencies": { + "amplitudejs": "5.2.0" + }, "devDependencies": { "autoprefixer": "10.0.1", "browser-sync": "2.26.12", diff --git a/src/category.pug b/src/category.pug new file mode 100644 index 00000000000..02b639945cb --- /dev/null +++ b/src/category.pug @@ -0,0 +1,16 @@ +extends /template.pug + +block main + - const current = episodes.shift() + + section#podcast + h1.centered= categoryName + + h2.centered Aktuelle Episode + .current + +episodePlayer(current).centered + + h2.centered Weitere Episoden + .episodes + each e in episodes + +episodeItem(e) diff --git a/src/css/base/layout.css b/src/css/base/layout.css index 1f1563c9679..82f4a33342d 100644 --- a/src/css/base/layout.css +++ b/src/css/base/layout.css @@ -19,8 +19,8 @@ .main { flex: 1; - padding-top: var(--space-xl); - padding-bottom: var(--space-xl); + padding-top: var(--space-xxl); + padding-bottom: var(--space-xxl); & h1, & h2 { @@ -28,6 +28,12 @@ color: var(--color-secondary); } + & .centered { + text-align: center; + margin-left: auto; + margin-righht: auto; + } + & .lead { margin-bottom: var(--space-xxl); } diff --git a/src/css/base/variables.css b/src/css/base/variables.css index 44607aace7a..74cd16f998f 100644 --- a/src/css/base/variables.css +++ b/src/css/base/variables.css @@ -25,7 +25,7 @@ --space-m: .5rem; --space-l: 1rem; --space-xl: 2rem; - --space-xxl: 4rem; + --space-xxl: 3rem; --transition-duration-fast: 0.25s; --transition-duration-medium: 0.75s; diff --git a/src/css/sections/podcast.css b/src/css/sections/podcast.css index e52dab2feed..922ca356843 100644 --- a/src/css/sections/podcast.css +++ b/src/css/sections/podcast.css @@ -1,12 +1,42 @@ #podcast { + & .categories { + display: inline-flex; + flex-wrap: wrap; + list-style: none; + margin: 0 0 var(--space-xxl) 0; + + & li { + margin: 0 var(--space-m) var(--space-m) 0; + } + + & a { + display: inline-block; + text-align: center; + padding: var(--space-s) var(--space-l); + color: var(--color-neutral-0); + text-decoration: none; + border-radius: var(--space-xl); + border: 1px solid var(--color-secondary); + + &:hover { + @media not all and (hover: none) { + color: var(--color-neutral-0); + background-color: var(--color-secondary); + text-decoration: none; + } + } + } + } + + & .current { + margin-bottom: 4rem; + } } .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; @@ -28,7 +58,6 @@ padding: var(--space-xl); } - & a { display: flex; } @@ -76,3 +105,82 @@ line-height: 1.2; } } + +.player { + margin: 0 auto; + border-radius: var(--space-m); + overflow: hidden; + min-width: 300px; + max-width: 480px; +} + +.player__cover{ + display:block; + width:100% +} +.player__bottom { + color: var(--color-body-text); + background-color: var(--color-card-bg); + padding-bottom: var(--space-l); +} +.player__progress{ + display: block; + background-color: rgba(255,255,255,.25); + appearance:none; + width: 100%; + height: var(--space-m); + margin-bottom: var(--space-m); + cursor:pointer; + border:none +} +.player__progress[value] { + -webkit-appearance: none; +} +.player__progress::-webkit-progress-bar{ + background-color: var(--color-secondary); +} +.player__progress::-moz-progress-bar, +.player__progress::-webkit-progress-value { + background-color: var(--color-accent); +} +.player__time{ + display:flex; + justify-content:space-between; + margin:.5em 0 .75em; + padding:0 .5em; + opacity:.5; + font-size: var(--font-size-s); +} +.player__current-time{ + margin-left:var(--space-m); +} +.player__duration{ + margin-right:var(--space-m); +} +.player__controls{ + display:flex; + align-items: center; + margin-top: var(--space-m); + padding: 0 var(--space-l); +} +.player__button{ + width:70px; + height:70px; + margin-right:var(--space-l); + background-size:cover; + background-repeat:no-repeat; + cursor:pointer +} +.player__button.amplitude-paused{ + background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzUiIGhlaWdodD0iNzUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMSAxKSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIHN0cm9rZT0iIzQyNDM1RiIgY3g9IjM2LjUiIGN5PSIzNi41IiByPSIzNi41Ii8+PHBhdGggZD0iTTQ3LjYzIDM1LjhMMjcuNjQgMjMuNDNjLTEuNDYtLjk2LTIuNjUtLjI2LTIuNjUgMS41NlY0OWMwIDEuODEgMS4xOSAyLjUyIDIuNjUgMS41Nkw0Ny42MyAzOC4ycy43LS41LjctMS4yLS43LTEuMi0uNy0xLjJ6IiBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L2c+PC9zdmc+) +} +.player__button.amplitude-playing{ + background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzUiIGhlaWdodD0iNzUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMSAxKSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIHN0cm9rZT0iIzQyNDM1RiIgY3g9IjM2LjUiIGN5PSIzNi41IiByPSIzNi41Ii8+PHBhdGggZD0iTTMwLjUyIDIzSDI2LjJBMi4yIDIuMiAwIDAgMCAyNCAyNS4ydjIzLjZjMCAxLjIxLjk4IDIuMiAyLjIgMi4yaDQuMzJhMi4yIDIuMiAwIDAgMCAyLjE5LTIuMlYyNS4yYTIuMiAyLjIgMCAwIDAtMi4xOS0yLjJ6TTQ2LjkgMjNoLTQuMzJhMi4yIDIuMiAwIDAgMC0yLjE5IDIuMnYyMy42YzAgMS4yMS45OCAyLjIgMi4xOSAyLjJoNC4zM2EyLjIgMi4yIDAgMCAwIDIuMTktMi4yVjI1LjJjMC0xLjIxLS45OC0yLjItMi4yLTIuMnoiIGZpbGw9IiNGRkYiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvZz48L3N2Zz4=) +} +.player__info{ + flex:1; + text-align: left; +} +.player__album{ + color:var(--color-secondary); +} diff --git a/src/includes/mixins.pug b/src/includes/mixins.pug index c7a790bc550..277cce02dda 100644 --- a/src/includes/mixins.pug +++ b/src/includes/mixins.pug @@ -13,6 +13,46 @@ mixin episodeItem(e) time(datetime=e.date)= e.block || formatDate(e.date) h3=e.titlePlain +mixin episodePlayer(e) + .player.player--single&attributes(attributes) + img.player__cover(src=imgLarge data-amplitude-song-info="cover_art_url" data-amplitude-main-song-info="true") + .player__bottom + progress.player__progress.amplitude-song-played-progress(data-amplitude-main-song-played-progress="true") + + .player__time + span.player__current-time + span.player__current-minutes.amplitude-current-minutes(data-amplitude-main-current-minutes="true") + = ':' + span.player__current-seconds.amplitude-current-seconds(data-amplitude-main-current-seconds="true") + + span.player__duration + span.player__duration-hours.amplitude-duration-hours(data-amplitude-main-duration-hours="true") + = ':' + span.player__duration-minutes.amplitude-duration-minutes(data-amplitude-main-duration-minutes="true") + = ':' + span.player__duration-seconds.amplitude-duration-seconds(data-amplitude-main-duration-seconds="true") + + .player__controls + .player__button.amplitude-play-pause(data-amplitude-main-play-pause="true" id="play-pause") + .player__info + .player__album(data-amplitude-song-info="album" data-amplitude-main-song-info="true") + .player__name(data-amplitude-song-info="name" data-amplitude-main-song-info="true") + + script. + window.Einundzwanzig = { + amplitude: { + songs: [ + { + "name": "#{e.titlePlain}", + "artist": "Einundzwanzig", + "album": "#{e.categoryName + (e.number ? ` #${e.number}` : '')}", + "url": "#{e.enclosure.url}", + "cover_art_url": "#{e.image}" + } + ] + } + }; + mixin episodeDetails(e) article.episodeDetails&attributes(attributes) .media diff --git a/src/includes/template.pug b/src/includes/template.pug index d760fe70a83..2bfc5882b59 100644 --- a/src/includes/template.pug +++ b/src/includes/template.pug @@ -62,4 +62,5 @@ html(lang="en") .wrap p Craig Wright is a fraud. - script(src=assetPath("/js/main.js")) + script(src=assetPath("/js/amplitude.js") defer) + script(src=assetPath("/js/main.js") defer) diff --git a/src/podcast.pug b/src/podcast.pug index ddf05a00655..d2350216edb 100644 --- a/src/podcast.pug +++ b/src/podcast.pug @@ -1,18 +1,32 @@ extends /template.pug block main + - const current = episodes.shift() + section#podcast - .lead + .lead.centered h1 Podcast :markdown-it(html linkify typographer) - Du findest unsere Episoden auf den üblichen Plattformen wie + Du findest uns auf [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 - h2 Alle Episoden + .centered + h2 Kategorien + ul.categories + li: a(href="/podcast/news") News + li: a(href="/podcast/interviews") Interviews + li: a(href="/podcast/lesestunde") Lesestunde + li: a(href="/podcast/der-weg") Der Weg + + h2.centered Aktuelle Episode + .current + +episodePlayer(current).centered + + h2.centered Weitere Episoden .episodes each e in episodes +episodeItem(e) diff --git a/src/team.pug b/src/team.pug index ba0eead0d55..0708b1e2414 100644 --- a/src/team.pug +++ b/src/team.pug @@ -2,8 +2,7 @@ extends /template.pug block main section#team - .lead - h1 Team + h1 Team ul.members(data-shuffle) each m in shuffle(team) li.member diff --git a/static/js/main.js b/static/js/main.js index 398808ef96c..4ecb6beb833 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -42,4 +42,15 @@ document.addEventListener("DOMContentLoaded", () => { list.innerHTML = "" shuffle(items).forEach(item => list.appendChild(item)) }) + + // Player + if (window.Einundzwanzig.amplitude && window.Amplitude) { + window.Amplitude.init(window.Einundzwanzig.amplitude) + + document.querySelector('.player__progress').addEventListener('click', function (e) { + var offset = this.getBoundingClientRect() + var x = e.pageX - offset.left + window.Amplitude.setSongPlayedPercentage((parseFloat(x) / parseFloat(this.offsetWidth)) * 100) + }) + } }) diff --git a/tasks/fetch_feed.js b/tasks/fetch_feed.js index f777bc34623..4e1f732d4ff 100644 --- a/tasks/fetch_feed.js +++ b/tasks/fetch_feed.js @@ -38,10 +38,4 @@ const 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 88585b8618f..17c0ec4e483 100644 --- a/tasks/generate_pages.js +++ b/tasks/generate_pages.js @@ -7,11 +7,12 @@ const site = require('../generated/site-data.json') const episodes = require('../generated/episodes.json') const team = require('../content/team.json') -const renderPage = (name, out, data = {}) => { - const file = resolve(__dirname, '..', `src/${name}.pug`) +const renderPage = (template, out, data = {}) => { + const file = resolve(__dirname, '..', `src/${template}.pug`) const options = Object.assign({}, config, { site }, data) const rendered = pug.renderFile(file, options) - const dst = resolve(__dirname, '..', 'dist', `${out}.html`) + const dest = out === 'index' ? 'index.html' : `${out}/index.html` + const dst = resolve(__dirname, '..', 'dist', dest) const dir = dirname(dst) mkdirSync(dir, { recursive: true }) @@ -19,5 +20,10 @@ const renderPage = (name, out, data = {}) => { } renderPage('index', 'index', { navCurrent: 'index' }) -renderPage('team', 'team/index', { navCurrent: 'team', team }) -renderPage('podcast', 'podcast/index', { navCurrent: 'podcast', episodes }) +renderPage('team', 'team', { navCurrent: 'team', team }) +renderPage('podcast', 'podcast', { navCurrent: 'podcast', episodes: [...episodes] }) + +renderPage('category', 'podcast/news', { navCurrent: 'podcast', categoryName: 'News', episodes: episodes.filter(e => e.category === 'news') }) +renderPage('category', 'podcast/interviews', { navCurrent: 'podcast', categoryName: 'Interviews', episodes: episodes.filter(e => e.category === 'interview') }) +renderPage('category', 'podcast/lesestunde', { navCurrent: 'podcast', categoryName: 'Lesestunde', episodes: episodes.filter(e => e.category === 'lesestunde') }) +renderPage('category', 'podcast/der-weg', { navCurrent: 'podcast', categoryName: 'Der Weg', episodes: episodes.filter(e => e.category === 'der-weg') })