diff --git a/.babelrc b/.babelrc index f5cb571..4b1ab1f 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,7 @@ { - "presets": ["node6", "react", "stage-2"], + "presets": ["stage-2", "node6", "react"], "plugins": [ + "transform-es2015-destructuring", "react-require", [ "module-alias", diff --git a/.eslintrc b/.eslintrc index 55a78b4..b1095e8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,12 +1,6 @@ { - "extends": "kentcdodds/react", + "extends": ["kentcdodds", "kentcdodds/react", "kentcdodds/babel-module-alias", "kentcdodds/babel-react-require"], "rules": { "comma-dangle": [1, "always-multiline"], // I got tired of this breaking the build ¯\_(ツ)_/¯ - "react/react-in-jsx-scope": 0, - }, - "settings": { - "import/resolver": { - "babel-module-alias": {}, - }, }, } diff --git a/data/contributors/index.js b/data/contributors/index.js index 0ba7b74..3c068d3 100644 --- a/data/contributors/index.js +++ b/data/contributors/index.js @@ -84,12 +84,10 @@ const contributors = [ twitter: 'kurtiskemple', contributions: 'Site PRs', }, -].map(c => { - return { - imgSrc: `/data/contributors/${c.twitter}.png`, - link: `https://twitter.com/${c.twitter}`, - ...c, - } -}) +].map(c => ({ + imgSrc: `/data/contributors/${c.twitter}.png`, + link: `https://twitter.com/${c.twitter}`, + ...c, +})) export default contributors diff --git a/data/deals/index.js b/data/deals/index.js index dde5908..b4c833e 100644 --- a/data/deals/index.js +++ b/data/deals/index.js @@ -107,6 +107,6 @@ const deals = [ ] const sortedDeals = sortBy(deals, d => d.organization.toLowerCase()) -const filteredDeals = sortedDeals.filter(d => d.expires ? isFuture(d.expires) : true) +const filteredDeals = sortedDeals.filter(d => (d.expires ? isFuture(d.expires) : true)) export default filteredDeals diff --git a/episodes/.eslintrc b/episodes/.eslintrc index 54a19fe..d3eb487 100644 --- a/episodes/.eslintrc +++ b/episodes/.eslintrc @@ -2,5 +2,6 @@ "rules": { "max-len": 0, "no-trailing-spaces": 0, + "max-lines": 0, } } diff --git a/episodes/2016-01-20/index.js b/episodes/2016-01-20/index.js index b656b92..1cbd288 100644 --- a/episodes/2016-01-20/index.js +++ b/episodes/2016-01-20/index.js @@ -1,8 +1,8 @@ /* eslint max-len:0 no-trailing-spaces:0 */ -import {panelists} from '/panelists' +import panelists from '/panelists' import {cloneDeep} from 'lodash' -const guests = cloneDeep(panelists) +const guests = cloneDeep(panelists) const guestLinksTipsAndPicks = [ { diff --git a/episodes/2016-06-01/index.js b/episodes/2016-06-01/index.js index d72cf40..7de6ce4 100644 --- a/episodes/2016-06-01/index.js +++ b/episodes/2016-06-01/index.js @@ -1,4 +1,5 @@ import rerun from '../2015-12-30' + export default { ...rerun, title: `(Rerun) ${rerun.title}`, diff --git a/episodes/get-episode-directories.js b/episodes/get-episode-directories.js index 89e2048..69fac4e 100644 --- a/episodes/get-episode-directories.js +++ b/episodes/get-episode-directories.js @@ -13,5 +13,6 @@ function getEpisodeDirectories() { function trimIndexJs(string) { const indexJs = '/index.js' - return string.slice(0, -indexJs.length) + const first = 0 + return string.slice(first, -indexJs.length) } diff --git a/episodes/index.js b/episodes/index.js index c42060e..883e942 100644 --- a/episodes/index.js +++ b/episodes/index.js @@ -6,9 +6,9 @@ import getEpisodeDirectories from './get-episode-directories' const episodeDirectories = getEpisodeDirectories() const episodes = episodeDirectories.map(getEpisodeData) -const episodeGroups = groupBy(episodes, e => e.past ? 'past' : 'future') +const episodeGroups = groupBy(episodes, e => (e.past ? 'past' : 'future')) const {future = [], past} = episodeGroups -const nextEpisode = future[0] +const [nextEpisode] = future export default episodes export {future, past, episodes, nextEpisode} diff --git a/generate/contributors.js b/generate/contributors.js index 12c84d9..09868bf 100644 --- a/generate/contributors.js +++ b/generate/contributors.js @@ -4,28 +4,23 @@ import renderComponentToFile from './renderComponentToFile' import host from '/host' import sponsors from '../sponsors' import contributors from '../data/contributors' -import {panelists} from '/panelists' +import panelists from '/panelists' import {nextEpisode} from '../episodes' import Contributors from '../src/pages/contributors' import {sortPeople} from '/utils' - -const panelistContributors = panelists.map(p => { - return { - contributions: 'Panelist', - ...p, - } -}) +const panelistContributors = panelists.map(p => ({ + contributions: 'Panelist', + ...p, +})) const sponsorContributors = Object.keys(sponsors).reduce((array, sponsorGroupKey) => { const groupName = sponsorGroupKey.replace('Sponsors', '').replace(/^./, m => m.toUpperCase()) - const groupArray = sponsors[sponsorGroupKey].map(sponsor => { - return { - contributions: `${groupName} Sponsor`, - squareImage: true, - ...sponsor, - } - }) + const groupArray = sponsors[sponsorGroupKey].map(sponsor => ({ + contributions: `${groupName} Sponsor`, + squareImage: true, + ...sponsor, + })) return [...array, ...groupArray] }, []) diff --git a/generate/deals.js b/generate/deals.js index fc7ea98..83bcb1a 100644 --- a/generate/deals.js +++ b/generate/deals.js @@ -5,14 +5,12 @@ import deals from '../data/deals' import DealsPage from '../src/pages/deals' import {markdownToHTML} from '/utils' -const theDeals = deals.map(d => { - return { - dealHTML: markdownToHTML(d.deal), - descriptionHTML: markdownToHTML(d.description), - // imgSrc: 'some-default', // TODO add a default image - ...d, - } -}) +const theDeals = deals.map(d => ({ + dealHTML: markdownToHTML(d.deal), + descriptionHTML: markdownToHTML(d.description), + // imgSrc: 'some-default', // TODO add a default image + ...d, +})) renderComponentToFile( , diff --git a/generate/episode-email.js b/generate/episode-email.js index ca64340..618a6cd 100644 --- a/generate/episode-email.js +++ b/generate/episode-email.js @@ -1,9 +1,9 @@ import {resolve} from 'path' -import renderComponentToFile from './renderComponentToFile' -import {nextEpisode} from '../episodes' import getEpisodeData from '/get-episode-data' +import {nextEpisode} from '../episodes' import EpisodeEmail from '../src/pages/episode-email' import {getSponsorsForDate} from '../sponsors' +import renderComponentToFile from './renderComponentToFile' export default generateEpisodeEmailPage diff --git a/generate/episode.js b/generate/episode.js index 5246da4..ef654d0 100644 --- a/generate/episode.js +++ b/generate/episode.js @@ -1,10 +1,9 @@ import {resolve} from 'path' -import renderComponentToFile from './renderComponentToFile' - -import {nextEpisode} from '../episodes' import getEpisodeData from '/get-episode-data' -import {getSponsorsForDate} from '../sponsors' +import {nextEpisode} from '../episodes' import EpisodePage from '../src/pages/episode' +import {getSponsorsForDate} from '../sponsors' +import renderComponentToFile from './renderComponentToFile' export default generateEpisode diff --git a/generate/home.js b/generate/home.js index 6597d8a..c42bdcb 100644 --- a/generate/home.js +++ b/generate/home.js @@ -1,12 +1,9 @@ import {resolve} from 'path' -import renderComponentToFile from './renderComponentToFile' - +import panelists from '/panelists' +import Home from '../src/pages/home' import {future as futureEpisodes, past as pastEpisodes} from '../episodes' import {currentSponsors} from '../sponsors' -import {panelists} from '/panelists' - -import Home from '../src/pages/home' - +import renderComponentToFile from './renderComponentToFile' renderComponentToFile( /host' +import panelists from '/panelists' import {episodes} from '../episodes' -import {panelists} from '/panelists' import sponsors from '../sponsors' -import * as config from './json-files-configs' +import {writeJsonArray, writeJsonObject} from './write-to-json-file' + +const config = getConfig() // all episodes data in a same data.json file (filtered properties only) writeJsonArray(episodes, config.episodesToJson, true) @@ -27,3 +28,49 @@ writeJsonArray(sponsorsArray, config.sponsorsToJson, true) // all host data in a data.json file (no property filter) writeJsonObject(host, config.hostToJson, false) + + +function getConfig() { + return { + hostToJson: { + destDirectoryFromHere: '../resources/host/', + propertiesToFilter: [ + 'imgSrc', + 'name', + 'twitter', + 'link', + ], + }, + sponsorsToJson: { + destDirectoryFromHere: '../sponsors/', + propertiesToFilter: [ + 'premierSponsors', + 'goldSponsors', + 'silverSponsors', + 'appreciationSponsors,', + ], + }, + panelistsToJson: { + destDirectoryFromHere: '../resources/panelists/', + propertiesToFilter: [ + 'imgSrc', + 'name', + 'twitter', + 'link', + ], + }, + episodesToJson: { + destDirectoryFromHere: '../episodes/', + propertiesToFilter: [ + 'numberDisplay', + 'title', + 'date', + 'time', + 'description', + 'hangoutId', + 'youTubeId', + 'guests', + ], + }, + } +} diff --git a/generate/links-tips-picks.js b/generate/links-tips-picks.js index 2e3bb1e..3865225 100644 --- a/generate/links-tips-picks.js +++ b/generate/links-tips-picks.js @@ -1,7 +1,7 @@ import {resolve} from 'path' -import renderComponentToFile from './renderComponentToFile' -import {episodes, nextEpisode} from '../episodes' import LinksTipsPicks from '../src/pages/links-tips-picks' +import {episodes, nextEpisode} from '../episodes' +import renderComponentToFile from './renderComponentToFile' renderComponentToFile( { - return ReactDOMServer.renderToStaticMarkup(comp) - }) + const {html, css} = StyleSheetServer.renderStatic(() => ReactDOMServer.renderToStaticMarkup(comp)) const string = html.replace('/* aphrodite-content */', css.content) writeFile(destination, string, cb) } diff --git a/generate/screenshot/server.js b/generate/screenshot/server.js index d7c70d5..df5f2a4 100644 --- a/generate/screenshot/server.js +++ b/generate/screenshot/server.js @@ -19,8 +19,8 @@ function runServer(cb) { }) server.start(() => { address = `http://localhost:${port}` - console.log('server listening at: ' + address) - console.log('with a root at: ' + rootPath) + console.log(`server listening at: ${address}`) + console.log(`with a root at: ${rootPath}`) cb(address) }) } diff --git a/generate/write-to-json-file.js b/generate/write-to-json-file.js index 516dfa9..b24ef08 100644 --- a/generate/write-to-json-file.js +++ b/generate/write-to-json-file.js @@ -2,24 +2,23 @@ import path from 'path' import fs from 'fs' export function writeJsonArray(arrayNewItems, config, applyFilter = false) { - const DEST_JSON_FILE = path.join(__dirname, config.destDirectoryFromHere, `data.json`) + const destFile = path.join(__dirname, config.destDirectoryFromHere, `data.json`) const allFileContent = arrayNewItems.map(item => { return applyFilter ? filterProperties(config.propertiesToFilter, item) : item }) - writeToFile(DEST_JSON_FILE, JSON.stringify(allFileContent)) + writeToFile(destFile, JSON.stringify(allFileContent)) } export function writeJsonObject(newItem, config, applyFilter = false) { - const DEST_JSON_FILE = path.join(__dirname, config.destDirectoryFromHere, `data.json`) + const destFile = path.join(__dirname, config.destDirectoryFromHere, `data.json`) const itemToCopy = applyFilter ? filterProperties(config.propertiesToFilter, newItem) : newItem - writeToFile(DEST_JSON_FILE, JSON.stringify(itemToCopy)) + writeToFile(destFile, JSON.stringify(itemToCopy)) } function writeToFile(file, content) { fs.writeFile(file, content, err => { if (err) { - process.stdout.write(`😱ERROR: could not write json file: ${file}\n->Error details: ${err}\n`) - process.exit(1) + throw new Error(`😱ERROR: could not write json file: ${file}\n->Error details: ${err}\n`) } }) } diff --git a/other/components/episode-email/banner.js b/other/components/episode-email/banner.js new file mode 100644 index 0000000..d81ec55 --- /dev/null +++ b/other/components/episode-email/banner.js @@ -0,0 +1,70 @@ +import {PropTypes} from 'react' +import {Box, Item, Image, A} from 'react-html-email' + +export default Banner + +function Banner({page}) { + const link = `${page}#email` + const textColor = '#656565' + const styles = { + item: { + paddingBottom: 9, + msoLineHeightRule: 'exactly', + msTextSizeAdjust: '100%', + WebkitTextSizeAdjust: '100%', + wordBreak: 'break-word', + fontFamily: 'Helvetica', + textAlign: 'center', + }, + div: { + display: 'inline-block', + width: 300, + }, + p: { + color: textColor, + fontSize: '12px', + lineHeight: '150%', + display: 'inline', + }, + a: { + color: textColor, + }, + } + return ( + + + +
+

+ The live broadcast podcast all about JavaScript. +

+
+ +
+
+ +
+ ) +} +Banner.propTypes = { + page: PropTypes.string.isRequired, +} + +function BannerLogo() { + return ( + + JavaScript Air Logo + + ) +} diff --git a/other/components/episode-email.js b/other/components/episode-email/index.js similarity index 52% rename from other/components/episode-email.js rename to other/components/episode-email/index.js index 92516e5..2aed5d7 100644 --- a/other/components/episode-email.js +++ b/other/components/episode-email/index.js @@ -1,16 +1,29 @@ import {PropTypes} from 'react' import {Email, Box, Item, Image, A} from 'react-html-email' -import LinksPicksTips from './links-picks-tips' -import ShowDescription from './show-description' -import personPropType from './prop-types/person' -import Person from './person' - import * as utils from '/utils' +import hijackConsole from '../../utils/hijack-console' +import ShowDescription from '../show-description' +import Banner from './banner' +import WatchShowButton from './watch-show-button' +import SponsorsSection from './sponsors-section' +import ShowNotes from './show-notes' +import {Clear, Spacer, Line} from './util-components' const episodePropType = PropTypes.object const sponsorsPropType = PropTypes.object -hijackConsole() +const ignoreLogs = [ + 'in outlook:', + 'unsupported in: outlook.', + 'unsupported in: outlook-web.', + 'mso-line-height-rule', + 'border-radius` supplied to `Box` unsupported in', + 'border-radius` supplied to `Image` unsupported in', + ' cannot appear as a child of
', // can't think of how to get around this :-/ + 'Unknown prop `xmlns` on tag', // fb.me/react-unknown-prop + /Unknown props.*on tag\./, // fb.me/react-unknown-prop +] +hijackConsole(ignoreLogs) export default EpisodeEmail @@ -87,120 +100,6 @@ function getHeadCSS() { ` } -function Banner({page}) { - const link = `${page}#email` - const textColor = '#656565' - const styles = { - item: { - paddingBottom: 9, - msoLineHeightRule: 'exactly', - msTextSizeAdjust: '100%', - WebkitTextSizeAdjust: '100%', - wordBreak: 'break-word', - fontFamily: 'Helvetica', - textAlign: 'center', - }, - div: { - display: 'inline-block', - width: 300, - }, - p: { - color: textColor, - fontSize: '12px', - lineHeight: '150%', - display: 'inline', - }, - a: { - color: textColor, - }, - } - return ( - - - -
-

- The live broadcast podcast all about JavaScript. -

-
- -
-
- -
- ) -} -Banner.propTypes = { - page: PropTypes.string.isRequired, -} - -function BannerLogo() { - return ( - - - - ) -} - -function WatchShowButton({page}) { - const styles = { - box: { - borderCollapse: 'separate !important', - borderRadius: '3px', - backgroundColor: '#AAAAAA', - msoTableLspace: '0pt', - msoTableRspace: '0pt', - msTextSizeAdjust: '100%', - WebkitTextSizeAdjust: '100%', - }, - p: { - lineHeight: '100%', - fontSize: '16px', - padding: '15px', - margin: 0, - }, - a: { - fontWeight: 'bold', - letterSpacing: 'normal', - textAlign: 'center', - textDecoration: 'none', - color: '#FFFFFF', - msoLineHeightRule: 'exactly', - msTextSizeAdjust: '100%', - WebkitTextSizeAdjust: '100%', - display: 'block', - }, - } - return ( - - -

- - Watch the latest episode now - -

-
-
- ) -} -WatchShowButton.propTypes = { - page: PropTypes.string.isRequired, -} - function ShowImage({src}) { return ( @@ -308,101 +207,6 @@ ClickButtonWithAudioOption.propTypes = { number: PropTypes.number.isRequired, } -function SponsorsSection({sponsors}) { - const styles = { - box: { - minWidth: '100%', - border: '2px solid #222222', - borderCollapse: 'collapse', - msoTableLspace: '0pt', - msoTableRspace: '0pt', - msTextSizeAdjust: '100%', - WebkitTextSizeAdjust: '100%', - }, - item: { - fontFamily: 'Helvetica', - fontSize: '14px', - fontWeight: 'normal', - textAlign: 'left', - msoLineHeightRule: 'exactly', - msTextSizeAdjust: '100%', - WebkitTextSizeAdjust: '100%', - wordBreak: 'break-word', - color: '#202020', - lineHeight: '150%', - padding: 18, - }, - h2: { - marginTop: 0, - }, - } - return ( - - -

This episode made possible by

-
    - { - sponsors.map(({name, link, tagline}, i) => ( -
  • - {name} - {tagline} -
  • - )) - } -
-
-
- ) -} -SponsorsSection.propTypes = { - sponsors: PropTypes.array, -} - -function ShowNotes({people}) { - const styles = { - h2: { - marginTop: 0, - }, - td: { - verticalAlign: 'top', - }, - } - return ( - - -

Links, Picks, and Tips

- {people.map((person, i) => { - return ( - - {i !== 0 ? : null} - -
- - - - - - ) - })} - - - ) -} -ShowNotes.propTypes = { - people: PropTypes.arrayOf(personPropType), -} - function ShowDescriptionSection() { const styles = { box: { @@ -519,58 +323,3 @@ function LegalText() { ) } - -function Clear({children}) { - const styles = { - visibility: 'hidden', - display: 'block', - height: 0, - clear: 'both', - } - return ( -
- {children} -
-
- ) -} -Clear.propTypes = { - children: PropTypes.node.isRequired, -} - -function Spacer({space = 32}) { - return
-} -Spacer.propTypes = { - space: PropTypes.number, -} - -function Line() { - return
-} - -function hijackConsole() { - const ignoreLogs = [ - 'in outlook:', - 'unsupported in: outlook.', - 'unsupported in: outlook-web.', - 'mso-line-height-rule', - 'border-radius` supplied to `Box` unsupported in', - 'border-radius` supplied to `Image` unsupported in', - ' cannot appear as a child of
', // can't think of how to get around this :-/ - ] - hijack('warn') - hijack('error') - - function hijack(logger) { - const original = console[logger] - console[logger] = function hijackedConsole(...args) { - const line = args.join(' ') - const shouldIgnore = ignoreLogs.some(l => line.includes(l)) - if (!shouldIgnore) { - return original(...args) - } - return undefined - } - } -} diff --git a/other/components/episode-email/show-notes.js b/other/components/episode-email/show-notes.js new file mode 100644 index 0000000..2f432d6 --- /dev/null +++ b/other/components/episode-email/show-notes.js @@ -0,0 +1,55 @@ +import {PropTypes} from 'react' +import {Box, Item, Image} from 'react-html-email' +import LinksPicksTips from '../links-picks-tips' +import personPropType from '../prop-types/person' +import Person from '../person' +import {Clear, Spacer} from './util-components' + +export default ShowNotes + +function ShowNotes({people}) { + const styles = { + h2: { + marginTop: 0, + }, + td: { + verticalAlign: 'top', + }, + } + return ( + + +

Links, Picks, and Tips

+ {people.map((person, i) => { + const noNotes = !person.hasNotes + return ( + + {i === 0 ? null : } + +
+ + + + + + ) + })} + + + ) +} +ShowNotes.propTypes = { + people: PropTypes.arrayOf(personPropType), +} diff --git a/other/components/episode-email/sponsors-section.js b/other/components/episode-email/sponsors-section.js new file mode 100644 index 0000000..259b6a8 --- /dev/null +++ b/other/components/episode-email/sponsors-section.js @@ -0,0 +1,53 @@ +import {PropTypes} from 'react' +import {Box, Item, A} from 'react-html-email' + +export default SponsorsSection + +function SponsorsSection({sponsors}) { + const styles = { + box: { + minWidth: '100%', + border: '2px solid #222222', + borderCollapse: 'collapse', + msoTableLspace: '0pt', + msoTableRspace: '0pt', + msTextSizeAdjust: '100%', + WebkitTextSizeAdjust: '100%', + }, + item: { + fontFamily: 'Helvetica', + fontSize: '14px', + fontWeight: 'normal', + textAlign: 'left', + msoLineHeightRule: 'exactly', + msTextSizeAdjust: '100%', + WebkitTextSizeAdjust: '100%', + wordBreak: 'break-word', + color: '#202020', + lineHeight: '150%', + padding: 18, + }, + h2: { + marginTop: 0, + }, + } + return ( + + +

This episode made possible by

+
    + { + sponsors.map(({name, link, tagline}, i) => ( +
  • + {name} - {tagline} +
  • + )) + } +
+
+
+ ) +} +SponsorsSection.propTypes = { + sponsors: PropTypes.array, +} diff --git a/other/components/episode-email/util-components.js b/other/components/episode-email/util-components.js new file mode 100644 index 0000000..1f01166 --- /dev/null +++ b/other/components/episode-email/util-components.js @@ -0,0 +1,32 @@ +import {PropTypes} from 'react' + +export {Clear, Spacer, Line} + +function Clear({children}) { + const styles = { + visibility: 'hidden', + display: 'block', + height: 0, + clear: 'both', + } + return ( +
+ {children} +
+
+ ) +} +Clear.propTypes = { + children: PropTypes.node.isRequired, +} + +function Spacer({space = 32}) { + return
+} +Spacer.propTypes = { + space: PropTypes.number, +} + +function Line() { + return
+} diff --git a/other/components/episode-email/watch-show-button.js b/other/components/episode-email/watch-show-button.js new file mode 100644 index 0000000..0e7021c --- /dev/null +++ b/other/components/episode-email/watch-show-button.js @@ -0,0 +1,52 @@ +import {PropTypes} from 'react' +import {Box, Item, A} from 'react-html-email' + +export default WatchShowButton + +function WatchShowButton({page}) { + const styles = { + box: { + borderCollapse: 'separate !important', + borderRadius: '3px', + backgroundColor: '#AAAAAA', + msoTableLspace: '0pt', + msoTableRspace: '0pt', + msTextSizeAdjust: '100%', + WebkitTextSizeAdjust: '100%', + }, + p: { + lineHeight: '100%', + fontSize: '16px', + padding: '15px', + margin: 0, + }, + a: { + fontWeight: 'bold', + letterSpacing: 'normal', + textAlign: 'center', + textDecoration: 'none', + color: '#FFFFFF', + msoLineHeightRule: 'exactly', + msTextSizeAdjust: '100%', + WebkitTextSizeAdjust: '100%', + display: 'block', + }, + } + return ( + + +

+ + Watch the latest episode now + +

+
+
+ ) +} +WatchShowButton.propTypes = { + page: PropTypes.string.isRequired, +} diff --git a/other/components/show-description.js b/other/components/show-description.js index c8f4c8a..1dd9228 100644 --- a/other/components/show-description.js +++ b/other/components/show-description.js @@ -5,25 +5,27 @@ export default ShowDescription function ShowDescription() { return ( - [JavaScriptAir](http://javascriptair.com) is the live broadcast podcast - all about JavaScript hosted by - [egghead.io instructor](https://egghead.io/instructors/kentcdodds) - [Kent C. Dodds](https://twitter.com/kentcdodds). - Please visit the JavaScript Air website - ([javascriptair.com](http://javascriptair.com)) - to see upcoming and past episodes. - Go to [jsair.io/suggest](http://jsair.io/suggest) to - suggest topics and guests for the show. - Go to [jsair.io/feedback](http://jsair.io/feedback) to - provide feedback on this and other episodes. - Also be sure to subscribe to our email newsletter - at [jsair.io/email](http://jsair.io/email) and - follow JavaScript Air on - [Twitter](https://twitter.com/JavaScriptAir) - and [Google+](https://plus.google.com/105493143005968326308) - to stay up to date with future episodes. - Also, all episodes are on the - [YouTube](http://video.javascriptair.com) channel as well. + {` + [JavaScriptAir](http://javascriptair.com) is the live broadcast podcast + all about JavaScript hosted by + [egghead.io instructor](https://egghead.io/instructors/kentcdodds) + [Kent C. Dodds](https://twitter.com/kentcdodds). + Please visit the JavaScript Air website + ([javascriptair.com](http://javascriptair.com)) + to see upcoming and past episodes. + Go to [jsair.io/suggest](http://jsair.io/suggest) to + suggest topics and guests for the show. + Go to [jsair.io/feedback](http://jsair.io/feedback) to + provide feedback on this and other episodes. + Also be sure to subscribe to our email newsletter + at [jsair.io/email](http://jsair.io/email) and + follow JavaScript Air on + [Twitter](https://twitter.com/JavaScriptAir) + and [Google+](https://plus.google.com/105493143005968326308) + to stay up to date with future episodes. + Also, all episodes are on the + [YouTube](http://video.javascriptair.com) channel as well. + `} ) } diff --git a/other/generate-episode-description.js b/other/generate-episode-description.js index 2efcb62..8bb51b4 100644 --- a/other/generate-episode-description.js +++ b/other/generate-episode-description.js @@ -1,13 +1,12 @@ /* eslint react/prop-types: 0 */ import ReactDOMServer from 'react-dom/server' - +import inquirer from 'inquirer' import {copy} from 'copy-paste' import {getSponsorsForDate} from '../sponsors' import * as utils from '/utils' import episodeList from './utils/episode-list' -import inquirer from 'inquirer' import LinksPicksTips from './components/links-picks-tips' import Person from './components/person' @@ -80,9 +79,9 @@ function EpisodeDescription({episode, sponsors}) { Links, Picks, and Tips: { showAttendees.map((a, i) => ( -
+
- +
)) } diff --git a/other/get-srt-text.js b/other/get-srt-text.js index 1dd6058..280ce49 100644 --- a/other/get-srt-text.js +++ b/other/get-srt-text.js @@ -1,9 +1,10 @@ -const parser = require('subtitles-parser') const fs = require('fs') +const parser = require('subtitles-parser') + const input = process.argv[2] const output = input.replace('.srt', '.txt') const srt = fs.readFileSync(input, 'utf8') - + const data = parser.fromSrt(srt) const string = data.map(i => i.text).join(' ').replace(/\n/g, ' ').replace(/ {2,}/g, ' ') @@ -11,4 +12,3 @@ const string = data.map(i => i.text).join(' ').replace(/\n/g, ' ').replace(/ {2, fs.writeFileSync(output, string, 'utf8') console.log('written to %s', output) // eslint-disable-line no-console - diff --git a/other/get-up-tweet.js b/other/get-up-tweet.js index db4fa2f..e7fc3cc 100644 --- a/other/get-up-tweet.js +++ b/other/get-up-tweet.js @@ -8,7 +8,7 @@ import episodeList from './utils/episode-list' getEpisodeDirectory() .then(episodePathToEpisodeData) .then(generateMessage) - .then(message => message + ' ' + getRandomPositiveEmoji()) + .then(message => `${message} ${getRandomPositiveEmoji()}`) .then(copyMessage) .then(result => console.log(result)) .catch(err => console.error(err)) @@ -41,7 +41,7 @@ function episodePathToEpisodeData(episodeData) { } function generateMessage({title, guests, shortUrl}) { - const twitterHandles = sortPeople(guests).map(g => g.twitter ? `@${g.twitter}` : g.name) + const twitterHandles = sortPeople(guests).map(g => (g.twitter ? `@${g.twitter}` : g.name)) const url = shortUrl ? ` ${shortUrl}` : '' return `"${title}" w/ ${displayListify(twitterHandles).join('')} is up!${url}` diff --git a/other/podcastify-audio.js b/other/podcastify-audio.js index f311a64..90d3d35 100644 --- a/other/podcastify-audio.js +++ b/other/podcastify-audio.js @@ -1,5 +1,5 @@ -import podcastifyAudio from './utils/audio' import inquirer from 'inquirer' +import podcastifyAudio from './utils/audio' inquirer.prompt([ { diff --git a/other/shorten-episode-url/logic.js b/other/shorten-episode-url/logic.js index 23a19ab..c1ce53a 100644 --- a/other/shorten-episode-url/logic.js +++ b/other/shorten-episode-url/logic.js @@ -58,7 +58,7 @@ function beginPrompt(episodeData, noCopy) { function getApiKey(data = {}) { let key try { - key = require('../hive.api.ignored.json').key + key = require('../hive.api.ignored.json').key // eslint-disable-line global-require } catch (e) { key = null } diff --git a/other/shorten-url.js b/other/shorten-url.js index f6ca4fb..df36116 100644 --- a/other/shorten-url.js +++ b/other/shorten-url.js @@ -1,6 +1,7 @@ import qs from 'qs' import axios from 'axios' import {copy} from 'copy-paste' + export default shortenUrl function shortenUrl({ @@ -23,7 +24,7 @@ function shortenUrl({ return Promise.resolve(short) } return new Promise((resolve, reject) => { - copy(short, (err) => { + copy(short, err => { if (err) { reject(err) } @@ -37,7 +38,7 @@ function shortenUrl({ function getApiKey() { try { - return require('./hive.api.ignored.json').key + return require('./hive.api.ignored.json').key // eslint-disable-line global-require } catch (e) { throw new Error('you must provide an API key or have a hive.api.ignored.json') } diff --git a/other/utils/audio.js b/other/utils/audio.js index 503f12b..0de0f71 100644 --- a/other/utils/audio.js +++ b/other/utils/audio.js @@ -2,7 +2,9 @@ import path from 'path' import spawn from 'spawn-command' import shellEscape from 'shell-escape' import filenamify from 'filenamify' -import ffmpegPath from './ffmpeg-path' +import getFfmpegPath from './ffmpeg-path' + +const ffmpegPath = getFfmpegPath() export {podcastifyAudio as default, analyzeAudio, optimizeAudio} @@ -29,12 +31,12 @@ function analyzeAudio({file, episode}) { let buffer = '' const options = {stdio: [process.stdin, process.stdout, 'pipe']} spawn(command, options) - .stderr.on('data', (data) => { + .stderr.on('data', data => { const chunk = data.toString() buffer += chunk console.log(chunk) }) - .on('error', (err) => { + .on('error', err => { reject(err) }) .on('close', () => { diff --git a/other/utils/compress-image.js b/other/utils/compress-image.js index 1ccc54b..5e7178b 100644 --- a/other/utils/compress-image.js +++ b/other/utils/compress-image.js @@ -1,5 +1,5 @@ -import Imagina from 'imagina' import path from 'path' +import Imagina from 'imagina' import mv from 'mv' export default compressImage @@ -10,9 +10,9 @@ function compressImage(inputImagePath, outputFilePath) { } const imageDir = path.dirname(inputImagePath) const imageExtension = path.extname(inputImagePath).substring(1) - const imageFilename = path.basename(inputImagePath, '.' + imageExtension) - const resizedImagePath = path.join(imageDir, imageFilename + '.resized.' + imageExtension) - const finalImagePath = path.join(imageDir, imageFilename + '.resized.png') + const imageFilename = path.basename(inputImagePath, `.${imageExtension}`) + const resizedImagePath = path.join(imageDir, `${imageFilename}.resized.${imageExtension}`) + const finalImagePath = path.join(imageDir, `${imageFilename}.resized.png`) const im = new Imagina() @@ -37,7 +37,9 @@ function compressImage(inputImagePath, outputFilePath) { } function maybeConvert() { - if (imageExtension !== 'png') { + if (imageExtension === 'png') { + return undefined + } else { return new Promise((resolve, reject) => { convert(resizedImagePath, function onConvertDone(doneErr) { if (doneErr) { @@ -47,16 +49,14 @@ function compressImage(inputImagePath, outputFilePath) { } }) }) - } else { - return undefined } } function convert(inputPath, cb) { const dir = path.dirname(inputPath) const extension = path.extname(inputPath).substring(1) - const filename = path.basename(inputPath, '.' + extension) - const outputPath = path.join(dir, filename + '.png') + const filename = path.basename(inputPath, `.${extension}`) + const outputPath = path.join(dir, `${filename}.png`) im.convert(inputPath, outputPath, cb) } diff --git a/other/utils/download-twitter-photo.js b/other/utils/download-twitter-photo.js index 567b4c3..f75a077 100644 --- a/other/utils/download-twitter-photo.js +++ b/other/utils/download-twitter-photo.js @@ -1,8 +1,8 @@ -import path from 'path' -import request from 'request' import fs from 'fs' +import path from 'path' import temp from 'temp' import filenamify from 'filenamify' +import request from 'request' import {getProfileImageURL} from './twitter' import compressImage from './compress-image' diff --git a/other/utils/ffmpeg-path.js b/other/utils/ffmpeg-path.js index bcdae2d..c32010e 100644 --- a/other/utils/ffmpeg-path.js +++ b/other/utils/ffmpeg-path.js @@ -1,12 +1,19 @@ import which from 'which' let ffmpegPath -try { - ffmpegPath = which.sync('ffmpeg') -} catch (err) { - throw new Error( - 'Unable to get ffmpeg executable which is required to use podcastify-youtube-video' - ) -} -export default ffmpegPath +export default get + +function get() { + if (ffmpegPath) { + return ffmpegPath + } + try { + ffmpegPath = which.sync('ffmpeg') + return ffmpegPath + } catch (err) { + throw new Error( + 'Unable to get ffmpeg executable which is required to use podcastify-youtube-video' + ) + } +} diff --git a/other/utils/hijack-console.js b/other/utils/hijack-console.js new file mode 100644 index 0000000..82d1cd4 --- /dev/null +++ b/other/utils/hijack-console.js @@ -0,0 +1,24 @@ +export default hijackConsole + +function hijackConsole(ignoreLogs) { + hijack('warn') + hijack('error') + + function hijack(logger) { + const original = console[logger] + console[logger] = function hijackedConsole(...args) { + const line = args.join(' ') + const shouldIgnore = ignoreLogs.some(ignore => { + if (typeof ignore === 'string') { + return line.includes(ignore) + } else { + return ignore.test(line) + } + }) + if (!shouldIgnore) { + return original(...args) + } + return undefined + } + } +} diff --git a/other/utils/twitter.js b/other/utils/twitter.js index 74b1dc6..9761271 100644 --- a/other/utils/twitter.js +++ b/other/utils/twitter.js @@ -7,10 +7,10 @@ function getProfileImageURL(handle) { return new Promise((resolve, reject) => { const options = {screen_name: handle} // eslint-disable-line camelcase TW.get('users/show.json', options, (error, data) => { - if (!error) { - resolve(data.profile_image_url.replace('_normal', '')) - } else { + if (error) { reject(error) + } else { + resolve(data.profile_image_url.replace('_normal', '')) } }) }) @@ -19,10 +19,10 @@ function getProfileImageURL(handle) { function sendTweet(message) { return new Promise((resolve, reject) => { TW.post('statuses/update', {status: message}, (error, tweet) => { - if (!error) { - resolve(`Tweet sent ${tweet}`) - } else { + if (error) { reject(error) + } else { + resolve(`Tweet sent ${tweet}`) } }) }) @@ -30,7 +30,7 @@ function sendTweet(message) { function getTwitterConfig() { try { - return require('./twitter.api.ignored.json') + return require('./twitter.api.ignored.json') // eslint-disable-line global-require } catch (e) { throw new Error('you must provide Twitter API info with a twitter.api.ignored.json') } diff --git a/other/utils/video.js b/other/utils/video.js index 02c5444..ca5cac4 100644 --- a/other/utils/video.js +++ b/other/utils/video.js @@ -14,10 +14,12 @@ import YoutubeMp3Downloader from 'youtube-mp3-downloader' import inquirer from 'inquirer' import ProgressBar from 'cli-progress-bar' import {random as randomEmoji} from 'random-emoji' -import ffmpegPath from './ffmpeg-path' +import getFfmpegPath from './ffmpeg-path' import podcastifyAudio from './audio' import episodeList from './episode-list' +const ffmpegPath = getFfmpegPath() + export default podcastifyVideo function podcastifyVideo() { diff --git a/package-scripts.js b/package-scripts.js index 95ea157..9bf5f33 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -1,5 +1,5 @@ /* eslint max-len:[2, 200] */ -const commonWatch = '--watch ' + [ +const commonWatch = '--watch ' + [ // eslint-disable-line prefer-template 'src', 'sponsors', 'episodes', 'shared', 'generate', ].join(' --watch ') const otherComponentsWatch = `${commonWatch} --watch other/components` diff --git a/package.json b/package.json index 35dae8a..7ff2083 100644 --- a/package.json +++ b/package.json @@ -22,66 +22,67 @@ }, "homepage": "https://github.com/javascriptair/javascriptair.github.io#readme", "devDependencies": { - "aphrodite": "0.3.1", + "aphrodite": "0.4.1", "async": "2.0.0-rc.6", - "autoprefixer": "6.3.6", - "axios": "0.11.0", - "babel-cli": "6.7.7", - "babel-core": "6.7.7", - "babel-eslint": "6.0.4", - "babel-plugin-module-alias": "1.4.0", + "autoprefixer": "6.3.7", + "axios": "0.12.0", + "babel-cli": "6.10.1", + "babel-core": "6.10.4", + "babel-eslint": "6.1.0", + "babel-plugin-module-alias": "1.5.0", "babel-plugin-react-require": "2.1.0", + "babel-plugin-transform-es2015-destructuring": "6.9.0", "babel-preset-node6": "11.0.0", - "babel-preset-react": "6.5.0", - "babel-preset-stage-2": "6.5.0", - "classnames": "2.2.4", + "babel-preset-react": "6.11.1", + "babel-preset-stage-2": "6.11.0", + "classnames": "2.2.5", "cli-progress-bar": "1.0.0", - "copy-paste": "1.2.0", - "cssnano": "3.5.2", + "copy-paste": "1.3.0", + "cssnano": "3.7.1", "deindent": "0.1.0", - "eslint": "2.8.0", - "eslint-config-kentcdodds": "6.2.1", - "eslint-import-resolver-babel-module-alias": "1.0.3", - "eslint-plugin-react": "5.0.1", + "eslint": "3.0.1", + "eslint-config-kentcdodds": "8.0.0", "filenamify": "1.2.1", - "ghooks": "1.2.1", - "glob": "7.0.3", + "ghooks": "1.3.2", + "glob": "7.0.5", "http-server": "0.9.0", "imagina": "0.1.4", - "inquirer": "1.0.2", + "inquirer": "1.1.2", "inquirer-directory": "2.0.0", - "lodash": "4.11.1", + "lodash": "4.13.1", "marked": "0.3.5", - "moment": "2.13.0", + "moment": "2.14.1", "mv": "2.1.1", - "nodemon": "1.9.1", - "npm-run-all": "1.8.0", - "p-s": "1.0.0", + "nodemon": "1.9.2", + "npm-run-all": "2.3.0", + "p-s": "1.0.2", "pageres": "4.1.2", "plop": "1.5.0", "postcss-cli": "2.5.2", - "postcss-import": "8.1.0", - "postcss-mixins": "^4.0.0", + "postcss-import": "8.1.2", + "postcss-mixins": "5.0.0", "postcss-nested": "^1.0.0", - "qs": "6.1.0", - "query-string": "4.1.0", + "qs": "6.2.0", + "query-string": "4.2.2", "random-emoji": "1.0.1", - "react": "15.0.1", - "react-dom": "15.0.1", + "react": "15.2.0", + "react-dom": "15.2.0", "react-html-email": "1.0.2", "read-glob": "2.1.0", - "rimraf": "2.5.2", + "request": "2.72.0", + "rimraf": "2.5.3", "shell-escape": "0.2.0", - "static-server": "2.0.2", + "spawn-command": "0.0.2-1", + "static-server": "2.0.3", "striptags": "2.1.1", "subtitles-parser": "0.0.2", - "surge": "0.17.7", + "surge": "0.18.0", "temp": "0.8.3", "trash": "3.4.1", - "twitter": "1.2.5", - "uglify-js": "2.6.2", + "twitter": "1.3.0", + "uglify-js": "2.7.0", "user-home": "2.0.0", - "which": "1.2.4", + "which": "1.2.10", "youtube-mp3-downloader": "0.4.2" } } diff --git a/resources/panelists/index.js b/resources/panelists/index.js index 6db2f69..25fdb4a 100644 --- a/resources/panelists/index.js +++ b/resources/panelists/index.js @@ -16,4 +16,4 @@ panelists.forEach(p => { p.imgSrc = `/resources/panelists/${p.twitter}.png` }) -export {panelists} +export default panelists diff --git a/scripts/screenshot-episode.js b/scripts/screenshot-episode.js index 03edfce..ddedea1 100644 --- a/scripts/screenshot-episode.js +++ b/scripts/screenshot-episode.js @@ -1,10 +1,12 @@ import generateEpisodeScreenshotPage from '../generate/episode-screenshot' -const path = process.argv[2] +const pathArg = 2 +const path = process.argv[pathArg] generateEpisodeScreenshotPage(path, (err, screenshotFile) => { if (err) { throw err } console.log(`done creating the screenshot ${screenshotFile} 🎉`) - process.exit(0) + const noError = 0 + process.exit(noError) // eslint-disable-line no-process-exit }) diff --git a/scripts/screenshot-episodes.js b/scripts/screenshot-episodes.js index 7d2bdd8..5d49aa1 100644 --- a/scripts/screenshot-episodes.js +++ b/scripts/screenshot-episodes.js @@ -11,5 +11,7 @@ async.eachLimit(episodeDirectories, parallelLimit, generateEpisodeScreenshotPage throw err } console.log('All screenshots taken 🌈') - process.exit(0) + // not sure how to avoid process.exit here... + const NO_ERROR = 0 + process.exit(NO_ERROR) // eslint-disable-line no-process-exit }) diff --git a/service-worker.js b/service-worker.js index 48790c1..bc75c75 100644 --- a/service-worker.js +++ b/service-worker.js @@ -35,14 +35,14 @@ self.addEventListener('fetch', function onServiceWorkerFetch(event) { // First we look if we can get the (maybe updated) // resource from the network fetch(event.request).then(function updateCacheAndReturnNetworkResponse(networkResponse) { - console.log('fetch from network for ' + event.request.url + ' successfull, updating cache') + console.log(`fetch from network for ${event.request.url} successfull, updating cache`) caches.open(currentCache).then(function addToCache(cache) { return cache.add(event.request) }) return networkResponse }).catch(function lookupCachedResponse(reason) { // On failure, look up in the Cache for the requested resource - console.log('fetch from network for ' + event.request.url + ' failed:', reason) + console.log(`fetch from network for ${event.request.url} failed:`, reason) return caches.match(event.request).then(function returnCachedResponse(cachedResponse) { return cachedResponse }) diff --git a/src/components/decor.js b/src/components/decor.js index f01ed24..f0fc4f0 100644 --- a/src/components/decor.js +++ b/src/components/decor.js @@ -1,4 +1,5 @@ import {StyleSheet, css} from 'aphrodite' + export default Decor function Decor() { diff --git a/src/components/episode/calendar.js b/src/components/episode/calendar.js index c5211e5..e1ccfbf 100644 --- a/src/components/episode/calendar.js +++ b/src/components/episode/calendar.js @@ -43,8 +43,9 @@ function Calendar({episode}) { page, } = episode const dateCurrent = moment(date) - const dateAfter = moment(date).add(1, 'days') - const dateBefore = moment(date).subtract(1, 'days') + const dayDifference = 1 + const dateAfter = moment(date).add(dayDifference, 'days') + const dateBefore = moment(date).subtract(dayDifference, 'days') const {styles} = Calendar return (
@@ -130,7 +131,7 @@ function MainDate({page, date, hangoutUrl}) { black={true} iconStyles={styles.mainDateRSVPIcon} /> - Add to Calendar + Add to Calendar
) @@ -176,7 +177,7 @@ MainDate.styles = StyleSheet.create({ function DateSquare({side, date, children}) { const {styles} = DateSquare return ( -
+
{date.format('ddd')}
{date.format('D')}
{children} diff --git a/src/components/episode/tweet-link.js b/src/components/episode/tweet-link.js index d4c2f22..1ed1aec 100644 --- a/src/components/episode/tweet-link.js +++ b/src/components/episode/tweet-link.js @@ -16,6 +16,7 @@ function TweetLink({episode}) { black={true} href={tweetUrl} target="_blank" + rel="noopener noreferrer" title="Tweet this" name="twitter" /> @@ -37,9 +38,9 @@ TweetLink.styles = StyleSheet.create({ }) function getMessage({past, taglessTitle, sortedGuests, shortUrl, date}) { - const guestList = sortedGuests.map((guest) => guest.twitter ? `@${guest.twitter}` : guest.name) + const guestList = sortedGuests.map(guest => (guest.twitter ? `@${guest.twitter}` : guest.name)) const message = past ? `Check out "${taglessTitle}" w/ ${displayListify(guestList).join('')} ${shortUrl}` : `Watch "${taglessTitle}" live w/ ${displayListify(guestList).join('')} on ${date} ${shortUrl}` - return message + ' ' + getRandomPositiveEmoji() + return `${message} ${getRandomPositiveEmoji()}` } diff --git a/src/components/icon-link.js b/src/components/icon-link.js index f289a6a..52b4ac1 100644 --- a/src/components/icon-link.js +++ b/src/components/icon-link.js @@ -7,12 +7,12 @@ import Icon from './icon' export default IconLink function IconLink(props) { + const {black, ...remaining} = props // eslint-disable-line const {styles} = IconLink - const {black} = props const iconClassName = css(black ? styles.blackIcon : styles.icon) return ( - - + + ) } diff --git a/src/components/icon.js b/src/components/icon.js index 2706302..483fe38 100644 --- a/src/components/icon.js +++ b/src/components/icon.js @@ -12,12 +12,12 @@ export const graphics = { facebook: , github: , calendar: ( - - - ), + + + ), chevronRight: ( - - ), + + ), logo: , contactus: , } diff --git a/src/components/page.js b/src/components/page.js index 19af100..7fda031 100644 --- a/src/components/page.js +++ b/src/components/page.js @@ -1,6 +1,6 @@ +import striptags from 'striptags' import GoogleAnalyticsScript from './scripts/google-analytics' import StartServiceWorker from './scripts/start-service-worker' -import striptags from 'striptags' export default Page @@ -35,7 +35,7 @@ function Page({ /> - + {children} diff --git a/src/components/person-group.js b/src/components/person-group.js index 773f88d..8550117 100644 --- a/src/components/person-group.js +++ b/src/components/person-group.js @@ -1,6 +1,6 @@ import {PropTypes} from 'react' -import Person from './person' import {StyleSheet, css} from 'aphrodite' +import Person from './person' export default PersonGroup diff --git a/src/components/person.js b/src/components/person.js index 84283ed..cee5179 100644 --- a/src/components/person.js +++ b/src/components/person.js @@ -15,7 +15,7 @@ function Person({ const {styles} = Person const personClassName = css(styles.person) className = className ? `${className} ${personClassName}` : personClassName - className += ' ' + personClassNames.root + className += ` ${personClassNames.root}` const nameClassName = `${personClassNames.name} ${css(styles.name)}` const twitterClassName = `${personClassNames.twitter} ${css(styles.twitter)}` const circularImageClassName = squareImage ? '' : css(styles.circular) diff --git a/src/components/rsvp-icon.js b/src/components/rsvp-icon.js index fb55bee..e88ca26 100644 --- a/src/components/rsvp-icon.js +++ b/src/components/rsvp-icon.js @@ -1,6 +1,6 @@ import {PropTypes} from 'react' -import IconLink from './icon-link' import {StyleSheet, css} from 'aphrodite' +import IconLink from './icon-link' export default RSVPIcon @@ -9,14 +9,14 @@ function RSVPIcon(props) { return
- - - { - !person.hasNotes ? - 'No links, tips, or picks this week' : - - } -
+ + + { + noNotes ? + 'No links, tips, or picks this week' : + + } +