├── .babelrc ├── .gitignore ├── .lfsconfig ├── README.md ├── content ├── week0.md ├── week2.md ├── week3.md ├── week4.md ├── week5.md ├── week6.md └── week7.md ├── fs.js ├── netlify.toml ├── node.api.js ├── package.json ├── public ├── episodes │ ├── week-0.mp3 │ ├── week-2.mp3 │ ├── week-3.mp3 │ ├── week-4.mp3 │ ├── week-5.mp3 │ ├── week-6.mp3 │ └── week-7.mp3 ├── favicon.png ├── robots.txt └── rss │ └── index.xml ├── src ├── App.tsx ├── app.css ├── components │ ├── FancyDiv.tsx │ ├── Footer.tsx │ ├── Header │ │ ├── HeaderRight.tsx │ │ ├── SubscribeBar.tsx │ │ └── index.tsx │ ├── Player.tsx │ ├── ShowList.tsx │ ├── ShowNotes │ │ ├── DownloadBar.tsx │ │ └── index.tsx │ ├── player.css │ └── player.styl ├── index.tsx ├── normalize.css ├── pages │ ├── 404.tsx │ ├── episode.tsx │ └── index.tsx ├── types.js ├── types.ts └── utils │ ├── formatTime.js │ └── formatTime.ts ├── static.config.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-static/babel-preset.js"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | dist 4 | .netlify 5 | **/.DS_Store -------------------------------------------------------------------------------- /.lfsconfig: -------------------------------------------------------------------------------- 1 | [lfs] 2 | url = https://f5644c79-b4f2-49fd-aeb3-0e9b1dfeffa4.netlify.com/.netlify/large-media 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Static Podcast hosting 2 | 3 | use this to build your next free podcast site! see it live at https://reactstaticpodcast.netlify.com/ 4 | 5 | ## Make Your Own 6 | 7 | ```bash 8 | git clone --depth 1 https://github.com/sw-yx/react-static-podcast-hosting 9 | ``` 10 | 11 | The builds for this have been cobbled together so they are a little messy. Here's a quick guide to the npm scripts: 12 | 13 | - `build:stylus`: the player was taken from Syntax.fm, which was written in .styl. We use `stylus` to compile to a single css file which we simply import into `Player`. So run this script whenever you change the .styl file. 14 | 15 | We've put it into the `build` script so that you don't forget to run them but you should just know whats going on in case you need to debug stuff. 16 | 17 | Markdown content goes in the `content` folder, and should have a `mp3URL` field pointing to its associated sound file. In this example I have put that inside `public/episodes/` to avoid file copying on build, but you are welcome to modify this once you are comfortable with the code. See the [podcats documentation](https://github.com/sw-yx/podcats) for an example of the kind of markdown + mp3 pairing that is expected. 18 | 19 | ## Feed validators for testing 20 | 21 | - https://castfeedvalidator.com/ 22 | - https://podba.se/validate/?url=https://reactstaticpodcast.netlify.com/rss/index.xml 23 | - https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Freactstaticpodcast.netlify.com%2Frss%2Findex.xml 24 | 25 | more RSS tips 26 | 27 | - https://resourcecenter.odee.osu.edu/digital-media-production/how-write-podcast-rss-xml 28 | - https://github.com/gpodder/podcast-feed-best-practice/blob/master/podcast-feed-best-practice.md 29 | - https://jackbarber.co.uk/blog/2017-02-14-podcast-rss-feed-template 30 | 31 | ## More Resources that helped me make this 32 | 33 | - RS-TS starter: https://github.com/sw-yx/react-static-typescript-starter 34 | - Apple podcast feed requirements 35 | - https://help.apple.com/itc/podcasts_connect/#/itc1723472cb 36 | - https://feedforall.com/itune-tutorial-tags.htm#category 37 | - NOTE: NEW TAGS introduced in 2017: https://theaudacitytopodcast.com/how-to-start-using-the-new-itunes-podcast-tags-for-ios-11-tap316/ 38 | - NOTE: NEW PODCAST CATEGORIES: https://castos.com/itunes-podcast-category-list/ 39 | - Podcast RSS Feed and Content Generator: https://github.com/sw-yx/podcats 40 | - xml utility https://www.npmjs.com/package/xml 41 | - source: https://github.com/jpmonette/feed 42 | - from: https://benmccormick.org/2017/06/03/rss-atom-json-gatsby/ 43 | - in-browser player https://github.com/wesbos/Syntax/blob/26040ba07dd247ac7cc35eb69428f31ef5863b9e/components/Player.js 44 | - i did not know this existed until too late but doesnt have TS anyway https://github.com/maxnowack/node-podcast 45 | 46 | ## Free podcast music 47 | 48 | - https://www.instantmusicnow.com/ 49 | - http://freemusicarchive.org/ 50 | - https://www.weeditpodcasts.com/11-resources-for-royalty-free-music/ 51 | -------------------------------------------------------------------------------- /content/week0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TypeScript, UI Engineering, and FBT 3 | episode: 0 4 | date: 2019-01-06 5 | mp3URL: episodes/week-0.mp3 6 | description: the first episode 7 | --- 8 | 9 | This episode first debuted on Anchor: https://anchor.fm/swyx/episodes/0--TypeScript--UI-Engineering--and-FBT-e2r98k 10 | 11 | Links discussed: 12 | 13 | - [Typescript](https://reddit.com/r/reactjs/comments/abtrxy/everything_i_write_in_2019_will_be_in_typescript/) 14 | - [React Kawaii](https://reddit.com/r/reactjs/comments/ac8yyt/react_kawaii_cute_react_svg_components/) 15 | - [UI Engineering](https://reddit.com/r/reactjs/comments/ab2184/the_elements_of_ui_engineering/) 16 | - [FBT](https://reddit.com/r/reactjs/comments/ac8m69/fbt_has_been_open_sourced_framework_used_for/) 17 | - [Movies App Lynks](https://reddit.com/r/reactjs/comments/act2sy/only_hooks_functional_components/) 18 | - [React Katas](https://reddit.com/r/reactjs/comments/acwzt0/im_making_a_react_katas_to_practice/) 19 | -------------------------------------------------------------------------------- /content/week2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks in DevTools, and Regrettable Tech Choices 3 | episode: 2 4 | date: 2019-01-20 5 | mp3URL: episodes/week-2.mp3 6 | description: the second episode 7 | --- 8 | 9 | This episode first debuted on Anchor: https://anchor.fm/swyx/episodes/2---Hooks-in-DevTools--and-Regrettable-Tech-Choices-e30ap7 10 | 11 | Links discussed: 12 | 13 | - Hooks support added in DevTools v3.6 -Tech Choices I Regret at Spectrum 14 | - React Best Practices 15 | - A Pokedex using PokeAPI 16 | -------------------------------------------------------------------------------- /content/week3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks enabled, Intro to FP, Building Youtube, and the Top Post of All Time 3 | episode: 3 4 | date: 2019-01-27 5 | mp3URL: episodes/week-3.mp3 6 | description: the third episode 7 | --- 8 | 9 | This episode was supposed to be on Anchor but Anchor screwed up and that was the last straw. 10 | 11 | ## Discussions 12 | 13 | - [Hooks are now merged and enabled - release coming soon](https://www.reddit.com/r/reactjs/comments/aj4uzz/hooks_are_now_merged_and_enabled_release_coming/) 14 | - [9 y/o React Developer Revel Carlberg West talks about React Hooks](https://www.reddit.com/r/reactjs/comments/aiqtb5/9_yo_react_developer_revel_carlberg_west_talks/) 15 | - [How to get team of java developers comfortable with ReactJs?](https://www.reddit.com/r/reactjs/comments/ajqhco/how_to_get_team_of_java_developers_comfortable/) 16 | - [An Intro to Functional Programming](https://www.reddit.com/r/reactjs/comments/aielk8/an_intro_to_functional_programming/) 17 | 18 | ## Projects 19 | 20 | - [Tour, a drag-drop-based travel planning app](https://www.reddit.com/r/reactjs/comments/ak2sjo/after_falling_in_love_with_react_native_less_than/) 21 | - [Build Youtube in React - a free epic length tutorial](https://www.reddit.com/r/reactjs/comments/ai7umk/build_youtube_in_react_a_free_epic_length_tutorial/) 22 | - [The Periodic Table of Elements](https://www.reddit.com/r/reactjs/comments/ajd7i3/i_made_the_periodic_table_of_elements_with_css/) 23 | - [rbx – a UI Framework based on Bulma](https://www.reddit.com/r/reactjs/comments/ait5h2/new_ui_framework_based_on_bulma_written_in/) 24 | -------------------------------------------------------------------------------- /content/week4.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks Day Coming, React as a UI Runtime, React-Redux Roadmap 3 | episode: 4 4 | date: 2019-02-03 5 | mp3URL: episodes/week-4.mp3 6 | description: the fourth episode 7 | --- 8 | 9 | ## Discussions 10 | 11 | - [React 16.8 (The One Hopefully with Hooks) planned for Feb 4](https://www.reddit.com/r/reactjs/comments/al3zj7/react_168_the_one_hopefully_with_hooks_planned/) 12 | - [Weekend Reads: React Docs on Hooks](https://www.reddit.com/r/reactjs/comments/amhr5y/weekend_reads_react_docs_on_hooks/) 13 | - [React as a UI Runtime](https://www.reddit.com/r/reactjs/comments/aml427/react_as_a_ui_runtime/) 14 | - [React-Redux Roadmap: v6, Context, Subscriptions, and Hooks](https://www.reddit.com/r/reactjs/comments/amuhwi/reactredux_roadmap_v6_context_subscriptions_and/) 15 | 16 | ## Projects 17 | 18 | - [Nice and smooth! I've created a collection of animated burgers (HTML/CSS + React)](https://www.reddit.com/r/reactjs/comments/alevhs/nice_and_smooth_ive_created_a_collection_of/) 19 | - [Fully functional WhatsApp Clone using React (Hooks+Suspense), GraphQL, Apollo, TypeScript and PostgreSQL](https://www.reddit.com/r/reactjs/comments/am58h2/fully_functional_whatsapp_clone_using_react/) 20 | - [/dondonleroy's personal site](https://www.reddit.com/r/reactjs/comments/akvead/hey_guys_just_finished_my_personal_website_using/) 21 | - [I had a real nostalgic attack so I created this library](https://www.reddit.com/r/reactjs/comments/alpo0q/i_had_a_real_nostalgic_attack_so_i_created_this/) 22 | - [Introducing react-movable: Drag and drop for your lists and tables. 3.5kB gzipped. Accessible.](https://www.reddit.com/r/reactjs/comments/al1c1p/introducing_reactmovable_drag_and_drop_for_your/) 23 | - [Announcing Overmind, reducing the pain of state management](https://www.reddit.com/r/reactjs/comments/alpjyc/announcing_overmind_reducing_the_pain_of_state/) 24 | - [Road to React with Firebase](https://www.reddit.com/r/reactjs/comments/akr0ax/road_to_react_with_firebase/) 25 | 26 | --- 27 | 28 | Listen til the end for a [easter egg!](https://www.youtube.com/watch?v=ckbhnKVRWUA) 29 | -------------------------------------------------------------------------------- /content/week5.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks Questions Galore, TypeScript + GraphQL + NextJs, React Hooks Contest 3 | episode: 5 4 | date: 2019-02-10 5 | mp3URL: episodes/week-5.mp3 6 | description: the fifth episode 7 | --- 8 | 9 | ## Discussions 10 | 11 | - [React 16.8 - the one with Hooks](https://www.reddit.com/r/reactjs/comments/anonoe/react_v168_the_one_with_hooks_react_blog/) 12 | - [Do you still need Redux with the new Hook APIs](https://www.reddit.com/r/reactjs/comments/ap12uq/do_you_still_need_redux_with_the_new_hook_apis/) 13 | - [Should I ever use classes now?](https://www.reddit.com/r/reactjs/comments/aoecpw/should_i_ever_use_classes_now/) 14 | - [What React Hooks can do for you?](https://www.reddit.com/r/reactjs/comments/ap2cp6/what_react_hooks_can_do_for_you/) 15 | - [Typescript, GraphQL, Nextjs youtube series](https://www.reddit.com/r/reactjs/comments/anf5h4/just_finished_making_a_series_on_typescript/) 16 | - [Firebase + React Hooks Authentication](https://www.reddit.com/r/reactjs/comments/ap4aw7/firebase_react_hooks_authentication/) 17 | 18 | ## Projects 19 | 20 | - [github history](https://www.reddit.com/r/reactjs/comments/anuj5z/browse_the_history_of_any_file_from_github_with/) 21 | - [react RPG](https://www.reddit.com/r/reactjs/comments/anbts1/im_building_an_opensource_rpg_made_with_react/) 22 | - [Diamonds Editor](https://www.reddit.com/r/reactjs/comments/aop9x7/my_first_personal_react_project_diamonds_editor/) 23 | - [Linaria v1.0](https://www.reddit.com/r/reactjs/comments/aojnyd/announcing_linaria_10_zero_runtime_cssinjs_library/) 24 | - [React Hooks Contest](https://www.reddit.com/r/reactjs/comments/anpxum/rreactjs_react_hooks_contest/) 25 | 26 | --- 27 | 28 | Listen til the end for a [easter egg!](https://www.youtube.com/watch?v=ckbhnKVRWUA) 29 | -------------------------------------------------------------------------------- /content/week6.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Performance, DevTools v4, and Windows 95 in Hooks 3 | episode: 6 4 | date: 2019-02-17 5 | mp3URL: episodes/week-6.mp3 6 | description: the sixth episode 7 | --- 8 | 9 | ## Discussions 10 | 11 | - [This benchmark is indeed flawed](https://www.reddit.com/r/reactjs/comments/apopn3/this_benchmark_is_indeed_flawed_dan_abramov_medium/) 12 | - [Early demo of the new React DevTools](https://www.reddit.com/r/reactjs/comments/aq1g6w/early_demo_of_the_new_react_devtools/_) 13 | - [React DevTools v4 will allow inspectable complex hook values](https://www.reddit.com/r/reactjs/comments/aqu26j/react_devtools_v4_will_allow_inspectable_complex/) 14 | - [useDarkMode](https://www.reddit.com/r/reactjs/comments/apiy3t/usedarkmode_add_a_dark_mode_toggle_to_your/) 15 | - [Video-tutorial about performance profiling using Profiler and Chrome Performance Tab](https://www.reddit.com/r/reactjs/comments/ar6ejo/videotutorial_about_performance_profiling_using/) 16 | - [Creating a File Upload Component with React](https://www.reddit.com/r/reactjs/comments/aricfc/creating_a_file_upload_component_with_react/) 17 | 18 | ## Projects 19 | 20 | - [Shards Dashboard React](https://www.reddit.com/r/reactjs/comments/aqk3yg/i_made_a_free_admin_dashboard_template_pack_using/) 21 | - [VSCode React Refactor](https://www.reddit.com/r/reactjs/comments/apvc84/i_made_this_vscode_extension_that_makes_jsx_code/) 22 | - [Windows 95 built with React Hooks](https://www.reddit.com/r/reactjs/comments/arbuf2/windows_95_built_with_react_hooks/) 23 | - [Asperitas full-stack Reddit clone](https://www.reddit.com/r/reactjs/comments/arox51/i_made_a_barebones_fullstack_reddit_clone_to/) 24 | -------------------------------------------------------------------------------- /content/week7.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Progressive React, Open Source Codebases, and a Full-Stack Reddit Clone 3 | episode: 7 4 | date: 2019-02-24 5 | mp3URL: episodes/week-7.mp3 6 | description: the seventh episode 7 | --- 8 | 9 | ## Discussions 10 | 11 | - [Progressive React](https://www.reddit.com/r/reactjs/comments/at7uh4/a_brain_dump_of_all_the_things_you_can_do_to_make/) 12 | - [List of open source React Production Codebases](https://www.reddit.com/r/reactjs/comments/atq0uh/what_are_some_nice_open_source_reactjs_production/) 13 | - [How to use WordPress with React](https://www.reddit.com/r/reactjs/comments/arxu7e/how_to_use_wordpress_with_react/) 14 | - [Beginners guide to route level auth](https://www.reddit.com/r/reactjs/comments/atvy6t/a_beginners_guide_to_route_level_authentication/) 15 | - [RFC: createElement changes and surrounding deprecations ](https://www.reddit.com/r/reactjs/comments/atfq5u/rfc_createelement_changes_and_surrounding/) 16 | 17 | ## Projects 18 | 19 | - [Asperitas - a barebones Full-stack Reddit Clone](https://www.reddit.com/r/reactjs/comments/arox51/i_made_a_barebones_fullstack_reddit_clone_to/) 20 | - [react-spotify-api](https://www.reddit.com/r/reactjs/comments/aslt9y/reactspotifyapi_easily_fetch_data_from_the/) 21 | - [Collection of 300+ React Hooks](https://www.reddit.com/r/reactjs/comments/at1gfz/collection_of_300_react_hooks/) 22 | - [React Resources](https://www.reddit.com/r/reactjs/comments/at59kz/collection_of_2200_react_resources_in_138_topics/) 23 | - [ReactN - state management](https://www.reddit.com/r/reactjs/comments/arsgia/this_weekend_my_project_reactn_a_package_for/) 24 | - [Navi - An async with Suspense and Hooks](https://www.reddit.com/r/reactjs/comments/as0g0e/navi_an_async_router_with_suspense_and_hooks/) 25 | -------------------------------------------------------------------------------- /fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | // const ncp = require('ncp').ncp 3 | // const config = require('../../gatsby-config') 4 | 5 | // const BASE_PATH = config.siteMetadata.rootPath 6 | const BASE_PATH = process.cwd() 7 | // // From http://stackoverflow.com/questions/11293857/fastest-way-to-copy-file-in-node-js 8 | // const copyFile = (sourcePath, targetPath) => 9 | // new Promise((resolve, reject) => { 10 | // console.log(`copy ${BASE_PATH + sourcePath} to ${BASE_PATH + targetPath}`) 11 | // ncp(BASE_PATH + sourcePath, BASE_PATH + targetPath, err => { 12 | // if (err) { 13 | // console.log('oops, failed to copy dir') 14 | // reject() 15 | // } 16 | // resolve() 17 | // }) 18 | // }) 19 | 20 | // const copyDir = (sourcePath, targetPath) => 21 | // new Promise((resolve, reject) => { 22 | // console.log(`copy ${BASE_PATH + sourcePath} to ${BASE_PATH + targetPath}`) 23 | // ncp(BASE_PATH + sourcePath, BASE_PATH + targetPath, err => { 24 | // if (err) { 25 | // console.log('oops, failed to copy dir') 26 | // reject() 27 | // } 28 | // resolve() 29 | // }) 30 | // }) 31 | 32 | const mkDir = path => { 33 | try { 34 | fs.mkdirSync(BASE_PATH + path) 35 | } catch (e) { 36 | //this is probably fine, it may fail if the file already exists 37 | } 38 | } 39 | 40 | const mkFile = (path, content) => { 41 | try { 42 | fs.writeFileSync(BASE_PATH + path, content) 43 | } catch (e) { 44 | //this is probably fine, it may fail if the file already exists 45 | console.log(e) 46 | console.log( 47 | `🔥 Failed to write a file to ${BASE_PATH + 48 | path}, something is probably wrong`, 49 | ) 50 | } 51 | } 52 | 53 | module.exports = { 54 | // copyFile, 55 | mkFile, 56 | mkDir, 57 | // copyDir, 58 | } 59 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "yarn build" 3 | # functions = "lambda" # netlify-lambda reads this 4 | publish = "dist" -------------------------------------------------------------------------------- /node.api.js: -------------------------------------------------------------------------------- 1 | // node.api.js 2 | 3 | export default pluginOptions => ({ 4 | webpack: config => { 5 | config.resolve.extensions.push('.ts', '.tsx') 6 | 7 | // hooks dont work with uglifyjs https://github.com/webpack-contrib/uglifyjs-webpack-plugin/issues/374 8 | if (config.optimization.minimizer) config.optimization.minimizer.shift() 9 | // console.log('----------------------------------------------') 10 | 11 | return config 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-static-podcast-hosting", 3 | "private": true, 4 | "scripts": { 5 | "start": "react-static start", 6 | "build": "yarn run build:stylus && react-static build", 7 | "build:stylus": "stylus src/components/player.styl --out src/components" 8 | }, 9 | "dependencies": { 10 | "@reach/router": "^1.2.1", 11 | "@types/react-helmet": "^5.0.8", 12 | "@types/styled-components": "^4.1.6", 13 | "@types/xml": "^1.0.2", 14 | "axios": "^0.18.0", 15 | "date-fns": "^1.30.1", 16 | "front-matter": "^3.0.1", 17 | "lodash": "^4.17.11", 18 | "markdown-it": "^8.4.2", 19 | "podcats": "^0.1.8", 20 | "react": "^16.8.0-alpha.1", 21 | "react-dom": "^16.8.0-alpha.1", 22 | "react-helmet": "^5.2.0", 23 | "react-hot-loader": "^4.3.12", 24 | "react-icons": "^3.3.0", 25 | "react-static": "^6.0.18", 26 | "react-static-plugin-styled-components": "^6.3.0", 27 | "react-static-plugin-typescript": "^3.1.0", 28 | "styled-components": "^4.1.3", 29 | "stylus": "^0.54.5", 30 | "xml": "^1.0.1" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^10.12.18", 34 | "@types/reach__router": "^1.2.2", 35 | "@types/react": "^16.7.18", 36 | "@types/react-dom": "^16.0.11", 37 | "@types/react-hot-loader": "^4.1.0", 38 | "@types/webpack-env": "^1.13.6", 39 | "concurrently": "^4.1.0", 40 | "convert-tsconfig-paths-to-webpack-aliases": "^0.9.2", 41 | "mp3-duration": "^1.1.0", 42 | "ts-loader": "^5.3.3", 43 | "typescript": "^3.2.2" 44 | }, 45 | "prettier": { 46 | "semi": false, 47 | "singleQuote": true, 48 | "trailingComma": "all" 49 | }, 50 | "resolutions": { 51 | "react": "16.8.0-alpha.1", 52 | "react-dom": "16.8.0-alpha.1" 53 | }, 54 | "version": "1.0.0", 55 | "description": "make your podcast feed AND host it on a React site at the same time", 56 | "main": "static.config.js", 57 | "repository": "https://github.com/sw-yx/react-static-podcast-hosting.git", 58 | "author": "sw-yx ", 59 | "license": "MIT" 60 | } 61 | -------------------------------------------------------------------------------- /public/episodes/week-0.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-0.mp3 -------------------------------------------------------------------------------- /public/episodes/week-2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-2.mp3 -------------------------------------------------------------------------------- /public/episodes/week-3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-3.mp3 -------------------------------------------------------------------------------- /public/episodes/week-4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-4.mp3 -------------------------------------------------------------------------------- /public/episodes/week-5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-5.mp3 -------------------------------------------------------------------------------- /public/episodes/week-6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-6.mp3 -------------------------------------------------------------------------------- /public/episodes/week-7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/episodes/week-7.mp3 -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /public/rss/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This Week in r/Reactjs 5 | https://reactstaticpodcast.netlify.com 6 | en 7 | a podcast feed and blog generator in React and hosted on Netlify 8 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com) 9 | Sun, 24 Feb 2019 13:51:38 GMT 10 | Sun, 24 Feb 2019 13:51:38 GMT 11 | https://reactstaticpodcast.netlify.com 12 | https://github.com/sw-yx/react-static-typescript-starter 13 | This Week in r/Reactjs 14 | This Week in r/Reactjs 15 | Technology 16 | 17 | 18 | 19 | 20 | 21 | clean 22 | 23 | 24 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com 25 | 26 | episodic 27 | copyright REACTSTATICPODCAST_YOURNAMEHERE 28 | 29 | 30 | <![CDATA[Hooks enabled, Intro to FP, Building Youtube, and the Top Post of All Time]]> 31 | 32 | https://reactstaticpodcast.netlify.com/episodes/week-3.mp3 33 | Sun, 27 Jan 2019 00:00:00 GMT 34 | This episode was supposed to be on Anchor but Anchor screwed up and that was the last straw.

35 |

Discussions

36 | 42 |

Projects

43 | 49 | ]]>
50 | This episode was supposed to be on Anchor but Anchor screwed up and that was the last straw.

