<|start_filename|>src/@swizec/gatsby-theme-course-platform/components/FormCK/formsQuery.js<|end_filename|>
export const formsQuery = `
  query {
    site {
      siteMetadata {
        convertkit {
          defaultFormId
        }
      }
    }
  }
`


<|start_filename|>src/@swizec/gatsby-theme-course-platform/components/headerLogo.js<|end_filename|>
import React from "react"
import { NavLink } from "theme-ui"

const HeaderLogo = ({ siteTitle, logo }) => {
  return (
    <NavLink
      variant="nav"
      href="/"
      sx={{
        width: "100%",
        maxWidth: 220,
        alignItems: "center",
        pl: [0, 2, 2],
      }}
    >
      <p>{siteTitle}</p>
    </NavLink>
  )
}

export default HeaderLogo


<|start_filename|>src/components/homepage.js<|end_filename|>
import React from "react"
import { useAuth } from "react-use-auth"
import { Heading, Flex, Box, Text, Button } from "theme-ui"
import { GumroadButton, TinyFormCK } from "@swizec/gatsby-theme-course-platform"
import { StaticImage } from "gatsby-plugin-image"

export const ChapterHeading = ({ sx }) => {
  const { isAuthorized } = useAuth()

  if (isAuthorized(["ServerlessHandbook"])) {
    return <Heading sx={sx}>Chapters</Heading>
  } else {
    return <Heading sx={sx}>Chapter Previews</Heading>
  }
}

export const NavGrid = (props) => (
  <Box
    {...props}
    sx={{
      fontFamily: "heading",
      ul: {
        listStyle: "none",
        p: 0,
        display: "grid",
        gridGap: 3,
        gridTemplateRows: [`repeat(9, 1fr)`, `repeat(5, 1fr)`],
        gridTemplateColumns: ["repeat(2, 1fr)", "repeat(3, 1fr)"],
        gridAutoFlow: ["dense", "column"],
        counterReset: "nav-grid",
      },
      li: {
        fontWeight: "bold",
        fontSize: [1, 2, 2],
        counterIncrement: "nav-grid",
        mb: 0,
        "::before": {
          content: "counter(nav-grid)",
          display: "inline-block",
          pr: 1,
        },
      },
      a: {
        color: "inherit",
        textDecoration: "none",
        transition: "color .2s ease-out",
        ":hover,:focus": {
          color: "primary",
        },
      },
    }}
  />
)

const CoverImage = () => {
  return (
    <a href="https://geni.us/serverless-handbook">
      <StaticImage
        src="../images/cover.png"
        alt="Serverless Handbook cover"
        loading="eager"
        objectFit="cover"
        objectPosition="50% 50%"
      />
    </a>
  )
}

export const HomeTitle = () => (
  <Flex sx={{ flexWrap: "wrap" }}>
    <Box
      sx={{
        p: 3,
        minWidth: 250,
        flex: 1,
        textAlign: "center",
        margin: "auto auto",
      }}
    >
      <Heading sx={{ fontSize: 6 }}>
        Serverless Handbook
        <Text sx={{ fontSize: 4, display: "block" }}>
          for frontend engineers
        </Text>
      </Heading>
      <Text>Dive into modern backend. Understand any backend</Text>

      <Box sx={{ my: 3 }}>
        <Flex
          sx={{
            alignItems: "center",
            justifyContent: "center",
            mb: 1,
            flexDirection: ["column", "row"],
          }}
        >
          <Button
            variant="buy-shiny"
            as="a"
            sx={{ mr: [0, 2], mb: [1, 0] }}
            href="https://geni.us/serverless-handbook"
          >
            Buy now on Amazon
          </Button>
          <a
            className="gumroad-button"
            href="https://gum.co/NsUlA"
            data-gumroad-single-product="true"
            target="_blank"
            rel="noopener noreferrer"
          >
            Buy Digital for $15
          </a>
        </Flex>
        <Text sx={{ fontSize: 0, display: "block" }}>
          #1 new release in Web Development
        </Text>
      </Box>

      <Box
        sx={{
          p: 3,
          minWidth: 250,
          flex: 1,
          textAlign: "center",
          display: ["block", "none"],
        }}
      >
        <CoverImage />
      </Box>

      <Box sx={{ mt: 3 }}>
        <TinyFormCK copyBefore="" submitText="Send it to me! 💌">
          <Heading sx={{ fontSize: 4, pt: 2 }}>Get your free chapter!</Heading>
          <p>
            Wanna see what’s in Serverless Handbook, but not ready to buy the
            full book? Start with a free chapter.
          </p>
        </TinyFormCK>
      </Box>
    </Box>

    <Box
      sx={{
        p: 3,
        minWidth: 250,
        flex: 1,
        textAlign: "center",
        display: ["none", "block"],
      }}
    >
      <CoverImage />
    </Box>
  </Flex>
)


