diff --git a/.prettierrc b/.prettierrc index 3b59ede..b31f053 100644 --- a/.prettierrc +++ b/.prettierrc @@ -13,6 +13,12 @@ "options": { "printWidth": 53 } + }, + { + "files": ["snippets/snippets/**/ignoreWidth/*.ts"], + "options": { + "printWidth": 120 + } } ] } diff --git a/README.md b/README.md index 9e8428e..6f797df 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,29 @@ # [TypeScript for Beginner Programmers](https://ts.chibicode.com) -### This is the repository for the TypeScript tutorial website called **[TypeScript for Beginner Programmers](https://ycombinator.chibicode.com/)**. +This is the repository for the TypeScript tutorial website called **[TypeScript for Beginner Programmers](https://ycombinator.chibicode.com/)**.

+## Article 3: [Your Coding Tutorial Might Need Some Refactoring](https://ts.chibicode.com/refactor) + +

+ +

+ +## Article 2: [TypeScript Tutorial for JS Programmers Who Know How to Build a Todo App](https://ts.chibicode.com/todo) + +

+ +

+ +## Article 1: [TypeScript Generics for People Who Gave Up on Understanding Generics](https://ts.chibicode.com/generics) + +

+ +

+ ## License & Credits - For emojis, I’m using [Twemoji](https://github.com/twitter/twemoji) by Twitter (CC-BY 4.0 license). @@ -16,6 +34,6 @@ **Shu Uesugi** -- Twitter: [@chibicode](https://twitter.com/chibicode) - [Website](https://chibicode.com) +- Twitter: [@chibicode](https://twitter.com/chibicode) - Email: [shu@chibicode.com](mailto:shu@chibicode.com) \ No newline at end of file diff --git a/public/images/og-refactor.png b/public/images/og-refactor.png new file mode 100644 index 0000000..991e782 Binary files /dev/null and b/public/images/og-refactor.png differ diff --git a/public/images/refactor/asOfWriting.png b/public/images/refactor/asOfWriting.png new file mode 100644 index 0000000..a48e8d0 Binary files /dev/null and b/public/images/refactor/asOfWriting.png differ diff --git a/public/images/refactor/tsdoc.gif b/public/images/refactor/tsdoc.gif new file mode 100644 index 0000000..74f3192 Binary files /dev/null and b/public/images/refactor/tsdoc.gif differ diff --git a/snippets/bin/generateSnippetsBundle.ts b/snippets/bin/generateSnippetsBundle.ts index 149394a..2c7e0ed 100644 --- a/snippets/bin/generateSnippetsBundle.ts +++ b/snippets/bin/generateSnippetsBundle.ts @@ -2,18 +2,29 @@ const glob = require('glob') const fs = require('fs') const chokidar = require('chokidar') +const filePathToKey = (file: string) => + file + .replace(/\.\/snippets\/snippets\/\w+\//, '') + .replace(/longerWidth\//, '') + .replace(/ignoreWidth\//, '') + .replace(/\.ts/, '') + const regenerate = () => { glob('./snippets/snippets/**/*.ts', (_: any, files: readonly string[]) => { + if ( + new Set(files.map(filePathToKey)).size !== files.map(filePathToKey).length + ) { + throw new Error('Duplicate file name') + } + const result = files .map(file => { const contents = fs.readFileSync(file, 'utf8') - return `export const ${file - .replace(/\.\/snippets\/snippets\/\w+\//, '') - .replace(/longerWidth\//, '') - .replace(/\.ts/, '')} = \`${contents + return `export const ${filePathToKey(file)} = \`${contents .trim() .replace(/^;/m, '') - .replace(/`/g, '\\`')}\`` + .replace(/`/g, '\\`') + .replace(/\$/g, '\\$')}\`` }) .join('\n\n') diff --git a/snippets/snippets/refactor/bxzx.ts b/snippets/snippets/refactor/bxzx.ts new file mode 100644 index 0000000..cf0e5ed --- /dev/null +++ b/snippets/snippets/refactor/bxzx.ts @@ -0,0 +1,5 @@ +// It could have been useful if you could pass +// both number AND string, and have it repeat +// the string the specified number of times +padLeft('Hello world', 4, '#') +// → "####Hello world" diff --git a/snippets/snippets/refactor/crgn.ts b/snippets/snippets/refactor/crgn.ts new file mode 100644 index 0000000..08a0ebe --- /dev/null +++ b/snippets/snippets/refactor/crgn.ts @@ -0,0 +1,6 @@ +// If the second parameter is string, then +// that string is appended to the left side +padLeft('Hello world', 'Jim: ') +// → "Jim: Hello world" + +// Ask yourself: Would you EVER do this? diff --git a/snippets/snippets/refactor/hfdq.ts b/snippets/snippets/refactor/hfdq.ts new file mode 100644 index 0000000..216f386 --- /dev/null +++ b/snippets/snippets/refactor/hfdq.ts @@ -0,0 +1,16 @@ +function paddingLeftCss(val: number | string) { + if (typeof val === 'number') { + return `padding-left: ${val * 0.25}rem;` + } else { + return `padding-left: ${val};` + } +} + +// padding-left: 0.25rem; +paddingLeftCss(1) + +// padding-left: 0.5rem; +paddingLeftCss(2) + +// padding-left: 10%; +paddingLeftCss('10%') diff --git a/snippets/snippets/refactor/ignoreWidth/mvsz.ts b/snippets/snippets/refactor/ignoreWidth/mvsz.ts new file mode 100644 index 0000000..a8caf53 --- /dev/null +++ b/snippets/snippets/refactor/ignoreWidth/mvsz.ts @@ -0,0 +1 @@ +function makePair() {} diff --git a/snippets/snippets/refactor/ignoreWidth/zgvn.ts b/snippets/snippets/refactor/ignoreWidth/zgvn.ts new file mode 100644 index 0000000..b9a3d67 --- /dev/null +++ b/snippets/snippets/refactor/ignoreWidth/zgvn.ts @@ -0,0 +1 @@ +type Todo = Readonly<{ id: number; text: string; done: boolean; place: Place }> diff --git a/snippets/snippets/refactor/lcfe.ts b/snippets/snippets/refactor/lcfe.ts new file mode 100644 index 0000000..11f373e --- /dev/null +++ b/snippets/snippets/refactor/lcfe.ts @@ -0,0 +1,9 @@ +// If the second parameter is number, then that +// number of spaces is added to the left side +padLeft('Hello world', 4) +// → " Hello world" + +// If the second parameter is string, then +// that string is appended to the left side +padLeft('Hello world', 'Jim: ') +// → "Jim: Hello world" diff --git a/snippets/snippets/refactor/longerWidth/riis.ts b/snippets/snippets/refactor/longerWidth/riis.ts new file mode 100644 index 0000000..27eece3 --- /dev/null +++ b/snippets/snippets/refactor/longerWidth/riis.ts @@ -0,0 +1,19 @@ +/** + * Takes a string and adds "padding" to the left. + * + * If 'padding' is a number, then that number of + * spaces is added to the left side. + * + * If 'padding' is a string, then 'padding' is + * appended to the left side. + */ +function padLeft( + value: string, + padding: number | string +) { + if (typeof padding === 'number') { + return Array(padding + 1).join(' ') + value + } else { + return padding + value + } +} diff --git a/snippets/snippets/refactor/lplh.ts b/snippets/snippets/refactor/lplh.ts new file mode 100644 index 0000000..2dbce8d --- /dev/null +++ b/snippets/snippets/refactor/lplh.ts @@ -0,0 +1 @@ +console.log(1 + 2) diff --git a/snippets/snippets/refactor/onux.ts b/snippets/snippets/refactor/onux.ts new file mode 100644 index 0000000..bc7bb89 --- /dev/null +++ b/snippets/snippets/refactor/onux.ts @@ -0,0 +1,37 @@ +function extend( + first: First, + second: Second +): First & Second { + const result: Partial = {} + for (const prop in first) { + if (first.hasOwnProperty(prop)) { + ;(result as First)[prop] = first[prop] + } + } + for (const prop in second) { + if (second.hasOwnProperty(prop)) { + ;(result as Second)[prop] = second[prop] + } + } + return result as First & Second +} + +class Person { + constructor(public name: string) {} +} + +interface Loggable { + log(name: string): void +} + +class ConsoleLogger implements Loggable { + log(name) { + console.log(`Hello, I'm ${name}.`) + } +} + +const jim = extend( + new Person('Jim'), + ConsoleLogger.prototype +) +jim.log(jim.name) diff --git a/snippets/snippets/refactor/vnfq.ts b/snippets/snippets/refactor/vnfq.ts new file mode 100644 index 0000000..f270ca8 --- /dev/null +++ b/snippets/snippets/refactor/vnfq.ts @@ -0,0 +1,13 @@ +type Person = { name: string } +type Loggable = { log: (name: string) => void } + +// Use & to make jim BOTH Person AND Loggable +const jim: Person & Loggable = { + name: 'Jim', + log: name => { + console.log(`Hello, I'm ${name}.`) + } +} + +// "Hello, I’m Jim." +jim.log(jim.name) diff --git a/snippets/snippets/refactor/xwbz.ts b/snippets/snippets/refactor/xwbz.ts new file mode 100644 index 0000000..d964c04 --- /dev/null +++ b/snippets/snippets/refactor/xwbz.ts @@ -0,0 +1,4 @@ +function makePair< + F extends number | string, + S extends boolean | F +>() {} diff --git a/src/components/BubbleQuotes.tsx b/src/components/BubbleQuotes.tsx index f04cc01..5c11e53 100644 --- a/src/components/BubbleQuotes.tsx +++ b/src/components/BubbleQuotes.tsx @@ -5,7 +5,7 @@ import useTheme from 'src/hooks/useTheme' import { allColors } from 'src/lib/theme/colors' interface BubbleQuoteProps { - type: keyof typeof emojiToComponent + type?: keyof typeof emojiToComponent children: React.ReactNode backgroundColor?: keyof typeof allColors } @@ -39,21 +39,23 @@ const BubbleQuotes = ({ display: flex; `} > -
- -
+ ${ns} { + padding-top: ${size === 'lg' ? spaces(0) : spaces(0.25)}; + margin-right: ${size === 'lg' ? spaces(1) : spaces(0.75)}; + font-size: ${size === 'lg' ? fontSizes(4) : fontSizes(2)}; + } + `} + > + + + )}
{ - const { ns, colors, fontSizes, spaces, radii, lineHeights } = useTheme() + const { + ns, + colors, + fontSizes, + spaces, + radii, + lineHeights, + letterSpacings + } = useTheme() return ( <> + {!isFirst && ( +
+ )}
Slide{' '} @@ -94,7 +115,7 @@ const Card = ({ {' '} @@ -189,7 +210,7 @@ const Card = ({ padding: ${spaces(0.75)}; ${ns} { - padding-top: ${spaces(1.25)}; + padding-top: ${spaces(1)}; padding-left: ${spaces(1.5)}; padding-right: ${spaces(1.5)}; padding-bottom: ${spaces(1)}; @@ -199,21 +220,26 @@ const Card = ({ border-bottom-left-radius: ${radii(0.5)}; `} > +