51 |

Discussions

52 | 58 |

Projects

59 | 65 | ]]>
66 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 67 | 68 | 05:49 69 | no 70 | 71 | full 72 | 3 73 |
74 | 75 | <![CDATA[Performance, DevTools v4, and Windows 95 in Hooks]]> 76 | 77 | https://reactstaticpodcast.netlify.com/episodes/week-6.mp3 78 | Sun, 17 Feb 2019 00:00:00 GMT 79 | Discussions 80 | 88 |

Projects

89 | 95 | ]]>
96 | Discussions 97 | 105 |

Projects

106 | 112 | ]]>
113 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 114 | 115 | 06:52 116 | no 117 | 118 | full 119 | 6 120 |
121 | 122 | <![CDATA[Hooks in DevTools, and Regrettable Tech Choices]]> 123 | 124 | https://reactstaticpodcast.netlify.com/episodes/week-2.mp3 125 | Sun, 20 Jan 2019 00:00:00 GMT 126 | This episode first debuted on Anchor: https://anchor.fm/swyx/episodes/2---Hooks-in-DevTools--and-Regrettable-Tech-Choices-e30ap7

127 |

Links discussed:

128 | 133 | ]]>
134 | This episode first debuted on Anchor: https://anchor.fm/swyx/episodes/2---Hooks-in-DevTools--and-Regrettable-Tech-Choices-e30ap7