<|start_filename|>src/components/ClaimForm.js<|end_filename|>
import React from "react"
import { useEmailForm } from "@swizec/gatsby-theme-course-platform/src/components/FormCK"
import BouncingLoader from "@swizec/gatsby-theme-course-platform/src/components/BouncingLoader"
import { Flex, Label, Input, Box, Button, Text, Heading, Image } from "theme-ui"
import emailRobot from "@swizec/gatsby-theme-course-platform/src/images/email-robot.gif"
import { useLocalStorage } from "./useLocalStorage"

async function createUser({ name, email }) {
  await fetch(
    "https://tq43ps6oh2.execute-api.us-east-1.amazonaws.com/dev/gumroadPing",
    {
      method: "POST",
      mode: "no-cors",
      body: new URLSearchParams({
        product_permalink: "https://gum.co/NsUlA",
        email,
        full_name: name,
      }).toString(),
    }
  )
}

export const ClaimForm = () => {
  const [unlockHandbook, setUnlockHandbook] = useLocalStorage("unlock_handbook")
  const [saleId, setSaleId] = useLocalStorage("sale_id")

  const {
    formSuccess,
    onSubmit,
    uniqueId,
    register,
    formState,
    submitError,
  } = useEmailForm("claim", async (formData) => {
    await createUser(formData)
    setUnlockHandbook(true)
    setSaleId("came-from-a-claim")
    window.location.href = "/thanks"
  })

  if (formSuccess) {
    return (
      <Flex
        sx={{
          justifyContent: "center",
          flexDirection: "column",
        }}
      >
        <Heading>Thank you ❤️</Heading>
        <p>
          My robots have sent email that tells you how to finish setting up your
          account. Redirecting you to the first chapter with a temporary unlock.
        </p>
        <p>Finish setting up your account and you can login from any device.</p>
        <Image src={emailRobot} />
      </Flex>
    )
  }

  console.log(formState)

  return (
    <Flex
      as="form"
      onSubmit={onSubmit}
      sx={{
        justifyContent: "center",
        flexDirection: "column",
        "& .address": { display: "none" },
      }}
    >
      <Label htmlFor={`${uniqueId}-name`}>Your Name</Label>
      <Input
        id={`${uniqueId}-name`}
        type="text"
        name="name"
        {...register("name", {
          required: "⚠️ Name is required",
          message: "⚠️ Name is required",
        })}
        placeholder="Your name"
      />
      {formState.errors.name && <span>{formState.errors.name.message}</span>}

      <Label htmlFor={`${uniqueId}-email`} sx={{ mt: 2 }}>
        Your Email
      </Label>
      <Input
        id={`${uniqueId}-email`}
        type="email"
        name="email"
        {...register("email", {
          required: "⚠️ E-mail is required",
          pattern: {
            value:
              "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$",
            message: "⚠️ Invalid email address",
          },
        })}
        placeholder="Your email address"
      />
      {formState.errors.email && <span>{formState.errors.email.message}</span>}

      <Label htmlFor={`${uniqueId}-sharelink`} sx={{ mt: 2 }}>
        Share link
      </Label>
      <Input
        id={`${uniqueId}-sharelink`}
        type="text"
        name="share_link"
        {...register("share_link", {
          required: "⚠️ Share link is required",
          message: "⚠️ Share link is required",
        })}
        placeholder="Where did you share? I'd love to see your post"
      />
      {formState.errors.share_link && (
        <span>{formState.errors.share_link.message}</span>
      )}

      <Box className="address">
        <Label htmlFor={`${uniqueId}-address`} className="required">
          Your Address
        </Label>
        <input
          className="required"
          autoComplete="nope"
          type="text"
          id={`${uniqueId}-address`}
          name="address"
          {...register}
          placeholder="Your address here"
        />
      </Box>
      <Button
        type="submit"
        variant="buy-shiny"
        disabled={formState.isSubmitting}
        sx={{ mt: 2 }}
      >
        {formState.isSubmitting ? <BouncingLoader /> : "Claim my copy"}
      </Button>

      {submitError && <p dangerouslySetInnerHTML={{ __html: submitError }}></p>}

      <Text sx={{ fontSize: "0.8em", mb: 2, textAlign: "center" }}>
        I like privacy too. No spam, no selling your data.
      </Text>
    </Flex>
  )
}


