CINXE.COM

Ember Octane: Airtable Time

<!DOCTYPE html> <html lang="en-US" class="auto" > <head> <title>Ember Octane: Airtable Time</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="HandheldFriendly" content="True"/> <link rel="apple-touch-icon" href="https://yehudakatz.com/content/images/2024/08/yehuda-square.png"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="https://yehudakatz.com/assets/built/screen.min.css?v=b917a20540"> <script> function colored(e){document.querySelectorAll("."+e).forEach(function(e){e.textContent.includes("✦")&&(e.innerHTML=e.innerHTML.replace("✦","<span>✦</span>"))})} const htmlElement=document.documentElement,classList=htmlElement.classList; window.matchMedia("(prefers-color-scheme: dark)").matches?(classList.add("dark"),localStorage.setItem("theme","dark")):(classList.remove("dark"),localStorage.setItem("theme","light")); document.addEventListener("DOMContentLoaded",()=>{"dark"===localStorage.getItem("theme")?classList.add("dark"):classList.remove("dark")}),window.addEventListener("storage",()=>{"dark"===localStorage.getItem("theme")?classList.add("dark"):classList.remove("dark")}); window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{e.matches?(classList.add("dark"),localStorage.setItem("theme","dark")):(classList.remove("dark"),localStorage.setItem("theme","light"))});function initializeDarkMode(){var e=document.querySelectorAll(".light-logo"),o=document.querySelectorAll(".footer-light-logo");document.querySelector("html");var r=window.matchMedia("(prefers-color-scheme: dark)");function t(){r.matches?(e.forEach(function(e){e.src=""}),o.forEach(function(e){e.src=""})):(e.forEach(function(e){e.src="https://yehudakatz.com/content/images/2024/08/wycats--2-.png"}),o.forEach(function(e){e.src="https://yehudakatz.com/content/images/2024/08/wycats--2-.png"}))}t(),r.addEventListener("change",t)} </script> <link rel="icon" href="https://yehudakatz.com/content/images/size/w256h256/2024/08/yehuda-square.png" type="image/png"> <link rel="canonical" href="https://yehudakatz.com/2020/04/06/ember-octane-airtable-time/"> <meta name="referrer" content="no-referrer-when-downgrade"> <link rel="amphtml" href="https://yehudakatz.com/2020/04/06/ember-octane-airtable-time/amp/"> <meta property="og:site_name" content="Katz Got Your Tongue"> <meta property="og:type" content="article"> <meta property="og:title" content="Ember Octane: Airtable Time"> <meta property="og:description" content="This post is the fourth in a series on building an Ember application HTML-first. In this series, we&#x27;re going to build the EmberConf schedule application from the ground up. 1. Let&#x27;s Go [https://yehudakatz.com/2020/03/25/ember-octane-lets-go/] 2. Components [https://yehudakatz.com/2020/03/26/ember-octane-components/] 3. Pulling"> <meta property="og:url" content="https://yehudakatz.com/2020/04/06/ember-octane-airtable-time/"> <meta property="article:published_time" content="2020-04-06T17:20:31.000Z"> <meta property="article:modified_time" content="2020-04-06T17:20:31.000Z"> <meta name="twitter:card" content="summary"> <meta name="twitter:title" content="Ember Octane: Airtable Time"> <meta name="twitter:description" content="This post is the fourth in a series on building an Ember application HTML-first. In this series, we&#x27;re going to build the EmberConf schedule application from the ground up. 1. Let&#x27;s Go [https://yehudakatz.com/2020/03/25/ember-octane-lets-go/] 2. Components [https://yehudakatz.com/2020/03/26/ember-octane-components/] 3. Pulling"> <meta name="twitter:url" content="https://yehudakatz.com/2020/04/06/ember-octane-airtable-time/"> <meta name="twitter:label1" content="Written by"> <meta name="twitter:data1" content="Yehuda Katz"> <meta name="twitter:site" content="@wycats"> <meta name="twitter:creator" content="@wycats"> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "publisher": { "@type": "Organization", "name": "Katz Got Your Tongue", "url": "https://yehudakatz.com/", "logo": { "@type": "ImageObject", "url": "https://yehudakatz.com/content/images/2024/08/wycats--2-.png" } }, "author": { "@type": "Person", "name": "Yehuda Katz", "image": { "@type": "ImageObject", "url": "https://yehudakatz.com/content/images/2021/04/Profile---Correct.png", "width": 500, "height": 500 }, "url": "https://yehudakatz.com/author/wycats/", "sameAs": [ "https://twitter.com/wycats" ] }, "headline": "Ember Octane: Airtable Time", "url": "https://yehudakatz.com/2020/04/06/ember-octane-airtable-time/", "datePublished": "2020-04-06T17:20:31.000Z", "dateModified": "2020-04-06T17:20:31.000Z", "description": "This post is the fourth in a series on building an Ember application HTML-first.\nIn this series, we&#x27;re going to build the EmberConf schedule application from the\nground up.\n\n 1. Let&#x27;s Go [https://yehudakatz.com/2020/03/25/ember-octane-lets-go/]\n 2. Components [https://yehudakatz.com/2020/03/26/ember-octane-components/]\n 3. Pulling Out Data [https://yehudakatz.com/2020/03/30/ember-octane-a-data-file/]\n 4. Airtable Time ← This post\n 5. Cleaning Things Up\n 6. Adding More Pages\n 7. Polishing: Server", "mainEntityOfPage": "https://yehudakatz.com/2020/04/06/ember-octane-airtable-time/" } </script> <meta name="generator" content="Ghost 5.101"> <link rel="alternate" type="application/rss+xml" title="Katz Got Your Tongue" href="https://yehudakatz.com/rss/"> <script defer src="https://cdn.jsdelivr.net/ghost/portal@~2.46/umd/portal.min.js" data-i18n="true" data-ghost="https://yehudakatz.com/" data-key="7d10c674ca44f7fe0a608ff2fb" data-api="https://yehudakatz.ghost.io/ghost/api/content/" data-locale="en-US" crossorigin="anonymous"></script><style id="gh-members-styles">.gh-post-upgrade-cta-content, .gh-post-upgrade-cta { display: flex; flex-direction: column; align-items: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; text-align: center; width: 100%; color: #ffffff; font-size: 16px; } .gh-post-upgrade-cta-content { border-radius: 8px; padding: 40px 4vw; } .gh-post-upgrade-cta h2 { color: #ffffff; font-size: 28px; letter-spacing: -0.2px; margin: 0; padding: 0; } .gh-post-upgrade-cta p { margin: 20px 0 0; padding: 0; } .gh-post-upgrade-cta small { font-size: 16px; letter-spacing: -0.2px; } .gh-post-upgrade-cta a { color: #ffffff; cursor: pointer; font-weight: 500; box-shadow: none; text-decoration: underline; } .gh-post-upgrade-cta a:hover { color: #ffffff; opacity: 0.8; box-shadow: none; text-decoration: underline; } .gh-post-upgrade-cta a.gh-btn { display: block; background: #ffffff; text-decoration: none; margin: 28px 0 0; padding: 8px 18px; border-radius: 4px; font-size: 16px; font-weight: 600; } .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; }</style><script async src="https://js.stripe.com/v3/"></script> <script defer src="https://cdn.jsdelivr.net/ghost/sodo-search@~1.5/umd/sodo-search.min.js" data-key="7d10c674ca44f7fe0a608ff2fb" data-styles="https://cdn.jsdelivr.net/ghost/sodo-search@~1.5/umd/main.css" data-sodo-search="https://yehudakatz.ghost.io/" data-locale="en-US" crossorigin="anonymous"></script> <script defer src="https://cdn.jsdelivr.net/ghost/announcement-bar@~1.1/umd/announcement-bar.min.js" data-announcement-bar="https://yehudakatz.com/" data-api-url="https://yehudakatz.com/members/api/announcement/" crossorigin="anonymous"></script> <link href="https://yehudakatz.com/webmentions/receive/" rel="webmention"> <script defer src="/public/cards.min.js?v=b917a20540"></script> <link rel="stylesheet" type="text/css" href="/public/cards.min.css?v=b917a20540"> <script defer src="/public/member-attribution.min.js?v=b917a20540"></script><style>:root {--ghost-accent-color: #ff9b36;}</style> <style> :root { --vp-code-line-height: 1.7; --vp-code-font-size: .875em; --vp-code-color: var(--vp-c-brand-1); --vp-code-link-color: var(--vp-c-brand-1); --vp-code-link-hover-color: var(--vp-c-brand-2); --vp-code-bg: var(--vp-c-default-soft); --vp-code-block-color: var(--vp-c-text-2); --vp-code-block-bg: var(--vp-c-bg-alt); --vp-code-block-divider-color: var(--vp-c-gutter); --vp-code-lang-color: var(--vp-c-text-3); --vp-code-line-highlight-color: var(--vp-c-default-soft); --vp-code-line-number-color: var(--vp-c-text-3); --vp-code-line-diff-add-color: var(--vp-c-success-soft); --vp-code-line-diff-add-symbol-color: var(--vp-c-success-1); --vp-code-line-diff-remove-color: var(--vp-c-danger-soft); --vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1); --vp-code-line-warning-color: var(--vp-c-warning-soft); --vp-code-line-error-color: var(--vp-c-danger-soft); --vp-code-copy-code-border-color: var(--vp-c-divider); --vp-code-copy-code-bg: var(--vp-c-bg-soft); --vp-code-copy-code-hover-border-color: var(--vp-c-divider); --vp-code-copy-code-hover-bg: var(--vp-c-bg); --vp-code-copy-code-active-text: var(--vp-c-text-2); --vp-code-copy-copied-text-content: "Copied"; --vp-code-tab-divider: var(--vp-code-block-divider-color); --vp-code-tab-text-color: var(--vp-c-text-2); --vp-code-tab-bg: var(--vp-code-block-bg); --vp-code-tab-hover-text-color: var(--vp-c-text-1); --vp-code-tab-active-text-color: var(--vp-c-text-1); --vp-code-tab-active-bar-color: var(--vp-c-brand-1); } :root { --vp-c-white: #ffffff; --vp-c-black: #000000; --vp-c-neutral: var(--vp-c-black); --vp-c-neutral-inverse: var(--vp-c-white) } .dark { --vp-c-neutral: var(--vp-c-white); --vp-c-neutral-inverse: var(--vp-c-black) } :root { --vp-c-gray-1: #dddde3; --vp-c-gray-2: #e4e4e9; --vp-c-gray-3: #ebebef; --vp-c-gray-soft: rgba(142, 150, 170, .14); --vp-c-indigo-1: #3451b2; --vp-c-indigo-2: #3a5ccc; --vp-c-indigo-3: #5672cd; --vp-c-indigo-soft: rgba(100, 108, 255, .14); --vp-c-purple-1: #6f42c1; --vp-c-purple-2: #7e4cc9; --vp-c-purple-3: #8e5cd9; --vp-c-purple-soft: rgba(159, 122, 234, .14); --vp-c-green-1: #18794e; --vp-c-green-2: #299764; --vp-c-green-3: #30a46c; --vp-c-green-soft: rgba(16, 185, 129, .14); --vp-c-yellow-1: #915930; --vp-c-yellow-2: #946300; --vp-c-yellow-3: #9f6a00; --vp-c-yellow-soft: rgba(234, 179, 8, .14); --vp-c-red-1: #b8272c; --vp-c-red-2: #d5393e; --vp-c-red-3: #e0575b; --vp-c-red-soft: rgba(244, 63, 94, .14); --vp-c-sponsor: #db2777 } .dark { --vp-c-gray-1: #515c67; --vp-c-gray-2: #414853; --vp-c-gray-3: #32363f; --vp-c-gray-soft: rgba(101, 117, 133, .16); --vp-c-indigo-1: #a8b1ff; --vp-c-indigo-2: #5c73e7; --vp-c-indigo-3: #3e63dd; --vp-c-indigo-soft: rgba(100, 108, 255, .16); --vp-c-purple-1: #c8abfa; --vp-c-purple-2: #a879e6; --vp-c-purple-3: #8e5cd9; --vp-c-purple-soft: rgba(159, 122, 234, .16); --vp-c-green-1: #3dd68c; --vp-c-green-2: #30a46c; --vp-c-green-3: #298459; --vp-c-green-soft: rgba(16, 185, 129, .16); --vp-c-yellow-1: #f9b44e; --vp-c-yellow-2: #da8b17; --vp-c-yellow-3: #a46a0a; --vp-c-yellow-soft: rgba(234, 179, 8, .16); --vp-c-red-1: #f66f81; --vp-c-red-2: #f14158; --vp-c-red-3: #b62a3c; --vp-c-red-soft: rgba(244, 63, 94, .16) } :root { --vp-c-bg: #ffffff; --vp-c-bg-alt: #f6f6f7; --vp-c-bg-elv: #ffffff; --vp-c-bg-soft: #f6f6f7 } .dark { --vp-c-bg: #1b1b1f; --vp-c-bg-alt: #161618; --vp-c-bg-elv: #202127; --vp-c-bg-soft: #202127 } :root { --vp-c-border: #c2c2c4; --vp-c-divider: #e2e2e3; --vp-c-gutter: #e2e2e3 } .dark { --vp-c-border: #3c3f44; --vp-c-divider: #2e2e32; --vp-c-gutter: #000000 } :root { --vp-c-text-1: rgba(60, 60, 67); --vp-c-text-2: rgba(60, 60, 67, .78); --vp-c-text-3: rgba(60, 60, 67, .56) } :root { --vp-c-default-1: var(--vp-c-gray-1); --vp-c-default-2: var(--vp-c-gray-2); --vp-c-default-3: var(--vp-c-gray-3); --vp-c-default-soft: var(--vp-c-gray-soft); --vp-c-brand-1: var(--vp-c-indigo-1); --vp-c-brand-2: var(--vp-c-indigo-2); --vp-c-brand-3: var(--vp-c-indigo-3); --vp-c-brand-soft: var(--vp-c-indigo-soft); --vp-c-brand: var(--vp-c-brand-1); --vp-c-tip-1: var(--vp-c-brand-1); --vp-c-tip-2: var(--vp-c-brand-2); --vp-c-tip-3: var(--vp-c-brand-3); --vp-c-tip-soft: var(--vp-c-brand-soft); --vp-c-note-1: var(--vp-c-brand-1); --vp-c-note-2: var(--vp-c-brand-2); --vp-c-note-3: var(--vp-c-brand-3); --vp-c-note-soft: var(--vp-c-brand-soft); --vp-c-success-1: var(--vp-c-green-1); --vp-c-success-2: var(--vp-c-green-2); --vp-c-success-3: var(--vp-c-green-3); --vp-c-success-soft: var(--vp-c-green-soft); --vp-c-important-1: var(--vp-c-purple-1); --vp-c-important-2: var(--vp-c-purple-2); --vp-c-important-3: var(--vp-c-purple-3); --vp-c-important-soft: var(--vp-c-purple-soft); --vp-c-warning-1: var(--vp-c-yellow-1); --vp-c-warning-2: var(--vp-c-yellow-2); --vp-c-warning-3: var(--vp-c-yellow-3); --vp-c-warning-soft: var(--vp-c-yellow-soft); --vp-c-danger-1: var(--vp-c-red-1); --vp-c-danger-2: var(--vp-c-red-2); --vp-c-danger-3: var(--vp-c-red-3); --vp-c-danger-soft: var(--vp-c-red-soft); --vp-c-caution-1: var(--vp-c-red-1); --vp-c-caution-2: var(--vp-c-red-2); --vp-c-caution-3: var(--vp-c-red-3); --vp-c-caution-soft: var(--vp-c-red-soft) } .content pre { max-height: initial; } .content pre.shiki, .content pre.shiki code, code[class*=language-], pre:has(code[class*=language-]) { --font-size: 0.8rem; font-size: var(--font-size) !important; line-height: calc(var(--font-size) * 1.5) !important; } pre.shiki { position: relative; z-index: 1; margin: 0; padding: 20px 20px; background: transparent; overflow-x: auto } pre.shiki code { padding: 0; width: fit-content; min-width: 100%; line-height: var(--vp-code-line-height); font-size: var(--vp-code-font-size); color: var(--vp-code-block-color); transition: color .5s } pre.shiki code .highlighted { background-color: var(--vp-code-line-highlight-color); transition: background-color .5s; margin: 0 -24px; padding: 0 24px; width: calc(100% + 44px); display: inline-block } pre.shiki code .highlighted.error { background-color: var(--vp-code-line-error-color) } pre.shiki code .highlighted.warning { background-color: var(--vp-code-line-warning-color) } pre.shiki code .diff { transition: background-color .5s; margin: 0 -24px; padding: 0 24px; width: calc(100% + 44px); display: inline-block } pre.shiki code .diff:before { position: absolute; left: 10px } pre.shiki code .diff.remove { background-color: var(--vp-code-line-diff-remove-color); opacity: .7 } pre.shiki code .diff.remove:before { content: "-"; color: var(--vp-code-line-diff-remove-symbol-color) } pre.shiki code .diff.add { background-color: var(--vp-code-line-diff-add-color) } pre.shiki code .diff.add:before { content: "+"; color: var(--vp-code-line-diff-add-symbol-color) } a[title="Home"] img { border-radius: 100px; } .post-image:not(:has(img)) { display: none !important; } section.post-content details { margin-bottom: 2em; } section.post-content details[open] { margin-bottom: 0; } .kg-toggle-heading-text { padding-block-start: 0 !important; } </style> <script type="comment"> import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/es/highlight.min.js'; // and it's easy to individually load additional languages import {setup} from 'https://cdn.jsdelivr.net/npm/highlightjs-glimmer@2.2.1/dist/glimmer.esm.min.js'; setup(hljs); hljs.addPlugin(new LineFocusPlugin({ focusedStyle: { borderLeft: "2px solid #78e08f55", background: "#78e08f05", padding: "2px", paddingLeft: "10px", }, unfocusedStyle: { borderLeft: "2px solid transparent", paddingLeft: "10px", opacity: "0.5", filter: "grayscale(1)" } })); </script> <script type='module'> import { codeToHtml } from 'https://esm.sh/shiki@1.17.0' import {transformerNotationHighlight,transformerNotationDiff} from 'https://esm.sh/@shikijs/transformers@1.17.0' const containers = document.querySelectorAll('code[class*=language-]'); console.log (containers); for (const container of containers) { console.log(container.innerHTML); const className = [...container.classList].find(name => name.startsWith("language-")); const lang = className.split("-").at(-1); const html = await codeToHtml(container.innerText, { lang, theme: 'github-light-high-contrast', transformers: [transformerNotationHighlight(), transformerNotationDiff()] }); container.parentElement.outerHTML = html; } </script> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12/dist/css/tabby-ui.min.css"> <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12/dist/js/tabby.polyfills.min.js"></script> </head> <body class="post-template bg-white dark:bg-zinc-950"> <nav class="main-navbar relative h-16 sm:h-20 min-h-max w-full px-4 xl:px-0 !z-40"> <div class="container mx-auto flex h-full max-w-screen-lg items-center justify-between"> <div class="main-navbar-logo flex-[1] justify-start"> <a href="https://yehudakatz.com" class="c-logo-light flex h-fit w-fit items-center"> <img class="c-logo-light-img light-logo min-h-[18px] !max-h-12 w-fit object-contain object-left " style=" height:48px" src="" alt="Katz Got Your Tongue" /> </a> <script> initializeDarkMode(); </script> </div> <div class="main-navbar-links main-nav mx-auto h-full w-fit items-center gap-6 px-4 hidden lg:flex select-none justify-center"> <a href="http://www.yehudakatz.com/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">Home</a> <a href="https://yehudakatz.com/about/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">About</a> <a href="https://yehudakatz.com/projects/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">Projects</a> <a href="https://yehudakatz.com/talks/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">Talks</a> <script>colored("nv")</script> </div> <div class="site-login h-full flex w-fit min-w-fit items-center gap-x-3 xs:gap-x-4 flex-[1] justify-end"> <a href="javascript:" data-ghost-search class="main-navbar-search-button primary-button p-0 min-w-[34px] min-h-[34px] w-[34px] h-[34px]" aria-label="Search"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="13" height="13"><path d="M14.5 14.5l-4-4m-4 2a6 6 0 110-12 6 6 0 010 12z" stroke="currentColor"></path></svg> </a> <a data-portal="signin" href="javascript:" aria-label="Sign in" class="main-navbar-sign-button secondary-button text-opacity-80 xs:text-opacity-100 p-0 xs:p-1 xs:px-3 xs:pr-3.5 xs:py-2 min-w-[34px] max-w-[34px] xs:max-w-fit "> <span class="block xs:hidden font-medium"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14"> <path d="M4.5 6.5v-3a3 3 0 016 0V4m-8 2.5h10a1 1 0 011 1v6a1 1 0 01-1 1h-10a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="currentColor" stroke-width="1.3px"></path> </svg> </span> <span class="main-navbar-sign-button-text hidden xs:block font-medium">✦ &nbsp;Sign in</span> </a> <a href="javascript:" aria-label="mobile-menu" class="main-navbar-mobile-icon primary-button p-0 min-w-[34px] max-w-[34px] min-h-[34px] hamburger lg:hidden"> <svg width="16px" height="16px" stroke-width="1.6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor"><path d="M3 5H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 12H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 19H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg> </a> </div> </div> </nav> <div class="mobile-menu mobile-menu-section fixed hidden flex-col top-16 lg:hidden h-[calc(100svh-64px)] w-full bg-white dark:bg-zinc-950 !z-[9999]"> <div class="mobile-menu flex flex-col text-center w-full !overflow-y-auto scroll-two h-[calc(100%-60px)]"> <a href="http://www.yehudakatz.com/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> Home </a> <a href="https://yehudakatz.com/about/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> About </a> <a href="https://yehudakatz.com/projects/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> Projects </a> <a href="https://yehudakatz.com/talks/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> Talks </a> </div> <div class="mobile-social-accounts flex flex-wrap justify-center items-center w-full h-[60px] bg-zinc-50 dark:bg-zinc-900 border-t border-zinc-100/[0.8] dark:border-zinc-800/[0.7] px-6"> <div class="mobile-social-accounts-items flex flex-wrap flex-row gap-5 items-center justify-center sm:justify-end text-three py-2"> <a href="https://twitter.com/@wycats" target="_blank" rel="noopener" aria-label="Twitter X Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.52217 6.77491L15.4785 0H14.0671L8.89516 5.88256L4.76437 0H0L6.24656 8.89547L0 16H1.41155L6.87321 9.78782L11.2356 16H16L9.52183 6.77491H9.52217ZM7.58887 8.97384L6.95596 8.08805L1.92015 1.03974H4.0882L8.15216 6.72795L8.78507 7.61374L14.0677 15.0075H11.8997L7.58887 8.97418V8.97384Z" fill="currentColor"/></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11 3.5h1M4.5.5h6a4 4 0 014 4v6a4 4 0 01-4 4h-6a4 4 0 01-4-4v-6a4 4 0 014-4zm3 10a3 3 0 110-6 3 3 0 010 6z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 6v5m6 0V8.5a2 2 0 10-4 0V11 6M4 4.5h1M1.5.5h12a1 1 0 011 1v12a1 1 0 01-1 1h-12a1 1 0 01-1-1v-12a1 1 0 011-1z" stroke="currentColor"></path></svg> </a> <a href="https://github.com/hedwik" target="_blank" rel="noopener" aria-label="Github Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M5.65 12.477a.5.5 0 10-.3-.954l.3.954zm-3.648-2.96l-.484-.128-.254.968.484.127.254-.968zM9 14.5v.5h1v-.5H9zm.063-4.813l-.054-.497a.5.5 0 00-.299.852l.352-.354zM12.5 5.913h.5V5.91l-.5.002zm-.833-2.007l-.466-.18a.5.5 0 00.112.533l.354-.353zm-.05-2.017l.456-.204a.5.5 0 00-.319-.276l-.137.48zm-2.173.792l-.126.484a.5.5 0 00.398-.064l-.272-.42zm-3.888 0l-.272.42a.5.5 0 00.398.064l-.126-.484zM3.383 1.89l-.137-.48a.5.5 0 00-.32.276l.457.204zm-.05 2.017l.354.353a.5.5 0 00.112-.534l-.466.181zM2.5 5.93H3v-.002l-.5.002zm3.438 3.758l.352.355a.5.5 0 00-.293-.851l-.06.496zM5.5 11H6l-.001-.037L5.5 11zM5 14.5v.5h1v-.5H5zm.35-2.977c-.603.19-.986.169-1.24.085-.251-.083-.444-.25-.629-.49a4.8 4.8 0 01-.27-.402c-.085-.139-.182-.302-.28-.447-.191-.281-.473-.633-.929-.753l-.254.968c.08.02.184.095.355.346.082.122.16.252.258.412.094.152.202.32.327.484.253.33.598.663 1.11.832.51.168 1.116.15 1.852-.081l-.3-.954zm4.65-.585c0-.318-.014-.608-.104-.878-.096-.288-.262-.51-.481-.727l-.705.71c.155.153.208.245.237.333.035.105.053.254.053.562h1zm-.884-.753c.903-.097 1.888-.325 2.647-.982.78-.675 1.237-1.729 1.237-3.29h-1c0 1.359-.39 2.1-.892 2.534-.524.454-1.258.653-2.099.743l.107.995zM13 5.91a3.354 3.354 0 00-.98-2.358l-.707.706c.438.44.685 1.034.687 1.655l1-.003zm-.867-1.824c.15-.384.22-.794.21-1.207l-1 .025a2.12 2.12 0 01-.142.82l.932.362zm.21-1.207a3.119 3.119 0 00-.27-1.195l-.913.408c.115.256.177.532.184.812l1-.025zm-.726-.99c.137-.481.137-.482.136-.482h-.003l-.004-.002a.462.462 0 00-.03-.007 1.261 1.261 0 00-.212-.024 2.172 2.172 0 00-.51.054c-.425.091-1.024.317-1.82.832l.542.84c.719-.464 1.206-.634 1.488-.694a1.2 1.2 0 01.306-.03l-.008-.001a.278.278 0 01-.01-.002l-.006-.002h-.003l-.002-.001c-.001 0-.002 0 .136-.482zm-2.047.307a8.209 8.209 0 00-4.14 0l.252.968a7.209 7.209 0 013.636 0l.252-.968zm-3.743.064c-.797-.514-1.397-.74-1.822-.83a2.17 2.17 0 00-.51-.053 1.259 1.259 0 00-.241.03l-.004.002h-.003l.136.481.137.481h-.001l-.002.001-.003.001a.327.327 0 01-.016.004l-.008.001h.008a1.19 1.19 0 01.298.03c.282.06.769.23 1.488.694l.543-.84zm-2.9-.576a3.12 3.12 0 00-.27 1.195l1 .025a2.09 2.09 0 01.183-.812l-.913-.408zm-.27 1.195c-.01.413.06.823.21 1.207l.932-.362a2.12 2.12 0 01-.143-.82l-1-.025zm.322.673a3.354 3.354 0 00-.726 1.091l.924.38c.118-.285.292-.545.51-.765l-.708-.706zm-.726 1.091A3.354 3.354 0 002 5.93l1-.003c0-.31.06-.616.177-.902l-.924-.38zM2 5.93c0 1.553.458 2.597 1.239 3.268.757.65 1.74.88 2.64.987l.118-.993C5.15 9.09 4.416 8.89 3.89 8.438 3.388 8.007 3 7.276 3 5.928H2zm3.585 3.404c-.5.498-.629 1.09-.584 1.704L6 10.963c-.03-.408.052-.683.291-.921l-.705-.709zM5 11v3.5h1V11H5zm5 3.5V13H9v1.5h1zm0-1.5v-2.063H9V13h1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M.5.5V0a.5.5 0 00-.5.5h.5zm14 0h.5a.5.5 0 00-.5-.5v.5zm0 8l.354.354A.5.5 0 0015 8.5h-.5zm-3 3v.5a.5.5 0 00.354-.146L11.5 11.5zm-5 0V11a.5.5 0 00-.325.12l.325.38zm-3.5 3h-.5a.5.5 0 00.825.38L3 14.5zm0-3h.5A.5.5 0 003 11v.5zm-2.5 0H0a.5.5 0 00.5.5v-.5zM.5 1h14V0H.5v1zM14 .5v8h1v-8h-1zm.146 7.646l-3 3 .708.708 3-3-.708-.708zM11.5 11h-5v1h5v-1zm-5.325.12l-3.5 3 .65.76 3.5-3-.65-.76zM3.5 14.5v-3h-1v3h1zM3 11H.5v1H3v-1zm-2 .5V.5H0v11h1zM10 3v5h1V3h-1zM7 3v5h1V3H7z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M9.5 0v11A3.5 3.5 0 116 7.5m8-2A4.5 4.5 0 019.5 1" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1.61 12.738l-.104.489.105-.489zm11.78 0l.104.489-.105-.489zm0-10.476l.104-.489-.105.489zm-11.78 0l.106.489-.105-.489zM6.5 5.5l.277-.416A.5.5 0 006 5.5h.5zm0 4H6a.5.5 0 00.777.416L6.5 9.5zm3-2l.277.416a.5.5 0 000-.832L9.5 7.5zM0 3.636v7.728h1V3.636H0zm15 7.728V3.636h-1v7.728h1zM1.506 13.227c3.951.847 8.037.847 11.988 0l-.21-.978a27.605 27.605 0 01-11.568 0l-.21.978zM13.494 1.773a28.606 28.606 0 00-11.988 0l.21.978a27.607 27.607 0 0111.568 0l.21-.978zM15 3.636c0-.898-.628-1.675-1.506-1.863l-.21.978c.418.09.716.458.716.885h1zm-1 7.728a.905.905 0 01-.716.885l.21.978A1.905 1.905 0 0015 11.364h-1zm-14 0c0 .898.628 1.675 1.506 1.863l.21-.978A.905.905 0 011 11.364H0zm1-7.728c0-.427.298-.796.716-.885l-.21-.978A1.905 1.905 0 000 3.636h1zM6 5.5v4h1v-4H6zm.777 4.416l3-2-.554-.832-3 2 .554.832zm3-2.832l-3-2-.554.832 3 2 .554-.832z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1c-1.155 0-2.174.412-2.894 1.281-.642.775-1.006 2.35-1.066 3.666l-.073.01-.022.004a8.68 8.68 0 00-.368.059c-.465.089-1.346.326-1.543 1.277-.093.445.011.833.247 1.134.211.269.497.429.708.53.09.041.181.08.274.117-.21.584-.579 1.184-.987 1.728-.382.508-.28 1.153-.083 1.573.197.421.57.402 1.192.43.352.015.722.051 1.09.12.166.03.362.098.606.2.142.06.283.123.423.187.113.052.235.106.374.167.573.25 1.276.517 2.056.517s1.483-.267 2.055-.517c.14-.06.26-.115.375-.167l.025-.012c.135-.06.26-.117.398-.174.243-.103.44-.17.606-.201a7.951 7.951 0 011.09-.12c.622-.028 1.104-.009 1.303-.43.197-.42.298-1.065-.084-1.573-.406-.54-.772-1.136-.983-1.716a5.24 5.24 0 00.305-.127c.216-.098.518-.261.73-.543.245-.326.315-.739.175-1.184-.28-.886-1.092-1.122-1.568-1.216a6.857 6.857 0 00-.355-.058l-.012-.002-.056-.009c-.065-1.234-.41-2.795-1.036-3.581C9.695 1.485 8.682 1 7.5 1z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 13.5l3-7m-3.236 3a2.989 2.989 0 01-.764-2V7A3.5 3.5 0 017 3.5h1A3.5 3.5 0 0111.5 7v.5a3 3 0 01-3 3 2.081 2.081 0 01-1.974-1.423L6.5 9m1 5.5a7 7 0 110-14 7 7 0 010 14z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1.5l.121-.485A.5.5 0 007 1.5h.5zm5.5 8c0 .774-.55 1.641-1.583 2.343C10.4 12.533 8.998 13 7.5 13v1c1.696 0 3.294-.525 4.479-1.33C13.148 11.876 14 10.743 14 9.5h-1zM7.5 13c-1.498 0-2.9-.466-3.917-1.157C2.551 11.14 2 10.273 2 9.5H1c0 1.243.852 2.376 2.021 3.17C4.206 13.475 5.804 14 7.5 14v-1zM2 9.5c0-.774.55-1.641 1.583-2.343C4.6 6.467 6.002 6 7.5 6V5c-1.696 0-3.294.525-4.479 1.33C1.852 7.124 1 8.257 1 9.5h1zM7.5 6c1.498 0 2.9.467 3.917 1.157C12.449 7.86 13 8.727 13 9.5h1c0-1.243-.852-2.376-2.021-3.17C10.794 5.525 9.196 5 7.5 5v1zm2.306 4.54c-.69.29-1.32.46-2.306.46v1c1.136 0 1.898-.204 2.694-.54l-.388-.92zM7.5 11c-.987 0-1.617-.17-2.306-.46l-.388.92c.796.336 1.558.54 2.694.54v-1zM8 5.5v-4H7v4h1zm-.621-3.515l4 1 .242-.97-4-1-.242.97zM3.974 6.841c-.286-.855-1.12-1.297-1.952-1.297v1c.51 0 .886.261 1.004.615l.948-.318zM2.022 5.544A2.022 2.022 0 000 7.566h1a1.02 1.02 0 011.022-1.022v-1zM0 7.566C0 8.589.76 9.424 1.74 9.56l.139-.99A1.016 1.016 0 011 7.565H0zm11.974-.407c.118-.354.493-.615 1.004-.615v-1c-.832 0-1.666.442-1.952 1.297l.948.318zm1.004-.615A1.02 1.02 0 0114 7.566h1a2.022 2.022 0 00-2.022-2.022v1zM14 7.566c0 .511-.38.934-.879 1.004l.139.99A2.016 2.016 0 0015 7.567h-1zM12.5 3a.5.5 0 01-.5-.5h-1A1.5 1.5 0 0012.5 4V3zm.5-.5a.5.5 0 01-.5.5v1A1.5 1.5 0 0014 2.5h-1zm-.5-.5a.5.5 0 01.5.5h1A1.5 1.5 0 0012.5 1v1zm0-1A1.5 1.5 0 0011 2.5h1a.5.5 0 01.5-.5V1zM5 9h1V8H5v1zm4 0h1V8H9v1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11.5 13.5l-.326.379a.5.5 0 00.342.12L11.5 13.5zm-1.066-1.712a.5.5 0 00-.785.62l.785-.62zm.398-.41l-.174-.468a.672.672 0 00-.02.007l.194.461zm-1.738.516L9.01 11.4l-.008.001.092.492zm-3.104-.012l-.095.49.003.001.092-.491zm-1.762-.515l-.182.465.182-.466zm-.875-.408l-.278.415a.46.46 0 00.033.021l.245-.436zm-.108-.06l.277-.416a.491.491 0 00-.054-.031l-.223.447zm-.048-.036l.353-.354a.502.502 0 00-.11-.083l-.243.437zm2.154 1.52a.5.5 0 00-.785-.62l.785.62zM3.5 13.5l-.016.5a.5.5 0 00.347-.125L3.5 13.5zm-3-2.253H0a.5.5 0 00.006.08l.494-.08zm1.726-8.488l-.3-.4a.5.5 0 00-.168.225l.468.175zM5.594 1.5l.498-.047A.5.5 0 005.605 1l-.01.5zm-.378 1.306a.5.5 0 00.996-.095l-.996.095zm3.526-.063a.5.5 0 00.992.127l-.992-.127zM9.406 1.5L9.395 1a.5.5 0 00-.485.436l.496.064zm3.368 1.259l.468-.175a.5.5 0 00-.168-.225l-.3.4zm1.726 8.488l.494.08a.497.497 0 00.006-.08h-.5zM6.481 8.8l-.5-.008V8.8h.5zm5.019 4.7l.326-.379-.002-.002a.794.794 0 01-.044-.038 21.355 21.355 0 01-.536-.48c-.325-.298-.66-.622-.81-.813l-.785.62c.208.264.603.64.918.93a29.109 29.109 0 00.593.53l.01.008.003.002.327-.378zm.436-3.246c-.46.303-.894.513-1.278.656l.348.937a7.352 7.352 0 001.48-.758l-.55-.835zm-1.297.663a7.387 7.387 0 01-1.629.484l.168.986a8.39 8.39 0 001.848-.548l-.387-.922zm-1.637.485a7.895 7.895 0 01-2.92-.012l-.184.983a8.896 8.896 0 003.288.012l-.184-.983zm-2.917-.011a9.57 9.57 0 01-1.675-.49l-.364.931c.512.2 1.13.402 1.849.54l.19-.981zm-1.675-.49a6.536 6.536 0 01-.813-.378l-.489.872c.326.183.648.324.938.437l.364-.931zm-.78-.358a.802.802 0 00-.108-.061c-.02-.01-.011-.007 0 .001l-.555.832a.87.87 0 00.108.061c.021.01.012.007 0-.002l.556-.83zm-.162-.091a.332.332 0 01.082.058l-.707.707c.023.023.081.08.178.13l.447-.895zm-.028-.026a4.697 4.697 0 01-.28-.168l-.011-.008a.025.025 0 00-.001 0l-.287.41-.286.409.001.001.002.002.007.004.021.014.075.049c.064.04.156.096.273.161l.486-.874zm1.126 1.338c-.152.193-.489.525-.813.829a30.38 30.38 0 01-.538.491l-.034.031-.01.008-.001.002h-.001l.331.375.331.375.001-.001.003-.002.01-.009.036-.032a38.039 38.039 0 00.555-.508c.315-.296.708-.677.915-.94l-.785-.62zM3.516 13c-1.166-.037-1.778-.521-2.11-.96a2.394 2.394 0 01-.4-.82 1.1 1.1 0 01-.013-.056v.002l-.493.08c-.494.08-.494.08-.493.081v.006a1.367 1.367 0 00.028.127 3.394 3.394 0 00.573 1.183c.505.667 1.393 1.31 2.876 1.357l.032-1zM1 11.247c0-1.867.42-3.94.847-5.564a35.45 35.45 0 01.776-2.552 16.43 16.43 0 01.067-.186l.004-.01v-.001l-.468-.175-.469-.175v.001l-.001.003-.004.011a9.393 9.393 0 00-.072.2 36.445 36.445 0 00-.8 2.629C.443 7.083 0 9.253 0 11.247h1zm1.526-8.088c.8-.6 1.577-.89 2.15-1.03a4.764 4.764 0 01.86-.128A1.48 1.48 0 015.585 2h-.001l.01-.5.01-.5H5.6a.848.848 0 00-.028 0h-.068a3.973 3.973 0 00-.24.016 5.763 5.763 0 00-.825.141 6.938 6.938 0 00-2.513 1.2l.6.8zm2.57-1.612l.12 1.259.996-.095-.12-1.258-.996.094zM9.734 2.87l.168-1.306-.992-.128-.168 1.307.992.127zM9.406 1.5l.01.5h-.001a.497.497 0 01.049 0c.038.002.1.005.179.013.16.014.394.047.681.117a5.94 5.94 0 012.15 1.029l.6-.8a6.937 6.937 0 00-2.513-1.2 5.76 5.76 0 00-.825-.142A3.98 3.98 0 009.399 1h-.003l.01.5zm3.368 1.259l-.469.174.001.003.004.009.013.037.053.149a35.482 35.482 0 01.777 2.552c.428 1.624.847 3.697.847 5.564h1c0-1.994-.444-4.164-.88-5.819a36.512 36.512 0 00-.8-2.629 15.246 15.246 0 00-.057-.158l-.015-.042-.004-.01-.001-.004-.47.174zm1.726 8.488l-.493-.08v-.003l-.002.008-.01.047c-.012.045-.03.113-.061.197-.062.17-.167.396-.34.624-.332.439-.944.923-2.11.96l.032 1c1.483-.047 2.37-.69 2.876-1.356a3.395 3.395 0 00.573-1.184 2.05 2.05 0 00.026-.118l.002-.01v-.004c0-.001 0-.002-.493-.081zM5.259 6.97c-1.002 0-1.723.867-1.723 1.83h1c0-.498.357-.83.723-.83v-1zM3.536 8.8c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zM6.98 8.81c.016-.978-.728-1.84-1.722-1.84v1.001c.372 0 .73.338.722.822l1 .017zm2.653-1.84c-1.002.001-1.723.868-1.723 1.831h1c0-.498.357-.83.723-.83v-1zM7.91 8.802c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zm1.722-1.83c0-.963-.721-1.83-1.722-1.83v1c.365 0 .722.332.722.83h1zM3.74 4.44c1.443-.787 2.619-1.154 3.763-1.155 1.145 0 2.318.365 3.758 1.154l.48-.876c-1.522-.835-2.865-1.279-4.238-1.278-1.373 0-2.717.445-4.241 1.277l.478.878z" fill="currentColor"></path></svg> </a> </div> </div> </div> <nav class="post-nav flex items-center justify-center -translate-y-[61px] sm:-translate-y-[71px] h-16 sm:h-[74px] transition-transform duration-300 ease-in-out fixed top-0 w-full bg-white dark:bg-zinc-900 !z-[999999] px-4 sm:px-6"> <div class="post-nav-content w-full flex justify-between items-center max-w-screen-lg gap-3"> <div class="post-nav-left flex items-center justify-start gap-3 xs:gap-4"> <h4 class="post-nav-title text-one font-semibold text-base sm:text-lg line-clamp-1">Ember Octane: Airtable Time</h4> </div> <button type="button" class="post-nav-button share-button size-9 min-w-9 max-w-9 sm:px-4 sm:h-9 sm:w-fit sm:max-w-fit flex items-center justify-center gap-2 text-two border border-zinc-200/70 bg-zinc-100 dark:bg-zinc-800 dark:border-zinc-700/70 hover:opacity-80 transition-opacity duration-300 rounded-full"> <svg class="copying" width="17px" height="17px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor"><path d="M19.4 20H9.6C9.26863 20 9 19.7314 9 19.4V9.6C9 9.26863 9.26863 9 9.6 9H19.4C19.7314 9 20 9.26863 20 9.6V19.4C20 19.7314 19.7314 20 19.4 20Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9V4.6C15 4.26863 14.7314 4 14.4 4H4.6C4.26863 4 4 4.26863 4 4.6V14.4C4 14.7314 4.26863 15 4.6 15H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <svg class="copied hidden" width="17px" height="17px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor"><path d="M5 13L9 17L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <span class="post-nav-button-text text-current font-medium text-sm hidden sm:block">Copy Link</span> </button> </div> <div class="scroll-line-content absolute bottom-0 left-0 w-full h-[3px] bg-zinc-200/30 dark:bg-zinc-200/5"> <div class="scroll-line fixed h-[3px] rounded-r-sm bg-ghost-accent bottom-0 before:absolute before:content-[''] before:inset-0 before:size-full before:opacity-30 before:shadow-[0px_0.5px_4px_var(--ghost-accent-color)]"></div> </div> </nav> <style> .scroll-line { transition: 0.5s cubic-bezier(0.075, 0.82, 0.165, 1) width; z-index: 1; } </style> <div class="native-post-content xs:my-16 mx-auto my-12 flex w-full flex-col items-center justify-start gap-2 px-4 sm:my-24 md:max-w-2xl lg:px-0"> <div class="post-heading post-heading-centered xs:gap-6 flex w-full flex-col items-start justify-start gap-4 sm:items-center md:gap-8"> <div class="post-info text-four flex flex-row items-center justify-start gap-3 sm:justify-center"> <span class="post-heading-centered-date text-md text-left font-normal sm:text-center">06 Apr 2020</span> <i class="separator cursor-default text-[11px] !text-zinc-400 dark:!text-zinc-600">•</i> <span class="post-heading-centered-reading-time text-md text-left font-normal sm:text-center">10 min read</span> </div> <h1 class="post-title xxs:text-3xl text-one w-full text-left text-[28px] font-bold !leading-[1.2] sm:text-center sm:text-4xl md:text-5xl">Ember Octane: Airtable Time</h1> <div class="post-authors hidden sm:block"> <div class="c-author-area xs:flex -ml-1 hidden select-none flex-row items-center"> <a href="/author/wycats/" aria-label="Yehuda Katz" class="c-author-area-item flex flex-row rounded-full gap-0 first:order-1 first:mr-1 [&:nth-child(3)]:order-2 [&:nth-child(3)]:z-10 [&:nth-child(3)]:-ml-5 [&:nth-child(3)]:mr-1.5 "> <figure class="c-author-area-image aspect-[1/1] overflow-hidden rounded-full border-white bg-zinc-100 hover:z-20 dark:border-zinc-950 dark:bg-zinc-900 h-10 w-10 border-4 "> <img class="c-author-area-image-img lazyhedwik-image h-full w-full rounded-full object-cover object-center" src="/content/images/size/w90/format/webp/2021/04/Profile---Correct.png" data-src="/content/images/size/w180/format/webp/2021/04/Profile---Correct.png" alt="Yehuda Katz" /> </figure> </a> <a href="/author/wycats/" aria-label="Yehuda Katz" class="c-author-area-button author-1 wycats [&:nth-child(2)]:order-3 [&:nth-child(4)]:order-4 [&:nth-child(4)]:before:inline-block [&:nth-child(4)]:before:content-['&'] [&:nth-child(4)]:before:!no-underline [&:nth-child(4)]:before:hover:text-two [&:nth-child(4)]:before:mr-1.5 [&:nth-child(4)]:ml-1.5 text-base [&:nth-child(4)]:before:opacity-50 text-two hover:underline before:decoration-transparent underline-offset-6 decoration-dotted decoration-transparent hover:text-ghost-accent hover:decoration-ghost-accent font-medium transition-colors duration-300">Yehuda Katz</a> </div> </div> <div class="post-image aspect-[12/7] h-fit w-full select-none overflow-hidden bg-zinc-50 sm:rounded-2xl dark:bg-zinc-900"> </div> </div> <div class="content"> <p>This post is the fourth in a series on building an Ember application HTML-first. In this series, we're going to build the EmberConf schedule application from the ground up.</p><ol><li><a href="https://yehudakatz.com/2020/03/25/ember-octane-lets-go/">Let's Go</a></li><li><a href="https://yehudakatz.com/2020/03/26/ember-octane-components/">Components</a></li><li><a href="https://yehudakatz.com/2020/03/30/ember-octane-a-data-file/">Pulling Out Data</a></li><li>Airtable Time ← <em>This post</em></li><li>Cleaning Things Up</li><li>Adding More Pages</li><li>Polishing: Server-Side Rendering, Prerendering and Code Splitting</li></ol><p>In the last post, we pulled out our schedule data into a JSON file, and used <code>fetch</code> to load the data into our schedule.</p><p>In this post, we'll create an Airtable spreadsheet to store the data, and then load it into our Ember app.</p><p>Let's take a look at an example event in our <code>events.json</code>:</p><!--kg-card-begin: markdown--><pre><code class="language-json">{ &quot;id&quot;: &quot;recvSqXajPl7zLQ1R&quot;, &quot;created_at&quot;: &quot;2016-10-30T21:41:29.000Z&quot;, &quot;updated_keys&quot;: [], &quot;fields&quot;: { &quot;name&quot;: &quot;EmberQuest: Building an Octane Role Playing Game&quot;, &quot;speakers&quot;: [&quot;Dan Monroe&quot;], &quot;description&quot;: &quot;Journey with me as I discuss how Ember Octane made building an RPG easier. There are many challenges to make a playable game; rendering maps, moving the player and monsters, player inventory, combat. Never fear, we have magic on our side! \n\nWe'll use a Glimmering component for the main viewport, mini world map, Path Finding, and Fog of War. The magic of Ember-Concurrency will help drive moving and combat. The wizardry of Ember-Auto-Import will allow the use of Konva to draw on our HTML5 canvas. \n\nTogether, with the magic Octane sword I've named Tracked, we'll level-up and complete our EmberQuest!\n\n&quot;, &quot;day&quot;: &quot;Wednesday&quot;, &quot;end_time&quot;: &quot;11:50am&quot;, &quot;start_time&quot;: &quot;11:25am&quot;, &quot;slides_url&quot;: &quot;https://drive.google.com/file/d/1LNOhvq9PQaQeefh33PtIrnaU5kMRD5yA/view&quot; } } </code></pre> <!--kg-card-end: markdown--><p>Let's zero in on the <code>fields</code> section, which we used in our <code>Event</code> component.</p><ul><li><code>name</code> the title of the talk</li><li><code>speakers</code> a list of speaker names</li><li><code>description</code> a long text description of the talk (we haven't used this yet)</li><li><code>day</code> which day of the week the talk is on</li><li><code>start_time</code> the start time of the talk</li><li><code>end_time</code> the end time of the talk</li><li><code>slides_url</code> an optional link to the slides for the talk</li></ul><h2 id="0-create-an-airtable-account">0. Create an Airtable Account</h2><p>To get started, you'll need to create a new Airtable account.</p><p>The quickest way to get going is to create an account with your Google account, and then select "personal" in the first screen after signup.</p><p>Name your workspace "Events Tutorial" and feel free to skip the video tutorial. You can come back to it later by <a href="https://fast.wistia.net/embed/iframe/lpq4wsb9j9?ref=yehudakatz.com">clicking this link</a>.</p><h2 id="1-set-up-airtable">1. Set Up Airtable</h2><p>Next, you'll create an Airtable spreadsheet, which is called a "base" in Airtable terminology (think "database").</p><p>To make this easier, I've created a CSV file that you can import. Click "Add a Base" and then "Import a spreadsheet".</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image.png" class="kg-image" alt loading="lazy"></figure><p>Then select "Choose a .CSV file", and when the window pops up, click on the link icon on the left of the Window.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-1.png" class="kg-image" alt loading="lazy"></figure><p>Paste this URL into the URL box:</p><p><a href="https://gist.githubusercontent.com/wycats/6525e471ee36fd8532a0058f0ee4dba6/raw/7dbee76a3d28e21822e4a20a2a5e36e846d782c4/event-data.csv?ref=yehudakatz.com">https://gist.githubusercontent.com/wycats/6525e471ee36fd8532a0058f0ee4dba6/raw/7dbee76a3d28e21822e4a20a2a5e36e846d782c4/event-data.csv</a></p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-2.png" class="kg-image" alt loading="lazy"></figure><p>Then click the upload button and name your new base <em>events</em> (lowercase).</p><p>Next, we'll do a little clean up. First, rename the table to <em>events</em>.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-3.png" class="kg-image" alt loading="lazy"></figure><p>Next, we'll want to change the data types of <em>speakers</em>, <em>start_time</em> and <em>end_time</em>. Let's start by changing the data type of the speakers field. It came in as a comma-separated list, and we want to turn it into an Airtable list (called "multiple select" in Airtable).</p><p>Here's how to do that.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-4.png" class="kg-image" alt loading="lazy"></figure><p>Now we're going to repeat the process for <em>start_date</em> and <em>end_date</em>, but this time we're going to choose the date type. Follow the same steps as before, and select the "date" data type. After doing that, make sure you enable the "Include a time field" toggle. A date without a time is not very useful for our schedule after all.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-5.png" class="kg-image" alt loading="lazy"></figure><p>Repeat the process for <em>end_time</em>. Finally, change the <em>description</em> column to "long text".</p><p>And here's what it should look like when you're all done. </p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-6.png" class="kg-image" alt loading="lazy"></figure><p>Pretty cool, no?</p><h2 id="2-airtable-docs">2. Airtable Docs</h2><p>The coolest thing about Airtable is that their API docs are tailored for the table that you created. First, click on the API documentation in the help section.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-46.png" class="kg-image" alt loading="lazy"></figure><p>The API docs that show up aren't just generic docs for all of Airtable. Instead, they're customized for your specific setup.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-47.png" class="kg-image" alt loading="lazy"></figure><h3 id="sidebar-set-up-api-key">Sidebar: Set Up API Key</h3><p>In order to use our Airtable spreadsheet as an API, we will need generate an API key. Click on the "authentication" section on the documentation sidebar and then follow the "account" link.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-11.png" class="kg-image" alt loading="lazy"></figure><p>Next, click the "Generate API key". After a few seconds, Airtable will generate an API key for you. If you want, you can copy the API key from this section, but you can also get it directly from the documentation later.</p><h3 id="onward">Onward</h3><p>After that detour, let's go back to the documentation. Click on "Events Table" and then "List Records", and you'll get documentation for the Events table we created.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-48.png" class="kg-image" alt loading="lazy"></figure><p>The "JavaScript" section describes how to use their Node-based JavaScript library, which is a lot more involved that what we need here. Instead, we'll use the "curl" tab, and use the information in that tab to create the <code>fetch</code> command that we need.</p><h2 id="3-updating-the-route">3. Updating the Route</h2><p>Now that we have Airtable set up, we want to change the data source for our application from our local <code>events.json</code> file to Airtable.</p><p>As a quick reminder, the <em>route</em> in Ember is responsible for getting the <em>model</em> and giving it to the application template.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-7.png" class="kg-image" alt loading="lazy"></figure><p>In the last<strong> </strong>post, our <em>route </em>got its <em>model</em> from the <code>events.json</code> file. Now, we want to change our application's data source to Airtable. We'll get our model from Airtable, rather than an <code>events.json</code> file<em>.</em> </p><p>First, let's remind ourselves what it looked like to fetch the events from the <code>events.json</code> file.</p><!--kg-card-begin: markdown--><pre><code class="language-js">import Route from &quot;@ember/routing/route&quot;; import fetch from &quot;fetch&quot;; export default class ApplicationRoute extends Route { async model() { let response = await fetch(&quot;/events.json&quot;); let data = await response.json(); return data.events; } } </code></pre> <!--kg-card-end: markdown--><p>In its simplest form, the <code>fetch</code> function takes a URL and gives back an response that you can <code>await</code>.</p><p>The Airtable documentation tells us to make a request that looks like this:</p><!--kg-card-begin: markdown--><pre><code>curl &quot;https://api.airtable.com/v0/appOaZeZ2orNCUCRr/events?maxRecords=3&amp;view=Grid%20view&quot; \ -H &quot;Authorization: Bearer YOUR_API_KEY&quot; </code></pre> <!--kg-card-end: markdown--><p>Don't be intimidated by this <code>curl</code> stuff. The first part after the word <code>curl</code> is the URL, and the part after <code>-H</code> is the "headers", which is a bunch of extra information that you can send along with the URL.</p><p>To convert the curl docs to fetch, we're going to use this <a href="https://kigiri.github.io/fetch/?ref=yehudakatz.com">awesome web app</a> that does the work for us. </p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-8.png" class="kg-image" alt loading="lazy"></figure><p>And here's what it looks like to translate the Airtable docs to <code>fetch</code>.</p><!--kg-card-begin: markdown--><pre><code class="language-diff">import Route from &quot;@ember/routing/route&quot;; import fetch from &quot;fetch&quot;; export default class ApplicationRoute extends Route { async model() { - let response = await fetch(&quot;/events.json&quot;); + let response = await fetch( + &quot;https://api.airtable.com/v0/appOaZeZ2orNCUCRr/events?maxRecords=100&amp;view=Grid%20view&quot;, + { + headers: { + Authorization: &quot;Bearer YOUR_API_KEY&quot; + } + } + ); let data = await response.json(); - return data.events; + return data.records; } } </code></pre> <!--kg-card-end: markdown--><p>A couple of things.</p><ul><li>Don't leave "YOUR_API_KEY" in the code. Instead, click on "show API key" in the upper right side of the documentation and the API key will appear in the curl documentation (if you don't see "show API key", go back to "Sidebar: Set Up API Key" and make sure you did that step properly).</li></ul><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/image-9.png" class="kg-image" alt loading="lazy"></figure><ul><li>Airtable defaults to giving you 3 records at a time, which is way too few for us. Change <code>maxRecords=3</code> in the URL to <code>maxRecords=100</code>. The biggest number you can pick is 100. Read Airtable's docs for more information.</li><li><strong>Make sure</strong> to change <code>data.events</code> to <code>data.records</code>. In our original <code>events.json</code>, the top-level key in the JSON was <code>events</code>. The Airtable documentation shows us that the top-level key in their API is <code>records</code>.</li></ul><p>The "example response" in the Airtable docs is very useful. Pay close attention to it if you're trying to understand what you're expecting to get back from the server.</p><p>If we did everything right, and reload the page, we'll get something like this.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-50.png" class="kg-image" alt loading="lazy"></figure><h2 id="4-formatting-timestamps">4. Formatting Timestamps</h2><p>This looks pretty good, except that the times on the left side aren't quite what we wanted. Instead, we got an <a href="https://en.wikipedia.org/wiki/ISO_8601?ref=yehudakatz.com">"ISO 8601"</a> formatted date, which doesn't work at all for human readers.</p><p>We know it's an "ISO 8601" date because the Airtable docs say so in the "fields" section.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-51.png" class="kg-image" alt loading="lazy"></figure><p>First, we need to figure out how to take an ISO 8601 timestamp and turn it into a date. To figure that out I googled <a href="https://www.google.com/search?q=parse+iso+8601+javascript&ref=yehudakatz.com">parse iso 8601 javascript</a>, and the top result is an MDN article for <code>Date.parse</code>, which seems promising.</p><!--kg-card-begin: markdown--><blockquote> <p>The first part of the documentation says that they don't recommend using <code>Date.parse</code>, but later on in the documentation, it says that it's safe to use <code>Date.parse</code> for ISO 8601 timestamps.</p> </blockquote> <!--kg-card-end: markdown--><p>I tried it out in the developer tools with the string I got back from the Airtable API.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-52.png" class="kg-image" alt loading="lazy"></figure><p>This isn't what we need, but it's progress!</p><p>The next question we need to answer is how to convert a timestamp into a useful, human-readable string. These days, the built-in <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl?ref=yehudakatz.com">Intl</a></code> API is the way to convert numbers and dates into human-readable results. For dates, we want to use the appropriately named <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat?ref=yehudakatz.com">Intl.DateTimeFormat</a></code>.</p><p>The way that API works is that you first construct a new formatter.</p><!--kg-card-begin: markdown--><pre><code class="language-js">let formatter = new Intl.DateTimeFormat(&lt;LOCALE&gt;, &lt;OPTIONS&gt;) </code></pre> <!--kg-card-end: markdown--><p>And then you use the formatter to format the date. If we don't pass any options to <code>new Int.DateTimeFormat</code>, you get this:</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-53.png" class="kg-image" alt loading="lazy"></figure><p>What we're looking at is the date represented by the timestamp, formatted according to the rules of our current locale. For me, that's <code>en-US</code>.</p><p>Let's see what happens if we pass in other locales.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-55.png" class="kg-image" alt loading="lazy"></figure><p>Ok, this is cool and all, but we need to "9:30 AM" not "3/17/2020".</p><p>That's where the second parameter to <code>new Intl.DateTimeFormat</code> comes in. It took me a little sleuthing, but check this out.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-56.png" class="kg-image" alt loading="lazy"></figure><p>Boom! We got it!</p><h2 id="5-creating-a-helper">5. Creating a Helper</h2><p>To use it, all we need to do is create a helper that takes in one of these weird timestamps that we got from Airtable and format it using the <code>Intl</code> API.</p><p>Let's generate a helper. We'll call it <code>format-time</code>.</p><!--kg-card-begin: markdown--><pre><code>$ ember g helper format-time </code></pre> <!--kg-card-end: markdown--><p>Just like in the last post, Ember gave us a new helper, this time in <code>app/helpers/format-time.js</code>.</p><!--kg-card-begin: markdown--><pre><code class="language-js">import { helper } from &quot;@ember/component/helper&quot;; export default helper(function formatTime(params /*, hash*/) { return params; }); </code></pre> <!--kg-card-end: markdown--><p>Let's update our <code>Event</code> component to call <code>format-time</code> and throw a debugger into the function.</p><p>First, <code>app/components/event.hbs</code>.</p><!--kg-card-begin: markdown--><pre><code class="language-diff">&lt;li class=&quot;event&quot;&gt; &lt;div class=&quot;time&quot;&gt; - &lt;p&gt;{{@start}}&lt;/p&gt; + &lt;p&gt;{{format-time @start}}&lt;/p&gt; - &lt;p&gt;{{@end}}&lt;/p&gt; + &lt;p&gt;{{format-time @end}}&lt;/p&gt; &lt;/div&gt; &lt;h1&gt;{{@title}}&lt;/h1&gt; &lt;h2&gt; &lt;ul&gt; {{#each @speakers as |speaker|}} &lt;li&gt;{{speaker}}&lt;/li&gt; {{/each}} &lt;/ul&gt; &lt;/h2&gt; &lt;ul class=&quot;images&quot;&gt; {{#each @speakers as |speaker|}} &lt;li&gt; &lt;img src=&quot;https://emberconf.com/assets/images/people/{{anchorize speaker}}.jpg&quot;&gt; &lt;/li&gt; {{/each}} &lt;/ul&gt; &lt;/li&gt; </code></pre> <!--kg-card-end: markdown--><p>And then, <code>app/helpers/format-time.js</code>.</p><!--kg-card-begin: markdown--><pre><code class="language-diff">import { helper } from &quot;@ember/component/helper&quot;; export default helper(function formatTime(params /*, hash*/) { + debugger; return params; }); </code></pre> <!--kg-card-end: markdown--><p>Reload the page with the developer tools open. We'll find ourselves dropped into the console with the browser paused where we placed the debugger.</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/03/image-57.png" class="kg-image" alt loading="lazy"></figure><p>Cool, it worked. We can update the helper with this code.</p><!--kg-card-begin: markdown--><pre><code class="language-diff">import { helper } from &quot;@ember/component/helper&quot;; export default helper(function formatTime(params /*, hash*/) { - debugger; - return params; + let time = Date.parse(params[0]); + let formatter = new Intl.DateTimeFormat(&quot;en-US&quot;, { + hour: &quot;numeric&quot;, + minute: &quot;numeric&quot; + }); + return formatter.format(time); }); </code></pre> <!--kg-card-end: markdown--><p>Reload the page, and voila!</p><figure class="kg-card kg-image-card"><img src="https://yehudakatz.com/content/images/2020/04/screely-1586193564616.png" class="kg-image" alt loading="lazy"></figure><p>In the next post, we'll clean things up a bit and break up the schedule up so that each day is its own section.</p> </div> <div class="c-post-footer post-footer flex flex-col gap-9 items-start justify-start w-full"> <div class="c-post-footer-unless-member lazyhedwik-element relative flex flex-col sm:flex-row items-start sm:items-center py-6 px-7 gap-4 justify-between w-full h-fit bg-transparent after:absolute after:z-[-2] after:inset-0 after:w-full after:h-full after:opacity-[0.06] after:border-[1.6px] after:border-ghost-accent after:rounded-2xl before:absolute before:z-[-1] before:inset-0 before:w-full before:h-full before:bg-ghost-accent before:rounded-2xl before:opacity-[0.036] dark:before:bg-zinc-900 dark:before:opacity-70 dark:after:border-zinc-800 dark:after:opacity-80 rounded-xl select-none"> <div class="c-post-footer-unless-member-content flex w-full flex-col xs:flex-row items-center gap-3 xxs:gap-4 xs:gap-5 justify-center xs:justify-start"> <div class="cta-favicon"> <img src="https://yehudakatz.com/content/images/2024/08/yehuda-square.png" alt="Katz Got Your Tongue" class="aspect-[1/1] min-w-[64px] min-h-[64px] w-16 h-16"> </div> <div class="c-post-footer-texts flex flex-col items-center justify-start xs:items-start xs:justify-start gap-2"> <h5 class="c-post-footer-head text-lg font-bold leading-none text-one text-center xs:text-left line-clamp-1">Enjoy this Post?</h5> <span class="c-post-footer-description text-sm leading-subheading text-three text-center xs:text-left sm:line-clamp-1">Dive into the unlimited – upgrade to premium now!</span> </div> </div> <a href="javascript:" data-portal="signup" class="c-post-footer-button secondary-button w-full sm:max-w-[120px] h-10 min-h-10 max-h-10 font-medium rounded-full">✦ &nbsp;Sign up</a> </div> <hr class="c-post-footer-divider border-t border-zinc-100/[0.8] dark:border-zinc-800/[0.8] w-full"> <div class="related-posts-section lazyhedwik-element flex w-full flex-col items-start justify-start gap-4"> <h3 class="related-posts-head text-three text-[13px] font-semibold uppercase tracking-wider">You might also like</h3> </div></div></div> <footer class="footer footer-primary lazyhedwik-element footer-minimal xs:px-6 xs:py-12 relative border-t !border-zinc-100/[0.7] dark:!border-zinc-900 flex h-fit w-full select-none flex-col items-center justify-center px-4 lg:px-0 py-10 sm:py-14"> <div class="footer-primary-content flex flex-col items-center justify-center gap-6"> <a href="https://yehudakatz.com" class="c-footer-light-logo flex h-fit w-fit items-center footer-logo"> <img class="c-footer-logo-img footer-light-logo !w-fit object-contain min-h-[24px] object-center" style=" height:36px" src="" alt="Katz Got Your Tongue" /> </a> <script> initializeDarkMode(); </script> <span class="footer-primary-description text-two max-w-md text-center text-lg font-medium leading-normal">Long-form writing by Yehuda Katz, co-creator of Ember.js and serial Open Sourcerer.</span> <div class="footer-social-accounts footer-primary-social-accounts flex flex-row gap-5 items-center justify-start sm:justify-end text-three py-2"> <a href="https://twitter.com/@wycats" target="_blank" rel="noopener" aria-label="Twitter X Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.52217 6.77491L15.4785 0H14.0671L8.89516 5.88256L4.76437 0H0L6.24656 8.89547L0 16H1.41155L6.87321 9.78782L11.2356 16H16L9.52183 6.77491H9.52217ZM7.58887 8.97384L6.95596 8.08805L1.92015 1.03974H4.0882L8.15216 6.72795L8.78507 7.61374L14.0677 15.0075H11.8997L7.58887 8.97418V8.97384Z" fill="currentColor"/></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11 3.5h1M4.5.5h6a4 4 0 014 4v6a4 4 0 01-4 4h-6a4 4 0 01-4-4v-6a4 4 0 014-4zm3 10a3 3 0 110-6 3 3 0 010 6z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 6v5m6 0V8.5a2 2 0 10-4 0V11 6M4 4.5h1M1.5.5h12a1 1 0 011 1v12a1 1 0 01-1 1h-12a1 1 0 01-1-1v-12a1 1 0 011-1z" stroke="currentColor"></path></svg> </a> <a href="https://github.com/hedwik" target="_blank" rel="noopener" aria-label="Github Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M5.65 12.477a.5.5 0 10-.3-.954l.3.954zm-3.648-2.96l-.484-.128-.254.968.484.127.254-.968zM9 14.5v.5h1v-.5H9zm.063-4.813l-.054-.497a.5.5 0 00-.299.852l.352-.354zM12.5 5.913h.5V5.91l-.5.002zm-.833-2.007l-.466-.18a.5.5 0 00.112.533l.354-.353zm-.05-2.017l.456-.204a.5.5 0 00-.319-.276l-.137.48zm-2.173.792l-.126.484a.5.5 0 00.398-.064l-.272-.42zm-3.888 0l-.272.42a.5.5 0 00.398.064l-.126-.484zM3.383 1.89l-.137-.48a.5.5 0 00-.32.276l.457.204zm-.05 2.017l.354.353a.5.5 0 00.112-.534l-.466.181zM2.5 5.93H3v-.002l-.5.002zm3.438 3.758l.352.355a.5.5 0 00-.293-.851l-.06.496zM5.5 11H6l-.001-.037L5.5 11zM5 14.5v.5h1v-.5H5zm.35-2.977c-.603.19-.986.169-1.24.085-.251-.083-.444-.25-.629-.49a4.8 4.8 0 01-.27-.402c-.085-.139-.182-.302-.28-.447-.191-.281-.473-.633-.929-.753l-.254.968c.08.02.184.095.355.346.082.122.16.252.258.412.094.152.202.32.327.484.253.33.598.663 1.11.832.51.168 1.116.15 1.852-.081l-.3-.954zm4.65-.585c0-.318-.014-.608-.104-.878-.096-.288-.262-.51-.481-.727l-.705.71c.155.153.208.245.237.333.035.105.053.254.053.562h1zm-.884-.753c.903-.097 1.888-.325 2.647-.982.78-.675 1.237-1.729 1.237-3.29h-1c0 1.359-.39 2.1-.892 2.534-.524.454-1.258.653-2.099.743l.107.995zM13 5.91a3.354 3.354 0 00-.98-2.358l-.707.706c.438.44.685 1.034.687 1.655l1-.003zm-.867-1.824c.15-.384.22-.794.21-1.207l-1 .025a2.12 2.12 0 01-.142.82l.932.362zm.21-1.207a3.119 3.119 0 00-.27-1.195l-.913.408c.115.256.177.532.184.812l1-.025zm-.726-.99c.137-.481.137-.482.136-.482h-.003l-.004-.002a.462.462 0 00-.03-.007 1.261 1.261 0 00-.212-.024 2.172 2.172 0 00-.51.054c-.425.091-1.024.317-1.82.832l.542.84c.719-.464 1.206-.634 1.488-.694a1.2 1.2 0 01.306-.03l-.008-.001a.278.278 0 01-.01-.002l-.006-.002h-.003l-.002-.001c-.001 0-.002 0 .136-.482zm-2.047.307a8.209 8.209 0 00-4.14 0l.252.968a7.209 7.209 0 013.636 0l.252-.968zm-3.743.064c-.797-.514-1.397-.74-1.822-.83a2.17 2.17 0 00-.51-.053 1.259 1.259 0 00-.241.03l-.004.002h-.003l.136.481.137.481h-.001l-.002.001-.003.001a.327.327 0 01-.016.004l-.008.001h.008a1.19 1.19 0 01.298.03c.282.06.769.23 1.488.694l.543-.84zm-2.9-.576a3.12 3.12 0 00-.27 1.195l1 .025a2.09 2.09 0 01.183-.812l-.913-.408zm-.27 1.195c-.01.413.06.823.21 1.207l.932-.362a2.12 2.12 0 01-.143-.82l-1-.025zm.322.673a3.354 3.354 0 00-.726 1.091l.924.38c.118-.285.292-.545.51-.765l-.708-.706zm-.726 1.091A3.354 3.354 0 002 5.93l1-.003c0-.31.06-.616.177-.902l-.924-.38zM2 5.93c0 1.553.458 2.597 1.239 3.268.757.65 1.74.88 2.64.987l.118-.993C5.15 9.09 4.416 8.89 3.89 8.438 3.388 8.007 3 7.276 3 5.928H2zm3.585 3.404c-.5.498-.629 1.09-.584 1.704L6 10.963c-.03-.408.052-.683.291-.921l-.705-.709zM5 11v3.5h1V11H5zm5 3.5V13H9v1.5h1zm0-1.5v-2.063H9V13h1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M.5.5V0a.5.5 0 00-.5.5h.5zm14 0h.5a.5.5 0 00-.5-.5v.5zm0 8l.354.354A.5.5 0 0015 8.5h-.5zm-3 3v.5a.5.5 0 00.354-.146L11.5 11.5zm-5 0V11a.5.5 0 00-.325.12l.325.38zm-3.5 3h-.5a.5.5 0 00.825.38L3 14.5zm0-3h.5A.5.5 0 003 11v.5zm-2.5 0H0a.5.5 0 00.5.5v-.5zM.5 1h14V0H.5v1zM14 .5v8h1v-8h-1zm.146 7.646l-3 3 .708.708 3-3-.708-.708zM11.5 11h-5v1h5v-1zm-5.325.12l-3.5 3 .65.76 3.5-3-.65-.76zM3.5 14.5v-3h-1v3h1zM3 11H.5v1H3v-1zm-2 .5V.5H0v11h1zM10 3v5h1V3h-1zM7 3v5h1V3H7z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M9.5 0v11A3.5 3.5 0 116 7.5m8-2A4.5 4.5 0 019.5 1" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1.61 12.738l-.104.489.105-.489zm11.78 0l.104.489-.105-.489zm0-10.476l.104-.489-.105.489zm-11.78 0l.106.489-.105-.489zM6.5 5.5l.277-.416A.5.5 0 006 5.5h.5zm0 4H6a.5.5 0 00.777.416L6.5 9.5zm3-2l.277.416a.5.5 0 000-.832L9.5 7.5zM0 3.636v7.728h1V3.636H0zm15 7.728V3.636h-1v7.728h1zM1.506 13.227c3.951.847 8.037.847 11.988 0l-.21-.978a27.605 27.605 0 01-11.568 0l-.21.978zM13.494 1.773a28.606 28.606 0 00-11.988 0l.21.978a27.607 27.607 0 0111.568 0l.21-.978zM15 3.636c0-.898-.628-1.675-1.506-1.863l-.21.978c.418.09.716.458.716.885h1zm-1 7.728a.905.905 0 01-.716.885l.21.978A1.905 1.905 0 0015 11.364h-1zm-14 0c0 .898.628 1.675 1.506 1.863l.21-.978A.905.905 0 011 11.364H0zm1-7.728c0-.427.298-.796.716-.885l-.21-.978A1.905 1.905 0 000 3.636h1zM6 5.5v4h1v-4H6zm.777 4.416l3-2-.554-.832-3 2 .554.832zm3-2.832l-3-2-.554.832 3 2 .554-.832z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1c-1.155 0-2.174.412-2.894 1.281-.642.775-1.006 2.35-1.066 3.666l-.073.01-.022.004a8.68 8.68 0 00-.368.059c-.465.089-1.346.326-1.543 1.277-.093.445.011.833.247 1.134.211.269.497.429.708.53.09.041.181.08.274.117-.21.584-.579 1.184-.987 1.728-.382.508-.28 1.153-.083 1.573.197.421.57.402 1.192.43.352.015.722.051 1.09.12.166.03.362.098.606.2.142.06.283.123.423.187.113.052.235.106.374.167.573.25 1.276.517 2.056.517s1.483-.267 2.055-.517c.14-.06.26-.115.375-.167l.025-.012c.135-.06.26-.117.398-.174.243-.103.44-.17.606-.201a7.951 7.951 0 011.09-.12c.622-.028 1.104-.009 1.303-.43.197-.42.298-1.065-.084-1.573-.406-.54-.772-1.136-.983-1.716a5.24 5.24 0 00.305-.127c.216-.098.518-.261.73-.543.245-.326.315-.739.175-1.184-.28-.886-1.092-1.122-1.568-1.216a6.857 6.857 0 00-.355-.058l-.012-.002-.056-.009c-.065-1.234-.41-2.795-1.036-3.581C9.695 1.485 8.682 1 7.5 1z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 13.5l3-7m-3.236 3a2.989 2.989 0 01-.764-2V7A3.5 3.5 0 017 3.5h1A3.5 3.5 0 0111.5 7v.5a3 3 0 01-3 3 2.081 2.081 0 01-1.974-1.423L6.5 9m1 5.5a7 7 0 110-14 7 7 0 010 14z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1.5l.121-.485A.5.5 0 007 1.5h.5zm5.5 8c0 .774-.55 1.641-1.583 2.343C10.4 12.533 8.998 13 7.5 13v1c1.696 0 3.294-.525 4.479-1.33C13.148 11.876 14 10.743 14 9.5h-1zM7.5 13c-1.498 0-2.9-.466-3.917-1.157C2.551 11.14 2 10.273 2 9.5H1c0 1.243.852 2.376 2.021 3.17C4.206 13.475 5.804 14 7.5 14v-1zM2 9.5c0-.774.55-1.641 1.583-2.343C4.6 6.467 6.002 6 7.5 6V5c-1.696 0-3.294.525-4.479 1.33C1.852 7.124 1 8.257 1 9.5h1zM7.5 6c1.498 0 2.9.467 3.917 1.157C12.449 7.86 13 8.727 13 9.5h1c0-1.243-.852-2.376-2.021-3.17C10.794 5.525 9.196 5 7.5 5v1zm2.306 4.54c-.69.29-1.32.46-2.306.46v1c1.136 0 1.898-.204 2.694-.54l-.388-.92zM7.5 11c-.987 0-1.617-.17-2.306-.46l-.388.92c.796.336 1.558.54 2.694.54v-1zM8 5.5v-4H7v4h1zm-.621-3.515l4 1 .242-.97-4-1-.242.97zM3.974 6.841c-.286-.855-1.12-1.297-1.952-1.297v1c.51 0 .886.261 1.004.615l.948-.318zM2.022 5.544A2.022 2.022 0 000 7.566h1a1.02 1.02 0 011.022-1.022v-1zM0 7.566C0 8.589.76 9.424 1.74 9.56l.139-.99A1.016 1.016 0 011 7.565H0zm11.974-.407c.118-.354.493-.615 1.004-.615v-1c-.832 0-1.666.442-1.952 1.297l.948.318zm1.004-.615A1.02 1.02 0 0114 7.566h1a2.022 2.022 0 00-2.022-2.022v1zM14 7.566c0 .511-.38.934-.879 1.004l.139.99A2.016 2.016 0 0015 7.567h-1zM12.5 3a.5.5 0 01-.5-.5h-1A1.5 1.5 0 0012.5 4V3zm.5-.5a.5.5 0 01-.5.5v1A1.5 1.5 0 0014 2.5h-1zm-.5-.5a.5.5 0 01.5.5h1A1.5 1.5 0 0012.5 1v1zm0-1A1.5 1.5 0 0011 2.5h1a.5.5 0 01.5-.5V1zM5 9h1V8H5v1zm4 0h1V8H9v1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11.5 13.5l-.326.379a.5.5 0 00.342.12L11.5 13.5zm-1.066-1.712a.5.5 0 00-.785.62l.785-.62zm.398-.41l-.174-.468a.672.672 0 00-.02.007l.194.461zm-1.738.516L9.01 11.4l-.008.001.092.492zm-3.104-.012l-.095.49.003.001.092-.491zm-1.762-.515l-.182.465.182-.466zm-.875-.408l-.278.415a.46.46 0 00.033.021l.245-.436zm-.108-.06l.277-.416a.491.491 0 00-.054-.031l-.223.447zm-.048-.036l.353-.354a.502.502 0 00-.11-.083l-.243.437zm2.154 1.52a.5.5 0 00-.785-.62l.785.62zM3.5 13.5l-.016.5a.5.5 0 00.347-.125L3.5 13.5zm-3-2.253H0a.5.5 0 00.006.08l.494-.08zm1.726-8.488l-.3-.4a.5.5 0 00-.168.225l.468.175zM5.594 1.5l.498-.047A.5.5 0 005.605 1l-.01.5zm-.378 1.306a.5.5 0 00.996-.095l-.996.095zm3.526-.063a.5.5 0 00.992.127l-.992-.127zM9.406 1.5L9.395 1a.5.5 0 00-.485.436l.496.064zm3.368 1.259l.468-.175a.5.5 0 00-.168-.225l-.3.4zm1.726 8.488l.494.08a.497.497 0 00.006-.08h-.5zM6.481 8.8l-.5-.008V8.8h.5zm5.019 4.7l.326-.379-.002-.002a.794.794 0 01-.044-.038 21.355 21.355 0 01-.536-.48c-.325-.298-.66-.622-.81-.813l-.785.62c.208.264.603.64.918.93a29.109 29.109 0 00.593.53l.01.008.003.002.327-.378zm.436-3.246c-.46.303-.894.513-1.278.656l.348.937a7.352 7.352 0 001.48-.758l-.55-.835zm-1.297.663a7.387 7.387 0 01-1.629.484l.168.986a8.39 8.39 0 001.848-.548l-.387-.922zm-1.637.485a7.895 7.895 0 01-2.92-.012l-.184.983a8.896 8.896 0 003.288.012l-.184-.983zm-2.917-.011a9.57 9.57 0 01-1.675-.49l-.364.931c.512.2 1.13.402 1.849.54l.19-.981zm-1.675-.49a6.536 6.536 0 01-.813-.378l-.489.872c.326.183.648.324.938.437l.364-.931zm-.78-.358a.802.802 0 00-.108-.061c-.02-.01-.011-.007 0 .001l-.555.832a.87.87 0 00.108.061c.021.01.012.007 0-.002l.556-.83zm-.162-.091a.332.332 0 01.082.058l-.707.707c.023.023.081.08.178.13l.447-.895zm-.028-.026a4.697 4.697 0 01-.28-.168l-.011-.008a.025.025 0 00-.001 0l-.287.41-.286.409.001.001.002.002.007.004.021.014.075.049c.064.04.156.096.273.161l.486-.874zm1.126 1.338c-.152.193-.489.525-.813.829a30.38 30.38 0 01-.538.491l-.034.031-.01.008-.001.002h-.001l.331.375.331.375.001-.001.003-.002.01-.009.036-.032a38.039 38.039 0 00.555-.508c.315-.296.708-.677.915-.94l-.785-.62zM3.516 13c-1.166-.037-1.778-.521-2.11-.96a2.394 2.394 0 01-.4-.82 1.1 1.1 0 01-.013-.056v.002l-.493.08c-.494.08-.494.08-.493.081v.006a1.367 1.367 0 00.028.127 3.394 3.394 0 00.573 1.183c.505.667 1.393 1.31 2.876 1.357l.032-1zM1 11.247c0-1.867.42-3.94.847-5.564a35.45 35.45 0 01.776-2.552 16.43 16.43 0 01.067-.186l.004-.01v-.001l-.468-.175-.469-.175v.001l-.001.003-.004.011a9.393 9.393 0 00-.072.2 36.445 36.445 0 00-.8 2.629C.443 7.083 0 9.253 0 11.247h1zm1.526-8.088c.8-.6 1.577-.89 2.15-1.03a4.764 4.764 0 01.86-.128A1.48 1.48 0 015.585 2h-.001l.01-.5.01-.5H5.6a.848.848 0 00-.028 0h-.068a3.973 3.973 0 00-.24.016 5.763 5.763 0 00-.825.141 6.938 6.938 0 00-2.513 1.2l.6.8zm2.57-1.612l.12 1.259.996-.095-.12-1.258-.996.094zM9.734 2.87l.168-1.306-.992-.128-.168 1.307.992.127zM9.406 1.5l.01.5h-.001a.497.497 0 01.049 0c.038.002.1.005.179.013.16.014.394.047.681.117a5.94 5.94 0 012.15 1.029l.6-.8a6.937 6.937 0 00-2.513-1.2 5.76 5.76 0 00-.825-.142A3.98 3.98 0 009.399 1h-.003l.01.5zm3.368 1.259l-.469.174.001.003.004.009.013.037.053.149a35.482 35.482 0 01.777 2.552c.428 1.624.847 3.697.847 5.564h1c0-1.994-.444-4.164-.88-5.819a36.512 36.512 0 00-.8-2.629 15.246 15.246 0 00-.057-.158l-.015-.042-.004-.01-.001-.004-.47.174zm1.726 8.488l-.493-.08v-.003l-.002.008-.01.047c-.012.045-.03.113-.061.197-.062.17-.167.396-.34.624-.332.439-.944.923-2.11.96l.032 1c1.483-.047 2.37-.69 2.876-1.356a3.395 3.395 0 00.573-1.184 2.05 2.05 0 00.026-.118l.002-.01v-.004c0-.001 0-.002-.493-.081zM5.259 6.97c-1.002 0-1.723.867-1.723 1.83h1c0-.498.357-.83.723-.83v-1zM3.536 8.8c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zM6.98 8.81c.016-.978-.728-1.84-1.722-1.84v1.001c.372 0 .73.338.722.822l1 .017zm2.653-1.84c-1.002.001-1.723.868-1.723 1.831h1c0-.498.357-.83.723-.83v-1zM7.91 8.802c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zm1.722-1.83c0-.963-.721-1.83-1.722-1.83v1c.365 0 .722.332.722.83h1zM3.74 4.44c1.443-.787 2.619-1.154 3.763-1.155 1.145 0 2.318.365 3.758 1.154l.48-.876c-1.522-.835-2.865-1.279-4.238-1.278-1.373 0-2.717.445-4.241 1.277l.478.878z" fill="currentColor"></path></svg> </a> </div> <span class="footer-primary-site-title text-two text-base font-normal">2024 &nbsp; <span class="text-sm opacity-70">•</span> &nbsp; <a href="https://yehudakatz.com" class="font-bold">Katz Got Your Tongue</a></span> </div> </footer> <script src="https://yehudakatz.com/assets/js/exclude/cards.js?v=b917a20540" defer></script> <script src="https://yehudakatz.com/assets/js/exclude/fslightbox.min.js?v=b917a20540" defer></script> <script src="https://yehudakatz.com/assets/js/lib/lazyhedwik.js?v=b917a20540"></script> <script src="https://yehudakatz.com/assets/built/main.min.js?v=b917a20540"></script> <script> function initPopover(){let e=document.getElementById("popoverButton"),s=document.getElementById("hedwikPopover");window.addEventListener("scroll",()=>{s.classList.contains("popover-show")&&(s.classList.remove("popover-show"),s.classList.add("popover-hide"),setTimeout(()=>{s.style.display="none"},150))}),e.addEventListener("click",()=>{s.classList.contains("popover-show")?(s.classList.remove("popover-show"),s.classList.add("popover-hide"),setTimeout(()=>{s.style.display="none"},150)):(s.style.display="block",s.classList.remove("popover-hide"),s.classList.add("popover-show"),s.style.left=`${e.offsetLeft+(e.offsetWidth-s.offsetWidth)/2}px`,s.style.top=`${e.offsetTop+e.offsetHeight+10}px`)}),document.addEventListener("click",o=>{e.contains(o.target)||s.contains(o.target)||(s.classList.remove("popover-show"),s.classList.add("popover-hide"),setTimeout(()=>{s.style.display="none"},150))})}initPopover(); </script> <script> const scrollLine = document.querySelector('.scroll-line'); function fillScrollLine() { const windowHeight = window.innerHeight; const fullHeight = document.body.clientHeight; const scrolled = window.scrollY; const percentScrolled = (scrolled / (fullHeight - windowHeight)) * 100; scrollLine.style.width = `${percentScrolled}%`; } window.addEventListener('scroll', debounce(fillScrollLine)); function debounce(func, wait = 15, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } function copyPageLink(){let e=window.location.href,t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t);let o=document.querySelector(".share-button"),n=document.querySelector(".copying"),c=document.querySelector(".copied"),d=o.querySelector("span");n.classList.add("hidden"),c.classList.remove("hidden"),d.textContent="Copied",setTimeout(()=>{n.classList.remove("hidden"),c.classList.add("hidden"),d.textContent="Copy Link"},2e3)}const shareButton=document.querySelector(".share-button");shareButton.addEventListener("click",copyPageLink); </script> <script defer src="https://ghostboard.io/t/5cc01a81a803662c9f6bab1d.js" type="text/javascript" async></script><noscript><img src="https://ghostboard.io/api/noscript/5cc01a81a803662c9f6bab1d/pixel.gif" alt="" border="0" /></noscript> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script> <script defer> var tabs = new Tabby('[data-tabs]'); </script> </body> </html>

Pages: 1 2 3 4 5 6 7 8 9 10