+): keyof typeof allColors =>
+ ({
+ default: 'brown' as const
+ }[color])
+
+const Card = ({
+ title,
+ color = 'default',
+ children,
+ slideNumber,
+ slideCount,
+ isLast,
+ footer
+}: CardProps) => {
+ const { ns, colors, fontSizes, spaces, radii } = useTheme()
+ return (
+ <>
+
+ {slideNumber && slideCount && (
+
+ <>
+
+ Slide{' '}
+
+
+ {slideNumber}
+ {' '}
+
+ / {slideCount}
+
+ >
+
+ )}
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {children}
+
+ {footer && (
+
+ {footer.content}
+
+ )}
+
+
+ {!isLast && (
+
+ )}
+ >
+ )
+}
+
+export default Card
diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx
new file mode 100644
index 0000000..8c65d1d
--- /dev/null
+++ b/src/components/CodeBlock.tsx
@@ -0,0 +1,263 @@
+/** @jsx jsx */
+import { css, jsx } from '@emotion/core'
+import { useState } from 'react'
+import useTheme from 'src/hooks/useTheme'
+import Caption from 'src/components/Caption'
+import PrismHighlight, { defaultProps } from 'prism-react-renderer'
+import theme from 'src/lib/prismTheme'
+
+const CodeBlock = ({
+ children,
+ shouldHighlight,
+ result,
+ // pointToRunButton,
+ defaultResultVisible,
+ caption,
+ noHighlight
+}: {
+ children: string
+ shouldHighlight?: (lineNumber: number, tokenNumber: number) => boolean
+ result?: string
+ // pointToRunButton?: boolean
+ defaultResultVisible?: boolean
+ caption?: React.ReactNode
+ noHighlight?: boolean
+}) => {
+ const [resultVisible] = useState(defaultResultVisible)
+ const { radii, colors, ns, maxWidths, spaces, fontSizes } = useTheme()
+ // const buttonOnClick = () => setResultVisible(true)
+ return (
+
+ {caption && (
+
+
{caption}
+
+ )}
+
+ {({ tokens, getLineProps, getTokenProps }) => (
+
+
+ {tokens.map((line, i) => {
+ const { key: divKey, ...lineProps } = getLineProps({
+ line,
+ key: i
+ })
+ return (
+
+ {line.map((token, key) => {
+ const { key: spanKey, ...tokenProps } = getTokenProps({
+ token,
+ key
+ })
+ return (
+
+ )
+ })}
+
+ )
+ })}
+
+
+ )}
+
+ {/* result && (
+ <>
+
+ {resultVisible ? (
+
+
+ Result:{' '}
+
+
+ {result}
+
+
+ ) : (
+ <>
+
+ Run
+
+ {pointToRunButton && (
+
+ ← Press this button!
+
+ )}
+ >
+ )}
+
+ >
+ )*/}
+
+ )
+}
+
+export default CodeBlock
diff --git a/src/components/ContentTags/H3.tsx b/src/components/ContentTags/H3.tsx
new file mode 100644
index 0000000..410d2d4
--- /dev/null
+++ b/src/components/ContentTags/H3.tsx
@@ -0,0 +1,26 @@
+/** @jsx jsx */
+import { css, jsx } from '@emotion/core'
+import useTheme from 'src/hooks/useTheme'
+
+const H3 = (props: JSX.IntrinsicElements['h3']) => {
+ const { fontSizes, lineHeights, ns, spaces } = useTheme()
+ return (
+
+ )
+}
+
+export default H3
diff --git a/src/components/ContentTags/P.tsx b/src/components/ContentTags/P.tsx
index 1266d41..0bd0469 100644
--- a/src/components/ContentTags/P.tsx
+++ b/src/components/ContentTags/P.tsx
@@ -2,7 +2,7 @@
import { css, jsx } from '@emotion/core'
import useTheme from 'src/hooks/useTheme'
-export const P = (props: JSX.IntrinsicElements['p']) => {
+const P = (props: JSX.IntrinsicElements['p']) => {
const { spaces } = useTheme()
return (
{
/>
)
}
+
+export default P
diff --git a/src/components/ContentTags/index.tsx b/src/components/ContentTags/index.tsx
index 207a07f..5352edc 100644
--- a/src/components/ContentTags/index.tsx
+++ b/src/components/ContentTags/index.tsx
@@ -1 +1,2 @@
-export { P } from 'src/components/ContentTags/P'
+export { default as P } from 'src/components/ContentTags/P'
+export { default as H3 } from 'src/components/ContentTags/H3'
diff --git a/src/components/Emoji/CryingCat.tsx b/src/components/Emoji/CryingCat.tsx
new file mode 100644
index 0000000..0a89310
--- /dev/null
+++ b/src/components/Emoji/CryingCat.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+
+const SvgCryingCat = (props: React.SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
+
+export default SvgCryingCat
diff --git a/src/components/Emoji/Question.tsx b/src/components/Emoji/Question.tsx
new file mode 100644
index 0000000..0f39341
--- /dev/null
+++ b/src/components/Emoji/Question.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+const SvgQuestion = (props: React.SVGProps) => (
+
+
+
+
+)
+
+export default SvgQuestion
diff --git a/src/components/Emoji/Run.tsx b/src/components/Emoji/Run.tsx
new file mode 100644
index 0000000..fcf0c89
--- /dev/null
+++ b/src/components/Emoji/Run.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+const SvgRun = (props: React.SVGProps) => (
+
+
+
+
+)
+
+export default SvgRun
diff --git a/src/components/Emoji/index.tsx b/src/components/Emoji/index.tsx
index aadf7cd..22a37e5 100644
--- a/src/components/Emoji/index.tsx
+++ b/src/components/Emoji/index.tsx
@@ -1,9 +1,15 @@
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import Bird from 'src/components/Emoji/Bird'
+import CryingCat from 'src/components/Emoji/CryingCat'
+import Question from 'src/components/Emoji/Question'
+import Run from 'src/components/Emoji/Run'
export const emojiToComponent = {
- bird: Bird
+ bird: Bird,
+ cryingCat: CryingCat,
+ question: Question,
+ run: Run
}
const Emoji = ({
diff --git a/src/components/EmojiSeparator.tsx b/src/components/EmojiSeparator.tsx
new file mode 100644
index 0000000..e370d1e
--- /dev/null
+++ b/src/components/EmojiSeparator.tsx
@@ -0,0 +1,83 @@
+/** @jsx jsx */
+import { css, jsx, SerializedStyles } from '@emotion/core'
+import React from 'react'
+import Emoji, { emojiToComponent } from 'src/components/Emoji'
+import { allFontSizes } from 'src/lib/theme/fontSizes'
+import { allSpaces } from 'src/lib/theme/spaces'
+import useTheme from 'src/hooks/useTheme'
+import Caption from 'src/components/Caption'
+
+interface EmojiSeparatorProps {
+ emojis: ReadonlyArray
+ size?: 'md'
+ cssOverrides?: SerializedStyles
+ description?: React.ReactNode
+}
+
+const fontSize = (
+ size: NonNullable
+): ReadonlyArray =>
+ ({
+ md: [2, 2.5] as const
+ }[size])
+
+const margins = (
+ size: NonNullable
+): ReadonlyArray =>
+ ({
+ md: [1.25, 1.5] as const
+ }[size])
+
+const SideSpace = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+)
+
+const EmojiSeparator = ({
+ emojis,
+ size = 'md',
+ cssOverrides,
+ description
+}: EmojiSeparatorProps) => {
+ const { spaces, ns, fontSizes } = useTheme()
+ return (
+
+ <>
+
+ {emojis.map((emoji, index) => (
+
+
+
+ ))}
+
+ {description &&
{description} }
+ >
+
+ )
+}
+
+export default EmojiSeparator
diff --git a/src/components/GlobalStyles.tsx b/src/components/GlobalStyles.tsx
index fd91edf..7477490 100644
--- a/src/components/GlobalStyles.tsx
+++ b/src/components/GlobalStyles.tsx
@@ -16,7 +16,7 @@ const GlobalStyles = ({ children }: { children: React.ReactNode }) => {
font-size: 18px;
color: ${colors('black')};
background: ${colors('lightYellow2')};
- line-height: ${lineHeights(1.6)};
+ line-height: ${lineHeights(1)};
}
body {
@@ -57,6 +57,32 @@ const GlobalStyles = ({ children }: { children: React.ReactNode }) => {
font-size: inherit;
line-height: inherit;
}
+
+ @font-face {
+ font-family: 'Iosevka Web';
+ font-weight: 400;
+ font-style: normal;
+ font-display: fallback;
+ src: url('/fonts/iosevka-regular.woff2') format('woff2'),
+ url('/fonts/iosevka-regular.woff') format('woff'),
+ url('/fonts/iosevka-regular.ttf') format('truetype');
+ }
+
+ @font-face {
+ font-family: 'Iosevka Web';
+ font-weight: 700;
+ font-style: oblique;
+ font-display: fallback;
+ src: url('/fonts/iosevka-boldoblique.woff2') format('woff2'),
+ url('/fonts/iosevka-boldoblique.woff') format('woff'),
+ url('/fonts/iosevka-boldoblique.ttf') format('truetype');
+ }
+
+ code,
+ pre {
+ font-family: 'Iosevka Web', SFMono-Regular, Consolas,
+ Liberation Mono, Menlo, Courier, monospace;
+ }
`
]}
/>
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 83dacbe..3f6e05c 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -6,14 +6,18 @@ import { siteTitle } from 'src/lib/meta'
import InternalLink from 'src/components/InternalLink'
const LogoLink = ({ children, ...props }: JSX.IntrinsicElements['h2']) => {
- const { colors } = useTheme()
+ const { colors, spaces, radii } = useTheme()
return (
{
{
const url = `${baseUrl}/${articleKey}`
const description = articlesData[articleKey]['description']
const title = articlesData[articleKey]['title']
const ogImage = `${baseUrl}/images/og-${articlesData[articleKey]['ogImage']}.png`
+ const {
+ colors,
+ fontSizes,
+ ns,
+ lineHeights,
+ letterSpacings,
+ spaces
+ } = useTheme()
return (
@@ -33,7 +53,61 @@ const PostPage = ({
content={dateSchemaString(articlesData[articleKey]['date'])}
/>
- Sorry, this page is under construction.
+
+
+ {dateString(articlesData[articleKey]['date'])}
+
+
+
+ {title}
+
+
+
+ >
+ )
+ }
+ ]}
+ >
+ {cards.map(({ title, content, footer }, index) => (
+
+ {content}
+
+ ))}
)
}
diff --git a/src/lib/prismTheme.ts b/src/lib/prismTheme.ts
new file mode 100644
index 0000000..d5bf56c
--- /dev/null
+++ b/src/lib/prismTheme.ts
@@ -0,0 +1,75 @@
+// Converted from Yi light theme
+// https://github.com/FormidableLabs/prism-react-renderer/tree/master/tools/themeFromVsCode
+import { PrismTheme } from 'prism-react-renderer'
+
+const theme: PrismTheme = {
+ plain: {
+ color: '#383a42',
+ backgroundColor: '#fafafa'
+ },
+ styles: [
+ {
+ types: ['comment'],
+ style: {
+ color: 'rgb(160, 161, 167)',
+ fontStyle: 'italic'
+ }
+ },
+ {
+ types: ['keyword', 'selector', 'changed'],
+ style: {
+ color: 'rgb(166, 38, 164)'
+ }
+ },
+ {
+ types: ['operator'],
+ style: {
+ color: 'rgb(56, 58, 66)'
+ }
+ },
+ {
+ types: ['constant', 'builtin', 'attr-name'],
+ style: {
+ color: 'rgb(152, 104, 1)'
+ }
+ },
+ {
+ types: ['char', 'symbol'],
+ style: {
+ color: 'rgb(1, 132, 188)'
+ }
+ },
+ {
+ types: ['variable', 'tag', 'deleted'],
+ style: {
+ color: 'rgb(228, 86, 73)'
+ }
+ },
+ {
+ types: ['string', 'inserted'],
+ style: {
+ color: 'rgb(80, 161, 79)'
+ }
+ },
+ {
+ types: ['punctuation'],
+ style: {
+ color: 'rgb(160, 161, 167)'
+ }
+ },
+ {
+ types: ['function'],
+ style: {
+ color: 'rgb(64, 120, 242)'
+ }
+ },
+ {
+ types: ['class-name'],
+ style: {
+ color: 'rgb(193, 132, 1)'
+ }
+ }
+ ]
+}
+
+export default theme
diff --git a/src/lib/theme/colors.ts b/src/lib/theme/colors.ts
index 54512e5..90e0a85 100644
--- a/src/lib/theme/colors.ts
+++ b/src/lib/theme/colors.ts
@@ -2,7 +2,14 @@ export const allColors = {
black: '#260808',
lightYellow1: '#FEF5DD',
lightYellow2: '#FFE8BF',
- brown: '#917340'
+ lightPink1: '#FFF2E4',
+ lightPink2: '#FFDFC7',
+ brown: '#806538',
+ pink: '#FCCFC0',
+ white: '#FFFFFF',
+ white75: 'rgba(255, 255, 255, 0.75)',
+ paleGreen: '#C8DCC7',
+ red: '#DB4003'
}
const colors = (x: keyof typeof allColors) => allColors[x]
diff --git a/src/lib/theme/fontSizes.ts b/src/lib/theme/fontSizes.ts
index 2419720..11dbf9e 100644
--- a/src/lib/theme/fontSizes.ts
+++ b/src/lib/theme/fontSizes.ts
@@ -1,5 +1,6 @@
export const allFontSizes = {
0.75: 0.75,
+ 0.8: 0.8,
0.85: 0.85,
1: 1,
1.2: 1.2,
diff --git a/src/lib/theme/letterSpacings.ts b/src/lib/theme/letterSpacings.ts
index 438fcf1..1fd5750 100644
--- a/src/lib/theme/letterSpacings.ts
+++ b/src/lib/theme/letterSpacings.ts
@@ -1,6 +1,6 @@
export const allLetterSpacings = {
- title: '-0.025em',
- wide: '0.2em'
+ title: '-0.02em',
+ wide: '0.15em'
}
const letterSpacings = (x: keyof typeof allLetterSpacings) =>
diff --git a/src/lib/theme/lineHeights.ts b/src/lib/theme/lineHeights.ts
index 63ff333..7e72e63 100644
--- a/src/lib/theme/lineHeights.ts
+++ b/src/lib/theme/lineHeights.ts
@@ -1,11 +1,20 @@
+import { allFontSizes } from 'src/lib/theme/fontSizes'
+
export const allLineHeights = {
- 1: 1,
- 1.1: 1.1,
- 1.3: 1.3,
- 1.5: 1.5,
- 1.6: 1.6
+ 0.75: 1.6,
+ 0.8: 1.6,
+ 0.85: 1.6,
+ 1: 1.6,
+ 1.2: 1.5,
+ 1.4: 1.4,
+ 1.6: 1.3,
+ 2: 1.25,
+ 2.5: 1.2,
+ 3: 1.1,
+ 4: 1,
+ 5: 1
}
-const lineHeights = (x: keyof typeof allLineHeights) => allLineHeights[x]
+const lineHeights = (x: keyof typeof allFontSizes) => allLineHeights[x]
export default lineHeights
diff --git a/src/lib/underConstructionCard.tsx b/src/lib/underConstructionCard.tsx
new file mode 100644
index 0000000..f38e6fb
--- /dev/null
+++ b/src/lib/underConstructionCard.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import { P } from 'src/components/ContentTags'
+
+const underConstructionCard = {
+ title: 'Under Construction',
+ content: (
+ <>
+ This article is currently under construction.
+ >
+ )
+}
+
+export default underConstructionCard
diff --git a/src/pages/generics.tsx b/src/pages/generics.tsx
index 1c014c3..e696219 100644
--- a/src/pages/generics.tsx
+++ b/src/pages/generics.tsx
@@ -1,6 +1,63 @@
import React from 'react'
import PostPage from 'src/components/PostPage'
+import { P } from 'src/components/ContentTags'
+import EmojiSeparator from 'src/components/EmojiSeparator'
+import CodeBlock from 'src/components/CodeBlock'
+import underConstructionCard from 'src/lib/underConstructionCard'
-const Page = () =>
+const Page = () => (
+
+ {`// Confused by generics code like this?
+function getProperty(
+ obj: T,
+ key: K
+) { /* ... */ }`}
+
+ If you’re (1) new to TypeScript, (2) new to{' '}
+ generics , and (3) struggling to
+ understand generics, then you’re exactly like me from 13 years
+ ago.
+
+
+ Back then, I was studying the Java programming language in
+ college. Generics were a relatively new feature for Java at the
+ time. I was a beginner programmer then, and{' '}
+ generics felt very difficult . So I gave up on actually
+ understanding generics and used them without knowing what I was
+ doing. I never really understood generics until I had to use them
+ a lot for my job.
+
+
+ I gave up on understanding generics when I was learning Java
+ 13 years ago
+ >
+ }
+ />
+
+ Similarly, if you feel that TypeScript generics are too
+ difficult—maybe you’ve only done frontend engineering in JS and
+ React/Vue, or you’re simply new to programming—this tutorial is
+ for you! I’ll try to help you actually understand generics.
+
+
+ (If you didn’t find TypeScript generics to be very difficult, this
+ tutorial might be too easy for you.)
+
+ >
+ )
+ },
+ underConstructionCard
+ ]}
+ />
+)
export default Page
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index c8544bb..f90619c 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -9,6 +9,7 @@ import { articlesList, articlesData } from 'src/lib/articles'
import { dateString } from 'src/lib/date'
import { siteTitle, baseUrl, siteDescription, siteOgImage } from 'src/lib/meta'
import Head from 'next/head'
+import { useState } from 'react'
const ArticleLink = ({
title,
@@ -19,7 +20,14 @@ const ArticleLink = ({
href: string
date: string
}) => {
- const { colors, ns, fontSizes, spaces, lineHeights } = useTheme()
+ const {
+ colors,
+ ns,
+ fontSizes,
+ spaces,
+ lineHeights,
+ letterSpacings
+ } = useTheme()
return (
{
+ const [visible, setVisible] = useState(defaultVisible)
+ const { colors } = useTheme()
+ return (
+ <>
+
+ Hello! I write tutorials to help{' '}
+ beginner programmers learn TypeScript. My tutorials might NOT
+ be as useful for experienced programmers learning TypeScript.
+ {!visible && (
+ <>
+ {' '}
+ (
+ setVisible(true)}
+ >
+ Read more…
+
+ )
+ >
+ )}
+
+ {visible && (
+
+ Why target beginner programmers? As TypeScript is
+ becoming popular, I believe that more beginner programmers (people
+ with only a few months of coding experience) will be learning it,{' '}
+ possibly as one of their first languages . So I wanted to
+ create tutorials specifically targeting beginner programmers.
+
+ )}
+ >
+ )
+}
+
const Index = () => {
const { ns, spaces, fontSizes, letterSpacings, colors } = useTheme()
return (
@@ -86,21 +145,7 @@ const Index = () => {
type: 'bird',
children: (
<>
-
- Hello! I write tutorials that might help{' '}
- beginner programmers learn TypeScript. They might NOT
- be as useful for experienced programmers who are new to
- TypeScript.
-
-
- Why targeting beginner programmers? As
- TypeScript is becoming popular, I believe that more beginner
- programmers (people with only a few months of coding
- experience) will be learning it,{' '}
- possibly as one of their first languages . So I wanted
- to create tutorials specifically targeting beginner
- programmers.
-
+
>
)
}