<|start_filename|>src/components/TestCloudFunctions.js<|end_filename|>
import React, { useState } from "react"
import { Box, Button, Heading, Input, Label } from "theme-ui"
import prettier from "prettier/standalone"
import parserBabel from "prettier/parser-babel"

export const TestCloudFunction = ({
  serviceName,
  urlPlaceholder,
  jsonPlaceholder,
  defaults = false,
}) => {
  const [url, setUrl] = useState(defaults ? urlPlaceholder : "")
  const [payload, setPayload] = useState(defaults ? jsonPlaceholder : "")
  const [result, setResult] = useState("")
  const [error, setError] = useState(null)

  async function runTest() {
    setError(null)
    setResult(null)

    if (!url) {
      setError(`Paste the URL you got from ${serviceName}`)
    } else {
      const corsUrl = `https://cors.bridged.cc/${url}`

      try {
        let res
        if (payload) {
          res = await fetch(corsUrl, {
            method: "POST",
            body: payload,
            headers: {
              "Content-Type": "application/json",
            },
          })
        } else {
          res = await fetch(corsUrl)
        }

        setResult(await res.text())
      } catch (e) {
        setError(e.message)
      }
    }
  }

  return (
    <Box bg="muted" p={[2, 3, 3]}>
      <Heading as="h3" mb={[1, 2, 2]}>
        Try your function
      </Heading>
      <p>Leave payload empty for GET requests, valid JSON for POST.</p>
      <Label>Target URL:</Label>
      <Input
        value={url}
        onChange={(ev) => setUrl(ev.target.value)}
        placeholder={urlPlaceholder}
      />
      <Label>JSON payload:</Label>
      <Input
        value={payload}
        onChange={(ev) => setPayload(ev.target.value)}
        placeholder={jsonPlaceholder}
      />
      <Button
        onClick={runTest}
        sx={{ mt: [1, 2, 2], mb: [2, 3, 3], cursor: "pointer" }}
      >
        Run {payload ? "POST" : "GET"} request
      </Button>
      {result ? (
        <>
          <Heading as="h4">Result:</Heading>
          <pre>
            {prettier.format(result, {
              parser: "json",
              plugins: [parserBabel],
            })}
          </pre>
        </>
      ) : null}
      {error ? (
        <>
          <Heading color="red" as="h4">
            Error:
          </Heading>
          <pre style={{ color: "red" }}>{error}</pre>
        </>
      ) : null}
    </Box>
  )
}


<|start_filename|>src/@swizec/gatsby-theme-course-platform/components/FormCK/useFormsQuery.js<|end_filename|>
import { useStaticQuery, graphql } from "gatsby"

export const useFormsQuery = () => {
  // change this query when you add forms
  const { site } = useStaticQuery(graphql`
    query {
      site {
        siteMetadata {
          convertkit {
            defaultFormId
            claimFormId
          }
        }
      }
    }
  `)

  return site.siteMetadata.convertkit
}


<|start_filename|>src/@swizec/gatsby-theme-course-platform/components/layout.js<|end_filename|>
import React, { useState, useRef } from "react"
import { Box, Flex } from "theme-ui"
import { Sidenav, Pagination } from "@theme-ui/sidenav"
import {
  Footer,
  Head,
  Header,
  Reactions,
} from "@swizec/gatsby-theme-course-platform"
import Nav from "./nav"
import { Paywall, SnipContent, usePaywall } from "../../../components/Paywall"