+ Side Note +

{footer.content}
)}
- {!isLast && ( -
- )} ) } diff --git a/src/components/CardTitleText.tsx b/src/components/CardTitleText.tsx index 342aa56..cc74746 100644 --- a/src/components/CardTitleText.tsx +++ b/src/components/CardTitleText.tsx @@ -3,14 +3,14 @@ import { css, jsx } from '@emotion/core' import useTheme from 'src/hooks/useTheme' const CardTitleText = (props: JSX.IntrinsicElements['h3']) => { - const { fontSizes, lineHeights, ns, spaces } = useTheme() + const { fontSizes, lineHeights, ns, spaces, letterSpacings } = useTheme() return (

boolean @@ -37,6 +39,8 @@ const CodeBlock = ({ tokenIndexIndentWorkaround?: number defaultErrorHighlight?: boolean narrowText?: boolean + smallText?: boolean + semiTransparentTextExceptHighlight?: boolean }) => { const [resultVisible, setResultVisible] = useState(defaultResultVisible) const { @@ -46,7 +50,8 @@ const CodeBlock = ({ maxWidths, spaces, fontSizes, - letterSpacings + letterSpacings, + nt } = useTheme() const buttonOnClick = () => setResultVisible(true) @@ -93,29 +98,42 @@ const CodeBlock = ({ narrowText && css` letter-spacing: ${letterSpacings('smallCode')}; + `, + smallText && + css` + font-size: ${fontSizes(0.75)}; + + ${nt} { + font-size: ${fontSizes(0.75)}; + } ` ]} - lineCssOverrides={(lineIndex, tokenIndex) => + lineCssOverrides={(lineIndex, tokenIndex) => [ + semiTransparentTextExceptHighlight && + css` + opacity: 0.5; + `, ((!!shouldHighlight && !resultVisible && shouldHighlight(lineIndex, tokenIndex)) || (!!shouldHighlightResult && resultVisible && shouldHighlightResult(lineIndex, tokenIndex))) && - css` - font-weight: bold; - background: ${((shouldHighlightResult && resultVisible) || - defaultErrorHighlight) && - resultError - ? colors('white') - : colors('yellowHighlight')}; - border-bottom: ${((shouldHighlightResult && resultVisible) || - defaultErrorHighlight) && - resultError - ? `3px solid ${colors('red')}` - : `2px solid ${colors('darkOrange')}`}; - ` - } + css` + opacity: 1; + font-weight: bold; + background: ${((shouldHighlightResult && resultVisible) || + defaultErrorHighlight) && + resultError + ? colors('white') + : colors('yellowHighlight')}; + border-bottom: ${((shouldHighlightResult && resultVisible) || + defaultErrorHighlight) && + resultError + ? `3px solid ${colors('red')}` + : `2px solid ${colors('darkOrange')}`}; + ` + ]} language={noHighlight ? 'diff' : 'typescript'} /> {result && ( diff --git a/src/components/CodeResult.tsx b/src/components/CodeResult.tsx index eb211ff..fb15a98 100644 --- a/src/components/CodeResult.tsx +++ b/src/components/CodeResult.tsx @@ -6,12 +6,12 @@ const CodeResult = ({ resultText, resultComponent, resultError, - resultType + resultType = 'default' }: { resultText?: React.ReactNode resultError?: boolean resultComponent?: React.ReactNode - resultType: 'default' | 'top' | 'bottom' + resultType?: 'default' | 'top' | 'bottom' caption?: React.ReactNode belowResult?: React.ReactNode }) => { diff --git a/src/components/CodeResultWrapper.tsx b/src/components/CodeResultWrapper.tsx new file mode 100644 index 0000000..bcbf1f5 --- /dev/null +++ b/src/components/CodeResultWrapper.tsx @@ -0,0 +1,30 @@ +/** @jsx jsx */ +import { css, jsx } from '@emotion/core' +import useTheme from 'src/hooks/useTheme' + +const CodeResultWrapper = ({ children }: { children: React.ReactNode }) => { + const { spaces, ns, maxWidths } = useTheme() + return ( +
+
+ {children} +
+
+ ) +} + +export default CodeResultWrapper diff --git a/src/components/ContentTags/A.tsx b/src/components/ContentTags/A.tsx index 500a5cc..ed73553 100644 --- a/src/components/ContentTags/A.tsx +++ b/src/components/ContentTags/A.tsx @@ -5,7 +5,7 @@ import colors from 'src/lib/theme/colors' export const baseLinkCss = (themeColors: typeof colors) => css` &:hover { - background: ${themeColors('white85')}; + background: ${themeColors('white70')}; } ` diff --git a/src/components/ContentTags/Blockquote.tsx b/src/components/ContentTags/Blockquote.tsx new file mode 100644 index 0000000..741ffb1 --- /dev/null +++ b/src/components/ContentTags/Blockquote.tsx @@ -0,0 +1,20 @@ +/** @jsx jsx */ +import { css, jsx } from '@emotion/core' +import useTheme from 'src/hooks/useTheme' + +const Blockquote = (props: JSX.IntrinsicElements['blockquote']) => { + const { colors, spaces } = useTheme() + return ( +
+ ) +} + +export default Blockquote diff --git a/src/components/ContentTags/Highlight.tsx b/src/components/ContentTags/Highlight.tsx index 4e2e9df..94bbeb6 100644 --- a/src/components/ContentTags/Highlight.tsx +++ b/src/components/ContentTags/Highlight.tsx @@ -13,7 +13,24 @@ const Highlight = ({ {...props} css={[ css` - background: ${color ? colors(color) : colors('white85')}; + background: ${color ? colors(color) : colors('white70')}; + ` + ]} + /> + ) +} + +export const ForegroundHighlight = ({ + color, + ...props +}: JSX.IntrinsicElements['span'] & { color?: keyof typeof allColors }) => { + const { colors } = useTheme() + return ( + diff --git a/src/components/ContentTags/Hr.tsx b/src/components/ContentTags/Hr.tsx index d317c8f..e2928e0 100644 --- a/src/components/ContentTags/Hr.tsx +++ b/src/components/ContentTags/Hr.tsx @@ -1,8 +1,12 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core' import useTheme from 'src/hooks/useTheme' +import { allColors } from 'src/lib/theme/colors' -const Hr = (props: JSX.IntrinsicElements['hr']) => { +const Hr = ({ + color, + ...props +}: JSX.IntrinsicElements['hr'] & { color?: keyof typeof allColors }) => { const { colors, spaces } = useTheme() return (
{ border-top: none; border-left: none; border-right: none; - border-bottom: 5px solid ${colors('white85')}; + border-bottom: 5px solid ${colors(color || 'white')}; margin: ${spaces(2)} auto ${spaces(2)}; max-width: 8rem; `} diff --git a/src/components/ContentTags/Image.tsx b/src/components/ContentTags/Image.tsx index 06f6d7c..9023a82 100644 --- a/src/components/ContentTags/Image.tsx +++ b/src/components/ContentTags/Image.tsx @@ -2,12 +2,15 @@ import { css, jsx } from '@emotion/core' import useTheme from 'src/hooks/useTheme' import Caption from 'src/components/Caption' +import { allMaxWidths } from 'src/lib/theme/maxWidths' const Image = ({ caption, + customWidth, ...props }: JSX.IntrinsicElements['img'] & { caption?: React.ReactNode + customWidth?: keyof typeof allMaxWidths }) => { const { spaces, maxWidths } = useTheme() return ( @@ -22,7 +25,7 @@ const Image = ({ display: block; margin: 0 auto; max-width: 100%; - width: ${maxWidths('sm')}; + width: ${maxWidths(customWidth || 'sm')}; `} {...props} /> diff --git a/src/components/ContentTags/index.tsx b/src/components/ContentTags/index.tsx index 70a8b9f..5200f8c 100644 --- a/src/components/ContentTags/index.tsx +++ b/src/components/ContentTags/index.tsx @@ -1,7 +1,11 @@ export { default as P } from 'src/components/ContentTags/P' export { default as Code } from 'src/components/ContentTags/Code' -export { default as Highlight } from 'src/components/ContentTags/Highlight' +export { + default as Highlight, + ForegroundHighlight +} from 'src/components/ContentTags/Highlight' export { default as Hr } from 'src/components/ContentTags/Hr' export { default as A } from 'src/components/ContentTags/A' +export { default as Blockquote } from 'src/components/ContentTags/Blockquote' export { default as Image } from 'src/components/ContentTags/Image' export { Ul, Ol, UlLi, OlLi } from 'src/components/ContentTags/List' diff --git a/src/components/Emoji/BadExample.tsx b/src/components/Emoji/BadExample.tsx new file mode 100644 index 0000000..1b2cb22 --- /dev/null +++ b/src/components/Emoji/BadExample.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +const SvgBadExample = (props: React.SVGProps) => ( + + + + + + + + + + + +) + +export default SvgBadExample diff --git a/src/components/Emoji/Brain.tsx b/src/components/Emoji/Brain.tsx new file mode 100644 index 0000000..447b279 --- /dev/null +++ b/src/components/Emoji/Brain.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +const SvgBrain = (props: React.SVGProps) => ( + + + + + + + + + +) + +export default SvgBrain diff --git a/src/components/Emoji/CleanCode.tsx b/src/components/Emoji/CleanCode.tsx new file mode 100644 index 0000000..33763e6 --- /dev/null +++ b/src/components/Emoji/CleanCode.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +const SvgCleanCode = (props: React.SVGProps) => ( + + + + + + + +) + +export default SvgCleanCode diff --git a/src/components/Emoji/CleanTutorial.tsx b/src/components/Emoji/CleanTutorial.tsx new file mode 100644 index 0000000..2bf4237 --- /dev/null +++ b/src/components/Emoji/CleanTutorial.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +const SvgCleanTutorial = (props: React.SVGProps) => ( + + + + + + + + + + +) + +export default SvgCleanTutorial diff --git a/src/components/Emoji/Cross.tsx b/src/components/Emoji/Cross.tsx new file mode 100644 index 0000000..8bb45bf --- /dev/null +++ b/src/components/Emoji/Cross.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +const SvgCross = (props: React.SVGProps) => ( + + + +) + +export default SvgCross diff --git a/src/components/Emoji/Dash.tsx b/src/components/Emoji/Dash.tsx new file mode 100644 index 0000000..6f99ff5 --- /dev/null +++ b/src/components/Emoji/Dash.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +const SvgDash = (props: React.SVGProps) => ( + + + +) + +export default SvgDash diff --git a/src/components/Emoji/DoubleArrow.tsx b/src/components/Emoji/DoubleArrow.tsx new file mode 100644 index 0000000..9c18ddc --- /dev/null +++ b/src/components/Emoji/DoubleArrow.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const SvgDoubleArrow = (props: React.SVGProps) => ( + + + + + +) + +export default SvgDoubleArrow diff --git a/src/components/Emoji/FastForward.tsx b/src/components/Emoji/FastForward.tsx new file mode 100644 index 0000000..e82d525 --- /dev/null +++ b/src/components/Emoji/FastForward.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const SvgFastForward = (props: React.SVGProps) => ( + + + + +) + +export default SvgFastForward diff --git a/src/components/Emoji/HeartCat.tsx b/src/components/Emoji/HeartCat.tsx new file mode 100644 index 0000000..5d4360c --- /dev/null +++ b/src/components/Emoji/HeartCat.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +const SvgHeartCat = (props: React.SVGProps) => ( + + + + + + + + + + +) + +export default SvgHeartCat diff --git a/src/components/Emoji/HeartLetter.tsx b/src/components/Emoji/HeartLetter.tsx new file mode 100644 index 0000000..1fe5711 --- /dev/null +++ b/src/components/Emoji/HeartLetter.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +const SvgHeartLetter = (props: React.SVGProps) => ( + + + + + + + +) + +export default SvgHeartLetter diff --git a/src/components/Emoji/LetterC.tsx b/src/components/Emoji/LetterC.tsx new file mode 100644 index 0000000..4f87302 --- /dev/null +++ b/src/components/Emoji/LetterC.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +const SvgLetterC = (props: React.SVGProps) => ( + + + + + + + + +) + +export default SvgLetterC diff --git a/src/components/Emoji/NumberText.tsx b/src/components/Emoji/NumberText.tsx new file mode 100644 index 0000000..c622cd6 --- /dev/null +++ b/src/components/Emoji/NumberText.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +const SvgNumberText = (props: React.SVGProps) => ( + + + + + + +) + +export default SvgNumberText diff --git a/src/components/Emoji/Plane.tsx b/src/components/Emoji/Plane.tsx new file mode 100644 index 0000000..2a7971d --- /dev/null +++ b/src/components/Emoji/Plane.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +const SvgPlane = (props: React.SVGProps) => ( + + + + + + +) + +export default SvgPlane diff --git a/src/components/Emoji/Prettier.tsx b/src/components/Emoji/Prettier.tsx new file mode 100644 index 0000000..69c4bb9 --- /dev/null +++ b/src/components/Emoji/Prettier.tsx @@ -0,0 +1,224 @@ +import React from 'react' + +const SvgPrettier = (props: React.SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export default SvgPrettier diff --git a/src/components/Emoji/Pumpkin.tsx b/src/components/Emoji/Pumpkin.tsx new file mode 100644 index 0000000..3a36d76 --- /dev/null +++ b/src/components/Emoji/Pumpkin.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +const SvgPumpkin = (props: React.SVGProps) => ( + + + + + +) + +export default SvgPumpkin diff --git a/src/components/Emoji/RefactorArrow.tsx b/src/components/Emoji/RefactorArrow.tsx new file mode 100644 index 0000000..642855e --- /dev/null +++ b/src/components/Emoji/RefactorArrow.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +const SvgRefactorArrow = (props: React.SVGProps) => ( + + + + + + + + + +) + +export default SvgRefactorArrow diff --git a/src/components/Emoji/Rhino.tsx b/src/components/Emoji/Rhino.tsx new file mode 100644 index 0000000..9338447 --- /dev/null +++ b/src/components/Emoji/Rhino.tsx @@ -0,0 +1,37 @@ +import React from 'react' + +const SvgRhino = (props: React.SVGProps) => ( + + + + + + + + + + +) + +export default SvgRhino diff --git a/src/components/Emoji/Rocket.tsx b/src/components/Emoji/Rocket.tsx new file mode 100644 index 0000000..1a4f7fd --- /dev/null +++ b/src/components/Emoji/Rocket.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +const SvgRocket = (props: React.SVGProps) => ( + + + + + + + + +) + +export default SvgRocket diff --git a/src/components/Emoji/Running.tsx b/src/components/Emoji/Running.tsx new file mode 100644 index 0000000..2715901 --- /dev/null +++ b/src/components/Emoji/Running.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +const SvgRunning = (props: React.SVGProps) => ( + + + + + + + + +) + +export default SvgRunning diff --git a/src/components/Emoji/ScaryCat.tsx b/src/components/Emoji/ScaryCat.tsx new file mode 100644 index 0000000..8cee802 --- /dev/null +++ b/src/components/Emoji/ScaryCat.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +const SvgScaryCat = (props: React.SVGProps) => ( + + + + + + + + + + +) + +export default SvgScaryCat diff --git a/src/components/Emoji/Smartphone.tsx b/src/components/Emoji/Smartphone.tsx new file mode 100644 index 0000000..c43bd9c --- /dev/null +++ b/src/components/Emoji/Smartphone.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const SvgSmartphone = (props: React.SVGProps) => ( + + + + +) + +export default SvgSmartphone diff --git a/src/components/Emoji/Star.tsx b/src/components/Emoji/Star.tsx new file mode 100644 index 0000000..fd9ab99 --- /dev/null +++ b/src/components/Emoji/Star.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +const SvgStar = (props: React.SVGProps) => ( + + + +) + +export default SvgStar diff --git a/src/components/Emoji/StringText.tsx b/src/components/Emoji/StringText.tsx new file mode 100644 index 0000000..211a818 --- /dev/null +++ b/src/components/Emoji/StringText.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +const SvgStringText = (props: React.SVGProps) => ( + + + + + + +) + +export default SvgStringText diff --git a/src/components/Emoji/UglyCode.tsx b/src/components/Emoji/UglyCode.tsx new file mode 100644 index 0000000..3bbea20 --- /dev/null +++ b/src/components/Emoji/UglyCode.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +const SvgUglyCode = (props: React.SVGProps) => ( + + + + + + + +) + +export default SvgUglyCode diff --git a/src/components/Emoji/UglyTutorial.tsx b/src/components/Emoji/UglyTutorial.tsx new file mode 100644 index 0000000..52c3cfd --- /dev/null +++ b/src/components/Emoji/UglyTutorial.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +const SvgUglyTutorial = (props: React.SVGProps) => ( + + + + + + + +) + +export default SvgUglyTutorial diff --git a/src/components/Emoji/index.tsx b/src/components/Emoji/index.tsx index ed4aceb..9bad9ff 100644 --- a/src/components/Emoji/index.tsx +++ b/src/components/Emoji/index.tsx @@ -26,6 +26,31 @@ import VerticalBar from 'src/components/Emoji/VerticalBar' import Work from 'src/components/Emoji/Work' import Home from 'src/components/Emoji/Home' import Pin from 'src/components/Emoji/Pin' +import CleanCode from 'src/components/Emoji/CleanCode' +import UglyCode from 'src/components/Emoji/UglyCode' +import RefactorArrow from 'src/components/Emoji/RefactorArrow' +import UglyTutorial from 'src/components/Emoji/UglyTutorial' +import CleanTutorial from 'src/components/Emoji/CleanTutorial' +import Smartphone from 'src/components/Emoji/Smartphone' +import Cross from 'src/components/Emoji/Cross' +import Rhino from 'src/components/Emoji/Rhino' +import Prettier from 'src/components/Emoji/Prettier' +import ScaryCat from 'src/components/Emoji/ScaryCat' +import BadExample from 'src/components/Emoji/BadExample' +import Running from 'src/components/Emoji/Running' +import Dash from 'src/components/Emoji/Dash' +import Star from 'src/components/Emoji/Star' +import HeartCat from 'src/components/Emoji/HeartCat' +import Pumpkin from 'src/components/Emoji/Pumpkin' +import HeartLetter from 'src/components/Emoji/HeartLetter' +import DoubleArrow from 'src/components/Emoji/DoubleArrow' +import Brain from 'src/components/Emoji/Brain' +import Plane from 'src/components/Emoji/Plane' +import Rocket from 'src/components/Emoji/Rocket' +import StringText from 'src/components/Emoji/StringText' +import NumberText from 'src/components/Emoji/NumberText' +import LetterC from 'src/components/Emoji/LetterC' +import FastForward from 'src/components/Emoji/FastForward' export const emojiToComponent = { bird: Bird, @@ -53,7 +78,32 @@ export const emojiToComponent = { verticalBar: VerticalBar, work: Work, home: Home, - pin: Pin + pin: Pin, + cleanCode: CleanCode, + uglyCode: UglyCode, + refactorArrow: RefactorArrow, + uglyTutorial: UglyTutorial, + cleanTutorial: CleanTutorial, + smartphone: Smartphone, + cross: Cross, + rhino: Rhino, + prettier: Prettier, + scaryCat: ScaryCat, + badExample: BadExample, + running: Running, + dash: Dash, + star: Star, + heartCat: HeartCat, + pumpkin: Pumpkin, + heartLetter: HeartLetter, + brain: Brain, + doubleArrow: DoubleArrow, + rocket: Rocket, + plane: Plane, + stringText: StringText, + numberText: NumberText, + letterC: LetterC, + fastForward: FastForward } export const EmojiWrapper = ({ diff --git a/src/components/EmojiSeparator.tsx b/src/components/EmojiSeparator.tsx index 80a8272..99cbdeb 100644 --- a/src/components/EmojiSeparator.tsx +++ b/src/components/EmojiSeparator.tsx @@ -13,6 +13,8 @@ interface EmojiSeparatorProps { cssOverrides?: SerializedStyles description?: React.ReactNode customChildren?: React.ReactNode + leftAlign?: boolean + href?: string } const fontSize = ( @@ -20,7 +22,7 @@ const fontSize = ( ): ReadonlyArray => ({ sm: [1, 1.2] as const, - md: [2, 2.5] as const, + md: [2.2, 2.5] as const, lg: [3, 4] as const }[size]) @@ -49,30 +51,43 @@ const EmojiSeparator = ({ size = 'md', cssOverrides, description, - customChildren + customChildren, + leftAlign, + href }: EmojiSeparatorProps) => { const { spaces, ns, fontSizes } = useTheme() + const EmojiWrapperComponent = href ? 'a' : 'span' + const emojiWrapperComponentAttrs = href ? { href } : {} return (
<> - {customChildren || (emojis || []).map((emoji, index) => ( @@ -80,7 +95,7 @@ const EmojiSeparator = ({ ))} - + {description && ( { - const { fontSizes, ns, spaces, colors } = useTheme() + const { fontSizes, spaces, colors } = useTheme() return ( Source available on{' '} diff --git a/src/components/GitHubButton.tsx b/src/components/GitHubButton.tsx index b4ec06f..9546d6b 100644 --- a/src/components/GitHubButton.tsx +++ b/src/components/GitHubButton.tsx @@ -3,10 +3,14 @@ import { css, jsx } from '@emotion/core' import { A } from 'src/components/ContentTags' import useTheme from 'src/hooks/useTheme' -export const SourceAvailableText = () => ( +export const SourceAvailableText = ({ page }: { page?: string }) => ( <> - The source code for this site is on{' '} - + The source code for this {page ? 'page' : 'site'} is on{' '} + GitHub : diff --git a/src/components/Page.tsx b/src/components/Page.tsx index 4e3802f..22693fa 100644 --- a/src/components/Page.tsx +++ b/src/components/Page.tsx @@ -56,7 +56,7 @@ const Page = ({
{children} diff --git a/src/components/PostPage.tsx b/src/components/PostPage.tsx index 1b104a6..6793dc7 100644 --- a/src/components/PostPage.tsx +++ b/src/components/PostPage.tsx @@ -19,14 +19,17 @@ export interface EpisodeCardType { color?: CardProps['color'] heading?: React.ReactNode subtitle?: React.ReactNode + anchor?: CardProps['anchor'] } const PostPage = ({ articleKey, - cards + cards, + hideIntroQuote }: { articleKey: keyof typeof articlesData cards: readonly EpisodeCardType[] + hideIntroQuote?: boolean }) => { const url = `${baseUrl}/${articleKey}` const description = articlesData[articleKey]['description'] @@ -80,7 +83,7 @@ const PostPage = ({

{title}

- - - - ) - } - ]} - > + {!hideIntroQuote && ( + + + + ) + } + ]} + > + )}
{cards.map( - ({ title, content, footer, color, heading, subtitle }, index) => ( + ( + { title, content, footer, color, heading, subtitle, anchor }, + index + ) => ( {content} diff --git a/src/components/ReadMore.tsx b/src/components/ReadMore.tsx index 38e3a31..9f7fc49 100644 --- a/src/components/ReadMore.tsx +++ b/src/components/ReadMore.tsx @@ -29,7 +29,7 @@ const ReadMore = ({ text-decoration: underline; cursor: pointer; &:hover { - background: ${colors('white85')}; + background: ${colors('white70')}; } `} tabIndex={0} @@ -41,7 +41,17 @@ const ReadMore = ({ ) : ( - <>{showReadMoreTextWhenVisible && <> {readMoreText}} + <> + {showReadMoreTextWhenVisible && ( + + {readMoreText} + + )} + ) )} {visible && rest} diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx index 89ca6f7..e868131 100644 --- a/src/components/TodoItem.tsx +++ b/src/components/TodoItem.tsx @@ -64,10 +64,12 @@ const TodoItem = ({ > dispatch({ type: 'toggle', index })} diff --git a/src/components/TodoWithData.tsx b/src/components/TodoWithData.tsx index 4908bb5..8c095d5 100644 --- a/src/components/TodoWithData.tsx +++ b/src/components/TodoWithData.tsx @@ -3,6 +3,7 @@ import { css, jsx } from '@emotion/core' import useTheme from 'src/hooks/useTheme' import { useReducer } from 'react' import CodeResult from 'src/components/CodeResult' +import CodeResultWrapper from 'src/components/CodeResultWrapper' import Caption from 'src/components/Caption' import TodoList from 'src/components/TodoList' import PromptArrowText from 'src/components/PromptArrowText' @@ -94,100 +95,84 @@ const TodoWithData = ({ narrowText?: boolean customSnippet?: string }) => { - const { spaces, ns, maxWidths, radii, colors, letterSpacings } = useTheme() + const { spaces, radii, colors, letterSpacings } = useTheme() const [state, dispatch] = useReducer(reducer, { todos: defaultData, lastChangedIndices: [] }) return ( -
-
+ {caption} + + } + /> + {showData && ( + - {caption} - - } - /> - {showData && ( - - shouldAlwaysHighlight && - shouldAlwaysHighlight(lineIndex, tokenIndex) && + language="javascript" + cssOverrides={[ + css` + margin-top: ${0}; + margin-bottom: ${spaces(1.75)}; + border-bottom-left-radius: ${radii(0.5)}; + border-bottom-right-radius: ${radii(0.5)}; + `, + narrowText && css` - background: ${colors('yellowHighlight')}; - border-bottom: 2px solid ${colors('darkOrange')}; - font-weight: bold; + letter-spacing: ${letterSpacings('smallCode')}; ` - } - lineCssOverridesAnimation={(lineIndex, tokenIndex) => - shouldHighlight && - state.lastChangedIndices - .map( - lastChangedIndex => - lastChangedIndex + (highlightLineIndexOffset || 0) - ) - .includes(lineIndex) && - shouldHighlight(tokenIndex) - ? { - background: `${colors('yellowHighlight')}`, - borderBottom: `2px solid ${colors('darkOrange')}`, - reset: true, - fontWeight: 'bold', - from: { - background: `${colors('yellowHighlightTransparent')}`, - borderBottom: `1px solid ${colors( - 'darkOrangeTransparent' - )}` - } + ]} + lineCssOverrides={(lineIndex, tokenIndex) => + shouldAlwaysHighlight && + shouldAlwaysHighlight(lineIndex, tokenIndex) && + css` + background: ${colors('yellowHighlight')}; + border-bottom: 2px solid ${colors('darkOrange')}; + font-weight: bold; + ` + } + lineCssOverridesAnimation={(lineIndex, tokenIndex) => + shouldHighlight && + state.lastChangedIndices + .map( + lastChangedIndex => + lastChangedIndex + (highlightLineIndexOffset || 0) + ) + .includes(lineIndex) && + shouldHighlight(tokenIndex) + ? { + background: `${colors('yellowHighlight')}`, + borderBottom: `2px solid ${colors('darkOrange')}`, + reset: true, + fontWeight: 'bold', + from: { + background: `${colors('yellowHighlightTransparent')}`, + borderBottom: `1px solid ${colors( + 'darkOrangeTransparent' + )}` } - : undefined - } - > - )} - {promptArrowText && ( - {promptArrowText} - )} -
-
+ } + : undefined + } + > + )} + {promptArrowText && state.lastChangedIndices.length === 0 && ( + {promptArrowText} + )} +
) } diff --git a/src/lib/articles.ts b/src/lib/articles.ts index fc11621..53a2d4c 100644 --- a/src/lib/articles.ts +++ b/src/lib/articles.ts @@ -19,5 +19,11 @@ export const articlesData = { date: DateTime.fromISO('2019-11-22T12:00:00Z'), description: 'TypeScript Generics Too Hard?', ogImage: 'generics' + }, + refactor: { + title: 'Your Coding Tutorial Might Need Some Refactoring', + date: DateTime.fromISO('2020-01-01T09:00:00Z'), + description: 'Seven Opinionated Tips', + ogImage: 'refactor' } } diff --git a/src/lib/snippets.ts b/src/lib/snippets.ts index 28235cf..3a91bbe 100644 --- a/src/lib/snippets.ts +++ b/src/lib/snippets.ts @@ -469,6 +469,129 @@ const { getState, setState } = makeState() setState('foo') console.log(getState())` +export const bxzx = `// It could have been useful if you could pass +// both number AND string, and have it repeat +// the string the specified number of times +padLeft('Hello world', 4, '#') +// → "####Hello world"` + +export const crgn = `// If the second parameter is string, then +// that string is appended to the left side +padLeft('Hello world', 'Jim: ') +// → "Jim: Hello world" + +// Ask yourself: Would you EVER do this?` + +export const hfdq = `function paddingLeftCss(val: number | string) { + if (typeof val === 'number') { + return \`padding-left: \${val * 0.25}rem;\` + } else { + return \`padding-left: \${val};\` + } +} + +// padding-left: 0.25rem; +paddingLeftCss(1) + +// padding-left: 0.5rem; +paddingLeftCss(2) + +// padding-left: 10%; +paddingLeftCss('10%')` + +export const mvsz = `function makePair() {}` + +export const zgvn = `type Todo = Readonly<{ id: number; text: string; done: boolean; place: Place }>` + +export const lcfe = `// If the second parameter is number, then that +// number of spaces is added to the left side +padLeft('Hello world', 4) +// → " Hello world" + +// If the second parameter is string, then +// that string is appended to the left side +padLeft('Hello world', 'Jim: ') +// → "Jim: Hello world"` + +export const riis = `/** + * Takes a string and adds "padding" to the left. + * + * If 'padding' is a number, then that number of + * spaces is added to the left side. + * + * If 'padding' is a string, then 'padding' is + * appended to the left side. + */ +function padLeft( + value: string, + padding: number | string +) { + if (typeof padding === 'number') { + return Array(padding + 1).join(' ') + value + } else { + return padding + value + } +}` + +export const lplh = `console.log(1 + 2)` + +export const onux = `function extend( + first: First, + second: Second +): First & Second { + const result: Partial = {} + for (const prop in first) { + if (first.hasOwnProperty(prop)) { + ;(result as First)[prop] = first[prop] + } + } + for (const prop in second) { + if (second.hasOwnProperty(prop)) { + ;(result as Second)[prop] = second[prop] + } + } + return result as First & Second +} + +class Person { + constructor(public name: string) {} +} + +interface Loggable { + log(name: string): void +} + +class ConsoleLogger implements Loggable { + log(name) { + console.log(\`Hello, I'm \${name}.\`) + } +} + +const jim = extend( + new Person('Jim'), + ConsoleLogger.prototype +) +jim.log(jim.name)` + +export const vnfq = `type Person = { name: string } +type Loggable = { log: (name: string) => void } + +// Use & to make jim BOTH Person AND Loggable +const jim: Person & Loggable = { + name: 'Jim', + log: name => { + console.log(\`Hello, I'm \${name}.\`) + } +} + +// "Hello, I’m Jim." +jim.log(jim.name)` + +export const xwbz = `function makePair< + F extends number | string, + S extends boolean | F +>() {}` + export const ampt = `function toggleTodo(todo: Todo): Todo { return { // This line was missing diff --git a/src/lib/theme/colors.ts b/src/lib/theme/colors.ts index 0bc9daa..656ee3c 100644 --- a/src/lib/theme/colors.ts +++ b/src/lib/theme/colors.ts @@ -12,7 +12,7 @@ export const allColors = { darkOrangeTransparent: 'rgba(236, 150, 2, 0)', pink: '#FFD9CC', white: '#FFFFFF', - white85: 'rgba(255, 255, 255, 0.85)', + white70: 'rgba(255, 255, 255, 0.7)', paleGreen: '#C8DCC7', darkGreen: '#009795', red: '#DB4003', diff --git a/src/lib/theme/fontSizes.ts b/src/lib/theme/fontSizes.ts index cb9c82a..6992e25 100644 --- a/src/lib/theme/fontSizes.ts +++ b/src/lib/theme/fontSizes.ts @@ -7,6 +7,7 @@ export const allFontSizes = { 1.4: 1.4, 1.6: 1.6, 2: 2, + 2.2: 2.2, // EmojiSeparator only 2.5: 2.5, 3: 3, 4: 4, diff --git a/src/lib/theme/letterSpacings.ts b/src/lib/theme/letterSpacings.ts index ec10a97..3023c0a 100644 --- a/src/lib/theme/letterSpacings.ts +++ b/src/lib/theme/letterSpacings.ts @@ -1,7 +1,8 @@ export const allLetterSpacings = { title: '-0.02em', + titleSmall: '-0.01em', wide: '0.15em', - smallCode: '-0.035em' + smallCode: '-0.045em' } const letterSpacings = (x: keyof typeof allLetterSpacings) => diff --git a/src/lib/theme/lineHeights.ts b/src/lib/theme/lineHeights.ts index 7e72e63..b5b45d8 100644 --- a/src/lib/theme/lineHeights.ts +++ b/src/lib/theme/lineHeights.ts @@ -9,6 +9,7 @@ export const allLineHeights = { 1.4: 1.4, 1.6: 1.3, 2: 1.25, + 2.2: 1.25, 2.5: 1.2, 3: 1.1, 4: 1, diff --git a/src/lib/theme/maxWidths.ts b/src/lib/theme/maxWidths.ts index 9f57886..899082a 100644 --- a/src/lib/theme/maxWidths.ts +++ b/src/lib/theme/maxWidths.ts @@ -1,5 +1,6 @@ export const allMaxWidths = { sm: (1140 / 12) * 5, + smmd: (1140 / 12) * 7, md: (1140 / 12) * 8, lg: 1140 } diff --git a/src/pages/generics.tsx b/src/pages/generics.tsx index 94677f0..8f15c1c 100644 --- a/src/pages/generics.tsx +++ b/src/pages/generics.tsx @@ -57,9 +57,9 @@ const Page = () => ( content: ( <>

- Note: If you already understand generics, you - won’t find anything new in this tutorial. However,{' '} - + If you already understand generics, you won’t find anything new + in this tutorial. However,{' '} + you might know someone (maybe one of your Twitter followers) who’s struggling with generics @@ -129,10 +129,9 @@ const Page = () => ( content: ( <>

- Note: If you’ve used React, you might have - realized that makeState() is - similar to the useState(){' '} - hook. + If you’ve used React, you might have realized that{' '} + makeState() is similar to the{' '} + useState() hook.

( content: ( <>

- Note: You need to set{' '} + You need to set{' '} "strictPropertyInitialization": false {' '} diff --git a/src/pages/refactor.tsx b/src/pages/refactor.tsx new file mode 100644 index 0000000..69781f0 --- /dev/null +++ b/src/pages/refactor.tsx @@ -0,0 +1,1371 @@ +/** @jsx jsx */ +import { css, jsx } from '@emotion/core' +import PostPage from 'src/components/PostPage' +import EmojiSeparator from 'src/components/EmojiSeparator' +import { + P, + Highlight, + Ol, + OlLi, + Ul, + UlLi, + ForegroundHighlight, + A, + Code, + Hr, + Blockquote +} from 'src/components/ContentTags' +import * as snippets from 'src/lib/snippets' +import TwitterLink from 'src/components/TwitterLink' +import AboutMe from 'src/components/AboutMe' +import CodeBlock from 'src/components/CodeBlock' +import Caption from 'src/components/Caption' +import CodeResult from 'src/components/CodeResult' +import CodeResultWrapper from 'src/components/CodeResultWrapper' +import ResultHighlight from 'src/components/ResultHighlight' +import Emoji from 'src/components/Emoji' +import InternalLink from 'src/components/InternalLink' +import { articlesData } from 'src/lib/articles' +import { baseUrl } from 'src/lib/meta' +import { SourceAvailableText } from 'src/components/GitHubButton' +import TodoWithData from 'src/components/TodoWithData' + +const techniques = [ + { + title: ( + <> + Make code samples mobile-ready + + ), + emojis: ['check', 'smartphone', 'check'] + }, + { + title: ( + <> + Prefer minimal code samples + + ), + emojis: ['badExample', 'singleArrow', 'star'] + }, + { + title: ( + <> + Prefer practical code samples + + ), + emojis: ['rocket', 'singleArrow', 'plane'] + }, + { + title: ( + <> + Fail fast + + ), + emojis: ['cross', 'running', 'dash'] + }, + { + title: ( + <> + Use themes, analogies, and{' '} + quizzes + + ), + emojis: ['pumpkin', 'doubleArrow', 'question'] + }, + { + title: ( + <> + Add a thoughtful touch + + ), + emojis: ['sparkles', 'heartLetter', 'sparkles'] + } +] as const + +const RefactorSubtitle = ({ index }: { index: number }) => ( + + Refactoring Tip {index + 1} of 6: + +) + +const refactoringCardProps = (index: number) => ({ + subtitle: , + title: techniques[index].title, + anchor: `tip${index + 1}` +}) + +const Page = () => ( + Both code and coding tutorials can be refactored, + content: ( + <> +

+ At some point in your coding career, you’ve probably come across a + piece of spaghetti code that’s not so reader-friendly. It needed + some refactoring. +

+ Spaghetti code needs refactoring} + /> +

+ Similarly,{' '} + + you’ve probably also come across a coding{' '} + tutorial that’s not so reader-friendly. + {' '} + Maybe you wanted to learn a new language, library, or framework, + but the tutorial you found made you more frustrated than before. +

+ + You’ve probably come across a coding tutorial that’s not so + reader-friendly + + } + /> +

+ But they can be improved. As someone who’s written many coding + tutorials, I realized that{' '} + + most coding tutorials can be refactored to be + more reader-friendly + + —just like refactoring code. +

+ Coding tutorials can also be refactored} + /> +

+ However, while many programmers have written guidelines on how to + refactor code,{' '} + + guidelines on how to refactor coding tutorials are rare. + {' '} +

+

+ So, in this article, I’ll share six opinionated + tips on refactoring coding tutorials. I’ve used these techniques + on my own tutorials to make them more reader-friendly. Here’s the + list: +

+
    + {techniques.map((technique, index) => ( + + + {technique.title} + +
    + +
    + ))} +
+

Let’s take a look!

+ + ), + footer: { + content: ( + <> +

+ Even if you’ve never written a coding tutorial,{' '} + + you might know someone who has (maybe one of your Twitter + followers) + + . I’d appreciate it if you could share this article with them. + You can{' '} + + click here to tweet this article. + +

+

+ +

+ + ) + } + }, + { + ...refactoringCardProps(0), + content: ( + <> + +

+ Take a look at the code below. It’s in + TypeScript, but don’t worry if you don’t know TypeScript. I used + it for my tutorial called “ + + {articlesData['todo']['title']} + + ”: +

+ +

+ Did you notice that the above code is{' '} + formatted to fit on a small screen?{' '} + + Because each line length is short (max 31 chars), + {' '} + you can read it without side-scrolling on most phones. +

+

+ If the above code was formatted in a single line like below + instead, you’d have to side-scroll or wrap text on a small screen, + which hurts readability. +

+ + Max line length: {' '} + 79 characters +
+ + Line is too long; must scroll or wrap on a phone + + + } + /> +

+ Here’s another example I used for my tutorial called “ + + {articlesData['generics']['title']} + + ”. This is good formatting ( + fits on a small screen): +

+ + Max line length: {' '} + 28 characters + + } + /> +

+ And this is the same code in BAD formatting ( + doesn’t fit on a small screen): +

+ + Max line length: {' '} + 72 characters +
+ + Line is too long; must scroll or wrap on a phone + + + } + /> +

+ So, here’s my first refactoring tip:{' '} + + + Make code samples in your tutorials mobile-ready. + + +

+

+ You can ensure this by keeping line length short.{' '} + I try to keep it under about 50 characters{' '} + (at 14px font size). I use{' '} + + Prettier + {' '} + with custom printWidth to automate this (my{' '} + .prettierrc is{' '} + + here + + ). +

+ I use Prettier to keep line length short} + /> +

Here are some other techniques:

+
    + + Prefer shorter variable names (but don’t + sacrifice code readability). + + + If you can customize the CSS,{' '} + use narrow coding fonts. I use{' '} + + Iosevka + + —it’s slim and looks great. You can also tighten{' '} + letter-spacing to fit more characters. + + + If you want code samples to have longer line length on a larger + screen,{' '} + you can use Prettier in the browser to + dynamically adjust line length based on window width. + +
+
+

+ Why is this necessary?{' '} + + Because many people actually read coding tutorials on + their phones. + {' '} +

+ Many people read coding tutorials on their phones + } + /> +

+ You might be tempted to assume that your readers will read (and + follow along) your coding tutorial on a computer.{' '} + But that’s a bad assumption. +

+

+ In the past, I’ve used Google Analytics to track desktop vs mobile + usage on my coding tutorials. Even though my tutorials are meant + to be done on a computer, surprisingly many people accessed them + from a mobile device. +

+

+ This is because{' '} + + many people discover coding tutorials while + using a phone to browse Twitter, mailing lists, and online + forums. + +

+ + Many people discover coding tutorials while + using a phone to browse Twitter, etc. + + } + /> +

+ That’s why mobile reading experience is important.{' '} + + If you can easily read all the code samples on a phone, you + might be able to finish the tutorial without pulling out your + computer. + {' '} + Sometimes you need to follow along on your computer to fully + understand the content, but that’s not always the case. +

+

+ The bottom line:{' '} + + Assume that people will discover your coding tutorial on their + phone and try to deliver the best possible first impression. + +

+ + ), + footer: { + content: ( + <> +

+ Video tutorials: What I’ve said so far applies + to text-based tutorials.{' '} + For video tutorials (screencasts), it’d + be ideal if the fonts are large enough to be legible on a phone + (in landscape mode). I like watching coding tutorials on + YouTube, but sometimes the fonts are too small when viewed on my + phone. +

+ + ) + } + }, + { + ...refactoringCardProps(1), + content: ( + <> + +

+ As of writing, the + following code appears on the official{' '} + + TypeScript handbook + + . And the handbook{' '} + + uses this code to explain how to use{' '} + a particular TypeScript keyword/operator. + +

+

+ Question:{' '} + + Can you tell which keyword/operator is being explained through + this code? + {' '} + Hint: It is one of the keywords/operators used in the code. (You + don’t need to know TypeScript—just guess!) +

+ +

+ Answer: The official{' '} + + TypeScript handbook + {' '} + uses the above code to explain{' '} + + how to use the{' '} + + “&” operator + {' '} + in TypeScript. + +

+ + (Brief explanation: In TypeScript, the “&” + operator creates an intersection of two types. You can learn + more on my tutorial + .) + + } + /> +

+ I know what you’re thinking: It’s hard to tell! If you look at the + code again, only{' '} + + the highlighted part + {' '} + below is related to the{' '} + + “&” operator + + . There are too many other keywords that are just noise (e.g.{' '} + Partial<>, hasOwnProperty,{' '} + as, constructor, public,{' '} + interface, void, implements + , prototype, etc). +

+ + (lineIndex === 3 && tokenIndex >= 4 && tokenIndex <= 8) || + (lineIndex === 4 && tokenIndex >= 7 && tokenIndex <= 11) || + (lineIndex === 15 && tokenIndex >= 5) + } + caption={ + <> + Only{' '} + + the highlighted part + {' '} + is related to{' '} + + “&” + + , the topic being explained through this code. Every other + keyword is just noise! + + } + /> +

+ So, in my opinion,{' '} + + the above code sample is NOT a great way + {' '} + to explain how to use the{' '} + + “&” operator + {' '} + in TypeScript. To make matters worse,{' '} + as of writing, this + is the ONLY code sample used to explain the “&” + operator on the official handbook! +

+

+ If I were to explain how the “&” operator works + in TypeScript,{' '} + + I’d refactor the earlier code as follows + + —it basically does the same thing in a simpler way. You don’t need + to understand TypeScript to know that this is more minimal and + focused on explaining how to use the “&” + operator. +

+ + lineIndex === 4 && tokenIndex >= 5 && tokenIndex <= 9 + } + caption={ + <> + If I were to explain how the “&” operator + works, I’d rewrite the above code as follows—much simpler! + + } + /> +

+ + What I want to say is: Prefer minimal code samples. + {' '} + + If you’re trying to teach a new concept (let’s call this “ + X”), just focus on X in the + code sample and don’t add too much extra stuff. + {' '} + Add extra stuff only when it really helps the reader’s + understanding. +

+

+ You might be thinking: “Well, that’s obvious.” But trust + me, so many tutorials (including the official TypeScript + handbook!) fail at keeping things simple and end up with too much + noise in code samples, making them hard to follow. +

+
+

+ Minimal reproducible example: When you ask a + question on StackOverflow or file an issue on GitHub, you’re often + asked to create a{' '} + minimal reproducible example. WYour code + needs to be as small as possible, such that it is just sufficient + to demonstrate the problem, but without any additional complexity + ( + + Wikipedia’s definition + + ). +

+

+ You should use the same principle when writing code samples for + your tutorials. Ask yourself:{' '} + + Can I make this code sample more minimal while maintaining the + learning experience? + +

+ + ), + footer: { + content: ( + <> +

+ Tutorials vs Documentations: There is a + difference in what should code samples be like for{' '} + tutorials vs documentations.{' '} + Tutorials are for{' '} + learning, so it’s often better to keep + code samples minimal to reduce confusion (except for advanced + topics). Documentations are{' '} + references, so it’s often better to have + comprehensive code samples. (The official TypeScript handbook I + mentioned earlier is written more like a tutorial than + documentation.) +

+ + ) + } + }, + { + ...refactoringCardProps(2), + content: ( + <> + +

+ As of writing, the + following code appears on the official{' '} + + TypeScript handbook + {' '} + (right after the example we showed earlier). And the handbook{' '} + + uses this code to explain how to use the{' '} + union operator in TypeScript, + {' '} + which is the{' '} + + “|” + {' '} + symbol highlighted{' '} + below. +

+ + lineIndex === 11 && tokenIndex >= 2 + } + caption={ + <>(Slightly modified from the original for readability) + } + /> +

+ In TypeScript,{' '} + + you can write{' '} + + number | string + {' '} + to specify that a parameter can either be number OR{' '} + string + + . So in this case, the second padding parameter can + be either number or string. +

+
    + + If padding is number, then that number + of spaces is added to the left side of value. + + + If padding is string, then{' '} + padding is added to the left side of{' '} + value. + +
+ + (lineIndex === 2 && tokenIndex === 6) || + (lineIndex === 7 && tokenIndex === 6) + } + /> +

+ Now, a question for you:{' '} + + Is padLeft() a good example to explain how{' '} + the{' '} + + “|” + {' '} + operator works in TypeScript? + +

+ + Is the above code a good example to explain how the{' '} + + “|” + {' '} + operator works? + + } + /> +

+ I’d say NO—it’s NOT a good example. You don’t + need to know TypeScript to see why. +

+

+ Take a look below and ask yourself:{' '} + + Would you EVER use padLeft() for the case where the + second parameter is string? + +

+ + (lineIndex === 2 && tokenIndex === 6) || lineIndex === 5 + } + /> +

+ You probably would not. It just does simple + string concatenation in reverse.{' '} + + You probably would just use other standard ways to concatenate + strings, such as 'Jim: ' + 'Hello World'. + {' '} + There’s no good reason why padLeft() should support + the second string parameter. +

+

+ The bottom line:{' '} + padLeft(value, padding) is useful if{' '} + padding is number but is{' '} + + pretty useless if padding is string. + {' '} + So setting padding’s type as{' '} + number | string is not useful—it could just be{' '} + number. That’s why this is NOT a good example. +

+
+

+ So, here’s my third refactoring tip:{' '} + Prefer practical code samples.{' '} + Avoid showing code no one would write. If + you’re trying to teach a new concept (let’s call this “ + X”),{' '} + + come up with a practical code sample where{' '} + X is actually useful in solving the + problem. + +

+

+ By showing a practical example,{' '} + + readers will understand why X is worth + learning. + {' '} + If you show them a useless example, they’d think:{' '} + + “What’s the point of learning X?” + +

+

+ Example: If I were to explain how to use{' '} + number | string in TypeScript, instead of the earlier{' '} + padLeft() example, I would use the following{' '} + + paddingLeftCss() + {' '} + function. The name is similar, but this one is used to generate a + CSS padding-left string: +

+ + lineNumber === 0 && tokenNumber >= 6 && tokenNumber <= 8 + } + /> +

+ paddingLeftCss() can take a number or{' '} + string: +

+
    + + If it’s number, it returns{' '} + padding-left CSS that’s a multiple of a predefined + spacing unit. In this case, 1 = 0.25rem,{' '} + 2 = 0.5rem, etc. This would be helpful for visual + consistency when designing UI. + + + If it’s string, it just uses that string as{' '} + padding-left. + +
+

+ This is similar to how UI libraries like{' '} + + styled-system + {' '} + work. In other words, it’s a practical example.{' '} + + It actually makes sense to have the parameter be either{' '} + number or string, + {' '} + unlike the previous example. +

+
+

+ To summarize, always ask yourself:{' '} + + Is my code sample practical? Would anyone ever write code like + this? + +

+ + ), + footer: { + content: ( + <> +

+ padLeft() could have been + useful if you could pass both a{' '} + number AND a{' '} + string, and have it repeat the + string the specified number of times (see below). But that’s not + what was in the handbook. +

+ + lineIndex === 3 && tokenIndex >= 6 && tokenIndex <= 9 + } + /> + + ) + } + }, + { + ...refactoringCardProps(3), + content: ( + <> + +

+ One of the best ways to capture your reader’s attention is to{' '} + FAIL. When things don’t go according to plan, + people will pay more attention than when everything goes smoothly. + Use this to your advantage. +

+

+ Furthermore, it’s more effective if you fail fast + .{' '} + + Try to show a failing scenario{' '} + as early as possible in your article. + {' '} + By doing so, you’ll be able to capture your reader’s attention + right off the bat. +

+

+ For example, on{' '} + my TypeScript tutorial, + I start with an example where, if you run the code, the actual + result is different from the expected result (failure). Then I + talk about how to prevent failures like this using TypeScript’s + features. +

+ + + The first example on my tutorial: The actual + result is different from the expected result + + + Expected: +
+ {`{ id: 1, text: '…', done: false }`} + Actual: +
+ {`{ text: '…', done: false }`} + + } + /> +
+

+ Here’s a simple technique you can use. If you want to teach a new + concept (let’s call this “X”),{' '} + + start with a concrete scenario where things fail or + aren’t ideal when you don’t use X. + {' '} + Then,{' '} + + use X to solve the problem. + {' '} + Your readers will pay more attention and also understand why{' '} + X is worth learning. +

+

+ I used this technique on{' '} + + my TypeScript generics tutorial + + . Early in the article, I attempt to solve a problem that can only + be solved by generics…without using generics. Of course, I fail. + Then, I use generics to successfully solve the problem. +

+
+

+ In a way, this is similar to{' '} + test driven development (TDD). In TDD, you write + a failing test first, and after you watch it fail, you try to make + it pass. Similarly, in a coding tutorial,{' '} + + it’s more effective if you show a failing example first and have + the readers watch it fail. + +

+ + In TDD, you write a failing test first. In a coding tutorial, + make readers go through a failing example first + + } + /> +

+ It’s tempting to be lazy and skip writing a failing test in TDD. + Similarly, when writing a coding tutorial, it’s tempting to skip + showing a failing example and just start with a successful + example. But resist this temptation— + failure is your friend in expository + writing. +

+

+ The bottom line:{' '} + + Double-check to see where the first failing example appears in + your tutorial. If it’s missing, add one near the beginning. + +

+ + ), + footer: { + content: ( + <> +

+ Fail unexpectedly: It’s also more effective if + the failure is surprising. Trick your + reader into thinking that a code sample would work + perfectly…then make it fail. Make your readers think,{' '} + “WTF? How come it doesn’t work?”—and they’ll be more + curious. Unexpected failure = more memorable learning + experience. +

+ + ) + } + }, + { + ...refactoringCardProps(4), + content: ( + <> + +

+ Let’s talk about the 3 simple techniques you can + use to engage the reader’s brain. +

+
+

+ First,{' '} + + use themes: + {' '} + + If your tutorial doesn’t have an underlying{' '} + theme that ties together your examples, try to + add one + + . Having an extremely simple theme is better than having no theme. +

+

+ For example, on{' '} + one of my tutorials, I + teach 8 beginner TypeScript topics{' '} + + (types, read-only properties, mapped types, array types, literal + types, intersection types, union types, and optional properties) + + . Instead of covering each topic separately,{' '} + + I use a simple theme of building a todo app to + explain all those 8 topics. + {' '} + The idea is to add features to a todo app one by one using + TypeScript. +

+

+ First, you implement the “toggle todo”{' '} + feature of a todo app. This lets you check and uncheck the + checkboxes—try it below! +

+ ↑ Check and uncheck the checkboxes!} + defaultData={[ + { id: 1, text: 'First todo', done: false }, + { id: 2, text: 'Second todo', done: false } + ]} + /> +

+ To implement this feature, you need to write the{' '} + toggleTodo() function, and in that process, I explain + TypeScript types, read-only properties, and mapped types. +

+

+ After that, you implement the{' '} + “mark all as completed” feature, which + checks all the checkboxes at once. +

+ + ↑ Try pressing “Mark all as completed” + + } + defaultData={[ + { id: 1, text: 'First todo', done: false }, + { id: 2, text: 'Second todo', done: false } + ]} + showMarkAllAsCompleted + /> +

+ To implement this feature, you need to write the{' '} + completeAll() function, and in that process, I + explain{' '} + array types, literal types, and intersection types in + TypeScript. +

+

+ You get the idea.{' '} + + When I want to teach many concepts at once, I prefer to use{' '} + a specific theme to explain them all—in this + case, building a todo app. + {' '} + By doing so, readers won’t have to do as much context switching in + their head. +

+
+

+ Second,{' '} + + use analogies to explain new concepts. + {' '} + Tie a new concept with the concept your reader already knows. +

+

+ By the way, did you notice that I used several analogies in this + article? +

+
    + + I compared refactoring code with{' '} + refactoring a coding tutorial. + + + I compared minimum reproducible examples{' '} + with minimal code samples. + + + I compared TDD with{' '} + using failing examples. + +
+

+ Note:{' '} + + If your analogy isn’t perfect, use it anyway but say “it’s not a + perfect comparison.” + {' '} + It would still help your reader memorize the concept. I did this + on my{' '} + TypeScript tutorial when + I compare TypeScript’s type-checking feature with unit tests. + Here’s what I wrote: +

+
+

+ So in a sense, TypeScript’s types act as lightweight unit tests + that run every time you save (compile) the code. ( + + Of course, this analogy is a simplification. You should still + write tests in TypeScript! + + ) +

+
+
+

+ Finally,{' '} + + use quizzes + {' '} + to make your readers pause, think, and be engaged. +

+

+ In your tutorial,{' '} + + count how many times you ask simple questions, such as{' '} + “what would happen if you do X?” or{' '} + “what’s wrong with the following code?” + + . Even non-interactive, simple yes-no quizzes are better than + having no quiz! +

+ + ) + }, + { + ...refactoringCardProps(5), + content: ( + <> + +

+ This is the final section! Here are some mini-tips to add a + thoughtful touch to your tutorials. +

+
+

+ + Visually emphasize important parts in your code samples. + {' '} + Highlight or{' '} + bold important words/lines so your readers know + what to pay attention to. You can also{' '} + add a comment next to the emphasized + words/lines for more clarity. +

+ + Highlight or{' '} + bold important words/lines + + } + shouldHighlight={(lineIndex, tokenIndex) => + lineIndex === 3 && tokenIndex >= 0 && tokenIndex <= 4 + } + /> +
+

+ Use graphics that show faces. There’s a reason a + lot of ads have someone’s face in it. Our brains are wired to pay + attention to faces. Take advantage of this to grab your reader’s + attention. +

+

+ One of the easiest ways to do this is to use{' '} + emojis. Just insert a happy{' '} + emoji when things are going well and a + scared emoji when things fail. +

+ + Use emojis to express emotions your readers should be feeling + + } + /> +
+

+ Use mostly-text graphics. Graphics made of basic + shapes and texts are simple yet effective. For example, I used + this graphic earlier: +

+ +

+ It takes a only few minutes to create, but it helps your readers + visually remember the idea. +

+
+

+ Avoid difficult English words/phrases.{' '} + + I spent a year traveling the world in 2018 + + , and one thing I learned is that{' '} + + so many people in the world can speak some English, but many + don’t speak English well + + . Globally,{' '} + + there are 3x as many non-native English speakers as native + English speakers + + . +

+

+ So when in doubt, use simpler English words/phrases. It will + increase the size of your audience. +

+ Use simple English} + /> +

+ Also: If you’re living in the US,{' '} + + avoid cultural references and humor that only people familiar + with American culture would understand + + . Always ask yourself:{' '} + + “Would someone living far, far away from where I live understand + what I’m writing?” + +

+

+ (Note: I’m an English-Japanese translator, and I + often find it really hard to translate some cultural references + into Japanese. If you write a good technical article, people will + volunteer to translate it. Try to make it easy for the translators + by minimizing the number of cultural references you use!) +

+
+

+ + When you skip a step or assume prerequisite knowledge, say so. + {' '} + Sometimes you have to skip some steps to keep your tutorial + concise. But skipping steps can also lead to confusion. +

+

+ So if possible,{' '} + + let your reader know what steps you’re skipping + + —by doing so, they won’t be as confused, and they can Google how + to do the missing steps if necessary. +

+ + Be explicit:{' '} + + “Skipping + !” + + + } + /> +

+ Also,{' '} + + if your tutorial requires some prerequisite knowledge, + explicitly list them + + . For example,{' '} + + my TypeScript generics tutorial + {' '} + assumes prior knowledge of closure and ES2015 syntax. I mention + this and added MDN documentation links in case the reader is + unfamiliar with them. +

+

+ Finally,{' '} + + check if you’re using a convention that newcomers may not know + about. + {' '} + On{' '} + + my TypeScript generics tutorial + + , I explain why generic type parameters are often written using a + single uppercase letter (like T, E,{' '} + K, V, etc). This convention is actually + borrowed from Java—it’s{' '} + + mentioned in the official Java documentation. + {' '} + Beginner programmers who have never touched Java may not know + about this, so I explained it on my tutorial. +

+
+

+ Finally, when things get hard, be encouraging.{' '} + Use phrases like:{' '} + + “This topic is harder than other topics we’ve covered. Don’t + worry if you don’t get immediately—just keep reading and you’ll + get it eventually!” + {' '} + A little touch of empathy can go a long way.{' '} + +

+ + ) + }, + { + title: <>Parting thoughts, + content: ( + <> +

+ In 2017, Dan Abramov from the React.js team gave an excellent talk + called “ + + The Melting Pot of JavaScript + + ”. He talked about how one should approach building tools (like + React,{' '} + + create-react-app + + , etc) that many beginners/newcomers use. + Here’s his quote: +

+
+

+ If you’re building tools like me, there’s this fact that we have + become the new gatekeepers to one of the largest programming + communities in the world. +

+

+ And this is scary stuff. Because it means that every time our + tool prints an incomprehensible error message, somebody + somewhere decides that they’re just not cut out for programming. + And this is a big responsibility. +

+

+ [...] If you’re a maintainer of an open-source project it is + invaluable to go out there in the field and see what they + struggle with as they try to use your projects. +

+

+ And if you think improving newcomer experience is polish, it’s + not polish. If you go out there in the field you will see that + it makes a real difference in people’s lives, and what they can + learn, and what they can build with it. So it’s not just polish. + Take this seriously. +

+
+

+ I think his quote applies not just to coding tools, but also to + coding tutorials. +

+

+ You don’t have to follow all the guidelines I mentioned on this + page. Sometimes you have to break the rule when refactoring + code—and the same is true for refactoring tutorials. But do try to + revise as much as possible. As the saying goes, “Writing is + rewriting”. +

+
+

+ If you have feedback, including other ways to improve coding + tutorials, please let me know on{' '} + + Twitter at @chibicode + + . +

+ + ), + footer: { + content: + } + } + ]} + /> +) + +export default Page diff --git a/src/pages/todo.tsx b/src/pages/todo.tsx index 9188c5f..1d250d6 100644 --- a/src/pages/todo.tsx +++ b/src/pages/todo.tsx @@ -231,9 +231,9 @@ const Page = () => ( content: ( <>

- Note: If you already know TypeScript basics, - you won’t find anything new in this tutorial. However,{' '} - + If you already know TypeScript basics, you won’t find anything + new in this tutorial. However,{' '} + you might know someone (maybe one of your Twitter followers) who’re interested in learning TypeScript