├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
52 | >
53 | )
54 | },
55 | )
56 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withSiteData } from 'react-static'
3 | // import { Link } from '@reach/router'
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 |
52 | >
53 | )
54 | },
55 | )
56 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyxio/react-static-podcast-hosting/fb0c225d0cb4b1cf77d3f0e816858f7f984af70c/src/types.js
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | // export interface Post {
2 | // body: string
3 | // id: number
4 | // title: string
5 | // }
6 | export interface Episode {
7 | frontmatter: FMType
8 | body: any
9 | }
10 | export type FMType = {
11 | title: string
12 | mp3URL: string
13 | date: string
14 | description: string
15 | episodeType?: 'full' | 'trailer' | 'bonus'
16 | episode?: number
17 | season?: number
18 | slug?: string
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/formatTime.js:
--------------------------------------------------------------------------------
1 | export function formatTime(timeInSeconds) {
2 | const hours = Math.floor(timeInSeconds / (60 * 60));
3 | timeInSeconds -= hours * 60 * 60;
4 | const minutes = Math.floor(timeInSeconds / 60);
5 | timeInSeconds -= minutes * 60;
6 | // left pad number with 0
7 | const leftPad = (num) => `${num}`.padStart(2, '0');
8 | const str = (hours ? `${leftPad(hours)}:` : '') +
9 | // (minutes ? `${leftPad(minutes)}:` : '00') +
10 | `${leftPad(minutes)}:` +
11 | leftPad(Math.round(timeInSeconds));
12 | return str;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/formatTime.ts:
--------------------------------------------------------------------------------
1 | export function formatTime(timeInSeconds: number) {
2 | const hours = Math.floor(timeInSeconds / (60 * 60))
3 | timeInSeconds -= hours * 60 * 60
4 | const minutes = Math.floor(timeInSeconds / 60)
5 | timeInSeconds -= minutes * 60
6 |
7 | // left pad number with 0
8 | const leftPad = (num: number) => `${num}`.padStart(2, '0')
9 | const str =
10 | (hours ? `${leftPad(hours)}:` : '') +
11 | // (minutes ? `${leftPad(minutes)}:` : '00') +
12 | `${leftPad(minutes)}:` +
13 | leftPad(Math.round(timeInSeconds))
14 | return str
15 | }
16 |
--------------------------------------------------------------------------------
/static.config.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import path from 'path'
3 | import { mkDir, mkFile } from './fs'
4 | const fs = require('fs')
5 | import { buildFeed, grabContents } from 'podcats'
6 |
7 | /// config
8 | const myURL = 'https://reactstaticpodcast.netlify.com'
9 |
10 | const description =
11 | 'a podcast feed and blog generator in React and hosted on Netlify'
12 | const image = 'https://placekitten.com/g/1400/1400' // TODO: itunes cover and opengraph image. you should customise this!
13 | const ghURL = 'https://github.com/sw-yx/react-static-podcast-hosting'
14 | const rss = myURL + '/rss/index.xml'
15 | const itURL =
16 | 'https://itunes.apple.com/us/podcast/this-week-in-r-reactjs/id1448641675?mt=2&uo=4'
17 | const netlifyURL = 'https://app.netlify.com/sites/reactstaticpodcast'
18 | const spotifyURL = 'https://open.spotify.com/show/3zkaO56g7xAQAemOCFWHc0'
19 | const googlepodURL =
20 | 'https://www.google.com/podcasts?feed=aHR0cHM6Ly9hbmNob3IuZm0vcy84NTMwNWNjL3BvZGNhc3QvcnNz'
21 | const overcastURL =
22 | 'https://overcast.fm/itunes1448641675/this-week-in-r-reactjs'
23 | const redditURL = 'https://www.reddit.com/r/reactjs/'
24 | const contentFolder = 'content'
25 | const author = {
26 | name: 'This Week in r/Reactjs',
27 | email: 'REACTSTATICPODCAST_AUTHOR_EMAIL@foo.com',
28 | link: 'https://REACTSTATICPODCAST_AUTHOR_LINK.com',
29 | }
30 | const feedOptions = {
31 | // blog feed options
32 | title: 'This Week in r/Reactjs',
33 | description,
34 | link: myURL,
35 | id: myURL,
36 | copyright: 'copyright REACTSTATICPODCAST_YOURNAMEHERE',
37 | feedLinks: {
38 | atom: safeJoin(myURL, 'atom.xml'),
39 | json: safeJoin(myURL, 'feed.json'),
40 | rss: safeJoin(myURL, 'rss'),
41 | },
42 | author,
43 | }
44 | const iTunesChannelFields = {
45 | // itunes options
46 | summary: 'This Week in r/Reactjs',
47 | author: author.name,
48 | keywords: ['Technology'],
49 | categories: [
50 | { cat: 'Technology' },
51 | { cat: 'Technology', child: 'Tech News' },
52 | ],
53 | image,
54 | explicit: false,
55 | owner: author,
56 | type: 'episodic',
57 | }
58 |
59 | // preprocessing'
60 | const filenames = fs.readdirSync(contentFolder).reverse() // reverse chron
61 | const filepaths = filenames.map(file =>
62 | path.join(process.cwd(), contentFolder, file),
63 | )
64 | const contents = grabContents(filepaths, myURL)
65 | const frontmatters = contents.map(c => c.frontmatter)
66 | mkDir('/public/rss/')
67 |
68 | // generate HTML
69 | export default {
70 | plugins: [
71 | 'react-static-plugin-styled-components',
72 | 'react-static-plugin-typescript',
73 | ],
74 | entry: path.join(__dirname, 'src', 'index.tsx'),
75 | siteRoot: myURL,
76 | getSiteData: async () => {
77 | // generate RSS
78 | let feed = await buildFeed(
79 | contents,
80 | myURL,
81 | author,
82 | feedOptions,
83 | iTunesChannelFields,
84 | )
85 | mkFile('/public/rss/index.xml', feed.rss2())
86 | return {
87 | title: 'This Week in r/Reactjs',
88 | description,
89 | rss,
90 | frontmatters,
91 | ghURL,
92 | myURL,
93 | image,
94 | mostRecentEpisode: contents[0], // necessary evil to show on '/'
95 | subscribeLinks: [
96 | { type: 'iTunes', url: itURL },
97 | { type: 'RSS', url: rss },
98 | { type: 'GitHub', url: ghURL },
99 | { type: 'Netlify', url: netlifyURL },
100 | { type: 'Spotify', url: spotifyURL },
101 | { type: 'GooglePlay', url: googlepodURL },
102 | { type: 'Overcast', url: overcastURL },
103 | { type: 'Reddit', url: redditURL },
104 | ],
105 | }
106 | },
107 | getRoutes: async () => {
108 | return [
109 | {
110 | path: 'episode',
111 | getData: () => ({
112 | contents,
113 | }),
114 | children: contents.map(content => ({
115 | path: `/${content.frontmatter.slug}`,
116 | component: 'src/pages/episode',
117 | getData: () => ({
118 | content,
119 | myURL,
120 | }),
121 | })),
122 | },
123 | ]
124 | },
125 | }
126 |
127 | function safeJoin(a, b) {
128 | /** strip starting/leading slashes and only use our own */
129 | let a1 = a.slice(-1) === '/' ? a.slice(0, a.length - 1) : a
130 | let b1 = b.slice(0) === '/' ? b.slice(1) : b
131 | return `${a1}/${b1}`
132 | }
133 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "module": "esnext",
6 | "jsx": "preserve",
7 | "allowJs": true,
8 | "moduleResolution": "node",
9 | "allowSyntheticDefaultImports": true,
10 | "noImplicitAny": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "removeComments": false,
14 | "preserveConstEnums": true,
15 | "sourceMap": true,
16 | "skipLibCheck": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@src/*": ["src/*"]
20 | },
21 | "typeRoots": ["./node_modules/@types"],
22 | "lib": ["dom", "es2015", "es2016", "es2017"]
23 | },
24 | "exclude": ["dist/**/*", "public/**/*", "node_modules"]
25 | }
26 |
--------------------------------------------------------------------------------