const Sidebar = (props) => {
  const { unlocked: contentUnlocked } = usePaywall(props.location.pathname)

  return (
    <Flex
      sx={{
        pt: 64,
      }}
    >
      <Box
        as={Sidenav}
        ref={props.nav}
        open={props.open}
        onClick={(e) => {
          props.setMenu(false)
        }}
        onBlur={(e) => {
          props.setMenu(false)
        }}
        onFocus={(e) => {
          props.setMenu(true)
        }}
        sx={{
          width: [256, 256, 320],
          flex: "none",
          px: 3,
          mt: [64, 0],
          ul: {
            p: 0,
            m: 0,
          },
          "ul > li": {
            mb: 0,
          },
          "ul > li > a": {
            p: "8px",
          },
          maxHeight: "100vh !important",
          overflowY: "scroll",
          height: "100%",
        }}
      >
        <Nav />
      </Box>
      <Box
        sx={{
          width: "100%",
          minWidth: 0,
          maxWidth: 768,
          minHeight: "calc(100vh - 64px)",
          mx: "auto",
          px: [3, 4],
          pb: 5,
        }}
      >
        {contentUnlocked ? (
          <main id="content">{props.children}</main>
        ) : (
          <main id="content">
            <SnipContent>{props.children}</SnipContent>
          </main>
        )}
        {contentUnlocked ? <Reactions page={props.href} /> : null}
        <Paywall pagePath={props.location.pathname} />
        <Nav
          pathname={props.location.pathname}
          components={{
            wrapper: Pagination,
          }}
        />
      </Box>
    </Flex>
  )
}

const Layout = (props) => {
  const fullwidth = props.location.pathname === "/"
  const [menu, setMenu] = useState(false)
  const nav = useRef(null)

  if (props.pageContext.frontmatter) {
    props = {
      ...props,
      ...props.pageContext.frontmatter,
    }
  }

  return (
    <Box
      sx={{
        variant: "styles.root",
      }}
    >
      <Header
        siteTitle="Serverless Handbook"
        courseFirstLesson={props.courseFirstLesson}
        showMenu={false}
        fullwidth={fullwidth}
        menu={menu}
        setMenu={setMenu}
        nav={nav}
      />
      {fullwidth ? (
        <>
          <Head {...props} />
          <main id="content">{props.children}</main>
        </>
      ) : (
        <Sidebar {...props} nav={nav} open={menu} setMenu={setMenu}>
          <Head {...props} />
          {props.children}
        </Sidebar>
      )}
      <Footer />
    </Box>
  )
}

export default Layout


<|start_filename|>src/gatsby-plugin-theme-ui/index.js<|end_filename|>
import { future } from "@theme-ui/presets"
import merge from "lodash.merge"
import { toTheme } from "@theme-ui/typography"
import typography from "typography-theme-stow-lake"
import amazonBuy from "../images/buy-now-amazon.png"
import "typeface-neuton"
import "typeface-lato"

import { courseTheme } from "@swizec/gatsby-theme-course-platform"

const theme = merge(
  future,
  {
    buttons: {
      buy: {
        cursor: "pointer",
        fontWeight: "heading",
        background: `url(${amazonBuy})`,
        backgroundSize: "fit",
        backgroundPosition: "center center",
        backgroundRepeat: "no-repeat",
        textIndent: "-1000%",
        width: 240,
      },
      "buy-shiny": {
        cursor: "pointer",
        fontWeight: "heading",
        bg: "highlight",
        borderRadius: 35,
        "&:hover": { bg: "secondary" },
      },
    },
  },
  toTheme(typography),
  courseTheme
)

export default theme


<|start_filename|>src/components/Paywall.js<|end_filename|>
import React, { useRef, useEffect } from "react"
import ReactDOMServer from "react-dom/server"
import { useAuth } from "react-use-auth"
import { Box } from "theme-ui"
import { wrapRootElement } from "gatsby-plugin-theme-ui/gatsby-browser"

import { default as PaywallCopy } from "../components/paywall-copy"
import QuickThanks from "./quickthanks"
import { useLocalStorage } from "./useLocalStorage"

function toggleLockedContent(show) {
  if (typeof window !== "undefined") {
    let children = document.getElementById("content").children

    let isLocked = false
    for (let child of children) {
      if (child.id === "lock") isLocked = true
      if (isLocked === true) {
        child.style.display = show ? "block" : "none"
      }
    }
  }
}