135 |

Links discussed:

136 | 141 | ]]>
142 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 143 | 144 | 07:08 145 | no 146 | 147 | full 148 | 2 149 |
150 | 151 | <![CDATA[some words]]> 152 | 153 | https://reactstaticpodcast.netlify.com/episodes/week-7.mp3 154 | Sun, 24 Feb 2019 00:00:00 GMT 155 | Discussions 156 |

sdsd

157 |

Projects

158 |

sdsdw

159 | ]]>
160 | Discussions 161 |

sdsd

162 |

Projects

163 |

sdsdw

164 | ]]>
165 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 166 | 167 | 09:25 168 | no 169 | 170 | full 171 | 7 172 |
173 | 174 | <![CDATA[Hooks Day Coming, React as a UI Runtime, React-Redux Roadmap]]> 175 | 176 | https://reactstaticpodcast.netlify.com/episodes/week-4.mp3 177 | Sun, 03 Feb 2019 00:00:00 GMT 178 | Discussions 179 | 185 |

Projects

186 | 195 |
196 |

Listen til the end for a easter egg!

197 | ]]>
198 | Discussions 199 | 205 |

Projects

206 | 215 |
216 |

Listen til the end for a easter egg!

217 | ]]>
218 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 219 | 220 | 09:35 221 | no 222 | 223 | full 224 | 4 225 |
226 | 227 | <![CDATA[Hooks Questions Galore, TypeScript + GraphQL + NextJs, React Hooks Contest]]> 228 | 229 | https://reactstaticpodcast.netlify.com/episodes/week-5.mp3 230 | Sun, 10 Feb 2019 00:00:00 GMT 231 | Discussions 232 | 240 |

