├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── TODO.md ├── bin └── blog.js ├── client-js └── index.js ├── components ├── article-image.js ├── article-wrap.js ├── back-to-base.js ├── card.js ├── code.js ├── cv.js ├── flex.js ├── inline-code.js ├── link.js ├── list-item.js ├── list.js ├── mdx.js ├── paragraph.js ├── quote.js ├── social-icons.js ├── spacer.js ├── title.js ├── toggle-theme.js └── wrap.js ├── fela.config.js ├── next.config.js ├── now.json ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── _error.js ├── blog │ ├── 2009-02-01-removing-from-node-list.md │ ├── 2013-02-06-stubbing-window-location-javascript.md │ ├── 2013-02-22-best-practice-testing-javascript-event-handlers.md │ ├── 2014-04-03-sortable-list-component-react-js.md │ ├── 2014-04-24-truly-reactive-sortable-component.md │ ├── 2020-01-26-mx-518.md │ ├── 2020-02-02-personal-website-2020-tech-stack.md │ ├── 2020-02-16-github-actions-preview-deploys.md │ ├── 2020-03-12-nextjs-without-client-side-react.md │ ├── 2020-04-06-flash-of-unstyled-dark-theme.md │ ├── 2021-11-23-minimal-git-config-mac-m1-zsh.md │ ├── 2024-04-23-mapped-indexed-access-types-typescript.md │ └── 2024-06-11-improved-control-flow-analysis-typescript-5.5.md ├── cv.js └── index.js ├── public ├── client-side-bundle.jpg ├── favicon.ico ├── ga-10-years.jpg ├── gatsby-dark-theme-lighthouse.jpg ├── github-deploy-action.png ├── hello-world-react.jpg ├── instant.js ├── mx518.jpg ├── netflix-react-ssr.jpg ├── photo.jpg ├── serverless-ssr.jpg └── vintage-webcloud-2010.jpg ├── test └── snapshots.js └── theme.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | # Based on https://github.com/facebook/react/blob/master/.editorconfig 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 80 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | max_line_length = 0 16 | trim_trailing_whitespace = false 17 | indent_size = 2 18 | indent_style = space 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | rules: { 16 | "react/prop-types": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # don't use yarn 28 | yarn.lock 29 | 30 | # autogenerated blog manifest 31 | blog-manifest.json 32 | 33 | # Lighthouse CI 34 | .lighthouseci 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webcloud 2 | 3 | This is the source code for my personal website, blog & playground. 4 | You can read about what/why/how/where the site was built in [this blog post](https://webcloud.se/blog/2020-02-02-personal-website-2020-tech-stack/) 5 | 6 | ## TLDR 7 | 8 | Here's a shortlist of tools, libraries and services used to build this website. 9 | 10 | ### Development 11 | 12 | - [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) 13 | - [React](https://reactjs.org/) - Server side only (SSR) 14 | - [Next.js](https://nextjs.org/) - Static site generation (SSG) 15 | - [MDX](https://mdxjs.com/) - Markdown + JSX 16 | - [date-fns](https://date-fns.org/) - JavaScript date utility library 17 | - [Fela](http://fela.js.org/) - State-driven atomic CSS styling in JavaScript 18 | - [UglifyJS](http://lisperator.net/uglifyjs/) - Minifier for client-side ES5 vanilla JavaScript code 19 | - [Prism](https://prismjs.com/) & [prism-react-renderer](https://github.com/FormidableLabs/prism-react-renderer) - Syntax highlighting for code examples 20 | - [instant.page](https://instant.page/) - Uses just-in-time preloading — it preloads a page right before a user clicks on it. 21 | 22 | ### Deployment 23 | 24 | - [Zeit Now](https://zeit.co/home) - Zero config static site deployments, reverse proxy & CDN 25 | 26 | ### Misc 27 | 28 | - [Simple Analytics](https://simpleanalytics.com/) - Like Google Analytics, but simple 29 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## Backlog 2 | 3 | - Create a "tweet" component to replace screenshots from twitter. 4 | - Check 404 hits in analytics and create approriate redirects. 5 | - Send PR/Branch information to Percy and Lighthouse via Github Actions 6 | - Currently not sent in deployment_status event(?) 7 | - If I figure out how, update the GH actions preview deploy blog post 8 | - Add "estimated article read time" feature 9 | - Style Guide Page + Add Visual Regression Test /ui 10 | - Curated Link List 11 | - RSS Feed for articles and link list 12 | - Add "Projects" section 13 | - web audio ir mixer 14 | - Lizard 15 | - Jquery Collapse 16 | - Add "Experiments" section 17 | - Assortment 18 | -------------------------------------------------------------------------------- /bin/blog.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const glob = require("glob"); 3 | 4 | const getDateFromFileName = name => { 5 | const [year, month, day] = name.split("-"); 6 | return `${year}-${month}-${day}`; 7 | }; 8 | 9 | // Searches the file for the first occurance of a markdown header 10 | const getTitleFromPost = filePath => { 11 | const file = fs.readFileSync(filePath, "utf-8"); 12 | return file 13 | .split("\n") 14 | .find(str => str.match(/^#/g)) 15 | .split("# ")[1]; 16 | }; 17 | 18 | const listPosts = () => { 19 | return glob 20 | .sync("./pages/blog/*.md", {}) 21 | .map(path => ({ 22 | path, 23 | title: path 24 | .split("/") 25 | .slice(-1)[0] 26 | .slice(0, -3) 27 | })) 28 | .map(({ path, title }) => ({ 29 | title: getTitleFromPost(path), 30 | date: getDateFromFileName(title), 31 | path: `/blog/${title}` 32 | })) 33 | .sort((a, b) => { 34 | return parseInt(a.date.replace(/\-/g,"")) - parseInt(b.date.replace(/\-/g,"")) 35 | }); 36 | }; 37 | 38 | const output = listPosts().reverse(); 39 | fs.writeFileSync("./blog-manifest.json", JSON.stringify(output, null, 2)); 40 | -------------------------------------------------------------------------------- /client-js/index.js: -------------------------------------------------------------------------------- 1 | (function(window, document) { 2 | // Note: Use ES5 compatible JS here 3 | var root = document.documentElement.style; 4 | var matcher = window.matchMedia("(prefers-color-scheme: dark)"); 5 | var light = "light"; 6 | var dark = "dark"; 7 | var userPreference = matcher.matches ? dark : light; 8 | var button; 9 | var currentTheme; 10 | 11 | try { 12 | currentTheme = 13 | window.localStorage.getItem("current-theme") || userPreference; 14 | } catch (e) { 15 | console.warn("could not read theme from local storage"); 16 | } 17 | 18 | if (currentTheme) { 19 | setTheme(currentTheme); 20 | } 21 | 22 | /* Open all
elements for print/PDF export */ 23 | window.addEventListener("beforeprint", function() { 24 | var details = document.querySelectorAll("details"); 25 | details.forEach(function(detail) { detail.open = true }) 26 | }); 27 | 28 | document.addEventListener("DOMContentLoaded", function() { 29 | button = document.querySelector("#toggle-theme"); 30 | setButtonLabel(); 31 | button.style.display = "block"; 32 | button.addEventListener("click", buttonClick); 33 | }); 34 | 35 | matcher.addListener(function() { 36 | setTheme(matcher.matches ? dark : light); 37 | setButtonLabel(); 38 | }); 39 | 40 | function setTheme(themeName) { 41 | // eslint-disable-next-line 42 | var theme = THEMES[themeName]; 43 | try { 44 | window.localStorage.setItem("current-theme", themeName); 45 | } catch (e) { 46 | console.warn("could not persist theme choice"); 47 | } 48 | currentTheme = themeName; 49 | Object.keys(theme).forEach(function(key) { 50 | root.setProperty("--" + key, theme[key]); 51 | }); 52 | } 53 | 54 | function setButtonLabel() { 55 | button.innerHTML = currentTheme === dark ? light : dark; 56 | } 57 | 58 | function buttonClick() { 59 | var theme = currentTheme === light ? dark : light; 60 | setTheme(theme); 61 | setButtonLabel(theme); 62 | } 63 | })(window, document); 64 | -------------------------------------------------------------------------------- /components/article-image.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createComponentWithProxy } from "react-fela"; 3 | 4 | export const Span = createComponentWithProxy({}, "span"); 5 | 6 | export const Image = createComponentWithProxy( 7 | { 8 | display: "block", 9 | width: "calc(100% + 32px)", 10 | position: "relative", 11 | left: "-16px", 12 | boxShadow: "var(--shadow)" 13 | }, 14 | "img" 15 | ); 16 | 17 | export const ArticleImage = ({ ...props }) => ( 18 | // Span required here because wrapping a
in causes issues with 19 | // Chrome/Lightouse acessibility audits 20 | 37 | 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /components/article-wrap.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { useRouter } from "next/router"; 4 | import { Wrap } from "./wrap"; 5 | import { Link } from "./link"; 6 | import { Flex } from "./flex"; 7 | import { Card } from "./card"; 8 | import { Spacer } from "./spacer"; 9 | import { Paragraph } from "./paragraph"; 10 | import { Twitter, Bluesky } from "../components/social-icons"; 11 | import { Title } from "./title"; 12 | import { BackToBase } from "./back-to-base"; 13 | import { createComponentWithProxy } from "react-fela"; 14 | import { SocialLink } from "../components/cv"; 15 | 16 | const Avatar = createComponentWithProxy( 17 | { 18 | width: "72px", 19 | height: "72px", 20 | "@media (min-width: 480px)": { 21 | width: "96px", 22 | height: "96px" 23 | }, 24 | borderRadius: "50%", 25 | boxShadow: "var(--shadow)" 26 | }, 27 | "img" 28 | ); 29 | 30 | const TWEET = "https://twitter.com/intent/tweet?text="; 31 | const BLUESKY = "https://bsky.app/intent/compose?text="; 32 | 33 | export const ArticleWrap = ({ title, children, nextPost, prevPost }) => { 34 | const router = useRouter(); 35 | return ( 36 | 37 | 38 | {title} - webcloud 39 | 40 | 41 | {children} 42 | 43 | 44 | 50 | 55 |
56 | 57 |
58 | 59 | Share on Twitter 60 |
61 | 66 |
67 | 68 |
69 | 70 | Share on Bluesky 71 |
72 |
73 | 74 | 77 |
78 | 79 |
80 | 81 | 82 | Hi, thanks for reading! 83 | 84 | 85 | I’m Daniel, Software Engineer from Sweden. If you have any 86 | questions regarding this article please reach out to me on{" "} 87 | Bluesky. 88 | You can also find me on{" "} 89 | GitHub. 90 | 91 | 92 |
93 | 94 | Read more articles written by me: 95 | 96 | 103 | {prevPost && ( 104 | <> 105 | 106 | 107 | ← {prevPost.title} 108 | 109 | 110 | 111 | )} 112 | {nextPost && ( 113 | 114 | 115 | {nextPost.title} → 116 | 117 | )} 118 | 119 | 120 | 121 |
122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /components/back-to-base.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "./link"; 3 | import { Spacer } from "./spacer"; 4 | import { createComponentWithProxy } from "react-fela"; 5 | 6 | const HideOnPrint = createComponentWithProxy( 7 | { 8 | display: "flex", 9 | fontWeight: "var(--font-weight-normal)", 10 | flexDirection: "column", 11 | "@media print": { 12 | display: "none" 13 | } 14 | }, 15 | "div" 16 | ); 17 | 18 | export const BackToBase = () => ( 19 | 20 | 21 |
22 | ↖ Back to base 23 |
24 | 25 |
26 | ); 27 | -------------------------------------------------------------------------------- /components/card.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from "react-fela"; 2 | 3 | export const Card = createComponent( 4 | { 5 | padding: "32px 16px", 6 | flexDirection: "column", 7 | display: "flex", 8 | background: "var(--color-bg-alt)", 9 | boxShadow: "var(--shadow)" 10 | }, 11 | "div" 12 | ); 13 | -------------------------------------------------------------------------------- /components/code.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createComponentWithProxy } from "react-fela"; 3 | import Highlight, { defaultProps } from "prism-react-renderer"; 4 | 5 | const codeBlock = { 6 | padding: "16px", 7 | background: "var(--color-bg-alt)", 8 | color: "var(--color-fg-alt)", 9 | boxShadow: "var(--shadow)", 10 | fontSize: "14px", 11 | lineHeight: "16px", 12 | width: "100%", 13 | overflowX: "auto", 14 | position: "relative", 15 | marginLeft: "-16px", 16 | marginBottom: "32px" 17 | }; 18 | const CodeBlock = createComponentWithProxy(codeBlock, "div"); 19 | 20 | const Token = createComponentWithProxy({}, "span"); 21 | 22 | const tokenColor = tokens => { 23 | if (tokens.includes("punctuation")) return "var(--color-code-punctuation)"; 24 | if (tokens.includes("comment")) return "var(--color-code-comment)"; 25 | if (tokens.includes("keyword")) return "var(--color-primary)"; 26 | if (tokens.includes("method")) return "var(--color-code-method)"; 27 | if (tokens.includes("string")) return "var(--color-code-string)"; 28 | if (tokens.includes("function")) return "var(--color-code-function)"; 29 | if (tokens.includes("dom")) return "var(--color-code-dom)"; 30 | if (tokens.includes("property-access")) 31 | return "var(--color-code-property-access)"; 32 | if (tokens.includes("class-name")) return "var(--color-code-class-name)"; 33 | if (tokens.includes("operator")) return "var(--color-code-operator)"; 34 | if (tokens.includes("selector")) return "var(--color-primary)"; 35 | if (tokens.includes("property")) return "var(--color-code-method)"; 36 | }; 37 | 38 | export const Code = ({ children, className }) => { 39 | const language = className.replace(/language-/, ""); 40 | // Remove newline from end of code 41 | const code = children.replace(/\n$/g, ""); 42 | 43 | return ( 44 | 50 | {({ tokens, getTokenProps }) => ( 51 | 52 | {tokens.map((line, i) => ( 53 |
54 | {line.map((token, key) => { 55 | const props = getTokenProps({ token, key }); 56 | return ( 57 | 63 | {props.children} 64 | 65 | ); 66 | })} 67 |
68 | ))} 69 |
70 | )} 71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /components/cv.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createComponent, createComponentWithProxy } from "react-fela"; 3 | import { Paragraph } from "../components/paragraph"; 4 | import { Flex } from "../components/flex"; 5 | import { Spacer } from "../components/spacer"; 6 | 7 | export const Job = createComponentWithProxy( 8 | { 9 | display: "flex", 10 | pageBreakInside: "avoid", 11 | flexDirection: "column", 12 | paddingTop: "24px", 13 | "@media not print": { 14 | "[open] summary:before": { 15 | content: "'-'" 16 | } 17 | } 18 | }, 19 | "details" 20 | ); 21 | 22 | export const JobSummary = createComponent( 23 | { 24 | display: "flex", 25 | flexDirection: "column", 26 | paddingBottom: "16px", 27 | paddingLeft: 0, 28 | marginBottom: "16px", 29 | borderBottom: "1px solid var(--color-border)", 30 | position: "relative", 31 | listStyle: "none", 32 | "::-webkit-details-marker": { 33 | display: "none" 34 | }, 35 | "@media not print": { 36 | ":before": { 37 | top: "50%", 38 | transform: "translateY(-50%)", 39 | fontSize: "24px", 40 | fontWeight: "200", 41 | position: "absolute", 42 | right: "16px", 43 | color: "var(--color-primary)", 44 | content: "'+'" 45 | }, 46 | ":hover": { 47 | cursor: "pointer", 48 | borderBottom: "1px solid var(--color-fg)", 49 | ":before": { 50 | color: "var(--color-fg)" 51 | } 52 | } 53 | }, 54 | }, 55 | "summary" 56 | ); 57 | 58 | export const JobTitle = createComponent( 59 | { fontSize: "24px", fontWeight: 200, marginBottom: "16px" }, 60 | "div" 61 | ); 62 | export const JobCompany = createComponent( 63 | { 64 | marginBottom: "16px", 65 | fontSize: "16px", 66 | fontWeight: 500 67 | }, 68 | "span" 69 | ); 70 | export const JobDuration = createComponent({}, "span"); 71 | 72 | export const JobLocation = createComponent({}, "span"); 73 | 74 | export const JobMeta = ({ children }) => ( 75 | 84 | {React.Children.toArray(children).map((item, i) => ( 85 | 86 | {i !== 0 && <> {"|"} } 87 | {item} 88 | 89 | ))} 90 | 91 | ); 92 | 93 | export const JobDescription = ({ children }) => ( 94 | 95 | {React.Children.toArray(children).map((item, i) => ( 96 | 97 | {item} 98 | 99 | 100 | ))} 101 | 102 | ); 103 | 104 | export const SocialLink = ({ children, ...props }) => ( 105 | 140 | {children} 141 | 142 | ); 143 | -------------------------------------------------------------------------------- /components/flex.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | export const Flex = createComponentWithProxy( 4 | { 5 | flexDirection: "column", 6 | display: "flex", 7 | "@media print": { 8 | display: "block" 9 | } 10 | }, 11 | "div" 12 | ); 13 | -------------------------------------------------------------------------------- /components/inline-code.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const inlineCode = { 4 | margin: "0 4px", 5 | fontSize: "14px", 6 | background: "var(--color-bg-alt)", 7 | color: "var(--color-fg)", 8 | boxShadow: "var(--shadow)", 9 | verticalAlign: "bottom" 10 | }; 11 | 12 | export const InlineCode = createComponentWithProxy(inlineCode, "code"); 13 | -------------------------------------------------------------------------------- /components/link.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const link = { 4 | textDecoration: "none", 5 | color: "var(--color-primary)", 6 | lineHeight: "24px", 7 | borderBottom: "1px solid var(--color-border)", 8 | transition: "all 0.2s ease-in-out", 9 | "&:hover": { 10 | color: "var(--color-fg)", 11 | borderBottom: "1px solid var(--color-primary)" 12 | } 13 | }; 14 | 15 | const iconLink = { 16 | opacity: 0.8, 17 | transition: "opacity 0.2s ease-in-out", 18 | "&:hover": { 19 | opacity: 1 20 | } 21 | }; 22 | 23 | export const Link = createComponentWithProxy(link, "a"); 24 | export const IconLink = createComponentWithProxy(iconLink, "a"); 25 | -------------------------------------------------------------------------------- /components/list-item.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const listItem = () => ({ 4 | lineHeight: "24px", 5 | margin: "0 0 8px", 6 | "& > ul": { 7 | margin: "8px 0 0", 8 | } 9 | }); 10 | 11 | export const ListItem = createComponentWithProxy(listItem, "li"); 12 | -------------------------------------------------------------------------------- /components/list.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const list = () => ({ 4 | margin: 0, 5 | paddingLeft: "24px", 6 | color: "var(--color-fg-alt)", 7 | fontWeight: "var(--font-weight-normal)", 8 | }); 9 | 10 | export const List = createComponentWithProxy(list, "ul"); 11 | -------------------------------------------------------------------------------- /components/mdx.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createComponentWithProxy } from "react-fela"; 3 | import { ArticleImage } from "../components/article-image"; 4 | import { Title } from "../components/title"; 5 | import { Paragraph } from "../components/paragraph"; 6 | import { Link } from "../components/link"; 7 | import { Spacer } from "../components/spacer"; 8 | import { Code } from "../components/code"; 9 | import { InlineCode } from "../components/inline-code"; 10 | import { List } from "../components/list"; 11 | import { ListItem } from "../components/list-item"; 12 | import { Quote } from "../components/quote"; 13 | 14 | const Span = createComponentWithProxy({}, "span"); 15 | const Strong = createComponentWithProxy( 16 | { 17 | fontWeight: "var(--font-weight-bold)", 18 | color: "var(--color-fg)" 19 | }, 20 | "strong" 21 | ); 22 | const Intro = ({ ...props }) => ( 23 | <> 24 | 25 | <Spacer size={6} /> 26 | </> 27 | ); 28 | const p = ({ ...props }) => ( 29 | <> 30 | <Paragraph extend={{ color: "var(--color-fg-alt)" }} {...props} /> 31 | <Spacer size={4} /> 32 | </> 33 | ); 34 | 35 | export const components = { 36 | h2: function H2({ ...props }) { 37 | return ( 38 | <> 39 | <Spacer size={5} /> 40 | <Title as="h2" variant="orange" {...props} /> 41 | <Spacer size={3} /> 42 | </> 43 | ); 44 | }, 45 | h3: function H3({ ...props }) { 46 | return ( 47 | <> 48 | <Spacer size={4} /> 49 | <Title as="h3" variant="apple" {...props} /> 50 | <Spacer size={3} /> 51 | </> 52 | ); 53 | }, 54 | Intro, 55 | p, 56 | a: Link, 57 | img: ArticleImage, 58 | Link, 59 | li: ListItem, 60 | ul: function ul({ ...props }) { 61 | return ( 62 | <> 63 | <List {...props} /> 64 | <Spacer size={4} /> 65 | </> 66 | ); 67 | }, 68 | ol: function ol({ ...props }) { 69 | return ( 70 | <> 71 | <List as="ol" {...props} /> 72 | <Spacer size={4} /> 73 | </> 74 | ); 75 | }, 76 | hr: function thematicBreak() { 77 | return <Spacer size={8} />; 78 | }, 79 | code: Code, 80 | strong: Strong, 81 | blockquote: Quote, 82 | inlineCode: InlineCode, 83 | Spacer, 84 | SponsoredParagraph: function SponsoredParagraph({ ...props }) { 85 | return ( 86 | <> 87 | <Paragraph extend={{ color: "var(--color-fg-alt)" }} {...props} /> 88 | <Spacer size={2} /> 89 | </> 90 | ); 91 | }, 92 | Disclaimer: function Disclaimer({ ...props }) { 93 | return ( 94 | <> 95 | <Spacer size={2} /> 96 | <Paragraph 97 | extend={{ 98 | fontSize: "12px", 99 | lineHeight: 1.6, 100 | fontStyle: "italic", 101 | opacity: 0.5, 102 | color: "var(--color-fg-alt)" 103 | }} 104 | {...props} 105 | /> 106 | <Spacer size={4} /> 107 | </> 108 | ); 109 | }, 110 | SponsoredLink: function SponsoredLink({ children, ...props }) { 111 | return ( 112 | <> 113 | <Link {...props}>{children}</Link> 114 | <Span 115 | title="Disclaimer: I get paid if you click on this link and buy something." 116 | extend={{ 117 | cursor: "help", 118 | fontSize: "10px", 119 | letterSpacing: "0.03em", 120 | padding: "2px 4px", 121 | position: "relative", 122 | top: "-2px", 123 | background: "var(--color-primary-alt)", 124 | marginLeft: "8px", 125 | color: "var(--color-fg)" 126 | }} 127 | > 128 | sponsored link 129 | </Span> 130 | </> 131 | ); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /components/paragraph.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const p = () => ({ 4 | lineHeight: "24px", 5 | color: "var(--color-fg)", 6 | fontWeight: "var(--font-weight-normal)" 7 | }); 8 | 9 | export const Paragraph = createComponentWithProxy(p, "p"); 10 | -------------------------------------------------------------------------------- /components/quote.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const quote = () => ({ 4 | margin: "0", 5 | fontFamily: "serif", 6 | fontSize: "24px", 7 | fontWeight: "200", 8 | position: "relative", 9 | textRendering: "optimizeLegibility", 10 | fontSmoothing: "antialiased", 11 | "& p": { 12 | lineHeight: "28px" 13 | }, 14 | "-webkit-font-smoothing": "antialiased", 15 | "-moz-osx-fon-smoothing": "grayscale", 16 | "& p:first-of-type:before": { 17 | content: "'“'", 18 | position: "absolute", 19 | left: "-.5em" 20 | }, 21 | "& p:first-of-type:after": { 22 | content: "'”'" 23 | } 24 | }); 25 | 26 | export const Quote = createComponentWithProxy(quote, "blockquote"); 27 | -------------------------------------------------------------------------------- /components/social-icons.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createComponentWithProxy } from "react-fela"; 3 | 4 | const Svg = createComponentWithProxy( 5 | { 6 | fill: "var(--color-fg)", 7 | transition: "fill 0.2s ease-in" 8 | }, 9 | "svg" 10 | ); 11 | 12 | export const Github = () => ( 13 | <Svg width="24" height="24"> 14 | <title>GitHub 15 | 16 | 17 | ); 18 | 19 | export const Bluesky = () => ( 20 | 21 | Bluesky 22 | 23 | 24 | ); 25 | 26 | export const Twitter = () => ( 27 | 28 | Twitter 29 | 30 | 31 | ); 32 | 33 | export const Linkedin = () => ( 34 | 35 | LinkedIn 36 | 37 | 38 | ); 39 | 40 | export const Email = () => ( 41 | 42 | Email 43 | 44 | 45 | ); 46 | -------------------------------------------------------------------------------- /components/spacer.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const spacer = ({ size = 1 }) => ({ 4 | flexBasis: size * 8 + "px", 5 | flexShrink: 0, 6 | "@media print": { 7 | height: size * 8 + "px" 8 | } 9 | }); 10 | 11 | export const Spacer = createComponentWithProxy(spacer, "div"); 12 | -------------------------------------------------------------------------------- /components/title.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const title = ({ variant = "banana" }) => ({ 4 | ...(variant === "banana" && { 5 | fontWeight: "var(--font-weight-bold)", 6 | fontSize: "40px", 7 | lineHeight: "44px", 8 | "@media (min-width: 480px)": { 9 | fontSize: "56px", 10 | lineHeight: "60px" 11 | }, 12 | }), 13 | ...(variant === "pear" && { 14 | fontSize: "32px", 15 | fontWeight: "var(--font-weight-bold)", 16 | lineHeight: "32px" 17 | }), 18 | ...(variant === "orange" && { 19 | fontSize: "24px", 20 | fontWeight: "var(--font-weight-bold)", 21 | lineHeight: "24px" 22 | }), 23 | ...(variant === "apple" && { 24 | fontSize: "18px", 25 | fontWeight: "var(--font-weight-bold)", 26 | lineHeight: "24px" 27 | }), 28 | ...(variant === "kiwi" && { 29 | fontSize: "24px", 30 | lineHeight: "32px", 31 | fontWeight: "var(--font-weight-light)" 32 | }) 33 | }); 34 | 35 | export const Title = createComponentWithProxy(title, "h1"); 36 | -------------------------------------------------------------------------------- /components/toggle-theme.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithProxy } from "react-fela"; 2 | 3 | const toggleTheme = () => ({ 4 | padding: "8px 12px", 5 | border: "none", 6 | zIndex: 1, 7 | display: "none", 8 | cursor: "pointer", 9 | position: "fixed", 10 | fontSize: "12px", 11 | lineHeight: "1", 12 | fontWeight: "400", 13 | letterSpacing: "0.04em", 14 | fontFamily: "monospace", 15 | transition: "all 200ms ease-in", 16 | top: "32px", 17 | right: "32px", 18 | background: "var(--color-bg-alt)", 19 | color: "var(--color-fg)", 20 | boxShadow: "4px 4px 0px var(--color-primary-alt)", 21 | ":hover": { 22 | boxShadow: "4px 4px 0px var(--color-primary)" 23 | }, 24 | ":active": { 25 | boxShadow: "none", 26 | transform: "translate(4px,4px)" 27 | }, 28 | "@media print": { 29 | display: "none!important" 30 | } 31 | }); 32 | 33 | export const ToggleTheme = createComponentWithProxy(toggleTheme, "button"); 34 | -------------------------------------------------------------------------------- /components/wrap.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createComponentWithProxy } from "react-fela"; 3 | 4 | const wrap = () => ({ 5 | padding: "0 16px", 6 | margin: "0 auto", 7 | maxWidth: "640px", 8 | display: "flex", 9 | flexDirection: "column", 10 | "@media print": { 11 | display: "block" 12 | } 13 | }); 14 | 15 | export const InnerWrap = createComponentWithProxy(wrap, "div"); 16 | 17 | export const Wrap = ({ children, ...props }) => ( 18 | {children} 19 | ); 20 | -------------------------------------------------------------------------------- /fela.config.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "fela"; 2 | import { themes } from "./theme"; 3 | 4 | export function getRenderer() { 5 | const renderer = createRenderer(); 6 | 7 | const theme = themes["light"]; 8 | const cssVariables = {}; 9 | Object.keys(theme).forEach(key => { 10 | cssVariables["--" + key] = theme[key]; 11 | }); 12 | 13 | renderer.renderStatic(cssVariables, ":root"); 14 | renderer.renderStatic(` 15 | @media print { 16 | :root { 17 | ${Object.keys(theme) 18 | .map(key => "--" + key + ":" + theme[key] + "!important;") 19 | .join("")} 20 | } 21 | } 22 | `); 23 | 24 | renderer.renderStatic( 25 | { 26 | background: "var(--color-bg)", 27 | color: "var(--color-fg)", 28 | fontSize: "16px", 29 | transition: "all 0.2s ease-in", 30 | lineHeight: 1, 31 | fontFamily: 32 | 'system-ui, -apple-system, BlinkMacSystemFont, "avenir next", avenir, "helvetica neue", helvetica, ubuntu, roboto, noto, "segoe ui", arial, sans-serif' 33 | }, 34 | "body" 35 | ); 36 | 37 | renderer.renderStatic( 38 | { 39 | margin: "0", 40 | padding: "0" 41 | }, 42 | "*" 43 | ); 44 | 45 | return renderer; 46 | } 47 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const slug = require("rehype-slug"); 2 | const withMDX = require("@next/mdx")({ 3 | extension: /\.mdx?$/, 4 | options: { 5 | rehypePlugins: [slug] 6 | } 7 | }); 8 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 9 | enabled: process.env.ANALYZE === "true" 10 | }); 11 | 12 | module.exports = withBundleAnalyzer( 13 | withMDX({ 14 | exportTrailingSlash: true, 15 | pageExtensions: ["js", "jsx", "md", "mdx"] 16 | }) 17 | ); 18 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingSlash": true, 3 | "redirects": [ 4 | { 5 | "source": "/stubbing-window-location-javascript/", 6 | "destination": "/blog/2013-02-06-stubbing-window-location-javascript/" 7 | }, 8 | { 9 | "source": "/best-practice-testing-javascript-event-handlers/", 10 | "destination": "/blog/2013-02-22-best-practice-testing-javascript-event-handlers/" 11 | }, 12 | { 13 | "source": "/sortable-list-component-react-js/", 14 | "destination": "/blog/2014-04-03-sortable-list-component-react-js/" 15 | }, 16 | { 17 | "source": "/truly-reactive-sortable-component/", 18 | "destination": "/blog/2014-04-24-truly-reactive-sortable-component/" 19 | }, 20 | { 21 | "source": "/react-sortable/", 22 | "destination": "/blog/2014-04-03-sortable-list-component-react-js/" 23 | } 24 | ], 25 | "rewrites": [ 26 | { 27 | "source": "/jQuery-Collapse/(.*)", 28 | "destination": "https://webcloud-jquery-collapse.netlify.app/$1" 29 | }, 30 | { 31 | "source": "/Assortment/(.*)", 32 | "destination": "https://webcloud-assortment.netlify.app/$1" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webcloud.se", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "export NODE_OPTIONS='--openssl-legacy-provider' && node bin/blog && next dev", 7 | "build": "node bin/blog && next build && next export" 8 | }, 9 | "dependencies": { 10 | "@mdx-js/loader": "1.5.9", 11 | "@mdx-js/react": "1.5.9", 12 | "@next/bundle-analyzer": "9.3.0", 13 | "@next/mdx": "^9.1.3", 14 | "date-fns": "^2.9.0", 15 | "next": "9.3.5", 16 | "prism-react-renderer": "^1.0.2", 17 | "react": "16.12.0", 18 | "react-dom": "16.12.0", 19 | "react-fela": "^11.1.2", 20 | "rehype-slug": "^3.0.0", 21 | "uglify-js": "^3.6.9", 22 | "webpack": "^4.46.0" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^6.8.0", 26 | "eslint-plugin-react": "^7.18.0", 27 | "prettier": "^1.19.1" 28 | } 29 | } -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import App from "next/app"; 3 | import Head from "next/head"; 4 | import { format } from "date-fns"; 5 | import { Flex } from "../components/flex"; 6 | import { withRouter } from "next/router"; 7 | import { Spacer } from "../components/spacer"; 8 | import { Paragraph } from "../components/paragraph"; 9 | import { MDXProvider } from "@mdx-js/react"; 10 | import { components } from "../components/mdx"; 11 | import { ArticleWrap } from "../components/article-wrap"; 12 | import { ToggleTheme } from "../components/toggle-theme"; 13 | import { Title } from "../components/title"; 14 | import blogPosts from "../blog-manifest.json"; 15 | 16 | class MyApp extends App { 17 | render() { 18 | const { Component, pageProps } = this.props; 19 | 20 | const path = this.props.router.pathname; 21 | const type = path.split("/")[1]; 22 | let post = false; 23 | let nextPost = false; 24 | let prevPost = false; 25 | 26 | if (type === "blog") { 27 | post = blogPosts.filter(post => post.path == path)[0]; 28 | prevPost = blogPosts[blogPosts.indexOf(post) + 1]; 29 | nextPost = blogPosts[blogPosts.indexOf(post) - 1]; 30 | } 31 | 32 | return ( 33 | 44 | ); 45 | }, 46 | h1: function BlogTitle() { 47 | return ( 48 | 49 | {post.title} 50 | 51 | 54 | {format(new Date(post.date), "d MMMM Y")} 55 | 56 | 57 | ); 58 | } 59 | }} 60 | > 61 | 62 | webcloud 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default withRouter(MyApp); 72 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { Main, Head } from "next/document"; 3 | import { RendererProvider, renderToNodeList } from "react-fela"; 4 | import { getRenderer } from "../fela.config.js"; 5 | import { themes } from "../theme"; 6 | import fs from "fs"; 7 | var UglifyJS = require("uglify-js"); 8 | 9 | const clientSideJs = UglifyJS.minify( 10 | fs.readFileSync("client-js/index.js", "utf8") 11 | ).code; 12 | 13 | class MyHead extends Head { 14 | render() { 15 | let { head } = this.context._documentProps; 16 | let children = this.props.children; 17 | return ( 18 | 19 | {children} 20 | {head} 21 | 22 | ); 23 | } 24 | } 25 | 26 | export class MyMain extends Main { 27 | render() { 28 | const { html } = this.context._documentProps; 29 | return
; 30 | } 31 | } 32 | 33 | export default class extends Document { 34 | static async getInitialProps(ctx) { 35 | const renderer = getRenderer(); 36 | const originalRenderPage = ctx.renderPage; 37 | ctx.renderPage = () => 38 | originalRenderPage({ 39 | enhanceApp: App => props => ( 40 | 41 | 42 | 43 | ) 44 | }); 45 | const initialProps = await Document.getInitialProps(ctx); 46 | return { ...initialProps, renderer }; 47 | } 48 | 49 | render() { 50 | return ( 51 | 52 | 53 | {renderToNodeList(this.props.renderer)} 54 |