function hidePaywall(paywallDiv) {
  if (paywallDiv.current) {
    paywallDiv.current.style = `display: none`
  }

  const overlays =
    typeof window !== "undefined" &&
    document.querySelectorAll(".fadeout-overlay")

  if (overlays) {
    for (let overlay of overlays) {
      overlay.style = "display: none"
    }
  }

  // show content
  toggleLockedContent(true)
}

function showPaywall(paywallDiv) {
  // hide content
  toggleLockedContent(false)

  const overlay = typeof window !== "undefined" && document.createElement("div")
  const main =
    typeof window !== "undefined" && document.querySelector("main#content")

  const style = `
            background-image: linear-gradient(rgba(255, 255, 255, 0) 80%,  rgb(255, 255, 255, 1) 100%);
            width: 100%;
            top: 0px;
            bottom: 0px;
            position: absolute;
          `
  overlay.style = style
  overlay.className = "fadeout-overlay"

  main.style = "position: relative;"
  main.appendChild(overlay)

  const dimensions = main.getBoundingClientRect()

  if (paywallDiv.current) {
    paywallDiv.current.style = `
            top: ${Math.round(dimensions.height * 0.1)}px;
            width: ${Math.round(dimensions.width)}px;
            background-color: var(--theme-ui-colors-muted,#f6f6ff);
          `
  }
}

const LOCKED_PAGES = [
  "/getting-started",
  "/serverless-pros-cons",
  "/serverless-flavors",
  "/serverless-dx",
  "/serverless-architecture-principles",
  "/serverless-elements",
  "/robust-backend-design",
  "/databases",
  "/serverless-rest-api",
  "/serverless-graphql",
  "/lambda-pipelines",
  "/serverless-monitoring",
  "/dev-qa-prod",
  "/serverless-performance",
  "/serverless-chrome-puppeteer",
  "/handling-secrets",
  "/serverless-authentication",
  "/glossary",
  "/appendix-more-databases",
  "/downloads",
]

export function usePaywall(pagePath) {
  const paywallDiv = useRef(null)
  const { isAuthorized } = useAuth()
  const [unlockHandbook] = useLocalStorage("unlock_handbook")
  const [saleId] = useLocalStorage("sale_id")
  const [unlockedPages, setUnlockedPages] = useLocalStorage(
    "unlocked_pages",
    []
  )
  const hasLock = LOCKED_PAGES.includes(pagePath.replace(/\/$/, ""))

  const unlocked =
    !hasLock ||
    (pagePath &&
      unlockedPages.filter((p) => p !== "/downloads").includes(pagePath)) ||
    isAuthorized(["ServerlessHandbook"]) ||
    (unlockHandbook && saleId)

  useEffect(() => {
    // doesn't run during on-page navigation for some reason
    if (typeof window !== "undefined") {
      window.requestAnimationFrame(() => {
        if (unlocked) {
          hidePaywall(paywallDiv)
        } else {
          showPaywall(paywallDiv)
        }
      })
    }
  }, [unlocked])

  function unlockCurrentPage() {
    setUnlockedPages((unlockedPages) => [...unlockedPages, pagePath])
  }

  return { unlocked, paywallDiv, unlockCurrentPage, SnipContent }
}

export function SnipContent({ children }) {
  const html = ReactDOMServer.renderToString(children).split(
    '<div id="lock"></div>'
  )[0]

  //   return wrapRootElement({
  //     element: children,
  //   })

  //   return wrapRootElement({
  //     element: <div dangerouslySetInnerHTML={{ __html: html }} />,
  //   })

  return <div dangerouslySetInnerHTML={{ __html: html }} />

  //   return (
  //     <WrapRootElement
  //       element={<div dangerouslySetInnerHTML={{ __html: html }} />}
  //     />
  //   )
}

export const Paywall = ({ pagePath }) => {
  const { unlocked, paywallDiv, unlockCurrentPage } = usePaywall(pagePath)

  if (unlocked) {
    return <QuickThanks />
  } else {
    return (
      <Box id="paywall-copy" ref={paywallDiv}>
        <PaywallCopy unlockCurrentPage={unlockCurrentPage} />
      </Box>
    )
  }
}