Projects

241 | 248 |
249 |

Listen til the end for a easter egg!

250 | ]]>
251 | Discussions 252 | 260 |

Projects

261 | 268 |
269 |

Listen til the end for a easter egg!

270 | ]]>
271 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 272 | 273 | 09:31 274 | no 275 | 276 | full 277 | 5 278 |
279 | 280 | <![CDATA[TypeScript, UI Engineering, and FBT]]> 281 | 282 | https://reactstaticpodcast.netlify.com/episodes/week-0.mp3 283 | Sun, 06 Jan 2019 00:00:00 GMT 284 | This episode first debuted on Anchor: https://anchor.fm/swyx/episodes/0--TypeScript--UI-Engineering--and-FBT-e2r98k

285 |

Links discussed:

286 | 294 | ]]>
295 | This episode first debuted on Anchor: https://anchor.fm/swyx/episodes/0--TypeScript--UI-Engineering--and-FBT-e2r98k

296 |

Links discussed:

297 | 305 | ]]>
306 | REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com (This Week in r/Reactjs) 307 | 308 | 10:55 309 | no 310 | 311 | full 312 |
313 |
314 |
-------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Root, 4 | Routes, 5 | // withSiteData 6 | } from 'react-static' 7 | // import { Link } from '@reach/router' 8 | import './normalize.css' 9 | import './app.css' 10 | 11 | function App() { 12 | return ( 13 | 14 |
15 | 16 |
17 |
18 | ) 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | /* * { 2 | box-sizing: border-box; 3 | } */ 4 | body { 5 | font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', 6 | Helvetica, Arial, 'Lucida Grande', sans-serif; 7 | font-weight: 300; 8 | font-size: 10px; 9 | margin: 0; 10 | padding: 0; 11 | background: #1d1d1d; 12 | color: #eee; 13 | border-top: 3px solid #f1c15d; 14 | /* line-height: 1.5; */ 15 | } 16 | main { 17 | color: #1d1d1d; 18 | } 19 | header, 20 | main, 21 | footer { 22 | max-width: 1000px; 23 | margin: 0 auto; 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | color: #108db8; 29 | /* font-weight: bold; */ 30 | } 31 | a:hover { 32 | text-decoration: underline; 33 | } 34 | 35 | img { 36 | max-width: 100%; 37 | } 38 | 39 | nav { 40 | width: 100%; 41 | background: #108db8; 42 | } 43 | 44 | nav a { 45 | color: white; 46 | padding: 1rem; 47 | display: inline-block; 48 | } 49 | 50 | .content { 51 | padding: 1rem; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/FancyDiv.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FancyDiv: React.FC = ({ children }) => { 4 | return
{children}
5 | } 6 | export default FancyDiv 7 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | export default styled(Footer)` 5 | text-align: center; 6 | ` 7 | 8 | function Footer(props: any) { 9 | return ( 10 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Header/HeaderRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const HRDiv = styled('div')` 5 | width: 30%; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | img { 10 | width: 80px; 11 | border-radius: 50%; 12 | float: left; 13 | margin-right: 20px; 14 | margin-bottom: 15px; 15 | border: 3px solid #fff; 16 | box-shadow: inset 0 0 10px #f00; 17 | } 18 | * { 19 | margin: 0; 20 | } 21 | ` 22 | export default function HeaderRight() { 23 | // const [Potato, setPotato] = React.useState('potato'); 24 | // React.useEffect(() => { 25 | // // do some stuff 26 | // }) 27 | return ( 28 | 29 | {/*
30 |

YOUR TAGLINE HERE

31 |
*/} 32 |
33 |
34 | swyx 38 |

swyx

39 | 44 | @swyx 45 | 46 |

Infinite Builder

47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Header/SubscribeBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { withSiteData } from 'react-static' 4 | 5 | const SubDiv = styled('ul')` 6 | width: 100%; 7 | margin: 0; 8 | padding: 0; 9 | display: flex; 10 | list-style: none; 11 | align-items: stretch; 12 | flex-wrap: wrap; 13 | justify-content: space-around; 14 | 15 | a { 16 | color: rgba(0, 0, 0, 0.8); 17 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.2); 18 | box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.05); 19 | font-size: 1.5rem; 20 | padding: 0.7rem 1rem; 21 | text-align: center; 22 | border-radius: 3px; 23 | font-family: sans-serif; 24 | font-weight: 100; 25 | transition: all 0.2s; 26 | display: flex; 27 | align-items: center; 28 | } 29 | 30 | @media (max-width: 650px) { 31 | a { 32 | font-size: 1.25rem; 33 | } 34 | } 35 | .iTunes { 36 | background: linear-gradient( 37 | to bottom, 38 | #cd66f6 0%, 39 | #9a3dd1 80%, 40 | #8e34c9 100% 41 | ); 42 | } 43 | .RSS { 44 | background: linear-gradient( 45 | to bottom, 46 | #4366f6 0%, 47 | #433dd1 80%, 48 | #4334c9 100% 49 | ); 50 | } 51 | 52 | .GitHub { 53 | color: white; 54 | background: linear-gradient(to bottom, #333 0%, #999 80%, #767676 100%); 55 | } 56 | 57 | .Netlify { 58 | background: linear-gradient( 59 | to bottom, 60 | #18bea8 0%, 61 | #18bea8 80%, 62 | #18bea8 100% 63 | ); 64 | } 65 | .iTunes { 66 | background: linear-gradient( 67 | to bottom, 68 | #9796f0 0%, 69 | #fb00d4 80%, 70 | #fbc7d4 100% 71 | ); 72 | } 73 | .Spotify { 74 | color: #1db954; 75 | background: #ffffff; 76 | } 77 | .GooglePlay { 78 | background: linear-gradient( 79 | to bottom, 80 | #bbd2c5 0%, 81 | #536976 80%, 82 | #292e49 100% 83 | ); 84 | } 85 | .Overcast { 86 | background: linear-gradient( 87 | to bottom, 88 | #f96a0d 0%, 89 | #f96a0d 80%, 90 | #f96a0d 100% 91 | ); 92 | } 93 | .Reddit { 94 | background: linear-gradient( 95 | to bottom, 96 | #9494ff 0%, 97 | #ff4500 80%, 98 | #ed001c 100% 99 | ); 100 | } 101 | ` 102 | type Props = { 103 | subscribeLinks: { 104 | type: string 105 | url: string 106 | }[] 107 | } 108 | function SubscribeBar({ subscribeLinks }: Props) { 109 | return ( 110 | 111 | {subscribeLinks.map(link => ( 112 |
  • 113 | 119 | {link.type} 120 | 121 |
  • 122 | ))} 123 |
    124 | ) 125 | } 126 | 127 | export default withSiteData(SubscribeBar) 128 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import HeaderRight from './HeaderRight' 4 | import SubscribeBar from './SubscribeBar' 5 | import { Helmet } from 'react-helmet' 6 | import { withRouteData } from 'react-static' 7 | import { Episode } from 'podcats' 8 | 9 | export default withRouteData(Header) 10 | 11 | type Props = { content?: Episode; mostRecentEpisode?: Episode } 12 | type SiteData = { 13 | title: string 14 | description: string 15 | myURL: string 16 | image: string 17 | } 18 | 19 | const HLDiv = styled('div')` 20 | width: 70%; 21 | text-align: center; 22 | display: grid; 23 | align-items: center; 24 | font-size: 1.5rem; 25 | ` 26 | const AHeader = styled('header')` 27 | flex-wrap: wrap; 28 | display: flex; 29 | ` 30 | function Header({ 31 | siteData, 32 | content, 33 | mostRecentEpisode, 34 | }: { siteData: SiteData } & Props) { 35 | const { title, description, myURL, image } = siteData 36 | const curEp = content || mostRecentEpisode 37 | const titleHead = curEp.frontmatter.episode 38 | ? `Ep ${curEp.frontmatter.episode}: ${curEp.frontmatter.title}` 39 | : curEp.frontmatter.title 40 | const desc = content ? description : mostRecentEpisode.frontmatter.description 41 | return ( 42 | 43 | 44 | 45 | {titleHead} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |

    60 | {title} 61 |

    62 | {/* */} 63 |
    64 | 65 | 66 |
    67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Player.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FaPlay, FaPause } from 'react-icons/fa' 3 | // import { formatTime } from '../utils/formatTime' 4 | import './player.css' 5 | import { Episode } from '../types' 6 | 7 | import { withRouteData } from 'react-static' 8 | 9 | function usePrevious(value: T) { 10 | const ref = React.useRef(value) 11 | React.useEffect(() => { 12 | ref.current = value 13 | }) 14 | return ref.current 15 | } 16 | type ShowProps = { 17 | number: number 18 | displayNumber: string 19 | title: string 20 | /** url of the mp3 of the show */ 21 | url: string 22 | } 23 | 24 | export default ({ mostRecentEpisode }: { mostRecentEpisode: Episode }) => { 25 | const Comp = withRouteData(Player(mostRecentEpisode)) 26 | return 27 | } 28 | 29 | type Props = { content?: Episode } 30 | const Player = (mostRecentEpisode: Episode) => ({ content }: Props) => { 31 | const curEp = content || mostRecentEpisode 32 | if (!curEp) return 'no content' 33 | const show: ShowProps = { 34 | number: curEp.frontmatter.episode, 35 | displayNumber: '' + curEp.frontmatter.episode, 36 | title: curEp.frontmatter.title, 37 | url: `/${curEp.frontmatter.mp3URL}`, 38 | } 39 | let lastPlayed = 0 40 | // // for SSR 41 | // if (typeof window !== 'undefined') { 42 | // const { show } = this.props 43 | // const lp = localStorage.getItem(`lastPlayed${show.number}`) 44 | // // eslint-disable-next-line 45 | // if (lp) lastPlayed = JSON.parse(lp).lastPlayed 46 | // } 47 | const [state, _setState] = React.useState({ 48 | progressTime: 50, 49 | playing: false, 50 | duration: 1, 51 | currentTime: lastPlayed, 52 | playbackRate: 1, 53 | timeWasLoaded: lastPlayed !== 0, 54 | showTooltip: false, 55 | tooltipPosition: 0, 56 | tooltipTime: '0:00', 57 | }) 58 | const { 59 | playing, 60 | playbackRate, 61 | progressTime, 62 | currentTime, 63 | duration, 64 | showTooltip, 65 | tooltipPosition, 66 | tooltipTime, 67 | } = state 68 | 69 | const setState = (obj: Partial) => 70 | _setState({ ...state, ...obj }) 71 | let audio = React.createRef() 72 | let progress = React.createRef() 73 | let prevShow = usePrevious(show) 74 | React.useEffect(() => { 75 | audio.current.playbackRate = state.playbackRate 76 | if (show.number !== prevShow.number) { 77 | const lp = localStorage.getItem(`lastPlayed${show.number}`) 78 | if (lp) { 79 | const data = JSON.parse(lp) 80 | // eslint-disable-next-line 81 | setState({ 82 | currentTime: data.lastPlayed, 83 | }) 84 | audio.current.currentTime = data.lastPlayed 85 | } 86 | audio.current.play() 87 | } else { 88 | localStorage.setItem( 89 | `lastPlayed${show.number}`, 90 | JSON.stringify({ lastPlayed: currentTime }), 91 | ) 92 | } 93 | }) 94 | 95 | const timeUpdate = (e: React.SyntheticEvent) => { 96 | const { timeWasLoaded } = state 97 | // Check if the user already had a current time 98 | if (timeWasLoaded) { 99 | const lp = localStorage.getItem(`lastPlayed${show.number}`) 100 | if (lp) { 101 | e.currentTarget.currentTime = JSON.parse(lp).lastPlayed 102 | } 103 | setState({ timeWasLoaded: false }) 104 | } else { 105 | const { currentTime = 0, duration = 1 } = e.currentTarget 106 | 107 | const progressTime = (currentTime / duration) * 100 108 | if (Number.isNaN(progressTime)) return 109 | setState({ progressTime, currentTime, duration }) 110 | } 111 | } 112 | 113 | const togglePlay = () => { 114 | const method = playing ? 'pause' : 'play' 115 | audio.current[method]() 116 | } 117 | 118 | const scrubTime = (eventData: React.MouseEvent) => 119 | (eventData.nativeEvent.offsetX / progress.current.offsetWidth) * 120 | audio.current.duration 121 | 122 | const scrub = (e: React.MouseEvent) => { 123 | audio.current.currentTime = +scrubTime(e) 124 | } 125 | 126 | const seekTime = (e: React.MouseEvent) => { 127 | setState({ 128 | tooltipPosition: e.nativeEvent.offsetX, 129 | tooltipTime: formatTime(scrubTime(e)), 130 | }) 131 | } 132 | 133 | const playPause = () => { 134 | setState({ playing: !audio.current.paused }) 135 | // const method = audio.current.paused ? 'add' : 'remove' 136 | // document.querySelector('.bars').classList[method]('bars--paused') // 💩 137 | } 138 | 139 | const volume: React.ChangeEventHandler = e => { 140 | audio.current.volume = +e.currentTarget.value 141 | } 142 | 143 | const speed = (change: number) => { 144 | const playbackRateMax = 2.5 145 | const playbackRateMin = 0.75 146 | // eslint-disable-next-line 147 | let playbackRate = state.playbackRate + change 148 | 149 | if (playbackRate > playbackRateMax) { 150 | playbackRate = playbackRateMin 151 | } 152 | 153 | if (playbackRate < playbackRateMin) { 154 | playbackRate = playbackRateMax 155 | } 156 | 157 | setState({ playbackRate }) 158 | } 159 | const speedUp = () => speed(0.25) 160 | 161 | const speedDown = () => speed(-0.25) 162 | 163 | // // currently this is a bug only in produciton - duration is always infinity in git LFS 164 | // const playerTime = `${formatTime(currentTime)}` 165 | const playerTime = `${formatTime(currentTime)} / ${formatTime(duration)}` 166 | return ( 167 |
    168 |
    169 | 177 |
    178 | 179 |
    180 | {/* eslint-disable */} 181 |
    { 186 | setState({ showTooltip: true }) 187 | }} 188 | onMouseLeave={() => { 189 | setState({ showTooltip: false }) 190 | }} 191 | ref={progress} 192 | > 193 | {/* eslint-enable */} 194 |
    198 |
    199 |

    200 | {show.displayNumber}: {show.title} 201 |

    202 | ` 203 |
    210 | {tooltipTime} 211 |
    212 |
    213 | 214 |
    215 | 224 | 225 |
    226 |

    VOLUME

    227 |
    228 | 236 | 241 | 249 | 254 | 262 | 267 | 275 | 280 | 288 | 293 | 301 | 306 | 314 | 319 | 327 | 332 | 341 | 346 | 354 | 359 |
    360 |
    361 |
    362 | {/* eslint-disable */} 363 |
    378 | ) 379 | } 380 | 381 | function formatTime(timeInSeconds: number) { 382 | const hours = Math.floor(timeInSeconds / (60 * 60)) 383 | timeInSeconds -= hours * 60 * 60 384 | const minutes = Math.floor(timeInSeconds / 60) 385 | timeInSeconds -= minutes * 60 386 | 387 | // left pad number with 0 388 | const leftPad = (num: number) => `${num}`.padStart(2, '0') 389 | const str = 390 | (hours ? `${leftPad(hours)}:` : '') + 391 | // (minutes ? `${leftPad(minutes)}:` : '00') + 392 | `${leftPad(minutes)}:` + 393 | leftPad(Math.round(timeInSeconds)) 394 | return str 395 | } 396 | -------------------------------------------------------------------------------- /src/components/ShowList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FMType } from '../types' 3 | import { Link } from '@reach/router' 4 | import styled from 'styled-components' 5 | import { Location } from '@reach/router' 6 | 7 | type A = { isActive: boolean } 8 | const LI = styled('div')` 9 | border-right: 1px solid #e4e4e4; 10 | border-bottom: 1px solid #e4e4e4; 11 | border-left: 10px solid #e4e4e4; 12 | background: ${({ isActive }: A) => (isActive ? '#fff' : '#f9f9f9')}; 13 | ${({ isActive }: A) => 14 | isActive && 15 | ` 16 | border-right-color: #fff; 17 | border-left: 0; 18 | padding-left: 1rem; 19 | :before { 20 | display: block; 21 | background: linear-gradient(30deg, #d2ff52 0%, #03fff3 100%); 22 | width: 10px; 23 | height: 100%; 24 | content: ''; 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | } 29 | 30 | `}; 31 | position: relative; 32 | display: flex; 33 | a { 34 | flex: 1 1 auto; 35 | padding: 10px; 36 | } 37 | .playbutton { 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | width: 5rem; 42 | flex-shrink: 0; 43 | padding: 1rem; 44 | button { 45 | background: none; 46 | border: 0; 47 | outline-color: #f1c15d; 48 | } 49 | } 50 | strong { 51 | color: #1d1d1d; 52 | font-size: 1.25rem; 53 | margin: 0; 54 | } 55 | p { 56 | text-transform: uppercase; 57 | margin: 0; 58 | color: #666; 59 | } 60 | ` 61 | type Props = { 62 | frontmatter: FMType 63 | isActive: boolean 64 | } 65 | function ListItem({ frontmatter, isActive }: Props) { 66 | return ( 67 |
  • 68 | 69 |

    Episode {frontmatter.episode}

    70 | {frontmatter.title} 71 | 72 |
    73 | 85 |
    86 |
  • 87 | ) 88 | } 89 | 90 | const UL = styled('div')` 91 | width: 38%; 92 | display: flex; 93 | flex-direction: column; 94 | padding: 0; 95 | @media (max-width: 650px) { 96 | height: 300px; 97 | width: 100%; 98 | overflow-x: auto; 99 | overflow-y: scroll; 100 | } 101 | ` 102 | 103 | type MyProps = { 104 | frontmatters: FMType[] 105 | setSelected?: Function 106 | } 107 | 108 | export default function ShowList({ frontmatters }: MyProps) { 109 | return ( 110 | 111 | {props => { 112 | let activeEpisodeSlug = frontmatters[0].slug 113 | if (props.location.pathname !== '/') { 114 | activeEpisodeSlug = props.location.pathname 115 | .split('/episode/') 116 | .slice(-1)[0] // just grab the slug at the end. pretty brittle but ok 117 | } 118 | // console.log('propslocation', props.location.pathname) 119 | return ( 120 |
      121 | {frontmatters.map(fm => ( 122 | 127 | ))} 128 |
    129 | ) 130 | }} 131 |
    132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /src/components/ShowNotes/DownloadBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Episode } from '../../types' 4 | import { withSiteData } from 'react-static' 5 | 6 | const StyledDiv = styled('div')` 7 | display: flex; 8 | font-size: 0.8rem; 9 | justify-content: space-between; 10 | @media (max-width: 650px) { 11 | flex-direction: column-reverse; 12 | } 13 | .button { 14 | border: 0; 15 | background: #f9f9f9; 16 | color: #1d1d1d; 17 | line-height: 1; 18 | padding: 1rem; 19 | display: inline-block; 20 | transition: all 0.2s; 21 | } 22 | .icon { 23 | border-right: 1px solid #e4e4e4; 24 | padding-right: 0.5rem; 25 | margin-right: 0.5rem; 26 | } 27 | #date { 28 | margin-top: 0; 29 | text-align: right; 30 | color: #666; 31 | font-size: 1.2rem; 32 | } 33 | ` 34 | export type DownloadBarProps = { curEp: Episode; ghURL: string } 35 | export const DownloadBar: React.FC = ({ curEp, ghURL }) => { 36 | // const [Bool, setBool] = React.useState(true) 37 | // React.useEffect(() => {}, []) 38 | return ( 39 | 40 | {/* */} 44 | 45 | 👇 Download Show 46 | 47 | 53 | ✏️ Edit Show Notes 54 | 55 |

    {new Date(curEp.frontmatter.date).toLocaleDateString()}

    56 |
    57 | ) 58 | } 59 | 60 | export default withSiteData(DownloadBar) 61 | -------------------------------------------------------------------------------- /src/components/ShowNotes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Episode } from '../../types' 3 | import { withRouteData } from 'react-static' 4 | import styled from 'styled-components' 5 | import DownloadBar from './DownloadBar' 6 | const SNDiv = styled('div')` 7 | width: 62%; 8 | font-size: 1.25rem; 9 | padding: 2rem; 10 | h2 { 11 | border-bottom: 1px solid #e4e4e4; 12 | padding-bottom: 1rem; 13 | margin-bottom: 0; 14 | } 15 | @media (max-width: 650px) { 16 | width: 100%; 17 | } 18 | ` 19 | type Props = { content?: Episode; mostRecentEpisode?: Episode } 20 | export default withRouteData(({ content, mostRecentEpisode }: Props) => { 21 | const curEp = content || mostRecentEpisode 22 | if (!curEp) return 'no content' 23 | const titleHead = curEp.frontmatter.episode 24 | ? `Ep ${curEp.frontmatter.episode}: ${curEp.frontmatter.title}` 25 | : curEp.frontmatter.title 26 | return ( 27 | 28 |

    {titleHead}

    29 | 30 |
    31 | 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/player.css: -------------------------------------------------------------------------------- 1 | .sr-only { 2 | border: 0 !important; 3 | clip: rect(1px, 1px, 1px, 1px) !important; 4 | clip-path: inset(50%) !important; 5 | height: 1px !important; 6 | overflow: hidden !important; 7 | padding: 0 !important; 8 | position: absolute !important; 9 | width: 1px !important; 10 | white-space: nowrap !important; 11 | } 12 | .player { 13 | bottom: 0; 14 | width: 100%; 15 | background: #000; 16 | border-top: 1px solid #ff0; 17 | color: #fff; 18 | display: flex; 19 | flex-wrap: wrap; 20 | position: relative; 21 | position: sticky; 22 | position: -webkit-sticky; 23 | top: -1px; 24 | z-index: 2; 25 | } 26 | .player__section { 27 | order: 2; 28 | } 29 | .player__section--left { 30 | width: 100px; 31 | min-width: 80px; 32 | } 33 | @media (max-width: 650px) { 34 | .player__section--left { 35 | flex: 1; 36 | } 37 | } 38 | .player__section--left > * { 39 | width: 100%; 40 | } 41 | .player__section--middle { 42 | position: relative; 43 | flex: 1 1 auto; 44 | border-right: 1px solid rgba(0,0,0,0.6); 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | @media (max-width: 650px) { 49 | .player__section--middle { 50 | order: 1; 51 | width: 100%; 52 | } 53 | } 54 | .player__section--right { 55 | display: flex; 56 | } 57 | @media (max-width: 650px) { 58 | .player__section--right { 59 | flex: 2; 60 | } 61 | } 62 | .player__section--right > * { 63 | width: 100%; 64 | } 65 | .player__icon { 66 | font-size: 1.25rem; 67 | line-height: 0.5; 68 | } 69 | .player__title { 70 | font-size: 1.5rem; 71 | font-weight: inherit; 72 | margin: 0; 73 | flex: 1 0 auto; 74 | display: flex; 75 | align-items: center; 76 | padding-left: 2rem; 77 | max-width: 650px; 78 | } 79 | @media (max-width: 650px) { 80 | .player__title { 81 | padding: 1rem; 82 | } 83 | } 84 | .player__tooltip { 85 | position: absolute; 86 | top: 22px; 87 | transform: translate(-50%); 88 | opacity: 0; 89 | } 90 | .player__tooltip:after { 91 | content: " "; 92 | position: absolute; 93 | bottom: 94%; 94 | left: 50%; 95 | margin-left: -2px; 96 | border-width: 2px; 97 | border-style: solid; 98 | border-color: transparent transparent #fff transparent; 99 | } 100 | .player button { 101 | background: #000; 102 | border: 0; 103 | color: #fff; 104 | padding: 1rem; 105 | border-right: 1px solid rgba(0,0,0,0.6); 106 | outline-color: #ff0; 107 | } 108 | .player__speeddisplay { 109 | height: 2.5rem; 110 | display: flex; 111 | justify-content: center; 112 | align-items: center; 113 | } 114 | .player__speed { 115 | flex: 0 1 auto; 116 | padding: 1rem; 117 | display: flex; 118 | flex-wrap: wrap; 119 | justify-content: space-around; 120 | flex-direction: column; 121 | align-items: center; 122 | } 123 | .player__speed > * { 124 | width: 100%; 125 | margin: 0; 126 | } 127 | .player__speed__display { 128 | height: 2.5rem; 129 | } 130 | .player__inputs { 131 | font-size: 0; 132 | } 133 | .player__volume { 134 | width: 120px; 135 | text-align: center; 136 | display: flex; 137 | flex-direction: column; 138 | justify-content: space-around; 139 | align-items: center; 140 | padding: 1rem; 141 | flex-wrap: wrap; 142 | flex: 1 0 auto; 143 | } 144 | .player__volume:focus-within { 145 | outline: #ff0 auto 5px; 146 | } 147 | .player__volume:hover label { 148 | border-top: 1px solid #ff0; 149 | } 150 | .player__volume label { 151 | border-top: 1px solid #008000; 152 | } 153 | .player__volume label:hover ~ label { 154 | border-top: 1px solid #000; 155 | } 156 | .player__volume p { 157 | width: 100%; 158 | margin: 0; 159 | } 160 | .player__volume input ~ label { 161 | background: #008000; 162 | border-right: 2px solid #000; 163 | display: inline-block; 164 | width: 8px; 165 | height: 2.5rem; 166 | } 167 | .player__volume input:checked ~ label { 168 | background: #808080; 169 | } 170 | .player__volume input:checked + label { 171 | background: #008000; 172 | } 173 | .progress { 174 | background: #0d0d0d; 175 | height: 2rem; 176 | cursor: crosshair; 177 | overflow: hidden; 178 | } 179 | .progress__time { 180 | background: #008000; 181 | border-right: 1px solid rgba(0,0,0,0.1); 182 | min-width: 20px; 183 | height: 100%; 184 | transition: width 0.1s; 185 | background: linear-gradient(30deg, #d2ff52 0%, #03fff3 100%); 186 | } 187 | -------------------------------------------------------------------------------- /src/components/player.styl: -------------------------------------------------------------------------------- 1 | // accessible way of hiding inputs and labels 2 | .sr-only 3 | border 0 !important 4 | clip rect(1px, 1px, 1px, 1px) !important 5 | clip-path inset(50%) !important 6 | height 1px !important 7 | overflow hidden !important 8 | padding 0 !important 9 | position absolute !important 10 | width 1px !important 11 | white-space nowrap !important 12 | 13 | .player 14 | bottom 0 15 | width 100% 16 | background black 17 | border-top 1px solid yellow 18 | color white 19 | display flex 20 | flex-wrap wrap 21 | position relative 22 | position sticky 23 | position -webkit-sticky 24 | top: -1px 25 | z-index 2 26 | // flex-wrap wrap 27 | &__section 28 | order 2 29 | &--left 30 | width 100px 31 | min-width 80px 32 | @media (max-width: 650px) 33 | flex 1 34 | & > * 35 | width 100% 36 | &--middle 37 | position relative 38 | flex 1 1 auto 39 | border-right 1px solid rgba(0,0,0,0.6) 40 | display flex 41 | flex-direction column 42 | @media (max-width: 650px) 43 | order 1 44 | width 100% 45 | &--right 46 | display flex 47 | @media (max-width: 650px) 48 | flex 2 49 | & > * 50 | width 100% 51 | &__icon 52 | font-size 1.25rem 53 | line-height 0.5 54 | &__title 55 | font-size 1.5rem 56 | font-weight inherit 57 | margin 0 58 | flex 1 0 auto 59 | display flex 60 | align-items center 61 | padding-left 2rem 62 | max-width 650px 63 | @media (max-width: 650px) 64 | padding 1rem 65 | &__tooltip 66 | position absolute 67 | top 22px 68 | transform translate(-50%) 69 | opacity 0 70 | &:after 71 | content " " 72 | position absolute 73 | bottom 94% 74 | left 50% 75 | margin-left -2px 76 | border-width 2px 77 | border-style solid 78 | border-color transparent transparent white transparent 79 | button 80 | background black 81 | border 0 82 | color white 83 | padding 1rem 84 | border-right 1px solid rgba(0,0,0,0.6) 85 | outline-color yellow 86 | &__speeddisplay 87 | &__speeddisplay 88 | height 2.5rem 89 | display flex 90 | justify-content center 91 | align-items center 92 | &__speed 93 | flex 0 1 auto 94 | padding 1rem 95 | display flex 96 | flex-wrap wrap 97 | justify-content space-around 98 | flex-direction column 99 | align-items center 100 | & > * 101 | width 100% 102 | margin 0 103 | &__display 104 | height 2.5rem 105 | &__inputs 106 | font-size 0 107 | &__volume 108 | width 120px 109 | text-align center 110 | display flex 111 | flex-direction column 112 | justify-content space-around 113 | align-items center 114 | padding 1rem 115 | flex-wrap wrap 116 | flex 1 0 auto 117 | &:focus-within 118 | outline: yellow auto 5px; 119 | &:hover 120 | label 121 | border-top 1px solid yellow 122 | label 123 | border-top 1px solid green 124 | &:hover 125 | & ~ label 126 | border-top 1px solid black 127 | p 128 | width 100% 129 | margin 0 130 | input ~ label 131 | background green 132 | border-right 2px solid black 133 | display inline-block 134 | width 8px 135 | height 2.5rem 136 | input:checked ~ label 137 | background grey 138 | input:checked + label 139 | background green 140 | 141 | .progress 142 | background lighten(#000,5%) 143 | height 2rem 144 | cursor crosshair 145 | overflow hidden 146 | &__time 147 | background green 148 | border-right 1px solid rgba(0,0,0,0.1) 149 | min-width 20px 150 | height 100% 151 | transition width 0.1s 152 | background linear-gradient(30deg, #d2ff52 0%, #03fff3 100%) 153 | 154 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | // Your top level component 5 | import App from './App' 6 | 7 | // Export your top level component as JSX (for static rendering) 8 | export default App 9 | 10 | // Render your app 11 | if (typeof document !== 'undefined') { 12 | const renderMethod = module.hot 13 | ? ReactDOM.render 14 | : ReactDOM.hydrate || ReactDOM.render 15 | 16 | const render = (Comp: Function) => { 17 | renderMethod(, document.getElementById('root')) 18 | } 19 | 20 | // Render! 21 | render(App) 22 | 23 | // Hot Module Replacement 24 | if (module.hot) { 25 | module.hot.accept('./App', () => render(require('./App').default)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/normalize.css: -------------------------------------------------------------------------------- 1 | article, 2 | aside, 3 | details, 4 | figcaption, 5 | figure, 6 | footer, 7 | header, 8 | hgroup, 9 | nav, 10 | section, 11 | summary { 12 | display: block; 13 | } 14 | audio, 15 | canvas, 16 | video { 17 | display: inline-block; 18 | } 19 | audio:not([controls]) { 20 | display: none; 21 | height: 0; 22 | } 23 | [hidden] { 24 | display: none; 25 | } 26 | html { 27 | font-family: sans-serif; 28 | -webkit-text-size-adjust: 100%; 29 | -ms-text-size-adjust: 100%; 30 | } 31 | a:focus { 32 | outline: thin dotted; 33 | } 34 | a:active, 35 | a:hover { 36 | outline: 0; 37 | } 38 | h1 { 39 | font-size: 2em; 40 | } 41 | abbr[title] { 42 | border-bottom: 1px dotted; 43 | } 44 | b, 45 | strong { 46 | font-weight: 700; 47 | } 48 | dfn { 49 | font-style: italic; 50 | } 51 | mark { 52 | background: #ff0; 53 | color: #000; 54 | } 55 | code, 56 | kbd, 57 | pre, 58 | samp { 59 | font-family: monospace, serif; 60 | font-size: 1em; 61 | } 62 | pre { 63 | white-space: pre-wrap; 64 | word-wrap: break-word; 65 | } 66 | q { 67 | quotes: \201c \201d \2018 \2019; 68 | } 69 | small { 70 | font-size: 80%; 71 | } 72 | sub, 73 | sup { 74 | font-size: 75%; 75 | line-height: 0; 76 | position: relative; 77 | vertical-align: baseline; 78 | } 79 | sup { 80 | top: -0.5em; 81 | } 82 | sub { 83 | bottom: -0.25em; 84 | } 85 | img { 86 | border: 0; 87 | } 88 | svg:not(:root) { 89 | overflow: hidden; 90 | } 91 | fieldset { 92 | border: 1px solid silver; 93 | margin: 0 2px; 94 | padding: 0.35em 0.625em 0.75em; 95 | } 96 | button, 97 | input, 98 | select, 99 | textarea { 100 | font-family: inherit; 101 | font-size: 100%; 102 | margin: 0; 103 | } 104 | button, 105 | input { 106 | line-height: normal; 107 | } 108 | button,html input[type=button],/* 1 */ 109 | input[type=reset],input[type=submit] { 110 | -webkit-appearance: button; 111 | cursor: pointer; 112 | } 113 | button[disabled], 114 | input[disabled] { 115 | cursor: default; 116 | } 117 | input[type='checkbox'], 118 | input[type='radio'] { 119 | box-sizing: border-box; 120 | padding: 0; 121 | } 122 | input[type='search'] { 123 | -webkit-appearance: textfield; 124 | -moz-box-sizing: content-box; 125 | -webkit-box-sizing: content-box; 126 | box-sizing: content-box; 127 | } 128 | input[type='search']::-webkit-search-cancel-button, 129 | input[type='search']::-webkit-search-decoration { 130 | -webkit-appearance: none; 131 | } 132 | textarea { 133 | overflow: auto; 134 | vertical-align: top; 135 | } 136 | table { 137 | border-collapse: collapse; 138 | border-spacing: 0; 139 | } 140 | body, 141 | figure { 142 | margin: 0; 143 | } 144 | legend, 145 | button::-moz-focus-inner, 146 | input::-moz-focus-inner { 147 | border: 0; 148 | padding: 0; 149 | } 150 | 151 | .clearfix:after { 152 | visibility: hidden; 153 | display: block; 154 | font-size: 0; 155 | content: ' '; 156 | clear: both; 157 | height: 0; 158 | } 159 | 160 | * { 161 | -moz-box-sizing: border-box; 162 | -webkit-box-sizing: border-box; 163 | box-sizing: border-box; 164 | } 165 | 166 | img { 167 | max-width: 100%; 168 | } 169 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => ( 4 |
    5 |

    404 - Oh no's! We couldn't find that page :(

    6 |
    7 | ) 8 | -------------------------------------------------------------------------------- /src/pages/episode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { withSiteData } from 'react-static' 4 | import { Episode, FMType } from '../types' 5 | import Header from '@src/components/Header' 6 | import Player from '@src/components/Player' 7 | import Footer from '@src/components/Footer' 8 | import ShowList from '@src/components/ShowList' 9 | import ShowNotes from '@src/components/ShowNotes' 10 | import styled from 'styled-components' 11 | 12 | const Main = styled('main')` 13 | background: #fff; 14 | display: flex; 15 | flex-wrap: wrap; 16 | ` 17 | 18 | type Props = { 19 | frontmatters: FMType[] 20 | mostRecentEpisode: Episode 21 | title: string 22 | description: string 23 | myURL: string 24 | image: string 25 | } 26 | export default withSiteData( 27 | ({ 28 | frontmatters, 29 | mostRecentEpisode, 30 | title, 31 | description, 32 | myURL, 33 | image, 34 | }: Props) => { 35 | return ( 36 | <> 37 |
    46 |
    47 | 48 | 49 | 50 |
    51 |